summaryrefslogtreecommitdiffstats
path: root/scripts
diff options
context:
space:
mode:
authorBruce Ashfield <bruce.ashfield@gmail.com>2026-05-29 17:40:22 +0000
committerBruce Ashfield <bruce.ashfield@gmail.com>2026-05-29 17:40:22 +0000
commit2f15273aa5a6bd84e6d20fb9d6503c18401adc1e (patch)
tree3073e70049d08bf5b9c1e4e3598e5b8cf118f531 /scripts
parent07bfa04b7718dd1b2a98c698fb1d1889de885664 (diff)
downloadmeta-virtualization-2f15273aa5a6bd84e6d20fb9d6503c18401adc1e.tar.gz
oe-go-mod-fetcher: encode Go module paths in LIC_FILES_CHKSUM URIs
bitbake's do_populate_lic walks pkg/mod/<path>@<version>/ at build time, where <path> uses Go's filesystem-safe encoding for ASCII uppercase letters: every 'A'-'Z' becomes '!' followed by the lowercase form, so `github.com/HdrHistogram/...` lives at `github.com/!hdr!histogram/...` on disk. The license-scan writer was emitting canonical-form paths ("github.com/HdrHistogram/..."), so do_populate_lic couldn't find the LICENSE file and failed with "invalid file" QA errors for every uppercase-bearing imported module — 6 errors in k3s's case. The same canonical-vs-encoded gap also lived in the filter input. _get_unpacked_modules walks GOMODCACHE and returned the encoded form, while modules.json (the loop driver in scan_module_licenses) uses the canonical form, so the `key in selected_set` test for uppercase modules silently failed — those modules were dropped from the filter output entirely. That's why cosign/docker-compose/incus's committed go-mod-licenses.inc files don't have any uppercase entries: the build passed do_populate_lic because the entries were absent, not because they were correct. Normalize to canonical form internally and encode only at the output boundary: - _decode_go_module_path: '!x' -> 'X' for ASCII lowercase x. Used by _get_unpacked_modules so all three filter sources return canonical-keyed sets that compare cleanly against modules.json. - _encode_go_module_path: 'X' -> '!x'. Used by write_license_inc when constructing the file:// URI's pkg/mod/<path> portion. - Version (the @v... suffix) is not encoded — Go doesn't apply EscapeVersion to the on-disk layout in this position. Backwards-compatible for recipes whose imported modules happen to be all-lowercase (cosign's committed sidecars are unchanged on a real regen). For recipes whose imports include uppercase paths (k3s, likely others), regeneration is needed to fill in the previously- dropped entries; do_populate_lic will pass cleanly thereafter. Signed-off-by: Bruce Ashfield <bruce.ashfield@gmail.com>
Diffstat (limited to 'scripts')
-rwxr-xr-xscripts/oe-go-mod-fetcher.py74
1 files changed, 64 insertions, 10 deletions
diff --git a/scripts/oe-go-mod-fetcher.py b/scripts/oe-go-mod-fetcher.py
index b098a601..74459204 100755
--- a/scripts/oe-go-mod-fetcher.py
+++ b/scripts/oe-go-mod-fetcher.py
@@ -4731,22 +4731,41 @@ def _get_imported_modules(source_dir: Optional[Path],
4731 return selected if selected else None 4731 return selected if selected else None
4732 4732
4733 4733
4734def _decode_go_module_path(encoded: str) -> str:
4735 """Inverse of _encode_go_module_path: '!x' (where x is a lowercase
4736 letter) → 'X'. Other characters pass through unchanged."""
4737 result = []
4738 i = 0
4739 while i < len(encoded):
4740 if (encoded[i] == '!' and i + 1 < len(encoded)
4741 and 'a' <= encoded[i + 1] <= 'z'):
4742 result.append(encoded[i + 1].upper())
4743 i += 2
4744 else:
4745 result.append(encoded[i])
4746 i += 1
4747 return ''.join(result)
4748
4749
4734def _get_unpacked_modules(gomodcache: Optional[str] = None 4750def _get_unpacked_modules(gomodcache: Optional[str] = None
4735 ) -> Optional[Set[str]]: 4751 ) -> Optional[Set[str]]:
4736 """ 4752 """
4737 Walk GOMODCACHE for unpacked module dirs and return them as the set of 4753 Walk GOMODCACHE for unpacked module dirs and return them as the set of
4738 "module_path@version" strings (in Go's filesystem encoding, which for 4754 "module_path@version" strings in **canonical** form (with uppercase
4739 all-lowercase module paths is identical to the canonical form). 4755 letters in the path, matching what `go list` produces and what
4756 modules.json contains).
4757
4758 The on-disk layout uses Go's filesystem-safe encoding ('A-Z' → '!a-z')
4759 so paths like `github.com/HdrHistogram/...` are stored at
4760 `github.com/!hdr!histogram/...`. We decode back to canonical on read so
4761 set membership against canonical-keyed modules.json entries works
4762 uniformly. write_license_inc re-encodes when constructing the
4763 filesystem path for LIC_FILES_CHKSUM.
4740 4764
4741 This is the most accurate signal for "what bitbake will unpack at build 4765 This is the most accurate signal for "what bitbake will unpack at build
4742 time" — it's exactly what `go build` populated for the discovery step, 4766 time" — it's exactly what `go build` populated for the discovery step,
4743 and the recipe's SRC_URI entries will re-unpack the same set at build 4767 and the recipe's SRC_URI entries will re-unpack the same set at build
4744 time. Returns None if gomodcache is not set or empty. 4768 time. Returns None if gomodcache is not set or empty.
4745
4746 Why this beats `go list -m all`: the MVS-selected set includes test- and
4747 tool-only deps that never reach `go build`'s import graph, so they get
4748 listed in go.sum but never unpacked under pkg/mod/<module>@<version>/.
4749 Walking the already-populated cache skips those entirely.
4750 """ 4769 """
4751 cache = gomodcache or os.environ.get('GOMODCACHE') 4770 cache = gomodcache or os.environ.get('GOMODCACHE')
4752 if not cache or not os.path.isdir(cache): 4771 if not cache or not os.path.isdir(cache):
@@ -4761,8 +4780,16 @@ def _get_unpacked_modules(gomodcache: Optional[str] = None
4761 for d in list(dirs): 4780 for d in list(dirs):
4762 if '@v' in d: 4781 if '@v' in d:
4763 full = Path(root) / d 4782 full = Path(root) / d
4764 rel = full.relative_to(base) 4783 rel = str(full.relative_to(base))
4765 selected.add(str(rel)) 4784 # Decode FS-encoded path back to canonical. Only the
4785 # path-before-@version uses the encoding; the version
4786 # itself is left alone.
4787 if '@' in rel:
4788 mpath, _, mver = rel.partition('@')
4789 rel = f"{_decode_go_module_path(mpath)}@{mver}"
4790 else:
4791 rel = _decode_go_module_path(rel)
4792 selected.add(rel)
4766 dirs.remove(d) # don't recurse into the module dir 4793 dirs.remove(d) # don't recurse into the module dir
4767 return selected if selected else None 4794 return selected if selected else None
4768 4795
@@ -4894,6 +4921,25 @@ def _tidy_licenses(licenses: Set[str]) -> List[str]:
4894 return sorted(licenses - {'Unknown'}, key=str.casefold) 4921 return sorted(licenses - {'Unknown'}, key=str.casefold)
4895 4922
4896 4923
4924def _encode_go_module_path(module_path: str) -> str:
4925 """
4926 Encode a Go module path the way Go's module cache stores it on disk.
4927
4928 Go's `golang.org/x/mod/module.EscapePath` replaces every ASCII uppercase
4929 letter with '!' followed by its lowercase counterpart, to avoid clashes
4930 on case-insensitive filesystems:
4931
4932 github.com/HdrHistogram/hdrhistogram-go
4933 → github.com/!hdr!histogram/hdrhistogram-go
4934
4935 bitbake's do_populate_lic walks pkg/mod/<encoded-path>@<version>/ at
4936 build time, so LIC_FILES_CHKSUM entries must use the encoded form or
4937 they fail the "invalid file" QA check.
4938 """
4939 return ''.join('!' + c.lower() if 'A' <= c <= 'Z' else c
4940 for c in module_path)
4941
4942
4897def write_license_inc(output_dir: Path, license_results: Dict[str, Tuple[str, str, str]]) -> Path: 4943def write_license_inc(output_dir: Path, license_results: Dict[str, Tuple[str, str, str]]) -> Path:
4898 """ 4944 """
4899 Write go-mod-licenses.inc with LICENSE and LIC_FILES_CHKSUM. 4945 Write go-mod-licenses.inc with LICENSE and LIC_FILES_CHKSUM.
@@ -4912,8 +4958,16 @@ def write_license_inc(output_dir: Path, license_results: Dict[str, Tuple[str, st
4912 4958
4913 for mod_ver, (spdx, lic_file, md5) in license_results.items(): 4959 for mod_ver, (spdx, lic_file, md5) in license_results.items():
4914 licenses.add(spdx) 4960 licenses.add(spdx)
4961 # Encode the module-path portion of mod_ver — bitbake unpacks Go
4962 # modules using Go's filesystem-safe encoding (uppercase → !lower).
4963 # Version (after @) is left unchanged.
4964 if '@' in mod_ver:
4965 mpath, _, mver = mod_ver.partition('@')
4966 encoded_mod_ver = f"{_encode_go_module_path(mpath)}@{mver}"
4967 else:
4968 encoded_mod_ver = _encode_go_module_path(mod_ver)
4915 # Build path matching OE-core format: pkg/mod/<module@version>/<file> 4969 # Build path matching OE-core format: pkg/mod/<module@version>/<file>
4916 lic_path = f"pkg/mod/{mod_ver}/{lic_file}" 4970 lic_path = f"pkg/mod/{encoded_mod_ver}/{lic_file}"
4917 encoded_spdx = urllib.parse.quote_plus(spdx) 4971 encoded_spdx = urllib.parse.quote_plus(spdx)
4918 lic_entries.append(f"file://{lic_path};md5={md5};spdx={encoded_spdx}") 4972 lic_entries.append(f"file://{lic_path};md5={md5};spdx={encoded_spdx}")
4919 4973