github.com/hugelgupf/u-root@v0.0.0-20191023214958-4807c632154c/pkg/bb/bb.go (about)

     1  // Copyright 2015-2019 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 bb builds one busybox-like binary out of many Go command sources.
     6  //
     7  // This allows you to take two Go commands, such as Go implementations of `sl`
     8  // and `cowsay` and compile them into one binary, callable like `./bb sl` and
     9  // `./bb cowsay`.
    10  //
    11  // Which command is invoked is determined by `argv[0]` or `argv[1]` if
    12  // `argv[0]` is not recognized.
    13  //
    14  // Under the hood, bb implements a Go source-to-source transformation on pure
    15  // Go code. This AST transformation does the following:
    16  //
    17  //   - Takes a Go command's source files and rewrites them into Go package files
    18  //     without global side effects.
    19  //   - Writes a `main.go` file with a `main()` that calls into the appropriate Go
    20  //     command package based on `argv[0]`.
    21  //
    22  // Principally, the AST transformation moves all global side-effects into
    23  // callable package functions. E.g. `main` becomes `Main`, each `init` becomes
    24  // `InitN`, and global variable assignments are moved into their own `InitN`.
    25  package bb
    26  
    27  import (
    28  	"bytes"
    29  	"fmt"
    30  	"go/ast"
    31  	"go/format"
    32  	"go/token"
    33  	"go/types"
    34  	"io/ioutil"
    35  	"log"
    36  	"os"
    37  	"path"
    38  	"path/filepath"
    39  	"strconv"
    40  	"strings"
    41  
    42  	"golang.org/x/tools/go/ast/astutil"
    43  	"golang.org/x/tools/go/packages"
    44  	"golang.org/x/tools/imports"
    45  
    46  	"github.com/u-root/u-root/pkg/cp"
    47  	"github.com/u-root/u-root/pkg/golang"
    48  )
    49  
    50  // BuildBusybox builds a busybox of the given Go packages.
    51  //
    52  // pkgs is a list of Go import paths. If nil is returned, binaryPath will hold
    53  // the busybox-style binary.
    54  func BuildBusybox(env golang.Environ, pkgs []string, binaryPath string) (nerr error) {
    55  	tmpDir, err := ioutil.TempDir("", "bb-")
    56  	if err != nil {
    57  		return err
    58  	}
    59  	defer func() {
    60  		if nerr != nil {
    61  			log.Printf("Preserving bb temporary directory at %s due to error", tmpDir)
    62  		} else {
    63  			os.RemoveAll(tmpDir)
    64  		}
    65  	}()
    66  
    67  	// INB4: yes, this *is* too clever. It's because Go modules are too
    68  	// clever. Sorry.
    69  	//
    70  	// Inevitably, we will build commands across multiple modules, e.g.
    71  	// u-root and u-bmc each have their own go.mod, but will get built into
    72  	// one busybox executable.
    73  	//
    74  	// Each u-bmc and u-root command need their respective go.mod
    75  	// dependencies, so we'll preserve their module file.
    76  	//
    77  	// However, we *also* need to still allow building with GOPATH and
    78  	// vendoring. The structure we build *has* to also resemble a
    79  	// GOPATH-based build.
    80  	//
    81  	// The easiest thing to do is to make the directory structure
    82  	// GOPATH-compatible, and use go.mods to replace modules with the local
    83  	// directories.
    84  	//
    85  	// So pretend GOPATH=tmp.
    86  	//
    87  	// Structure we'll build:
    88  	//
    89  	//   tmp/src/bb
    90  	//   tmp/src/bb/main.go
    91  	//      import "<module1>/cmd/foo"
    92  	//      import "<module2>/cmd/bar"
    93  	//
    94  	//      func main()
    95  	//
    96  	// The only way to refer to other Go modules locally is to replace them
    97  	// with local paths in a top-level go.mod:
    98  	//
    99  	//   tmp/go.mod
   100  	//      package bb.u-root.com
   101  	//
   102  	//	replace <module1> => ./src/<module1>
   103  	//	replace <module2> => ./src/<module2>
   104  	//
   105  	// Because GOPATH=tmp, the source should be in $GOPATH/src, just to
   106  	// accommodate both build systems.
   107  	//
   108  	//   tmp/src/<module1>
   109  	//   tmp/src/<module1>/go.mod
   110  	//   tmp/src/<module1>/cmd/foo/main.go
   111  	//
   112  	//   tmp/src/<module2>
   113  	//   tmp/src/<module2>/go.mod
   114  	//   tmp/src/<module2>/cmd/bar/main.go
   115  
   116  	bbDir := filepath.Join(tmpDir, "src/bb")
   117  	if err := os.MkdirAll(bbDir, 0755); err != nil {
   118  		return err
   119  	}
   120  	pkgDir := filepath.Join(tmpDir, "src")
   121  
   122  	// Collect all packages that we need to actually re-write.
   123  	var fpkgs []string
   124  	seenPackages := make(map[string]struct{})
   125  	for _, pkg := range pkgs {
   126  		basePkg := path.Base(pkg)
   127  		if _, ok := seenPackages[basePkg]; ok {
   128  			return fmt.Errorf("failed to build with bb: found duplicate pkgs %s", basePkg)
   129  		}
   130  		seenPackages[basePkg] = struct{}{}
   131  
   132  		fpkgs = append(fpkgs, pkg)
   133  	}
   134  
   135  	// Ask go about all the packages in one batch for dependency caching.
   136  	ps, err := NewPackages(env, fpkgs...)
   137  	if err != nil {
   138  		return fmt.Errorf("finding packages failed: %v", err)
   139  	}
   140  
   141  	var bbImports []string
   142  	for _, p := range ps {
   143  		destination := filepath.Join(pkgDir, p.Pkg.PkgPath)
   144  		if err := p.Rewrite(destination, "github.com/u-root/u-root/pkg/bb/bbmain"); err != nil {
   145  			return fmt.Errorf("rewriting %q failed: %v", p.Pkg.PkgPath, err)
   146  		}
   147  		bbImports = append(bbImports, p.Pkg.PkgPath)
   148  	}
   149  
   150  	bb, err := NewPackages(env, "github.com/u-root/u-root/pkg/bb/bbmain/cmd")
   151  	if err != nil {
   152  		return err
   153  	}
   154  	if len(bb) == 0 {
   155  		return fmt.Errorf("bb cmd template missing")
   156  	}
   157  
   158  	// Add bb to the list of packages that need their dependencies.
   159  	mainPkgs := append(ps, bb[0])
   160  
   161  	// Module-enabled Go programs resolve their dependencies in one of two ways:
   162  	//
   163  	// - locally, if the dependency is *in* the module
   164  	// - remotely, if it is outside of the module
   165  	//
   166  	// I.e. if the module is github.com/u-root/u-root,
   167  	//
   168  	// - local: github.com/u-root/u-root/pkg/uio
   169  	// - remote: github.com/hugelgupf/p9/p9
   170  	//
   171  	// For remote dependencies, we copy the go.mod into the temporary directory.
   172  	// For local dependencies, we copy all dependency packages' files over.
   173  	var depPkgs, modulePaths []string
   174  	for _, p := range mainPkgs {
   175  		// Find all dependency packages that are *within* module boundaries for this package.
   176  		//
   177  		// writeDeps also copies the go.mod into the right place.
   178  		mods, modulePath, err := writeDeps(env, pkgDir, p.Pkg)
   179  		if err != nil {
   180  			return fmt.Errorf("resolving dependencies for %q failed: %v", p.Pkg.PkgPath, err)
   181  		}
   182  		depPkgs = append(depPkgs, mods...)
   183  		if len(modulePath) > 0 {
   184  			modulePaths = append(modulePaths, modulePath)
   185  		}
   186  	}
   187  
   188  	// Create bb main.go.
   189  	if err := CreateBBMainSource(bb[0].Pkg, bbImports, bbDir); err != nil {
   190  		return fmt.Errorf("creating bb main() file failed: %v", err)
   191  	}
   192  
   193  	// Copy local dependency packages into temporary module directories.
   194  	deps, err := NewPackages(env, depPkgs...)
   195  	if err != nil {
   196  		return err
   197  	}
   198  	for _, p := range deps {
   199  		if err := p.Write(filepath.Join(pkgDir, p.Pkg.PkgPath)); err != nil {
   200  			return err
   201  		}
   202  	}
   203  
   204  	// Add local replace rules for all modules we're compiling.
   205  	//
   206  	// This is the only way to locally reference another modules'
   207  	// repository. Otherwise, go'll try to go online to get the source.
   208  	//
   209  	// The module name is something that'll never be online, lest Go
   210  	// decides to go on the internet.
   211  	if len(modulePaths) == 0 {
   212  		env.GOPATH = tmpDir
   213  		// Compile bb.
   214  		return env.Build("bb", binaryPath)
   215  	}
   216  
   217  	content := `module bb.u-root.com`
   218  	for _, mpath := range modulePaths {
   219  		content += fmt.Sprintf("\nreplace %s => ./src/%s\n", mpath, mpath)
   220  	}
   221  	if err := ioutil.WriteFile(filepath.Join(tmpDir, "go.mod"), []byte(content), 0755); err != nil {
   222  		return err
   223  	}
   224  
   225  	// Compile bb.
   226  	return env.BuildDir(bbDir, binaryPath)
   227  }
   228  
   229  func writeDeps(env golang.Environ, pkgDir string, p *packages.Package) ([]string, string, error) {
   230  	listp, err := env.FindOne(p.PkgPath)
   231  	if err != nil {
   232  		return nil, "", err
   233  	}
   234  
   235  	var deps []string
   236  	if listp.Module != nil {
   237  		if err := os.MkdirAll(filepath.Join(pkgDir, listp.Module.Path), 0755); err != nil {
   238  			return nil, "", err
   239  		}
   240  
   241  		// Use the module file for all outside dependencies.
   242  		if err := cp.Copy(listp.Module.GoMod, filepath.Join(pkgDir, listp.Module.Path, "go.mod")); err != nil {
   243  			return nil, "", err
   244  		}
   245  
   246  		// Collect all "local" dependency packages, to be copied into
   247  		// the temporary directory structure later.
   248  		for _, dep := range listp.Deps {
   249  			// Is this a dependency within the module?
   250  			if strings.HasPrefix(dep, listp.Module.Path) {
   251  				deps = append(deps, dep)
   252  			}
   253  		}
   254  		return deps, listp.Module.Path, nil
   255  	}
   256  
   257  	// If modules are not enabled, we need a copy of *ALL*
   258  	// non-standard-library dependencies in the temporary directory.
   259  	for _, dep := range listp.Deps {
   260  		// First component of package path contains a "."?
   261  		//
   262  		// Poor man's standard library test.
   263  		firstComp := strings.SplitN(dep, "/", 2)
   264  		if strings.Contains(firstComp[0], ".") {
   265  			deps = append(deps, dep)
   266  		}
   267  	}
   268  	return deps, "", nil
   269  }
   270  
   271  // CreateBBMainSource creates a bb Go command that imports all given pkgs.
   272  //
   273  // p must be the bb template.
   274  //
   275  // - For each pkg in pkgs, add
   276  //     import _ "pkg"
   277  //   to astp's first file.
   278  // - Write source file out to destDir.
   279  func CreateBBMainSource(p *packages.Package, pkgs []string, destDir string) error {
   280  	if len(p.Syntax) != 1 {
   281  		return fmt.Errorf("bb cmd template is supposed to only have one file")
   282  	}
   283  	for _, pkg := range pkgs {
   284  		// Add side-effect import to bb binary so init registers itself.
   285  		//
   286  		// import _ "pkg"
   287  		astutil.AddNamedImport(p.Fset, p.Syntax[0], "_", pkg)
   288  	}
   289  
   290  	return writeFiles(destDir, p.Fset, p.Syntax)
   291  }
   292  
   293  // Package is a Go package.
   294  type Package struct {
   295  	// Name is the executable command name.
   296  	//
   297  	// In the standard Go tool chain, this is usually the base name of the
   298  	// directory containing its source files.
   299  	Name string
   300  
   301  	// Pkg is the actual data about the package.
   302  	Pkg *packages.Package
   303  
   304  	// initCount keeps track of what the next init's index should be.
   305  	initCount uint
   306  
   307  	// init is the cmd.Init function that calls all other InitXs in the
   308  	// right order.
   309  	init *ast.FuncDecl
   310  
   311  	// initAssigns is a map of assignment expression -> InitN function call
   312  	// statement.
   313  	//
   314  	// That InitN should contain the assignment statement for the
   315  	// appropriate assignment expression.
   316  	//
   317  	// types.Info.InitOrder keeps track of Initializations by Lhs name and
   318  	// Rhs ast.Expr.  We reparent the Rhs in assignment statements in InitN
   319  	// functions, so we use the Rhs as an easy key here.
   320  	// types.Info.InitOrder + initAssigns can then easily be used to derive
   321  	// the order of Stmts in the "real" init.
   322  	//
   323  	// The key Expr must also be the AssignStmt.Rhs[0].
   324  	initAssigns map[ast.Expr]ast.Stmt
   325  }
   326  
   327  // NewPackages collects package metadata about all named packages.
   328  func NewPackages(env golang.Environ, names ...string) ([]*Package, error) {
   329  	ps, err := loadPkgs(env, names...)
   330  	if err != nil {
   331  		return nil, fmt.Errorf("failed to load package %v: %v", names, err)
   332  	}
   333  	var ips []*Package
   334  	for _, p := range ps {
   335  		ips = append(ips, NewPackage(path.Base(p.PkgPath), p))
   336  	}
   337  	return ips, nil
   338  }
   339  
   340  func loadPkgs(env golang.Environ, patterns ...string) ([]*packages.Package, error) {
   341  	cfg := &packages.Config{
   342  		Mode: packages.NeedName | packages.NeedImports | packages.NeedFiles | packages.NeedDeps | packages.NeedTypes | packages.NeedSyntax | packages.NeedTypesInfo | packages.NeedCompiledGoFiles,
   343  		Env:  append(os.Environ(), env.Env()...),
   344  	}
   345  	return packages.Load(cfg, patterns...)
   346  }
   347  
   348  // NewPackage creates a new Package based on an existing packages.Package.
   349  func NewPackage(name string, p *packages.Package) *Package {
   350  	pp := &Package{
   351  		// Name is the executable name.
   352  		Name:        path.Base(name),
   353  		Pkg:         p,
   354  		initAssigns: make(map[ast.Expr]ast.Stmt),
   355  	}
   356  
   357  	// This Init will hold calls to all other InitXs.
   358  	pp.init = &ast.FuncDecl{
   359  		Name: ast.NewIdent("Init"),
   360  		Type: &ast.FuncType{
   361  			Params:  &ast.FieldList{},
   362  			Results: nil,
   363  		},
   364  		Body: &ast.BlockStmt{},
   365  	}
   366  	return pp
   367  }
   368  
   369  func (p *Package) nextInit(addToCallList bool) *ast.Ident {
   370  	i := ast.NewIdent(fmt.Sprintf("Init%d", p.initCount))
   371  	if addToCallList {
   372  		p.init.Body.List = append(p.init.Body.List, &ast.ExprStmt{X: &ast.CallExpr{Fun: i}})
   373  	}
   374  	p.initCount++
   375  	return i
   376  }
   377  
   378  // TODO:
   379  // - write an init name generator, in case InitN is already taken.
   380  func (p *Package) rewriteFile(f *ast.File) bool {
   381  	hasMain := false
   382  
   383  	// Change the package name declaration from main to the command's name.
   384  	f.Name.Name = p.Name
   385  
   386  	// Map of fully qualified package name -> imported alias in the file.
   387  	importAliases := make(map[string]string)
   388  	for _, impt := range f.Imports {
   389  		if impt.Name != nil {
   390  			importPath, err := strconv.Unquote(impt.Path.Value)
   391  			if err != nil {
   392  				panic(err)
   393  			}
   394  			importAliases[importPath] = impt.Name.Name
   395  		}
   396  	}
   397  
   398  	// When the types.TypeString function translates package names, it uses
   399  	// this function to map fully qualified package paths to a local alias,
   400  	// if it exists.
   401  	qualifier := func(pkg *types.Package) string {
   402  		name, ok := importAliases[pkg.Path()]
   403  		if ok {
   404  			return name
   405  		}
   406  		// When referring to self, don't use any package name.
   407  		if pkg == p.Pkg.Types {
   408  			return ""
   409  		}
   410  		return pkg.Name()
   411  	}
   412  
   413  	for _, decl := range f.Decls {
   414  		switch d := decl.(type) {
   415  		case *ast.GenDecl:
   416  			// We only care about vars.
   417  			if d.Tok != token.VAR {
   418  				break
   419  			}
   420  			for _, spec := range d.Specs {
   421  				s := spec.(*ast.ValueSpec)
   422  				if s.Values == nil {
   423  					continue
   424  				}
   425  
   426  				// For each assignment, create a new init
   427  				// function, and place it in the same file.
   428  				for i, name := range s.Names {
   429  					varInit := &ast.FuncDecl{
   430  						Name: p.nextInit(false),
   431  						Type: &ast.FuncType{
   432  							Params:  &ast.FieldList{},
   433  							Results: nil,
   434  						},
   435  						Body: &ast.BlockStmt{
   436  							List: []ast.Stmt{
   437  								&ast.AssignStmt{
   438  									Lhs: []ast.Expr{name},
   439  									Tok: token.ASSIGN,
   440  									Rhs: []ast.Expr{s.Values[i]},
   441  								},
   442  							},
   443  						},
   444  					}
   445  					// Add a call to the new init func to
   446  					// this map, so they can be added to
   447  					// Init0() in the correct init order
   448  					// later.
   449  					p.initAssigns[s.Values[i]] = &ast.ExprStmt{X: &ast.CallExpr{Fun: varInit.Name}}
   450  					f.Decls = append(f.Decls, varInit)
   451  				}
   452  
   453  				// Add the type of the expression to the global
   454  				// declaration instead.
   455  				if s.Type == nil {
   456  					typ := p.Pkg.TypesInfo.Types[s.Values[0]]
   457  					s.Type = ast.NewIdent(types.TypeString(typ.Type, qualifier))
   458  				}
   459  				s.Values = nil
   460  			}
   461  
   462  		case *ast.FuncDecl:
   463  			if d.Recv == nil && d.Name.Name == "main" {
   464  				d.Name.Name = "Main"
   465  				hasMain = true
   466  			}
   467  			if d.Recv == nil && d.Name.Name == "init" {
   468  				d.Name = p.nextInit(true)
   469  			}
   470  		}
   471  	}
   472  
   473  	// Now we change any import names attached to package declarations. We
   474  	// just upcase it for now; it makes it easy to look in bbsh for things
   475  	// we changed, e.g. grep -r bbsh Import is useful.
   476  	for _, cg := range f.Comments {
   477  		for _, c := range cg.List {
   478  			if strings.HasPrefix(c.Text, "// import") {
   479  				c.Text = "// Import" + c.Text[9:]
   480  			}
   481  		}
   482  	}
   483  	return hasMain
   484  }
   485  
   486  // Write writes p into destDir.
   487  func (p *Package) Write(destDir string) error {
   488  	if err := os.MkdirAll(destDir, 0755); err != nil {
   489  		return err
   490  	}
   491  
   492  	for _, fp := range p.Pkg.OtherFiles {
   493  		if err := cp.Copy(fp, filepath.Join(destDir, filepath.Base(fp))); err != nil {
   494  			return err
   495  		}
   496  	}
   497  
   498  	return writeFiles(destDir, p.Pkg.Fset, p.Pkg.Syntax)
   499  }
   500  
   501  func writeFiles(destDir string, fset *token.FileSet, files []*ast.File) error {
   502  	// Write all files out.
   503  	for _, file := range files {
   504  		name := fset.File(file.Package).Name()
   505  
   506  		path := filepath.Join(destDir, filepath.Base(name))
   507  		if err := writeFile(path, fset, file); err != nil {
   508  			return err
   509  		}
   510  	}
   511  	return nil
   512  }
   513  
   514  // Rewrite rewrites p into destDir as a bb package using bbImportPath for the
   515  // bb implementation.
   516  func (p *Package) Rewrite(destDir, bbImportPath string) error {
   517  	// This init holds all variable initializations.
   518  	//
   519  	// func Init0() {}
   520  	varInit := &ast.FuncDecl{
   521  		Name: p.nextInit(true),
   522  		Type: &ast.FuncType{
   523  			Params:  &ast.FieldList{},
   524  			Results: nil,
   525  		},
   526  		Body: &ast.BlockStmt{},
   527  	}
   528  
   529  	var mainFile *ast.File
   530  	for _, sourceFile := range p.Pkg.Syntax {
   531  		if hasMainFile := p.rewriteFile(sourceFile); hasMainFile {
   532  			mainFile = sourceFile
   533  		}
   534  	}
   535  	if mainFile == nil {
   536  		return fmt.Errorf("no main function found in package %q", p.Pkg.PkgPath)
   537  	}
   538  
   539  	// Add variable initializations to Init0 in the right order.
   540  	for _, initStmt := range p.Pkg.TypesInfo.InitOrder {
   541  		a, ok := p.initAssigns[initStmt.Rhs]
   542  		if !ok {
   543  			return fmt.Errorf("couldn't find init assignment %s", initStmt)
   544  		}
   545  		varInit.Body.List = append(varInit.Body.List, a)
   546  	}
   547  
   548  	// import bb "bbImportPath"
   549  	astutil.AddNamedImport(p.Pkg.Fset, mainFile, "bb", bbImportPath)
   550  
   551  	// func init() {
   552  	//   bb.Register(p.name, Init, Main)
   553  	// }
   554  	bbRegisterInit := &ast.FuncDecl{
   555  		Name: ast.NewIdent("init"),
   556  		Type: &ast.FuncType{},
   557  		Body: &ast.BlockStmt{
   558  			List: []ast.Stmt{
   559  				&ast.ExprStmt{X: &ast.CallExpr{
   560  					Fun: ast.NewIdent("bb.Register"),
   561  					Args: []ast.Expr{
   562  						// name=
   563  						&ast.BasicLit{
   564  							Kind:  token.STRING,
   565  							Value: strconv.Quote(p.Name),
   566  						},
   567  						// init=
   568  						ast.NewIdent("Init"),
   569  						// main=
   570  						ast.NewIdent("Main"),
   571  					},
   572  				}},
   573  			},
   574  		},
   575  	}
   576  
   577  	// We could add these statements to any of the package files. We choose
   578  	// the one that contains Main() to guarantee reproducibility of the
   579  	// same bbsh binary.
   580  	mainFile.Decls = append(mainFile.Decls, varInit, p.init, bbRegisterInit)
   581  
   582  	return p.Write(destDir)
   583  }
   584  
   585  func writeFile(path string, fset *token.FileSet, f *ast.File) error {
   586  	var buf bytes.Buffer
   587  	if err := format.Node(&buf, fset, f); err != nil {
   588  		return fmt.Errorf("error formatting Go file %q: %v", path, err)
   589  	}
   590  	return writeGoFile(path, buf.Bytes())
   591  }
   592  
   593  func writeGoFile(path string, code []byte) error {
   594  	// Format the file. Do not fix up imports, as we only moved code around
   595  	// within files.
   596  	opts := imports.Options{
   597  		Comments:   true,
   598  		TabIndent:  true,
   599  		TabWidth:   8,
   600  		FormatOnly: true,
   601  	}
   602  	code, err := imports.Process("commandline", code, &opts)
   603  	if err != nil {
   604  		return fmt.Errorf("bad parse while processing imports %q: %v", path, err)
   605  	}
   606  
   607  	if err := ioutil.WriteFile(path, code, 0644); err != nil {
   608  		return fmt.Errorf("error writing Go file to %q: %v", path, err)
   609  	}
   610  	return nil
   611  }