From bb049b67158c24c4f547eff7cd76c982b0a5a975 Mon Sep 17 00:00:00 2001 From: Chris Koch Date: Sat, 17 Feb 2024 20:51:19 +0000 Subject: [PATCH] uimage template configs Signed-off-by: Chris Koch --- go.mod | 1 + go.sum | 2 + uimage/templates/templates.go | 137 +++++++++++++++++++++++++++ uimage/templates/templates_test.go | 144 +++++++++++++++++++++++++++++ 4 files changed, 284 insertions(+) create mode 100644 uimage/templates/templates.go create mode 100644 uimage/templates/templates_test.go diff --git a/go.mod b/go.mod index bb1c1b2..96372d8 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( golang.org/x/sync v0.6.0 golang.org/x/sys v0.16.0 golang.org/x/tools v0.17.0 + gopkg.in/yaml.v3 v3.0.0 ) require ( diff --git a/go.sum b/go.sum index bfcee98..9eab4a4 100644 --- a/go.sum +++ b/go.sum @@ -67,6 +67,8 @@ golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc= golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA= gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= src.elv.sh v0.16.0-rc1.0.20220116211855-fda62502ad7f h1:pjVeIo9Ba6K1Wy+rlwX91zT7A+xGEmxiNRBdN04gDTQ= diff --git a/uimage/templates/templates.go b/uimage/templates/templates.go new file mode 100644 index 0000000..86a9167 --- /dev/null +++ b/uimage/templates/templates.go @@ -0,0 +1,137 @@ +// 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 templates defines a uimage template configuration file parser. +package templates + +import ( + "errors" + "fmt" + "os" + "path/filepath" + + "github.com/u-root/gobusybox/src/pkg/golang" + "github.com/u-root/mkuimage/uimage" + "gopkg.in/yaml.v3" +) + +// ErrTemplateNotExist is returned when the given config name did not exist. +var ErrTemplateNotExist = errors.New("config template does not exist") + +func findConfigFile(name string) (string, error) { + dir, err := os.Getwd() + if err != nil { + return "", err + } + for dir != "/" { + p := filepath.Join(dir, name) + if _, err := os.Stat(p); err == nil { + return p, nil + } + dir = filepath.Dir(dir) + } + return "", fmt.Errorf("%w: could not find %s in current directory or any parent", os.ErrNotExist, name) +} + +// Command represents commands to build. +type Command struct { + // Builder is bb, gbb, or binary. + // + // Defaults to bb if not given. + Builder string + + // Commands are commands or template names. + Commands []string +} + +// Config is a mkuimage build configuration. +type Config struct { + GOOS string + GOARCH string + BuildTags []string `yaml:"build_tags"` + Commands []Command + Files []string + Init string + Uinit string + Shell string +} + +// Templates are a set of mkuimage build configs and command templates. +type Templates struct { + Configs map[string]Config + + // Commands defines a set of command template name -> commands to expand. + Commands map[string][]string +} + +// Uimage returns the uimage modifiers for the given templated config name. +func (t *Templates) Uimage(config string) ([]uimage.Modifier, error) { + c, ok := t.Configs[config] + if !ok { + return nil, fmt.Errorf("%w: %q", ErrTemplateNotExist, config) + } + m := []uimage.Modifier{ + uimage.WithFiles(c.Files...), + uimage.WithInit(c.Init), + uimage.WithUinitCommand(c.Uinit), + uimage.WithShell(c.Shell), + uimage.WithEnv( + golang.WithGOOS(c.GOOS), + golang.WithGOARCH(c.GOARCH), + golang.WithBuildTag(c.BuildTags...), + ), + } + for _, cmds := range c.Commands { + switch cmds.Builder { + case "binary": + m = append(m, uimage.WithBinaryCommands(t.CommandsFor(cmds.Commands...)...)) + case "bb", "gbb": + fallthrough + default: + m = append(m, uimage.WithBusyboxCommands(t.CommandsFor(cmds.Commands...)...)) + } + } + return m, nil +} + +// CommandsFor expands commands according to command templates. +func (t *Templates) CommandsFor(names ...string) []string { + var c []string + for _, name := range names { + cmds, ok := t.Commands[name] + if ok { + c = append(c, cmds...) + } else { + c = append(c, name) + } + } + return c +} + +// TemplateFrom parses a template from bytes. +func TemplateFrom(b []byte) (*Templates, error) { + var tpl Templates + if err := yaml.Unmarshal(b, &tpl); err != nil { + return nil, err + } + return &tpl, nil +} + +// Template parses the first file named .mkuimage.yaml in the current directory or any of its parents. +func Template() (*Templates, error) { + p, err := findConfigFile(".mkuimage.yaml") + if err != nil { + return nil, fmt.Errorf("%w: no templates found", os.ErrNotExist) + } + return TemplateFromFile(p) +} + +// TemplateFromFile parses a template from the given file. +func TemplateFromFile(p string) (*Templates, error) { + b, err := os.ReadFile(p) + if err != nil { + return nil, err + } + return TemplateFrom(b) +} diff --git a/uimage/templates/templates_test.go b/uimage/templates/templates_test.go new file mode 100644 index 0000000..fd45477 --- /dev/null +++ b/uimage/templates/templates_test.go @@ -0,0 +1,144 @@ +// 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 templates + +import ( + "errors" + "os" + "path/filepath" + "reflect" + "testing" + + "github.com/u-root/gobusybox/src/pkg/golang" + "github.com/u-root/mkuimage/uimage" + "github.com/u-root/mkuimage/uimage/builder" +) + +func TestMods(t *testing.T) { + for _, tt := range []struct { + name string + tpl string + config string + want *uimage.Opts + err error + }{ + { + name: "ok", + tpl: ` +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/gosh + + minimal: + - github.com/u-root/u-root/cmds/core/ls + - github.com/u-root/u-root/cmds/core/init + +configs: + plan9: + goarch: amd64 + goos: plan9 + build_tags: [grpcnotrace] + files: + - /bin/bash + init: init + uinit: gosh script.sh + shell: gosh + commands: + - builder: bb + commands: [core, minimal] + - builder: bb + commands: [./u-bmc/cmd/foo] + - builder: binary + commands: [./u-bmc/cmd/bar] + - builder: binary + commands: [cmd/test2json] +`, + config: "plan9", + want: &uimage.Opts{ + Env: golang.Default(golang.WithGOARCH("amd64"), golang.WithGOOS("plan9"), golang.WithBuildTag("grpcnotrace")), + ExtraFiles: []string{"/bin/bash"}, + InitCmd: "init", + UinitCmd: "gosh", + UinitArgs: []string{"script.sh"}, + DefaultShell: "gosh", + Commands: []uimage.Commands{ + { + Builder: builder.Busybox, + Packages: []string{ + "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/gosh", + "github.com/u-root/u-root/cmds/core/ls", + "github.com/u-root/u-root/cmds/core/init", + "./u-bmc/cmd/foo", + }, + }, + { + Builder: builder.Binary, + Packages: []string{"./u-bmc/cmd/bar"}, + }, + { + Builder: builder.Binary, + Packages: []string{"cmd/test2json"}, + }, + }, + }, + }, + { + name: "missing_config", + tpl: ` +configs: + plan9: + goarch: amd64 + goos: plan9 +`, + config: "plan10", + err: ErrTemplateNotExist, + }, + } { + t.Run(tt.name, func(t *testing.T) { + tpl, err := TemplateFrom([]byte(tt.tpl)) + if err != nil { + t.Fatal(err) + } + mods, err := tpl.Uimage(tt.config) + if !errors.Is(err, tt.err) { + t.Fatalf("UimageMods = %v, want %v", err, tt.err) + } + if len(mods) == 0 { + return + } + got, err := uimage.OptionsFor(mods...) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, tt.want) { + t.Logf("got: %#v", got) + t.Logf("want: %#v", tt.want) + t.Errorf("not equal") + } + }) + } +} + +func TestTemplateErr(t *testing.T) { + if _, err := TemplateFrom([]byte("\t")); err == nil { + t.Fatal("Expected error") + } + + d := t.TempDir() + wd, _ := os.Getwd() + _ = os.Chdir(d) + defer func() { _ = os.Chdir(wd) }() + + if _, err := Template(); !errors.Is(err, os.ErrNotExist) { + t.Fatalf("Template = %v, want ErrNotExist", err) + } + if _, err := TemplateFromFile(filepath.Join(d, "foobar")); !os.IsNotExist(err) { + t.Fatalf("Template = %v, want not exist", err) + } +}