diff --git a/apt/extensions.bzl b/apt/extensions.bzl index 0ff1346..c82b0ed 100644 --- a/apt/extensions.bzl +++ b/apt/extensions.bzl @@ -7,7 +7,7 @@ load("//apt/private:deb_filemap.bzl", "deb_filemap") load("//apt/private:deb_import.bzl", "deb_import") load("//apt/private:lockfile.bzl", "lockfile") load("//apt/private:pgp.bzl", "pgp") -load("//apt/private:translate_dependency_set.bzl", "translate_dependency_set") +load("//apt/private:translate_dependency_set.bzl", "check_template_variable_collision", "translate_dependency_set") load("//apt/private:util.bzl", "util") load("//apt/private:version_constraint.bzl", "version_constraint") @@ -316,6 +316,22 @@ def compute_package_repo_modes(packages, roots_by_mode): return modes +def filter_package_templates(package_templates, depset_name): + """Filters package templates applicable to a specific dependency set. + + Args: + package_templates: list of package template dictionaries. + depset_name: name of the dependency set. + + Returns: + A list of package template dictionaries applicable to depset_name. + """ + return [ + pt + for pt in package_templates + if not pt.get("dependency_sets") or depset_name in pt["dependency_sets"] + ] + def _distroless_extension(mctx): # Detect facts API availability use_facts = hasattr(mctx, "facts") @@ -536,16 +552,50 @@ def _distroless_extension(mctx): arch_set = dependency_set["sets"].setdefault(arch, {}) arch_set[pkg_short_key] = package["Version"] + package_templates = [] + for mod in mctx.modules: + for pt in mod.tags.package_template: + if not mod.is_root: + fail("apt.package_template can only be declared by the root module, but was declared in module '{}'.".format(mod.name)) + if pt.template and pt.template_file: + fail("apt.package_template: exactly one of 'template' or 'template_file' must be specified, not both.") + if not pt.template and not pt.template_file: + fail("apt.package_template: either 'template' or 'template_file' must be specified.") + + if not pt.packages: + fail("apt.package_template: 'packages' attribute must not be empty.") + + collision = check_template_variable_collision(pt.additional_variables) + if collision: + fail("apt.package_template: additional variable '{}' conflicts with built-in template variable.".format(collision)) + + for ds in pt.dependency_sets: + if ds not in dependency_sets: + fail("apt.package_template: unknown dependency_set '{}'. Available dependency sets: {}".format( + ds, + sorted(dependency_sets.keys()), + )) + + tmpl = pt.template if pt.template else mctx.read(pt.template_file) + package_templates.append({ + "dependency_sets": pt.dependency_sets, + "packages": pt.packages, + "template": tmpl, + "additional_variables": dict(pt.additional_variables), + }) + # Generate a hub repo for every dependency set lock_content = glock.as_json() package_repo_modes = compute_package_repo_modes(glock.packages(), package_repo_roots) for depset_name in dependency_sets.keys(): depset_mergedusr = dependency_set_mergedusr.get(depset_name, False) + depset_templates = filter_package_templates(package_templates, depset_name) translate_dependency_set( name = depset_name, depset_name = depset_name, lock_content = lock_content, mergedusr = depset_mergedusr, + package_templates = json.encode(depset_templates), ) # Generate a repo per package which will be aliased by hub repo. @@ -790,6 +840,64 @@ lock = tag_class( }, ) +package_template = tag_class( + doc = """Configures a custom BUILD file template for packages matching specific name patterns. + +This tag can only be declared by the root module. Templates are evaluated in declaration order; +the first matching template applies. Place specific package patterns before broader wildcards. + +Target Contract: +The template is rendered into each architecture subpackage (`////BUILD.bazel`). +Custom templates must define the following public targets so the package root's multi-platform aliases and hub repo targets function correctly: + * `:data` (alias or target pointing to `{data_targets}`, with `visibility = ["//visibility:public"]`) + * `:control` (alias or target pointing to `{control_targets}`, with `visibility = ["//visibility:public"]`) + * `:{target_name}` (target representing the package for this architecture, with `visibility = ["//visibility:public"]`, referenced by the root package target `//` and hub repo `:packages` target). While the default template uses a `filegroup(srcs = {deps} + [":data"])`, custom templates may use other rules or omit transitive `{deps}` (e.g. for `include_transitive = False`). + +Template Syntax & Rules: + * Root module references: Because templates render inside external hub repos (`@`), rules or files loaded from the root workspace must use the canonical repository prefix `@@//` (e.g. `load("@@//:custom_rule.bzl", "my_rule")`). + * Brace escaping: Because Python-style `str.format()` is used, literal braces in templates (such as `{}` in comments or Starlark dictionaries) must be escaped by doubling them as `{{` and `}}`. + * Quoting conventions: Built-in label variables (`{data_targets}`, `{control_targets}`, `{src}`) already include double quotes (e.g. `actual = {data_targets}`). String metadata (`{name}`, `{version}`, `{suite}`, `{arch}`, `{target_name}`, `{sha256}`, `{repo_name}`) and `additional_variables` do not (e.g. `package_name = "{name}"`). List variables (`{deps}`, `{urls}`) format as Starlark lists (e.g. `srcs = {deps} + [":data"]`). + +Built-in variables available for formatting: + * `{target_name}`: Target architecture name (e.g. 'amd64'). + * `{name}`: Package name (raw string). + * `{version}`: Package version (raw string). + * `{suite}`: Distribution suite (e.g. 'bookworm') (raw string). + * `{arch}`: Package architecture (raw string). + * `{deps}`: List of direct dependencies formatted as labels. + * `{data_targets}`: Label pointing to the package data archive (quoted). + * `{control_targets}`: Label pointing to the package control archive (quoted). + * `{src}`: Label pointing to the package data archive (quoted, alias for `{data_targets}`). + * `{repo_name}`: Generated repository name for the package (raw string). + * `{urls}`: List of package download URLs. + * `{sha256}`: SHA256 checksum of the package archive (raw string). + +For reference on the standard structure, see the default template at +`//apt/private:package.BUILD.tmpl` (https://github.com/bazel-contrib/rules_distroless/blob/main/apt/private/package.BUILD.tmpl). +""", + attrs = { + "dependency_sets": attr.string_list( + doc = "List of dependency set names this template applies to. If empty, applies to all dependency sets.", + default = [], + ), + "packages": attr.string_list( + doc = "List of package names or glob patterns (e.g. ['nvidia-*', 'libc6', '*']) this template applies to.", + default = ["*"], + ), + "template": attr.string( + doc = "Inline template string for the package BUILD file. Must define ':data', ':control', and ':{target_name}' targets. Literal braces '{' and '}' must be escaped as '{{' and '}}'.", + ), + "template_file": attr.label( + doc = "Template file for the package BUILD file. Must define ':data', ':control', and ':{target_name}' targets. Literal braces '{' and '}' must be escaped as '{{' and '}}'.", + allow_single_file = True, + ), + "additional_variables": attr.string_dict( + doc = "Additional variables to pass into template formatting. Must not conflict with built-in template variables (e.g. 'name', 'version', 'suite', 'arch', 'deps', 'src', 'repo_name', 'target_name', 'data_targets', 'control_targets', 'urls', 'sha256').", + default = {}, + ), + }, +) + apt = module_extension( doc = _doc, implementation = _distroless_extension, @@ -797,5 +905,6 @@ apt = module_extension( "install": install, "sources_list": sources_list, "lock": lock, + "package_template": package_template, }, ) diff --git a/apt/private/translate_dependency_set.bzl b/apt/private/translate_dependency_set.bzl index 46bfe4c..46f4481 100644 --- a/apt/private/translate_dependency_set.bzl +++ b/apt/private/translate_dependency_set.bzl @@ -148,8 +148,56 @@ def package_deps_for_architecture(packages, package, architecture, mergedusr = F if packages[dep_key]["architecture"] in [architecture, "all"] ] +RESERVED_TEMPLATE_VARIABLES = [ + "arch", + "control_targets", + "data_targets", + "deps", + "name", + "repo_name", + "sha256", + "src", + "suite", + "target_name", + "urls", + "version", +] + +def check_template_variable_collision(variables, reserved = RESERVED_TEMPLATE_VARIABLES): + """Checks if any variable name in variables collides with reserved names. + + Args: + variables: dictionary or iterable of variable names. + reserved: list or iterable of reserved variable names. + + Returns: + The first colliding variable name found, or None. + """ + for key in variables: + if key in reserved: + return key + return None + +def resolve_package_template(package_name, package_templates, default_template): + """Resolves the package template and additional variables for a package name. + + Args: + package_name: name of the deb package. + package_templates: list of template dictionaries. + default_template: default template string to use if no pattern matches. + + Returns: + A tuple of (template_string, additional_variables_dict). + """ + for entry in package_templates: + for pattern in entry.get("packages", []): + if util.glob_match(pattern, package_name): + return (entry["template"], entry.get("additional_variables", {})) + return (default_template, {}) + def _translate_dependency_set_impl(rctx): - package_template = rctx.read(rctx.attr.package_template) + default_package_template = rctx.read(rctx.attr.package_template) + package_templates = json.decode(rctx.attr.package_templates) if rctx.attr.package_templates else [] lockf = lockfile.from_json(rctx, rctx.attr.lock_content) packages = lockf.packages() @@ -200,20 +248,37 @@ Please unify the versions manually, or use separate `apt.install` calls (with di ), ).architectures[architecture] = package_key + (tmpl, additional_variables) = resolve_package_template( + package_name, + package_templates, + default_package_template, + ) + + format_vars = { + "target_name": architecture, + "data_targets": '"@%s//:data"' % repo_name, + "control_targets": '"@%s//:control"' % repo_name, + "src": '"@%s//:data"' % repo_name, + "deps": package_deps_for_architecture(packages, package, architecture, mergedusr = rctx.attr.mergedusr), + "urls": package["urls"], + "name": package["name"], + "version": package["version"], + "suite": package["suite"], + "arch": package["architecture"], + "sha256": package["sha256"], + "repo_name": repo_name, + } + collision = check_template_variable_collision(additional_variables, format_vars) + if collision: + fail("apt.package_template: additional variable '{key}' for package '{name}' conflicts with built-in template variable.".format( + key = collision, + name = package_name, + )) + format_vars.update(additional_variables) + rctx.file( "%s/%s/BUILD.bazel" % (package["name"], architecture), - package_template.format( - target_name = architecture, - data_targets = '"@%s//:data"' % repo_name, - control_targets = '"@%s//:control"' % repo_name, - src = '"@%s//:data"' % repo_name, - deps = package_deps_for_architecture(packages, package, architecture, mergedusr = rctx.attr.mergedusr), - urls = package["urls"], - name = package["name"], - arch = package["architecture"], - sha256 = package["sha256"], - repo_name = repo_name, - ), + tmpl.format(**format_vars), ) for (_, info) in packages_to_architectures.items(): @@ -270,5 +335,6 @@ translate_dependency_set = repository_rule( "lock_content": attr.string(doc = "INTERNAL: DO NOT USE"), "mergedusr": attr.bool(default = False, doc = "INTERNAL: Whether package layers were normalized with merged-/usr semantics."), "package_template": attr.label(default = "//apt/private:package.BUILD.tmpl"), + "package_templates": attr.string(default = "[]", doc = "INTERNAL: JSON-encoded list of package template configurations"), }, ) diff --git a/apt/private/util.bzl b/apt/private/util.bzl index ec46714..8248ad9 100644 --- a/apt/private/util.bzl +++ b/apt/private/util.bzl @@ -126,6 +126,29 @@ def _warning(rctx, message): "\033[0;33mWARNING:\033[0m {}".format(message), ], quiet = False) +def _glob_match(pattern, text): + """Matches text against a glob pattern with '*' wildcards.""" + if pattern == "*": + return True + if "*" not in pattern: + return pattern == text + + parts = pattern.split("*") + if not text.startswith(parts[0]): + return False + text = text[len(parts[0]):] + + for i in range(1, len(parts) - 1): + sub = parts[i] + if not sub: + continue + idx = text.find(sub) + if idx == -1: + return False + text = text[idx + len(sub):] + + return text.endswith(parts[-1]) + util = struct( sanitize = _sanitize, package_repo_name = _package_repo_name, @@ -137,4 +160,5 @@ util = struct( index_fact_key = _index_fact_key, prune_uncacheable_facts = _prune_uncacheable_facts, parse_release_file = _parse_release_file, + glob_match = _glob_match, ) diff --git a/apt/tests/BUILD.bazel b/apt/tests/BUILD.bazel index 701a823..5b31f65 100644 --- a/apt/tests/BUILD.bazel +++ b/apt/tests/BUILD.bazel @@ -8,6 +8,7 @@ load(":lockfile_test.bzl", "lockfile_tests") load(":release_test.bzl", "release_tests") load(":resolution_test.bzl", "resolution_tests") load(":translate_dependency_set_test.bzl", "translate_dependency_set_tests") +load(":util_test.bzl", "util_tests") load(":version_test.bzl", "version_tests") version_tests() @@ -31,3 +32,5 @@ release_tests() translate_dependency_set_tests() extensions_tests() + +util_tests() diff --git a/apt/tests/extensions_test.bzl b/apt/tests/extensions_test.bzl index 90be6d3..6b76d14 100644 --- a/apt/tests/extensions_test.bzl +++ b/apt/tests/extensions_test.bzl @@ -1,7 +1,7 @@ "unit tests for apt.install mergedusr scoping" load("@bazel_skylib//lib:unittest.bzl", "asserts", "unittest") -load("//apt:extensions.bzl", "compute_package_repo_modes") +load("//apt:extensions.bzl", "compute_package_repo_modes", "filter_package_templates") _TEST_SUITE_PREFIX = "extensions/" @@ -49,5 +49,43 @@ def _scoped_mergedusr_test(ctx): scoped_mergedusr_test = unittest.make(_scoped_mergedusr_test) +def _filter_package_templates_test(ctx): + env = unittest.begin(ctx) + + templates = [ + { + "dependency_sets": ["trixie_java"], + "packages": ["*"], + "template": "trixie_template", + }, + { + "dependency_sets": [], + "packages": ["*"], + "template": "global_template", + }, + { + "dependency_sets": ["bullseye", "bookworm"], + "packages": ["nginx-*"], + "template": "nginx_template", + }, + ] + + # Matching trixie_java + trixie = filter_package_templates(templates, "trixie_java") + asserts.equals(env, ["trixie_template", "global_template"], [t["template"] for t in trixie]) + + # Matching bullseye + bullseye = filter_package_templates(templates, "bullseye") + asserts.equals(env, ["global_template", "nginx_template"], [t["template"] for t in bullseye]) + + # Matching other set + other = filter_package_templates(templates, "other_set") + asserts.equals(env, ["global_template"], [t["template"] for t in other]) + + return unittest.end(env) + +filter_package_templates_test = unittest.make(_filter_package_templates_test) + def extensions_tests(): scoped_mergedusr_test(name = _TEST_SUITE_PREFIX + "scoped_mergedusr") + filter_package_templates_test(name = _TEST_SUITE_PREFIX + "filter_package_templates") diff --git a/apt/tests/translate_dependency_set_test.bzl b/apt/tests/translate_dependency_set_test.bzl index 6e480cc..ed40441 100644 --- a/apt/tests/translate_dependency_set_test.bzl +++ b/apt/tests/translate_dependency_set_test.bzl @@ -1,7 +1,7 @@ "unit tests for dependency set translation" load("@bazel_skylib//lib:unittest.bzl", "asserts", "unittest") -load("//apt/private:translate_dependency_set.bzl", "package_deps_for_architecture") +load("//apt/private:translate_dependency_set.bzl", "check_template_variable_collision", "package_deps_for_architecture", "resolve_package_template") load("//apt/private:util.bzl", "util") _TEST_SUITE_PREFIX = "translate_dependency_set/" @@ -61,6 +61,96 @@ def _package_repo_name_modes_test(ctx): package_repo_name_modes_test = unittest.make(_package_repo_name_modes_test) +def _resolve_package_template_test(ctx): + env = unittest.begin(ctx) + + default_template = "default: {name}" + custom_nvidia_template = "nvidia: {name}" + custom_dev_template = "dev: {name}" + + templates = [ + { + "packages": ["nvidia-*"], + "template": custom_nvidia_template, + "additional_variables": {"cuda_version": "12.0"}, + }, + { + "packages": ["*-dev", "libc6"], + "template": custom_dev_template, + "additional_variables": {"is_dev": "true"}, + }, + ] + + # Matching nvidia-* prefix + (tmpl, vars) = resolve_package_template("nvidia-driver", templates, default_template) + asserts.equals(env, custom_nvidia_template, tmpl) + asserts.equals(env, {"cuda_version": "12.0"}, vars) + + # Matching *-dev suffix + (tmpl, vars) = resolve_package_template("libssl-dev", templates, default_template) + asserts.equals(env, custom_dev_template, tmpl) + asserts.equals(env, {"is_dev": "true"}, vars) + + # Matching exact "libc6" + (tmpl, vars) = resolve_package_template("libc6", templates, default_template) + asserts.equals(env, custom_dev_template, tmpl) + asserts.equals(env, {"is_dev": "true"}, vars) + + # Fallback to default template when unmatched + (tmpl, vars) = resolve_package_template("bash", templates, default_template) + asserts.equals(env, default_template, tmpl) + asserts.equals(env, {}, vars) + + # First match takes precedence + overlapping_templates = [ + { + "packages": ["lib*"], + "template": "lib_template", + "additional_variables": {"tier": "1"}, + }, + { + "packages": ["libc6"], + "template": "libc6_template", + "additional_variables": {"tier": "2"}, + }, + ] + (tmpl, vars) = resolve_package_template("libc6", overlapping_templates, default_template) + asserts.equals(env, "lib_template", tmpl) + asserts.equals(env, {"tier": "1"}, vars) + + return unittest.end(env) + +resolve_package_template_test = unittest.make(_resolve_package_template_test) + +def _check_template_variable_collision_test(ctx): + env = unittest.begin(ctx) + + # No collision with custom variables + asserts.equals(env, None, check_template_variable_collision({ + "custom_var": "val", + "another_var": "123", + })) + + # Collisions with reserved built-in keys + asserts.equals(env, "name", check_template_variable_collision({ + "name": "override", + })) + asserts.equals(env, "deps", check_template_variable_collision({ + "deps": "[]", + })) + asserts.equals(env, "version", check_template_variable_collision({ + "version": "1.0", + })) + asserts.equals(env, "suite", check_template_variable_collision({ + "suite": "bookworm", + })) + + return unittest.end(env) + +check_template_variable_collision_test = unittest.make(_check_template_variable_collision_test) + def translate_dependency_set_tests(): no_mixed_architectures_test(name = _TEST_SUITE_PREFIX + "no_mixed_architectures") package_repo_name_modes_test(name = _TEST_SUITE_PREFIX + "package_repo_name_modes") + resolve_package_template_test(name = _TEST_SUITE_PREFIX + "resolve_package_template") + check_template_variable_collision_test(name = _TEST_SUITE_PREFIX + "check_template_variable_collision") diff --git a/apt/tests/util_test.bzl b/apt/tests/util_test.bzl new file mode 100644 index 0000000..435b29e --- /dev/null +++ b/apt/tests/util_test.bzl @@ -0,0 +1,55 @@ +"unit tests for apt utility functions" + +load("@bazel_skylib//lib:unittest.bzl", "asserts", "unittest") +load("//apt/private:util.bzl", "util") + +_TEST_SUITE_PREFIX = "util/" + +def _glob_match_test(ctx): + env = unittest.begin(ctx) + + # Universal wildcard + asserts.true(env, util.glob_match("*", "anything")) + asserts.true(env, util.glob_match("*", "")) + asserts.true(env, util.glob_match("*", "libc6")) + + # Exact matches + asserts.true(env, util.glob_match("libc6", "libc6")) + asserts.false(env, util.glob_match("libc6", "libc6-dev")) + asserts.false(env, util.glob_match("libc6", "libm")) + asserts.true(env, util.glob_match("", "")) + asserts.false(env, util.glob_match("", "foo")) + + # Prefix match + asserts.true(env, util.glob_match("nvidia-*", "nvidia-driver")) + asserts.true(env, util.glob_match("nvidia-*", "nvidia-smi")) + asserts.true(env, util.glob_match("nvidia-*", "nvidia-")) + asserts.false(env, util.glob_match("nvidia-*", "libnvidia-driver")) + asserts.false(env, util.glob_match("nvidia-*", "nvid")) + + # Suffix match + asserts.true(env, util.glob_match("*-dev", "libc6-dev")) + asserts.true(env, util.glob_match("*-dev", "libssl-dev")) + asserts.true(env, util.glob_match("*-dev", "-dev")) + asserts.false(env, util.glob_match("*-dev", "libc6-dev-doc")) + asserts.false(env, util.glob_match("*-dev", "dev")) + + # Middle wildcard + asserts.true(env, util.glob_match("lib*-dev", "libc6-dev")) + asserts.true(env, util.glob_match("lib*-dev", "libssl-dev")) + asserts.true(env, util.glob_match("lib*-dev", "lib-dev")) + asserts.false(env, util.glob_match("lib*-dev", "libc6")) + asserts.false(env, util.glob_match("lib*-dev", "libssl-dbg")) + + # Multiple wildcards + asserts.true(env, util.glob_match("*foo*bar*", "1foo2bar3")) + asserts.true(env, util.glob_match("*foo*bar*", "foobar")) + asserts.false(env, util.glob_match("*foo*bar*", "barfoo")) + asserts.false(env, util.glob_match("*foo*bar*", "foo")) + + return unittest.end(env) + +glob_match_test = unittest.make(_glob_match_test) + +def util_tests(): + glob_match_test(name = _TEST_SUITE_PREFIX + "glob_match") diff --git a/e2e/smoke/BUILD b/e2e/smoke/BUILD index 32b9c97..21bf14d 100644 --- a/e2e/smoke/BUILD +++ b/e2e/smoke/BUILD @@ -5,6 +5,11 @@ load("@rules_distroless//distroless:defs.bzl", "cacerts", "group", "passwd") load("@rules_oci//oci:defs.bzl", "oci_image", "oci_load") load("@tar.bzl", "tar") +exports_files([ + "custom_package.BUILD.tmpl", + "custom_package_rule.bzl", +]) + COMPATIBLE_WITH = select({ "@platforms//cpu:x86_64": ["@platforms//cpu:x86_64"], "@platforms//cpu:arm64": ["@platforms//cpu:arm64"], @@ -142,3 +147,11 @@ container_structure_test( ], target_compatible_with = COMPATIBLE_WITH, ) + +build_test( + name = "custom_package_template_test", + target_compatible_with = COMPATIBLE_WITH, + targets = [ + "@bullseye//tzdata", + ], +) diff --git a/e2e/smoke/MODULE.bazel b/e2e/smoke/MODULE.bazel index 4e431d3..4a21e77 100644 --- a/e2e/smoke/MODULE.bazel +++ b/e2e/smoke/MODULE.bazel @@ -87,5 +87,13 @@ apt.install( "cloud-sdk", ], ) +apt.package_template( + additional_variables = { + "custom_annotation": "smoke_test_custom_variable", + }, + dependency_sets = ["bullseye"], + packages = ["tzdata"], + template_file = "//:custom_package.BUILD.tmpl", +) apt.lock(into = ":bullseye.lock.json") use_repo(apt, "bullseye") diff --git a/e2e/smoke/custom_package.BUILD.tmpl b/e2e/smoke/custom_package.BUILD.tmpl new file mode 100644 index 0000000..9b035d1 --- /dev/null +++ b/e2e/smoke/custom_package.BUILD.tmpl @@ -0,0 +1,26 @@ +"""Custom package template for smoke test. DO NOT EDIT.""" + +# Escaped literal braces test: {{escaped_literal_braces}} +load("@@//:custom_package_rule.bzl", "custom_package") + +alias( + name = "data", + actual = {data_targets}, + visibility = ["//visibility:public"], +) + +alias( + name = "control", + actual = {control_targets}, + visibility = ["//visibility:public"], +) + +custom_package( + name = "{target_name}", + data = ":data", + package_name = "{name}", + package_version = "{version}", + suite = "{suite}", + extra_annotation = "{custom_annotation}", + visibility = ["//visibility:public"], +) diff --git a/e2e/smoke/custom_package_rule.bzl b/e2e/smoke/custom_package_rule.bzl new file mode 100644 index 0000000..e96fa25 --- /dev/null +++ b/e2e/smoke/custom_package_rule.bzl @@ -0,0 +1,29 @@ +"""Custom rule for testing package_template in e2e smoke.""" + +def _custom_package_impl(ctx): + # Verifies that custom metadata variables are passed through format_vars + if not ctx.attr.package_name: + fail("package_name must be non-empty") + if not ctx.attr.package_version: + fail("package_version must be non-empty") + if not ctx.attr.suite: + fail("suite must be non-empty") + if not ctx.attr.extra_annotation: + fail("extra_annotation must be non-empty") + + # Pass through data files as DefaultInfo + data_files = ctx.files.data + return [ + DefaultInfo(files = depset(data_files)), + ] + +custom_package = rule( + implementation = _custom_package_impl, + attrs = { + "data": attr.label(mandatory = True, allow_files = True), + "package_name": attr.string(mandatory = True), + "package_version": attr.string(mandatory = True), + "suite": attr.string(mandatory = True), + "extra_annotation": attr.string(mandatory = True), + }, +)