From 15aa0c7c770c4ff2378905c0998f23480e32d548 Mon Sep 17 00:00:00 2001 From: Lavish Pal Date: Tue, 2 Jun 2026 04:28:35 +0530 Subject: [PATCH 1/4] Add support for gzip-compressed CPIO output Signed-off-by: Lavish Pal --- internal/cli/build-cpio.go | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/internal/cli/build-cpio.go b/internal/cli/build-cpio.go index fef620249..ad6c057d5 100644 --- a/internal/cli/build-cpio.go +++ b/internal/cli/build-cpio.go @@ -15,10 +15,13 @@ package cli import ( + "compress/gzip" "context" "fmt" + "io" "os" "runtime" + "strings" "github.com/spf13/cobra" @@ -109,8 +112,15 @@ func BuildCPIOCmd(ctx context.Context, dest string, opts ...build.Option) error } defer f.Close() - // TODO(mattmoor): Consider wrapping in a gzip writer if the filename - // ends in .gz + // Use a gzip-compressed writer when the output filename ends with .gz. + var w io.Writer = f - return cpio.FromLayer(layer, f) + if strings.HasSuffix(dest, ".gz") { + gzw := gzip.NewWriter(f) + defer gzw.Close() + + w = gzw + } + + return cpio.FromLayer(layer, w) } From 8752f58579f1f5170bf246d3d18555d4fcd443b0 Mon Sep 17 00:00:00 2001 From: Lavish Pal Date: Fri, 12 Jun 2026 05:48:36 +0530 Subject: [PATCH 2/4] Add the tests for gzip-compressed CPIO output Signed-off-by: Lavish Pal --- internal/cli/build-cpio.go | 13 +++---- internal/cli/build_cpio_test.go | 66 +++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 7 deletions(-) create mode 100644 internal/cli/build_cpio_test.go diff --git a/internal/cli/build-cpio.go b/internal/cli/build-cpio.go index ad6c057d5..a7acac563 100644 --- a/internal/cli/build-cpio.go +++ b/internal/cli/build-cpio.go @@ -18,7 +18,6 @@ import ( "compress/gzip" "context" "fmt" - "io" "os" "runtime" "strings" @@ -113,14 +112,14 @@ func BuildCPIOCmd(ctx context.Context, dest string, opts ...build.Option) error defer f.Close() // Use a gzip-compressed writer when the output filename ends with .gz. - var w io.Writer = f - if strings.HasSuffix(dest, ".gz") { gzw := gzip.NewWriter(f) - defer gzw.Close() - - w = gzw + if err := cpio.FromLayer(layer, gzw); err != nil { + gzw.Close() + return err + } + return gzw.Close() } - return cpio.FromLayer(layer, w) + return cpio.FromLayer(layer, f) } diff --git a/internal/cli/build_cpio_test.go b/internal/cli/build_cpio_test.go new file mode 100644 index 000000000..b16f2083d --- /dev/null +++ b/internal/cli/build_cpio_test.go @@ -0,0 +1,66 @@ +// Copyright 2026 Chainguard, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cli + +import ( + "compress/gzip" + "context" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" + + "chainguard.dev/apko/pkg/build" +) + +func TestBuildCPIOCmd(t *testing.T) { + ctx := context.Background() + + // Minimal config — no packages, no repositories — keeps the + // build fully offline and fast. + configFile := filepath.Join(t.TempDir(), "apko.yaml") + err := os.WriteFile(configFile, []byte("contents:\n repositories:\n - https://dl-cdn.alpinelinux.org/alpine/edge/main\n packages: []\n"), 0644) + require.NoError(t, err) + + t.Run("gz suffix produces gzip-compressed output", func(t *testing.T) { + dest := filepath.Join(t.TempDir(), "out.cpio.gz") + + err := BuildCPIOCmd(ctx, dest, build.WithConfig(configFile, []string{})) + require.NoError(t, err) + + f, err := os.Open(dest) + require.NoError(t, err) + defer f.Close() + + gzr, err := gzip.NewReader(f) + require.NoError(t, err, "expected output to be valid gzip") + defer gzr.Close() + }) + + t.Run("non-gz suffix produces plain cpio output", func(t *testing.T) { + dest := filepath.Join(t.TempDir(), "out.cpio") + + err := BuildCPIOCmd(ctx, dest, build.WithConfig(configFile, []string{})) + require.NoError(t, err) + + f, err := os.Open(dest) + require.NoError(t, err) + defer f.Close() + + _, err = gzip.NewReader(f) + require.Error(t, err, "expected plain cpio, got gzip") + }) +} From 711bd8d767d253d7af606bb9d2ad6451958ae53b Mon Sep 17 00:00:00 2001 From: Lavish Pal Date: Sat, 27 Jun 2026 08:41:20 +0530 Subject: [PATCH 3/4] reuse testdata/apko.yaml fixture instead of inline config Signed-off-by: Lavish Pal --- internal/cli/build_cpio_test.go | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/internal/cli/build_cpio_test.go b/internal/cli/build_cpio_test.go index b16f2083d..0da9566dc 100644 --- a/internal/cli/build_cpio_test.go +++ b/internal/cli/build_cpio_test.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package cli +package cli_test import ( "compress/gzip" @@ -21,6 +21,7 @@ import ( "path/filepath" "testing" + "chainguard.dev/apko/internal/cli" "github.com/stretchr/testify/require" "chainguard.dev/apko/pkg/build" @@ -29,16 +30,10 @@ import ( func TestBuildCPIOCmd(t *testing.T) { ctx := context.Background() - // Minimal config — no packages, no repositories — keeps the - // build fully offline and fast. - configFile := filepath.Join(t.TempDir(), "apko.yaml") - err := os.WriteFile(configFile, []byte("contents:\n repositories:\n - https://dl-cdn.alpinelinux.org/alpine/edge/main\n packages: []\n"), 0644) - require.NoError(t, err) - t.Run("gz suffix produces gzip-compressed output", func(t *testing.T) { dest := filepath.Join(t.TempDir(), "out.cpio.gz") - err := BuildCPIOCmd(ctx, dest, build.WithConfig(configFile, []string{})) + err := cli.BuildCPIOCmd(ctx, dest, build.WithConfig("testdata/apko.yaml", []string{})) require.NoError(t, err) f, err := os.Open(dest) @@ -53,7 +48,7 @@ func TestBuildCPIOCmd(t *testing.T) { t.Run("non-gz suffix produces plain cpio output", func(t *testing.T) { dest := filepath.Join(t.TempDir(), "out.cpio") - err := BuildCPIOCmd(ctx, dest, build.WithConfig(configFile, []string{})) + err := cli.BuildCPIOCmd(ctx, dest, build.WithConfig("testdata/apko.yaml", []string{})) require.NoError(t, err) f, err := os.Open(dest) From e173a2b89078b840b1faf9a791f3fea8d135de10 Mon Sep 17 00:00:00 2001 From: Lavish Pal Date: Sat, 27 Jun 2026 08:43:24 +0530 Subject: [PATCH 4/4] rename the file name from build-cpio.go to build_cpio.go Signed-off-by: Lavish Pal --- internal/cli/{build-cpio.go => build_cpio.go} | 0 internal/cli/build_cpio_test.go | 3 ++- pkg/build/layers.go | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) rename internal/cli/{build-cpio.go => build_cpio.go} (100%) diff --git a/internal/cli/build-cpio.go b/internal/cli/build_cpio.go similarity index 100% rename from internal/cli/build-cpio.go rename to internal/cli/build_cpio.go diff --git a/internal/cli/build_cpio_test.go b/internal/cli/build_cpio_test.go index 0da9566dc..ed097aa67 100644 --- a/internal/cli/build_cpio_test.go +++ b/internal/cli/build_cpio_test.go @@ -21,9 +21,10 @@ import ( "path/filepath" "testing" - "chainguard.dev/apko/internal/cli" "github.com/stretchr/testify/require" + "chainguard.dev/apko/internal/cli" + "chainguard.dev/apko/pkg/build" ) diff --git a/pkg/build/layers.go b/pkg/build/layers.go index f0716616a..91c2692f9 100644 --- a/pkg/build/layers.go +++ b/pkg/build/layers.go @@ -299,8 +299,8 @@ func splitLayers(ctx context.Context, fsys apkfs.FullFS, groups []*group, pkgToD // Maintain our "main" stack. if f.header.Typeflag == tar.TypeDir { // Pop off any directories that are not parents of the current file's directory. - for i := len(stack) - 1; i >= 0; i-- { - if stack[i].path == path.Dir(f.path) { + for i, v := range slices.Backward(stack) { + if v.path == path.Dir(f.path) { break }