summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorBruce Ashfield <bruce.ashfield@gmail.com>2026-05-29 13:37:32 +0000
committerBruce Ashfield <bruce.ashfield@gmail.com>2026-05-29 13:37:32 +0000
commit07bfa04b7718dd1b2a98c698fb1d1889de885664 (patch)
tree61a6634d0a0aa663ec19c373fc146d6cd557d5a2
parent18b5550b69e47883c71d6060a05e0ad69c8e4fa1 (diff)
downloadmeta-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>
-rw-r--r--classes/go-mod-discovery.bbclass21
-rwxr-xr-xscripts/oe-go-mod-fetcher.py106
2 files changed, 119 insertions, 8 deletions
diff --git a/classes/go-mod-discovery.bbclass b/classes/go-mod-discovery.bbclass
index 9609ee35..3d58811c 100644
--- a/classes/go-mod-discovery.bbclass
+++ b/classes/go-mod-discovery.bbclass
@@ -79,6 +79,18 @@
79# Default: "${FILE_DIRNAME}" (recipe's directory) 79# Default: "${FILE_DIRNAME}" (recipe's directory)
80# 80#
81# GO_MOD_DISCOVERY_SKIP_LICENSES - Set to "1" to skip license scanning 81# GO_MOD_DISCOVERY_SKIP_LICENSES - Set to "1" to skip license scanning
82#
83# GO_MOD_DISCOVERY_LICENSE_TARGETS - Space-separated list of go build
84# targets used by the recipe's
85# do_compile. When set, the license
86# scan filter is derived from
87# `go list -deps` on these targets
88# (the most accurate filter). Useful
89# when do_compile builds multiple
90# binaries with a wider import graph
91# than the discovery's single
92# BUILD_TARGET. Falls back to the
93# GOMODCACHE walk when unset.
82# Default: "" (scan enabled) 94# Default: "" (scan enabled)
83# 95#
84# WORKFLOW EXAMPLES: 96# WORKFLOW EXAMPLES:
@@ -421,6 +433,15 @@ Or run 'bitbake ${PN} -c show_upgrade_commands' to see manual options."
421 LICENSE_FLAGS="" 433 LICENSE_FLAGS=""
422 if [ "${GO_MOD_DISCOVERY_SKIP_LICENSES}" != "1" ]; then 434 if [ "${GO_MOD_DISCOVERY_SKIP_LICENSES}" != "1" ]; then
423 LICENSE_FLAGS="--scan-licenses --common-license-dir ${COMMON_LICENSE_DIR} --discovery-cache ${GO_MOD_DISCOVERY_DIR}/cache" 435 LICENSE_FLAGS="--scan-licenses --common-license-dir ${COMMON_LICENSE_DIR} --discovery-cache ${GO_MOD_DISCOVERY_DIR}/cache"
436 # If the recipe declares its full set of build targets (used by its
437 # own do_compile), pass each one through so the license scan can
438 # scope the filter via `go list -deps` rather than the discovery's
439 # single BUILD_TARGET-derived GOMODCACHE walk. Use repeated
440 # --build-target flags so the values survive shell word-splitting
441 # without quoting tricks.
442 for target in ${GO_MOD_DISCOVERY_LICENSE_TARGETS}; do
443 LICENSE_FLAGS="${LICENSE_FLAGS} --build-target ${target}"
444 done
424 fi 445 fi
425 446
426 python3 "${FETCHER_SCRIPT}" \ 447 python3 "${FETCHER_SCRIPT}" \
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
4676def _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
4656def _get_unpacked_modules(gomodcache: Optional[str] = None 4734def _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"