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