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

     1  // Copyright 2015-2018 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 main
     6  
     7  import (
     8  	"encoding/json"
     9  	"flag"
    10  	"fmt"
    11  	"log"
    12  	"os"
    13  	"path"
    14  	"path/filepath"
    15  	"runtime"
    16  	"sort"
    17  	"strings"
    18  	"time"
    19  
    20  	gbbgolang "github.com/u-root/gobusybox/src/pkg/golang"
    21  	"github.com/mvdan/u-root-coreutils/pkg/shlex"
    22  	"github.com/mvdan/u-root-coreutils/pkg/ulog"
    23  	"github.com/mvdan/u-root-coreutils/pkg/uroot"
    24  	"github.com/mvdan/u-root-coreutils/pkg/uroot/builder"
    25  	"github.com/mvdan/u-root-coreutils/pkg/uroot/initramfs"
    26  )
    27  
    28  // multiFlag is used for flags that support multiple invocations, e.g. -files
    29  type multiFlag []string
    30  
    31  func (m *multiFlag) String() string {
    32  	return fmt.Sprint(*m)
    33  }
    34  
    35  func (m *multiFlag) Set(value string) error {
    36  	*m = append(*m, value)
    37  	return nil
    38  }
    39  
    40  // Flags for u-root builder.
    41  var (
    42  	build, format, tmpDir, base, outputPath *string
    43  	uinitCmd, initCmd                       *string
    44  	defaultShell                            *string
    45  	useExistingInit                         *bool
    46  	noCommands                              *bool
    47  	extraFiles                              multiFlag
    48  	statsOutputPath                         *string
    49  	statsLabel                              *string
    50  	shellbang                               *bool
    51  	tags                                    *string
    52  	// For the new gobusybox support
    53  	usegobusybox *bool
    54  	genDir       *string
    55  	// For the new "filepath only" logic
    56  	urootSourceDir *string
    57  )
    58  
    59  func init() {
    60  	var sh string
    61  	switch gbbgolang.Default().GOOS {
    62  	case "plan9":
    63  		sh = ""
    64  	default:
    65  		sh = "elvish"
    66  	}
    67  
    68  	build = flag.String("build", "gbb", "u-root build format (e.g. bb/gbb or binary).")
    69  	format = flag.String("format", "cpio", "Archival format.")
    70  
    71  	tmpDir = flag.String("tmpdir", "", "Temporary directory to put binaries in.")
    72  
    73  	base = flag.String("base", "", "Base archive to add files to. By default, this is a couple of directories like /bin, /etc, etc. u-root has a default internally supplied set of files; use base=/dev/null if you don't want any base files.")
    74  	useExistingInit = flag.Bool("useinit", false, "Use existing init from base archive (only if --base was specified).")
    75  	outputPath = flag.String("o", "", "Path to output initramfs file.")
    76  
    77  	initCmd = flag.String("initcmd", "init", "Symlink target for /init. Can be an absolute path or a u-root command name. Use initcmd=\"\" if you don't want the symlink.")
    78  	uinitCmd = flag.String("uinitcmd", "", "Symlink target and arguments for /bin/uinit. Can be an absolute path or a u-root command name. Use uinitcmd=\"\" if you don't want the symlink. E.g. -uinitcmd=\"echo foobar\"")
    79  	defaultShell = flag.String("defaultsh", sh, "Default shell. Can be an absolute path or a u-root command name. Use defaultsh=\"\" if you don't want the symlink.")
    80  
    81  	noCommands = flag.Bool("nocmd", false, "Build no Go commands; initramfs only")
    82  
    83  	flag.Var(&extraFiles, "files", "Additional files, directories, and binaries (with their ldd dependencies) to add to archive. Can be speficified multiple times.")
    84  
    85  	shellbang = flag.Bool("shellbang", false, "Use #! instead of symlinks for busybox")
    86  
    87  	statsOutputPath = flag.String("stats-output-path", "", "Write build stats to this file (JSON)")
    88  	statsLabel = flag.String("stats-label", "", "Use this statsLabel when writing stats")
    89  
    90  	tags = flag.String("tags", "", "Comma separated list of build tags")
    91  
    92  	// Flags for the gobusybox, which we hope to move to, since it works with modules.
    93  	genDir = flag.String("gen-dir", "", "Directory to generate source in")
    94  
    95  	// Flag for the new filepath only mode. This will be required to find the u-root commands and make templates work
    96  	// In almost every case, "." is fine.
    97  	urootSourceDir = flag.String("uroot-source", ".", "Path to the locally checked out u-root source tree in case commands from there are desired.")
    98  }
    99  
   100  type buildStats struct {
   101  	Label      string  `json:"label,omitempty"`
   102  	Time       int64   `json:"time"`
   103  	Duration   float64 `json:"duration"`
   104  	OutputSize int64   `json:"output_size"`
   105  }
   106  
   107  func writeBuildStats(stats buildStats, path string) error {
   108  	var allStats []buildStats
   109  	if data, err := os.ReadFile(*statsOutputPath); err == nil {
   110  		json.Unmarshal(data, &allStats)
   111  	}
   112  	found := false
   113  	for i, s := range allStats {
   114  		if s.Label == stats.Label {
   115  			allStats[i] = stats
   116  			found = true
   117  			break
   118  		}
   119  	}
   120  	if !found {
   121  		allStats = append(allStats, stats)
   122  		sort.Slice(allStats, func(i, j int) bool {
   123  			return strings.Compare(allStats[i].Label, allStats[j].Label) == -1
   124  		})
   125  	}
   126  	data, err := json.MarshalIndent(allStats, "", "  ")
   127  	if err != nil {
   128  		return err
   129  	}
   130  	if err := os.WriteFile(*statsOutputPath, data, 0o644); err != nil {
   131  		return err
   132  	}
   133  	return nil
   134  }
   135  
   136  func generateLabel(env gbbgolang.Environ) string {
   137  	var baseCmds []string
   138  	if len(flag.Args()) > 0 {
   139  		// Use the last component of the name to keep the label short
   140  		for _, e := range flag.Args() {
   141  			baseCmds = append(baseCmds, path.Base(e))
   142  		}
   143  	} else {
   144  		baseCmds = []string{"core"}
   145  	}
   146  	return fmt.Sprintf("%s-%s-%s-%s", *build, env.GOOS, env.GOARCH, strings.Join(baseCmds, "_"))
   147  }
   148  
   149  func main() {
   150  	gbbOpts := &gbbgolang.BuildOpts{}
   151  	gbbOpts.RegisterFlags(flag.CommandLine)
   152  
   153  	l := log.New(os.Stderr, "", log.Ltime)
   154  
   155  	// Register an alias for -go-no-strip for backwards compatibility.
   156  	flag.CommandLine.BoolVar(&gbbOpts.NoStrip, "no-strip", false, "Build unstripped binaries")
   157  	flag.Parse()
   158  
   159  	if usrc := os.Getenv("UROOT_SOURCE"); usrc != "" && *urootSourceDir == "" {
   160  		*urootSourceDir = usrc
   161  	}
   162  
   163  	env := gbbgolang.Default()
   164  	env.BuildTags = strings.Split(*tags, ",")
   165  	if env.CgoEnabled {
   166  		l.Printf("Disabling CGO for u-root...")
   167  		env.CgoEnabled = false
   168  	}
   169  	l.Printf("Build environment: %s", env)
   170  	if env.GOOS != "linux" {
   171  		l.Printf("GOOS is not linux. Did you mean to set GOOS=linux?")
   172  	}
   173  
   174  	start := time.Now()
   175  
   176  	// Main is in a separate functions so defers run on return.
   177  	if err := Main(l, env, gbbOpts); err != nil {
   178  		l.Fatalf("Build error: %v", err)
   179  	}
   180  
   181  	elapsed := time.Now().Sub(start)
   182  
   183  	stats := buildStats{
   184  		Label:    *statsLabel,
   185  		Time:     start.Unix(),
   186  		Duration: float64(elapsed.Milliseconds()) / 1000,
   187  	}
   188  	if stats.Label == "" {
   189  		stats.Label = generateLabel(env)
   190  	}
   191  	if stat, err := os.Stat(*outputPath); err == nil && stat.ModTime().After(start) {
   192  		l.Printf("Successfully built %q (size %d).", *outputPath, stat.Size())
   193  		stats.OutputSize = stat.Size()
   194  		if *statsOutputPath != "" {
   195  			if err := writeBuildStats(stats, *statsOutputPath); err == nil {
   196  				l.Printf("Wrote stats to %q (label %q)", *statsOutputPath, stats.Label)
   197  			} else {
   198  				l.Printf("Failed to write stats to %s: %v", *statsOutputPath, err)
   199  			}
   200  		}
   201  	}
   202  }
   203  
   204  var recommendedVersions = []string{
   205  	"go1.19",
   206  	"go1.20",
   207  }
   208  
   209  func isRecommendedVersion(v string) bool {
   210  	for _, r := range recommendedVersions {
   211  		if strings.HasPrefix(v, r) {
   212  			return true
   213  		}
   214  	}
   215  	return false
   216  }
   217  
   218  func canFindSource(dir string) error {
   219  	d := filepath.Join(dir, "cmds", "core")
   220  	if _, err := os.Stat(d); err != nil {
   221  		return fmt.Errorf("can not build u-root in %q:%w (-uroot-source may be incorrect or not set)", *urootSourceDir, os.ErrNotExist)
   222  	}
   223  	return nil
   224  }
   225  
   226  // Main is a separate function so defers are run on return, which they wouldn't
   227  // on exit.
   228  func Main(l ulog.Logger, env gbbgolang.Environ, buildOpts *gbbgolang.BuildOpts) error {
   229  	v, err := env.Version()
   230  	if err != nil {
   231  		l.Printf("Could not get environment's Go version, using runtime's version: %v", err)
   232  		v = runtime.Version()
   233  	}
   234  	if !isRecommendedVersion(v) {
   235  		l.Printf(`WARNING: You are not using one of the recommended Go versions (have = %s, recommended = %v).
   236  			Some packages may not compile.
   237  			Go to https://golang.org/doc/install to find out how to install a newer version of Go,
   238  			or use https://godoc.org/golang.org/dl/%s to install an additional version of Go.`,
   239  			v, recommendedVersions, recommendedVersions[0])
   240  	}
   241  
   242  	archiver, err := initramfs.GetArchiver(*format)
   243  	if err != nil {
   244  		return err
   245  	}
   246  
   247  	// Open the target initramfs file.
   248  	if *outputPath == "" {
   249  		if len(env.GOOS) == 0 && len(env.GOARCH) == 0 {
   250  			return fmt.Errorf("passed no path, GOOS, and GOARCH to CPIOArchiver.OpenWriter")
   251  		}
   252  		*outputPath = fmt.Sprintf("/tmp/initramfs.%s_%s.cpio", env.GOOS, env.GOARCH)
   253  	}
   254  	w, err := archiver.OpenWriter(l, *outputPath)
   255  	if err != nil {
   256  		return err
   257  	}
   258  
   259  	var baseFile initramfs.Reader
   260  	if *base != "" {
   261  		bf, err := os.Open(*base)
   262  		if err != nil {
   263  			return err
   264  		}
   265  		defer bf.Close()
   266  		baseFile = archiver.Reader(bf)
   267  	} else {
   268  		baseFile = uroot.DefaultRamfs().Reader()
   269  	}
   270  
   271  	tempDir := *tmpDir
   272  	if tempDir == "" {
   273  		var err error
   274  		tempDir, err = os.MkdirTemp("", "u-root")
   275  		if err != nil {
   276  			return err
   277  		}
   278  		defer os.RemoveAll(tempDir)
   279  	} else if _, err := os.Stat(tempDir); os.IsNotExist(err) {
   280  		if err := os.MkdirAll(tempDir, 0o755); err != nil {
   281  			return fmt.Errorf("temporary directory %q did not exist; tried to mkdir but failed: %v", tempDir, err)
   282  		}
   283  	}
   284  
   285  	var (
   286  		c           []uroot.Commands
   287  		initCommand = *initCmd
   288  	)
   289  	if !*noCommands {
   290  		var b builder.Builder
   291  		switch *build {
   292  		case "bb", "gbb":
   293  			l.Printf("NOTE: building with the new gobusybox; to get the old behavior check out commit 8b790de")
   294  			b = builder.GBBBuilder{ShellBang: *shellbang}
   295  		case "binary":
   296  			b = builder.BinaryBuilder{}
   297  		case "source":
   298  			return fmt.Errorf("source mode has been deprecated")
   299  		default:
   300  			return fmt.Errorf("could not find builder %q", *build)
   301  		}
   302  
   303  		// Resolve globs into package imports.
   304  		//
   305  		// Currently allowed format:
   306  		//   Paths to Go package directories; e.g. $GOPATH/src/github.com/mvdan/u-root-coreutils/cmds/*
   307  		//   u-root templates; e.g. all, core, minimal (requires uroot-source be valid)
   308  		//   Import paths of u-root commands; e.g. github.com/mvdan/u-root-coreutils/cmds/* (requires uroot-source)
   309  		var pkgs []string
   310  		for _, a := range flag.Args() {
   311  			p, ok := templates[a]
   312  			if !ok {
   313  				if !validateArg(a) {
   314  					l.Printf("%q is not a valid path, allowed are only existing relative or absolute file paths!", a)
   315  					continue
   316  				}
   317  				pkgs = append(pkgs, a)
   318  				continue
   319  			}
   320  			pkgs = append(pkgs, p...)
   321  		}
   322  		if len(pkgs) == 0 {
   323  			pkgs = []string{"github.com/mvdan/u-root-coreutils/cmds/core/*"}
   324  		}
   325  
   326  		// The command-line tool only allows specifying one build mode
   327  		// right now.
   328  		c = append(c, uroot.Commands{
   329  			Builder:  b,
   330  			Packages: pkgs,
   331  		})
   332  	}
   333  
   334  	opts := uroot.Opts{
   335  		Env:             &env,
   336  		Commands:        c,
   337  		UrootSource:     *urootSourceDir,
   338  		TempDir:         tempDir,
   339  		ExtraFiles:      extraFiles,
   340  		OutputFile:      w,
   341  		BaseArchive:     baseFile,
   342  		UseExistingInit: *useExistingInit,
   343  		InitCmd:         initCommand,
   344  		DefaultShell:    *defaultShell,
   345  		BuildOpts:       buildOpts,
   346  	}
   347  	uinitArgs := shlex.Argv(*uinitCmd)
   348  	if len(uinitArgs) > 0 {
   349  		opts.UinitCmd = uinitArgs[0]
   350  	}
   351  	if len(uinitArgs) > 1 {
   352  		opts.UinitArgs = uinitArgs[1:]
   353  	}
   354  	return uroot.CreateInitramfs(l, opts)
   355  }
   356  
   357  func validateArg(arg string) bool {
   358  	// Do the simple thing first: stat the path.
   359  	// This saves incorrect diagnostics when the
   360  	// path is a perfectly valid relative path.
   361  	if _, err := os.Stat(arg); err == nil {
   362  		return true
   363  	}
   364  	if !checkPrefix(arg) {
   365  		paths, err := filepath.Glob(arg)
   366  		if err != nil {
   367  			return false
   368  		}
   369  		for _, path := range paths {
   370  			if !checkPrefix(path) {
   371  				return false
   372  			}
   373  		}
   374  	}
   375  
   376  	return true
   377  }
   378  
   379  func checkPrefix(arg string) bool {
   380  	prefixes := []string{".", "/", "-", "cmds", "github.com/mvdan/u-root-coreutils"}
   381  	for _, prefix := range prefixes {
   382  		if strings.HasPrefix(arg, prefix) {
   383  			return true
   384  		}
   385  	}
   386  
   387  	return false
   388  }