Skip to content

Commit b21f33f

Browse files
committed
perf: parallelize license provenance audit
1 parent bcd9310 commit b21f33f

1 file changed

Lines changed: 97 additions & 22 deletions

File tree

‎scripts/audit-license-provenance.py‎

Lines changed: 97 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
ERROR upstream fetch failed
1111
"""
1212
import base64
13+
from concurrent.futures import ThreadPoolExecutor, as_completed
14+
from functools import lru_cache
1315
import hashlib
1416
import json
1517
import os
@@ -68,13 +70,57 @@
6870
"license", "License.txt", "NOTICE"]
6971

7072

73+
@lru_cache(maxsize=None)
7174
def gh_api(path):
75+
"""Fetch one GitHub API path once per audit process."""
7276
r = subprocess.run(["gh", "api", path], capture_output=True, text=True)
7377
if r.returncode != 0:
7478
return None
7579
return r.stdout
7680

7781

82+
def gh_json(path):
83+
out = gh_api(path)
84+
if not out:
85+
return None
86+
try:
87+
return json.loads(out)
88+
except json.JSONDecodeError:
89+
return None
90+
91+
92+
@lru_cache(maxsize=None)
93+
def default_branch(repo):
94+
data = gh_json(f"repos/{repo}")
95+
return data.get("default_branch") if isinstance(data, dict) else None
96+
97+
98+
@lru_cache(maxsize=None)
99+
def upstream_tree(repo, ref=None):
100+
"""Return one repository/ref tree, or None when GitHub cannot provide it."""
101+
tree_ref = ref or default_branch(repo)
102+
if not tree_ref:
103+
return None
104+
data = gh_json(f"repos/{repo}/git/trees/{tree_ref}?recursive=1")
105+
if not isinstance(data, dict) or data.get("truncated"):
106+
return None
107+
return {
108+
entry["path"]: entry["sha"]
109+
for entry in data.get("tree", [])
110+
if entry.get("type") == "blob" and entry.get("path") and entry.get("sha")
111+
}
112+
113+
114+
def upstream_blob(repo, sha):
115+
data = gh_json(f"repos/{repo}/git/blobs/{sha}")
116+
if not isinstance(data, dict) or data.get("encoding") != "base64":
117+
return None
118+
try:
119+
return base64.b64decode(data.get("content", "")).decode("utf-8", "replace")
120+
except (ValueError, UnicodeDecodeError):
121+
return None
122+
123+
78124
def upstream_default_license(repo):
79125
out = gh_api(f"repos/{repo}/license")
80126
if not out:
@@ -87,6 +133,10 @@ def upstream_default_license(repo):
87133

88134

89135
def upstream_file(repo, path, ref=None):
136+
tree = upstream_tree(repo, ref)
137+
if tree is not None and path in tree:
138+
return upstream_blob(repo, tree[path])
139+
90140
url = f"repos/{repo}/contents/{path}"
91141
if ref:
92142
url += f"?ref={ref}"
@@ -102,6 +152,20 @@ def upstream_file(repo, path, ref=None):
102152
return None
103153

104154

155+
def upstream_named_file(repo, name, ref=None):
156+
"""Find a named license in a cached tree, falling back to contents API."""
157+
tree = upstream_tree(repo, ref)
158+
if tree is not None:
159+
wanted = name.lower()
160+
for path, sha in tree.items():
161+
if os.path.basename(path).lower() == wanted:
162+
content = upstream_blob(repo, sha)
163+
if content is not None:
164+
return content
165+
return None
166+
return upstream_file(repo, name, ref)
167+
168+
105169
def local_license(dirpath):
106170
if not os.path.isdir(dirpath):
107171
return None, None
@@ -132,43 +196,40 @@ def main():
132196
manifest = parse_manifest()
133197
results = {}
134198

135-
def check_upstream(key, dirpath, repo, pinned=None, exact_path=None):
199+
def check_upstream(dirpath, repo, pinned=None, exact_path=None):
136200
fname, ours = local_license(dirpath)
137201
if ours is None:
138-
results[key] = ("NO-LOCAL-LICENSE", "")
139-
return
202+
return "NO-LOCAL-LICENSE", ""
140203
# 1) exact upstream path (e.g. lz4 lib/LICENSE), at HEAD then pinned
141204
if exact_path:
142205
for ref in (None, pinned):
143206
up = upstream_file(repo, exact_path, ref)
144207
if up is not None and up == ours:
145-
results[key] = ("IDENTICAL" if ref is None else "IDENTICAL@PINNED",
146-
f"{repo}:{exact_path}")
147-
return
208+
return ("IDENTICAL" if ref is None else "IDENTICAL@PINNED",
209+
f"{repo}:{exact_path}")
148210
# 2) default-branch detected license
149211
up = upstream_default_license(repo)
150212
if up is not None and up == ours:
151-
results[key] = ("IDENTICAL", f"{repo} (default branch)")
152-
return
213+
return "IDENTICAL", f"{repo} (default branch)"
153214
# 3) pinned-commit candidates by filename
154215
if pinned:
155216
tried = [fname] + [c for c in CANDIDATE_NAMES if c != fname]
156217
for cand in tried:
157-
up2 = upstream_file(repo, cand, pinned)
218+
up2 = upstream_named_file(repo, cand, pinned)
158219
if up2 is not None and up2 == ours:
159-
results[key] = ("IDENTICAL@PINNED", f"{repo}:{cand}@{pinned}")
160-
return
220+
return "IDENTICAL@PINNED", f"{repo}:{cand}@{pinned}"
161221
# 4) HEAD candidates by filename
162222
for cand in CANDIDATE_NAMES:
163-
up3 = upstream_file(repo, cand)
223+
up3 = upstream_named_file(repo, cand)
164224
if up3 is not None and up3 == ours:
165-
results[key] = ("IDENTICAL", f"{repo}:{cand} (default branch)")
166-
return
167-
results[key] = ("DIFFERS" if up is not None else "ERROR", f"{repo}")
225+
return "IDENTICAL", f"{repo}:{cand} (default branch)"
226+
return ("DIFFERS" if up is not None else "ERROR", f"{repo}")
227+
228+
checks = []
168229

169230
# Libraries
170231
for rel, (repo, exact) in LIBS.items():
171-
check_upstream(rel, os.path.join(ROOT, rel), repo, exact_path=exact)
232+
checks.append((rel, os.path.join(ROOT, rel), repo, None, exact))
172233

173234
# Special: nomic = canonical Apache-2.0 text; sqlite3 = first-party notice
174235
#
@@ -206,20 +267,29 @@ def check_upstream(key, dirpath, repo, pinned=None, exact_path=None):
206267
results[key] = ("FIRST-PARTY-VAR", f"{fname}: differs from root LICENSE")
207268
continue
208269
if g in FORKS:
209-
check_upstream(key, d, FORKS[g])
270+
checks.append((key, d, FORKS[g], None, None))
210271
continue
211272
if g in SPECIAL_NOTICE:
212273
results[key] = ("MANUAL-VERIFIED", SPECIAL_NOTICE[g])
213274
continue
214275
if g in DISAGREEMENT:
215-
check_upstream(key, d, DISAGREEMENT[g])
276+
checks.append((key, d, DISAGREEMENT[g], None, None))
216277
continue
217278
if g in manifest:
218279
repo, pinned = manifest[g]
219-
check_upstream(key, d, repo, pinned=pinned)
280+
checks.append((key, d, repo, pinned, None))
220281
continue
221282
results[key] = ("NO-MANIFEST-ENTRY", "")
222283

284+
workers = max(1, int(os.environ.get("CBM_PROVENANCE_WORKERS", "16")))
285+
with ThreadPoolExecutor(max_workers=workers) as pool:
286+
futures = {
287+
pool.submit(check_upstream, directory, repo, pinned, exact): key
288+
for key, directory, repo, pinned, exact in checks
289+
}
290+
for future in as_completed(futures):
291+
results[futures[future]] = future.result()
292+
223293
# Report
224294
from collections import Counter
225295
counts = Counter(v[0] for v in results.values())
@@ -232,9 +302,14 @@ def check_upstream(key, dirpath, repo, pinned=None, exact_path=None):
232302
verdict, detail = results[key]
233303
if verdict not in ("IDENTICAL", "IDENTICAL@PINNED", "FIRST-PARTY-OK"):
234304
print(f" {key}: {verdict} [{detail}]")
235-
json.dump({k: list(v) for k, v in results.items()},
236-
open("/tmp/audit_licenses_results.json", "w"), indent=1)
237-
print("\nfull results: /tmp/audit_licenses_results.json")
305+
results_path = os.environ.get(
306+
"CBM_PROVENANCE_RESULTS",
307+
os.path.join(ROOT, "build", "audit_licenses_results.json"),
308+
)
309+
os.makedirs(os.path.dirname(os.path.abspath(results_path)), exist_ok=True)
310+
with open(results_path, "w", encoding="utf-8") as fh:
311+
json.dump({k: list(v) for k, v in results.items()}, fh, indent=1)
312+
print(f"\nfull results: {results_path}")
238313

239314
accepted = {"IDENTICAL", "IDENTICAL@PINNED", "FIRST-PARTY-OK",
240315
"FIRST-PARTY-NOTICE", "MANUAL-VERIFIED"}

0 commit comments

Comments
 (0)