#!/usr/bin/env python3 """Preserve complete notices for the actual locked runtime/build crate graph.""" import argparse import hashlib import json from pathlib import Path import re import subprocess REVIEWED = {'GPL-3.0-only', 'MIT', 'MIT OR Apache-2.0', 'Apache-2.0 OR MIT', 'MIT/Apache-2.0', 'Unlicense OR MIT', 'Unlicense/MIT', 'MPL-2.0', 'Apache-2.0', 'Apache-2.0 OR BSL-1.0', 'Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT', 'BSD-2-Clause OR Apache-2.0 OR MIT', 'Zlib OR Apache-2.0 OR MIT', 'MIT OR Apache-2.0 OR Zlib', '(MIT OR Apache-2.0) AND Unicode-3.0'} NOTICE_NAME = re.compile(r'^(licen[cs]e|copying|notice|unlicense|copyright)([-.]|$)', re.I) p = argparse.ArgumentParser(description=__doc__) p.add_argument('destination', type=Path) p.add_argument('--revision', required=True) a = p.parse_args() root = Path(__file__).resolve().parent.parent host = next(x[6:] for x in subprocess.check_output(['rustc', '-vV'], text=True).splitlines() if x.startswith('host: ')) metadata = json.loads(subprocess.check_output(['cargo', 'metadata', '--locked', '--format-version', '1', '--filter-platform', host], cwd=root, text=True)) # Cargo metadata includes dev-unified/optional edges; use Cargo's actual normal # and build tree to select the shipped graph instead of guessing feature edges. tree = subprocess.check_output(['cargo', 'tree', '--locked', '-p', 'sized', '--target', host, '--edges', 'normal,build', '--prefix', 'none', '--format', '{p}'], cwd=root, text=True) selected = {tuple(re.match(r'^(\S+) v(\S+)', line).groups()) for line in tree.splitlines()} packages = sorted([x for x in metadata['packages'] if (x['name'], x['version']) in selected], key=lambda x: (x['name'], x['version'])) assert len(packages) == len(selected), 'Ambiguous or missing package identity' texts, inventory = {}, [] for package in packages: if package['license'] not in REVIEWED: raise ValueError(f"Unreviewed licence: {package['name']} {package['license']}") directory = Path(package['manifest_path']).parent files = sorted(f for f in directory.iterdir() if f.is_file() and NOTICE_NAME.match(f.name)) if not files: raise ValueError(f"Missing licence text: {package['name']}") notices = [] for f in files: data = f.read_bytes(); text = data.decode('utf-8'); assert text.strip() digest = hashlib.sha256(data).hexdigest() texts.setdefault(digest, {'labels': [], 'text': text})['labels'].append(f"{package['name']} {package['version']} / {f.name}") notices.append({'file': f.name, 'sha256': digest}) inventory.append({'name': package['name'], 'version': package['version'], 'license': package['license'], 'notices': notices}) header = ['Sized - Licences and credits', '', f'Source revision: {a.revision}', f'Target: {host}', '', 'Sized and its core are GPL-3.0-only, without warranty.', 'Matching source, including the pinned core and dependency sources, is available at:', 'https://gamertan.com/projects/sized/ and the matching Gitea release.', 'The colored 2.2.0 source remains under MPL-2.0 and is additionally distributed', 'under GPL-3.0-only as part of this Larger Work under MPL section 3.3.', 'All original notices are preserved. See docs/RELEASE-LICENSING.md in the source.', '', 'Runtime and build dependencies (development-only dependencies are excluded):'] header += [f" {x['name']} {x['version']} - {x['license']}" for x in inventory] for x in texts.values(): header += ['', '='*72, '\n'.join(x['labels']), '='*72, x['text']] a.destination.mkdir(parents=True, exist_ok=True) (a.destination/'LICENCES.txt').write_text('\n'.join(header)+'\n') (a.destination/'DEPENDENCIES.json').write_text(json.dumps({'revision': a.revision, 'target': host, 'packages': inventory}, indent=2)+'\n') print(f'Packaged complete notices for {len(inventory)} runtime/build crates ({host}).')