From 3a6a5b414b4116aec707349bf851cff7d956e917 Mon Sep 17 00:00:00 2001 From: Chris Koch Date: Sun, 18 Feb 2024 03:18:06 +0000 Subject: [PATCH] Reusable flags package Signed-off-by: Chris Koch --- cmd/mkuimage/main.go | 203 +++++++---------------------------- cmd/mkuimage/main_test.go | 13 ++- uimage/builder/builder.go | 4 +- uimage/builder/gbb.go | 2 +- uimage/uflags/uflags.go | 149 +++++++++++++++++++++++++ uimage/uflags/uflags_test.go | 38 +++++++ uimage/uimage.go | 80 +++++++++++++- uimage/uimage_test.go | 116 ++++++++++++++++++++ 8 files changed, 438 insertions(+), 167 deletions(-) create mode 100644 uimage/uflags/uflags.go create mode 100644 uimage/uflags/uflags_test.go diff --git a/cmd/mkuimage/main.go b/cmd/mkuimage/main.go index 5ba46d1..1782edb 100644 --- a/cmd/mkuimage/main.go +++ b/cmd/mkuimage/main.go @@ -16,78 +16,16 @@ import ( "strings" "github.com/dustin/go-humanize" - "github.com/hugelgupf/go-shlex" "github.com/u-root/gobusybox/src/pkg/golang" - "github.com/u-root/gobusybox/src/pkg/uflag" "github.com/u-root/mkuimage/uimage" - "github.com/u-root/mkuimage/uimage/builder" - "github.com/u-root/mkuimage/uimage/initramfs" + "github.com/u-root/mkuimage/uimage/uflags" "github.com/u-root/uio/llog" ) -// multiFlag is used for flags that support multiple invocations, e.g. -files. -type multiFlag []string - -func (m *multiFlag) String() string { - return fmt.Sprint(*m) -} - -// Set implements flag.Value.Set. -func (m *multiFlag) Set(value string) error { - *m = append(*m, value) - return nil -} - var ( errEmptyFilesArg = errors.New("empty argument to -files") ) -// Flags for u-root builder. -var ( - build, format, tmpDir, basePath, outputPath *string - uinitCmd, initCmd *string - defaultShell *string - useExistingInit *bool - noCommands *bool - extraFiles multiFlag - shellbang *bool - // For the new "filepath only" logic. - urootSourceDir *string -) - -func init() { - var sh string - switch golang.Default().GOOS { - case "plan9": - sh = "" - default: - sh = "elvish" - } - - build = flag.String("build", "gbb", "u-root build format (e.g. bb/gbb or binary).") - format = flag.String("format", "cpio", "Archival format.") - - tmpDir = flag.String("tmpdir", "", "Temporary directory to put binaries in.") - - basePath = flag.String("base", "", "Base archive to add files to. By default, this is a couple of directories like /bin, /etc, etc. u-root has a default internally supplied set of files; use base=/dev/null if you don't want any base files.") - useExistingInit = flag.Bool("useinit", false, "Use existing init from base archive (only if --base was specified).") - outputPath = flag.String("o", "", "Path to output initramfs file.") - - initCmd = flag.String("initcmd", "init", "Symlink target for /init. Can be an absolute path or a u-root command name. Use initcmd=\"\" if you don't want the symlink.") - uinitCmd = flag.String("uinitcmd", "", "Symlink target and arguments for /bin/uinit. Can be an absolute path or a u-root command name. Use uinitcmd=\"\" if you don't want the symlink. E.g. -uinitcmd=\"echo foobar\"") - defaultShell = flag.String("defaultsh", sh, "Default shell. Can be an absolute path or a u-root command name. Use defaultsh=\"\" if you don't want the symlink.") - - noCommands = flag.Bool("nocmd", false, "Build no Go commands; initramfs only") - - flag.Var(&extraFiles, "files", "Additional files, directories, and binaries (with their ldd dependencies) to add to archive. Can be specified multiple times.") - - shellbang = flag.Bool("shellbang", false, "Use #! instead of symlinks for busybox") - - // Flag for the new filepath only mode. This will be required to find the u-root commands and make templates work - // In almost every case, "." is fine. - urootSourceDir = flag.String("uroot-source", ".", "Path to the locally checked out u-root source tree in case commands from there are desired.") -} - // checkArgs checks for common mistakes that cause confusion. // 1. -files as the last argument // 2. -files followed by any switch, indicating a shell expansion problem @@ -123,41 +61,41 @@ func main() { log.Fatal(err) } - gbbOpts := &golang.BuildOpts{} - gbbOpts.RegisterFlags(flag.CommandLine) - // Register an alias for -go-no-strip for backwards compatibility. - flag.CommandLine.BoolVar(&gbbOpts.NoStrip, "no-strip", false, "Build unstripped binaries") + var sh string + if golang.Default().GOOS != "plan9" { + sh = "gosh" + } - env := golang.Default() - env.RegisterFlags(flag.CommandLine) - tags := (*uflag.Strings)(&env.BuildTags) - flag.CommandLine.Var(tags, "tags", "Go build tags -- repeat the flag for multiple values") + env := golang.Default(golang.DisableCGO()) + f := &uflags.Flags{ + Commands: uflags.CommandFlags{ + Builder: "bb", + BuildOpts: &golang.BuildOpts{}, + }, + Init: "init", + Shell: sh, + ArchiveFormat: "cpio", + OutputFile: defaultFile(env), + } + f.RegisterFlags(flag.CommandLine) l := llog.Default() l.RegisterVerboseFlag(flag.CommandLine, "v", slog.LevelDebug) flag.Parse() - if usrc := os.Getenv("UROOT_SOURCE"); usrc != "" && *urootSourceDir == "" { - *urootSourceDir = usrc - } - - if env.CgoEnabled { - l.Infof("Disabling CGO for u-root...") - env.CgoEnabled = false - } l.Infof("Build environment: %s", env) if env.GOOS != "linux" { l.Warnf("GOOS is not linux. Did you mean to set GOOS=linux?") } // Main is in a separate functions so defers run on return. - if err := Main(l, env, gbbOpts); err != nil { + if err := Main(l, env, f); err != nil { l.Errorf("Build error: %v", err) return } - if stat, err := os.Stat(*outputPath); err == nil { - l.Infof("Successfully built %q (size %d bytes -- %s).", *outputPath, stat.Size(), humanize.IBytes(uint64(stat.Size()))) + if stat, err := os.Stat(f.OutputFile); err == nil && f.ArchiveFormat == "cpio" { + l.Infof("Successfully built %q (size %d bytes -- %s).", f.OutputFile, stat.Size(), humanize.IBytes(uint64(stat.Size()))) } } @@ -176,26 +114,6 @@ func isRecommendedVersion(v string) bool { return false } -func getReader(format string, path string) initramfs.ReadOpener { - switch format { - case "cpio": - return &initramfs.CPIOFile{Path: path} - default: - return nil - } -} - -func getWriter(format string, path string) initramfs.WriteOpener { - switch format { - case "cpio": - return &initramfs.CPIOFile{Path: path} - case "dir": - return &initramfs.Dir{Path: path} - default: - return nil - } -} - func defaultFile(env *golang.Environ) string { if len(env.GOOS) == 0 || len(env.GOARCH) == 0 { return "/tmp/initramfs.cpio" @@ -205,7 +123,7 @@ func defaultFile(env *golang.Environ) string { // Main is a separate function so defers are run on return, which they wouldn't // on exit. -func Main(l *llog.Logger, env *golang.Environ, buildOpts *golang.BuildOpts) error { +func Main(l *llog.Logger, env *golang.Environ, f *uflags.Flags) error { v, err := env.Version() if err != nil { l.Infof("Could not get environment's Go version, using runtime's version: %v", err) @@ -219,73 +137,34 @@ func Main(l *llog.Logger, env *golang.Environ, buildOpts *golang.BuildOpts) erro v, recommendedVersions, recommendedVersions[0]) } - if *outputPath == "" && *format == "cpio" { - *outputPath = defaultFile(env) - } - output := getWriter(*format, *outputPath) - - var base initramfs.ReadOpener - base = &initramfs.Archive{Archive: uimage.DefaultRamfs()} - if *basePath != "" { - base = getReader(*format, *basePath) - } - - tempDir := *tmpDir - if tempDir == "" { + if f.TempDir == "" { var err error - tempDir, err = os.MkdirTemp("", "u-root") + f.TempDir, err = os.MkdirTemp("", "u-root") if err != nil { return err } - defer os.RemoveAll(tempDir) - } else if _, err := os.Stat(tempDir); os.IsNotExist(err) { - if err := os.MkdirAll(tempDir, 0o755); err != nil { - return fmt.Errorf("temporary directory %q did not exist; tried to mkdir but failed: %v", tempDir, err) + if f.KeepTempDir { + defer func() { + l.Infof("Keeping temp dir %s", f.TempDir) + }() + } else { + defer os.RemoveAll(f.TempDir) } - } - - var c []uimage.Commands - if !*noCommands { - var b builder.Builder - switch *build { - case "bb", "gbb": - b = builder.GBBBuilder{ShellBang: *shellbang} - case "binary": - b = builder.BinaryBuilder{} - default: - return fmt.Errorf("could not find builder %q", *build) + } else if _, err := os.Stat(f.TempDir); os.IsNotExist(err) { + if err := os.MkdirAll(f.TempDir, 0o755); err != nil { + return fmt.Errorf("temporary directory %q did not exist; tried to mkdir but failed: %v", f.TempDir, err) } - - pkgs := flag.Args() - if len(pkgs) == 0 { - pkgs = []string{"github.com/u-root/u-root/cmds/core/*"} - } - - c = append(c, uimage.Commands{ - Builder: b, - Packages: pkgs, - BuildOpts: buildOpts, - }) } - opts := uimage.Opts{ - Env: env, - Commands: c, - UrootSource: *urootSourceDir, - TempDir: tempDir, - ExtraFiles: extraFiles, - OutputFile: output, - BaseArchive: base, - UseExistingInit: *useExistingInit, - InitCmd: *initCmd, - DefaultShell: *defaultShell, + // Set defaults. + m := []uimage.Modifier{ + uimage.WithReplaceEnv(env), + uimage.WithBaseArchive(uimage.DefaultRamfs()), + uimage.WithCPIOOutput(defaultFile(env)), } - uinitArgs := shlex.Split(*uinitCmd) - if len(uinitArgs) > 0 { - opts.UinitCmd = uinitArgs[0] - } - if len(uinitArgs) > 1 { - opts.UinitArgs = uinitArgs[1:] + more, err := f.Modifiers(flag.Args()...) + if err != nil { + return err } - return uimage.CreateInitramfs(l, opts) + return uimage.Create(l, append(m, more...)...) } diff --git a/cmd/mkuimage/main_test.go b/cmd/mkuimage/main_test.go index af74d05..efd5b65 100644 --- a/cmd/mkuimage/main_test.go +++ b/cmd/mkuimage/main_test.go @@ -42,7 +42,8 @@ func TestUrootCmdline(t *testing.T) { } gocoverdir := filepath.Join(wd, "cover") - if err := os.Mkdir(gocoverdir, 0o777); err != nil && !os.IsNotExist(err) { + os.RemoveAll(gocoverdir) + if err := os.Mkdir(gocoverdir, 0o777); err != nil && !os.IsExist(err) { t.Fatal(err) } @@ -130,6 +131,16 @@ func TestUrootCmdline(t *testing.T) { }, }, }, + { + name: "binary build", + args: []string{"-build=binary", "-defaultsh=", "github.com/u-root/u-root/cmds/core/init", "github.com/u-root/u-root/cmds/core/echo"}, + err: nil, + validators: []itest.ArchiveValidator{ + itest.HasFile{Path: "bin/init"}, + itest.HasFile{Path: "bin/echo"}, + itest.HasRecord{R: cpio.CharDev("dev/tty", 0o666, 5, 0)}, + }, + }, { name: "hosted mode", args: append([]string{"-base=/dev/null", "-defaultsh=", "-initcmd="}, twocmds...), diff --git a/uimage/builder/builder.go b/uimage/builder/builder.go index 4ee070a..6da55e0 100644 --- a/uimage/builder/builder.go +++ b/uimage/builder/builder.go @@ -16,10 +16,10 @@ import ( var ( // Busybox is a shared GBBBuilder instance. - Busybox = GBBBuilder{} + Busybox = &GBBBuilder{} // Binary is a shared BinaryBuilder instance. - Binary = BinaryBuilder{} + Binary = &BinaryBuilder{} ) // Possible build errors. diff --git a/uimage/builder/gbb.go b/uimage/builder/gbb.go index a6d7e79..1794f27 100644 --- a/uimage/builder/gbb.go +++ b/uimage/builder/gbb.go @@ -48,7 +48,7 @@ func (GBBBuilder) DefaultBinaryDir() string { } // Build is an implementation of Builder.Build for a busybox-like initramfs. -func (b GBBBuilder) Build(l *llog.Logger, af *initramfs.Files, opts Opts) error { +func (b *GBBBuilder) Build(l *llog.Logger, af *initramfs.Files, opts Opts) error { // Build the busybox binary. if len(opts.TempDir) == 0 { return ErrTempDirMissing diff --git a/uimage/uflags/uflags.go b/uimage/uflags/uflags.go new file mode 100644 index 0000000..8121dab --- /dev/null +++ b/uimage/uflags/uflags.go @@ -0,0 +1,149 @@ +// Copyright 2024 the u-root Authors. All rights reserved +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package uflags defines mkuimage flags. +package uflags + +import ( + "flag" + "fmt" + "os" + + "github.com/u-root/gobusybox/src/pkg/golang" + "github.com/u-root/gobusybox/src/pkg/uflag" + "github.com/u-root/mkuimage/uimage" + "github.com/u-root/mkuimage/uimage/builder" +) + +// CommandFlags are flags related to Go commands to be built by mkuimage. +type CommandFlags struct { + NoCommands bool + Builder string + ShellBang bool + Mod golang.ModBehavior + BuildTags []string + BuildOpts *golang.BuildOpts +} + +// RegisterFlags registers flags related to Go commands being built. +func (c *CommandFlags) RegisterFlags(f *flag.FlagSet) { + f.StringVar(&c.Builder, "build", c.Builder, "uimage command build format (e.g. bb/gbb or binary).") + f.BoolVar(&c.NoCommands, "nocmd", c.NoCommands, "Build no Go commands; initramfs only") + f.BoolVar(&c.ShellBang, "shellbang", c.ShellBang, "Use #! instead of symlinks for busybox") + if c.BuildOpts == nil { + c.BuildOpts = &golang.BuildOpts{} + } + c.BuildOpts.RegisterFlags(f) + // Register an alias for -go-no-strip for backwards compatibility. + f.BoolVar(&c.BuildOpts.NoStrip, "no-strip", false, "Build unstripped binaries") + + // Flags for golang.Environ. + defMod := "" + if golang.Default().GO111MODULE != "off" { + defMod = "readonly" + } + f.StringVar((*string)(&c.Mod), "go-mod", defMod, "Value of -mod to go commands (allowed: (empty), vendor, mod, readonly)") + // Register an alias for -go-build-tags for backwards compatibility. + f.Var((*uflag.Strings)(&c.BuildTags), "tags", "Go build tags -- repeat the flag for multiple values") + f.Var((*uflag.Strings)(&c.BuildTags), "go-build-tags", "Go build tags -- repeat the flag for multiple values") +} + +// Modifiers turns the flag values into uimage modifiers. +func (c *CommandFlags) Modifiers(packages ...string) ([]uimage.Modifier, error) { + if c.NoCommands { + // Later modifiers may still add packages, so let's set the right environment. + return []uimage.Modifier{ + uimage.WithEnv(golang.WithBuildTag(c.BuildTags...), func(e *golang.Environ) { + e.Mod = c.Mod + }), + }, nil + } + + switch c.Builder { + case "bb", "gbb": + return []uimage.Modifier{ + uimage.WithEnv(golang.WithBuildTag(c.BuildTags...), func(e *golang.Environ) { + e.Mod = c.Mod + }), + uimage.WithBusyboxCommands(packages...), + uimage.WithShellBang(c.ShellBang), + uimage.WithBusyboxBuildOpts(c.BuildOpts), + }, nil + case "binary": + return []uimage.Modifier{ + uimage.WithEnv(golang.WithBuildTag(c.BuildTags...), func(e *golang.Environ) { + e.Mod = c.Mod + }), + uimage.WithCommands(c.BuildOpts, builder.Binary, packages...), + }, nil + default: + return nil, fmt.Errorf("%w: could not find binary builder format %q", os.ErrInvalid, c.Builder) + } +} + +// Flags are mkuimage command-line flags. +type Flags struct { + TempDir string + KeepTempDir bool + + Init string + Uinit string + Shell string + + Files []string + + BaseArchive string + ArchiveFormat string + OutputFile string + UseExistingInit bool + + Commands CommandFlags +} + +// Modifiers return uimage modifiers created from the flags. +func (f *Flags) Modifiers(packages ...string) ([]uimage.Modifier, error) { + m := []uimage.Modifier{ + uimage.WithTempDir(f.TempDir), + uimage.WithInit(f.Init), + uimage.WithUinitCommand(f.Uinit), + uimage.WithShell(f.Shell), + uimage.WithFiles(f.Files...), + // ArchiveFormat does not determine this, as only CPIO is supported. + uimage.WithBaseFile(f.BaseArchive), + uimage.WithExistingInit(f.UseExistingInit), + } + switch f.ArchiveFormat { + case "cpio": + m = append(m, uimage.WithCPIOOutput(f.OutputFile)) + case "dir": + m = append(m, uimage.WithOutputDir(f.OutputFile)) + default: + return nil, fmt.Errorf("%w: could not find output format %q", os.ErrInvalid, f.ArchiveFormat) + } + more, err := f.Commands.Modifiers(packages...) + if err != nil { + return nil, err + } + return append(m, more...), nil +} + +// RegisterFlags registers flags. +func (f *Flags) RegisterFlags(fs *flag.FlagSet) { + fs.StringVar(&f.TempDir, "tmp-dir", "", "Temporary directory to build binary and archive in. Deleted after build if --keep-tmp-dir is not set.") + fs.BoolVar(&f.KeepTempDir, "keep-tmp-dir", f.KeepTempDir, "Keep temporary directory after build") + + fs.StringVar(&f.Init, "initcmd", f.Init, "Symlink target for /init. Can be an absolute path or a Go command name. Use initcmd=\"\" if you don't want the symlink.") + fs.StringVar(&f.Uinit, "uinitcmd", f.Uinit, "Symlink target and arguments for /bin/uinit. Can be an absolute path or a Go command name, followed by command-line args. Use uinitcmd=\"\" if you don't want the symlink. E.g. -uinitcmd=\"echo foobar\"") + fs.StringVar(&f.Shell, "defaultsh", f.Shell, "Default shell. Can be an absolute path or a Go command name. Use defaultsh=\"\" if you don't want the symlink.") + + fs.Var((*uflag.Strings)(&f.Files), "files", "Additional files, directories, and binaries (with their ldd dependencies) to add to archive. Can be specified multiple times.") + + fs.StringVar(&f.BaseArchive, "base", f.BaseArchive, "Base archive to add files to. By default, this is a couple of directories like /bin, /etc, etc. Has a default internally supplied set of files; use base=/dev/null if you don't want any base files.") + fs.StringVar(&f.ArchiveFormat, "format", f.ArchiveFormat, "Archival input (for -base) and output (for -o) format.") + fs.StringVar(&f.OutputFile, "o", f.OutputFile, "Path to output initramfs file.") + fs.BoolVar(&f.UseExistingInit, "useinit", f.UseExistingInit, "Use existing init from base archive (only if --base was specified).") + fs.BoolVar(&f.UseExistingInit, "use-init", f.UseExistingInit, "Use existing init from base archive (only if --base was specified).") + + f.Commands.RegisterFlags(fs) +} diff --git a/uimage/uflags/uflags_test.go b/uimage/uflags/uflags_test.go new file mode 100644 index 0000000..153355d --- /dev/null +++ b/uimage/uflags/uflags_test.go @@ -0,0 +1,38 @@ +// Copyright 2024 the u-root Authors. All rights reserved +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uflags + +import ( + "errors" + "flag" + "os" + "testing" +) + +func TestFlagErrors(t *testing.T) { + for _, tt := range []struct { + input []string + err error + }{ + { + input: []string{"-build=else", "-format=cpio"}, + err: os.ErrInvalid, + }, + { + input: []string{"-format=else", "-build=bb"}, + err: os.ErrInvalid, + }, + } { + fs := flag.NewFlagSet("test", flag.ContinueOnError) + f := &Flags{} + f.RegisterFlags(fs) + if err := fs.Parse(tt.input); err != nil { + t.Fatal(err) + } + if _, err := f.Modifiers(); !errors.Is(err, tt.err) { + t.Errorf("Modifiers = %v, want %v", err, tt.err) + } + } +} diff --git a/uimage/uimage.go b/uimage/uimage.go index edb2b26..f6cd8c0 100644 --- a/uimage/uimage.go +++ b/uimage/uimage.go @@ -286,6 +286,14 @@ func WithSkipLDD() Modifier { } } +// WithReplaceEnv replaces the Go build environment. +func WithReplaceEnv(env *golang.Environ) Modifier { + return func(o *Opts) error { + o.Env = env + return nil + } +} + // WithEnv alters the Go build environment (e.g. build tags, GOARCH, GOOS env vars). func WithEnv(gopts ...golang.Opt) Modifier { return func(o *Opts) error { @@ -370,6 +378,55 @@ func WithBusyboxCommands(cmd ...string) Modifier { } } +// WithShellBang directs the busybox builder to use #! instead of symlinks. +func WithShellBang(b bool) Modifier { + return func(o *Opts) error { + for i, cmd := range o.Commands { + if _, ok := cmd.Builder.(*builder.GBBBuilder); ok { + // Make a copy, because the same object may + // have been used in other builds. + o.Commands[i].Builder = &builder.GBBBuilder{ + ShellBang: b, + } + return nil + } + } + + // Otherwise, add an empty builder with no packages. + // AddBusyboxCommands/WithBusyboxCommands will append to this. + // + // Yeah, it's a hack, sue me. + o.Commands = append(o.Commands, Commands{ + Builder: &builder.GBBBuilder{ShellBang: b}, + }) + return nil + } +} + +// WithBusyboxBuildOpts directs the busybox builder to use the given build opts. +// +// Overrides any previously defined build options. +func WithBusyboxBuildOpts(g *golang.BuildOpts) Modifier { + return func(o *Opts) error { + for i, cmd := range o.Commands { + if _, ok := cmd.Builder.(*builder.GBBBuilder); ok { + o.Commands[i].BuildOpts = g + return nil + } + } + + // Otherwise, add an empty builder with no packages. + // AddBusyboxCommands/WithBusyboxCommands will append to this. + // + // Yeah, it's a hack, sue me. + o.Commands = append(o.Commands, Commands{ + Builder: &builder.GBBBuilder{}, + BuildOpts: g, + }) + return nil + } +} + // WithBinaryCommands adds Go commands to compile as individual binaries and // add to the archive. // @@ -402,6 +459,16 @@ func WithOutput(w initramfs.WriteOpener) Modifier { } } +// WithExistingInit sets whether an existing init from BaseArchive should remain the init. +// +// If not, it will be renamed inito. +func WithExistingInit(use bool) Modifier { + return func(o *Opts) error { + o.UseExistingInit = use + return nil + } +} + // WithCPIOOutput sets the archive output file to be a CPIO created at the given path. func WithCPIOOutput(path string) Modifier { if path == "" { @@ -410,6 +477,11 @@ func WithCPIOOutput(path string) Modifier { return WithOutput(&initramfs.CPIOFile{Path: path}) } +// WithOutputDir sets the archive output to be in the given directory. +func WithOutputDir(path string) Modifier { + return WithOutput(&initramfs.Dir{Path: path}) +} + // WithBase is an existing initramfs to include in the resulting initramfs. func WithBase(base initramfs.ReadOpener) Modifier { return func(o *Opts) error { @@ -542,6 +614,9 @@ func CreateInitramfs(l *llog.Logger, opts Opts) error { // Expand commands. for index, cmds := range opts.Commands { + if len(cmds.Packages) == 0 { + continue + } paths, err := findpkg.ResolveGlobs(l.AtLevel(slog.LevelInfo), env, lookupEnv, cmds.Packages) if err != nil { return fmt.Errorf("%w: %w", errResolvePackage, err) @@ -551,6 +626,9 @@ func CreateInitramfs(l *llog.Logger, opts Opts) error { // Add each build mode's commands to the archive. for _, cmds := range opts.Commands { + if len(cmds.Packages) == 0 { + continue + } builderTmpDir, err := os.MkdirTemp(opts.TempDir, "builder") if err != nil { return err @@ -771,7 +849,7 @@ func (o *Opts) AddCommands(c ...Commands) { // AddBusyboxCommands adds Go commands to the busybox build. func (o *Opts) AddBusyboxCommands(pkgs ...string) { for i, cmds := range o.Commands { - if cmds.Builder == builder.Busybox { + if _, ok := cmds.Builder.(*builder.GBBBuilder); ok { o.Commands[i].Packages = append(o.Commands[i].Packages, pkgs...) return } diff --git a/uimage/uimage_test.go b/uimage/uimage_test.go index cdb4838..6ab14a3 100644 --- a/uimage/uimage_test.go +++ b/uimage/uimage_test.go @@ -9,6 +9,7 @@ import ( "fmt" "os" "path/filepath" + "reflect" "syscall" "testing" @@ -992,6 +993,56 @@ func TestCreateInitramfsWithAPI(t *testing.T) { itest.IsEmpty{}, }, }, + { + name: "shellbang", + opts: []Modifier{ + WithTempDir(dir), + WithEnv(golang.DisableCGO()), + WithShellBang(true), + WithBusyboxCommands( + "github.com/u-root/u-root/cmds/core/init", + "github.com/u-root/u-root/cmds/core/ls", + ), + }, + validators: []itest.ArchiveValidator{ + itest.HasFile{Path: "bbin/bb"}, + itest.HasRecord{R: cpio.StaticFile("bbin/init", "#!/bbin/bb #!init\n", 0o755)}, + itest.HasRecord{R: cpio.StaticFile("bbin/ls", "#!/bbin/bb #!ls\n", 0o755)}, + }, + }, + { + name: "shellbang after placement", + opts: []Modifier{ + WithTempDir(dir), + WithEnv(golang.DisableCGO()), + WithBusyboxCommands( + "github.com/u-root/u-root/cmds/core/init", + "github.com/u-root/u-root/cmds/core/ls", + ), + // Putting this after WithBusyboxCommands should not change the outcome. + WithShellBang(true), + }, + validators: []itest.ArchiveValidator{ + itest.HasFile{Path: "bbin/bb"}, + itest.HasRecord{R: cpio.StaticFile("bbin/init", "#!/bbin/bb #!init\n", 0o755)}, + itest.HasRecord{R: cpio.StaticFile("bbin/ls", "#!/bbin/bb #!ls\n", 0o755)}, + }, + }, + { + name: "shellbang no busybox", + opts: []Modifier{ + WithTempDir(dir), + WithEnv(golang.DisableCGO()), + WithBinaryCommands( + "github.com/u-root/u-root/cmds/core/init", + ), + // Putting this after WithBusyboxCommands should not change the outcome. + WithShellBang(true), + }, + validators: []itest.ArchiveValidator{ + itest.HasFile{Path: "bin/init"}, + }, + }, } { t.Run(fmt.Sprintf("Test %d [%s]", i, tt.name), func(t *testing.T) { archive := cpio.InMemArchive() @@ -1016,3 +1067,68 @@ func TestCreateInitramfsWithAPI(t *testing.T) { }) } } + +func TestOptionsFor(t *testing.T) { + for _, tt := range []struct { + name string + mods []Modifier + want *Opts + }{ + { + name: "buildopts after", + mods: []Modifier{ + WithBusyboxCommands( + "github.com/u-root/u-root/cmds/core/init", + "github.com/u-root/u-root/cmds/core/ls", + ), + WithBusyboxBuildOpts(&golang.BuildOpts{NoStrip: true}), + }, + want: &Opts{ + Env: golang.Default(), + Commands: []Commands{ + { + Builder: builder.Busybox, + Packages: []string{ + "github.com/u-root/u-root/cmds/core/init", + "github.com/u-root/u-root/cmds/core/ls", + }, + BuildOpts: &golang.BuildOpts{NoStrip: true}, + }, + }, + }, + }, + { + name: "buildopts before", + mods: []Modifier{ + WithBusyboxBuildOpts(&golang.BuildOpts{NoStrip: true}), + WithBusyboxCommands( + "github.com/u-root/u-root/cmds/core/init", + "github.com/u-root/u-root/cmds/core/ls", + ), + }, + want: &Opts{ + Env: golang.Default(), + Commands: []Commands{ + { + Builder: builder.Busybox, + Packages: []string{ + "github.com/u-root/u-root/cmds/core/init", + "github.com/u-root/u-root/cmds/core/ls", + }, + BuildOpts: &golang.BuildOpts{NoStrip: true}, + }, + }, + }, + }, + } { + t.Run(tt.name, func(t *testing.T) { + got, err := OptionsFor(tt.mods...) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("OptionsFor = \n%#v, want\n%#v", got, tt.want) + } + }) + } +}