freepdfeditor/ci/check-licenses.py

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:]))