diff --git a/cmd/mkuimage/main.go b/cmd/mkuimage/main.go index 1782edb..7553fad 100644 --- a/cmd/mkuimage/main.go +++ b/cmd/mkuimage/main.go @@ -18,6 +18,7 @@ import ( "github.com/dustin/go-humanize" "github.com/u-root/gobusybox/src/pkg/golang" "github.com/u-root/mkuimage/uimage" + "github.com/u-root/mkuimage/uimage/templates" "github.com/u-root/mkuimage/uimage/uflags" "github.com/u-root/uio/llog" ) @@ -26,6 +27,11 @@ var ( errEmptyFilesArg = errors.New("empty argument to -files") ) +var ( + config = flag.String("config", "", "Config to pick from templates") + configFile = flag.String("config-file", "", "Config file to read from (default: finds .mkuimage.yaml in cwd or parents)") +) + // 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 @@ -61,19 +67,12 @@ func main() { log.Fatal(err) } - var sh string - if golang.Default().GOOS != "plan9" { - sh = "gosh" - } - 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), } @@ -83,15 +82,32 @@ func main() { l.RegisterVerboseFlag(flag.CommandLine, "v", slog.LevelDebug) flag.Parse() + var tpl *templates.Templates + var err error + if *configFile != "" { + tpl, err = templates.TemplateFromFile(*configFile) + if err != nil { + l.Errorf("Failed to read template: %v", err) + os.Exit(1) + } + } else { + tpl, err = templates.Template() + // Only complain about not finding a template if user requested a templated config. + if err != nil && *config != "" { + l.Errorf("Could not find template file: %v", err) + os.Exit(1) + } + } + 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, f); err != nil { + if err := Main(l, env, tpl, f); err != nil { l.Errorf("Build error: %v", err) - return + os.Exit(1) } if stat, err := os.Stat(f.OutputFile); err == nil && f.ArchiveFormat == "cpio" { @@ -123,7 +139,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, f *uflags.Flags) error { +func Main(l *llog.Logger, env *golang.Environ, tpl *templates.Templates, 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) @@ -162,7 +178,21 @@ func Main(l *llog.Logger, env *golang.Environ, f *uflags.Flags) error { uimage.WithBaseArchive(uimage.DefaultRamfs()), uimage.WithCPIOOutput(defaultFile(env)), } - more, err := f.Modifiers(flag.Args()...) + // Evaluate template first -- template settings may always be + // appended/overridden by further flag-based settings. + if *config != "" { + mods, err := tpl.Uimage(*config) + if err != nil { + return err + } + m = append(m, mods...) + } + pkgs := flag.Args() + // Expand command templates. + if tpl != nil { + pkgs = tpl.CommandsFor(pkgs...) + } + more, err := f.Modifiers(pkgs...) if err != nil { return err } diff --git a/cmd/mkuimage/main_test.go b/cmd/mkuimage/main_test.go index efd5b65..fdcf756 100644 --- a/cmd/mkuimage/main_test.go +++ b/cmd/mkuimage/main_test.go @@ -14,6 +14,7 @@ import ( "os/exec" "path/filepath" "strings" + "syscall" "testing" "github.com/u-root/gobusybox/src/pkg/golang" @@ -65,7 +66,7 @@ func TestUrootCmdline(t *testing.T) { name string env []string args []string - err error + exitCode int validators []itest.ArchiveValidator } @@ -74,7 +75,6 @@ func TestUrootCmdline(t *testing.T) { name: "include one extra file", args: []string{"-nocmd", "-files=/bin/bash"}, env: []string{"GO111MODULE=off"}, - err: nil, validators: []itest.ArchiveValidator{ itest.HasFile{Path: "bin/bash"}, }, @@ -83,7 +83,6 @@ func TestUrootCmdline(t *testing.T) { name: "fix usage of an absolute path", args: []string{"-nocmd", fmt.Sprintf("-files=%s:/bin", sampledir)}, env: []string{"GO111MODULE=off"}, - err: nil, validators: []itest.ArchiveValidator{ itest.HasFile{Path: "/bin/foo"}, itest.HasFile{Path: "/bin/bar"}, @@ -122,7 +121,6 @@ func TestUrootCmdline(t *testing.T) { { name: "uinitcmd", args: []string{"-uinitcmd=echo foobar fuzz", "-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.HasRecord{R: cpio.Symlink("bin/uinit", "../bbin/echo")}, itest.HasContent{ @@ -134,7 +132,6 @@ 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"}, @@ -181,6 +178,56 @@ func TestUrootCmdline(t *testing.T) { "github.com/u-root/u-root/cmds/core/echo", }, }, + { + name: "template config", + args: []string{"-config-file=./testdata/test-config.yaml", "-v", "-config=coreconf"}, + validators: []itest.ArchiveValidator{ + itest.HasRecord{R: cpio.CharDev("dev/tty", 0o666, 5, 0)}, + itest.HasFile{Path: "bbin/bb"}, + itest.HasRecord{R: cpio.Symlink("bbin/echo", "bb")}, + itest.HasRecord{R: cpio.Symlink("bbin/ip", "bb")}, + itest.HasRecord{R: cpio.Symlink("bbin/init", "bb")}, + itest.HasRecord{R: cpio.Symlink("init", "bbin/init")}, + itest.HasRecord{R: cpio.Symlink("bin/sh", "../bbin/echo")}, + itest.HasRecord{R: cpio.Symlink("bin/uinit", "../bbin/echo")}, + itest.HasRecord{R: cpio.Symlink("bin/defaultsh", "../bbin/echo")}, + itest.HasContent{ + Path: "etc/uinit.flags", + Content: "\"script.sh\"", + }, + }, + }, + { + name: "template command", + args: []string{"-config-file=./testdata/test-config.yaml", "-v", "core"}, + validators: []itest.ArchiveValidator{ + itest.HasRecord{R: cpio.CharDev("dev/tty", 0o666, 5, 0)}, + itest.HasFile{Path: "bbin/bb"}, + itest.HasRecord{R: cpio.Symlink("bbin/echo", "bb")}, + itest.HasRecord{R: cpio.Symlink("bbin/ip", "bb")}, + itest.HasRecord{R: cpio.Symlink("bbin/init", "bb")}, + }, + }, + { + name: "template config not found", + args: []string{"-config-file=./testdata/test-config.yaml", "-v", "-config=foobar"}, + exitCode: 1, + }, + { + name: "builder not found", + args: []string{"-v", "build=source"}, + exitCode: 1, + }, + { + name: "template file not found", + args: []string{"-v", "-config-file=./testdata/doesnotexist"}, + exitCode: 1, + }, + { + name: "config not found with no default template", + args: []string{"-v", "-config=foo"}, + exitCode: 1, + }, } for _, tt := range append(noCmdTests, bareTests...) { @@ -192,18 +239,24 @@ func TestUrootCmdline(t *testing.T) { g.Go(func() error { var err error - f1, sum1, err = buildIt(t, execPath, tt.args, tt.env, tt.err, gocoverdir) + f1, sum1, err = buildIt(t, execPath, tt.args, tt.env, gocoverdir) return err }) g.Go(func() error { var err error - f2, sum2, err = buildIt(t, execPath, tt.args, tt.env, tt.err, gocoverdir) + f2, sum2, err = buildIt(t, execPath, tt.args, tt.env, gocoverdir) return err }) - if err := g.Wait(); err != nil { - t.Fatal(err) + var exitErr *exec.ExitError + if err := g.Wait(); errors.As(err, &exitErr) { + if ec := exitErr.Sys().(syscall.WaitStatus).ExitStatus(); ec != tt.exitCode { + t.Errorf("mkuimage exit code = %d, want %d", ec, tt.exitCode) + } + return + } else if err != nil { + return } a, err := itest.ReadArchive(f1.Name()) @@ -225,7 +278,7 @@ func TestUrootCmdline(t *testing.T) { } } -func buildIt(t *testing.T, execPath string, args, env []string, want error, gocoverdir string) (*os.File, []byte, error) { +func buildIt(t *testing.T, execPath string, args, env []string, gocoverdir string) (*os.File, []byte, error) { t.Helper() initramfs, err := os.CreateTemp(t.TempDir(), "u-root-") if err != nil { @@ -241,10 +294,10 @@ func buildIt(t *testing.T, execPath string, args, env []string, want error, goco c.Env = append(os.Environ(), env...) c.Env = append(c.Env, golang.Default().Env()...) c.Env = append(c.Env, "GOCOVERDIR="+gocoverdir) - if out, err := c.CombinedOutput(); err != want { - return nil, nil, fmt.Errorf("Error: %v\nOutput:\n%s", err, out) - } else if err != nil { - return initramfs, nil, err + out, err := c.CombinedOutput() + t.Logf("output for %s:\n%s", t.Name(), out) + if err != nil { + return nil, nil, err } h1 := sha256.New() diff --git a/cmd/mkuimage/testdata/test-config.yaml b/cmd/mkuimage/testdata/test-config.yaml new file mode 100644 index 0000000..c30b2ef --- /dev/null +++ b/cmd/mkuimage/testdata/test-config.yaml @@ -0,0 +1,37 @@ +commands: + core: + - github.com/u-root/u-root/cmds/core/ip + - github.com/u-root/u-root/cmds/core/init + - github.com/u-root/u-root/cmds/core/echo + + minimal: + - github.com/u-root/u-root/cmds/core/ls + - github.com/u-root/u-root/cmds/core/init + + plan9: + - github.com/u-root/u-root/cmds/core/ls + - github.com/u-root/u-root/cmds/core/init + - github.com/u-root/u-root/cmds/core/cat + +configs: + plan9: + goarch: amd64 + goos: plan9 + build_tags: [grpcnotrace] + files: + - /bin/bash + init: init + uinit: cat script.sh + shell: cat + commands: + - builder: bb + commands: [plan9] + + coreconf: + build_tags: [grpcnotrace] + init: init + uinit: echo script.sh + shell: echo + commands: + - builder: bb + commands: [core, minimal] diff --git a/uimage/uimage.go b/uimage/uimage.go index f6cd8c0..ac438e5 100644 --- a/uimage/uimage.go +++ b/uimage/uimage.go @@ -542,6 +542,9 @@ func WithUinitCommand(cmd string) Modifier { // and append arguments from both the kernel command-line // (uroot.uinitargs) as well as those specified in cmd. func WithUinit(arg0 string, args ...string) Modifier { + if arg0 == "" && len(args) == 0 { + return nil + } return func(opts *Opts) error { opts.UinitCmd = arg0 opts.UinitArgs = args @@ -554,6 +557,9 @@ func WithUinit(arg0 string, args ...string) Modifier { // This can be an absolute path or the name of a command included in // Commands. func WithInit(arg0 string) Modifier { + if arg0 == "" { + return nil + } return func(opts *Opts) error { opts.InitCmd = arg0 return nil @@ -566,6 +572,9 @@ func WithInit(arg0 string) Modifier { // This can be an absolute path or the name of a command included in // Commands. func WithShell(arg0 string) Modifier { + if arg0 == "" { + return nil + } return func(opts *Opts) error { opts.DefaultShell = arg0 return nil