diff --git a/README.md b/README.md index a6127c1..9eb0a8b 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,10 @@ Pick a license variant, then reference the platform you build for. | `GPLv2` | GPL-2.0 | | `GPLv3` | GPL-3.0 | +Every platform package carries upstream's `THIRD-PARTY-NOTICES.txt` at its root: the effective +licence, FFmpeg's build and source details, and every bundled library's licence text. NuGet does +not copy it into your build or publish output; distributing it with your app is up to you. + ``` DevEnvy.FFmpeg.Binaries path helper, no binaries DevEnvy.FFmpeg.Binaries..Runtime. one platform's binaries diff --git a/docs/superpowers/specs/2026-09-10-bundle-upstream-binaries-design.md b/docs/superpowers/specs/2026-09-10-bundle-upstream-binaries-design.md index f5c3789..b5ec0c6 100644 --- a/docs/superpowers/specs/2026-09-10-bundle-upstream-binaries-design.md +++ b/docs/superpowers/specs/2026-09-10-bundle-upstream-binaries-design.md @@ -6,8 +6,8 @@ ## Revisions during implementation -The sections below describe the approved design. Eight things changed while building it, each -because something was measured or reviewed rather than assumed. Where a section further down +The sections below describe the approved design. Nine things changed while building it or +since, each because something was measured, reviewed or changed upstream rather than assumed. Where a section further down contradicts this list, this list is current. 0. **Final package shape (supersedes items 1, 2 and 8 below).** One package per @@ -37,8 +37,9 @@ contradicts this list, this list is current. loaded resolver builds versioned Unix names (`libavcodec.so.61`) and cannot load Android's unversioned `.so` files or iOS frameworks, so depending on it would advertise mobile bindings that do not work. The helper is now pure path resolution; consumers add a binding themselves. -4. **`legal/` is packed at the package root, never under `runtimes/**/native/`.** Anything below - `native/` is a native asset NuGet copies into every consumer's build and publish output. +4. **The licence notices are packed at the package root, never under `runtimes/**/native/`.** + Anything below `native/` is a native asset NuGet copies into every consumer's build and + publish output. (Originally `legal/`; see item 9 for the single file that replaced it.) 5. **`release.yml` is not tag-triggered.** The original design had it triggered by a tag *and* creating that tag, which cannot work. It now runs on a merge to `main` or by hand, and is idempotent: a series is released when its computed version has no `v*` tag yet. @@ -51,6 +52,13 @@ contradicts this list, this list is current. the bit and `read` does not, so shared libraries were never affected. 8. **Package count is 57, not 53** — the four `.Rid` metas, plus the helper, 4 metas, 44 runtime and 4 Apple packages. +9. **`legal/` became one `THIRD-PARTY-NOTICES.txt`.** Upstream moved its archives to the + `ffmpeg-build` engine, which drops the `legal/` directory and instead puts a single + `THIRD-PARTY-NOTICES.txt` at every archive's root: the effective licence, FFmpeg's build and + source details, and every bundled library's licence text. The iOS bundle carries one too, at + the root beside the `.xcframework`s, covering all three slices. Every platform and Apple + package ships that file at its package root; a pack whose archive lacks it fails rather than + shipping binaries without their licence texts. One review finding was investigated and **not** acted on. Codex flagged that `linux-musl-x64` falls back to `linux-x64` in the RID graph, so the all-platform meta can have NuGet select glibc @@ -133,6 +141,7 @@ never downloads. All twelve carry a `legal/` directory holding the cell's license notice plus every bundled dependency's license. It is shipped in every package — it is the LGPL compliance payload. +(Superseded by `THIRD-PARTY-NOTICES.txt` at the archive root; see revision 9.) Since upstream release `9.0.1.5`, MoltenVK is statically linked into the iOS libraries, so every iOS cell now contains exactly the same six xcframeworks. The staging code still @@ -399,7 +408,7 @@ Every download is verified against the release's `SHA256SUMS` before extraction. - no symlinked or duplicate libraries survived staging (the size regression this guards against is roughly 3x) - no `include/` directory in any package -- `legal/` present in every platform package +- `THIRD-PARTY-NOTICES.txt` at the root of every platform package, and nowhere else - `libc++_shared.so` present in both Android packages - every runtime package's payload sits under `runtimes/{rid}/native/` - each meta's dependency groups match the TFM table above diff --git a/scripts/audit-upstream.py b/scripts/audit-upstream.py index f4400e2..33a8871 100644 --- a/scripts/audit-upstream.py +++ b/scripts/audit-upstream.py @@ -199,13 +199,13 @@ def audit(root: Path, name: str): def add(kind, detail, path=''): f['issues'].append({'kind': kind, 'detail': detail, 'file': path}) - entries = [p for p in root.rglob('*') if not p.is_dir() and 'legal' not in p.parts] + entries = [p for p in root.rglob('*') if not p.is_dir() and p.name != 'THIRD-PARTY-NOTICES.txt'] real = [p for p in entries if not p.is_symlink()] links = [p for p in entries if p.is_symlink()] f['files'] = len(entries) f['real'] = len(real) f['symlinks'] = len(links) - f['has_legal'] = (root / 'legal').is_dir() or any('legal' in p.parts for p in root.rglob('*')) + f['has_notices'] = (root / 'THIRD-PARTY-NOTICES.txt').is_file() # Byte-identical real files: upstream symlinks these on Linux, so any platform # that ships them as copies is inconsistent and inflates the download. diff --git a/scripts/pack.sh b/scripts/pack.sh index d9394dc..1eaeac2 100755 --- a/scripts/pack.sh +++ b/scripts/pack.sh @@ -46,6 +46,7 @@ DOWNLOAD_DIR="${REPO_ROOT}/.downloads/${UPSTREAM_TAG}" STAGING_DIR="${REPO_ROOT}/staging" NUSPEC_DIR="${REPO_ROOT}/.nuspec-gen" STUB_CSPROJ="${REPO_ROOT}/src/Packaging/Packaging.csproj" +NOTICES="THIRD-PARTY-NOTICES.txt" # at the root of every upstream archive # Upstream artifact name -> .NET RID. Upstream calls 32-bit ARM "armhf"; .NET calls it linux-arm. declare -A RID_FOR_ARTIFACT=( @@ -266,15 +267,19 @@ if [[ "${PHASE}" == "all" || "${PHASE}" == "base" ]]; then *) normalize_desktop "${work}" ;; esac - # legal/ is packed at the package root, never under runtimes/{rid}/native/. - # Anything beneath native/ is a native asset to NuGet, so license text left - # there is copied into every consumer's build and publish output, and the - # per-dependency notices collide across packages on a flattened publish. - if [[ -d "${work}/legal" ]]; then - mkdir -p "${stage}/legal" - mv "${work}/legal"/* "${stage}/legal/" - rmdir "${work}/legal" + # Upstream puts one THIRD-PARTY-NOTICES.txt at the archive root: the + # effective licence, FFmpeg's build and source details, and every bundled + # library's licence text. It is packed at the package root, never under + # runtimes/{rid}/native/. Anything beneath native/ is a native asset to + # NuGet, so a notices file left there is copied into every consumer's + # build and publish output and collides across packages on a flattened + # publish. A missing file fails the pack: shipping binaries without their + # licence texts is not a smaller package, it is a non-compliant one. + if [[ ! -f "${work}/${NOTICES}" ]]; then + echo "ERROR: ${tarball} has no ${NOTICES}" >&2 + exit 1 fi + mv "${work}/${NOTICES}" "${stage}/${NOTICES}" echo " staged ${PACKAGE_CELL[${cell}]}/${rid}" done @@ -310,6 +315,10 @@ if [[ "${PHASE}" == "all" || "${PHASE}" == "base" ]]; then unpacked="${STAGING_DIR}/.apple-${PACKAGE_CELL[${cell}]}" rm -rf "${unpacked}"; mkdir -p "${unpacked}" tar -xzf "${DOWNLOAD_DIR}/${tarball}" -C "${unpacked}" + if [[ ! -f "${unpacked}/${NOTICES}" ]]; then + echo "ERROR: ${tarball} has no ${NOTICES}" >&2 + exit 1 + fi for rid in "${!APPLE_SLICE[@]}"; do slice="${APPLE_SLICE[${rid}]}" @@ -328,7 +337,9 @@ if [[ "${PHASE}" == "all" || "${PHASE}" == "base" ]]; then exit 1 fi - [[ -d "${unpacked}/legal" ]] && cp -R "${unpacked}/legal" "${stage}/" + # The bundle's one notices file covers all three slices, so every + # Apple package carries it whole. + cp "${unpacked}/${NOTICES}" "${stage}/${NOTICES}" echo " staged ${PACKAGE_CELL[${cell}]}/${rid} (${found} frameworks from the ${slice} slice)" done rm -rf "${unpacked}" diff --git a/scripts/verify-packages.py b/scripts/verify-packages.py index a1e06cb..5964539 100644 --- a/scripts/verify-packages.py +++ b/scripts/verify-packages.py @@ -21,6 +21,10 @@ _spec.loader.exec_module(_audit) _pe, _SYSTEM_DLL = _audit.pe, _audit.SYSTEM_DLL +# Upstream's single notices file: the effective licence, FFmpeg's build and +# source details, and every bundled library's licence text. +NOTICES = "THIRD-PARTY-NOTICES.txt" + directory = pathlib.Path(sys.argv[1]) packages = sorted(directory.glob("*.nupkg")) if not packages: @@ -127,15 +131,18 @@ def doubled(path): check(not bad, f"{name}: doubled path segments (PackagePath/%(RecursiveDir) bug): {bad[:2]}") # Anything under runtimes/**/native is a native asset NuGet copies into the - # consumer's output. Licence text belongs at the package root instead. - check(not any("native/" in n and "/legal/" in n for n in names), - f"{name}: legal/ is nested under native/ and would be copied into consumer output") + # consumer's output, and frameworks/ is linked into the app. Licence text + # belongs at the package root instead, once. + misplaced = [n for n in names + if n.rsplit("/", 1)[-1] == NOTICES and n != NOTICES] + check(not misplaced, + f"{name}: {NOTICES} outside the package root would be copied into consumer output: {misplaced[:2]}") # These are file containers. A stray compiled assembly means IncludeBuildOutput leaked. if payload: check(not any(n.startswith("lib/") and n.endswith(".dll") for n in names), f"{name}: contains a lib/*.dll — IncludeBuildOutput leaked into a native package") - check(any(n.startswith("legal/") for n in names), f"{name}: ships no legal/ notices") + check(NOTICES in names, f"{name}: ships no {NOTICES} at the package root") check(bool(payload), f"{name}: has no native payload at all") if payload and "frameworks/" not in "".join(payload): diff --git a/src/Packaging/Apple.csproj b/src/Packaging/Apple.csproj index 3571764..243939e 100644 --- a/src/Packaging/Apple.csproj +++ b/src/Packaging/Apple.csproj @@ -38,7 +38,8 @@ directory portion would drop the bundle name from %(RecursiveDir) and flatten all six frameworks on top of each other. --> - + + diff --git a/src/Packaging/Runtime.csproj b/src/Packaging/Runtime.csproj index 253a8d2..19eee72 100644 --- a/src/Packaging/Runtime.csproj +++ b/src/Packaging/Runtime.csproj @@ -39,7 +39,7 @@ - +