docs: add Flutter license triage evidence
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
"""Create a hash-backed, non-legal triage report for unresolved Flutter licenses."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
from scripts.generate_component_inventory import (
|
||||
digest,
|
||||
flutter_license_file,
|
||||
package_roots,
|
||||
parse_lockfile,
|
||||
)
|
||||
except ModuleNotFoundError: # direct execution as `python scripts/...py`
|
||||
from generate_component_inventory import (
|
||||
digest,
|
||||
flutter_license_file,
|
||||
package_roots,
|
||||
parse_lockfile,
|
||||
)
|
||||
|
||||
|
||||
def candidate_license(text: str) -> tuple[str | None, str]:
|
||||
"""Suggest an SPDX identifier only when the license's identifying text is clear.
|
||||
|
||||
Suggestions are for human triage. They do not establish provenance, package
|
||||
applicability, exceptions, or permission to redistribute.
|
||||
"""
|
||||
normalized = re.sub(r"(?m)^\s*(?://|#|\*)\s?", "", text).lower()
|
||||
normalized = re.sub(r"\s+", " ", normalized)
|
||||
|
||||
matches: list[tuple[str, str]] = []
|
||||
if "mozilla public license version 2.0" in normalized:
|
||||
matches.append(("MPL-2.0", "license text contains the Mozilla Public License 2.0 title"))
|
||||
if "apache license" in normalized and "version 2.0" in normalized:
|
||||
matches.append(("Apache-2.0", "license text contains the Apache License 2.0 title"))
|
||||
if (
|
||||
"permission is hereby granted, free of charge" in normalized
|
||||
and "the software is provided" in normalized
|
||||
and "in no event shall" in normalized
|
||||
):
|
||||
matches.append(("MIT", "license text contains the standard MIT grant and warranty disclaimer"))
|
||||
if (
|
||||
"redistribution and use in source and binary forms" in normalized
|
||||
and "neither the name" in normalized
|
||||
and "disclaimer" in normalized
|
||||
):
|
||||
matches.append(("BSD-3-Clause", "license text contains the three-clause BSD endorsement restriction"))
|
||||
if (
|
||||
"redistribution and use in source and binary forms" in normalized
|
||||
and "neither the name" not in normalized
|
||||
and "disclaimer" in normalized
|
||||
and "provided that the following conditions are met" in normalized
|
||||
):
|
||||
matches.append(("BSD-2-Clause", "license text contains a two-clause BSD-style grant and disclaimer"))
|
||||
if len(matches) == 1:
|
||||
return matches[0]
|
||||
if len(matches) > 1:
|
||||
return None, "multiple license signatures occur in this file; manual review required"
|
||||
return None, "no conservative license-text signature matched"
|
||||
|
||||
|
||||
def build_report(root: Path) -> dict[str, object]:
|
||||
app_root = root / "flutter_app"
|
||||
packages = parse_lockfile(app_root / "pubspec.lock")
|
||||
roots = package_roots(app_root / ".dart_tool" / "package_config.json")
|
||||
entries: list[dict[str, object]] = []
|
||||
missing: list[dict[str, str]] = []
|
||||
|
||||
for package in packages:
|
||||
name = package["name"]
|
||||
package_root = roots.get(name)
|
||||
license_file = flutter_license_file(package_root) if package_root else None
|
||||
if license_file is None:
|
||||
missing.append({"name": name, "version": package["version"]})
|
||||
continue
|
||||
|
||||
text = license_file.read_text(encoding="utf-8", errors="replace")
|
||||
candidate, basis = candidate_license(text)
|
||||
entries.append(
|
||||
{
|
||||
"name": name,
|
||||
"version": package["version"],
|
||||
"scope": package.get("dependency", "transitive"),
|
||||
"license_file": license_file.name,
|
||||
"license_file_sha256": digest(license_file),
|
||||
"candidate_spdx": candidate,
|
||||
"candidate_basis": basis,
|
||||
"status": "candidate only; human review required",
|
||||
}
|
||||
)
|
||||
|
||||
counts: dict[str, int] = {}
|
||||
for entry in entries:
|
||||
candidate = entry["candidate_spdx"] or "unclassified"
|
||||
counts[str(candidate)] = counts.get(str(candidate), 0) + 1
|
||||
|
||||
return {
|
||||
"format": "flutter-license-triage-v1",
|
||||
"generated_at": datetime.now(UTC).isoformat(),
|
||||
"source": "flutter_app/pubspec.lock and resolved package LICENSE files",
|
||||
"notice": (
|
||||
"Text-signature suggestions only. Not legal advice, provenance verification, "
|
||||
"or approval to redistribute. Review each package, source, notices, and terms."
|
||||
),
|
||||
"summary": {
|
||||
"locked_packages": len(packages),
|
||||
"packages_with_license_file": len(entries),
|
||||
"packages_without_license_file": len(missing),
|
||||
"candidate_counts": dict(sorted(counts.items())),
|
||||
},
|
||||
"packages": sorted(entries, key=lambda item: str(item["name"]).lower()),
|
||||
"missing_license_file": sorted(missing, key=lambda item: item["name"].lower()),
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument(
|
||||
"--project-root",
|
||||
type=Path,
|
||||
default=Path(__file__).resolve().parents[1],
|
||||
)
|
||||
parser.add_argument("--output", type=Path, default=None)
|
||||
args = parser.parse_args()
|
||||
root = args.project_root.resolve()
|
||||
output = args.output or root / "sbom" / "flutter-license-triage.json"
|
||||
report = build_report(root)
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
output.write_text(
|
||||
json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8"
|
||||
)
|
||||
print(json.dumps(report["summary"], ensure_ascii=False, sort_keys=True))
|
||||
print(f"Wrote {output}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user