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