From b3f69cc6e7965b87740af8a47f009ffcbe3d12d3 Mon Sep 17 00:00:00 2001 From: Paul Johnston Date: Tue, 21 Jul 2026 14:09:36 -0600 Subject: [PATCH] fix: always create the workspace runfiles directory in the fallback tar Consumers set the image WorkingDir to /app/ so that runfiles-relative jvm_flags resolve, but the directory was only created when data runfiles existed. OCI runtimes fail chdir on a missing working directory, so images without data runfiles failed at container start. Add a repeatable --ensure_dir flag to jar_layerer and pass app/ from the rule unconditionally. --- cmd/jar_layerer/main.go | 3 +++ jvm_jar_layers.bzl | 6 ++++++ pkg/jarlayer/jarlayer.go | 11 +++++++++++ pkg/jarlayer/jarlayer_test.go | 30 ++++++++++++++++++++++++++++++ 4 files changed, 50 insertions(+) diff --git a/cmd/jar_layerer/main.go b/cmd/jar_layerer/main.go index e40b49f..462a7c6 100644 --- a/cmd/jar_layerer/main.go +++ b/cmd/jar_layerer/main.go @@ -34,6 +34,8 @@ func main() { 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)") + var ensureDirs repeatedFlag + flag.Var(&ensureDirs, "ensure_dir", "directory entry to always create in the fallback tar (repeatable)") flag.Parse() @@ -48,6 +50,7 @@ func main() { ClasspathPath: *classpath, AppPrefix: *appPrefix, PathPrefix: *pathPrefix, + EnsureDirs: ensureDirs, } // Read JAR list from file. diff --git a/jvm_jar_layers.bzl b/jvm_jar_layers.bzl index b103051..e580b77 100644 --- a/jvm_jar_layers.bzl +++ b/jvm_jar_layers.bzl @@ -277,6 +277,12 @@ def _jvm_jar_layers_impl(ctx): args.add("--fallback", fallback) tar_outputs.append(fallback) + # Always materialize the workspace runfiles directory: consumers commonly + # set the image WorkingDir to /app/ for runfiles-relative + # flags, and OCI runtimes fail chdir when the directory is absent (it is + # otherwise only created when data runfiles exist). + args.add("--ensure_dir", "app/" + ctx.workspace_name) + # Maven artifact layers via aspect. if ctx.file.maven_lock_file: lock_file = ctx.file.maven_lock_file diff --git a/pkg/jarlayer/jarlayer.go b/pkg/jarlayer/jarlayer.go index b36dbaa..0dd41c9 100644 --- a/pkg/jarlayer/jarlayer.go +++ b/pkg/jarlayer/jarlayer.go @@ -32,6 +32,11 @@ type LayerOptions struct { AppPrefix string // PathPrefix is prepended to tar entry paths (e.g., "app/lib/"). PathPrefix string + // EnsureDirs lists directories always created in the fallback tar (e.g. + // "app/_main"), so that an image WorkingDir pointing at them exists even + // when no other entry would create them. OCI runtimes fail chdir on a + // missing working directory. + EnsureDirs []string } // ArtifactLayer maps one or more artifact IDs to a single output tar. @@ -108,6 +113,12 @@ func LayerJars(opts LayerOptions) (retErr error) { // Track written directories to avoid duplicates across JARs. writtenDirs := make(map[string]map[string]bool) // writer path -> set of dirs + for _, dir := range opts.EnsureDirs { + if err := ensureParentDirs(fallback, strings.TrimSuffix(dir, "/")+"/", writtenDirs); err != nil { + return fmt.Errorf("ensuring dir %s: %w", dir, err) + } + } + // Process each JAR: determine layer, write to tar, collect classpath. var classpathEntries []string usedNames := make(map[string]bool) diff --git a/pkg/jarlayer/jarlayer_test.go b/pkg/jarlayer/jarlayer_test.go index a1f121b..6424703 100644 --- a/pkg/jarlayer/jarlayer_test.go +++ b/pkg/jarlayer/jarlayer_test.go @@ -622,3 +622,33 @@ func TestLayerData_RejectsUnsafeDestination(t *testing.T) { t.Fatalf("LayerData() error = %v, want unsafe destination error", err) } } + +func TestLayerJars_EnsureDirs(t *testing.T) { + dir := t.TempDir() + + jar1 := filepath.Join(dir, "dep1.jar") + createTestJar(t, jar1, map[string]string{ + "com/example/Foo.class": "foo-bytes", + }) + + fallbackPath := filepath.Join(dir, "fallback.tar") + + err := LayerJars(LayerOptions{ + JarPaths: []string{jar1}, + FallbackPath: fallbackPath, + ClasspathPath: filepath.Join(dir, "classpath"), + AppPrefix: "/app/lib", + PathPrefix: "app/lib/", + EnsureDirs: []string{"app/_main"}, + }) + if err != nil { + t.Fatal(err) + } + + entries := readTar(t, fallbackPath) + for _, want := range []string{"app/", "app/_main/"} { + if _, ok := entries[want]; !ok { + t.Errorf("expected directory entry %q in fallback tar", want) + } + } +}