github.com/u-root/u-root@v7.0.1-0.20200915234505-ad7babab0a8e+incompatible/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/build"
    32  	"go/format"
    33  	"go/importer"
    34  	"go/parser"
    35  	"go/token"
    36  	"go/types"
    37  	"io/ioutil"
    38  	"os"
    39  	"path"
    40  	"path/filepath"
    41  	"sort"
    42  	"strconv"
    43  	"strings"
    44  	"time"
    45  
    46  	"golang.org/x/tools/go/ast/astutil"
    47  	"golang.org/x/tools/imports"
    48  
    49  	"github.com/u-root/u-root/pkg/golang"
    50  	"github.com/u-root/u-root/pkg/lockfile"
    51  )
    52  
    53  // Commands to skip building in bb mode.
    54  var skip = map[string]struct{}{
    55  	"bb": {},
    56  }
    57  
    58  func getBBLock(bblock string) (*lockfile.Lockfile, error) {
    59  	secondsTimeout := 150
    60  	timer := time.After(time.Duration(secondsTimeout) * time.Second)
    61  	lock := lockfile.New(bblock)
    62  	for {
    63  		select {
    64  		case <-timer:
    65  			return nil, fmt.Errorf("could not acquire bblock file %q: %d second deadline expired", bblock, secondsTimeout)
    66  		default:
    67  		}
    68  
    69  		switch err := lock.TryLock(); err {
    70  		case nil:
    71  			return lock, nil
    72  
    73  		case lockfile.ErrBusy:
    74  			// This sucks. Use inotify.
    75  			time.Sleep(100 * time.Millisecond)
    76  
    77  		default:
    78  			return nil, err
    79  		}
    80  	}
    81  }
    82  
    83  // BuildBusybox builds a busybox of the given Go packages.
    84  //
    85  // pkgs is a list of Go import paths. If nil is returned, binaryPath will hold
    86  // the busybox-style binary.
    87  func BuildBusybox(env golang.Environ, pkgs []string, noStrip bool, binaryPath string) error {
    88  	urootPkg, err := env.Package("github.com/u-root/u-root")
    89  	if err != nil {
    90  		return err
    91  	}
    92  
    93  	bblock := filepath.Join(urootPkg.Dir, "bblock")
    94  	// Only one busybox can be compiled at a time.
    95  	//
    96  	// Since busybox files all get rewritten in
    97  	// GOPATH/src/github.com/u-root/u-root/bb/..., no more than one source
    98  	// transformation can be in progress at the same time. Otherwise,
    99  	// different bb processes will write a different set of files to the
   100  	// "bb" directory at the same time, potentially producing an unintended
   101  	// bb binary.
   102  	//
   103  	// Doing each rewrite in a temporary unique directory is not an option
   104  	// as that kills reproducible builds.
   105  	l, err := getBBLock(bblock)
   106  	if err != nil {
   107  		return err
   108  	}
   109  	defer l.Unlock()
   110  
   111  	bbDir := filepath.Join(urootPkg.Dir, "bb")
   112  	// Blow bb away before trying to re-create it.
   113  	if err := os.RemoveAll(bbDir); err != nil {
   114  		return err
   115  	}
   116  	if err := os.MkdirAll(bbDir, 0755); err != nil {
   117  		return err
   118  	}
   119  
   120  	var bbPackages []string
   121  	// Move and rewrite package files.
   122  	importer := importer.For("source", nil)
   123  	seenPackages := map[string]bool{}
   124  	for _, pkg := range pkgs {
   125  		basePkg := path.Base(pkg)
   126  		if _, ok := skip[basePkg]; ok {
   127  			continue
   128  		}
   129  		if _, ok := seenPackages[path.Base(pkg)]; ok {
   130  			return fmt.Errorf("failed to build with bb: found duplicate pkgs %s", basePkg)
   131  		}
   132  		seenPackages[basePkg] = true
   133  
   134  		// TODO: use bbDir to derive import path below or vice versa.
   135  		if err := RewritePackage(env, pkg, "github.com/u-root/u-root/pkg/bb/bbmain", importer); err != nil {
   136  			return err
   137  		}
   138  
   139  		bbPackages = append(bbPackages, path.Join(pkg, ".bb"))
   140  	}
   141  
   142  	bb, err := NewPackageFromEnv(env, "github.com/u-root/u-root/pkg/bb/bbmain/cmd", importer)
   143  	if err != nil {
   144  		return err
   145  	}
   146  	if bb == nil {
   147  		return fmt.Errorf("bb cmd template missing")
   148  	}
   149  	if len(bb.ast.Files) != 1 {
   150  		return fmt.Errorf("bb cmd template is supposed to only have one file")
   151  	}
   152  	// Create bb main.go.
   153  	if err := CreateBBMainSource(bb.fset, bb.ast, bbPackages, bbDir); err != nil {
   154  		return err
   155  	}
   156  
   157  	// Compile bb.
   158  	return env.Build("github.com/u-root/u-root/bb", binaryPath, golang.BuildOpts{NoStrip: noStrip})
   159  }
   160  
   161  // CreateBBMainSource creates a bb Go command that imports all given pkgs.
   162  //
   163  // p must be the bb template.
   164  //
   165  // - For each pkg in pkgs, add
   166  //     import _ "pkg"
   167  //   to astp's first file.
   168  // - Write source file out to destDir.
   169  func CreateBBMainSource(fset *token.FileSet, astp *ast.Package, pkgs []string, destDir string) error {
   170  	for _, pkg := range pkgs {
   171  		for _, sourceFile := range astp.Files {
   172  			// Add side-effect import to bb binary so init registers itself.
   173  			//
   174  			// import _ "pkg"
   175  			astutil.AddNamedImport(fset, sourceFile, "_", pkg)
   176  			break
   177  		}
   178  	}
   179  
   180  	// Write bb main binary out.
   181  	for filePath, sourceFile := range astp.Files {
   182  		path := filepath.Join(destDir, filepath.Base(filePath))
   183  		if err := writeFile(path, fset, sourceFile); err != nil {
   184  			return err
   185  		}
   186  		break
   187  	}
   188  	return nil
   189  }
   190  
   191  // Package is a Go package.
   192  //
   193  // It holds AST, type, file, and Go package information about a Go package.
   194  type Package struct {
   195  	// Name is the command name.
   196  	//
   197  	// In the standard Go tool chain, this is usually the base name of the
   198  	// directory containing its source files.
   199  	Name string
   200  
   201  	fset        *token.FileSet
   202  	ast         *ast.Package
   203  	sortedFiles []*ast.File
   204  
   205  	typeInfo types.Info
   206  	types    *types.Package
   207  
   208  	// initCount keeps track of what the next init's index should be.
   209  	initCount uint
   210  
   211  	// init is the cmd.Init function that calls all other InitXs in the
   212  	// right order.
   213  	init *ast.FuncDecl
   214  
   215  	// initAssigns is a map of assignment expression -> InitN function call
   216  	// statement.
   217  	//
   218  	// That InitN should contain the assignment statement for the
   219  	// appropriate assignment expression.
   220  	//
   221  	// types.Info.InitOrder keeps track of Initializations by Lhs name and
   222  	// Rhs ast.Expr.  We reparent the Rhs in assignment statements in InitN
   223  	// functions, so we use the Rhs as an easy key here.
   224  	// types.Info.InitOrder + initAssigns can then easily be used to derive
   225  	// the order of Stmts in the "real" init.
   226  	//
   227  	// The key Expr must also be the AssignStmt.Rhs[0].
   228  	initAssigns map[ast.Expr]ast.Stmt
   229  }
   230  
   231  // NewPackageFromEnv finds the package identified by importPath, and gathers
   232  // AST, type, and token information.
   233  func NewPackageFromEnv(env golang.Environ, importPath string, importer types.Importer) (*Package, error) {
   234  	p, err := env.Package(importPath)
   235  	if err != nil {
   236  		return nil, err
   237  	}
   238  	return NewPackage(filepath.Base(p.Dir), p.ImportPath, SrcFiles(p), importer)
   239  }
   240  
   241  // ParseAST parses the given files for a package named main.
   242  //
   243  // Only files with a matching package statement will be part of the AST
   244  // returned.
   245  func ParseAST(files []string) (*token.FileSet, *ast.Package, error) {
   246  	fset := token.NewFileSet()
   247  	p := &ast.Package{
   248  		Name:  "main",
   249  		Files: make(map[string]*ast.File),
   250  	}
   251  	for _, path := range files {
   252  		if src, err := parser.ParseFile(fset, path, nil, parser.ParseComments); err == nil && src.Name.Name == p.Name {
   253  			p.Files[path] = src
   254  		} else if err != nil {
   255  			return nil, nil, fmt.Errorf("failed to parse AST in file %q: %v", path, err)
   256  		}
   257  	}
   258  
   259  	// Did we parse anything?
   260  	if len(p.Files) == 0 {
   261  		return nil, nil, fmt.Errorf("no valid `main` package files found in %v", files)
   262  	}
   263  	return fset, p, nil
   264  }
   265  
   266  // SrcFiles lists all the Go source files for p.
   267  func SrcFiles(p *build.Package) []string {
   268  	files := make([]string, 0, len(p.GoFiles))
   269  	for _, name := range p.GoFiles {
   270  		files = append(files, filepath.Join(p.Dir, name))
   271  	}
   272  	return files
   273  }
   274  
   275  // RewritePackage rewrites pkgPath to be bb-mode compatible, where destDir is
   276  // the file system destination of the written files and bbImportPath is the Go
   277  // import path of the bb package to register with.
   278  func RewritePackage(env golang.Environ, pkgPath, bbImportPath string, importer types.Importer) error {
   279  	buildp, err := env.Package(pkgPath)
   280  	if err != nil {
   281  		return err
   282  	}
   283  
   284  	p, err := NewPackage(filepath.Base(buildp.Dir), buildp.ImportPath, SrcFiles(buildp), importer)
   285  	if err != nil {
   286  		return err
   287  	}
   288  	dest := filepath.Join(buildp.Dir, ".bb")
   289  	// If .bb directory already exists, delete it. This will prevent stale
   290  	// files from being included in the build.
   291  	if err := os.RemoveAll(dest); err != nil {
   292  		return fmt.Errorf("error removing stale directory %q: %v", dest, err)
   293  	}
   294  	return p.Rewrite(dest, bbImportPath)
   295  }
   296  
   297  // NewPackage gathers AST, type, and token information about a command.
   298  //
   299  // The given importer is used to resolve dependencies.
   300  func NewPackage(name string, pkgPath string, srcFiles []string, importer types.Importer) (*Package, error) {
   301  	fset, astp, err := ParseAST(srcFiles)
   302  	if err != nil {
   303  		return nil, err
   304  	}
   305  
   306  	p := &Package{
   307  		Name: name,
   308  		fset: fset,
   309  		ast:  astp,
   310  		typeInfo: types.Info{
   311  			Types: make(map[ast.Expr]types.TypeAndValue),
   312  		},
   313  		initAssigns: make(map[ast.Expr]ast.Stmt),
   314  	}
   315  
   316  	// This Init will hold calls to all other InitXs.
   317  	p.init = &ast.FuncDecl{
   318  		Name: ast.NewIdent("Init"),
   319  		Type: &ast.FuncType{
   320  			Params:  &ast.FieldList{},
   321  			Results: nil,
   322  		},
   323  		Body: &ast.BlockStmt{},
   324  	}
   325  
   326  	// The order of types.Info.InitOrder depends on this list of files
   327  	// always being passed to conf.Check in the same order.
   328  	filenames := make([]string, 0, len(p.ast.Files))
   329  	for name := range p.ast.Files {
   330  		filenames = append(filenames, name)
   331  	}
   332  	sort.Strings(filenames)
   333  
   334  	p.sortedFiles = make([]*ast.File, 0, len(p.ast.Files))
   335  	for _, name := range filenames {
   336  		p.sortedFiles = append(p.sortedFiles, p.ast.Files[name])
   337  	}
   338  
   339  	// Type-check the package before we continue. We need types to rewrite
   340  	// some statements.
   341  	conf := types.Config{
   342  		Importer: importer,
   343  
   344  		// We only need global declarations' types.
   345  		IgnoreFuncBodies: true,
   346  	}
   347  	tpkg, err := conf.Check(pkgPath, p.fset, p.sortedFiles, &p.typeInfo)
   348  	if err != nil {
   349  		return nil, fmt.Errorf("type checking failed: %v", err)
   350  	}
   351  	p.types = tpkg
   352  	return p, nil
   353  }
   354  
   355  func (p *Package) nextInit(addToCallList bool) *ast.Ident {
   356  	i := ast.NewIdent(fmt.Sprintf("Init%d", p.initCount))
   357  	if addToCallList {
   358  		p.init.Body.List = append(p.init.Body.List, &ast.ExprStmt{X: &ast.CallExpr{Fun: i}})
   359  	}
   360  	p.initCount++
   361  	return i
   362  }
   363  
   364  // TODO:
   365  // - write an init name generator, in case InitN is already taken.
   366  // - also rewrite all non-Go-stdlib dependencies.
   367  func (p *Package) rewriteFile(f *ast.File) bool {
   368  	hasMain := false
   369  
   370  	// Change the package name declaration from main to the command's name.
   371  	f.Name = ast.NewIdent(p.Name)
   372  
   373  	// Map of fully qualified package name -> imported alias in the file.
   374  	importAliases := make(map[string]string)
   375  	for _, impt := range f.Imports {
   376  		if impt.Name != nil {
   377  			importPath, err := strconv.Unquote(impt.Path.Value)
   378  			if err != nil {
   379  				panic(err)
   380  			}
   381  			importAliases[importPath] = impt.Name.Name
   382  		}
   383  	}
   384  
   385  	// When the types.TypeString function translates package names, it uses
   386  	// this function to map fully qualified package paths to a local alias,
   387  	// if it exists.
   388  	qualifier := func(pkg *types.Package) string {
   389  		name, ok := importAliases[pkg.Path()]
   390  		if ok {
   391  			return name
   392  		}
   393  		// When referring to self, don't use any package name.
   394  		if pkg == p.types {
   395  			return ""
   396  		}
   397  		return pkg.Name()
   398  	}
   399  
   400  	for _, decl := range f.Decls {
   401  		switch d := decl.(type) {
   402  		case *ast.GenDecl:
   403  			// We only care about vars.
   404  			if d.Tok != token.VAR {
   405  				break
   406  			}
   407  			for _, spec := range d.Specs {
   408  				s := spec.(*ast.ValueSpec)
   409  				if s.Values == nil {
   410  					continue
   411  				}
   412  
   413  				// For each assignment, create a new init
   414  				// function, and place it in the same file.
   415  				for i, name := range s.Names {
   416  					varInit := &ast.FuncDecl{
   417  						Name: p.nextInit(false),
   418  						Type: &ast.FuncType{
   419  							Params:  &ast.FieldList{},
   420  							Results: nil,
   421  						},
   422  						Body: &ast.BlockStmt{
   423  							List: []ast.Stmt{
   424  								&ast.AssignStmt{
   425  									Lhs: []ast.Expr{name},
   426  									Tok: token.ASSIGN,
   427  									Rhs: []ast.Expr{s.Values[i]},
   428  								},
   429  							},
   430  						},
   431  					}
   432  					// Add a call to the new init func to
   433  					// this map, so they can be added to
   434  					// Init0() in the correct init order
   435  					// later.
   436  					p.initAssigns[s.Values[i]] = &ast.ExprStmt{X: &ast.CallExpr{Fun: varInit.Name}}
   437  					f.Decls = append(f.Decls, varInit)
   438  				}
   439  
   440  				// Add the type of the expression to the global
   441  				// declaration instead.
   442  				if s.Type == nil {
   443  					typ := p.typeInfo.Types[s.Values[0]]
   444  					s.Type = ast.NewIdent(types.TypeString(typ.Type, qualifier))
   445  				}
   446  				s.Values = nil
   447  			}
   448  
   449  		case *ast.FuncDecl:
   450  			if d.Recv == nil && d.Name.Name == "main" {
   451  				d.Name.Name = "Main"
   452  				hasMain = true
   453  			}
   454  			if d.Recv == nil && d.Name.Name == "init" {
   455  				d.Name = p.nextInit(true)
   456  			}
   457  		}
   458  	}
   459  
   460  	// Now we change any import names attached to package declarations. We
   461  	// just upcase it for now; it makes it easy to look in bbsh for things
   462  	// we changed, e.g. grep -r bbsh Import is useful.
   463  	for _, cg := range f.Comments {
   464  		for _, c := range cg.List {
   465  			if strings.HasPrefix(c.Text, "// import") {
   466  				c.Text = "// Import" + c.Text[9:]
   467  			}
   468  		}
   469  	}
   470  	return hasMain
   471  }
   472  
   473  // Rewrite rewrites p into destDir as a bb package using bbImportPath for the
   474  // bb implementation.
   475  func (p *Package) Rewrite(destDir, bbImportPath string) error {
   476  	if err := os.MkdirAll(destDir, 0755); err != nil {
   477  		return err
   478  	}
   479  
   480  	// This init holds all variable initializations.
   481  	//
   482  	// func Init0() {}
   483  	varInit := &ast.FuncDecl{
   484  		Name: p.nextInit(true),
   485  		Type: &ast.FuncType{
   486  			Params:  &ast.FieldList{},
   487  			Results: nil,
   488  		},
   489  		Body: &ast.BlockStmt{},
   490  	}
   491  
   492  	var mainFile *ast.File
   493  	for _, sourceFile := range p.sortedFiles {
   494  		if hasMainFile := p.rewriteFile(sourceFile); hasMainFile {
   495  			mainFile = sourceFile
   496  		}
   497  	}
   498  	if mainFile == nil {
   499  		return os.RemoveAll(destDir)
   500  	}
   501  
   502  	// Add variable initializations to Init0 in the right order.
   503  	for _, initStmt := range p.typeInfo.InitOrder {
   504  		a, ok := p.initAssigns[initStmt.Rhs]
   505  		if !ok {
   506  			return fmt.Errorf("couldn't find init assignment %s", initStmt)
   507  		}
   508  		varInit.Body.List = append(varInit.Body.List, a)
   509  	}
   510  
   511  	// import bb "bbImportPath"
   512  	astutil.AddNamedImport(p.fset, mainFile, "bb", bbImportPath)
   513  
   514  	// func init() {
   515  	//   bb.Register(p.name, Init, Main)
   516  	// }
   517  	bbRegisterInit := &ast.FuncDecl{
   518  		Name: ast.NewIdent("init"),
   519  		Type: &ast.FuncType{},
   520  		Body: &ast.BlockStmt{
   521  			List: []ast.Stmt{
   522  				&ast.ExprStmt{X: &ast.CallExpr{
   523  					Fun: ast.NewIdent("bb.Register"),
   524  					Args: []ast.Expr{
   525  						// name=
   526  						&ast.BasicLit{
   527  							Kind:  token.STRING,
   528  							Value: strconv.Quote(p.Name),
   529  						},
   530  						// init=
   531  						ast.NewIdent("Init"),
   532  						// main=
   533  						ast.NewIdent("Main"),
   534  					},
   535  				}},
   536  			},
   537  		},
   538  	}
   539  
   540  	// We could add these statements to any of the package files. We choose
   541  	// the one that contains Main() to guarantee reproducibility of the
   542  	// same bbsh binary.
   543  	mainFile.Decls = append(mainFile.Decls, varInit, p.init, bbRegisterInit)
   544  
   545  	// Write all files out.
   546  	for filePath, sourceFile := range p.ast.Files {
   547  		path := filepath.Join(destDir, filepath.Base(filePath))
   548  		if err := writeFile(path, p.fset, sourceFile); err != nil {
   549  			return err
   550  		}
   551  	}
   552  	return nil
   553  }
   554  
   555  func writeFile(path string, fset *token.FileSet, f *ast.File) error {
   556  	var buf bytes.Buffer
   557  	if err := format.Node(&buf, fset, f); err != nil {
   558  		return fmt.Errorf("error formatting Go file %q: %v", path, err)
   559  	}
   560  	return writeGoFile(path, buf.Bytes())
   561  }
   562  
   563  func writeGoFile(path string, code []byte) error {
   564  	// Format the file. Do not fix up imports, as we only moved code around
   565  	// within files.
   566  	opts := imports.Options{
   567  		Comments:   true,
   568  		TabIndent:  true,
   569  		TabWidth:   8,
   570  		FormatOnly: true,
   571  	}
   572  	code, err := imports.Process("commandline", code, &opts)
   573  	if err != nil {
   574  		return fmt.Errorf("bad parse while processing imports %q: %v", path, err)
   575  	}
   576  
   577  	if err := ioutil.WriteFile(path, code, 0644); err != nil {
   578  		return fmt.Errorf("error writing Go file to %q: %v", path, err)
   579  	}
   580  	return nil
   581  }