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