diff --git a/cmd/mkuimage/main.go b/cmd/mkuimage/main.go index 7bfed17..58947f5 100644 --- a/cmd/mkuimage/main.go +++ b/cmd/mkuimage/main.go @@ -55,23 +55,14 @@ func checkArgs(args ...string) error { } func main() { + log.SetFlags(log.Ltime) if err := checkArgs(os.Args...); err != nil { log.Fatal(err) } - var sh string - if golang.Default().GOOS != "plan9" { - sh = "gosh" - } - env := golang.Default(golang.DisableCGO()) f := &mkuimage.Flags{ - Commands: mkuimage.CommandFlags{ - Builder: "bb", - BuildOpts: &golang.BuildOpts{}, - }, - Init: "init", - Shell: sh, + Commands: mkuimage.CommandFlags{Builder: "bb"}, ArchiveFormat: "cpio", OutputFile: defaultFile(env), } @@ -79,14 +70,18 @@ func main() { l := llog.Default() l.RegisterVerboseFlag(flag.CommandLine, "v", slog.LevelDebug) + + tf := &mkuimage.TemplateFlags{} + tf.RegisterFlags(flag.CommandLine) flag.Parse() + // Set defaults. m := []uimage.Modifier{ uimage.WithReplaceEnv(env), uimage.WithBaseArchive(uimage.DefaultRamfs()), uimage.WithCPIOOutput(defaultFile(env)), } - if err := mkuimage.CreateUimage(l, m, f, flag.Args()); err != nil { + if err := mkuimage.CreateUimage(l, m, tf, f, flag.Args()); err != nil { l.Errorf("mkuimage error: %v", err) os.Exit(1) } diff --git a/cmd/mkuimage/main_test.go b/cmd/mkuimage/main_test.go index f53a47d..7aca4bc 100644 --- a/cmd/mkuimage/main_test.go +++ b/cmd/mkuimage/main_test.go @@ -233,15 +233,6 @@ func TestUrootCmdline(t *testing.T) { }, exitCode: 1, }, - { - name: "build invalid", - args: []string{ - "-build=source", - "github.com/u-root/u-root/cmds/core/init", - "github.com/u-root/u-root/cmds/core/echo", - }, - exitCode: 1, - }, { name: "arch invalid preserves temp dir", env: []string{"GOARCH=doesnotexist"}, @@ -263,6 +254,56 @@ func TestUrootCmdline(t *testing.T) { exitCode: 1, wantOutput: dirExists(tempDir), }, + { + 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...) { 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/go.mod b/go.mod index 96372d8..084726a 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.21 require ( github.com/dustin/go-humanize v1.0.1 + github.com/google/go-cmp v0.5.9 github.com/hugelgupf/go-shlex v0.0.0-20200702092117-c80c9d0918fa github.com/u-root/gobusybox/src v0.0.0-20240218001334-a32c1883bffa github.com/u-root/u-root v0.12.0 diff --git a/uimage/mkuimage/cmd.go b/uimage/mkuimage/cmd.go index d91efe3..1f4dc88 100644 --- a/uimage/mkuimage/cmd.go +++ b/uimage/mkuimage/cmd.go @@ -13,6 +13,7 @@ import ( "github.com/u-root/mkuimage/uimage" "github.com/u-root/mkuimage/uimage/builder" + "github.com/u-root/mkuimage/uimage/templates" "github.com/u-root/uio/llog" ) @@ -31,8 +32,37 @@ func isRecommendedVersion(v string) bool { return false } -// CreateUimage creates a uimage from the given base modifiers and flags. -func CreateUimage(l *llog.Logger, base []uimage.Modifier, f *Flags, args []string) error { +func uimageOpts(l *llog.Logger, m []uimage.Modifier, tpl *templates.Templates, f *Flags, conf string, cmds []string) (*uimage.Opts, error) { + // Evaluate template first -- template settings may always be + // appended/overridden by further flag-based settings. + if conf != "" { + mods, err := tpl.Uimage(conf) + if err != nil { + return nil, err + } + l.Debugf("Config: %#v", tpl.Configs[conf]) + m = append(m, mods...) + } + + // Expand command templates. + if tpl != nil { + cmds = tpl.CommandsFor(cmds...) + } + + more, err := f.Modifiers(cmds...) + if err != nil { + return nil, err + } + return uimage.OptionsFor(append(m, more...)...) +} + +// CreateUimage creates a uimage with the given base modifiers and flags, using args as the list of commands. +func CreateUimage(l *llog.Logger, base []uimage.Modifier, tf *TemplateFlags, f *Flags, args []string) error { + tpl, err := tf.Get() + if err != nil { + return fmt.Errorf("failed to get template: %w", err) + } + keepTempDir := f.KeepTempDir if f.TempDir == "" { var err error @@ -53,13 +83,7 @@ func CreateUimage(l *llog.Logger, base []uimage.Modifier, f *Flags, args []strin } } - // Set defaults. - more, err := f.Modifiers(args...) - if err != nil { - return err - } - - opts, err := uimage.OptionsFor(append(base, more...)...) + opts, err := uimageOpts(l, base, tpl, f, tf.Config, args) if err != nil { return err } @@ -70,6 +94,7 @@ func CreateUimage(l *llog.Logger, base []uimage.Modifier, f *Flags, args []strin if env.GOOS != "linux" { l.Warnf("GOOS is not linux. Did you mean to set GOOS=linux?") } + v, err := env.Version() if err != nil { l.Infof("Could not get environment's Go version, using runtime's version: %v", err) diff --git a/uimage/mkuimage/cmd_test.go b/uimage/mkuimage/cmd_test.go new file mode 100644 index 0000000..03febfe --- /dev/null +++ b/uimage/mkuimage/cmd_test.go @@ -0,0 +1,190 @@ +// 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 mkuimage + +import ( + "errors" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/u-root/gobusybox/src/pkg/golang" + "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/templates" + "github.com/u-root/uio/llog" +) + +func TestOpts(t *testing.T) { + for _, tt := range []struct { + name string + m []uimage.Modifier + tpl *templates.Templates + f *Flags + conf string + cmds []string + opts *uimage.Opts + err error + }{ + { + name: "cmdline only", + m: []uimage.Modifier{ + uimage.WithReplaceEnv(golang.Default(golang.DisableCGO())), + uimage.WithCPIOOutput("/tmp/initramfs.cpio"), + uimage.WithTempDir("foo"), + }, + f: &Flags{ + Commands: CommandFlags{Builder: "bb", Mod: "readonly"}, + ArchiveFormat: "cpio", + Init: "init", + Uinit: "gosh script.sh", + OutputFile: "./foo.cpio", + Files: []string{"/bin/bash"}, + }, + cmds: []string{ + "github.com/u-root/u-root/cmds/core/init", + "github.com/u-root/u-root/cmds/core/gosh", + }, + opts: &uimage.Opts{ + Env: golang.Default(golang.DisableCGO()), + InitCmd: "init", + UinitCmd: "gosh", + UinitArgs: []string{"script.sh"}, + OutputFile: &initramfs.CPIOFile{Path: "./foo.cpio"}, + ExtraFiles: []string{"/bin/bash"}, + TempDir: "foo", + Commands: []uimage.Commands{ + { + Builder: &builder.GBBBuilder{}, + Packages: []string{ + "github.com/u-root/u-root/cmds/core/init", + "github.com/u-root/u-root/cmds/core/gosh", + }, + }, + }, + }, + }, + { + name: "template and cmdline combo", + m: []uimage.Modifier{ + uimage.WithReplaceEnv(golang.Default(golang.DisableCGO())), + uimage.WithCPIOOutput("/tmp/initramfs.cpio"), + uimage.WithTempDir("foo"), + }, + tpl: &templates.Templates{ + Configs: map[string]templates.Config{ + "plan9": templates.Config{ + GOOS: "plan9", + GOARCH: "amd64", + BuildTags: []string{"grpcnotrace"}, + Uinit: "gosh script.sh", + Files: []string{"foobar"}, + Commands: []templates.Command{ + { + Builder: "bb", + Commands: []string{ + "github.com/u-root/u-root/cmds/core/gosh", + }, + }, + { + Builder: "binary", + Commands: []string{ + "cmd/test2json", + }, + }, + }, + }, + }, + }, + conf: "plan9", + f: &Flags{ + Commands: CommandFlags{Builder: "bb", Mod: "readonly"}, + ArchiveFormat: "cpio", + Init: "init", + Uinit: "cat", + OutputFile: "./foo.cpio", + Files: []string{"/bin/bash"}, + }, + cmds: []string{ + "github.com/u-root/u-root/cmds/core/init", + "github.com/u-root/u-root/cmds/core/cat", + }, + opts: &uimage.Opts{ + Env: golang.Default(golang.DisableCGO(), golang.WithGOOS("plan9"), golang.WithGOARCH("amd64"), golang.WithBuildTag("grpcnotrace")), + InitCmd: "init", + UinitCmd: "cat", + OutputFile: &initramfs.CPIOFile{Path: "./foo.cpio"}, + ExtraFiles: []string{"foobar", "/bin/bash"}, + TempDir: "foo", + Commands: []uimage.Commands{ + { + Builder: &builder.GBBBuilder{}, + Packages: []string{ + "github.com/u-root/u-root/cmds/core/gosh", + "github.com/u-root/u-root/cmds/core/init", + "github.com/u-root/u-root/cmds/core/cat", + }, + }, + { + Builder: builder.Binary, + Packages: []string{ + "cmd/test2json", + }, + }, + }, + }, + }, + { + name: "expand cmdline config", + m: []uimage.Modifier{ + uimage.WithReplaceEnv(golang.Default(golang.DisableCGO())), + uimage.WithCPIOOutput("/tmp/initramfs.cpio"), + uimage.WithTempDir("foo"), + }, + f: &Flags{ + Commands: CommandFlags{Builder: "bb", Mod: "readonly"}, + ArchiveFormat: "cpio", + OutputFile: "./foo.cpio", + Files: []string{"/bin/bash"}, + }, + tpl: &templates.Templates{ + Commands: map[string][]string{ + "core": []string{ + "github.com/u-root/u-root/cmds/core/init", + "github.com/u-root/u-root/cmds/core/gosh", + }, + }, + }, + cmds: []string{"core", "github.com/u-root/u-root/cmds/core/cat"}, + opts: &uimage.Opts{ + Env: golang.Default(golang.DisableCGO()), + OutputFile: &initramfs.CPIOFile{Path: "./foo.cpio"}, + ExtraFiles: []string{"/bin/bash"}, + TempDir: "foo", + Commands: []uimage.Commands{ + { + Builder: &builder.GBBBuilder{}, + Packages: []string{ + "github.com/u-root/u-root/cmds/core/init", + "github.com/u-root/u-root/cmds/core/gosh", + "github.com/u-root/u-root/cmds/core/cat", + }, + }, + }, + }, + }, + } { + t.Run(tt.name, func(t *testing.T) { + opts, err := uimageOpts(llog.Test(t), tt.m, tt.tpl, tt.f, tt.conf, tt.cmds) + if !errors.Is(err, tt.err) { + t.Errorf("opts = %v, want %v", err, tt.err) + } + if diff := cmp.Diff(opts, tt.opts, cmpopts.IgnoreFields(uimage.Opts{}, "BaseArchive")); diff != "" { + t.Errorf("opts (-got, +want) = %v", diff) + } + }) + } +} diff --git a/uimage/mkuimage/uflags.go b/uimage/mkuimage/uflags.go index 317c96d..7ae9e1c 100644 --- a/uimage/mkuimage/uflags.go +++ b/uimage/mkuimage/uflags.go @@ -14,6 +14,7 @@ import ( "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/templates" ) // CommandFlags are flags related to Go commands to be built by mkuimage. @@ -147,3 +148,29 @@ func (f *Flags) RegisterFlags(fs *flag.FlagSet) { f.Commands.RegisterFlags(fs) } + +// TemplateFlags are flags for uimage config templates. +type TemplateFlags struct { + File string + Config string +} + +// RegisterFlags registers template flags. +func (tc *TemplateFlags) RegisterFlags(f *flag.FlagSet) { + f.StringVar(&tc.Config, "config", "", "Config to pick from templates") + f.StringVar(&tc.File, "config-file", "", "Config file to read from (default: finds .mkuimage.yaml in cwd or parents)") +} + +// Get turns template flags into templates. +func (tc *TemplateFlags) Get() (*templates.Templates, error) { + if tc.File != "" { + return templates.TemplateFromFile(tc.File) + } + + tpl, err := templates.Template() + // Only complain about not finding a template if user requested a templated config. + if err != nil && tc.Config != "" { + return nil, err + } + return tpl, nil +} diff --git a/uimage/templates/templates.go b/uimage/templates/templates.go index 86a9167..5788e6f 100644 --- a/uimage/templates/templates.go +++ b/uimage/templates/templates.go @@ -67,6 +67,12 @@ type Templates struct { // Uimage returns the uimage modifiers for the given templated config name. func (t *Templates) Uimage(config string) ([]uimage.Modifier, error) { + if config == "" { + return nil, nil + } + if t == nil { + return nil, fmt.Errorf("%w: no templates parsed", ErrTemplateNotExist) + } c, ok := t.Configs[config] if !ok { return nil, fmt.Errorf("%w: %q", ErrTemplateNotExist, config) @@ -97,6 +103,9 @@ func (t *Templates) Uimage(config string) ([]uimage.Modifier, error) { // CommandsFor expands commands according to command templates. func (t *Templates) CommandsFor(names ...string) []string { + if t == nil { + return names + } var c []string for _, name := range names { cmds, ok := t.Commands[name] diff --git a/uimage/templates/templates_test.go b/uimage/templates/templates_test.go index fd45477..b87bb92 100644 --- a/uimage/templates/templates_test.go +++ b/uimage/templates/templates_test.go @@ -99,6 +99,16 @@ configs: config: "plan10", err: ErrTemplateNotExist, }, + { + name: "no config", + tpl: ` +configs: + plan9: + goarch: amd64 + goos: plan9 +`, + config: "", + }, } { t.Run(tt.name, func(t *testing.T) { tpl, err := TemplateFrom([]byte(tt.tpl)) diff --git a/uimage/uimage.go b/uimage/uimage.go index dc4d005..2bc88f6 100644 --- a/uimage/uimage.go +++ b/uimage/uimage.go @@ -528,6 +528,8 @@ func WithUinitCommand(cmd string) Modifier { } if len(args) > 1 { opts.UinitArgs = args[1:] + } else { + opts.UinitArgs = nil } return nil } @@ -542,6 +544,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 +559,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 +574,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