diff --git a/uimage/uimage.go b/uimage/uimage.go index b8237b3..11fbf4d 100644 --- a/uimage/uimage.go +++ b/uimage/uimage.go @@ -176,6 +176,11 @@ type Opts struct { // - "/home/foo" is equivalent to "/home/foo:home/foo". ExtraFiles []string + // Symlinks to create in the archive. File path in archive -> target + // + // Target can be the name of a command. If not, it will be created as given. + Symlinks map[string]string + // If true, do not use ldd to pick up dependencies from local machine for // ExtraFiles. Useful if you have all deps revision controlled and wish to // ensure builds are repeatable, and/or if the local machine's binaries use @@ -293,6 +298,22 @@ func WithEnv(gopts ...golang.Opt) Modifier { } } +// WithSymlink adds a symlink to the archive. +// +// Target can be the name of a command. If not, it will be created as given. +func WithSymlink(file string, target string) Modifier { + return func(o *Opts) error { + if o.Symlinks == nil { + o.Symlinks = make(map[string]string) + } + if other, ok := o.Symlinks[file]; ok { + return fmt.Errorf("%w: cannot add symlink for %q as %q, already points to %q", os.ErrExist, file, target, other) + } + o.Symlinks[file] = target + return nil + } +} + // WithFiles adds files to the archive. // // Shared library dependencies will automatically also be added to the archive @@ -563,6 +584,15 @@ func CreateInitramfs(l *llog.Logger, opts Opts) error { if err := opts.addSymlinkTo(l, archive, opts.DefaultShell, "bin/defaultsh"); err != nil { return fmt.Errorf("%w: %w", err, errDefaultshSymlink) } + for p, target := range opts.Symlinks { + p = path.Clean(p) + if len(p) >= 1 && p[0] == '/' { + p = p[1:] + } + if err := opts.addSymlinkTo(l, archive, target, p); err != nil { + return fmt.Errorf("%w: could not add additional symlink", err) + } + } // Finally, write the archive. if err := initramfs.Write(archive); err != nil { diff --git a/uimage/uimage_test.go b/uimage/uimage_test.go index 44475e7..cdb4838 100644 --- a/uimage/uimage_test.go +++ b/uimage/uimage_test.go @@ -459,6 +459,52 @@ func TestCreateInitramfs(t *testing.T) { }, errs: []error{initramfs.ErrNoPath}, }, + { + name: "symlinks", + opts: Opts{ + Env: golang.Default(golang.DisableCGO()), + TempDir: dir, + InitCmd: "init", + DefaultShell: "ls", + Symlinks: map[string]string{ + "ubin/foo": "ls", + "ubin/fooa": "/bin/systemd", + }, + Commands: BusyboxCmds( + "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.Symlink("bbin/init", "bb")}, + itest.HasRecord{R: cpio.Symlink("bbin/ls", "bb")}, + itest.HasRecord{R: cpio.Symlink("bin/defaultsh", "../bbin/ls")}, + itest.HasRecord{R: cpio.Symlink("bin/sh", "../bbin/ls")}, + itest.HasRecord{R: cpio.Symlink("ubin/foo", "../bbin/ls")}, + itest.HasRecord{R: cpio.Symlink("ubin/fooa", "../bin/systemd")}, + }, + }, + { + name: "dup symlinks", + opts: Opts{ + Env: golang.Default(golang.DisableCGO()), + TempDir: dir, + InitCmd: "init", + DefaultShell: "ls", + Symlinks: map[string]string{ + "/bbin/ls": "init", + }, + Commands: BusyboxCmds( + "github.com/u-root/u-root/cmds/core/init", + "github.com/u-root/u-root/cmds/core/ls", + ), + }, + errs: []error{os.ErrExist}, + validators: []itest.ArchiveValidator{ + itest.IsEmpty{}, + }, + }, } { t.Run(fmt.Sprintf("Test %d [%s]", i, tt.name), func(t *testing.T) { archive := cpio.InMemArchive() @@ -892,6 +938,60 @@ func TestCreateInitramfsWithAPI(t *testing.T) { itest.HasContent{Path: "etc/foo", Content: "bar"}, }, }, + { + name: "symlinks", + opts: []Modifier{ + WithTempDir(dir), + WithEnv(golang.DisableCGO()), + WithInit("init"), + WithShell("ls"), + WithBusyboxCommands( + "github.com/u-root/u-root/cmds/core/init", + "github.com/u-root/u-root/cmds/core/ls", + ), + WithSymlink("ubin/foo", "ls"), + WithSymlink("ubin/fooa", "/bin/systemd"), + }, + validators: []itest.ArchiveValidator{ + itest.HasFile{Path: "bbin/bb"}, + itest.HasRecord{R: cpio.Symlink("bbin/init", "bb")}, + itest.HasRecord{R: cpio.Symlink("bbin/ls", "bb")}, + itest.HasRecord{R: cpio.Symlink("bin/defaultsh", "../bbin/ls")}, + itest.HasRecord{R: cpio.Symlink("bin/sh", "../bbin/ls")}, + itest.HasRecord{R: cpio.Symlink("ubin/foo", "../bbin/ls")}, + itest.HasRecord{R: cpio.Symlink("ubin/fooa", "../bin/systemd")}, + }, + }, + { + name: "dup symlinks", + opts: []Modifier{ + WithTempDir(dir), + WithEnv(golang.DisableCGO()), + WithInit("init"), + WithShell("ls"), + WithBusyboxCommands( + "github.com/u-root/u-root/cmds/core/init", + "github.com/u-root/u-root/cmds/core/ls", + ), + WithSymlink("/bbin/ls", "init"), + }, + errs: []error{os.ErrExist}, + validators: []itest.ArchiveValidator{ + itest.IsEmpty{}, + }, + }, + { + name: "dup with symlinks", + opts: []Modifier{ + WithTempDir(dir), + WithSymlink("ubin/ls", "/bin/foo"), + WithSymlink("ubin/ls", "../bbin/ls"), + }, + errs: []error{os.ErrExist}, + validators: []itest.ArchiveValidator{ + itest.IsEmpty{}, + }, + }, } { t.Run(fmt.Sprintf("Test %d [%s]", i, tt.name), func(t *testing.T) { archive := cpio.InMemArchive()