Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .bcr/presubmit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ bcr_test_module:
module_path: "example/hello"
matrix:
platform: ["macos", "ubuntu2204"]
bazel: ["8.*"]
bazel: ["8.*", "9.*"]
tasks:
build_example:
name: "Build example image"
Expand Down
6 changes: 5 additions & 1 deletion MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ bazel_dep(name = "gazelle", version = "0.47.0", dev_dependency = True)
bazel_dep(name = "rules_go", version = "0.59.0")
bazel_dep(name = "rules_java", version = "8.14.0")

go_sdk = use_extension("@rules_go//go:extensions.bzl", "go_sdk")
go_sdk = use_extension(
"@rules_go//go:extensions.bzl",
"go_sdk",
dev_dependency = True,
)
go_sdk.from_file(
go_mod = "//:go.mod",
)
Expand Down
9 changes: 9 additions & 0 deletions cmd/jar_layerer/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ func main() {
flag.Var(&artifactLayers, "artifact_layer", "ARTIFACT_ID=path.tar (repeatable)")
var artifactGroupLayers repeatedFlag
flag.Var(&artifactGroupLayers, "artifact_group_layer", "ID1,ID2,...=path.tar (repeatable)")
var padLayers repeatedFlag
flag.Var(&padLayers, "pad_layer", "path to write an empty layer tar (repeatable)")

flag.Parse()

Expand Down Expand Up @@ -124,4 +126,11 @@ func main() {
os.Exit(1)
}
}

for _, path := range padLayers {
if err := jarlayer.WriteEmptyTar(path); err != nil {
fmt.Fprintf(os.Stderr, "writing pad layer: %v\n", err)
os.Exit(1)
}
}
}
162 changes: 0 additions & 162 deletions example/hello/MODULE.bazel.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

38 changes: 35 additions & 3 deletions jvm_jar_layers.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,13 @@ def _group_artifacts(artifact_ids, max_groups):
# max_groups is 0: no artifact layers at all.
return []

# Non-maven layer slots: the data-runfiles tar and the fallback tar.
_EXTRA_LAYER_SLOTS = 2

def jvm_jar_layer_slots(max_layers):
"""Number of `<name>.layer_N` filegroups emitted for a given max_layers."""
return max_layers + _EXTRA_LAYER_SLOTS

def jvm_jar_layers(
name,
binary,
Expand All @@ -172,6 +179,13 @@ def jvm_jar_layers(
The container classpath uses Java's @file syntax to reference a classpath
file listing all JARs.

Because the number and names of the layer tars are only known at analysis
time, each tar is also exposed through a fixed-name `<name>.layer_N`
filegroup (N in range(jvm_jar_layer_slots(max_layers))) so image rules can
map each tar to its own image layer. Slots beyond the produced tar count
are padded with empty tars, which compress to byte-identical, deduplicable
layer blobs.

Args:
name: target name
binary: label of a java_binary or scala_binary target
Expand Down Expand Up @@ -201,6 +215,13 @@ def jvm_jar_layers(
**kwargs
)

for index in range(jvm_jar_layer_slots(max_layers)):
native.filegroup(
name = "%s.layer_%d" % (name, index),
srcs = [name],
output_group = "layer_%d" % index,
)

def _jvm_jar_layers_impl(ctx):
runtime_jars = _runtime_jars(ctx.attr.binary)
if not runtime_jars:
Expand Down Expand Up @@ -289,6 +310,14 @@ def _jvm_jar_layers_impl(ctx):
args.add("--artifact_group_layer", ",".join(group_ids) + "=" + group_out.path)
tar_outputs.append(group_out)

# Pad remaining slots with empty tars so every layer_N output group (and
# its filegroup) yields exactly one tar file — image rules typically reject
# labels that produce no tar.
for index in range(len(tar_outputs), jvm_jar_layer_slots(ctx.attr.max_layers)):
pad = ctx.actions.declare_file(ctx.label.name + ".pad_%d.tar" % index)
args.add("--pad_layer", pad)
tar_outputs.append(pad)

ctx.actions.run(
inputs = inputs,
outputs = tar_outputs + [classpath_file],
Expand All @@ -300,11 +329,14 @@ def _jvm_jar_layers_impl(ctx):

# DefaultInfo only includes tar files — the classpath file is a plain text
# file and must not be passed to container_image's tars attribute.
output_groups = {"classpath": depset([classpath_file])}
for index in range(jvm_jar_layer_slots(ctx.attr.max_layers)):
files = [tar_outputs[index]] if index < len(tar_outputs) else []
output_groups["layer_%d" % index] = depset(files)

return [
DefaultInfo(files = depset(tar_outputs)),
OutputGroupInfo(
classpath = depset([classpath_file]),
),
OutputGroupInfo(**output_groups),
]

_jvm_jar_layers = rule(
Expand Down
14 changes: 14 additions & 0 deletions pkg/jarlayer/jarlayer.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,20 @@ func LayerJars(opts LayerOptions) (retErr error) {
return nil
}

// WriteEmptyTar writes a valid tar archive containing no entries. Pad layers
// use it so every declared layer slot yields a byte-identical, deduplicable
// blob.
func WriteEmptyTar(outputPath string) (retErr error) {
lw, err := newLayerWriter(outputPath)
if err != nil {
return fmt.Errorf("creating pad layer: %w", err)
}
if closeErr := lw.Close(); closeErr != nil {
return fmt.Errorf("closing pad layer: %w", closeErr)
}
return nil
}

// LayerData writes files into a deterministic tar using their Bazel runfiles
// paths. Directories are expanded recursively and symlinks are rejected.
func LayerData(outputPath string, files []DataFile) (retErr error) {
Expand Down
Loading