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