github.com/mvdan/u-root-coreutils@v0.0.0-20230122170626-c2eef2898555/pkg/uroot/uroot.go (about)

     1  // Copyright 2015-2017 the u-root Authors. All rights reserved
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // Package uroot creates root file systems from Go programs.
     6  //
     7  // uroot will appropriately compile the Go programs, create symlinks for their
     8  // names, and assemble an initramfs with additional files as specified.
     9  package uroot
    10  
    11  import (
    12  	"debug/elf"
    13  	"fmt"
    14  	"os"
    15  	"path"
    16  	"path/filepath"
    17  	"strings"
    18  
    19  	"github.com/u-root/gobusybox/src/pkg/bb/findpkg"
    20  	gbbgolang "github.com/u-root/gobusybox/src/pkg/golang"
    21  	"github.com/mvdan/u-root-coreutils/pkg/cpio"
    22  	"github.com/mvdan/u-root-coreutils/pkg/ldd"
    23  	"github.com/mvdan/u-root-coreutils/pkg/uflag"
    24  	"github.com/mvdan/u-root-coreutils/pkg/ulog"
    25  	"github.com/mvdan/u-root-coreutils/pkg/uroot/builder"
    26  	"github.com/mvdan/u-root-coreutils/pkg/uroot/initramfs"
    27  )
    28  
    29  // These constants are used in DefaultRamfs.
    30  const (
    31  	// This is the literal timezone file for GMT-0. Given that we have no
    32  	// idea where we will be running, GMT seems a reasonable guess. If it
    33  	// matters, setup code should download and change this to something
    34  	// else.
    35  	gmt0 = "TZif2\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00GMT\x00\x00\x00TZif2\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x04\xf8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00GMT\x00\x00\x00\nGMT0\n"
    36  
    37  	nameserver = "nameserver 8.8.8.8\n"
    38  )
    39  
    40  // DefaultRamRamfs returns a cpio.Archive for the target OS.
    41  // If an OS is not known it will return a reasonable u-root specific
    42  // default.
    43  func DefaultRamfs() *cpio.Archive {
    44  	switch gbbgolang.Default().GOOS {
    45  	case "linux":
    46  		return cpio.ArchiveFromRecords([]cpio.Record{
    47  			cpio.Directory("bin", 0o755),
    48  			cpio.Directory("dev", 0o755),
    49  			cpio.Directory("env", 0o755),
    50  			cpio.Directory("etc", 0o755),
    51  			cpio.Directory("lib64", 0o755),
    52  			cpio.Directory("proc", 0o755),
    53  			cpio.Directory("sys", 0o755),
    54  			cpio.Directory("tcz", 0o755),
    55  			cpio.Directory("tmp", 0o777),
    56  			cpio.Directory("ubin", 0o755),
    57  			cpio.Directory("usr", 0o755),
    58  			cpio.Directory("usr/lib", 0o755),
    59  			cpio.Directory("var/log", 0o777),
    60  			cpio.CharDev("dev/console", 0o600, 5, 1),
    61  			cpio.CharDev("dev/tty", 0o666, 5, 0),
    62  			cpio.CharDev("dev/null", 0o666, 1, 3),
    63  			cpio.CharDev("dev/port", 0o640, 1, 4),
    64  			cpio.CharDev("dev/urandom", 0o666, 1, 9),
    65  			cpio.StaticFile("etc/resolv.conf", nameserver, 0o644),
    66  			cpio.StaticFile("etc/localtime", gmt0, 0o644),
    67  		})
    68  	default:
    69  		return cpio.ArchiveFromRecords([]cpio.Record{
    70  			cpio.Directory("ubin", 0o755),
    71  			cpio.Directory("bbin", 0o755),
    72  		})
    73  	}
    74  }
    75  
    76  // Commands specifies a list of Golang packages to build with a builder, e.g.
    77  // in busybox mode, source mode, or binary mode.
    78  //
    79  // See Builder for an explanation of build modes.
    80  type Commands struct {
    81  	// Builder is the Go compiler mode.
    82  	Builder builder.Builder
    83  
    84  	// Packages are the Go commands to include (compiled or otherwise) and
    85  	// add to the archive.
    86  	//
    87  	// Currently allowed formats:
    88  	//
    89  	//   - package imports; e.g. github.com/mvdan/u-root-coreutils/cmds/ls
    90  	//   - globs of package imports; e.g. github.com/mvdan/u-root-coreutils/cmds/*
    91  	//   - paths to package directories; e.g. $GOPATH/src/github.com/mvdan/u-root-coreutils/cmds/ls
    92  	//   - globs of paths to package directories; e.g. ./cmds/*
    93  	//
    94  	// Directories may be relative or absolute, with or without globs.
    95  	// Globs are resolved using filepath.Glob.
    96  	Packages []string
    97  
    98  	// BinaryDir is the directory in which the resulting binaries are
    99  	// placed inside the initramfs.
   100  	//
   101  	// BinaryDir may be empty, in which case Builder.DefaultBinaryDir()
   102  	// will be used.
   103  	BinaryDir string
   104  }
   105  
   106  // TargetDir returns the initramfs binary directory for these Commands.
   107  func (c Commands) TargetDir() string {
   108  	if len(c.BinaryDir) != 0 {
   109  		return c.BinaryDir
   110  	}
   111  	return c.Builder.DefaultBinaryDir()
   112  }
   113  
   114  // Opts are the arguments to CreateInitramfs.
   115  //
   116  // Opts contains everything that influences initramfs creation such as the Go
   117  // build environment.
   118  type Opts struct {
   119  	// Env is the Golang build environment (GOOS, GOARCH, etc).
   120  	//
   121  	// If nil, gbbgolang.Default is used.
   122  	Env *gbbgolang.Environ
   123  
   124  	// Commands specify packages to build using a specific builder.
   125  	//
   126  	// E.g. the following will build 'ls' and 'ip' in busybox mode, but
   127  	// 'cd' and 'cat' as separate binaries. 'cd', 'cat', 'bb', and symlinks
   128  	// from 'ls' and 'ip' will be added to the final initramfs.
   129  	//
   130  	//   []Commands{
   131  	//     Commands{
   132  	//       Builder: builder.BusyBox,
   133  	//       Packages: []string{
   134  	//         "github.com/mvdan/u-root-coreutils/cmds/ls",
   135  	//         "github.com/mvdan/u-root-coreutils/cmds/ip",
   136  	//       },
   137  	//     },
   138  	//     Commands{
   139  	//       Builder: builder.Binary,
   140  	//       Packages: []string{
   141  	//         "github.com/mvdan/u-root-coreutils/cmds/cd",
   142  	//         "github.com/mvdan/u-root-coreutils/cmds/cat",
   143  	//       },
   144  	//     },
   145  	//   }
   146  	Commands []Commands
   147  
   148  	// UrootSource is the filesystem path to the locally checked out
   149  	// u-root source tree. This is needed to resolve templates or
   150  	// import paths of u-root commands.
   151  	UrootSource string
   152  
   153  	// TempDir is a temporary directory for builders to store files in.
   154  	TempDir string
   155  
   156  	// ExtraFiles are files to add to the archive in addition to the Go
   157  	// packages.
   158  	//
   159  	// Shared library dependencies will automatically also be added to the
   160  	// archive using ldd, unless SkipLDD (below) is true.
   161  	//
   162  	// The following formats are allowed in the list:
   163  	//
   164  	//   - "/home/chrisko/foo:root/bar" adds the file from absolute path
   165  	//     /home/chrisko/foo on the host at the relative root/bar in the
   166  	//     archive.
   167  	//   - "/home/foo" is equivalent to "/home/foo:home/foo".
   168  	ExtraFiles []string
   169  
   170  	// If true, do not use ldd to pick up dependencies from local machine for
   171  	// ExtraFiles. Useful if you have all deps revision controlled and wish to
   172  	// ensure builds are repeatable, and/or if the local machine's binaries use
   173  	// instructions unavailable on the emulated cpu.
   174  	//
   175  	// If you turn this on but do not manually list all deps, affected binaries
   176  	// will misbehave.
   177  	SkipLDD bool
   178  
   179  	// OutputFile is the archive output file.
   180  	OutputFile initramfs.Writer
   181  
   182  	// BaseArchive is an existing initramfs to include in the resulting
   183  	// initramfs.
   184  	BaseArchive initramfs.Reader
   185  
   186  	// UseExistingInit determines whether the existing init from
   187  	// BaseArchive should be used.
   188  	//
   189  	// If this is false, the "init" from BaseArchive will be renamed to
   190  	// "inito" (init-original).
   191  	UseExistingInit bool
   192  
   193  	// InitCmd is the name of a command to link /init to.
   194  	//
   195  	// This can be an absolute path or the name of a command included in
   196  	// Commands.
   197  	//
   198  	// If this is empty, no init symlink will be created, but a user may
   199  	// still specify a command called init or include an /init file.
   200  	InitCmd string
   201  
   202  	// UinitCmd is the name of a command to link /bin/uinit to.
   203  	//
   204  	// This can be an absolute path or the name of a command included in
   205  	// Commands.
   206  	//
   207  	// The u-root init will always attempt to fork/exec a uinit program,
   208  	// and append arguments from both the kernel command-line
   209  	// (uroot.uinitargs) as well as specified in UinitArgs.
   210  	//
   211  	// If this is empty, no uinit symlink will be created, but a user may
   212  	// still specify a command called uinit or include a /bin/uinit file.
   213  	UinitCmd string
   214  
   215  	// UinitArgs are the arguments passed to /bin/uinit.
   216  	UinitArgs []string
   217  
   218  	// DefaultShell is the default shell to start after init.
   219  	//
   220  	// This can be an absolute path or the name of a command included in
   221  	// Commands.
   222  	//
   223  	// This must be specified to have a default shell.
   224  	DefaultShell string
   225  
   226  	// Build options for building go binaries. Ultimate this holds all the
   227  	// args that end up being passed to `go build`.
   228  	BuildOpts *gbbgolang.BuildOpts
   229  }
   230  
   231  // CreateInitramfs creates an initramfs built to opts' specifications.
   232  func CreateInitramfs(logger ulog.Logger, opts Opts) error {
   233  	if _, err := os.Stat(opts.TempDir); os.IsNotExist(err) {
   234  		return fmt.Errorf("temp dir %q must exist: %v", opts.TempDir, err)
   235  	}
   236  	if opts.OutputFile == nil {
   237  		return fmt.Errorf("must give output file")
   238  	}
   239  
   240  	env := gbbgolang.Default()
   241  	if opts.Env != nil {
   242  		env = *opts.Env
   243  	}
   244  	if opts.BuildOpts == nil {
   245  		opts.BuildOpts = &gbbgolang.BuildOpts{}
   246  	}
   247  
   248  	files := initramfs.NewFiles()
   249  
   250  	lookupEnv := findpkg.DefaultEnv()
   251  	if opts.UrootSource != "" {
   252  		lookupEnv.URootSource = opts.UrootSource
   253  	}
   254  
   255  	// Expand commands.
   256  	for index, cmds := range opts.Commands {
   257  		paths, err := findpkg.ResolveGlobs(logger, env, lookupEnv, cmds.Packages)
   258  		if err != nil {
   259  			return err
   260  		}
   261  		opts.Commands[index].Packages = paths
   262  	}
   263  
   264  	// Add each build mode's commands to the archive.
   265  	for _, cmds := range opts.Commands {
   266  		builderTmpDir, err := os.MkdirTemp(opts.TempDir, "builder")
   267  		if err != nil {
   268  			return err
   269  		}
   270  
   271  		// Build packages.
   272  		bOpts := builder.Opts{
   273  			Env:       env,
   274  			BuildOpts: opts.BuildOpts,
   275  			Packages:  cmds.Packages,
   276  			TempDir:   builderTmpDir,
   277  			BinaryDir: cmds.TargetDir(),
   278  		}
   279  		if err := cmds.Builder.Build(logger, files, bOpts); err != nil {
   280  			return fmt.Errorf("error building: %v", err)
   281  		}
   282  	}
   283  
   284  	// Open the target initramfs file.
   285  	archive := &initramfs.Opts{
   286  		Files:           files,
   287  		OutputFile:      opts.OutputFile,
   288  		BaseArchive:     opts.BaseArchive,
   289  		UseExistingInit: opts.UseExistingInit,
   290  	}
   291  	if err := ParseExtraFiles(logger, archive.Files, opts.ExtraFiles, !opts.SkipLDD); err != nil {
   292  		return err
   293  	}
   294  	if err := opts.addSymlinkTo(logger, archive, opts.UinitCmd, "bin/uinit"); err != nil {
   295  		return fmt.Errorf("%v: specify -uinitcmd=\"\" to ignore this error and build without a uinit", err)
   296  	}
   297  	if len(opts.UinitArgs) > 0 {
   298  		if err := archive.AddRecord(cpio.StaticFile("etc/uinit.flags", uflag.ArgvToFile(opts.UinitArgs), 0o444)); err != nil {
   299  			return fmt.Errorf("%v: could not add uinit arguments from UinitArgs (-uinitcmd) to initramfs", err)
   300  		}
   301  	}
   302  	if err := opts.addSymlinkTo(logger, archive, opts.InitCmd, "init"); err != nil {
   303  		return fmt.Errorf("%v: specify -initcmd=\"\" to ignore this error and build without an init (or, did you specify a list, and are you missing github.com/mvdan/u-root-coreutils/cmds/core/init?)", err)
   304  	}
   305  	if err := opts.addSymlinkTo(logger, archive, opts.DefaultShell, "bin/sh"); err != nil {
   306  		return fmt.Errorf("%v: specify -defaultsh=\"\" to ignore this error and build without a shell", err)
   307  	}
   308  	if err := opts.addSymlinkTo(logger, archive, opts.DefaultShell, "bin/defaultsh"); err != nil {
   309  		return fmt.Errorf("%v: specify -defaultsh=\"\" to ignore this error and build without a shell", err)
   310  	}
   311  
   312  	// Finally, write the archive.
   313  	if err := initramfs.Write(archive); err != nil {
   314  		return fmt.Errorf("error archiving: %v", err)
   315  	}
   316  	return nil
   317  }
   318  
   319  func (o *Opts) addSymlinkTo(logger ulog.Logger, archive *initramfs.Opts, command string, source string) error {
   320  	if len(command) == 0 {
   321  		return nil
   322  	}
   323  
   324  	target, err := resolveCommandOrPath(command, o.Commands)
   325  	if err != nil {
   326  		if o.Commands != nil {
   327  			return fmt.Errorf("could not create symlink from %q to %q: %v", source, command, err)
   328  		}
   329  		logger.Printf("Could not create symlink from %q to %q: %v", source, command, err)
   330  		return nil
   331  	}
   332  
   333  	// Make a relative symlink from /source -> target
   334  	//
   335  	// E.g. bin/defaultsh -> target, so you need to
   336  	// filepath.Rel(/bin, target) since relative symlinks are
   337  	// evaluated from their PARENT directory.
   338  	relTarget, err := filepath.Rel(filepath.Join("/", filepath.Dir(source)), target)
   339  	if err != nil {
   340  		return err
   341  	}
   342  
   343  	if err := archive.AddRecord(cpio.Symlink(source, relTarget)); err != nil {
   344  		return fmt.Errorf("failed to add symlink %s -> %s to initramfs: %v", source, relTarget, err)
   345  	}
   346  	return nil
   347  }
   348  
   349  func resolveCommandOrPath(cmd string, cmds []Commands) (string, error) {
   350  	if strings.ContainsRune(cmd, filepath.Separator) {
   351  		return cmd, nil
   352  	}
   353  
   354  	// Each build mode has its own binary dir (/bbin or /bin or /ubin).
   355  	//
   356  	// Figure out which build mode the shell is in, and symlink to that
   357  	// build mode.
   358  	for _, c := range cmds {
   359  		for _, p := range c.Packages {
   360  			if name := path.Base(p); name == cmd {
   361  				return path.Join("/", c.TargetDir(), cmd), nil
   362  			}
   363  		}
   364  	}
   365  
   366  	return "", fmt.Errorf("command or path %q not included in u-root build", cmd)
   367  }
   368  
   369  // ParseExtraFiles adds files from the extraFiles list to the archive.
   370  //
   371  // The following formats are allowed in the extraFiles list:
   372  //
   373  //   - "/home/chrisko/foo:root/bar" adds the file from absolute path
   374  //     /home/chrisko/foo on the host at the relative root/bar in the
   375  //     archive.
   376  //   - "/home/foo" is equivalent to "/home/foo:home/foo".
   377  //
   378  // ParseExtraFiles will also add ldd-listed dependencies if lddDeps is true.
   379  func ParseExtraFiles(logger ulog.Logger, archive *initramfs.Files, extraFiles []string, lddDeps bool) error {
   380  	var err error
   381  	// Add files from command line.
   382  	for _, file := range extraFiles {
   383  		var src, dst string
   384  		parts := strings.SplitN(file, ":", 2)
   385  		if len(parts) == 2 {
   386  			// treat the entry with the new src:dst syntax
   387  			src = filepath.Clean(parts[0])
   388  			dst = filepath.Clean(parts[1])
   389  		} else {
   390  			// plain old syntax
   391  			// filepath.Clean interprets an empty string as CWD for no good reason.
   392  			if len(file) == 0 {
   393  				continue
   394  			}
   395  			src = filepath.Clean(file)
   396  			dst = src
   397  			if filepath.IsAbs(dst) {
   398  				dst, err = filepath.Rel("/", dst)
   399  				if err != nil {
   400  					return fmt.Errorf("cannot make path relative to /: %v: %v", dst, err)
   401  				}
   402  			}
   403  		}
   404  		src, err := filepath.Abs(src)
   405  		if err != nil {
   406  			return fmt.Errorf("couldn't find absolute path for %q: %v", src, err)
   407  		}
   408  		if err := archive.AddFileNoFollow(src, dst); err != nil {
   409  			return fmt.Errorf("couldn't add %q to archive: %v", file, err)
   410  		}
   411  
   412  		if lddDeps {
   413  			// Users are frequently naming directories now, not just files.
   414  			// Hence we must use walk here, not just check the one file.
   415  			if err := filepath.Walk(src, func(name string, info os.FileInfo, err error) error {
   416  				if err != nil {
   417  					return err
   418  				}
   419  				if info.IsDir() {
   420  					return nil
   421  				}
   422  				// Try to open it as an ELF. If that fails, we can skip the ldd
   423  				// step. The file will still be included from above.
   424  				f, err := elf.Open(name)
   425  				if err != nil {
   426  					return nil
   427  				}
   428  				if err = f.Close(); err != nil {
   429  					logger.Printf("WARNING: Closing ELF file %q: %v", name, err)
   430  				}
   431  				// Pull dependencies in the case of binaries. If `path` is not
   432  				// a binary, `libs` will just be empty.
   433  				libs, err := ldd.List([]string{name})
   434  				if err != nil {
   435  					return fmt.Errorf("WARNING: couldn't add ldd dependencies for %q: %v", name, err)
   436  				}
   437  				for _, lib := range libs {
   438  					// N.B.: we already added information about the src.
   439  					// Don't add it twice. We have to do this check here in
   440  					// case we're renaming the src to a different dest.
   441  					if lib == name {
   442  						continue
   443  					}
   444  					if err := archive.AddFileNoFollow(lib, lib[1:]); err != nil {
   445  						logger.Printf("WARNING: couldn't add ldd dependencies for %q: %v", lib, err)
   446  					}
   447  				}
   448  				return nil
   449  			}); err != nil {
   450  				logger.Printf("Getting dependencies for %q: %v", src, err)
   451  			}
   452  		}
   453  	}
   454  	return nil
   455  }
   456  
   457  // AddCommands adds commands to the build.
   458  func (o *Opts) AddCommands(c ...Commands) {
   459  	o.Commands = append(o.Commands, c...)
   460  }
   461  
   462  func (o *Opts) AddBusyBoxCommands(pkgs ...string) {
   463  	for i, cmds := range o.Commands {
   464  		if cmds.Builder == builder.BusyBox {
   465  			o.Commands[i].Packages = append(cmds.Packages, pkgs...)
   466  			return
   467  		}
   468  	}
   469  
   470  	// Not found? Add first busybox.
   471  	o.AddCommands(BusyBoxCmds(pkgs...)...)
   472  }
   473  
   474  // BinaryCmds returns a list of Commands with cmds built as a busybox.
   475  func BinaryCmds(cmds ...string) []Commands {
   476  	if len(cmds) == 0 {
   477  		return nil
   478  	}
   479  	return []Commands{
   480  		{
   481  			Builder:  builder.Binary,
   482  			Packages: cmds,
   483  		},
   484  	}
   485  }
   486  
   487  // BusyBoxCmds returns a list of Commands with cmds built as a busybox.
   488  func BusyBoxCmds(cmds ...string) []Commands {
   489  	if len(cmds) == 0 {
   490  		return nil
   491  	}
   492  	return []Commands{
   493  		{
   494  			Builder:  builder.BusyBox,
   495  			Packages: cmds,
   496  		},
   497  	}
   498  }