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 }