| /* Copyright 2017 The Bazel Authors. All rights reserved. |
| |
| Licensed under the Apache License, Version 2.0 (the "License"); |
| you may not use this file except in compliance with the License. |
| You may obtain a copy of the License at |
| |
| http://www.apache.org/licenses/LICENSE-2.0 |
| |
| Unless required by applicable law or agreed to in writing, software |
| distributed under the License is distributed on an "AS IS" BASIS, |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| See the License for the specific language governing permissions and |
| limitations under the License. |
| */ |
| |
| package golang |
| |
| import ( |
| "fmt" |
| "log" |
| "path" |
| "regexp" |
| "sort" |
| "strings" |
| |
| "github.com/bazelbuild/bazel-gazelle/config" |
| "github.com/bazelbuild/bazel-gazelle/language/proto" |
| "github.com/bazelbuild/bazel-gazelle/pathtools" |
| "github.com/bazelbuild/bazel-gazelle/rule" |
| ) |
| |
| // goPackage contains metadata for a set of .go and .proto files that can be |
| // used to generate Go rules. |
| type goPackage struct { |
| name, dir, rel string |
| library, binary, test goTarget |
| tests []goTarget |
| proto protoTarget |
| hasTestdata bool |
| hasMainFunction bool |
| importPath string |
| } |
| |
| // goTarget contains information used to generate an individual Go rule |
| // (library, binary, or test). |
| type goTarget struct { |
| sources, embedSrcs, imports, cppopts, copts, cxxopts, clinkopts platformStringsBuilder |
| cgo, hasInternalTest bool |
| } |
| |
| // protoTarget contains information used to generate a go_proto_library rule. |
| type protoTarget struct { |
| name string |
| sources platformStringsBuilder |
| imports platformStringsBuilder |
| hasServices bool |
| } |
| |
| // platformStringsBuilder is used to construct rule.PlatformStrings. Bazel |
| // has some requirements for deps list (a dependency cannot appear in more |
| // than one select expression; dependencies cannot be duplicated), so we need |
| // to build these carefully. |
| type platformStringsBuilder struct { |
| strs map[string]platformStringInfo |
| } |
| |
| // platformStringInfo contains information about a single string (source, |
| // import, or option). |
| type platformStringInfo struct { |
| set platformStringSet |
| osConstraints map[string]bool |
| archConstraints map[string]bool |
| platformConstraints map[rule.PlatformConstraint]bool |
| } |
| |
| type platformStringSet int |
| |
| const ( |
| genericSet platformStringSet = iota |
| osSet |
| archSet |
| platformSet |
| ) |
| |
| // Matches a package version, eg. the end segment of 'example.com/foo/v1' |
| var pkgVersionRe = regexp.MustCompile("^v[0-9]+$") |
| |
| // addFile adds the file described by "info" to a target in the package "p" if |
| // the file is buildable. |
| // |
| // "cgo" tells whether any ".go" file in the package contains cgo code. This |
| // affects whether C files are added to targets. |
| // |
| // An error is returned if a file is buildable but invalid (for example, a |
| // test .go file containing cgo code). Files that are not buildable will not |
| // be added to any target (for example, .txt files). |
| func (pkg *goPackage) addFile(c *config.Config, er *embedResolver, info fileInfo, cgo bool) error { |
| switch { |
| case info.ext == unknownExt || !cgo && (info.ext == cExt || info.ext == csExt): |
| return nil |
| case info.ext == protoExt: |
| if pcMode := getProtoMode(c); pcMode == proto.LegacyMode { |
| // Only add files in legacy mode. This is used to generate a filegroup |
| // that contains all protos. In order modes, we get the .proto files |
| // from information emitted by the proto language extension. |
| pkg.proto.addFile(info) |
| } |
| case info.isTest: |
| if getGoConfig(c).testMode == fileTestMode || len(pkg.tests) == 0 { |
| pkg.tests = append(pkg.tests, goTarget{}) |
| } |
| // Add the the file to the most recently added test target (in fileTestMode) |
| // or the only test target (in defaultMode). |
| // In both cases, this will be the last element in the slice. |
| test := &pkg.tests[len(pkg.tests)-1] |
| test.addFile(c, er, info) |
| if !info.isExternalTest { |
| test.hasInternalTest = true |
| } |
| default: |
| pkg.hasMainFunction = pkg.hasMainFunction || info.hasMainFunction |
| pkg.library.addFile(c, er, info) |
| } |
| |
| return nil |
| } |
| |
| // isCommand returns true if the package name is "main". |
| func (pkg *goPackage) isCommand() bool { |
| return pkg.name == "main" && pkg.hasMainFunction |
| } |
| |
| // isBuildable returns true if anything in the package is buildable. |
| // This is true if the package has Go code that satisfies build constraints |
| // on any platform or has proto files not in legacy mode. |
| func (pkg *goPackage) isBuildable(c *config.Config) bool { |
| return pkg.firstGoFile() != "" || !pkg.proto.sources.isEmpty() |
| } |
| |
| // firstGoFile returns the name of a .go file if the package contains at least |
| // one .go file, or "" otherwise. |
| func (pkg *goPackage) firstGoFile() string { |
| goSrcs := []platformStringsBuilder{ |
| pkg.library.sources, |
| pkg.binary.sources, |
| } |
| for _, test := range pkg.tests { |
| goSrcs = append(goSrcs, test.sources) |
| } |
| |
| for _, sb := range goSrcs { |
| if sb.strs != nil { |
| for s := range sb.strs { |
| if strings.HasSuffix(s, ".go") { |
| return s |
| } |
| } |
| } |
| } |
| return "" |
| } |
| |
| func (pkg *goPackage) haveCgo() bool { |
| if pkg.library.cgo || pkg.binary.cgo { |
| return true |
| } |
| for _, t := range pkg.tests { |
| if t.cgo { |
| return true |
| } |
| } |
| return false |
| } |
| |
| func (pkg *goPackage) inferImportPath(c *config.Config) error { |
| if pkg.importPath != "" { |
| log.Panic("importPath already set") |
| } |
| gc := getGoConfig(c) |
| if !gc.prefixSet { |
| 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) |
| } |
| pkg.importPath = InferImportPath(c, pkg.rel) |
| return nil |
| } |
| |
| // libNameFromImportPath returns a a suitable go_library name based on the import path. |
| // Major version suffixes (eg. "v1") are dropped. |
| func libNameFromImportPath(dir string) string { |
| i := strings.LastIndexAny(dir, "/\\") |
| if i < 0 { |
| return dir |
| } |
| name := dir[i+1:] |
| if pkgVersionRe.MatchString(name) { |
| dir := dir[:i] |
| i = strings.LastIndexAny(dir, "/\\") |
| if i >= 0 { |
| name = dir[i+1:] |
| } |
| } |
| return strings.ReplaceAll(name, ".", "_") |
| } |
| |
| // libNameByConvention returns a suitable name for a go_library using the given |
| // naming convention, the import path, and the package name. |
| func libNameByConvention(nc namingConvention, imp string, pkgName string) string { |
| if nc == goDefaultLibraryNamingConvention { |
| return defaultLibName |
| } |
| name := libNameFromImportPath(imp) |
| isCommand := pkgName == "main" |
| if name == "" { |
| if isCommand { |
| name = "lib" |
| } else { |
| name = pkgName |
| } |
| } else if isCommand { |
| name += "_lib" |
| } |
| return name |
| } |
| |
| // testNameByConvention returns a suitable name for a go_test using the given |
| // naming convention and the import path. |
| func testNameByConvention(nc namingConvention, imp string) string { |
| if nc == goDefaultLibraryNamingConvention { |
| return defaultTestName |
| } |
| libName := libNameFromImportPath(imp) |
| if libName == "" { |
| libName = "lib" |
| } |
| return libName + "_test" |
| } |
| |
| // testNameFromSingleSource returns a suitable name for a go_test using the |
| // single Go source file name. |
| func testNameFromSingleSource(src string) string { |
| if i := strings.LastIndexByte(src, '.'); i >= 0 { |
| src = src[0:i] |
| } |
| libName := libNameFromImportPath(src) |
| if libName == "" { |
| return "" |
| } |
| if strings.HasSuffix(libName, "_test") { |
| return libName |
| } |
| return libName + "_test" |
| } |
| |
| // binName returns a suitable name for a go_binary. |
| func binName(rel, prefix, repoRoot string) string { |
| return pathtools.RelBaseName(rel, prefix, repoRoot) |
| } |
| |
| func InferImportPath(c *config.Config, rel string) string { |
| gc := getGoConfig(c) |
| if rel == gc.prefixRel { |
| return gc.prefix |
| } else { |
| fromPrefixRel := strings.TrimPrefix(rel, gc.prefixRel+"/") |
| return path.Join(gc.prefix, fromPrefixRel) |
| } |
| } |
| |
| func goProtoPackageName(pkg proto.Package) string { |
| if value, ok := pkg.Options["go_package"]; ok { |
| if strings.LastIndexByte(value, '/') == -1 { |
| return value |
| } else { |
| if i := strings.LastIndexByte(value, ';'); i != -1 { |
| return value[i+1:] |
| } else { |
| return path.Base(value) |
| } |
| } |
| } |
| return strings.Replace(pkg.Name, ".", "_", -1) |
| } |
| |
| func goProtoImportPath(c *config.Config, pkg proto.Package, rel string) string { |
| if value, ok := pkg.Options["go_package"]; ok { |
| if strings.LastIndexByte(value, '/') == -1 { |
| return InferImportPath(c, rel) |
| } else if i := strings.LastIndexByte(value, ';'); i != -1 { |
| return value[:i] |
| } else { |
| return value |
| } |
| } |
| return InferImportPath(c, rel) |
| } |
| |
| func (t *goTarget) addFile(c *config.Config, er *embedResolver, info fileInfo) { |
| t.cgo = t.cgo || info.isCgo |
| add := getPlatformStringsAddFunction(c, info, nil) |
| add(&t.sources, info.name) |
| add(&t.imports, info.imports...) |
| if er != nil { |
| for _, embed := range info.embeds { |
| embedSrcs, err := er.resolve(embed) |
| if err != nil { |
| log.Print(err) |
| continue |
| } |
| add(&t.embedSrcs, embedSrcs...) |
| } |
| } |
| for _, cppopts := range info.cppopts { |
| optAdd := add |
| if !cppopts.empty() { |
| optAdd = getPlatformStringsAddFunction(c, info, cppopts) |
| } |
| optAdd(&t.cppopts, cppopts.opts) |
| } |
| for _, copts := range info.copts { |
| optAdd := add |
| if !copts.empty() { |
| optAdd = getPlatformStringsAddFunction(c, info, copts) |
| } |
| optAdd(&t.copts, copts.opts) |
| } |
| for _, cxxopts := range info.cxxopts { |
| optAdd := add |
| if !cxxopts.empty() { |
| optAdd = getPlatformStringsAddFunction(c, info, cxxopts) |
| } |
| optAdd(&t.cxxopts, cxxopts.opts) |
| } |
| for _, clinkopts := range info.clinkopts { |
| optAdd := add |
| if !clinkopts.empty() { |
| optAdd = getPlatformStringsAddFunction(c, info, clinkopts) |
| } |
| optAdd(&t.clinkopts, clinkopts.opts) |
| } |
| } |
| |
| func protoTargetFromProtoPackage(name string, pkg proto.Package) protoTarget { |
| target := protoTarget{name: name} |
| for _, fInfo := range pkg.Files { |
| target.sources.addGenericString(fInfo.Path) |
| } |
| for i := range pkg.Imports { |
| target.imports.addGenericString(i) |
| } |
| target.hasServices = pkg.HasServices |
| return target |
| } |
| |
| func (t *protoTarget) addFile(info fileInfo) { |
| t.sources.addGenericString(info.name) |
| for _, imp := range info.imports { |
| t.imports.addGenericString(imp) |
| } |
| t.hasServices = t.hasServices || info.hasServices |
| } |
| |
| // getPlatformStringsAddFunction returns a function used to add strings to |
| // a *platformStringsBuilder under the same set of constraints. This is a |
| // performance optimization to avoid evaluating constraints repeatedly. |
| func getPlatformStringsAddFunction(c *config.Config, info fileInfo, cgoTags *cgoTagsAndOpts) func(sb *platformStringsBuilder, ss ...string) { |
| isOSSpecific, isArchSpecific := isOSArchSpecific(info, cgoTags) |
| v := getGoConfig(c).rulesGoVersion |
| constraintPrefix := "@" + getGoConfig(c).rulesGoRepoName + "//go/platform:" |
| |
| switch { |
| case !isOSSpecific && !isArchSpecific: |
| if checkConstraints(c, "", "", info.goos, info.goarch, info.tags, cgoTags) { |
| return func(sb *platformStringsBuilder, ss ...string) { |
| for _, s := range ss { |
| sb.addGenericString(s) |
| } |
| } |
| } |
| |
| case isOSSpecific && !isArchSpecific: |
| var osMatch []string |
| for _, os := range rule.KnownOSs { |
| if rulesGoSupportsOS(v, os) && |
| checkConstraints(c, os, "", info.goos, info.goarch, info.tags, cgoTags) { |
| osMatch = append(osMatch, os) |
| } |
| } |
| if len(osMatch) > 0 { |
| return func(sb *platformStringsBuilder, ss ...string) { |
| for _, s := range ss { |
| sb.addOSString(s, osMatch, constraintPrefix) |
| } |
| } |
| } |
| |
| case !isOSSpecific && isArchSpecific: |
| var archMatch []string |
| for _, arch := range rule.KnownArchs { |
| if rulesGoSupportsArch(v, arch) && |
| checkConstraints(c, "", arch, info.goos, info.goarch, info.tags, cgoTags) { |
| archMatch = append(archMatch, arch) |
| } |
| } |
| if len(archMatch) > 0 { |
| return func(sb *platformStringsBuilder, ss ...string) { |
| for _, s := range ss { |
| sb.addArchString(s, archMatch, constraintPrefix) |
| } |
| } |
| } |
| |
| default: |
| var platformMatch []rule.Platform |
| for _, platform := range rule.KnownPlatforms { |
| if rulesGoSupportsPlatform(v, platform) && |
| checkConstraints(c, platform.OS, platform.Arch, info.goos, info.goarch, info.tags, cgoTags) { |
| platformMatch = append(platformMatch, platform) |
| } |
| } |
| if len(platformMatch) > 0 { |
| return func(sb *platformStringsBuilder, ss ...string) { |
| for _, s := range ss { |
| sb.addPlatformString(s, platformMatch, constraintPrefix) |
| } |
| } |
| } |
| } |
| |
| return func(_ *platformStringsBuilder, _ ...string) {} |
| } |
| |
| func (sb *platformStringsBuilder) isEmpty() bool { |
| return sb.strs == nil |
| } |
| |
| func (sb *platformStringsBuilder) hasGo() bool { |
| for s := range sb.strs { |
| if strings.HasSuffix(s, ".go") { |
| return true |
| } |
| } |
| return false |
| } |
| |
| func (sb *platformStringsBuilder) addGenericString(s string) { |
| if sb.strs == nil { |
| sb.strs = make(map[string]platformStringInfo) |
| } |
| sb.strs[s] = platformStringInfo{set: genericSet} |
| } |
| |
| func (sb *platformStringsBuilder) addOSString(s string, oss []string, constraintPrefix string) { |
| if sb.strs == nil { |
| sb.strs = make(map[string]platformStringInfo) |
| } |
| si, ok := sb.strs[s] |
| if !ok { |
| si.set = osSet |
| si.osConstraints = make(map[string]bool) |
| } |
| switch si.set { |
| case genericSet: |
| return |
| case osSet: |
| for _, os := range oss { |
| si.osConstraints[constraintPrefix+os] = true |
| } |
| default: |
| si.convertToPlatforms(constraintPrefix) |
| for _, os := range oss { |
| for _, arch := range rule.KnownOSArchs[os] { |
| si.platformConstraints[rule.PlatformConstraint{ |
| Platform: rule.Platform{OS: os, Arch: arch}, |
| ConstraintPrefix: constraintPrefix, |
| }] = true |
| } |
| } |
| } |
| sb.strs[s] = si |
| } |
| |
| func (sb *platformStringsBuilder) addArchString(s string, archs []string, constraintPrefix string) { |
| if sb.strs == nil { |
| sb.strs = make(map[string]platformStringInfo) |
| } |
| si, ok := sb.strs[s] |
| if !ok { |
| si.set = archSet |
| si.archConstraints = make(map[string]bool) |
| } |
| switch si.set { |
| case genericSet: |
| return |
| case archSet: |
| for _, arch := range archs { |
| si.archConstraints[constraintPrefix+arch] = true |
| } |
| default: |
| si.convertToPlatforms(constraintPrefix) |
| for _, arch := range archs { |
| for _, os := range rule.KnownArchOSs[arch] { |
| si.platformConstraints[rule.PlatformConstraint{ |
| Platform: rule.Platform{OS: os, Arch: arch}, |
| ConstraintPrefix: constraintPrefix, |
| }] = true |
| } |
| } |
| } |
| sb.strs[s] = si |
| } |
| |
| func (sb *platformStringsBuilder) addPlatformString(s string, platforms []rule.Platform, constraintPrefix string) { |
| if sb.strs == nil { |
| sb.strs = make(map[string]platformStringInfo) |
| } |
| si, ok := sb.strs[s] |
| if !ok { |
| si.set = platformSet |
| si.platformConstraints = make(map[rule.PlatformConstraint]bool) |
| } |
| switch si.set { |
| case genericSet: |
| return |
| default: |
| si.convertToPlatforms(constraintPrefix) |
| for _, p := range platforms { |
| pConstraint := rule.PlatformConstraint{Platform: rule.Platform{OS: p.OS, Arch: p.Arch}, ConstraintPrefix: constraintPrefix} |
| si.platformConstraints[pConstraint] = true |
| } |
| } |
| sb.strs[s] = si |
| } |
| |
| func (sb *platformStringsBuilder) build() rule.PlatformStrings { |
| var ps rule.PlatformStrings |
| for s, si := range sb.strs { |
| switch si.set { |
| case genericSet: |
| ps.Generic = append(ps.Generic, s) |
| case osSet: |
| if ps.OS == nil { |
| ps.OS = make(map[string][]string) |
| } |
| for os := range si.osConstraints { |
| ps.OS[os] = append(ps.OS[os], s) |
| } |
| case archSet: |
| if ps.Arch == nil { |
| ps.Arch = make(map[string][]string) |
| } |
| for arch := range si.archConstraints { |
| ps.Arch[arch] = append(ps.Arch[arch], s) |
| } |
| case platformSet: |
| if ps.Platform == nil { |
| ps.Platform = make(map[rule.PlatformConstraint][]string) |
| } |
| for p := range si.platformConstraints { |
| ps.Platform[p] = append(ps.Platform[p], s) |
| } |
| } |
| } |
| sort.Strings(ps.Generic) |
| if ps.OS != nil { |
| for _, ss := range ps.OS { |
| sort.Strings(ss) |
| } |
| } |
| if ps.Arch != nil { |
| for _, ss := range ps.Arch { |
| sort.Strings(ss) |
| } |
| } |
| if ps.Platform != nil { |
| for _, ss := range ps.Platform { |
| sort.Strings(ss) |
| } |
| } |
| return ps |
| } |
| |
| func (sb *platformStringsBuilder) buildFlat() []string { |
| strs := make([]string, 0, len(sb.strs)) |
| for s := range sb.strs { |
| strs = append(strs, s) |
| } |
| sort.Strings(strs) |
| return strs |
| } |
| |
| func (si *platformStringInfo) convertToPlatforms(constraintPrefix string) { |
| switch si.set { |
| case genericSet: |
| log.Panic("cannot convert generic string to platformConstraints") |
| case platformSet: |
| return |
| case osSet: |
| si.set = platformSet |
| si.platformConstraints = make(map[rule.PlatformConstraint]bool) |
| for osConstraint := range si.osConstraints { |
| os := strings.TrimPrefix(osConstraint, constraintPrefix) |
| for _, arch := range rule.KnownOSArchs[os] { |
| si.platformConstraints[rule.PlatformConstraint{ |
| Platform: rule.Platform{OS: os, Arch: arch}, |
| ConstraintPrefix: constraintPrefix, |
| }] = true |
| } |
| } |
| si.osConstraints = nil |
| case archSet: |
| si.set = platformSet |
| si.platformConstraints = make(map[rule.PlatformConstraint]bool) |
| for archConstraint := range si.archConstraints { |
| arch := strings.TrimPrefix(archConstraint, constraintPrefix) |
| for _, os := range rule.KnownArchOSs[arch] { |
| si.platformConstraints[rule.PlatformConstraint{ |
| Platform: rule.Platform{OS: os, Arch: arch}, |
| ConstraintPrefix: constraintPrefix, |
| }] = true |
| } |
| } |
| si.archConstraints = nil |
| } |
| } |
| |
| var semverRex = regexp.MustCompile(`^.*?(/v\d+)(?:/.*)?$`) |
| |
| // pathWithoutSemver removes a semantic version suffix from path. |
| // For example, if path is "example.com/foo/v2/bar", pathWithoutSemver |
| // will return "example.com/foo/bar". If there is no semantic version suffix, |
| // "" will be returned. |
| func pathWithoutSemver(path string) string { |
| m := semverRex.FindStringSubmatchIndex(path) |
| if m == nil { |
| return "" |
| } |
| v := path[m[2]+2 : m[3]] |
| if v[0] == '0' || v == "1" { |
| return "" |
| } |
| return path[:m[2]] + path[m[3]:] |
| } |