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