chore(licensing): LGPL FFmpeg swap, third-party notices, attribution hygiene
The MIT OR Apache-2.0 SOURCE license is clean (audit found no copied copyleft); the
gaps were all binary-distribution (Layer-2). This makes the shipped artifacts honest:
- Windows host + client: bundled FFmpeg BtbN gpl-shared -> lgpl-shared (AMF/QSV/decode
unaffected; the GPL-only x264/x265 were never used), and ship the FFmpeg LGPL notice
+ license text in the installer + MSIX (licenses/).
- THIRD-PARTY-NOTICES.txt generated + bundled into installer/MSIX/deb/rpm. Offline
generator (scripts/gen-third-party-notices.{py,sh}) + cargo-about config (about.toml/
.hbs) with a permissive-only accepted-license allow-list as a copyleft regression gate.
- Reword the win32u GPU-preference hook comments to reflect independent reimplementation
(no Apollo/Sunshine GPL-3.0 source copied).
- README dual-license + inbound=outbound contributor clause + non-affiliation trademark
disclaimer; new CONTRIBUTING.md.
- LICENSE files into the standalone driver + vk-layer workspaces; deb copyright holder
aligned to "unom and the punktfunk contributors".
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -102,22 +102,35 @@ if (Test-Path $rustup) {
|
||||
& $rustup target add aarch64-pc-windows-msvc
|
||||
} else { Write-Warning "rustup not found - install rustup then re-run (needed for the aarch64 target)." }
|
||||
|
||||
$ffArm = "C:\Users\Public\ffmpeg-arm64"
|
||||
if (-not (Test-Path (Join-Path $ffArm 'lib\avcodec.lib'))) {
|
||||
# BtbN winarm64 shared, FFmpeg 7.x (avcodec-61) to match the x64 tree's ABI. MSVC-linkable .lib
|
||||
# import libs + headers + bin\*.dll — exactly what ffmpeg-sys-next + pack-msix.ps1 consume.
|
||||
Write-Host "==> fetching ARM64 FFmpeg (BtbN winarm64 shared)"
|
||||
$ffUrl = 'https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-n7.1-latest-winarm64-gpl-shared-7.1.zip'
|
||||
$ffZip = "C:\Users\Public\ffmpeg-arm64.zip"
|
||||
$ffTmp = "C:\Users\Public\ffmpeg-arm64-extract"
|
||||
Invoke-WebRequest -Uri $ffUrl -OutFile $ffZip -UseBasicParsing
|
||||
if (Test-Path $ffTmp) { Remove-Item -Recurse -Force $ffTmp }
|
||||
Expand-Archive -Path $ffZip -DestinationPath $ffTmp -Force # BtbN zips have one top-level folder
|
||||
$inner = Get-ChildItem $ffTmp -Directory | Select-Object -First 1
|
||||
if (Test-Path $ffArm) { Remove-Item -Recurse -Force $ffArm }
|
||||
Move-Item -Path $inner.FullName -Destination $ffArm
|
||||
Remove-Item -Force $ffZip; Remove-Item -Recurse -Force $ffTmp -ErrorAction SilentlyContinue
|
||||
# FFmpeg shared trees for the host (amf-qsv encode) + clients (decode). We use BtbN **lgpl-shared**
|
||||
# builds: the AMD/Intel AMF + Intel QSV encoders, swscale, and the HEVC decoder are all present in the
|
||||
# LGPL build, and punktfunk never calls the GPL-only encoders (x264/x265 — software encode is the
|
||||
# separate BSD-2 openh264 crate; NVENC is the direct NVIDIA SDK). lgpl-shared keeps the bundled DLLs
|
||||
# LGPL-2.1+ (dynamic linking satisfies the relink duty) rather than GPL, so the shipped installer/MSIX
|
||||
# stay consistent with punktfunk's MIT OR Apache-2.0 posture.
|
||||
# MIGRATION: a runner previously provisioned with the old *gpl-shared* trees must be re-provisioned —
|
||||
# delete C:\Users\Public\ffmpeg and C:\Users\Public\ffmpeg-arm64, then re-run this script.
|
||||
function Get-BtbnFfmpeg {
|
||||
param([string]$Dir, [string]$ZipTag) # ZipTag: 'win64' (x64) or 'winarm64' (ARM64 cross tree)
|
||||
if (Test-Path (Join-Path $Dir 'lib\avcodec.lib')) { return }
|
||||
# FFmpeg 7.x (avcodec-61); MSVC-linkable .lib import libs + headers + bin\*.dll — exactly what
|
||||
# ffmpeg-sys-next + pack-host-installer.ps1 + pack-msix.ps1 consume. The extracted top-level folder
|
||||
# also carries FFmpeg's own LICENSE/COPYING text, preserved in $Dir for the packagers to bundle.
|
||||
Write-Host "==> fetching FFmpeg ($ZipTag, BtbN lgpl-shared)"
|
||||
$url = "https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-n7.1-latest-$ZipTag-lgpl-shared-7.1.zip"
|
||||
$zip = "$Dir.zip"; $tmp = "$Dir-extract"
|
||||
Invoke-WebRequest -Uri $url -OutFile $zip -UseBasicParsing
|
||||
if (Test-Path $tmp) { Remove-Item -Recurse -Force $tmp }
|
||||
Expand-Archive -Path $zip -DestinationPath $tmp -Force # BtbN zips have one top-level folder
|
||||
$inner = Get-ChildItem $tmp -Directory | Select-Object -First 1
|
||||
if (Test-Path $Dir) { Remove-Item -Recurse -Force $Dir }
|
||||
Move-Item -Path $inner.FullName -Destination $Dir
|
||||
Remove-Item -Force $zip; Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue
|
||||
}
|
||||
# x64 host+client tree (the workflow's default FFMPEG_DIR = C:\Users\Public\ffmpeg) and the ARM64 cross
|
||||
# tree (the aarch64 leg points FFMPEG_DIR at C:\Users\Public\ffmpeg-arm64).
|
||||
Get-BtbnFfmpeg -Dir "C:\Users\Public\ffmpeg" -ZipTag 'win64'
|
||||
Get-BtbnFfmpeg -Dir "C:\Users\Public\ffmpeg-arm64" -ZipTag 'winarm64'
|
||||
|
||||
# Inno Setup (ISCC.exe) for the host installer build (windows-host.yml). pack-host-installer.ps1
|
||||
# locates it at its fixed Program Files path, so it need not be on PATH — just present.
|
||||
|
||||
Executable
+134
@@ -0,0 +1,134 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate THIRD-PARTY-NOTICES.txt for the Rust workspace.
|
||||
|
||||
Offline, dependency-free attribution generator. It reads `cargo metadata`, then for every
|
||||
third-party crate (everything that is NOT a first-party workspace member) it pulls the crate's
|
||||
*actual* LICENSE/COPYING/NOTICE text out of the local cargo registry cache (or the in-tree
|
||||
vendored source for path deps), deduplicates identical license texts, and emits a single
|
||||
notices file: a per-crate manifest followed by the verbatim license texts.
|
||||
|
||||
This satisfies the binary-distribution attribution duty for the permissive (MIT/BSD/ISC/Zlib/
|
||||
Apache/Unicode/etc.) crates linked into shipped punktfunk artifacts. `cargo about` (see
|
||||
about.toml) produces an equivalent, network-augmented result in CI; this is the dependency-free
|
||||
fallback that also runs locally and is committed as a baseline.
|
||||
|
||||
Usage: python3 scripts/gen-third-party-notices.py [--out THIRD-PARTY-NOTICES.txt]
|
||||
"""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
LICENSE_GLOBS = ("license", "licence", "copying", "notice", "unlicense", "copyright")
|
||||
|
||||
|
||||
def find_license_files(pkg_dir):
|
||||
out = []
|
||||
try:
|
||||
names = sorted(os.listdir(pkg_dir))
|
||||
except OSError:
|
||||
return out
|
||||
for n in names:
|
||||
low = n.lower()
|
||||
if any(low == g or low.startswith(g + ".") or low.startswith(g + "-") or g in low for g in LICENSE_GLOBS):
|
||||
p = os.path.join(pkg_dir, n)
|
||||
if os.path.isfile(p):
|
||||
try:
|
||||
with open(p, "r", encoding="utf-8", errors="replace") as f:
|
||||
txt = f.read().strip()
|
||||
if txt:
|
||||
out.append((n, txt))
|
||||
except OSError:
|
||||
pass
|
||||
return out
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--out", default="THIRD-PARTY-NOTICES.txt")
|
||||
ap.add_argument("--manifest", default="Cargo.toml")
|
||||
args = ap.parse_args()
|
||||
|
||||
meta = json.loads(subprocess.check_output(
|
||||
["cargo", "metadata", "--format-version", "1", "--offline", "--manifest-path", args.manifest],
|
||||
text=True))
|
||||
ws_members = set(meta.get("workspace_members", []))
|
||||
|
||||
pkgs = []
|
||||
for p in meta["packages"]:
|
||||
if p["id"] in ws_members:
|
||||
continue # first-party (covered by the root LICENSE-MIT / LICENSE-APACHE)
|
||||
pkgs.append(p)
|
||||
pkgs.sort(key=lambda p: (p["name"].lower(), p["version"]))
|
||||
|
||||
# Group license texts: text-hash -> {text, name, crates[]}
|
||||
texts = {}
|
||||
no_text = []
|
||||
for p in pkgs:
|
||||
pkg_dir = os.path.dirname(p["manifest_path"])
|
||||
files = find_license_files(pkg_dir)
|
||||
label = f'{p["name"]} {p["version"]}'
|
||||
if not files:
|
||||
no_text.append(p)
|
||||
continue
|
||||
for fname, txt in files:
|
||||
h = hashlib.sha256(txt.encode("utf-8", "replace")).hexdigest()
|
||||
ent = texts.setdefault(h, {"text": txt, "filename": fname, "crates": set()})
|
||||
ent["crates"].add(label)
|
||||
|
||||
lines = []
|
||||
w = lines.append
|
||||
w("THIRD-PARTY SOFTWARE NOTICES")
|
||||
w("=" * 76)
|
||||
w("")
|
||||
w("punktfunk (https://git.unom.io/unom/punktfunk) is licensed under MIT OR Apache-2.0.")
|
||||
w("The binaries it ships statically/dynamically link the third-party Rust crates listed")
|
||||
w("below. Each is distributed under its own permissive license; the full license texts")
|
||||
w("follow the manifest. This file is generated by scripts/gen-third-party-notices.py")
|
||||
w("(or `cargo about`, see about.toml) — do not edit by hand.")
|
||||
w("")
|
||||
w(f"Total third-party crates: {len(pkgs)}")
|
||||
w("")
|
||||
w("-" * 76)
|
||||
w("MANIFEST (crate version — SPDX license — source)")
|
||||
w("-" * 76)
|
||||
for p in pkgs:
|
||||
lic = p.get("license") or (("file: " + p["license_file"]) if p.get("license_file") else "UNKNOWN")
|
||||
repo = p.get("repository") or ""
|
||||
w(f' {p["name"]} {p["version"]} — {lic}' + (f' — {repo}' if repo else ""))
|
||||
w("")
|
||||
|
||||
if no_text:
|
||||
w("-" * 76)
|
||||
w("Crates whose package did not embed a license file (SPDX + source only)")
|
||||
w("-" * 76)
|
||||
for p in no_text:
|
||||
lic = p.get("license") or "UNKNOWN"
|
||||
repo = p.get("repository") or ""
|
||||
w(f' {p["name"]} {p["version"]} — {lic}' + (f' — {repo}' if repo else ""))
|
||||
w("")
|
||||
|
||||
w("=" * 76)
|
||||
w("FULL LICENSE TEXTS (deduplicated)")
|
||||
w("=" * 76)
|
||||
# Stable order: by first crate name covered.
|
||||
for h, ent in sorted(texts.items(), key=lambda kv: sorted(kv[1]["crates"])[0].lower()):
|
||||
crates = ", ".join(sorted(ent["crates"]))
|
||||
w("")
|
||||
w("-" * 76)
|
||||
w(f"The following license ({ent['filename']}) applies to: {crates}")
|
||||
w("-" * 76)
|
||||
w(ent["text"])
|
||||
w("")
|
||||
|
||||
text = "\n".join(lines) + "\n"
|
||||
with open(args.out, "w", encoding="utf-8") as f:
|
||||
f.write(text)
|
||||
print(f"wrote {args.out}: {len(pkgs)} crates, {len(texts)} distinct license texts, "
|
||||
f"{len(no_text)} without embedded text", file=sys.stderr)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+21
@@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env bash
|
||||
# Regenerate THIRD-PARTY-NOTICES.txt for the Rust workspace.
|
||||
#
|
||||
# Prefers `cargo about` (full, network-augmented license harvest; see about.toml) and falls back to
|
||||
# the dependency-free offline generator (scripts/gen-third-party-notices.py, reads the cargo registry
|
||||
# cache). Run this when the dependency tree changes; CI also runs it before packaging.
|
||||
#
|
||||
# Usage: scripts/gen-third-party-notices.sh [output-file]
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
OUT="${1:-THIRD-PARTY-NOTICES.txt}"
|
||||
|
||||
if command -v cargo-about >/dev/null 2>&1; then
|
||||
echo "==> cargo about generate -> $OUT" >&2
|
||||
cargo about generate about.hbs --output-file "$OUT"
|
||||
else
|
||||
echo "==> cargo-about not installed; using offline fallback" >&2
|
||||
echo " (install the full generator with: cargo install cargo-about)" >&2
|
||||
python3 scripts/gen-third-party-notices.py --out "$OUT"
|
||||
fi
|
||||
echo "==> wrote $OUT" >&2
|
||||
Reference in New Issue
Block a user