summaryrefslogtreecommitdiffstats
path: root/scripts
diff options
context:
space:
mode:
authorBruce Ashfield <bruce.ashfield@gmail.com>2026-05-28 02:23:28 +0000
committerBruce Ashfield <bruce.ashfield@gmail.com>2026-05-28 02:23:28 +0000
commitbaa1fddbbed6bdf392413061c0744c66660eedf4 (patch)
tree52c32aa6d120c581775565ed3b89e5d64b23f697 /scripts
parent63c82fc8dfb89a829cf05ef8666a9d8dab7b4168 (diff)
downloadmeta-virtualization-baa1fddbbed6bdf392413061c0744c66660eedf4.tar.gz
oe-go-mod-fetcher: filter license scan to actually-unpacked modules
The --scan-licenses output (go-mod-licenses.inc) was iterating every module in modules.json, which mirrors go.sum and includes every version Go fetched for hash verification — including unselected indirect-dep versions and test-/tool-only deps. bitbake's do_populate_lic validates each LIC_FILES_CHKSUM entry against an unpacked module dir at pkg/mod/<module>@<version>/. Modules that go.sum lists but go build does not import never get unpacked there, so the QA check fails with "invalid file" errors — hundreds at a time on a busy project like cosign. Add a tiered filter to scan_module_licenses(): 1. Walk GOMODCACHE for *@v* directories. This is exactly the set the discovery step's `go build` populated, and matches 1:1 what bitbake will unpack at build time from the SRC_URI entries we generate. 2. Fall back to `go list -m all` (the MVS-selected set). Smaller than go.sum but larger than the unpacked set; useful when GOMODCACHE isn't yet populated. Set GOPROXY explicitly so the helper can download a newer toolchain if go.mod's directive requires one (do_generate_modules's env has GOPROXY=off). 3. No filter — original behavior, kept for safety. For cosign 3.0.6-tip, this trims go-mod-licenses.inc from 1320 to 265 lines (1068 unselected entries pruned) and resolves all do_populate_lic QA errors. Signed-off-by: Bruce Ashfield <bruce.ashfield@gmail.com>
Diffstat (limited to 'scripts')
-rwxr-xr-xscripts/oe-go-mod-fetcher.py152
1 files changed, 148 insertions, 4 deletions
diff --git a/scripts/oe-go-mod-fetcher.py b/scripts/oe-go-mod-fetcher.py
index c083b156..1f2fba19 100755
--- a/scripts/oe-go-mod-fetcher.py
+++ b/scripts/oe-go-mod-fetcher.py
@@ -2125,7 +2125,36 @@ def _execute(args: argparse.Namespace) -> int:
2125 if disc_cache and os.path.isdir(disc_cache): 2125 if disc_cache and os.path.isdir(disc_cache):
2126 license_db = _resolve_license_db(args.common_license_dir) 2126 license_db = _resolve_license_db(args.common_license_dir)
2127 if license_db: 2127 if license_db:
2128 lic_results = scan_module_licenses(modules, disc_cache, license_db) 2128 # Preferred filter: modules already unpacked under
2129 # GOMODCACHE by the discovery build (`go build`). This
2130 # matches exactly what bitbake will unpack at build time.
2131 # Fallback: `go list -m all` (MVS-selected set) — includes
2132 # test/tool deps that won't be unpacked, but still
2133 # dramatically smaller than the go.sum entry list.
2134 selected_set = _get_unpacked_modules(
2135 gomodcache=args.gomodcache,
2136 )
2137 if selected_set is not None:
2138 print(f" Filtering to GOMODCACHE-unpacked set: "
2139 f"{len(selected_set)} modules")
2140 else:
2141 selected_set = _get_mvs_selected_modules(
2142 source_dir=Path.cwd(),
2143 gomodcache=args.gomodcache,
2144 )
2145 if selected_set is None:
2146 print(" Warning: could not determine selected "
2147 "module set; scanning all modules (may "
2148 "produce stale entries)")
2149 else:
2150 print(f" Filtering to MVS-selected set "
2151 f"(GOMODCACHE empty, fell back to "
2152 f"`go list -m all`): "
2153 f"{len(selected_set)} modules")
2154 lic_results = scan_module_licenses(
2155 modules, disc_cache, license_db,
2156 selected_set=selected_set,
2157 )
2129 if lic_results: 2158 if lic_results:
2130 write_license_inc(output_dir, lic_results) 2159 write_license_inc(output_dir, lic_results)
2131 else: 2160 else:
@@ -4624,11 +4653,115 @@ def _find_module_zip(discovery_cache: str, module_path: str, version: str) -> Op
4624 return None 4653 return None
4625 4654
4626 4655
4656def _get_unpacked_modules(gomodcache: Optional[str] = None
4657 ) -> Optional[Set[str]]:
4658 """
4659 Walk GOMODCACHE for unpacked module dirs and return them as the set of
4660 "module_path@version" strings (in Go's filesystem encoding, which for
4661 all-lowercase module paths is identical to the canonical form).
4662
4663 This is the most accurate signal for "what bitbake will unpack at build
4664 time" — it's exactly what `go build` populated for the discovery step,
4665 and the recipe's SRC_URI entries will re-unpack the same set at build
4666 time. Returns None if gomodcache is not set or empty.
4667
4668 Why this beats `go list -m all`: the MVS-selected set includes test- and
4669 tool-only deps that never reach `go build`'s import graph, so they get
4670 listed in go.sum but never unpacked under pkg/mod/<module>@<version>/.
4671 Walking the already-populated cache skips those entirely.
4672 """
4673 cache = gomodcache or os.environ.get('GOMODCACHE')
4674 if not cache or not os.path.isdir(cache):
4675 return None
4676 base = Path(cache)
4677 selected: Set[str] = set()
4678 # Walk; prune $GOMODCACHE/cache/ which holds zips and metadata, not
4679 # unpacked module trees.
4680 for root, dirs, _files in os.walk(base):
4681 if Path(root) == base and 'cache' in dirs:
4682 dirs.remove('cache')
4683 for d in list(dirs):
4684 if '@v' in d:
4685 full = Path(root) / d
4686 rel = full.relative_to(base)
4687 selected.add(str(rel))
4688 dirs.remove(d) # don't recurse into the module dir
4689 return selected if selected else None
4690
4691
4692def _get_mvs_selected_modules(source_dir: Optional[Path] = None,
4693 gomodcache: Optional[str] = None
4694 ) -> Optional[Set[str]]:
4695 """
4696 Return the set of MVS-selected modules as "module_path@version" strings,
4697 using `go list -m all` from `source_dir` (defaults to CWD).
4698
4699 Returns None if `go` is unavailable, the source dir has no go.mod, or
4700 `go list` fails for any other reason — in which case the caller should
4701 fall back to scanning all modules.
4702
4703 Why this matters: the modules list passed to scan_module_licenses() is
4704 derived from go.sum and includes every module version Go fetched for
4705 hash verification, including unselected indirect-dep versions. Only the
4706 MVS-selected subset is actually unpacked into pkg/mod/<module>@<version>/
4707 at build time. Writing LIC_FILES_CHKSUM entries for unselected versions
4708 causes do_populate_lic to fail with QA errors ("invalid file").
4709 """
4710 src = Path(source_dir or '.').resolve()
4711 if not (src / 'go.mod').exists():
4712 print(f" Note: no go.mod at {src}, cannot determine MVS-selected set")
4713 return None
4714 env = os.environ.copy()
4715 env['GOFLAGS'] = (env.get('GOFLAGS', '') + ' -mod=mod').strip()
4716 # The bitbake do_generate_modules env sets GOPROXY=off, which prevents
4717 # `go list -m all` from resolving transitive metadata and from downloading
4718 # any toolchain version required by go.mod's `toolchain` directive.
4719 # Override here so this helper can do its job — it does not compile or
4720 # install anything, just lists modules.
4721 env['GOPROXY'] = env.get('GOPROXY_REAL',
4722 'https://proxy.golang.org,direct')
4723 env.pop('GOTOOLCHAIN', None) # let it auto-resolve if go.mod needs newer
4724 if gomodcache:
4725 env['GOMODCACHE'] = gomodcache
4726 try:
4727 result = subprocess.run(
4728 ['go', 'list', '-m', 'all'],
4729 cwd=str(src),
4730 env=env,
4731 capture_output=True,
4732 text=True,
4733 timeout=180,
4734 check=False,
4735 )
4736 except (FileNotFoundError, subprocess.TimeoutExpired) as exc:
4737 print(f" Note: 'go list -m all' could not be executed: {exc}")
4738 return None
4739 if result.returncode != 0:
4740 print(f" Note: 'go list -m all' exited {result.returncode}:")
4741 for line in (result.stderr or '').strip().splitlines()[:5]:
4742 print(f" {line}")
4743 return None
4744 selected: Set[str] = set()
4745 for line in result.stdout.strip().splitlines():
4746 parts = line.split()
4747 # Main module is "module/path" with no version — skip
4748 if len(parts) >= 2:
4749 module_path, version = parts[0], parts[1]
4750 selected.add(f"{module_path}@{version}")
4751 return selected
4752
4753
4627def scan_module_licenses(modules: List[Dict], discovery_cache: str, 4754def scan_module_licenses(modules: List[Dict], discovery_cache: str,
4628 license_db: Dict[str, str]) -> Dict[str, Tuple[str, str, str]]: 4755 license_db: Dict[str, str],
4756 selected_set: Optional[Set[str]] = None
4757 ) -> Dict[str, Tuple[str, str, str]]:
4629 """ 4758 """
4630 Scan all modules for licenses using their discovery cache zips. 4759 Scan all modules for licenses using their discovery cache zips.
4631 4760
4761 If `selected_set` is provided, modules whose "module_path@version" is
4762 not in that set are skipped (they appear in go.sum but are not MVS-
4763 selected, so they will not be unpacked at build time).
4764
4632 Returns dict mapping "module_path@version" to (spdx_name, license_file, md5). 4765 Returns dict mapping "module_path@version" to (spdx_name, license_file, md5).
4633 Only the first license file per module (matching OE-core behavior). 4766 Only the first license file per module (matching OE-core behavior).
4634 """ 4767 """
@@ -4636,12 +4769,20 @@ def scan_module_licenses(modules: List[Dict], discovery_cache: str,
4636 scanned = 0 4769 scanned = 0
4637 no_zip = 0 4770 no_zip = 0
4638 unknown = 0 4771 unknown = 0
4772 skipped_unselected = 0
4639 4773
4640 for module in modules: 4774 for module in modules:
4641 module_path = module['module_path'] 4775 module_path = module['module_path']
4642 version = module['version'] 4776 version = module['version']
4643 key = f"{module_path}@{version}" 4777 key = f"{module_path}@{version}"
4644 4778
4779 # Skip modules not selected by Go's MVS — they're in go.sum for hash
4780 # verification but never unpacked into pkg/mod/<module>@<version>/,
4781 # so a LIC_FILES_CHKSUM entry for them would fail do_populate_lic.
4782 if selected_set is not None and key not in selected_set:
4783 skipped_unselected += 1
4784 continue
4785
4645 zip_path = _find_module_zip(discovery_cache, module_path, version) 4786 zip_path = _find_module_zip(discovery_cache, module_path, version)
4646 if not zip_path: 4787 if not zip_path:
4647 no_zip += 1 4788 no_zip += 1
@@ -4657,8 +4798,11 @@ def scan_module_licenses(modules: List[Dict], discovery_cache: str,
4657 unknown += 1 4798 unknown += 1
4658 scanned += 1 4799 scanned += 1
4659 4800
4660 print(f"\n License scan: {len(results)} modules with licenses, " 4801 msg = (f"\n License scan: {len(results)} modules with licenses, "
4661 f"{no_zip} missing zips, {unknown} unknown") 4802 f"{no_zip} missing zips, {unknown} unknown")
4803 if selected_set is not None:
4804 msg += f", {skipped_unselected} skipped (not in selected set)"
4805 print(msg)
4662 return results 4806 return results
4663 4807
4664 4808