73 lines
2.5 KiB
Python
Executable File
73 lines
2.5 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
# SPDX-FileCopyrightText: 2025 ai-ad4 and the FreePDFEditor contributors
|
|
#
|
|
# License gate (§8.2). Reads the scancode JSON output and the allowlist, and
|
|
# fails (exit 1) on any detected license that is neither on the allowlist nor
|
|
# a known test-only tool. Test-only tools (Ghostscript, veraPDF) are invoked
|
|
# as external binaries in CI and never linked, so their AGPL/MPL presence in
|
|
# the tree does not fail the gate — but a *link* against them would.
|
|
#
|
|
# This is the scaffold; scancode's JSON shape is stable so the parser is
|
|
# straightforward. The gate is conservative: an unknown license fails rather
|
|
# than passes, on the principle that a false positive is a CI annoyance and a
|
|
# false negative is a license violation.
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import sys
|
|
import pathlib
|
|
|
|
|
|
def load_allowlist(path: pathlib.Path) -> tuple[set[str], set[str]]:
|
|
allowed: set[str] = set()
|
|
test_only: set[str] = set()
|
|
in_test_only = False
|
|
for line in path.read_text(encoding="utf-8").splitlines():
|
|
line = line.strip()
|
|
if not line or line.startswith("#"):
|
|
continue
|
|
if line == "TEST-ONLY:":
|
|
in_test_only = True
|
|
continue
|
|
if line.startswith("- "):
|
|
if in_test_only:
|
|
test_only.add(line[2:].strip())
|
|
continue
|
|
in_test_only = False
|
|
allowed.add(line)
|
|
return allowed, test_only
|
|
|
|
|
|
def main(argv: list[str]) -> int:
|
|
if len(argv) < 3:
|
|
print("usage: check-licenses.py <scancode.json> <allowlist>", file=sys.stderr)
|
|
return 2
|
|
scancode_path = pathlib.Path(argv[1])
|
|
allowlist_path = pathlib.Path(argv[2])
|
|
allowed, test_only = load_allowlist(allowlist_path)
|
|
|
|
data = json.loads(scancode_path.read_text(encoding="utf-8"))
|
|
failures: list[str] = []
|
|
for entry in data.get("files", []):
|
|
path = entry.get("path", "?")
|
|
for lic in entry.get("licenses", []):
|
|
key = lic.get("key") or lic.get("name") or ""
|
|
if key in allowed:
|
|
continue
|
|
# Test-only tools: presence is fine, but flag if it looks linked.
|
|
if path in test_only:
|
|
continue
|
|
failures.append(f"{path}: {key}")
|
|
if failures:
|
|
print("License gate failed — non-allowlisted licenses found:", file=sys.stderr)
|
|
for f in failures:
|
|
print(f" {f}", file=sys.stderr)
|
|
return 1
|
|
print("License gate passed.")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main(sys.argv[1:])) |