github.com/hugelgupf/u-root@v0.0.0-20191023214958-4807c632154c/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/format" 32 "go/token" 33 "go/types" 34 "io/ioutil" 35 "log" 36 "os" 37 "path" 38 "path/filepath" 39 "strconv" 40 "strings" 41 42 "golang.org/x/tools/go/ast/astutil" 43 "golang.org/x/tools/go/packages" 44 "golang.org/x/tools/imports" 45 46 "github.com/u-root/u-root/pkg/cp" 47 "github.com/u-root/u-root/pkg/golang" 48 ) 49 50 // BuildBusybox builds a busybox of the given Go packages. 51 // 52 // pkgs is a list of Go import paths. If nil is returned, binaryPath will hold 53 // the busybox-style binary. 54 func BuildBusybox(env golang.Environ, pkgs []string, binaryPath string) (nerr error) { 55 tmpDir, err := ioutil.TempDir("", "bb-") 56 if err != nil { 57 return err 58 } 59 defer func() { 60 if nerr != nil { 61 log.Printf("Preserving bb temporary directory at %s due to error", tmpDir) 62 } else { 63 os.RemoveAll(tmpDir) 64 } 65 }() 66 67 // INB4: yes, this *is* too clever. It's because Go modules are too 68 // clever. Sorry. 69 // 70 // Inevitably, we will build commands across multiple modules, e.g. 71 // u-root and u-bmc each have their own go.mod, but will get built into 72 // one busybox executable. 73 // 74 // Each u-bmc and u-root command need their respective go.mod 75 // dependencies, so we'll preserve their module file. 76 // 77 // However, we *also* need to still allow building with GOPATH and 78 // vendoring. The structure we build *has* to also resemble a 79 // GOPATH-based build. 80 // 81 // The easiest thing to do is to make the directory structure 82 // GOPATH-compatible, and use go.mods to replace modules with the local 83 // directories. 84 // 85 // So pretend GOPATH=tmp. 86 // 87 // Structure we'll build: 88 // 89 // tmp/src/bb 90 // tmp/src/bb/main.go 91 // import "<module1>/cmd/foo" 92 // import "<module2>/cmd/bar" 93 // 94 // func main() 95 // 96 // The only way to refer to other Go modules locally is to replace them 97 // with local paths in a top-level go.mod: 98 // 99 // tmp/go.mod 100 // package bb.u-root.com 101 // 102 // replace <module1> => ./src/<module1> 103 // replace <module2> => ./src/<module2> 104 // 105 // Because GOPATH=tmp, the source should be in $GOPATH/src, just to 106 // accommodate both build systems. 107 // 108 // tmp/src/<module1> 109 // tmp/src/<module1>/go.mod 110 // tmp/src/<module1>/cmd/foo/main.go 111 // 112 // tmp/src/<module2> 113 // tmp/src/<module2>/go.mod 114 // tmp/src/<module2>/cmd/bar/main.go 115 116 bbDir := filepath.Join(tmpDir, "src/bb") 117 if err := os.MkdirAll(bbDir, 0755); err != nil { 118 return err 119 } 120 pkgDir := filepath.Join(tmpDir, "src") 121 122 // Collect all packages that we need to actually re-write. 123 var fpkgs []string 124 seenPackages := make(map[string]struct{}) 125 for _, pkg := range pkgs { 126 basePkg := path.Base(pkg) 127 if _, ok := seenPackages[basePkg]; ok { 128 return fmt.Errorf("failed to build with bb: found duplicate pkgs %s", basePkg) 129 } 130 seenPackages[basePkg] = struct{}{} 131 132 fpkgs = append(fpkgs, pkg) 133 } 134 135 // Ask go about all the packages in one batch for dependency caching. 136 ps, err := NewPackages(env, fpkgs...) 137 if err != nil { 138 return fmt.Errorf("finding packages failed: %v", err) 139 } 140 141 var bbImports []string 142 for _, p := range ps { 143 destination := filepath.Join(pkgDir, p.Pkg.PkgPath) 144 if err := p.Rewrite(destination, "github.com/u-root/u-root/pkg/bb/bbmain"); err != nil { 145 return fmt.Errorf("rewriting %q failed: %v", p.Pkg.PkgPath, err) 146 } 147 bbImports = append(bbImports, p.Pkg.PkgPath) 148 } 149 150 bb, err := NewPackages(env, "github.com/u-root/u-root/pkg/bb/bbmain/cmd") 151 if err != nil { 152 return err 153 } 154 if len(bb) == 0 { 155 return fmt.Errorf("bb cmd template missing") 156 } 157 158 // Add bb to the list of packages that need their dependencies. 159 mainPkgs := append(ps, bb[0]) 160 161 // Module-enabled Go programs resolve their dependencies in one of two ways: 162 // 163 // - locally, if the dependency is *in* the module 164 // - remotely, if it is outside of the module 165 // 166 // I.e. if the module is github.com/u-root/u-root, 167 // 168 // - local: github.com/u-root/u-root/pkg/uio 169 // - remote: github.com/hugelgupf/p9/p9 170 // 171 // For remote dependencies, we copy the go.mod into the temporary directory. 172 // For local dependencies, we copy all dependency packages' files over. 173 var depPkgs, modulePaths []string 174 for _, p := range mainPkgs { 175 // Find all dependency packages that are *within* module boundaries for this package. 176 // 177 // writeDeps also copies the go.mod into the right place. 178 mods, modulePath, err := writeDeps(env, pkgDir, p.Pkg) 179 if err != nil { 180 return fmt.Errorf("resolving dependencies for %q failed: %v", p.Pkg.PkgPath, err) 181 } 182 depPkgs = append(depPkgs, mods...) 183 if len(modulePath) > 0 { 184 modulePaths = append(modulePaths, modulePath) 185 } 186 } 187 188 // Create bb main.go. 189 if err := CreateBBMainSource(bb[0].Pkg, bbImports, bbDir); err != nil { 190 return fmt.Errorf("creating bb main() file failed: %v", err) 191 } 192 193 // Copy local dependency packages into temporary module directories. 194 deps, err := NewPackages(env, depPkgs...) 195 if err != nil { 196 return err 197 } 198 for _, p := range deps { 199 if err := p.Write(filepath.Join(pkgDir, p.Pkg.PkgPath)); err != nil { 200 return err 201 } 202 } 203 204 // Add local replace rules for all modules we're compiling. 205 // 206 // This is the only way to locally reference another modules' 207 // repository. Otherwise, go'll try to go online to get the source. 208 // 209 // The module name is something that'll never be online, lest Go 210 // decides to go on the internet. 211 if len(modulePaths) == 0 { 212 env.GOPATH = tmpDir 213 // Compile bb. 214 return env.Build("bb", binaryPath) 215 } 216 217 content := `module bb.u-root.com` 218 for _, mpath := range modulePaths { 219 content += fmt.Sprintf("\nreplace %s => ./src/%s\n", mpath, mpath) 220 } 221 if err := ioutil.WriteFile(filepath.Join(tmpDir, "go.mod"), []byte(content), 0755); err != nil { 222 return err 223 } 224 225 // Compile bb. 226 return env.BuildDir(bbDir, binaryPath) 227 } 228 229 func writeDeps(env golang.Environ, pkgDir string, p *packages.Package) ([]string, string, error) { 230 listp, err := env.FindOne(p.PkgPath) 231 if err != nil { 232 return nil, "", err 233 } 234 235 var deps []string 236 if listp.Module != nil { 237 if err := os.MkdirAll(filepath.Join(pkgDir, listp.Module.Path), 0755); err != nil { 238 return nil, "", err 239 } 240 241 // Use the module file for all outside dependencies. 242 if err := cp.Copy(listp.Module.GoMod, filepath.Join(pkgDir, listp.Module.Path, "go.mod")); err != nil { 243 return nil, "", err 244 } 245 246 // Collect all "local" dependency packages, to be copied into 247 // the temporary directory structure later. 248 for _, dep := range listp.Deps { 249 // Is this a dependency within the module? 250 if strings.HasPrefix(dep, listp.Module.Path) { 251 deps = append(deps, dep) 252 } 253 } 254 return deps, listp.Module.Path, nil 255 } 256 257 // If modules are not enabled, we need a copy of *ALL* 258 // non-standard-library dependencies in the temporary directory. 259 for _, dep := range listp.Deps { 260 // First component of package path contains a "."? 261 // 262 // Poor man's standard library test. 263 firstComp := strings.SplitN(dep, "/", 2) 264 if strings.Contains(firstComp[0], ".") { 265 deps = append(deps, dep) 266 } 267 } 268 return deps, "", nil 269 } 270 271 // CreateBBMainSource creates a bb Go command that imports all given pkgs. 272 // 273 // p must be the bb template. 274 // 275 // - For each pkg in pkgs, add 276 // import _ "pkg" 277 // to astp's first file. 278 // - Write source file out to destDir. 279 func CreateBBMainSource(p *packages.Package, pkgs []string, destDir string) error { 280 if len(p.Syntax) != 1 { 281 return fmt.Errorf("bb cmd template is supposed to only have one file") 282 } 283 for _, pkg := range pkgs { 284 // Add side-effect import to bb binary so init registers itself. 285 // 286 // import _ "pkg" 287 astutil.AddNamedImport(p.Fset, p.Syntax[0], "_", pkg) 288 } 289 290 return writeFiles(destDir, p.Fset, p.Syntax) 291 } 292 293 // Package is a Go package. 294 type Package struct { 295 // Name is the executable command name. 296 // 297 // In the standard Go tool chain, this is usually the base name of the 298 // directory containing its source files. 299 Name string 300 301 // Pkg is the actual data about the package. 302 Pkg *packages.Package 303 304 // initCount keeps track of what the next init's index should be. 305 initCount uint 306 307 // init is the cmd.Init function that calls all other InitXs in the 308 // right order. 309 init *ast.FuncDecl 310 311 // initAssigns is a map of assignment expression -> InitN function call 312 // statement. 313 // 314 // That InitN should contain the assignment statement for the 315 // appropriate assignment expression. 316 // 317 // types.Info.InitOrder keeps track of Initializations by Lhs name and 318 // Rhs ast.Expr. We reparent the Rhs in assignment statements in InitN 319 // functions, so we use the Rhs as an easy key here. 320 // types.Info.InitOrder + initAssigns can then easily be used to derive 321 // the order of Stmts in the "real" init. 322 // 323 // The key Expr must also be the AssignStmt.Rhs[0]. 324 initAssigns map[ast.Expr]ast.Stmt 325 } 326 327 // NewPackages collects package metadata about all named packages. 328 func NewPackages(env golang.Environ, names ...string) ([]*Package, error) { 329 ps, err := loadPkgs(env, names...) 330 if err != nil { 331 return nil, fmt.Errorf("failed to load package %v: %v", names, err) 332 } 333 var ips []*Package 334 for _, p := range ps { 335 ips = append(ips, NewPackage(path.Base(p.PkgPath), p)) 336 } 337 return ips, nil 338 } 339 340 func loadPkgs(env golang.Environ, patterns ...string) ([]*packages.Package, error) { 341 cfg := &packages.Config{ 342 Mode: packages.NeedName | packages.NeedImports | packages.NeedFiles | packages.NeedDeps | packages.NeedTypes | packages.NeedSyntax | packages.NeedTypesInfo | packages.NeedCompiledGoFiles, 343 Env: append(os.Environ(), env.Env()...), 344 } 345 return packages.Load(cfg, patterns...) 346 } 347 348 // NewPackage creates a new Package based on an existing packages.Package. 349 func NewPackage(name string, p *packages.Package) *Package { 350 pp := &Package{ 351 // Name is the executable name. 352 Name: path.Base(name), 353 Pkg: p, 354 initAssigns: make(map[ast.Expr]ast.Stmt), 355 } 356 357 // This Init will hold calls to all other InitXs. 358 pp.init = &ast.FuncDecl{ 359 Name: ast.NewIdent("Init"), 360 Type: &ast.FuncType{ 361 Params: &ast.FieldList{}, 362 Results: nil, 363 }, 364 Body: &ast.BlockStmt{}, 365 } 366 return pp 367 } 368 369 func (p *Package) nextInit(addToCallList bool) *ast.Ident { 370 i := ast.NewIdent(fmt.Sprintf("Init%d", p.initCount)) 371 if addToCallList { 372 p.init.Body.List = append(p.init.Body.List, &ast.ExprStmt{X: &ast.CallExpr{Fun: i}}) 373 } 374 p.initCount++ 375 return i 376 } 377 378 // TODO: 379 // - write an init name generator, in case InitN is already taken. 380 func (p *Package) rewriteFile(f *ast.File) bool { 381 hasMain := false 382 383 // Change the package name declaration from main to the command's name. 384 f.Name.Name = p.Name 385 386 // Map of fully qualified package name -> imported alias in the file. 387 importAliases := make(map[string]string) 388 for _, impt := range f.Imports { 389 if impt.Name != nil { 390 importPath, err := strconv.Unquote(impt.Path.Value) 391 if err != nil { 392 panic(err) 393 } 394 importAliases[importPath] = impt.Name.Name 395 } 396 } 397 398 // When the types.TypeString function translates package names, it uses 399 // this function to map fully qualified package paths to a local alias, 400 // if it exists. 401 qualifier := func(pkg *types.Package) string { 402 name, ok := importAliases[pkg.Path()] 403 if ok { 404 return name 405 } 406 // When referring to self, don't use any package name. 407 if pkg == p.Pkg.Types { 408 return "" 409 } 410 return pkg.Name() 411 } 412 413 for _, decl := range f.Decls { 414 switch d := decl.(type) { 415 case *ast.GenDecl: 416 // We only care about vars. 417 if d.Tok != token.VAR { 418 break 419 } 420 for _, spec := range d.Specs { 421 s := spec.(*ast.ValueSpec) 422 if s.Values == nil { 423 continue 424 } 425 426 // For each assignment, create a new init 427 // function, and place it in the same file. 428 for i, name := range s.Names { 429 varInit := &ast.FuncDecl{ 430 Name: p.nextInit(false), 431 Type: &ast.FuncType{ 432 Params: &ast.FieldList{}, 433 Results: nil, 434 }, 435 Body: &ast.BlockStmt{ 436 List: []ast.Stmt{ 437 &ast.AssignStmt{ 438 Lhs: []ast.Expr{name}, 439 Tok: token.ASSIGN, 440 Rhs: []ast.Expr{s.Values[i]}, 441 }, 442 }, 443 }, 444 } 445 // Add a call to the new init func to 446 // this map, so they can be added to 447 // Init0() in the correct init order 448 // later. 449 p.initAssigns[s.Values[i]] = &ast.ExprStmt{X: &ast.CallExpr{Fun: varInit.Name}} 450 f.Decls = append(f.Decls, varInit) 451 } 452 453 // Add the type of the expression to the global 454 // declaration instead. 455 if s.Type == nil { 456 typ := p.Pkg.TypesInfo.Types[s.Values[0]] 457 s.Type = ast.NewIdent(types.TypeString(typ.Type, qualifier)) 458 } 459 s.Values = nil 460 } 461 462 case *ast.FuncDecl: 463 if d.Recv == nil && d.Name.Name == "main" { 464 d.Name.Name = "Main" 465 hasMain = true 466 } 467 if d.Recv == nil && d.Name.Name == "init" { 468 d.Name = p.nextInit(true) 469 } 470 } 471 } 472 473 // Now we change any import names attached to package declarations. We 474 // just upcase it for now; it makes it easy to look in bbsh for things 475 // we changed, e.g. grep -r bbsh Import is useful. 476 for _, cg := range f.Comments { 477 for _, c := range cg.List { 478 if strings.HasPrefix(c.Text, "// import") { 479 c.Text = "// Import" + c.Text[9:] 480 } 481 } 482 } 483 return hasMain 484 } 485 486 // Write writes p into destDir. 487 func (p *Package) Write(destDir string) error { 488 if err := os.MkdirAll(destDir, 0755); err != nil { 489 return err 490 } 491 492 for _, fp := range p.Pkg.OtherFiles { 493 if err := cp.Copy(fp, filepath.Join(destDir, filepath.Base(fp))); err != nil { 494 return err 495 } 496 } 497 498 return writeFiles(destDir, p.Pkg.Fset, p.Pkg.Syntax) 499 } 500 501 func writeFiles(destDir string, fset *token.FileSet, files []*ast.File) error { 502 // Write all files out. 503 for _, file := range files { 504 name := fset.File(file.Package).Name() 505 506 path := filepath.Join(destDir, filepath.Base(name)) 507 if err := writeFile(path, fset, file); err != nil { 508 return err 509 } 510 } 511 return nil 512 } 513 514 // Rewrite rewrites p into destDir as a bb package using bbImportPath for the 515 // bb implementation. 516 func (p *Package) Rewrite(destDir, bbImportPath string) error { 517 // This init holds all variable initializations. 518 // 519 // func Init0() {} 520 varInit := &ast.FuncDecl{ 521 Name: p.nextInit(true), 522 Type: &ast.FuncType{ 523 Params: &ast.FieldList{}, 524 Results: nil, 525 }, 526 Body: &ast.BlockStmt{}, 527 } 528 529 var mainFile *ast.File 530 for _, sourceFile := range p.Pkg.Syntax { 531 if hasMainFile := p.rewriteFile(sourceFile); hasMainFile { 532 mainFile = sourceFile 533 } 534 } 535 if mainFile == nil { 536 return fmt.Errorf("no main function found in package %q", p.Pkg.PkgPath) 537 } 538 539 // Add variable initializations to Init0 in the right order. 540 for _, initStmt := range p.Pkg.TypesInfo.InitOrder { 541 a, ok := p.initAssigns[initStmt.Rhs] 542 if !ok { 543 return fmt.Errorf("couldn't find init assignment %s", initStmt) 544 } 545 varInit.Body.List = append(varInit.Body.List, a) 546 } 547 548 // import bb "bbImportPath" 549 astutil.AddNamedImport(p.Pkg.Fset, mainFile, "bb", bbImportPath) 550 551 // func init() { 552 // bb.Register(p.name, Init, Main) 553 // } 554 bbRegisterInit := &ast.FuncDecl{ 555 Name: ast.NewIdent("init"), 556 Type: &ast.FuncType{}, 557 Body: &ast.BlockStmt{ 558 List: []ast.Stmt{ 559 &ast.ExprStmt{X: &ast.CallExpr{ 560 Fun: ast.NewIdent("bb.Register"), 561 Args: []ast.Expr{ 562 // name= 563 &ast.BasicLit{ 564 Kind: token.STRING, 565 Value: strconv.Quote(p.Name), 566 }, 567 // init= 568 ast.NewIdent("Init"), 569 // main= 570 ast.NewIdent("Main"), 571 }, 572 }}, 573 }, 574 }, 575 } 576 577 // We could add these statements to any of the package files. We choose 578 // the one that contains Main() to guarantee reproducibility of the 579 // same bbsh binary. 580 mainFile.Decls = append(mainFile.Decls, varInit, p.init, bbRegisterInit) 581 582 return p.Write(destDir) 583 } 584 585 func writeFile(path string, fset *token.FileSet, f *ast.File) error { 586 var buf bytes.Buffer 587 if err := format.Node(&buf, fset, f); err != nil { 588 return fmt.Errorf("error formatting Go file %q: %v", path, err) 589 } 590 return writeGoFile(path, buf.Bytes()) 591 } 592 593 func writeGoFile(path string, code []byte) error { 594 // Format the file. Do not fix up imports, as we only moved code around 595 // within files. 596 opts := imports.Options{ 597 Comments: true, 598 TabIndent: true, 599 TabWidth: 8, 600 FormatOnly: true, 601 } 602 code, err := imports.Process("commandline", code, &opts) 603 if err != nil { 604 return fmt.Errorf("bad parse while processing imports %q: %v", path, err) 605 } 606 607 if err := ioutil.WriteFile(path, code, 0644); err != nil { 608 return fmt.Errorf("error writing Go file to %q: %v", path, err) 609 } 610 return nil 611 }