Source code for generate_rust_licenses

#!/usr/bin/env python3
#
# ISC License
#
# Copyright (c) 2026, Autonomous Vehicle Systems Lab, University of Colorado at Boulder
#
# Permission to use, copy, modify, and/or distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

"""Generate a deterministic Rust third-party license report.

Defaults produce the report shipped with Basilisk. SDK extension projects can
override the manifest, policy, output, project name, and project-license text.
The report is derived from the locked Cargo workspace dependency graph. Use
``--check`` in automation to verify that the committed report is current. A
normal local check skips cleanly when the pinned generator is unavailable; CI
uses ``--require-tool`` so that the same condition is an error there.
"""

from __future__ import annotations

import argparse
import difflib
import json
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path
from typing import Any


CARGO_ABOUT_VERSION = "0.9.1"
REPOSITORY_ROOT = Path(__file__).resolve().parents[3]
WORKSPACE_MANIFEST = REPOSITORY_ROOT / "src" / "Cargo.toml"
SCRIPT_DIRECTORY = Path(__file__).resolve().parent
ABOUT_CONFIG = SCRIPT_DIRECTORY / "licenses" / "about.toml"
if not ABOUT_CONFIG.is_file():
    # bsk-sdk installs the synchronized generator beside its policy file.
    ABOUT_CONFIG = SCRIPT_DIRECTORY / "about.toml"
LICENSE_REPORT = REPOSITORY_ROOT / "LICENSES" / "RUST-THIRD-PARTY.txt"


[docs] def find_cargo_about(require_tool: bool) -> str | None: """Return the pinned ``cargo-about`` executable, if available.""" executable = shutil.which("cargo-about") if executable is None: message = ( "cargo-about is unavailable; install it with " "`cargo install cargo-about " f"--version '={CARGO_ABOUT_VERSION}' --locked --features cli`" ) if require_tool: raise RuntimeError(message) print(f"Skipping Rust license check: {message}.") return None result = subprocess.run( [executable, "--version"], check=True, capture_output=True, text=True, ) expected = f"cargo-about {CARGO_ABOUT_VERSION}" if result.stdout.strip() != expected: raise RuntimeError( f"expected {expected}, found {result.stdout.strip() or 'unknown version'}" ) return executable
[docs] def cargo_about_data( executable: str, workspace_manifest: Path, about_config: Path ) -> dict[str, Any]: """Return cargo-about's JSON report for the locked workspace.""" with tempfile.TemporaryDirectory(prefix="bsk-rust-licenses-") as directory: output_path = Path(directory) / "licenses.json" subprocess.run( [ executable, "generate", "--workspace", "--locked", "--offline", "--fail", "--manifest-path", str(workspace_manifest), "--config", str(about_config), "--format", "json", "--output-file", str(output_path), ], check=True, cwd=workspace_manifest.parent, ) return json.loads(output_path.read_text(encoding="utf-8"))
[docs] def third_party_licenses(data: dict[str, Any]) -> list[dict[str, Any]]: """Remove Basilisk workspace crates from cargo-about license entries.""" entries = [] for license_entry in data["licenses"]: entry = dict(license_entry) entry["used_by"] = [ item for item in entry["used_by"] if item["crate"].get("source") is not None ] if entry["used_by"]: entries.append(entry) return entries
[docs] def dependency_notices(entries: list[dict[str, Any]]) -> list[tuple[str, str]]: """Collect top-level third-party ``NOTICE`` files from reported crates.""" packages: dict[str, dict[str, Any]] = {} for entry in entries: for item in entry["used_by"]: package = item["crate"] packages[f"{package['name']} {package['version']}"] = package notices = [] for package_name, package in sorted(packages.items()): package_root = Path(package["manifest_path"]).parent for path in sorted(package_root.iterdir()): if path.is_file() and path.name.upper().startswith("NOTICE"): notices.append((f"{package_name}: {path.name}", path.read_text(encoding="utf-8"))) return notices
[docs] def normalize_newlines(text: str) -> str: """Return ``text`` with platform-independent line endings.""" return text.replace("\r\n", "\n").replace("\r", "\n")
[docs] def render_report( data: dict[str, Any], project_name: str = "Basilisk", manifest_label: str = "src/Cargo.lock", generator_label: str = "src/architecture/rust/generate_rust_licenses.py", project_license: str = "ISC License in the repository's LICENSE file.", ) -> str: """Render a deterministic, human-readable third-party license report.""" entries = third_party_licenses(data) title = f"{project_name} Rust Third-Party Licenses" underline_length = 35 if project_name == "Basilisk" else len(title) lines = [ title, "=" * underline_length, "", f"This file is generated from {manifest_label} by", f"{generator_label}. Do not edit it by hand.", "", "It covers non-development Rust dependencies used to build or included in", ( f"{project_name}'s native Rust modules. {project_name} itself remains " "licensed under the" ), project_license, "", ] for entry in entries: heading = f"{entry['name']} ({entry['id']})" lines.extend([heading, "-" * len(heading), "", "Used by:"]) for item in entry["used_by"]: package = item["crate"] lines.append(f"- {package['name']} {package['version']}") lines.extend(["", normalize_newlines(entry["text"]).rstrip(), ""]) notices = dependency_notices(entries) if notices: lines.extend(["Third-Party Notices", "-------------------", ""]) for heading, notice in notices: lines.extend( [heading, "~" * len(heading), "", normalize_newlines(notice).rstrip(), ""] ) return "\n".join(lines).rstrip() + "\n"
[docs] def check_report(expected: str, license_report: Path = LICENSE_REPORT) -> bool: """Return whether the committed report matches ``expected``.""" actual = license_report.read_text(encoding="utf-8") if license_report.exists() else "" if actual == expected: print(f"Rust third-party license report is current: {license_report}") return True difference = difflib.unified_diff( actual.splitlines(), expected.splitlines(), fromfile=str(license_report), tofile="generated report", lineterm="", ) print("\n".join(difference)) print( "Rust third-party license report is stale; run " "the Rust license generator without `--check`.", file=sys.stderr, ) return False
[docs] def main() -> int: """Generate or check the committed Rust third-party license report.""" parser = argparse.ArgumentParser(description=__doc__) has_basilisk_defaults = WORKSPACE_MANIFEST.is_file() parser.add_argument("--check", action="store_true", help="verify rather than update the report") parser.add_argument( "--require-tool", action="store_true", help="fail instead of skipping when cargo-about is unavailable", ) parser.add_argument( "--manifest-path", type=Path, default=WORKSPACE_MANIFEST if has_basilisk_defaults else None, required=not has_basilisk_defaults, help=( "Cargo workspace manifest to inspect; required outside the " "Basilisk source tree" ), ) parser.add_argument( "--config", type=Path, default=ABOUT_CONFIG, help="cargo-about policy file", ) parser.add_argument( "--output", type=Path, default=LICENSE_REPORT if has_basilisk_defaults else None, required=not has_basilisk_defaults, help=( "third-party license report path; required outside the Basilisk " "source tree" ), ) parser.add_argument( "--project-name", default="Basilisk", help="project name displayed in the report", ) parser.add_argument( "--project-license", default="ISC License in the repository's LICENSE file.", help="project-license sentence displayed after the dependency overview", ) parser.add_argument( "--manifest-label", default=None, help="stable Cargo.lock label written into the report", ) parser.add_argument( "--generator-label", default=None, help="stable generator label written into the report", ) arguments = parser.parse_args() try: executable = find_cargo_about(arguments.require_tool) if executable is None: return 0 workspace_manifest = arguments.manifest_path.resolve() about_config = arguments.config.resolve() output = arguments.output.resolve() using_basilisk_defaults = ( workspace_manifest == WORKSPACE_MANIFEST.resolve() and about_config == ABOUT_CONFIG.resolve() and output == LICENSE_REPORT.resolve() and arguments.project_name == "Basilisk" ) report = render_report( cargo_about_data(executable, workspace_manifest, about_config), project_name=arguments.project_name, project_license=arguments.project_license, manifest_label=( arguments.manifest_label or ("src/Cargo.lock" if using_basilisk_defaults else "Cargo.lock") ), generator_label=( arguments.generator_label or ( "src/architecture/rust/generate_rust_licenses.py" if using_basilisk_defaults else "the bsk-sdk Rust license generator" ) ), ) except ( OSError, RuntimeError, UnicodeError, subprocess.CalledProcessError, json.JSONDecodeError, ) as error: print(f"Rust license generation failed: {error}", file=sys.stderr) return 1 if arguments.check: return 0 if check_report(report, output) else 1 output.parent.mkdir(parents=True, exist_ok=True) output.write_text(report, encoding="utf-8") print(f"Generated {output}") return 0
if __name__ == "__main__": raise SystemExit(main())