From 674512e84760e5d59f3cc87fd0f53b7973e1cd8e Mon Sep 17 00:00:00 2001 From: Arjan Topolovec Date: Mon, 4 May 2026 14:03:17 +0200 Subject: [PATCH] Emit package_metadata. --- MODULE.bazel | 1 + apko/extensions.bzl | 9 +++++- apko/private/apk.bzl | 32 ++++++++++++++++++- apko/private/util.bzl | 27 ++++++++++++++++ apko/tests/BUILD.bazel | 3 ++ apko/tests/util_test.bzl | 66 ++++++++++++++++++++++++++++++++++++++++ apko/translate_lock.bzl | 6 ++++ 7 files changed, 142 insertions(+), 2 deletions(-) create mode 100644 apko/tests/util_test.bzl diff --git a/MODULE.bazel b/MODULE.bazel index 904a7139..363a6408 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -8,6 +8,7 @@ module( bazel_dep(name = "bazel_skylib", version = "1.7.1") bazel_dep(name = "platforms", version = "1.0.0") bazel_dep(name = "aspect_bazel_lib", version = "2.19.4") +bazel_dep(name = "package_metadata", version = "0.0.10") bazel_dep(name = "rules_pkg", version = "1.1.0", dev_dependency = True) bazel_dep(name = "rules_oci", version = "2.2.6", dev_dependency = True) diff --git a/apko/extensions.bzl b/apko/extensions.bzl index d19e8174..b0f978ee 100644 --- a/apko/extensions.bzl +++ b/apko/extensions.bzl @@ -28,6 +28,12 @@ Overriding the default is only permitted in the root module. apko_translate_lock = tag_class(attrs = { "name": attr.string(mandatory = True), "lock": attr.label(mandatory = True), + "vendors": attr.string_dict( + default = {}, + doc = """Extra ``host -> PURL vendor`` entries for the PURL namespace +(e.g. ``{"mirror.corp.example": "corp"}``). Layered on top of the built-in +defaults. Unknown hosts fall back to ``alpine``.""", + ), }) def _apko_extension_impl(module_ctx): @@ -68,9 +74,10 @@ def _apko_extension_impl(module_ctx): control_checksum = package["control"]["checksum"], data_range = package["data"]["range"], data_checksum = package["data"]["checksum"], + vendors = lock.vendors, ) - translate_apko_lock(name = lock.name, target_name = lock.name, lock = lock.lock) + translate_apko_lock(name = lock.name, target_name = lock.name, lock = lock.lock, vendors = lock.vendors) if mod.is_root: deps = root_direct_dev_deps if module_ctx.is_dev_dependency(lock) else root_direct_deps diff --git a/apko/private/apk.bzl b/apko/private/apk.bzl index 37c4e848..99f4848e 100644 --- a/apko/private/apk.bzl +++ b/apko/private/apk.bzl @@ -1,10 +1,19 @@ "Repository rules for importing remote apk packages" load("@bazel_skylib//lib:versions.bzl", "versions") +load("@package_metadata//purl:purl.bzl", "purl") load(":util.bzl", "util") APK_IMPORT_TMPL = """\ # Generated by apk_import. DO NOT EDIT +load("@package_metadata//rules:package_metadata.bzl", "package_metadata") + +package_metadata( + name = "package_metadata", + purl = "{purl}", + visibility = ["//visibility:public"], +) + filegroup( name = "all", srcs = glob( @@ -127,7 +136,24 @@ def _apk_import_impl(rctx): control = control_output, data = data_output, ) - rctx.file("BUILD.bazel", APK_IMPORT_TMPL) + + # PURL spec: https://github.com/package-url/purl-spec/blob/main/types-doc/apk-definition.md + purl_string = ( + purl.builder() + .type("apk") + .namespace(util.apk_namespace(rctx.attr.url, rctx.attr.vendors)) + .name(rctx.attr.package_name) + .version(rctx.attr.version) + .add_qualifier("arch", rctx.attr.architecture) + .add_qualifier("checksum", "sha256:" + data_sha256) + .add_qualifier("repository_url", util.repo_url(rctx.attr.url, rctx.attr.architecture)) + .build() + ) + rctx.file("BUILD.bazel", APK_IMPORT_TMPL.format(purl = purl_string)) + rctx.file( + "REPO.bazel", + "repo(default_package_metadata = [\"//:package_metadata\"])\n", + ) apk_import = repository_rule( implementation = _apk_import_impl, @@ -142,6 +168,10 @@ apk_import = repository_rule( "control_checksum": attr.string(mandatory = True), "data_range": attr.string(mandatory = True), "data_checksum": attr.string(mandatory = True), + "vendors": attr.string_dict( + default = {}, + doc = "Additional host -> PURL vendor namespace mappings, overlaid on top of util.DEFAULT_APK_VENDORS.", + ), }, ) diff --git a/apko/private/util.bzl b/apko/private/util.bzl index 03ff8e20..5b1d787b 100644 --- a/apko/private/util.bzl +++ b/apko/private/util.bzl @@ -120,6 +120,32 @@ def _concatenate_gzip_segments(rctx, output, signature, control, data): if r.return_code != 0: fail("""concatenate_gzip_segments failed.\nstderr: {}\nstdout: {}""".format(r.stdout, r.stderr)) +# Host -> PURL apk namespace. The PURL apk type spec requires a vendor +# namespace (https://github.com/package-url/purl-spec/blob/main/types-doc/apk-definition.md). +# Unknown hosts fall back to "alpine" since apk is Alpine's package format. +# Users can extend this map via the `vendors` attr on `apk.translate_lock(...)`. +DEFAULT_APK_VENDORS = { + "packages.wolfi.dev": "wolfi", + "packages.cgr.dev": "chainguard", + "dl-cdn.alpinelinux.org": "alpine", +} + +def _apk_namespace(url, overrides = None): + """Derive the PURL apk namespace (vendor) from a package download URL. + + Args: + url: the package download URL (e.g. ``https://packages.wolfi.dev/os/...``). + overrides: optional ``dict[str, str]`` of host → vendor pairs that is + layered on top of ``DEFAULT_APK_VENDORS``. User entries win on + collision. + """ + after_scheme = url.split("://", 1)[-1] + host = after_scheme.split("/", 1)[0] + vendors = dict(DEFAULT_APK_VENDORS) + if overrides: + vendors.update(overrides) + return vendors.get(host, "alpine") + util = struct( concatenate_gzip_segments = _concatenate_gzip_segments, normalize_sri = _normalize_sri, @@ -127,4 +153,5 @@ util = struct( sanitize_string = _sanitize_string, repo_url = _repo_url, url_escape = _url_escape, + apk_namespace = _apk_namespace, ) diff --git a/apko/tests/BUILD.bazel b/apko/tests/BUILD.bazel index 93eae3c5..d8b26c78 100644 --- a/apko/tests/BUILD.bazel +++ b/apko/tests/BUILD.bazel @@ -1,3 +1,6 @@ +load(":util_test.bzl", "util_test_suite") load(":versions_test.bzl", "versions_test_suite") versions_test_suite(name = "versions_test") + +util_test_suite(name = "util_test") diff --git a/apko/tests/util_test.bzl b/apko/tests/util_test.bzl new file mode 100644 index 00000000..38e0fbcb --- /dev/null +++ b/apko/tests/util_test.bzl @@ -0,0 +1,66 @@ +"""Unit tests for `apko/private/util.bzl` helpers.""" + +load("@bazel_skylib//lib:unittest.bzl", "asserts", "unittest") +load("//apko/private:util.bzl", "util") + +def _apk_namespace_known_hosts_test_impl(ctx): + env = unittest.begin(ctx) + asserts.equals( + env, + "wolfi", + util.apk_namespace("https://packages.wolfi.dev/os/x86_64/foo-1.0.apk"), + ) + asserts.equals( + env, + "chainguard", + util.apk_namespace("https://packages.cgr.dev/extras/x86_64/bar-1.0.apk"), + ) + asserts.equals( + env, + "alpine", + util.apk_namespace("https://dl-cdn.alpinelinux.org/alpine/v3.21/main/x86_64/musl-1.2.5-r11.apk"), + ) + return unittest.end(env) + +def _apk_namespace_unknown_host_falls_back_to_alpine_test_impl(ctx): + env = unittest.begin(ctx) + asserts.equals( + env, + "alpine", + util.apk_namespace("https://mirror.example.test/v3.21/main/x86_64/foo-1.0.apk"), + ) + return unittest.end(env) + +def _apk_namespace_override_wins_test_impl(ctx): + env = unittest.begin(ctx) + asserts.equals( + env, + "corp", + util.apk_namespace( + "https://mirror.corp.example/v3.21/main/x86_64/foo-1.0.apk", + {"mirror.corp.example": "corp"}, + ), + ) + + # Override on a known host wins over the built-in default. + asserts.equals( + env, + "custom-wolfi", + util.apk_namespace( + "https://packages.wolfi.dev/os/x86_64/foo-1.0.apk", + {"packages.wolfi.dev": "custom-wolfi"}, + ), + ) + return unittest.end(env) + +_apk_namespace_known_hosts_test = unittest.make(_apk_namespace_known_hosts_test_impl) +_apk_namespace_unknown_host_falls_back_to_alpine_test = unittest.make(_apk_namespace_unknown_host_falls_back_to_alpine_test_impl) +_apk_namespace_override_wins_test = unittest.make(_apk_namespace_override_wins_test_impl) + +def util_test_suite(name): + unittest.suite( + name, + _apk_namespace_known_hosts_test, + _apk_namespace_unknown_host_falls_back_to_alpine_test, + _apk_namespace_override_wins_test, + ) diff --git a/apko/translate_lock.bzl b/apko/translate_lock.bzl index 9fef1a9e..f4c43657 100644 --- a/apko/translate_lock.bzl +++ b/apko/translate_lock.bzl @@ -43,6 +43,7 @@ APK_IMPORT_TMPL = """\ control_checksum = "{control_checksum}", data_range = "{data_range}", data_checksum = "{data_checksum}", + vendors = {vendors}, ) """ @@ -100,6 +101,7 @@ def _translate_apko_lock_impl(rctx): control_checksum = package["control"]["checksum"], data_range = package["data"]["range"], data_checksum = package["data"]["checksum"], + vendors = repr(rctx.attr.vendors), )) for repository in lock_file["contents"]["repositories"]: @@ -126,6 +128,10 @@ translate_apko_lock = repository_rule( attrs = { "lock": attr.label(doc = "label to the `apko.lock.json` file.", mandatory = True), "target_name": attr.string(doc = "internal. do not use!"), + "vendors": attr.string_dict( + default = {}, + doc = "host -> PURL vendor namespace overrides forwarded to each generated apk_import.", + ), }, doc = _DOC, )