From 21ee1248a450913f5e2376d26f860e771320ac3e 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 ++++++++------------------------------ uimage/builder/builder.go | 4 +- uimage/builder/gbb.go | 2 +- uimage/uflags/uflags.go | 139 ++++++++++++++++++++++++++ uimage/uimage.go | 70 ++++++++++++- 5 files changed, 252 insertions(+), 166 deletions(-) create mode 100644 uimage/uflags/uflags.go diff --git a/cmd/mkuimage/main.go b/cmd/mkuimage/main.go index 5ba46d1..45e0849 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,42 @@ 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{ + Env: env, + 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, 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 { + l.Infof("Successfully built %q (size %d bytes -- %s).", f.OutputFile, stat.Size(), humanize.IBytes(uint64(stat.Size()))) } } @@ -176,26 +115,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 +124,8 @@ 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, f *uflags.Flags) error { + env := f.Commands.Env v, err := env.Version() if err != nil { l.Infof("Could not get environment's Go version, using runtime's version: %v", err) @@ -219,73 +139,32 @@ 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 default output. + m := []uimage.Modifier{ + 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/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..4d6de89 --- /dev/null +++ b/uimage/uflags/uflags.go @@ -0,0 +1,139 @@ +// 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 { + Env *golang.Environ + NoCommands bool + Builder string + ShellBang bool + 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") + + if c.Env == nil { + c.Env = golang.Default() + } + c.Env.RegisterFlags(f) + // Register an alias for -go-build-tags for backwards compatibility. + flag.CommandLine.Var((*uflag.Strings)(&c.Env.BuildTags), "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.WithReplaceEnv(c.Env), + }, nil + } + + switch c.Builder { + case "bb", "gbb": + return []uimage.Modifier{ + uimage.WithReplaceEnv(c.Env), + uimage.WithBusyboxCommands(packages...), + uimage.WithShellBang(c.ShellBang), + uimage.WithBusyboxBuildOpts(c.BuildOpts), + }, nil + case "binary": + return []uimage.Modifier{ + uimage.WithReplaceEnv(c.Env), + 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/uimage.go b/uimage/uimage.go index edb2b26..6abd614 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,51 @@ 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 _, cmd := range o.Commands { + if bb, ok := cmd.Builder.(*builder.GBBBuilder); ok { + bb.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 +455,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 +473,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 { @@ -771,7 +839,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 }