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