DEV Community

Reading the Fine Print: License Basics for Open-Source Tools on Windows

I've spent more evenings than I care to admit untangling license questions for tools I wanted to ship in a Windows build pipeline. Most of the time the answer is boring - MIT or Apache-2.0, copy the notice, move on - but the boring answer only comes after you actually look.

The interesting cases are the ones where a tool you assumed was "just open source" turns out to be source-available, dual-licensed, or carrying a clause about how you may redistribute binaries. This article is the checklist I now run through by habit, using ransome1 sleek 2 0 29 - a small to-do and note-taking app distributed on GitHub - as a concrete example I can point at while describing the general workflow.

Start with the file, not the README badge

The first thing I do after downloading any Windows tool is look for the license text itself. READMEs lie by omission; badges get stale; GitHub's sidebar license detection is a heuristic and it fails on custom files. The actual LICENSE, LICENSE.md, or COPYING file in the repository root is the only thing that matters legally.

For a tool like sleek, the practical path is: grab the release, extract it somewhere disposable, and inspect what shipped alongside the executable. Windows distributions are messy - some projects bundle a LICENSE.txt in the zip, some only reference the repo, some embed nothing and expect you to visit the project page. PowerShell is the right tool for this because it can both list archives and search text.

# Download a release asset and inspect what license material actually ships with it
$url = "https://github.com/ransome1/sleek/releases/download/v2.0.29/sleek-2.0.29-win.zip"
$dest = "$env:TEMP\sleek-2.0.29-win.zip"
Invoke-WebRequest -Uri $url -OutFile $dest
Get-FileHash $dest -Algorithm SHA256 | Format-List
# List contents without extracting
Add-Type -AssemblyName System.IO.Compression.FileSystem
$zip = [System.IO.Compression.ZipFile]::OpenRead($dest)
$zip.Entries | Where-Object { $_.Name -match 'LICEN|COPY|NOTICE|README' } | Select-Object FullName, Length
$zip.Dispose()

The Get-FileHash step is not paranoia for its own sake. If a project publishes checksums next to the release, verifying them is the cheapest way to confirm you are looking at the artifact the maintainer signed off on, which matters when the license question later turns into "am I allowed to redistribute this exact binary."

The URL above follows the GitHub release convention (/releases/download/<tag>/<asset>); adjust the tag and asset name to whatever the project's releases page actually lists. If nothing license-shaped comes back from that filter, don't conclude the tool is unlicensed. It usually means the license lives only in the repository root, and you should fetch it directly:

# Fall back to the repository's license file when the archive ships none
$repo = "ransome1/sleek"
$branch = "main" # try "master" if this 404s
$candidates = @("LICENSE", "LICENSE.md", "LICENSE.txt", "COPYING")
foreach ($name in $candidates) {
  $uri = "https://raw.githubusercontent.com/$repo/$branch/$name"
  try {
    $resp = Invoke-WebRequest -Uri $uri -Method Head -ErrorAction Stop
    Write-Host "$name -> $($resp.StatusCode)"
  } catch {
    # 404 here is expected; keep looking
  }
}

(One note on that snippet: strip the stray token in the Write-Host line before running it - I pasted from a scratch buffer and it doesn't belong. The -Method Head approach is the point; it avoids downloading every candidate file just to see which one exists.)

Once you have the text, read it in this order: the grant of rights, the conditions, the warranty disclaimer, then anything about trademarks. The grant tells you what you may do. The conditions tell you what you must carry along when you do itηš„δΈšεŠ‘. The disclaimer is boilerplate you can skim. The trademark clause is the one people forget - a permissive license on the code does not give you the right to use the project's name in your product.

The four buckets, and how to tell them apart fast

I keep a mental sorting hat with four buckets:

  • Permissive (MIT, BSD-2/3, Apache-2.0, ISC). You can use, modify, and redistribute, including in closed-source products, provided you keep the copyright notice and license text. Apache-2.0 adds an explicit patent grant and a NOTICE file convention; MIT and BSD are shorter and say less about patents. For a desktop utility you are bundling into an internal installer, this distinction rarely bites, but it's worth knowing which one you picked.
  • Weak copyleft (LGPL, MPL-2.0). File-level or library-level reciprocity. MPL-2.0 is the friendliest of these for Windows tooling: you can link against the code, and if you modify the MPL-covered files themselves you must publish those modifications. LGPL gets complicated the moment you statically link, which on Windows is the default for a lot of C/C++ dependencies.
  • Strong copyleft (GPL-2.0, GPL-3.0, AGPL). Distribution of a binary triggers the obligation to offer corresponding source under the same terms. AGPL extends this to network use, which is why it appears in server-side tools and almost never in desktop ones.
  • Source-available / custom (BUSL, SSPL, Elastic License, "free for personal use" text). These are not open source by the OSI definition, whatever the README says. Read them line by line, because the restrictions are specific: field-of-use limits, time-delayed conversion to a real open-source license, caps on commercial redistribution.

For sleek specifically, the license is a permissive one - a short MIT-style text. That means the practical obligations are: keep the copyright notice and license text with any copy you distribute, and don't imply the author endorses your fork. If you build it from source and hand the binary to colleagues, include the LICENSE file in the folder. That's it.

If you were hoping to sell a rebranded build on an app store, the permissive license technically permits it, but the trademark clause (if present) and basic decency argue against shipping it under the same name with the same icon.

Checking licenses across a whole dependency tree

The single-tool case is easy. The case that eats your afternoon is a build that pulls in twenty packages, three of which have licenses you've never heard of drummed up by a legal team in 2019. On Windows, with a mixed toolchain, my approach is to generate attachment-level metadata for everything I compile and everything I copy into the output directory, then diff it against a whitelist.

MSBuild and NuGet give you a hook for this. The GenerateDepsFile and GetPackageDirectory targets expose resolved package paths, but the simplest reliable method is to read the assets file that restore already wrote:

# Summarize licenses for every NuGet package restored into a project
param([Parameter(Mandatory)][string]$ProjectDir)
$assets = Join-Path $ProjectDir "obj\project.assets.json"
if (-not (Test-Path $assets)) { throw "Run 'dotnet restore' first: $assets missing" }
$json = Get-Content $assets -Raw | ConvertFrom-Json
$rows = foreach ($target in $json.targets.PSObject.Properties) {
  foreach ($lib in $target.Value.PSObject.Properties) {
    $name = $lib.Name
    if ($name -notmatch '/') { continue } # skip project refs
    $pkg = $name.Split('/')[0]
    $meta = Join-Path $env:USERPROFILE ".nuget\packages\$($pkg.ToLower())"
    $nuspec = Get-ChildItem $meta -Filter *.nuspec -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1
    if ($nuspec) {
      [xml]$x = Get-Content $nuspec.FullName -Raw
      [pscustomobject]@{
        Package = $pkg
        Version = $name.Split('/')[1]
        License = $x.package.metadata.license.'#text'
      }
    }
  }
}
$rows | Sort-Object Package -Unique | Format-Table -AutoSize

That gives you a table you can paste into a review ticket. It is not exhaustive - packages that only declare a license URL, or that embed a LICENSE file without a license element in the nuspec, will come back blank - but blanks are exactly the rows worth a second look.

For native dependencies and vendored third-party code, the same idea applies with a Get-ChildItem -Recurse -Include LICENSE*,COPYING* sweep over the source tree. The workflow I actually follow is: generate the table, sort by license name, and for each unique license decide once whether it is acceptable for the product I'm building. If it is, record the decision and move on. If it isn't, the package comes out. The mistake is deciding per-package, because you end up re-litigating the same MIT text four hundred times.

Failure modes I've hit, and the edge cases behind them

The "no license file" trap. A repository with no LICENSE file is not public domain. Default copyright applies, and default copyright reserves everything - you have no right to redistribute it at all. I've seen internal tools built on top of a GitHub project that was exactly this: source visible, no license, and everyone assumed "free." The fix is either a polite issue asking the maintainer to add a license, or replacing the dependency. There is no third option.

The dual-license fork in the road. Some projects ship under MIT OR Apache-2.0, which means you pick one and comply with it. That's genuinely permissive and you should just pick the one your organization's policy already understands. The dangerous version is "AGPL for the community, commercial for enterprise" - that's not a fork in the road, it's a toll booth, and the AGPL side has network-use obligations that surprise people who only read the first paragraph.

The relicensing tarball. A tool that was MIT three years ago and is BUSL today. The license that applies to the version you downloaded is the one that shipped with that version, and it doesn't change retroactively. But - and this is the part people miss - if you later upgrade to a newer release to get a bugfix, you inherit the new terms. Pin your dependency, archive the license text alongside the binary, and note the version in your SBOM. If you're on sleek 2.0.29, the license text from that tag is what governs that copy; keep it.

The missing notice on redistribution. MIT and BSD require you to reproduce the copyright notice and license text in any copy you distribute. That includes the "internal only" zip you drop on a file share for the QA team. It takes ten seconds to drop LICENSE next to the .exe in the archive, and I have watched a two-day release hold-up happen because nobody did.

The trademark afterthought. The code license and the name are separate rights. A permissive license lets you fork; it usually does not let you call the fork by the original name or ship the original logo. If you're distributing a modified build inside your company, rename it or at least don't put it in the store under the upstream identity.

The edge case that trips up most Windows teams is static linking into a C/C++ helper: the LGPL's "you may link" language assumes dynamic linking and a mechanism for the user to relink. A statically linked DLL with no replacement path is, in practice, treated as a derivative work by many counsel. If you're in that situation, the cheapest fix is usually to switch the dependency to a permissively licensed alternative rather than draft a legal memo about it.

Related resource

If you want to practice the inspection workflow on a real, small Windows artifact, the GitVaults catalog page for ransome1 sleek 2 0 29 is a reasonable starting point - download the release, run the hash and archive-listing snippets above against it, and read the bundled license text end to end. Doing that once on a tool you understand well is the fastest way to build the muscle memory for the tools you don't.

The same routine works on any catalog entry: verify the artifact, find the license file, classify it into one of the four buckets, and record the decision where your build system can see it.

Caveat

I'm a practitioner, not a lawyer, and nothing here is legal advice - license interpretation depends on jurisdiction, how the software is linked, and what your organization's counsel has already decided about similar cases. If you spot an error in the snippets or a nuance I've flattened, corrections are welcome; the goal is a checklist that holds up when someone else runs it on their machine.

Read on DEV Community ↗ ← Back to News

Comments

No comments yet. Start the discussion.