@@ -61,6 +61,8 @@ class LicenseCorrection:
6161 license_path : str
6262 license_name : str
6363 anchor : str
64+ license_from_repository : bool = False
65+ expected_versions : tuple [tuple [str , str ], ...] = ()
6466
6567
6668@dataclass (frozen = True )
@@ -122,6 +124,18 @@ class BundledComponent:
122124)
123125
124126PYTHON_MIT_CORRECTIONS = (
127+ LicenseCorrection (
128+ crates = ("jieba-macros" , "jieba-rs" ),
129+ license_crate = "jieba-rs" ,
130+ license_path = "third-party-licenses/jieba-rs-0.10.3.LICENSE" ,
131+ license_name = "MIT License (jieba-rs workspace)" ,
132+ anchor = "mit-jieba-rs-workspace" ,
133+ license_from_repository = True ,
134+ expected_versions = (
135+ ("jieba-macros" , "0.10.3" ),
136+ ("jieba-rs" , "0.10.3" ),
137+ ),
138+ ),
125139 LicenseCorrection (
126140 crates = (
127141 "ownedbytes" ,
@@ -342,14 +356,18 @@ def resolved_packages(metadata: dict) -> list[dict]:
342356 ]
343357
344358
345- def package_by_name (
346- metadata : dict , crate_name : str , required : bool = True
347- ) -> dict | None :
348- matches = [
359+ def packages_by_name (metadata : dict , crate_name : str ) -> list [dict ]:
360+ return [
349361 package
350362 for package in resolved_packages (metadata )
351363 if package ["name" ] == crate_name
352364 ]
365+
366+
367+ def package_by_name (
368+ metadata : dict , crate_name : str , required : bool = True
369+ ) -> dict | None :
370+ matches = packages_by_name (metadata , crate_name )
353371 if not matches and not required :
354372 return None
355373 if len (matches ) != 1 :
@@ -371,25 +389,61 @@ def package_features(metadata: dict, package: dict) -> set[str]:
371389 return set (matches [0 ])
372390
373391
374- def correction_html (metadata : dict , correction : LicenseCorrection ) -> str :
392+ def correction_html (
393+ root : Path , metadata : dict , correction : LicenseCorrection
394+ ) -> str :
395+ expected_versions = dict (correction .expected_versions )
375396 used_by = []
376397 for crate_name in correction .crates :
377- package = package_by_name (metadata , crate_name )
378- repository = package .get ("repository" ) or (
379- f"https://crates.io/crates/{ crate_name } "
380- )
381- used_by .append (
382- f' <li><a href="{ html .escape (repository , quote = True )} ">'
383- f"{ html .escape (crate_name )} { html .escape (package ['version' ])} </a></li>"
384- )
398+ packages = packages_by_name (metadata , crate_name )
399+ if not packages :
400+ raise RuntimeError (f"expected at least one resolved { crate_name } package" )
401+ for package in packages :
402+ if correction .license_from_repository :
403+ expected_version = expected_versions .get (crate_name )
404+ if expected_version is None :
405+ raise RuntimeError (
406+ "repository-backed correction is missing an expected "
407+ f"version for { crate_name } "
408+ )
409+ if package ["version" ] != expected_version :
410+ raise RuntimeError (
411+ f"expected { crate_name } { expected_version } , "
412+ f"found { package ['version' ]} "
413+ )
414+ repository = package .get ("repository" ) or (
415+ f"https://crates.io/crates/{ crate_name } "
416+ )
417+ used_by .append (
418+ f' <li><a href="{ html .escape (repository , quote = True )} ">'
419+ f"{ html .escape (crate_name )} { html .escape (package ['version' ])} </a></li>"
420+ )
385421
386- license_package = package_by_name (metadata , correction .license_crate )
387- license_file = (
388- Path (license_package ["manifest_path" ]).parent / correction .license_path
389- )
390- if not license_file .is_file ():
391- raise RuntimeError (f"corrected license file is missing: { license_file } " )
392- license_text = license_file .read_text (encoding = "utf-8" )
422+ if correction .license_from_repository :
423+ license_file = root / correction .license_path
424+ if not license_file .is_file ():
425+ raise RuntimeError (f"corrected license file is missing: { license_file } " )
426+ license_text = license_file .read_text (encoding = "utf-8" )
427+ else :
428+ license_files = [
429+ Path (package ["manifest_path" ]).parent / correction .license_path
430+ for package in packages_by_name (metadata , correction .license_crate )
431+ ]
432+ if not license_files :
433+ raise RuntimeError (
434+ f"expected at least one resolved { correction .license_crate } package"
435+ )
436+ missing = [path for path in license_files if not path .is_file ()]
437+ if missing :
438+ raise RuntimeError (f"corrected license file is missing: { missing [0 ]} " )
439+ license_texts = {
440+ path .read_text (encoding = "utf-8" ) for path in license_files
441+ }
442+ if len (license_texts ) != 1 :
443+ raise RuntimeError (
444+ f"corrected license files differ for { correction .license_crate } "
445+ )
446+ license_text = license_texts .pop ()
393447
394448 return "\n " .join (
395449 [
@@ -407,6 +461,7 @@ def correction_html(metadata: dict, correction: LicenseCorrection) -> str:
407461
408462
409463def replace_placeholder_entry (
464+ root : Path ,
410465 report_text : str ,
411466 metadata : dict ,
412467 marker : str ,
@@ -441,7 +496,7 @@ def replace_placeholder_entry(
441496 )
442497
443498 replacement = "\n " .join (
444- correction_html (metadata , correction ) for correction in corrections
499+ correction_html (root , metadata , correction ) for correction in corrections
445500 )
446501 return report_text [:entry_start ] + replacement + report_text [entry_end :]
447502
@@ -538,13 +593,13 @@ def complete_report(
538593 root : Path , base_report : str , report : Report , metadata : dict
539594) -> str :
540595 result = replace_placeholder_entry (
541- base_report , metadata , ALLOC_PLACEHOLDER , ALLOC_CORRECTIONS
596+ root , base_report , metadata , ALLOC_PLACEHOLDER , ALLOC_CORRECTIONS
542597 )
543598 mit_corrections = COMMON_MIT_CORRECTIONS
544599 if report .component == "python" :
545600 mit_corrections += PYTHON_MIT_CORRECTIONS
546601 result = replace_placeholder_entry (
547- result , metadata , MIT_PLACEHOLDER , mit_corrections
602+ root , result , metadata , MIT_PLACEHOLDER , mit_corrections
548603 )
549604
550605 for placeholder in LICENSE_PLACEHOLDERS :
0 commit comments