ci / bun-nix (pull_request) Successful in 29s
ci / docs-drift (pull_request) Failing after 30s
ci / docs-site (pull_request) Successful in 57s
ci / web (pull_request) Successful in 1m8s
apple / swift (pull_request) Successful in 2m18s
apple / distribute (pull_request) Skipped
apple / screenshots (pull_request) Skipped
ci / rust-arm64 (pull_request) Successful in 2m36s
android / android (pull_request) Failing after 3m30s
ci / rust (pull_request) Canceled after 5m15s
windows-client / client (arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (pull_request) Canceled after 5m20s
windows-client / client (x64, , x86_64-pc-windows-msvc, C:\t) (pull_request) Canceled after 0s
"All text looks boring and ugly" — the dial's discs carried short words because the console has no icon font. It still has none: the icons are Lucide v0.462.0 (ISC), each one carried as its 24×24 SVG path data in `icons.rs` — every shape of the source SVG folded into one path string by scripts/gen-lucide-icons.py (a leading relative `m` is absolutised, since concatenation would re-anchor it) — parsed by Skia's own SVG-path parser and stroked at Lucide's native width 2 with round caps, scaled to whatever box the caller gives. A designed, consistent set, rendered exactly as drawn, for a page of string constants: no font ships, no dependency lands, and every mark takes the console's paints. On the dial: End stream, Leave, Touch mode, Keyboard, Stats, Mic (and its struck twin while muted), Virtual controller, Send text, the three host powers (moon, cycle, power) and the centre's ellipsis. A shortcut keeps its stacked keycap chord — its content IS text — and an unknown host action falls back to its label, so nothing the set cannot name goes blank. An empty slot in the editor shows a faint plus; in-stream it stays plain glass. The hint bar's gamepad glyphs joined the same language, because they were three dialects: solid floating triangles beside badged letters, two ink alphas, three container treatments, and a keycap hairline that never scaled. Now the directional marks are the set's chevrons (a direction is not a button, so they stay badge-free), the remote's back arrow is the set's return mark inside its badge, every container wears the same scaled hairline, and the PlayStation shapes grew to match the letters' weight, round-joined. Licensing: LUCIDE-LICENSE sits in the crate and the notices generator lists it as vendored third-party source; THIRD-PARTY-NOTICES.txt is regenerated, which also catches the file up with this branch's real lockfile (nine crates left, chacha20 moved — the committed baseline was stale). Tests: every icon parses and stays inside its 24-unit box; every built-in slot maps to an icon while shortcuts and unknown host actions stay text.
273 lines
12 KiB
Python
Executable File
273 lines
12 KiB
Python
Executable File
#!/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.
|
|
|
|
By default it covers the WHOLE workspace, which is what the root file must be (the host and
|
|
the desktop clients ship out of it). `--packages <name>[,<name>…]` restricts it to the transitive
|
|
dependency closure of the named workspace members instead — the Apple and Android clients link
|
|
exactly one Rust crate each (`punktfunk-core`, and the JNI bridge over it), so a workspace-wide
|
|
copy attributed them things they do not contain: FFmpeg, the NVENC SDK, GTK, windows-rs. Listing a
|
|
dependency that is not there is not a licence violation, but it is a false statement in a file
|
|
whose entire job is to be true.
|
|
|
|
Usage: python3 scripts/gen-third-party-notices.py [--out THIRD-PARTY-NOTICES.txt]
|
|
[--packages punktfunk-core,…]
|
|
"""
|
|
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
|
|
|
|
|
|
# Crates that ship license-mit/license-apache-2.0 files but omit the `license` field in
|
|
# their Cargo.toml (publish = false upstream), so `cargo metadata` reports no SPDX for them.
|
|
LICENSE_OVERRIDES = {
|
|
"windows-reactor-setup": "MIT OR Apache-2.0",
|
|
}
|
|
|
|
|
|
# Third-party source trees VENDORED inside first-party workspace crates — the
|
|
# workspace-member skip in main() hides them from `cargo metadata`, so they are listed
|
|
# here explicitly: (label, license file relative to the repo root, source URL).
|
|
VENDORED_TREES = [
|
|
("Lucide 0.462.0 (icon path data, crates/pf-console-ui)",
|
|
"crates/pf-console-ui/LUCIDE-LICENSE",
|
|
"https://lucide.dev"),
|
|
("pyrowave (vendored, crates/pyrowave-sys)",
|
|
"crates/pyrowave-sys/vendor/pyrowave/LICENSE",
|
|
"https://github.com/Themaister/pyrowave"),
|
|
("Granite subset (vendored, crates/pyrowave-sys)",
|
|
"crates/pyrowave-sys/vendor/pyrowave/Granite/LICENSE",
|
|
"https://github.com/Themaister/Granite"),
|
|
("volk (vendored, crates/pyrowave-sys)",
|
|
"crates/pyrowave-sys/vendor/pyrowave/Granite/third_party/volk/LICENSE.md",
|
|
"https://github.com/zeux/volk"),
|
|
("Vulkan-Headers (vendored, crates/pyrowave-sys)",
|
|
"crates/pyrowave-sys/vendor/pyrowave/Granite/third_party/khronos/vulkan-headers/LICENSE.md",
|
|
"https://github.com/KhronosGroup/Vulkan-Headers"),
|
|
# OS brand marks for the host-card OS icon (assets/os-icons/, CC BY 4.0 / CC0 /
|
|
# Apache-2.0 — see assets/os-icons/README.md; clients embed per-platform derivatives).
|
|
("Font Awesome Free brand icons (vendored, assets/os-icons)",
|
|
"assets/os-icons/LICENSES/font-awesome-brands.txt",
|
|
"https://fontawesome.com"),
|
|
("Simple Icons (vendored, assets/os-icons)",
|
|
"assets/os-icons/LICENSES/simple-icons.txt",
|
|
"https://simpleicons.org"),
|
|
("Bazzite logo (vendored, assets/os-icons)",
|
|
"assets/os-icons/LICENSES/bazzite.txt",
|
|
"https://github.com/ublue-os/bazzite"),
|
|
# Launcher brand marks for the library's launcher tiles (assets/launcher-icons/, CC BY 4.0 /
|
|
# CC0 / MIT — see assets/launcher-icons/README.md). A separate registry from the OS marks
|
|
# above, with its own sources, so it carries its own notices even where a vendor overlaps.
|
|
("Font Awesome Free brand icons (vendored, assets/launcher-icons)",
|
|
"assets/launcher-icons/LICENSES/font-awesome-brands.txt",
|
|
"https://fontawesome.com"),
|
|
("Simple Icons (vendored, assets/launcher-icons)",
|
|
"assets/launcher-icons/LICENSES/simple-icons.txt",
|
|
"https://simpleicons.org"),
|
|
("Playnite logo (vendored, assets/launcher-icons)",
|
|
"assets/launcher-icons/LICENSES/playnite.txt",
|
|
"https://github.com/JosefNemec/Playnite"),
|
|
]
|
|
|
|
|
|
def closure(meta, roots):
|
|
"""Package ids reachable from `roots` through `cargo metadata`'s resolve graph.
|
|
|
|
Deliberately the WHOLE resolve graph, not a per-target one: `cargo metadata` resolves
|
|
every `cfg()`-gated dependency of every member, so this OVER-approximates (an
|
|
`cfg(windows)`-only crate is reachable from a root even on a Linux build). Over-listing an
|
|
attribution is the safe direction; under-listing one is the failure this file exists to
|
|
prevent. What it does NOT do is pull in crates reachable only from OTHER workspace members,
|
|
which is the whole point.
|
|
"""
|
|
by_name = {}
|
|
for p in meta["packages"]:
|
|
by_name.setdefault(p["name"], p["id"])
|
|
nodes = {n["id"]: n for n in meta.get("resolve", {}).get("nodes", [])}
|
|
seen, stack = set(), []
|
|
for r in roots:
|
|
pid = by_name.get(r)
|
|
if pid is None:
|
|
raise SystemExit(f"--packages: no package named {r!r} in this workspace")
|
|
stack.append(pid)
|
|
while stack:
|
|
pid = stack.pop()
|
|
if pid in seen:
|
|
continue
|
|
seen.add(pid)
|
|
stack.extend(nodes.get(pid, {}).get("dependencies", []))
|
|
return seen
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("--out", default="THIRD-PARTY-NOTICES.txt")
|
|
ap.add_argument("--manifest", default="Cargo.toml")
|
|
ap.add_argument(
|
|
"--packages",
|
|
default="",
|
|
help="comma-separated workspace member names; restrict the notices to their transitive "
|
|
"dependency closure instead of the whole workspace",
|
|
)
|
|
args = ap.parse_args()
|
|
|
|
# `--all-features` is what makes `--packages` a GUARANTEE rather than a coincidence. Without
|
|
# it, cargo resolves the workspace with default features and unifies them across members, so a
|
|
# scoped closure can pick up a crate only because some OTHER member turned the feature on —
|
|
# and, worse, can MISS one when no member does. punktfunk-core's `quic` is exactly that case:
|
|
# it is not a default feature, and quinn/opus/rustls reach the Apple file today only through
|
|
# the workspace-wide union. Resolving every feature over-approximates instead, which is the
|
|
# safe direction for an attribution file: listing a crate that is not linked is untidy,
|
|
# omitting one that is is the failure this file exists to prevent.
|
|
meta = json.loads(subprocess.check_output(
|
|
["cargo", "metadata", "--format-version", "1", "--offline", "--all-features",
|
|
"--manifest-path", args.manifest],
|
|
text=True))
|
|
ws_members = set(meta.get("workspace_members", []))
|
|
|
|
keep = None
|
|
if args.packages.strip():
|
|
keep = closure(meta, [n.strip() for n in args.packages.split(",") if n.strip()])
|
|
|
|
pkgs = []
|
|
for p in meta["packages"]:
|
|
if p["id"] in ws_members:
|
|
continue # first-party (covered by the root LICENSE-MIT / LICENSE-APACHE)
|
|
if keep is not None and p["id"] not in keep:
|
|
continue
|
|
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)
|
|
|
|
repo_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
vendored = []
|
|
for label, lic_path, url in VENDORED_TREES:
|
|
full = os.path.join(repo_root, lic_path)
|
|
try:
|
|
with open(full, encoding="utf-8", errors="replace") as f:
|
|
txt = f.read().strip()
|
|
except OSError:
|
|
print(f"WARNING: vendored license missing: {lic_path}", file=sys.stderr)
|
|
continue
|
|
vendored.append((label, url))
|
|
h = hashlib.sha256(txt.encode("utf-8", "replace")).hexdigest()
|
|
ent = texts.setdefault(
|
|
h, {"text": txt, "filename": os.path.basename(lic_path), "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.")
|
|
if keep is not None:
|
|
w("")
|
|
w(f"Scope: the Rust crates linked by {args.packages} — not the whole punktfunk workspace.")
|
|
w("")
|
|
w(f"Total third-party crates: {len(pkgs)}")
|
|
w("")
|
|
if vendored:
|
|
w("-" * 76)
|
|
w("VENDORED THIRD-PARTY SOURCE (inside first-party crates)")
|
|
w("-" * 76)
|
|
for label, url in vendored:
|
|
w(f" {label} — {url}")
|
|
w("")
|
|
w("-" * 76)
|
|
w("MANIFEST (crate version — SPDX license — source)")
|
|
w("-" * 76)
|
|
for p in pkgs:
|
|
lic = p.get("license") or LICENSE_OVERRIDES.get(p["name"]) 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 LICENSE_OVERRIDES.get(p["name"]) 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()
|