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