gopkg.in/hugelgupf/u-root.v2@v2.0.0-20180831055005-3f8fdb0ce09d/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  	"log"
    11  	"os"
    12  	"path"
    13  	"path/filepath"
    14  	"strings"
    15  
    16  	"github.com/u-root/u-root/pkg/cpio"
    17  	"github.com/u-root/u-root/pkg/golang"
    18  	"github.com/u-root/u-root/pkg/ldd"
    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.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 or 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 packages to compile and add to the archive.
    65  	//
    66  	// Currently allowed formats:
    67  	// - Go package imports; e.g. github.com/u-root/u-root/cmds/ls
    68  	// - Globs of Go package imports; e.g. github.com/u-root/u-root/cmds/*
    69  	// - Paths to Go package directories; e.g. $GOPATH/src/github.com/u-root/u-root/cmds/ls
    70  	// - Globs of paths to Go package directories; e.g. ./cmds/*
    71  	Packages []string
    72  
    73  	// BinaryDir is the directory in which the resulting binaries are
    74  	// placed inside the initramfs.
    75  	BinaryDir string
    76  }
    77  
    78  // TargetDir returns the initramfs binary directory for these Commands.
    79  func (c Commands) TargetDir() string {
    80  	if len(c.BinaryDir) != 0 {
    81  		return c.BinaryDir
    82  	}
    83  	return c.Builder.DefaultBinaryDir()
    84  }
    85  
    86  // Opts are the arguments to CreateInitramfs.
    87  //
    88  // Opts contains everything that influences initramfs creation such as the Go
    89  // build environment.
    90  //
    91  //
    92  type Opts struct {
    93  	// Env is the Golang build environment (GOOS, GOARCH, etc).
    94  	Env golang.Environ
    95  
    96  	// Commands specify packages to build using a specific builder.
    97  	Commands []Commands
    98  
    99  	// TempDir is a temporary directory for builders to store files in.
   100  	TempDir string
   101  
   102  	// ExtraFiles are files to add to the archive in addition to the Go
   103  	// packages.
   104  	//
   105  	// Shared library dependencies will automatically also be added to the
   106  	// archive using ldd.
   107  	ExtraFiles []string
   108  
   109  	// OutputFile is the archive output file.
   110  	OutputFile initramfs.Writer
   111  
   112  	// BaseArchive is an existing initramfs to include in the resulting
   113  	// initramfs.
   114  	BaseArchive initramfs.Reader
   115  
   116  	// UseExistingInit determines whether the existing init from
   117  	// BaseArchive should be used.
   118  	//
   119  	// If this is false, the "init" from BaseArchive will be renamed to
   120  	// "inito".
   121  	UseExistingInit bool
   122  
   123  	// InitCmd is the name of a command to link /init to.
   124  	//
   125  	// This can be an absolute path or the name of a command included in
   126  	// Commands.
   127  	//
   128  	// If this is empty, no init symlink will be created.
   129  	InitCmd string
   130  
   131  	// DefaultShell is the default shell to start after init.
   132  	//
   133  	// This can be an absolute path or the name of a command included in
   134  	// Commands.
   135  	//
   136  	// This must be specified to have a default shell.
   137  	DefaultShell string
   138  }
   139  
   140  // CreateInitramfs creates an initramfs built to opts' specifications.
   141  func CreateInitramfs(opts Opts) error {
   142  	if _, err := os.Stat(opts.TempDir); os.IsNotExist(err) {
   143  		return fmt.Errorf("temp dir %q must exist: %v", opts.TempDir, err)
   144  	}
   145  	if opts.OutputFile == nil {
   146  		return fmt.Errorf("must give output file")
   147  	}
   148  
   149  	files := initramfs.NewFiles()
   150  
   151  	// Expand commands.
   152  	for index, cmds := range opts.Commands {
   153  		importPaths, err := ResolvePackagePaths(opts.Env, cmds.Packages)
   154  		if err != nil {
   155  			return err
   156  		}
   157  		opts.Commands[index].Packages = importPaths
   158  	}
   159  
   160  	// Add each build mode's commands to the archive.
   161  	for _, cmds := range opts.Commands {
   162  		builderTmpDir, err := ioutil.TempDir(opts.TempDir, "builder")
   163  		if err != nil {
   164  			return err
   165  		}
   166  
   167  		// Build packages.
   168  		bOpts := builder.Opts{
   169  			Env:       opts.Env,
   170  			Packages:  cmds.Packages,
   171  			TempDir:   builderTmpDir,
   172  			BinaryDir: cmds.TargetDir(),
   173  		}
   174  		if err := cmds.Builder.Build(files, bOpts); err != nil {
   175  			return fmt.Errorf("error building %#v: %v", bOpts, err)
   176  		}
   177  	}
   178  
   179  	// Open the target initramfs file.
   180  	archive := initramfs.Opts{
   181  		Files:           files,
   182  		OutputFile:      opts.OutputFile,
   183  		BaseArchive:     opts.BaseArchive,
   184  		UseExistingInit: opts.UseExistingInit,
   185  		DefaultRecords:  DefaultRamfs,
   186  	}
   187  
   188  	if len(opts.DefaultShell) > 0 {
   189  		if target, err := resolveCommandOrPath(opts.DefaultShell, opts.Commands); err != nil {
   190  			log.Printf("No default shell: %v", err)
   191  		} else if err := archive.AddRecord(cpio.Symlink("bin/defaultsh", target)); err != nil {
   192  			return err
   193  		}
   194  	}
   195  
   196  	if len(opts.InitCmd) > 0 {
   197  		if target, err := resolveCommandOrPath(opts.InitCmd, opts.Commands); err != nil {
   198  			return fmt.Errorf("could not find init: %v", err)
   199  		} else if err := archive.AddRecord(cpio.Symlink("init", target)); err != nil {
   200  			return err
   201  		}
   202  	}
   203  
   204  	if err := ParseExtraFiles(archive.Files, opts.ExtraFiles, true); err != nil {
   205  		return err
   206  	}
   207  
   208  	// Finally, write the archive.
   209  	if err := initramfs.Write(&archive); err != nil {
   210  		return fmt.Errorf("error archiving: %v", err)
   211  	}
   212  	return nil
   213  }
   214  
   215  // resolvePackagePath finds import paths for a single import path or directory string
   216  func resolvePackagePath(env golang.Environ, pkg string) ([]string, error) {
   217  	// Search the current working directory, as well GOROOT and GOPATHs
   218  	prefixes := append([]string{""}, env.SrcDirs()...)
   219  	// Resolve file system paths to package import paths.
   220  	for _, prefix := range prefixes {
   221  		path := filepath.Join(prefix, pkg)
   222  		matches, err := filepath.Glob(path)
   223  		if len(matches) == 0 || err != nil {
   224  			continue
   225  		}
   226  
   227  		var importPaths []string
   228  		for _, match := range matches {
   229  			p, err := env.PackageByPath(match)
   230  			if err != nil {
   231  				log.Printf("Skipping package %q: %v", match, err)
   232  			} else if p.ImportPath == "." {
   233  				// TODO: I do not completely understand why
   234  				// this is triggered. This is only an issue
   235  				// while this function is run inside the
   236  				// process of a "go test".
   237  				importPaths = append(importPaths, pkg)
   238  			} else {
   239  				importPaths = append(importPaths, p.ImportPath)
   240  			}
   241  		}
   242  		return importPaths, nil
   243  	}
   244  
   245  	// No file import paths found. Check if pkg still resolves as a package name.
   246  	if _, err := env.Package(pkg); err != nil {
   247  		return nil, fmt.Errorf("%q is neither package or path/glob: %v", pkg, err)
   248  	}
   249  	return []string{pkg}, nil
   250  }
   251  
   252  func resolveCommandOrPath(cmd string, cmds []Commands) (string, error) {
   253  	if filepath.IsAbs(cmd) {
   254  		return cmd, nil
   255  	}
   256  
   257  	for _, c := range cmds {
   258  		for _, p := range c.Packages {
   259  			// Figure out which build mode the shell is in, and symlink to
   260  			// that build modee
   261  			if name := path.Base(p); name == cmd {
   262  				return path.Join("/", c.TargetDir(), cmd), nil
   263  			}
   264  		}
   265  	}
   266  
   267  	return "", fmt.Errorf("command or path %q not included in u-root build", cmd)
   268  }
   269  
   270  // ResolvePackagePaths takes a list of Go package import paths and directories
   271  // and turns them into exclusively import paths.
   272  //
   273  // Currently allowed formats:
   274  //   Go package imports; e.g. github.com/u-root/u-root/cmds/ls
   275  //   Paths to Go package directories; e.g. $GOPATH/src/github.com/u-root/u-root/cmds/ls
   276  //   Globs of package imports, e.g. github.com/u-root/u-root/cmds/*
   277  //   Globs of paths to Go package directories; e.g. ./cmds/*
   278  func ResolvePackagePaths(env golang.Environ, pkgs []string) ([]string, error) {
   279  	var importPaths []string
   280  	for _, pkg := range pkgs {
   281  		paths, err := resolvePackagePath(env, pkg)
   282  		if err != nil {
   283  			return nil, err
   284  		}
   285  		importPaths = append(importPaths, paths...)
   286  	}
   287  	return importPaths, nil
   288  }
   289  
   290  // ParseExtraFiles adds files from the extraFiles list to the archive.
   291  //
   292  // The following formats are allowed in the extraFiles list:
   293  // - hostPath:archivePath adds the file from hostPath at the relative
   294  //   archivePath in the archive.
   295  // - justAPath is added to the archive under justAPath.
   296  //
   297  // ParseExtraFiles will also add ldd-listed dependencies if lddDeps is true.
   298  func ParseExtraFiles(archive initramfs.Files, extraFiles []string, lddDeps bool) error {
   299  	var err error
   300  	// Add files from command line.
   301  	for _, file := range extraFiles {
   302  		var src, dst string
   303  		parts := strings.SplitN(file, ":", 2)
   304  		if len(parts) == 2 {
   305  			// treat the entry with the new src:dst syntax
   306  			src = filepath.Clean(parts[0])
   307  			dst = filepath.Clean(parts[1])
   308  		} else {
   309  			// plain old syntax
   310  			// filepath.Clean interprets an empty string as CWD for no good reason.
   311  			if len(file) == 0 {
   312  				continue
   313  			}
   314  			src = filepath.Clean(file)
   315  			dst = src
   316  			if filepath.IsAbs(dst) {
   317  				dst, err = filepath.Rel("/", dst)
   318  				if err != nil {
   319  					return fmt.Errorf("cannot make path relative to /: %v: %v", dst, err)
   320  				}
   321  			}
   322  		}
   323  		src, err := filepath.Abs(src)
   324  		if err != nil {
   325  			return fmt.Errorf("couldn't find absolute path for %q: %v", src, err)
   326  		}
   327  		if err := archive.AddFile(src, dst); err != nil {
   328  			return fmt.Errorf("couldn't add %q to archive: %v", file, err)
   329  		}
   330  
   331  		if lddDeps {
   332  			// Pull dependencies in the case of binaries. If `path` is not
   333  			// a binary, `libs` will just be empty.
   334  			libs, err := ldd.List([]string{src})
   335  			if err != nil {
   336  				log.Printf("WARNING: couldn't add ldd dependencies for %q: %v", file, err)
   337  				return nil
   338  			}
   339  			for _, lib := range libs {
   340  				if err := archive.AddFile(lib, lib[1:]); err != nil {
   341  					log.Printf("WARNING: couldn't add ldd dependencies for %q: %v", lib, err)
   342  				}
   343  			}
   344  		}
   345  	}
   346  	return nil
   347  }
   348  
   349  // DefaultPackageImports returns a list of default u-root packages to include.
   350  func DefaultPackageImports(env golang.Environ) ([]string, error) {
   351  	// Find u-root directory.
   352  	urootPkg, err := env.Package("github.com/u-root/u-root")
   353  	if err != nil {
   354  		return nil, fmt.Errorf("Couldn't find u-root src directory: %v", err)
   355  	}
   356  
   357  	matches, err := filepath.Glob(filepath.Join(urootPkg.Dir, "cmds/*"))
   358  	if err != nil {
   359  		return nil, fmt.Errorf("couldn't find u-root cmds: %v", err)
   360  	}
   361  	pkgs := make([]string, 0, len(matches))
   362  	for _, match := range matches {
   363  		pkg, err := env.PackageByPath(match)
   364  		if err == nil {
   365  			pkgs = append(pkgs, pkg.ImportPath)
   366  		}
   367  	}
   368  	return pkgs, nil
   369  }