mirror of
https://github.com/NVIDIA/OpenShell.git
synced 2026-10-19 05:50:41 +08:00
225 lines
7.2 KiB
Python
Executable File
225 lines
7.2 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
|
|
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
|
|
"""Generate THIRD-PARTY-NOTICES from Rust and Python dependency metadata.
|
|
|
|
Usage:
|
|
python scripts/generate_third_party_notices.py
|
|
|
|
Writes THIRD-PARTY-NOTICES to the repo root. Requires `cargo` on PATH
|
|
for Rust deps. Python deps are read from pyproject.toml.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
|
|
def find_repo_root() -> Path:
|
|
"""Walk up from CWD to find the directory containing .git."""
|
|
path = Path.cwd()
|
|
while path != path.parent:
|
|
if (path / ".git").exists():
|
|
return path
|
|
path = path.parent
|
|
return Path.cwd()
|
|
|
|
|
|
# Workspace member crate names to exclude (these are ours, not third-party).
|
|
WORKSPACE_PREFIXES = ("navigator-",)
|
|
|
|
|
|
def get_rust_deps_from_cargo_metadata() -> list[dict[str, str]] | None:
|
|
"""Try to extract third-party Rust deps via `cargo metadata`."""
|
|
try:
|
|
result = subprocess.run(
|
|
["cargo", "metadata", "--format-version", "1"],
|
|
capture_output=True,
|
|
text=True,
|
|
check=True,
|
|
)
|
|
except (FileNotFoundError, subprocess.CalledProcessError):
|
|
return None
|
|
|
|
meta = json.loads(result.stdout)
|
|
workspace_members = set(meta.get("workspace_members", []))
|
|
|
|
deps = []
|
|
for pkg in meta["packages"]:
|
|
if pkg["id"] in workspace_members:
|
|
continue
|
|
if any(pkg["name"].startswith(p) for p in WORKSPACE_PREFIXES):
|
|
continue
|
|
deps.append({
|
|
"name": pkg["name"],
|
|
"version": pkg["version"],
|
|
"license": pkg.get("license") or "Unknown",
|
|
"repository": pkg.get("repository") or "",
|
|
})
|
|
|
|
return sorted(deps, key=lambda d: d["name"].lower())
|
|
|
|
|
|
def get_rust_deps_from_lockfile(root: Path) -> list[dict[str, str]]:
|
|
"""Fallback: parse Cargo.lock for name+version (no license info)."""
|
|
lockfile = root / "Cargo.lock"
|
|
if not lockfile.exists():
|
|
return []
|
|
|
|
deps = []
|
|
content = lockfile.read_text()
|
|
name = None
|
|
version = None
|
|
for line in content.splitlines():
|
|
if line.startswith("name = "):
|
|
name = line.split('"')[1]
|
|
elif line.startswith("version = ") and '"' in line:
|
|
version = line.split('"')[1]
|
|
elif line == "[[package]]" or line == "":
|
|
if name and version:
|
|
if not any(name.startswith(p) for p in WORKSPACE_PREFIXES):
|
|
deps.append({
|
|
"name": name,
|
|
"version": version,
|
|
"license": "See crates.io",
|
|
"repository": f"https://crates.io/crates/{name}",
|
|
})
|
|
name = None
|
|
version = None
|
|
|
|
# Catch the last entry.
|
|
if name and version:
|
|
if not any(name.startswith(p) for p in WORKSPACE_PREFIXES):
|
|
deps.append({
|
|
"name": name,
|
|
"version": version,
|
|
"license": "See crates.io",
|
|
"repository": f"https://crates.io/crates/{name}",
|
|
})
|
|
|
|
return sorted(deps, key=lambda d: d["name"].lower())
|
|
|
|
|
|
def get_rust_deps(root: Path) -> list[dict[str, str]]:
|
|
"""Extract third-party Rust dependencies.
|
|
|
|
Prefers `cargo metadata` for full license info. Falls back to parsing
|
|
Cargo.lock when cargo is not available.
|
|
"""
|
|
deps = get_rust_deps_from_cargo_metadata()
|
|
if deps is not None:
|
|
return deps
|
|
|
|
print(" cargo not found, falling back to Cargo.lock parsing", file=sys.stderr)
|
|
return get_rust_deps_from_lockfile(root)
|
|
|
|
|
|
def get_python_deps(root: Path) -> list[dict[str, str]]:
|
|
"""Extract Python dependencies from pyproject.toml [project.dependencies]."""
|
|
pyproject = root / "pyproject.toml"
|
|
if not pyproject.exists():
|
|
return []
|
|
|
|
# Simple parser: read the dependencies list without a TOML library.
|
|
content = pyproject.read_text()
|
|
deps = []
|
|
in_deps = False
|
|
for line in content.splitlines():
|
|
stripped = line.strip()
|
|
if stripped.startswith("dependencies = ["):
|
|
in_deps = True
|
|
continue
|
|
if in_deps:
|
|
if stripped == "]":
|
|
break
|
|
# Parse "package>=version" style.
|
|
dep = stripped.strip('",').strip()
|
|
if dep:
|
|
# Split on first version specifier.
|
|
for sep in (">=", "==", "~=", "!=", "<", ">"):
|
|
if sep in dep:
|
|
name = dep[:dep.index(sep)].strip()
|
|
deps.append({
|
|
"name": name,
|
|
"version": dep[dep.index(sep):].strip(),
|
|
"license": "See PyPI",
|
|
"repository": f"https://pypi.org/project/{name}/",
|
|
})
|
|
break
|
|
else:
|
|
deps.append({
|
|
"name": dep,
|
|
"version": "",
|
|
"license": "See PyPI",
|
|
"repository": f"https://pypi.org/project/{dep}/",
|
|
})
|
|
|
|
return sorted(deps, key=lambda d: d["name"].lower())
|
|
|
|
|
|
def format_notices(rust_deps: list[dict], python_deps: list[dict]) -> str:
|
|
"""Format the THIRD-PARTY-NOTICES file content."""
|
|
lines = [
|
|
"THIRD-PARTY SOFTWARE NOTICES",
|
|
"",
|
|
"This file lists the third-party software packages used by OpenShell,",
|
|
"along with their respective licenses.",
|
|
"",
|
|
"To regenerate: uv run python scripts/generate_third_party_notices.py",
|
|
"",
|
|
]
|
|
|
|
if rust_deps:
|
|
lines.append("=" * 80)
|
|
lines.append("Rust Dependencies")
|
|
lines.append("=" * 80)
|
|
lines.append("")
|
|
for dep in rust_deps:
|
|
lines.append(f"Package: {dep['name']} {dep['version']}")
|
|
lines.append(f"License: {dep['license']}")
|
|
if dep["repository"]:
|
|
lines.append(f"Repository: {dep['repository']}")
|
|
lines.append("")
|
|
|
|
if python_deps:
|
|
lines.append("=" * 80)
|
|
lines.append("Python Dependencies")
|
|
lines.append("=" * 80)
|
|
lines.append("")
|
|
for dep in python_deps:
|
|
version_str = f" {dep['version']}" if dep["version"] else ""
|
|
lines.append(f"Package: {dep['name']}{version_str}")
|
|
lines.append(f"License: {dep['license']}")
|
|
if dep["repository"]:
|
|
lines.append(f"Repository: {dep['repository']}")
|
|
lines.append("")
|
|
|
|
return "\n".join(lines)
|
|
|
|
|
|
def main() -> int:
|
|
root = find_repo_root()
|
|
|
|
print("Collecting Rust dependencies...")
|
|
rust_deps = get_rust_deps(root)
|
|
print(f" Found {len(rust_deps)} Rust dependencies")
|
|
|
|
print("Collecting Python dependencies...")
|
|
python_deps = get_python_deps(root)
|
|
print(f" Found {len(python_deps)} Python dependencies")
|
|
|
|
notices = format_notices(rust_deps, python_deps)
|
|
output = root / "THIRD-PARTY-NOTICES"
|
|
output.write_text(notices)
|
|
print(f"Wrote {output}")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|