#!/usr/bin/env python3 import hashlib import json import sys from pathlib import Path SEPARATOR = "-" * 57 def load_manifest(repo): path = repo / "third_party/sources.json" data = json.loads(path.read_text(encoding="utf-8")) if data.get("format_version") != 2: raise RuntimeError("unsupported third_party/sources.json format_version") if not data.get("entries"): raise RuntimeError("third_party/sources.json has no entries") return data def read_verified_file(repo, file_entry): relative_path = file_entry["path"] path = repo / relative_path content = path.read_bytes() actual_hash = hashlib.sha256(content).hexdigest() expected_hash = file_entry["sha256"] if actual_hash != expected_hash: raise RuntimeError( "{} checksum mismatch: expected {}, got {}".format( relative_path, expected_hash, actual_hash ) ) return content.decode("utf-8").strip() def render_entry(repo, entry): required = ( "name", "license", "url", "sources", "license_files", ) missing = [field for field in required if not entry.get(field)] if missing: raise RuntimeError( "{} is missing fields: {}".format(entry.get("name", "entry"), ", ".join(missing)) ) if not entry.get("version") and not entry.get("revisions"): raise RuntimeError( "{} must have a version or revision".format(entry.get("name", "entry")) ) license_texts = [read_verified_file(repo, item) for item in entry["license_files"]] notice_files = entry.get("notice_files", []) notice_texts = [read_verified_file(repo, item) for item in notice_files] included_text = "\n".join(license_texts + notice_texts) title_parts = [entry["name"]] if entry.get("version"): title_parts.append(entry["version"]) title = " ".join(title_parts) + " - " + entry["license"] lines = [SEPARATOR, "", title, entry["url"]] revisions = entry.get("revisions", []) if len(revisions) == 1: lines.append("Revision: " + revisions[0]) elif revisions: lines.append("Revisions:") lines.extend("- " + revision for revision in revisions) attributions = [ attribution for attribution in entry.get("attributions", []) if attribution not in included_text ] lines.extend(attributions) adaptation_notice = entry.get("adaptation_notice") if adaptation_notice: lines.append(adaptation_notice) lines.extend(("", "\n\n---\n\n".join(license_texts))) if notice_files: lines.extend(("", "Upstream NOTICE:", "\n\n---\n\n".join(notice_texts))) lines.append(SEPARATOR) return "\n".join(lines) def render_notice(repo, manifest): entries = sorted(manifest["entries"], key=lambda entry: entry["name"].lower()) sections = [render_entry(repo, entry) for entry in entries] header = ( "NOTICES\n\n" "This repository incorporates material as listed below or described in the code." ) return header + "\n\n\n\n" + "\n\n".join(sections) + "\n" def main(): repo = Path(__file__).resolve().parent.parent output_path = repo / "ThirdPartyNotices.txt" try: manifest = load_manifest(repo) notice = render_notice(repo, manifest) except (OSError, RuntimeError, ValueError) as error: print("error: {}".format(error), file=sys.stderr) return 1 output_path.write_bytes(notice.encode("utf-8")) print("wrote {}".format(output_path)) return 0 if __name__ == "__main__": sys.exit(main())