package imagepull import ( "context" "errors" "fmt" "os" "strconv" "banger/internal/system" ) // MinExt4Size is the smallest ext4 image we'll create. mkfs.ext4 needs a // few megabytes for its bookkeeping; for a real rootfs the staging tree // will dominate anyway. const MinExt4Size int64 = 1 << 20 * 64 // 64 MiB // BuildExt4 creates outFile as a sparse ext4 image of sizeBytes and // populates it from srcDir using `mkfs.ext4 -F -d`. No mount, no sudo. // // sizeBytes must be at least MinExt4Size. Callers are expected to size // the file with headroom over the staged tree (the daemon orchestrator // does this; this function only enforces a sanity floor). // // The resulting image's file ownership reflects srcDir's on-disk // ownership — see the package doc for the implications. func BuildExt4(ctx context.Context, runner system.CommandRunner, srcDir, outFile string, sizeBytes int64) error { if sizeBytes < MinExt4Size { return fmt.Errorf("ext4 size %d below minimum %d", sizeBytes, MinExt4Size) } info, err := os.Stat(srcDir) if err != nil { return fmt.Errorf("stat source: %w", err) } if !info.IsDir() { return fmt.Errorf("%s is not a directory", srcDir) } if err := os.Remove(outFile); err != nil && !errors.Is(err, os.ErrNotExist) { return err } f, err := os.OpenFile(outFile, os.O_CREATE|os.O_WRONLY|os.O_EXCL, 0o644) if err != nil { return err } if err := f.Truncate(sizeBytes); err != nil { _ = f.Close() _ = os.Remove(outFile) return err } if err := f.Close(); err != nil { _ = os.Remove(outFile) return err } out, runErr := runner.Run(ctx, "mkfs.ext4", "-F", "-q", "-d", srcDir, "-L", "banger-rootfs", "-E", "root_owner=0:0", outFile, strconv.FormatInt(sizeBytes/4096, 10), // size in 4 KiB blocks ) if runErr != nil { _ = os.Remove(outFile) return fmt.Errorf("mkfs.ext4 -d: %w: %s", runErr, string(out)) } return nil }