diff options
| author | Bruce Ashfield <bruce.ashfield@gmail.com> | 2026-05-29 13:37:32 +0000 |
|---|---|---|
| committer | Bruce Ashfield <bruce.ashfield@gmail.com> | 2026-05-29 13:37:32 +0000 |
| commit | 07bfa04b7718dd1b2a98c698fb1d1889de885664 (patch) | |
| tree | 61a6634d0a0aa663ec19c373fc146d6cd557d5a2 /scripts | |
| parent | 18b5550b69e47883c71d6060a05e0ad69c8e4fa1 (diff) | |
| download | meta-virtualization-07bfa04b7718dd1b2a98c698fb1d1889de885664.tar.gz | |
oe-go-mod-fetcher + go-mod-discovery: add --build-target license-scan filter
The license-scan filter previously had two tiers — walk GOMODCACHE for
unpacked module dirs, falling back to `go list -m all` (the MVS set).
For recipes whose do_compile builds multiple binaries spanning a wider
import graph than the discovery step's single GO_MOD_DISCOVERY_BUILD_TARGET
(incus: 8 cmd/* binaries vs the single ./cmd/incus-migrate used by
discovery), the GOMODCACHE walk under-includes and the MVS fallback
over-includes — producing go-mod-licenses.inc entries that point to
modules bitbake never unpacks, which then trip do_populate_lic's
"invalid file" QA gate.
Add a tier-0 filter: when a recipe declares its full set of build
targets via the new GO_MOD_DISCOVERY_LICENSE_TARGETS variable, the
generator runs `go list -deps` on those targets and uses the union
of their imported modules as the filter set. This is the most accurate
signal for "what bitbake will unpack at build time" — equivalent to
what go build would resolve before bitbake's do_unpack runs.
Mechanism:
- oe-go-mod-fetcher.py: new --build-target flag (repeatable) and
_get_imported_modules() helper. Repeats over `action='append'`
so the per-target paths survive shell word-splitting in the bbclass
invocation without quoting tricks.
- go-mod-discovery.bbclass: when GO_MOD_DISCOVERY_LICENSE_TARGETS is
set, loop over its values and emit --build-target for each. The
list is space-separated in the recipe; the bbclass loop keeps
each target as its own shell word.
Tier order is now:
1. --build-target (this commit): `go list -deps` on declared targets
2. GOMODCACHE walk (existing): modules unpacked by the discovery build
3. `go list -m all` MVS-selected set (existing): full module graph
4. No filter (existing): all go.sum entries
Recipes without GO_MOD_DISCOVERY_LICENSE_TARGETS see no behavior change —
verified against cosign via a real bitbake -c discover_and_generate run
(zero diff vs HEAD on its sidecars). Incus's go-mod-licenses.inc drops
from 418 (MVS fallback) to 176 lines once the new tier is wired up,
matching the build's actual ~170 unpacked modules.
Signed-off-by: Bruce Ashfield <bruce.ashfield@gmail.com>
Diffstat (limited to 'scripts')
| -rwxr-xr-x | scripts/oe-go-mod-fetcher.py | 106 |
1 files changed, 98 insertions, 8 deletions
diff --git a/scripts/oe-go-mod-fetcher.py b/scripts/oe-go-mod-fetcher.py index 1f2fba19..b098a601 100755 --- a/scripts/oe-go-mod-fetcher.py +++ b/scripts/oe-go-mod-fetcher.py | |||
| @@ -2125,19 +2125,39 @@ 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 | # Preferred filter: modules already unpacked under | 2128 | # Filter tier order (most accurate first): |
| 2129 | # GOMODCACHE by the discovery build (`go build`). This | 2129 | # 1. --build-targets: `go list -deps` on the recipe's |
| 2130 | # matches exactly what bitbake will unpack at build time. | 2130 | # actual build targets. Needed when do_compile builds |
| 2131 | # Fallback: `go list -m all` (MVS-selected set) — includes | 2131 | # multiple binaries spanning a wider import graph |
| 2132 | # test/tool deps that won't be unpacked, but still | 2132 | # than the discovery's single BUILD_TARGET. |
| 2133 | # dramatically smaller than the go.sum entry list. | 2133 | # 2. GOMODCACHE walk: modules unpacked by `go build` of |
| 2134 | selected_set = _get_unpacked_modules( | 2134 | # the discovery's BUILD_TARGET. Right when the |
| 2135 | # discovery target's import set matches the recipe's. | ||
| 2136 | # 3. `go list -m all` (MVS-selected): full module graph | ||
| 2137 | # including test/tool deps. Too wide for some recipes | ||
| 2138 | # but better than no filter. | ||
| 2139 | selected_set = _get_imported_modules( | ||
| 2140 | source_dir=Path.cwd(), | ||
| 2141 | build_targets=args.build_targets, | ||
| 2135 | gomodcache=args.gomodcache, | 2142 | gomodcache=args.gomodcache, |
| 2136 | ) | 2143 | ) |
| 2137 | if selected_set is not None: | 2144 | if selected_set is not None: |
| 2138 | print(f" Filtering to GOMODCACHE-unpacked set: " | 2145 | print(f" Filtering to `go list -deps` set " |
| 2146 | f"(from --build-targets): " | ||
| 2139 | f"{len(selected_set)} modules") | 2147 | f"{len(selected_set)} modules") |
| 2140 | else: | 2148 | else: |
| 2149 | selected_set = _get_unpacked_modules( | ||
| 2150 | gomodcache=args.gomodcache, | ||
| 2151 | ) | ||
| 2152 | if selected_set is None: | ||
| 2153 | # both build-targets and GOMODCACHE walk gave nothing | ||
| 2154 | pass # fall through to MVS | ||
| 2155 | elif args.build_targets is None: | ||
| 2156 | # selected_set is from GOMODCACHE walk | ||
| 2157 | print(f" Filtering to GOMODCACHE-unpacked set: " | ||
| 2158 | f"{len(selected_set)} modules") | ||
| 2159 | |||
| 2160 | if selected_set is None: | ||
| 2141 | selected_set = _get_mvs_selected_modules( | 2161 | selected_set = _get_mvs_selected_modules( |
| 2142 | source_dir=Path.cwd(), | 2162 | source_dir=Path.cwd(), |
| 2143 | gomodcache=args.gomodcache, | 2163 | gomodcache=args.gomodcache, |
| @@ -4653,6 +4673,64 @@ def _find_module_zip(discovery_cache: str, module_path: str, version: str) -> Op | |||
| 4653 | return None | 4673 | return None |
| 4654 | 4674 | ||
| 4655 | 4675 | ||
| 4676 | def _get_imported_modules(source_dir: Optional[Path], | ||
| 4677 | build_targets: Optional[List[str]], | ||
| 4678 | gomodcache: Optional[str] = None | ||
| 4679 | ) -> Optional[Set[str]]: | ||
| 4680 | """ | ||
| 4681 | Run `go list -deps` on the given build targets and return the set of | ||
| 4682 | "module_path@version" strings that those targets actually import. | ||
| 4683 | |||
| 4684 | This is the most accurate filter when the recipe builds multiple binaries | ||
| 4685 | that span a wider import graph than the discovery step's single | ||
| 4686 | BUILD_TARGET. The discovery's GOMODCACHE walk only sees what | ||
| 4687 | `go build <BUILD_TARGET>` populated, which can be a strict subset of | ||
| 4688 | what the recipe's `do_compile` will actually unpack at build time. | ||
| 4689 | |||
| 4690 | Returns None if `go` is unavailable, source dir has no go.mod, | ||
| 4691 | build_targets is empty, or `go list` fails. | ||
| 4692 | """ | ||
| 4693 | if not build_targets: | ||
| 4694 | return None | ||
| 4695 | src = Path(source_dir or '.').resolve() | ||
| 4696 | if not (src / 'go.mod').exists(): | ||
| 4697 | return None | ||
| 4698 | |||
| 4699 | env = os.environ.copy() | ||
| 4700 | env['GOFLAGS'] = (env.get('GOFLAGS', '') + ' -mod=mod').strip() | ||
| 4701 | # Same rationale as _get_mvs_selected_modules: do_generate_modules sets | ||
| 4702 | # GOPROXY=off, but we need it on so go-list can resolve metadata and | ||
| 4703 | # download a newer toolchain if go.mod requires it. | ||
| 4704 | env['GOPROXY'] = env.get('GOPROXY_REAL', | ||
| 4705 | 'https://proxy.golang.org,direct') | ||
| 4706 | env.pop('GOTOOLCHAIN', None) | ||
| 4707 | if gomodcache: | ||
| 4708 | env['GOMODCACHE'] = gomodcache | ||
| 4709 | |||
| 4710 | cmd = ['go', 'list', '-deps', | ||
| 4711 | '-f', '{{if .Module}}{{.Module.Path}}@{{.Module.Version}}{{end}}'] | ||
| 4712 | cmd.extend(build_targets) | ||
| 4713 | try: | ||
| 4714 | result = subprocess.run(cmd, cwd=str(src), env=env, | ||
| 4715 | capture_output=True, text=True, | ||
| 4716 | timeout=300, check=False) | ||
| 4717 | except (FileNotFoundError, subprocess.TimeoutExpired) as exc: | ||
| 4718 | print(f" Note: 'go list -deps' could not be executed: {exc}") | ||
| 4719 | return None | ||
| 4720 | if result.returncode != 0: | ||
| 4721 | print(f" Note: 'go list -deps' exited {result.returncode}:") | ||
| 4722 | for line in (result.stderr or '').strip().splitlines()[:5]: | ||
| 4723 | print(f" {line}") | ||
| 4724 | return None | ||
| 4725 | |||
| 4726 | selected: Set[str] = set() | ||
| 4727 | for line in result.stdout.strip().splitlines(): | ||
| 4728 | line = line.strip() | ||
| 4729 | if line: | ||
| 4730 | selected.add(line) | ||
| 4731 | return selected if selected else None | ||
| 4732 | |||
| 4733 | |||
| 4656 | def _get_unpacked_modules(gomodcache: Optional[str] = None | 4734 | def _get_unpacked_modules(gomodcache: Optional[str] = None |
| 4657 | ) -> Optional[Set[str]]: | 4735 | ) -> Optional[Set[str]]: |
| 4658 | """ | 4736 | """ |
| @@ -4993,6 +5071,18 @@ Examples: | |||
| 4993 | ) | 5071 | ) |
| 4994 | 5072 | ||
| 4995 | parser.add_argument( | 5073 | parser.add_argument( |
| 5074 | "--build-target", | ||
| 5075 | action='append', | ||
| 5076 | default=None, | ||
| 5077 | dest='build_targets', | ||
| 5078 | help="Go build target used by the recipe's do_compile (e.g. './cmd/foo'). " | ||
| 5079 | "Can be repeated. When set, the license scan filter is derived from " | ||
| 5080 | "`go list -deps` on these targets — the most accurate signal for " | ||
| 5081 | "'what bitbake will unpack at build time'. Takes precedence over the " | ||
| 5082 | "GOMODCACHE walk." | ||
| 5083 | ) | ||
| 5084 | |||
| 5085 | parser.add_argument( | ||
| 4996 | "--validate", | 5086 | "--validate", |
| 4997 | action="store_true", | 5087 | action="store_true", |
| 4998 | help="Validate module commits without emitting recipe files" | 5088 | help="Validate module commits without emitting recipe files" |
