1010 ERROR upstream fetch failed
1111"""
1212import base64
13+ from concurrent .futures import ThreadPoolExecutor , as_completed
14+ from functools import lru_cache
1315import hashlib
1416import json
1517import os
6870 "license" , "License.txt" , "NOTICE" ]
6971
7072
73+ @lru_cache (maxsize = None )
7174def 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+
78124def 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
89135def 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+
105169def 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 ("\n full 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"\n full results: { results_path } " )
238313
239314 accepted = {"IDENTICAL" , "IDENTICAL@PINNED" , "FIRST-PARTY-OK" ,
240315 "FIRST-PARTY-NOTICE" , "MANUAL-VERIFIED" }
0 commit comments