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