From f8b749ea623e284944478569312234dc8fb81ee7 Mon Sep 17 00:00:00 2001 From: Cezar Craciunoiu Date: Tue, 3 Mar 2026 14:15:01 +0200 Subject: [PATCH 1/7] fix(fs): Fail early on erofs->cpio conversion Without this transformation would fail silently as it would be identified as a tar file. Signed-off-by: Cezar Craciunoiu --- fs/cpio/create.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/fs/cpio/create.go b/fs/cpio/create.go index b2bfccacc..591c3fe5d 100644 --- a/fs/cpio/create.go +++ b/fs/cpio/create.go @@ -77,6 +77,8 @@ func CreateFS(ctx context.Context, output string, source string, opts ...CpioCre if err := c.CreateFSFromCpio(ctx, writer, source); err != nil { return fmt.Errorf("could not create CPIO archive from CPIO file: %w", err) } + case fsutils.IsErofsFile(source): + return fmt.Errorf("creating CPIO from EroFS files is not currently supported") case fsutils.IsTarFile(source), fsutils.IsTarGzFile(source): if err := c.CreateFSFromTar(ctx, writer, source); err != nil { From a097ff6ed7c94fe2f604c8eb0bc87bac1bdd5d02 Mon Sep 17 00:00:00 2001 From: Cezar Craciunoiu Date: Tue, 3 Mar 2026 14:16:22 +0200 Subject: [PATCH 2/7] fix(fs): Direct copy cpio files Do not use special cpio writer if the file is already packed. We can use a simple copy. Signed-off-by: Cezar Craciunoiu --- fs/cpio/create.go | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/fs/cpio/create.go b/fs/cpio/create.go index 591c3fe5d..9e55dcc0c 100644 --- a/fs/cpio/create.go +++ b/fs/cpio/create.go @@ -74,7 +74,15 @@ func CreateFS(ctx context.Context, output string, source string, opts ...CpioCre return fmt.Errorf("could not create CPIO archive from OCI image: %w", err) } case fsutils.IsCpioFile(source): - if err := c.CreateFSFromCpio(ctx, writer, source); err != nil { + // If the source is already a CPIO file, we can simply copy its contents to the output. + // This means we close the writer and re-open is as a simple byte writer. + writer.Close() + _, err := f.Seek(0, io.SeekStart) + if err != nil { + return fmt.Errorf("could not seek to start of CPIO archive: %w", err) + } + + if err := c.CreateFSFromCpio(ctx, io.Writer(f), source); err != nil { return fmt.Errorf("could not create CPIO archive from CPIO file: %w", err) } case fsutils.IsErofsFile(source): @@ -555,7 +563,7 @@ func (c *createOptions) CreateFSFromDirectory(ctx context.Context, writer *cpio. } // CreateFSFromCpio creates a CPIO filesystem from an existing CPIO file. -func (c *createOptions) CreateFSFromCpio(ctx context.Context, writer *cpio.Writer, source string) error { +func (c *createOptions) CreateFSFromCpio(ctx context.Context, writer io.Writer, source string) error { // Open and copy all contents from 'source' to the writer f, err := os.Open(source) if err != nil { From 612039eae00536b659e32d95ad7d5c3c1f655559 Mon Sep 17 00:00:00 2001 From: Cezar Craciunoiu Date: Tue, 3 Mar 2026 14:45:08 +0200 Subject: [PATCH 3/7] feat(initrd): Expose and use 'file' FS type for direct copying Signed-off-by: Cezar Craciunoiu --- initrd/directory.go | 2 ++ initrd/dockerfile.go | 2 ++ initrd/file.go | 2 ++ initrd/ociimage.go | 2 ++ initrd/options.go | 1 + initrd/tarball.go | 2 ++ initrd/utils.go | 25 +++++++++++++++++++++++++ 7 files changed, 36 insertions(+) diff --git a/initrd/directory.go b/initrd/directory.go index 4f6c24525..51323c0c5 100644 --- a/initrd/directory.go +++ b/initrd/directory.go @@ -79,6 +79,8 @@ func (initrd *directory) Build(ctx context.Context) (string, error) { } switch initrd.opts.fsType { + case FsTypeFile: + return "", fmt.Errorf("cannot build initrd from directory with file output type") case FsTypeErofs: return initrd.opts.output, erofs.CreateFS(ctx, initrd.opts.output, initrd.path, erofs.WithAllRoot(!initrd.opts.keepOwners), diff --git a/initrd/dockerfile.go b/initrd/dockerfile.go index b8f974e3d..6e09ebeb5 100644 --- a/initrd/dockerfile.go +++ b/initrd/dockerfile.go @@ -417,6 +417,8 @@ func (initrd *dockerfile) Build(ctx context.Context) (string, error) { } switch initrd.opts.fsType { + case FsTypeFile: + return "", fmt.Errorf("cannot build initrd from dockerfile with file output type") case FsTypeErofs: return initrd.opts.output, erofs.CreateFS(ctx, initrd.opts.output, tarOutput.Name(), erofs.WithAllRoot(!initrd.opts.keepOwners), diff --git a/initrd/file.go b/initrd/file.go index 8dc7e35f2..86c556a4f 100644 --- a/initrd/file.go +++ b/initrd/file.go @@ -71,6 +71,8 @@ func (initrd *file) Build(ctx context.Context) (string, error) { } switch initrd.opts.fsType { + case FsTypeFile: + return initrd.opts.output, copyFile(initrd.path, initrd.opts.output) case FsTypeErofs: return initrd.opts.output, erofs.CreateFS(ctx, initrd.opts.output, initrd.path, erofs.WithAllRoot(!initrd.opts.keepOwners), diff --git a/initrd/ociimage.go b/initrd/ociimage.go index 758a61ca2..a96bb41f7 100644 --- a/initrd/ociimage.go +++ b/initrd/ociimage.go @@ -196,6 +196,8 @@ func (initrd *ociimage) Build(ctx context.Context) (string, error) { } switch initrd.opts.fsType { + case FsTypeFile: + return "", fmt.Errorf("cannot build initrd from oci image with file output type") case FsTypeErofs: return initrd.opts.output, erofs.CreateFS(ctx, initrd.opts.output, ociTarballPath, erofs.WithAllRoot(!initrd.opts.keepOwners), diff --git a/initrd/options.go b/initrd/options.go index 6ac00594a..9db57593a 100644 --- a/initrd/options.go +++ b/initrd/options.go @@ -207,6 +207,7 @@ func FsTypes() []FsType { return []FsType{ FsTypeCpio, FsTypeErofs, + FsTypeFile, } } diff --git a/initrd/tarball.go b/initrd/tarball.go index f83bd27fc..c80064578 100644 --- a/initrd/tarball.go +++ b/initrd/tarball.go @@ -70,6 +70,8 @@ func (initrd *tarball) Build(ctx context.Context) (string, error) { } switch initrd.opts.fsType { + case FsTypeFile: + return initrd.opts.output, copyFile(initrd.path, initrd.opts.output) case FsTypeErofs: return initrd.opts.output, erofs.CreateFS(ctx, initrd.opts.output, initrd.path, erofs.WithAllRoot(!initrd.opts.keepOwners), diff --git a/initrd/utils.go b/initrd/utils.go index 40516f634..3517dd0f9 100644 --- a/initrd/utils.go +++ b/initrd/utils.go @@ -9,6 +9,7 @@ import ( "fmt" "io" "os" + "path/filepath" ) func compressFiles(output string, path string) error { @@ -48,3 +49,27 @@ func compressFiles(output string, path string) error { return nil } + +func copyFile(src, dst string) error { + input, err := os.Open(src) + if err != nil { + return fmt.Errorf("opening source file: %w", err) + } + defer input.Close() + + if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil { + return fmt.Errorf("creating destination directory: %w", err) + } + + output, err := os.Create(dst) + if err != nil { + return fmt.Errorf("creating destination file: %w", err) + } + defer output.Close() + + if _, err := io.Copy(output, input); err != nil { + return fmt.Errorf("copying file contents: %w", err) + } + + return nil +} From 57221b379d33e1628c1b4d648f96d894fda98925 Mon Sep 17 00:00:00 2001 From: Cezar Craciunoiu Date: Tue, 3 Mar 2026 15:05:05 +0200 Subject: [PATCH 4/7] feat(fsutils): Add simple single-file check Signed-off-by: Cezar Craciunoiu --- fsutils/fsutils.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/fsutils/fsutils.go b/fsutils/fsutils.go index 1cd240269..10845a965 100644 --- a/fsutils/fsutils.go +++ b/fsutils/fsutils.go @@ -126,6 +126,16 @@ func IsCpioFile(path string) bool { return string(magic) == "070701" || string(magic) == "070702" } +// IsRegularFile checks if the given path is a regular file. +func IsRegularFile(path string) bool { + fi, err := os.Stat(path) + if err != nil { + return false + } + + return fi.Mode().IsRegular() +} + type FileInfo struct { Uid int Gid int From ee3617161f92dd588286b4b472e8c6192c771bdc Mon Sep 17 00:00:00 2001 From: Cezar Craciunoiu Date: Tue, 3 Mar 2026 15:05:37 +0200 Subject: [PATCH 5/7] feat(initrd): Expose and use 'unknown' FS type for file detection Signed-off-by: Cezar Craciunoiu --- initrd/directory.go | 2 ++ initrd/dockerfile.go | 2 ++ initrd/file.go | 25 +++++++++++++++++++++++++ initrd/ociimage.go | 2 ++ initrd/options.go | 1 + initrd/tarball.go | 2 ++ 6 files changed, 34 insertions(+) diff --git a/initrd/directory.go b/initrd/directory.go index 51323c0c5..e3c906909 100644 --- a/initrd/directory.go +++ b/initrd/directory.go @@ -79,6 +79,8 @@ func (initrd *directory) Build(ctx context.Context) (string, error) { } switch initrd.opts.fsType { + case FsTypeUnknown: + return "", fmt.Errorf("cannot build initrd from directory with unknown filesystem type") case FsTypeFile: return "", fmt.Errorf("cannot build initrd from directory with file output type") case FsTypeErofs: diff --git a/initrd/dockerfile.go b/initrd/dockerfile.go index 6e09ebeb5..ff8e52a8c 100644 --- a/initrd/dockerfile.go +++ b/initrd/dockerfile.go @@ -417,6 +417,8 @@ func (initrd *dockerfile) Build(ctx context.Context) (string, error) { } switch initrd.opts.fsType { + case FsTypeUnknown: + return "", fmt.Errorf("cannot build initrd from dockerfile with unknown filesystem type") case FsTypeFile: return "", fmt.Errorf("cannot build initrd from dockerfile with file output type") case FsTypeErofs: diff --git a/initrd/file.go b/initrd/file.go index 86c556a4f..420e67e0c 100644 --- a/initrd/file.go +++ b/initrd/file.go @@ -12,6 +12,7 @@ import ( "kraftkit.sh/fs/cpio" "kraftkit.sh/fs/erofs" + "kraftkit.sh/fsutils" ) type file struct { @@ -70,7 +71,14 @@ func (initrd *file) Build(ctx context.Context) (string, error) { return "", fmt.Errorf("CPIO archive path is the same as the source path, this is not allowed as it creates corrupted archives") } +reevaluateFsType: switch initrd.opts.fsType { + case FsTypeUnknown: + initrd.opts.fsType = detectFsType(initrd.path) + if initrd.opts.fsType == FsTypeUnknown { + return "", fmt.Errorf("could not detect filesystem type of input file, please specify it explicitly via the --fs-type flag") + } + goto reevaluateFsType case FsTypeFile: return initrd.opts.output, copyFile(initrd.path, initrd.opts.output) case FsTypeErofs: @@ -86,6 +94,23 @@ func (initrd *file) Build(ctx context.Context) (string, error) { } } +// detectFsType attempts to convert from the 'unknown' filesystem type to one +// of the known types like 'cpio'/'erofs'/'file'. +func detectFsType(source string) FsType { + switch { + case fsutils.IsErofsFile(source): + return FsTypeErofs + case fsutils.IsCpioFile(source): + return FsTypeCpio + case fsutils.IsTarFile(source), + fsutils.IsTarGzFile(source), + fsutils.IsRegularFile(source): + return FsTypeFile + default: + return FsTypeUnknown + } +} + // Options implements Initrd. func (initrd *file) Options() InitrdOptions { return initrd.opts diff --git a/initrd/ociimage.go b/initrd/ociimage.go index a96bb41f7..5f71f4a6d 100644 --- a/initrd/ociimage.go +++ b/initrd/ociimage.go @@ -196,6 +196,8 @@ func (initrd *ociimage) Build(ctx context.Context) (string, error) { } switch initrd.opts.fsType { + case FsTypeUnknown: + return "", fmt.Errorf("cannot build initrd from oci image with unknown filesystem type") case FsTypeFile: return "", fmt.Errorf("cannot build initrd from oci image with file output type") case FsTypeErofs: diff --git a/initrd/options.go b/initrd/options.go index 9db57593a..e74a0d14d 100644 --- a/initrd/options.go +++ b/initrd/options.go @@ -208,6 +208,7 @@ func FsTypes() []FsType { FsTypeCpio, FsTypeErofs, FsTypeFile, + FsTypeUnknown, } } diff --git a/initrd/tarball.go b/initrd/tarball.go index c80064578..52b268d7a 100644 --- a/initrd/tarball.go +++ b/initrd/tarball.go @@ -70,6 +70,8 @@ func (initrd *tarball) Build(ctx context.Context) (string, error) { } switch initrd.opts.fsType { + case FsTypeUnknown: + return "", fmt.Errorf("cannot build initrd from tarball with unknown filesystem type") case FsTypeFile: return initrd.opts.output, copyFile(initrd.path, initrd.opts.output) case FsTypeErofs: From e7cd0d685ee067bd76d652662ae89e529195b0c1 Mon Sep 17 00:00:00 2001 From: Justin Chadwell Date: Tue, 3 Mar 2026 09:58:12 +0000 Subject: [PATCH 6/7] fix: Avoid overwriting fstype with empty value This value will be set to the empty string from the CLI if it's not explicitly set with `--rootfs-type`. This essentially results in overwriting the pre-populated default cpio type with an empty fs type. This then just silently skips the build in older versions of kraft, or causes a weirdly strange internal error on staging. Signed-off-by: Justin Chadwell --- initrd/options.go | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/initrd/options.go b/initrd/options.go index e74a0d14d..f0f1063ff 100644 --- a/initrd/options.go +++ b/initrd/options.go @@ -128,8 +128,16 @@ func WithOutput(output string) InitrdOption { // WithOutputType sets the output type of the resulting root filesystem. func WithOutputType(fsType FsType) InitrdOption { return func(opts *InitrdOptions) error { - opts.fsType = fsType - return nil + if fsType == "" { + return nil + } + for _, validType := range FsTypes() { + if fsType == validType { + opts.fsType = fsType + return nil + } + } + return fmt.Errorf("invalid output type '%s', valid types are: %v", fsType, FsTypeNames()) } } From 888fab5be038379596b4fe2b1fe9e4073b540dd9 Mon Sep 17 00:00:00 2001 From: Justin Chadwell Date: Tue, 3 Mar 2026 10:01:10 +0000 Subject: [PATCH 7/7] fix: Don't throw detection errors on the floor Debugging without these is an absolute PITA. Signed-off-by: Justin Chadwell --- initrd/detect.go | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/initrd/detect.go b/initrd/detect.go index 8c658529a..c68392cec 100644 --- a/initrd/detect.go +++ b/initrd/detect.go @@ -7,6 +7,8 @@ package initrd import ( "context" "fmt" + + "kraftkit.sh/log" ) // New attempts to return the builder for a supplied path which @@ -14,14 +16,32 @@ import ( func New(ctx context.Context, path string, opts ...InitrdOption) (Initrd, error) { if builder, err := NewFromDockerfile(ctx, path, opts...); err == nil { return builder, nil - } else if builder, err := NewFromTarball(ctx, path, opts...); err == nil { + } else { + log.G(ctx).Tracef("could not build initrd from Dockerfile: %s", err) + } + + if builder, err := NewFromTarball(ctx, path, opts...); err == nil { return builder, nil - } else if builder, err := NewFromFile(ctx, path, opts...); err == nil { + } else { + log.G(ctx).Tracef("could not build initrd from tarball: %s", err) + } + + if builder, err := NewFromFile(ctx, path, opts...); err == nil { return builder, nil - } else if builder, err := NewFromDirectory(ctx, path, opts...); err == nil { + } else { + log.G(ctx).Tracef("could not build initrd from file: %s", err) + } + + if builder, err := NewFromDirectory(ctx, path, opts...); err == nil { return builder, nil - } else if builder, err := NewFromOCIImage(ctx, path, opts...); err == nil { + } else { + log.G(ctx).Tracef("could not build initrd from directory: %s", err) + } + + if builder, err := NewFromOCIImage(ctx, path, opts...); err == nil { return builder, nil + } else { + log.G(ctx).Tracef("could not build initrd from OCI image: %s", err) } return nil, fmt.Errorf("could not determine how to build initrd from: %s", path)