github.com/bazelbuild/bazel-gazelle@v0.36.1-0.20240520142334-61b277ba6fed/language/go/package.go (about) 1 /* Copyright 2017 The Bazel Authors. All rights reserved. 2 3 Licensed under the Apache License, Version 2.0 (the "License"); 4 you may not use this file except in compliance with the License. 5 You may obtain a copy of the License at 6 7 http://www.apache.org/licenses/LICENSE-2.0 8 9 Unless required by applicable law or agreed to in writing, software 10 distributed under the License is distributed on an "AS IS" BASIS, 11 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 See the License for the specific language governing permissions and 13 limitations under the License. 14 */ 15 16 package golang 17 18 import ( 19 "fmt" 20 "log" 21 "path" 22 "regexp" 23 "sort" 24 "strings" 25 26 "github.com/bazelbuild/bazel-gazelle/config" 27 "github.com/bazelbuild/bazel-gazelle/language/proto" 28 "github.com/bazelbuild/bazel-gazelle/pathtools" 29 "github.com/bazelbuild/bazel-gazelle/rule" 30 ) 31 32 // goPackage contains metadata for a set of .go and .proto files that can be 33 // used to generate Go rules. 34 type goPackage struct { 35 name, dir, rel string 36 library, binary, test goTarget 37 tests []goTarget 38 proto protoTarget 39 hasTestdata bool 40 hasMainFunction bool 41 importPath string 42 } 43 44 // goTarget contains information used to generate an individual Go rule 45 // (library, binary, or test). 46 type goTarget struct { 47 sources, embedSrcs, imports, cppopts, copts, cxxopts, clinkopts platformStringsBuilder 48 cgo, hasInternalTest bool 49 } 50 51 // protoTarget contains information used to generate a go_proto_library rule. 52 type protoTarget struct { 53 name string 54 sources platformStringsBuilder 55 imports platformStringsBuilder 56 hasServices bool 57 } 58 59 // platformStringsBuilder is used to construct rule.PlatformStrings. Bazel 60 // has some requirements for deps list (a dependency cannot appear in more 61 // than one select expression; dependencies cannot be duplicated), so we need 62 // to build these carefully. 63 type platformStringsBuilder struct { 64 strs map[string]platformStringInfo 65 } 66 67 // platformStringInfo contains information about a single string (source, 68 // import, or option). 69 type platformStringInfo struct { 70 set platformStringSet 71 osConstraints map[string]bool 72 archConstraints map[string]bool 73 platformConstraints map[rule.PlatformConstraint]bool 74 } 75 76 type platformStringSet int 77 78 const ( 79 genericSet platformStringSet = iota 80 osSet 81 archSet 82 platformSet 83 ) 84 85 // Matches a package version, eg. the end segment of 'example.com/foo/v1' 86 var pkgVersionRe = regexp.MustCompile("^v[0-9]+$") 87 88 // addFile adds the file described by "info" to a target in the package "p" if 89 // the file is buildable. 90 // 91 // "cgo" tells whether any ".go" file in the package contains cgo code. This 92 // affects whether C files are added to targets. 93 // 94 // An error is returned if a file is buildable but invalid (for example, a 95 // test .go file containing cgo code). Files that are not buildable will not 96 // be added to any target (for example, .txt files). 97 func (pkg *goPackage) addFile(c *config.Config, er *embedResolver, info fileInfo, cgo bool) error { 98 switch { 99 case info.ext == unknownExt || !cgo && (info.ext == cExt || info.ext == csExt): 100 return nil 101 case info.ext == protoExt: 102 if pcMode := getProtoMode(c); pcMode == proto.LegacyMode { 103 // Only add files in legacy mode. This is used to generate a filegroup 104 // that contains all protos. In order modes, we get the .proto files 105 // from information emitted by the proto language extension. 106 pkg.proto.addFile(info) 107 } 108 case info.isTest: 109 if info.isCgo { 110 return fmt.Errorf("%s: use of cgo in test not supported", info.path) 111 } 112 if getGoConfig(c).testMode == fileTestMode || len(pkg.tests) == 0 { 113 pkg.tests = append(pkg.tests, goTarget{}) 114 } 115 // Add the the file to the most recently added test target (in fileTestMode) 116 // or the only test target (in defaultMode). 117 // In both cases, this will be the last element in the slice. 118 test := &pkg.tests[len(pkg.tests)-1] 119 test.addFile(c, er, info) 120 if !info.isExternalTest { 121 test.hasInternalTest = true 122 } 123 default: 124 pkg.hasMainFunction = pkg.hasMainFunction || info.hasMainFunction 125 pkg.library.addFile(c, er, info) 126 } 127 128 return nil 129 } 130 131 // isCommand returns true if the package name is "main". 132 func (pkg *goPackage) isCommand() bool { 133 return pkg.name == "main" && pkg.hasMainFunction 134 } 135 136 // isBuildable returns true if anything in the package is buildable. 137 // This is true if the package has Go code that satisfies build constraints 138 // on any platform or has proto files not in legacy mode. 139 func (pkg *goPackage) isBuildable(c *config.Config) bool { 140 return pkg.firstGoFile() != "" || !pkg.proto.sources.isEmpty() 141 } 142 143 // firstGoFile returns the name of a .go file if the package contains at least 144 // one .go file, or "" otherwise. 145 func (pkg *goPackage) firstGoFile() string { 146 goSrcs := []platformStringsBuilder{ 147 pkg.library.sources, 148 pkg.binary.sources, 149 } 150 for _, test := range pkg.tests { 151 goSrcs = append(goSrcs, test.sources) 152 } 153 154 for _, sb := range goSrcs { 155 if sb.strs != nil { 156 for s := range sb.strs { 157 if strings.HasSuffix(s, ".go") { 158 return s 159 } 160 } 161 } 162 } 163 return "" 164 } 165 166 func (pkg *goPackage) haveCgo() bool { 167 if pkg.library.cgo || pkg.binary.cgo { 168 return true 169 } 170 for _, t := range pkg.tests { 171 if t.cgo { 172 return true 173 } 174 } 175 return false 176 } 177 178 func (pkg *goPackage) inferImportPath(c *config.Config) error { 179 if pkg.importPath != "" { 180 log.Panic("importPath already set") 181 } 182 gc := getGoConfig(c) 183 if !gc.prefixSet { 184 return fmt.Errorf("%s: go prefix is not set, so importpath can't be determined for rules. Set a prefix with a '# gazelle:prefix' comment or with -go_prefix on the command line", pkg.dir) 185 } 186 pkg.importPath = InferImportPath(c, pkg.rel) 187 return nil 188 } 189 190 // libNameFromImportPath returns a a suitable go_library name based on the import path. 191 // Major version suffixes (eg. "v1") are dropped. 192 func libNameFromImportPath(dir string) string { 193 i := strings.LastIndexAny(dir, "/\\") 194 if i < 0 { 195 return dir 196 } 197 name := dir[i+1:] 198 if pkgVersionRe.MatchString(name) { 199 dir := dir[:i] 200 i = strings.LastIndexAny(dir, "/\\") 201 if i >= 0 { 202 name = dir[i+1:] 203 } 204 } 205 return strings.ReplaceAll(name, ".", "_") 206 } 207 208 // libNameByConvention returns a suitable name for a go_library using the given 209 // naming convention, the import path, and the package name. 210 func libNameByConvention(nc namingConvention, imp string, pkgName string) string { 211 if nc == goDefaultLibraryNamingConvention { 212 return defaultLibName 213 } 214 name := libNameFromImportPath(imp) 215 isCommand := pkgName == "main" 216 if name == "" { 217 if isCommand { 218 name = "lib" 219 } else { 220 name = pkgName 221 } 222 } else if isCommand { 223 name += "_lib" 224 } 225 return name 226 } 227 228 // testNameByConvention returns a suitable name for a go_test using the given 229 // naming convention and the import path. 230 func testNameByConvention(nc namingConvention, imp string) string { 231 if nc == goDefaultLibraryNamingConvention { 232 return defaultTestName 233 } 234 libName := libNameFromImportPath(imp) 235 if libName == "" { 236 libName = "lib" 237 } 238 return libName + "_test" 239 } 240 241 // testNameFromSingleSource returns a suitable name for a go_test using the 242 // single Go source file name. 243 func testNameFromSingleSource(src string) string { 244 if i := strings.LastIndexByte(src, '.'); i >= 0 { 245 src = src[0:i] 246 } 247 libName := libNameFromImportPath(src) 248 if libName == "" { 249 return "" 250 } 251 if strings.HasSuffix(libName, "_test") { 252 return libName 253 } 254 return libName + "_test" 255 } 256 257 // binName returns a suitable name for a go_binary. 258 func binName(rel, prefix, repoRoot string) string { 259 return pathtools.RelBaseName(rel, prefix, repoRoot) 260 } 261 262 func InferImportPath(c *config.Config, rel string) string { 263 gc := getGoConfig(c) 264 if rel == gc.prefixRel { 265 return gc.prefix 266 } else { 267 fromPrefixRel := strings.TrimPrefix(rel, gc.prefixRel+"/") 268 return path.Join(gc.prefix, fromPrefixRel) 269 } 270 } 271 272 func goProtoPackageName(pkg proto.Package) string { 273 if value, ok := pkg.Options["go_package"]; ok { 274 if strings.LastIndexByte(value, '/') == -1 { 275 return value 276 } else { 277 if i := strings.LastIndexByte(value, ';'); i != -1 { 278 return value[i+1:] 279 } else { 280 return path.Base(value) 281 } 282 } 283 } 284 return strings.Replace(pkg.Name, ".", "_", -1) 285 } 286 287 func goProtoImportPath(c *config.Config, pkg proto.Package, rel string) string { 288 if value, ok := pkg.Options["go_package"]; ok { 289 if strings.LastIndexByte(value, '/') == -1 { 290 return InferImportPath(c, rel) 291 } else if i := strings.LastIndexByte(value, ';'); i != -1 { 292 return value[:i] 293 } else { 294 return value 295 } 296 } 297 return InferImportPath(c, rel) 298 } 299 300 func (t *goTarget) addFile(c *config.Config, er *embedResolver, info fileInfo) { 301 t.cgo = t.cgo || info.isCgo 302 add := getPlatformStringsAddFunction(c, info, nil) 303 add(&t.sources, info.name) 304 add(&t.imports, info.imports...) 305 if er != nil { 306 for _, embed := range info.embeds { 307 embedSrcs, err := er.resolve(embed) 308 if err != nil { 309 log.Print(err) 310 continue 311 } 312 add(&t.embedSrcs, embedSrcs...) 313 } 314 } 315 for _, cppopts := range info.cppopts { 316 optAdd := add 317 if !cppopts.empty() { 318 optAdd = getPlatformStringsAddFunction(c, info, cppopts) 319 } 320 optAdd(&t.cppopts, cppopts.opts) 321 } 322 for _, copts := range info.copts { 323 optAdd := add 324 if !copts.empty() { 325 optAdd = getPlatformStringsAddFunction(c, info, copts) 326 } 327 optAdd(&t.copts, copts.opts) 328 } 329 for _, cxxopts := range info.cxxopts { 330 optAdd := add 331 if !cxxopts.empty() { 332 optAdd = getPlatformStringsAddFunction(c, info, cxxopts) 333 } 334 optAdd(&t.cxxopts, cxxopts.opts) 335 } 336 for _, clinkopts := range info.clinkopts { 337 optAdd := add 338 if !clinkopts.empty() { 339 optAdd = getPlatformStringsAddFunction(c, info, clinkopts) 340 } 341 optAdd(&t.clinkopts, clinkopts.opts) 342 } 343 } 344 345 func protoTargetFromProtoPackage(name string, pkg proto.Package) protoTarget { 346 target := protoTarget{name: name} 347 for f := range pkg.Files { 348 target.sources.addGenericString(f) 349 } 350 for i := range pkg.Imports { 351 target.imports.addGenericString(i) 352 } 353 target.hasServices = pkg.HasServices 354 return target 355 } 356 357 func (t *protoTarget) addFile(info fileInfo) { 358 t.sources.addGenericString(info.name) 359 for _, imp := range info.imports { 360 t.imports.addGenericString(imp) 361 } 362 t.hasServices = t.hasServices || info.hasServices 363 } 364 365 // getPlatformStringsAddFunction returns a function used to add strings to 366 // a *platformStringsBuilder under the same set of constraints. This is a 367 // performance optimization to avoid evaluating constraints repeatedly. 368 func getPlatformStringsAddFunction(c *config.Config, info fileInfo, cgoTags *cgoTagsAndOpts) func(sb *platformStringsBuilder, ss ...string) { 369 isOSSpecific, isArchSpecific := isOSArchSpecific(info, cgoTags) 370 v := getGoConfig(c).rulesGoVersion 371 constraintPrefix := "@" + getGoConfig(c).rulesGoRepoName + "//go/platform:" 372 373 switch { 374 case !isOSSpecific && !isArchSpecific: 375 if checkConstraints(c, "", "", info.goos, info.goarch, info.tags, cgoTags) { 376 return func(sb *platformStringsBuilder, ss ...string) { 377 for _, s := range ss { 378 sb.addGenericString(s) 379 } 380 } 381 } 382 383 case isOSSpecific && !isArchSpecific: 384 var osMatch []string 385 for _, os := range rule.KnownOSs { 386 if rulesGoSupportsOS(v, os) && 387 checkConstraints(c, os, "", info.goos, info.goarch, info.tags, cgoTags) { 388 osMatch = append(osMatch, os) 389 } 390 } 391 if len(osMatch) > 0 { 392 return func(sb *platformStringsBuilder, ss ...string) { 393 for _, s := range ss { 394 sb.addOSString(s, osMatch, constraintPrefix) 395 } 396 } 397 } 398 399 case !isOSSpecific && isArchSpecific: 400 var archMatch []string 401 for _, arch := range rule.KnownArchs { 402 if rulesGoSupportsArch(v, arch) && 403 checkConstraints(c, "", arch, info.goos, info.goarch, info.tags, cgoTags) { 404 archMatch = append(archMatch, arch) 405 } 406 } 407 if len(archMatch) > 0 { 408 return func(sb *platformStringsBuilder, ss ...string) { 409 for _, s := range ss { 410 sb.addArchString(s, archMatch, constraintPrefix) 411 } 412 } 413 } 414 415 default: 416 var platformMatch []rule.Platform 417 for _, platform := range rule.KnownPlatforms { 418 if rulesGoSupportsPlatform(v, platform) && 419 checkConstraints(c, platform.OS, platform.Arch, info.goos, info.goarch, info.tags, cgoTags) { 420 platformMatch = append(platformMatch, platform) 421 } 422 } 423 if len(platformMatch) > 0 { 424 return func(sb *platformStringsBuilder, ss ...string) { 425 for _, s := range ss { 426 sb.addPlatformString(s, platformMatch, constraintPrefix) 427 } 428 } 429 } 430 } 431 432 return func(_ *platformStringsBuilder, _ ...string) {} 433 } 434 435 func (sb *platformStringsBuilder) isEmpty() bool { 436 return sb.strs == nil 437 } 438 439 func (sb *platformStringsBuilder) hasGo() bool { 440 for s := range sb.strs { 441 if strings.HasSuffix(s, ".go") { 442 return true 443 } 444 } 445 return false 446 } 447 448 func (sb *platformStringsBuilder) addGenericString(s string) { 449 if sb.strs == nil { 450 sb.strs = make(map[string]platformStringInfo) 451 } 452 sb.strs[s] = platformStringInfo{set: genericSet} 453 } 454 455 func (sb *platformStringsBuilder) addOSString(s string, oss []string, constraintPrefix string) { 456 if sb.strs == nil { 457 sb.strs = make(map[string]platformStringInfo) 458 } 459 si, ok := sb.strs[s] 460 if !ok { 461 si.set = osSet 462 si.osConstraints = make(map[string]bool) 463 } 464 switch si.set { 465 case genericSet: 466 return 467 case osSet: 468 for _, os := range oss { 469 si.osConstraints[constraintPrefix+os] = true 470 } 471 default: 472 si.convertToPlatforms(constraintPrefix) 473 for _, os := range oss { 474 for _, arch := range rule.KnownOSArchs[os] { 475 si.platformConstraints[rule.PlatformConstraint{ 476 Platform: rule.Platform{OS: os, Arch: arch}, 477 ConstraintPrefix: constraintPrefix, 478 }] = true 479 } 480 } 481 } 482 sb.strs[s] = si 483 } 484 485 func (sb *platformStringsBuilder) addArchString(s string, archs []string, constraintPrefix string) { 486 if sb.strs == nil { 487 sb.strs = make(map[string]platformStringInfo) 488 } 489 si, ok := sb.strs[s] 490 if !ok { 491 si.set = archSet 492 si.archConstraints = make(map[string]bool) 493 } 494 switch si.set { 495 case genericSet: 496 return 497 case archSet: 498 for _, arch := range archs { 499 si.archConstraints[constraintPrefix+arch] = true 500 } 501 default: 502 si.convertToPlatforms(constraintPrefix) 503 for _, arch := range archs { 504 for _, os := range rule.KnownArchOSs[arch] { 505 si.platformConstraints[rule.PlatformConstraint{ 506 Platform: rule.Platform{OS: os, Arch: arch}, 507 ConstraintPrefix: constraintPrefix, 508 }] = true 509 } 510 } 511 } 512 sb.strs[s] = si 513 } 514 515 func (sb *platformStringsBuilder) addPlatformString(s string, platforms []rule.Platform, constraintPrefix string) { 516 if sb.strs == nil { 517 sb.strs = make(map[string]platformStringInfo) 518 } 519 si, ok := sb.strs[s] 520 if !ok { 521 si.set = platformSet 522 si.platformConstraints = make(map[rule.PlatformConstraint]bool) 523 } 524 switch si.set { 525 case genericSet: 526 return 527 default: 528 si.convertToPlatforms(constraintPrefix) 529 for _, p := range platforms { 530 pConstraint := rule.PlatformConstraint{Platform: rule.Platform{OS: p.OS, Arch: p.Arch}, ConstraintPrefix: constraintPrefix} 531 si.platformConstraints[pConstraint] = true 532 } 533 } 534 sb.strs[s] = si 535 } 536 537 func (sb *platformStringsBuilder) build() rule.PlatformStrings { 538 var ps rule.PlatformStrings 539 for s, si := range sb.strs { 540 switch si.set { 541 case genericSet: 542 ps.Generic = append(ps.Generic, s) 543 case osSet: 544 if ps.OS == nil { 545 ps.OS = make(map[string][]string) 546 } 547 for os := range si.osConstraints { 548 ps.OS[os] = append(ps.OS[os], s) 549 } 550 case archSet: 551 if ps.Arch == nil { 552 ps.Arch = make(map[string][]string) 553 } 554 for arch := range si.archConstraints { 555 ps.Arch[arch] = append(ps.Arch[arch], s) 556 } 557 case platformSet: 558 if ps.Platform == nil { 559 ps.Platform = make(map[rule.PlatformConstraint][]string) 560 } 561 for p := range si.platformConstraints { 562 ps.Platform[p] = append(ps.Platform[p], s) 563 } 564 } 565 } 566 sort.Strings(ps.Generic) 567 if ps.OS != nil { 568 for _, ss := range ps.OS { 569 sort.Strings(ss) 570 } 571 } 572 if ps.Arch != nil { 573 for _, ss := range ps.Arch { 574 sort.Strings(ss) 575 } 576 } 577 if ps.Platform != nil { 578 for _, ss := range ps.Platform { 579 sort.Strings(ss) 580 } 581 } 582 return ps 583 } 584 585 func (sb *platformStringsBuilder) buildFlat() []string { 586 strs := make([]string, 0, len(sb.strs)) 587 for s := range sb.strs { 588 strs = append(strs, s) 589 } 590 sort.Strings(strs) 591 return strs 592 } 593 594 func (si *platformStringInfo) convertToPlatforms(constraintPrefix string) { 595 switch si.set { 596 case genericSet: 597 log.Panic("cannot convert generic string to platformConstraints") 598 case platformSet: 599 return 600 case osSet: 601 si.set = platformSet 602 si.platformConstraints = make(map[rule.PlatformConstraint]bool) 603 for osConstraint := range si.osConstraints { 604 os := strings.TrimPrefix(osConstraint, constraintPrefix) 605 for _, arch := range rule.KnownOSArchs[os] { 606 si.platformConstraints[rule.PlatformConstraint{ 607 Platform: rule.Platform{OS: os, Arch: arch}, 608 ConstraintPrefix: constraintPrefix, 609 }] = true 610 } 611 } 612 si.osConstraints = nil 613 case archSet: 614 si.set = platformSet 615 si.platformConstraints = make(map[rule.PlatformConstraint]bool) 616 for archConstraint := range si.archConstraints { 617 arch := strings.TrimPrefix(archConstraint, constraintPrefix) 618 for _, os := range rule.KnownArchOSs[arch] { 619 si.platformConstraints[rule.PlatformConstraint{ 620 Platform: rule.Platform{OS: os, Arch: arch}, 621 ConstraintPrefix: constraintPrefix, 622 }] = true 623 } 624 } 625 si.archConstraints = nil 626 } 627 } 628 629 var semverRex = regexp.MustCompile(`^.*?(/v\d+)(?:/.*)?$`) 630 631 // pathWithoutSemver removes a semantic version suffix from path. 632 // For example, if path is "example.com/foo/v2/bar", pathWithoutSemver 633 // will return "example.com/foo/bar". If there is no semantic version suffix, 634 // "" will be returned. 635 func pathWithoutSemver(path string) string { 636 m := semverRex.FindStringSubmatchIndex(path) 637 if m == nil { 638 return "" 639 } 640 v := path[m[2]+2 : m[3]] 641 if v[0] == '0' || v == "1" { 642 return "" 643 } 644 return path[:m[2]] + path[m[3]:] 645 }