diff options
| author | Bruce Ashfield <bruce.ashfield@gmail.com> | 2026-05-29 17:40:22 +0000 |
|---|---|---|
| committer | Bruce Ashfield <bruce.ashfield@gmail.com> | 2026-05-29 17:40:22 +0000 |
| commit | 2f15273aa5a6bd84e6d20fb9d6503c18401adc1e (patch) | |
| tree | 3073e70049d08bf5b9c1e4e3598e5b8cf118f531 /scripts | |
| parent | 07bfa04b7718dd1b2a98c698fb1d1889de885664 (diff) | |
| download | meta-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-x | scripts/oe-go-mod-fetcher.py | 74 |
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 | ||
| 4734 | def _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 | |||
| 4734 | def _get_unpacked_modules(gomodcache: Optional[str] = None | 4750 | def _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 | ||
| 4924 | def _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 | |||
| 4897 | def write_license_inc(output_dir: Path, license_results: Dict[str, Tuple[str, str, str]]) -> Path: | 4943 | def 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 | ||
