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 }