github.com/please-build/go-rules/tools/please_go@v0.0.0-20240319165128-ea27d6f5caba/generate/generate.go (about) 1 package generate 2 3 import ( 4 "bufio" 5 "fmt" 6 "go/build" 7 "io/fs" 8 "log" 9 "os" 10 "path" 11 "path/filepath" 12 "strings" 13 14 bazelbuild "github.com/bazelbuild/buildtools/build" 15 bazeledit "github.com/bazelbuild/buildtools/edit" 16 17 "github.com/please-build/go-rules/tools/please_go/generate/gomoddeps" 18 ) 19 20 type Generate struct { 21 moduleName string 22 moduleArg string 23 srcRoot string 24 subrepo string 25 buildContext build.Context 26 hostModFile string 27 buildFileNames []string 28 moduleDeps []string 29 replace map[string]string 30 knownImportTargets map[string]string // cache these so we don't end up looping over all the modules for every import 31 thirdPartyFolder string 32 install []string 33 } 34 35 func New(srcRoot, thirdPartyFolder, hostModFile, module, version, subrepo string, buildFileNames, moduleDeps, install []string, buildTags []string) *Generate { 36 moduleArg := module 37 if version != "" { 38 moduleArg += "@" + version 39 } 40 41 ctxt := build.Default 42 ctxt.BuildTags = buildTags 43 44 return &Generate{ 45 srcRoot: srcRoot, 46 buildContext: ctxt, 47 buildFileNames: buildFileNames, 48 moduleDeps: moduleDeps, 49 hostModFile: hostModFile, 50 knownImportTargets: map[string]string{}, 51 thirdPartyFolder: thirdPartyFolder, 52 install: install, 53 moduleName: module, 54 moduleArg: moduleArg, 55 subrepo: subrepo, 56 } 57 } 58 59 // Generate generates a new Please project at the src root. It will walk through the directory tree generating new BUILD 60 // files. This is primarily intended to generate a please subrepo for third party code. 61 func (g *Generate) Generate() error { 62 deps, replacements, err := gomoddeps.GetCombinedDepsAndReplacements(g.hostModFile, path.Join(g.srcRoot, "go.mod")) 63 if err != nil { 64 return err 65 } 66 // It's important to not override g.moduleDeps as it can already contains dependencies configured 67 // when `Generate` was constructed. 68 g.moduleDeps = append(g.moduleDeps, deps...) 69 g.moduleDeps = append(g.moduleDeps, g.moduleName) 70 g.replace = replacements 71 72 if err := g.writeConfig(); err != nil { 73 return fmt.Errorf("failed to write config: %w", err) 74 } 75 if err := g.parseImportConfigs(); err != nil { 76 return fmt.Errorf("failed to parse import configs: %w", err) 77 } 78 79 if err := g.generateAll(g.srcRoot); err != nil { 80 return fmt.Errorf("failed to generate BUILD files: %w", err) 81 } 82 return g.writeInstallFilegroup() 83 } 84 85 // parseImportConfigs walks through the build dir looking for .importconfig files, parsing the # please:target //foo:bar 86 // comments to generate the known imports. These are the deps that are passed to the go_repo e.g. for legacy go_module 87 // rules. 88 func (g *Generate) parseImportConfigs() error { 89 return filepath.WalkDir(".", func(path string, d fs.DirEntry, err error) error { 90 if filepath.Ext(path) == ".importconfig" { 91 target, pkgs, err := parseImportConfig(path) 92 if err != nil { 93 return err 94 } 95 if target == "" { 96 return nil 97 } 98 for _, p := range pkgs { 99 g.knownImportTargets[p] = target 100 } 101 } 102 return nil 103 }) 104 } 105 106 func parseImportConfig(path string) (string, []string, error) { 107 f, err := os.Open(path) 108 if err != nil { 109 return "", nil, err 110 } 111 defer f.Close() 112 113 target := "" 114 var imports []string 115 116 importCfg := bufio.NewScanner(f) 117 for importCfg.Scan() { 118 line := importCfg.Text() 119 if strings.HasPrefix(line, "#") { 120 if strings.HasPrefix(line, "# please:target ") { 121 target = strings.TrimSpace(strings.TrimPrefix(line, "# please:target ")) 122 if !strings.HasPrefix(target, "///") { 123 target = "@" + target 124 } 125 } 126 continue 127 } 128 parts := strings.Split(strings.TrimPrefix(line, "packagefile "), "=") 129 imports = append(imports, parts[0]) 130 } 131 return target, imports, nil 132 } 133 134 func (g *Generate) installTargets() ([]string, error) { 135 var targets []string 136 137 for _, i := range g.install { 138 dir := filepath.Join(g.srcRoot, i) 139 if strings.HasSuffix(dir, "/...") { 140 ts, err := g.targetsInDir(strings.TrimSuffix(dir, "/...")) 141 if err != nil { 142 return nil, err 143 } 144 targets = append(targets, ts...) 145 } else { 146 t, err := g.libTargetForBuildPackage(i) 147 if err != nil { 148 return nil, err 149 } 150 if t == "" { 151 return nil, fmt.Errorf("couldn't find install package %v", i) 152 } 153 targets = append(targets, t) 154 } 155 } 156 return targets, nil 157 } 158 159 func (g *Generate) targetsInDir(dir string) ([]string, error) { 160 var ret []string 161 err := filepath.WalkDir(dir, func(path string, info os.DirEntry, err error) error { 162 if g.isBuildFile(path) { 163 t, err := g.libTargetForBuildFile(trimPath(path, g.srcRoot)) 164 if err != nil { 165 return err 166 } 167 if t != "" { 168 ret = append(ret, t) 169 } 170 } 171 return nil 172 }) 173 return ret, err 174 } 175 176 func (g *Generate) isBuildFile(file string) bool { 177 base := filepath.Base(file) 178 for _, file := range g.buildFileNames { 179 if base == file { 180 return true 181 } 182 } 183 return false 184 } 185 186 func (g *Generate) writeInstallFilegroup() error { 187 buildFile, err := parseOrCreateBuildFile(g.srcRoot, g.buildFileNames) 188 if err != nil { 189 return err 190 } 191 192 rule := NewRule("filegroup", "installs") 193 installTargets, err := g.installTargets() 194 if err != nil { 195 return fmt.Errorf("failed to generate install targets: %v", err) 196 } 197 rule.SetAttr("exported_deps", NewStringList(installTargets)) 198 rule.SetAttr("visibility", NewStringList([]string{"PUBLIC"})) 199 200 buildFile.Stmt = append(buildFile.Stmt, rule.Call) 201 202 return saveBuildFile(buildFile) 203 } 204 205 func (g *Generate) writeConfig() error { 206 file, err := os.Create(filepath.Join(g.srcRoot, ".plzconfig")) 207 if err != nil { 208 return err 209 } 210 defer file.Close() 211 212 fmt.Fprintln(file, "[Plugin \"go\"]") 213 fmt.Fprintln(file, "Target=@//plugins:go") 214 fmt.Fprintf(file, "ImportPath=%s\n", g.moduleName) 215 for _, t := range g.buildContext.BuildTags { 216 fmt.Fprintf(file, "BuildTags=%s\n", t) 217 } 218 return nil 219 } 220 221 func (g *Generate) generateAll(dir string) error { 222 return filepath.WalkDir(dir, func(path string, info os.DirEntry, err error) error { 223 if err != nil { 224 return err 225 } 226 if info.IsDir() { 227 if info.Name() == "testdata" { 228 return filepath.SkipDir 229 } 230 if path != dir && strings.HasPrefix(info.Name(), "_") { 231 return filepath.SkipDir 232 } 233 234 if err := g.generate(trimPath(path, g.srcRoot)); err != nil { 235 switch err.(type) { 236 case *build.NoGoError: 237 // We might walk into a dir that has no .go files for the current arch. This shouldn't 238 // be an error so we just eat this 239 return nil 240 default: 241 return err 242 } 243 } 244 } 245 return nil 246 }) 247 } 248 249 func (g *Generate) pkgDir(target string) string { 250 p := strings.TrimPrefix(target, g.moduleName) 251 return filepath.Join(g.srcRoot, p) 252 } 253 254 func (g *Generate) importDir(target string) (*build.Package, error) { 255 dir := filepath.Join(os.Getenv("TMP_DIR"), g.pkgDir(target)) 256 pkg, err := g.buildContext.ImportDir(dir, 0) 257 if err != nil { 258 return nil, err 259 } 260 // We also need to discover & attach any .a files in the directory; some libraries use these 261 entries, err := os.ReadDir(dir) 262 if err != nil { 263 return nil, err 264 } 265 pkg.IgnoredOtherFiles = nil 266 for _, entry := range entries { 267 if name := entry.Name(); strings.HasSuffix(name, ".a") { 268 pkg.IgnoredOtherFiles = append(pkg.IgnoredOtherFiles, name) 269 } 270 } 271 return pkg, nil 272 } 273 274 func (g *Generate) generate(dir string) error { 275 pkg, err := g.importDir(dir) 276 if err != nil { 277 return err 278 } 279 280 // filter out pkg.GoFiles based on build tags 281 var goFiles []string 282 for _, f := range pkg.GoFiles { 283 match, err := g.buildContext.MatchFile(pkg.Dir, f) 284 if err != nil { 285 return err 286 } 287 if match { 288 goFiles = append(goFiles, f) 289 } 290 } 291 292 pkg.GoFiles = goFiles 293 lib := g.ruleForPackage(pkg, dir) 294 if lib == nil { 295 return nil 296 } 297 298 return g.createBuildFile(dir, lib, pkg.IgnoredOtherFiles) 299 } 300 301 func (g *Generate) matchesInstall(dir string) bool { 302 for _, i := range g.install { 303 i := filepath.Join(g.srcRoot, i) 304 pkgDir := g.pkgDir(dir) 305 306 if strings.HasSuffix(i, "/...") { 307 i = strings.TrimSuffix(i, "/...") 308 return strings.HasPrefix(pkgDir, i) 309 } 310 return i == pkgDir 311 } 312 return false 313 } 314 315 func (g *Generate) rule(rule *Rule) *bazelbuild.Rule { 316 r := NewRule(rule.kind, rule.name) 317 populateRule(r, rule) 318 r.SetAttr("visibility", NewStringList([]string{"PUBLIC"})) 319 if rule.kind == "go_library" { 320 r.SetAttr("cover", &bazelbuild.Ident{Name: "False"}) 321 } 322 323 return r 324 } 325 326 // parseOrCreateBuildFile loops through the available build file names to create a new build file or open the existing 327 // one. 328 func parseOrCreateBuildFile(path string, fileNames []string) (*bazelbuild.File, error) { 329 for _, name := range fileNames { 330 filePath := filepath.Join(path, name) 331 if f, err := os.Lstat(filePath); os.IsNotExist(err) { 332 return bazelbuild.ParseBuild(filePath, nil) 333 } else if !f.IsDir() { 334 bs, err := os.ReadFile(filePath) 335 if err != nil { 336 return nil, err 337 } 338 return bazelbuild.ParseBuild(filePath, bs) 339 } 340 } 341 return nil, fmt.Errorf("folders exist with the build file names in directory %v %v", path, fileNames) 342 } 343 344 func saveBuildFile(buildFile *bazelbuild.File) error { 345 f, err := os.Create(buildFile.Path) 346 if err != nil { 347 return err 348 } 349 defer f.Close() 350 351 _, err = f.Write(bazelbuild.Format(buildFile)) 352 return err 353 } 354 355 func (g *Generate) createBuildFile(pkg string, rule *Rule, aFiles []string) error { 356 buildFile, err := parseOrCreateBuildFile(g.pkgDir(pkg), g.buildFileNames) 357 if err != nil { 358 return err 359 } 360 361 var subincludes []bazelbuild.Expr 362 if strings.HasPrefix(rule.kind, "cgo") { 363 subincludes = []bazelbuild.Expr{NewStringExpr("///go//build_defs:cgo")} 364 } else { 365 subincludes = []bazelbuild.Expr{NewStringExpr("///go//build_defs:go")} 366 } 367 368 buildFile.Stmt = []bazelbuild.Expr{ 369 &bazelbuild.CallExpr{ 370 X: &bazelbuild.Ident{Name: "subinclude"}, 371 List: subincludes, 372 }, 373 } 374 375 buildFile.Stmt = append(buildFile.Stmt, g.rule(rule).Call) 376 377 if len(aFiles) != 0 { 378 filegroup := NewRule("filegroup", "a_files") 379 filegroup.SetAttr("srcs", NewStringList(aFiles)) 380 buildFile.Stmt = append(buildFile.Stmt, filegroup.Call) 381 } 382 383 return saveBuildFile(buildFile) 384 } 385 386 func NewRule(kind, name string) *bazelbuild.Rule { 387 rule, _ := bazeledit.ExprToRule(&bazelbuild.CallExpr{ 388 X: &bazelbuild.Ident{Name: kind}, 389 List: []bazelbuild.Expr{}, 390 }, kind) 391 392 rule.SetAttr("name", NewStringExpr(name)) 393 394 return rule 395 } 396 397 func NewStringExpr(s string) *bazelbuild.StringExpr { 398 return &bazelbuild.StringExpr{Value: s} 399 } 400 401 func NewStringList(ss []string) *bazelbuild.ListExpr { 402 l := new(bazelbuild.ListExpr) 403 for _, s := range ss { 404 l.List = append(l.List, NewStringExpr(s)) 405 } 406 return l 407 } 408 409 func packageKind(pkg *build.Package) string { 410 cgo := len(pkg.CgoFiles) > 0 411 if pkg.IsCommand() && cgo { 412 return "cgo_binary" 413 } 414 if pkg.IsCommand() { 415 return "go_binary" 416 } 417 if cgo { 418 return "cgo_library" 419 } 420 return "go_library" 421 } 422 423 func (g *Generate) depTargets(imports []string) []string { 424 deps := make([]string, 0) 425 for _, path := range imports { 426 target := g.depTarget(path) 427 if target == "" { 428 continue 429 } 430 deps = append(deps, target) 431 } 432 return deps 433 } 434 435 func (g *Generate) ruleForPackage(pkg *build.Package, dir string) *Rule { 436 if len(pkg.GoFiles) == 0 && len(pkg.CgoFiles) == 0 { 437 return nil 438 } 439 440 name := nameForLibInPkg(g.moduleName, trimPath(dir, g.srcRoot)) 441 deps := g.depTargets(pkg.Imports) 442 if len(pkg.IgnoredOtherFiles) != 0 { 443 deps = append(deps, ":a_files") 444 } 445 446 return &Rule{ 447 name: name, 448 kind: packageKind(pkg), 449 srcs: pkg.GoFiles, 450 module: g.moduleArg, 451 subrepo: g.subrepo, 452 cgoSrcs: pkg.CgoFiles, 453 cSrcs: pkg.CFiles, 454 compilerFlags: pkg.CgoCFLAGS, 455 linkerFlags: orderLinkerFlags(pkg.CgoLDFLAGS), 456 pkgConfigs: pkg.CgoPkgConfig, 457 asmFiles: pkg.SFiles, 458 hdrs: pkg.HFiles, 459 deps: deps, 460 embedPatterns: pkg.EmbedPatterns, 461 isCMD: pkg.IsCommand(), 462 } 463 } 464 465 // orderLinkerFlags collapses linker flags into one to enforce a consistent ordering 466 func orderLinkerFlags(in []string) []string { 467 if len(in) > 0 { 468 return []string{strings.Join(in, " ")} 469 } 470 return nil 471 } 472 473 func (g *Generate) depTarget(importPath string) string { 474 if target, ok := g.knownImportTargets[importPath]; ok { 475 return target 476 } 477 478 if replacement, ok := g.replace[importPath]; ok && replacement != importPath { 479 target := g.depTarget(replacement) 480 g.knownImportTargets[importPath] = target 481 return target 482 } 483 484 module := "" 485 for _, mod := range append(g.moduleDeps, g.moduleName) { 486 if strings.HasPrefix(importPath, mod) { 487 if len(module) < len(mod) { 488 module = mod 489 } 490 } 491 } 492 493 if module == "" { 494 // If we can't find this import, we can return nothing and the build rule will fail at build time reporting a 495 // sensible error. It may also be an import from the go SDK which is fine. 496 return "" 497 } 498 499 subrepoName := g.subrepoName(module) 500 packageName := trimPath(importPath, module) 501 name := nameForLibInPkg(module, packageName) 502 503 target := buildTarget(name, packageName, subrepoName) 504 g.knownImportTargets[importPath] = target 505 return target 506 } 507 508 // nameForLibInPkg returns the lib target name for a target in pkg. The pkg should be the relative pkg part excluding 509 // the module, e.g. pkg would be asset, and module would be github.com/stretchr/testify for 510 // github.com/stretchr/testify/assert, 511 func nameForLibInPkg(module, pkg string) string { 512 name := filepath.Base(pkg) 513 if pkg == "" || pkg == "." { 514 name = filepath.Base(module) 515 } 516 517 if name == "all" { 518 return "lib" 519 } 520 521 return name 522 } 523 524 // trimPath is like strings.TrimPrefix but is path aware. It removes base from target if target starts with base, 525 // otherwise returns target unmodified. 526 func trimPath(target, base string) string { 527 baseParts := strings.Split(filepath.Clean(base), "/") 528 targetParts := strings.Split(filepath.Clean(target), "/") 529 530 if len(targetParts) < len(baseParts) { 531 return target 532 } 533 534 for i := range baseParts { 535 if baseParts[i] != targetParts[i] { 536 return target 537 } 538 } 539 return strings.Join(targetParts[len(baseParts):], "/") 540 } 541 542 // libTargetForBuildFile finds the go_library or cgo_library target in the package 543 func (g *Generate) libTargetForBuildFile(path string) (string, error) { 544 bs, err := os.ReadFile(filepath.Join(g.srcRoot, path)) 545 if err != nil { 546 return "", err 547 } 548 file, err := bazelbuild.ParseBuild(path, bs) 549 if err != nil { 550 return "", err 551 } 552 553 libs := append(file.Rules("go_library"), file.Rules("cgo_library")...) 554 if len(libs) >= 1 { 555 if len(libs) != 1 { 556 log.Fatalf("more than one go library in installed package %v", path) 557 } 558 return buildTarget(libs[0].Name(), filepath.Dir(path), ""), nil 559 } 560 return "", nil 561 } 562 563 func (g *Generate) subrepoName(module string) string { 564 if g.moduleName == module { 565 return "" 566 } 567 return filepath.Join(g.thirdPartyFolder, strings.ReplaceAll(module, "/", "_")) 568 } 569 570 func (g *Generate) libTargetForBuildPackage(i string) (string, error) { 571 entries, err := os.ReadDir(filepath.Join(g.srcRoot, i)) 572 if err != nil { 573 return "", err 574 } 575 576 for _, e := range entries { 577 if g.isBuildFile(e.Name()) { 578 t, err := g.libTargetForBuildFile(filepath.Join(i, e.Name())) 579 if err != nil { 580 return "", err 581 } 582 return t, nil 583 } 584 } 585 return "", nil 586 } 587 588 func buildTarget(name, pkgDir, subrepo string) string { 589 bs := new(strings.Builder) 590 if subrepo != "" { 591 bs.WriteString("///") 592 bs.WriteString(subrepo) 593 } 594 595 // Bit of a special case here where we assume all build targets are absolute which is fine for our use case. 596 bs.WriteString("//") 597 598 if pkgDir == "." { 599 pkgDir = "" 600 } 601 602 if pkgDir != "" { 603 bs.WriteString(pkgDir) 604 if filepath.Base(pkgDir) != name { 605 bs.WriteString(":") 606 bs.WriteString(name) 607 } 608 } else { 609 bs.WriteString(":") 610 bs.WriteString(name) 611 } 612 return bs.String() 613 }