89 lines
2.6 KiB
Python
89 lines
2.6 KiB
Python
#!/usr/bin/env python3
|
|||
|
|
"""Audit ``uv.lock`` for license compliance.
|
||
|
|
|
||
|
|
Parses the lockfile (TOML) and reports each package's ``license`` field.
|
||
|
|
The tool is intentionally read-only — it never modifies ``uv.lock``.
|
||
|
|
Permissive licenses (MIT, BSD-2/3-Clause, Apache-2.0, ISC, MPL-2.0,
|
||
|
|
PSF-2.0, etc.) are flagged as OK; everything else is reported as a
|
||
|
|
warning. Unknown or missing licenses are also reported as warnings.
|
||
|
|
|
||
|
|
Usage::
|
||
|
|
|
||
|
|
uv run python tools/license-audit.py
|
||
|
|
"""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import sys
|
||
|
|
import tomllib
|
||
|
|
from pathlib import Path
|
||
|
|
from typing import Any
|
||
|
|
|
||
|
|
LOCK_PATH = Path(__file__).resolve().parent.parent / "uv.lock"
|
||
|
|
|
||
|
|
PERMISSIVE_LICENSES: frozenset[str] = frozenset(
|
||
|
|
{
|
||
|
|
# SPDX expressions (case-insensitive)
|
||
|
|
"mit",
|
||
|
|
"mit-0",
|
||
|
|
"apache-2.0",
|
||
|
|
"apache-2.0-with-clauses",
|
||
|
|
"bsd-2-clause",
|
||
|
|
"bsd-3-clause",
|
||
|
|
"isc",
|
||
|
|
"mpl-2.0",
|
||
|
|
"python-2.0",
|
||
|
|
"psf-2.0",
|
||
|
|
"zlib",
|
||
|
|
"cc0-1.0",
|
||
|
|
"cc-by-4.0",
|
||
|
|
"unlicense",
|
||
|
|
"0bsd",
|
||
|
|
"blueoak-1.0.0",
|
||
|
|
"lgpl-2.1",
|
||
|
|
"lgpl-3.0",
|
||
|
|
}
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def main() -> int:
|
||
|
|
if not LOCK_PATH.exists():
|
||
|
|
print(f"error: {LOCK_PATH} not found", file=sys.stderr)
|
||
|
|
return 2
|
||
|
|
with LOCK_PATH.open("rb") as fh:
|
||
|
|
data: dict[str, Any] = tomllib.load(fh)
|
||
|
|
packages: list[dict[str, Any]] = list(data.get("package", []))
|
||
|
|
if not packages:
|
||
|
|
print("error: no [[package]] entries in uv.lock", file=sys.stderr)
|
||
|
|
return 2
|
||
|
|
|
||
|
|
warnings_count = 0
|
||
|
|
for pkg in packages:
|
||
|
|
name = pkg.get("name", "?")
|
||
|
|
version = pkg.get("version", "?")
|
||
|
|
license_field = pkg.get("license")
|
||
|
|
if license_field is None:
|
||
|
|
# uv stores a separate ``[[package.license]]`` table only for
|
||
|
|
# SPDX-refs; for many packages the field is absent.
|
||
|
|
print(f"WARN {name}=={version}: license field missing")
|
||
|
|
warnings_count += 1
|
||
|
|
continue
|
||
|
|
text = str(license_field).strip().lower()
|
||
|
|
# Compound expressions like ``MIT OR Apache-2.0`` are accepted when
|
||
|
|
# any single token is permissive.
|
||
|
|
tokens = {
|
||
|
|
tok.strip(" ()")
|
||
|
|
for tok in text.replace(" and ", " ").replace(" or ", " ").split()
|
||
|
|
}
|
||
|
|
if tokens & PERMISSIVE_LICENSES:
|
||
|
|
print(f"OK {name}=={version}: {text}")
|
||
|
|
else:
|
||
|
|
print(f"WARN {name}=={version}: {text}")
|
||
|
|
warnings_count += 1
|
||
|
|
print(f"\nAudited {len(packages)} packages; {warnings_count} warning(s).")
|
||
|
|
return 0
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
raise SystemExit(main())
|