diff options
| author | Bruce Ashfield <bruce.ashfield@gmail.com> | 2026-05-28 02:23:28 +0000 |
|---|---|---|
| committer | Bruce Ashfield <bruce.ashfield@gmail.com> | 2026-05-28 02:23:28 +0000 |
| commit | baa1fddbbed6bdf392413061c0744c66660eedf4 (patch) | |
| tree | 52c32aa6d120c581775565ed3b89e5d64b23f697 /scripts | |
| parent | 63c82fc8dfb89a829cf05ef8666a9d8dab7b4168 (diff) | |
| download | meta-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-x | scripts/oe-go-mod-fetcher.py | 152 |
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 | ||
| 4656 | def _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 | |||
| 4692 | def _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 | |||
| 4627 | def scan_module_licenses(modules: List[Dict], discovery_cache: str, | 4754 | def 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 | ||
