diff --git a/internal/cli/build-cpio.go b/internal/cli/build_cpio.go similarity index 93% rename from internal/cli/build-cpio.go rename to internal/cli/build_cpio.go index fef620249..a7acac563 100644 --- a/internal/cli/build-cpio.go +++ b/internal/cli/build_cpio.go @@ -15,10 +15,12 @@ package cli import ( + "compress/gzip" "context" "fmt" "os" "runtime" + "strings" "github.com/spf13/cobra" @@ -109,8 +111,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. + if strings.HasSuffix(dest, ".gz") { + gzw := gzip.NewWriter(f) + if err := cpio.FromLayer(layer, gzw); err != nil { + gzw.Close() + return err + } + return gzw.Close() + } 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..ed097aa67 --- /dev/null +++ b/internal/cli/build_cpio_test.go @@ -0,0 +1,62 @@ +// 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_test + +import ( + "compress/gzip" + "context" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" + + "chainguard.dev/apko/internal/cli" + + "chainguard.dev/apko/pkg/build" +) + +func TestBuildCPIOCmd(t *testing.T) { + ctx := context.Background() + + t.Run("gz suffix produces gzip-compressed output", func(t *testing.T) { + dest := filepath.Join(t.TempDir(), "out.cpio.gz") + + err := cli.BuildCPIOCmd(ctx, dest, build.WithConfig("testdata/apko.yaml", []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 := cli.BuildCPIOCmd(ctx, dest, build.WithConfig("testdata/apko.yaml", []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") + }) +} 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 }