123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571 |
- /* Copyright 2018 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"
- "go/build"
- "log"
- "path"
- "path/filepath"
- "sort"
- "strings"
- "sync"
- "github.com/bazelbuild/bazel-gazelle/internal/config"
- "github.com/bazelbuild/bazel-gazelle/internal/language/proto"
- "github.com/bazelbuild/bazel-gazelle/internal/pathtools"
- "github.com/bazelbuild/bazel-gazelle/internal/rule"
- )
- func (gl *goLang) GenerateRules(c *config.Config, dir, rel string, f *rule.File, subdirs, regularFiles, genFiles []string, otherEmpty, otherGen []*rule.Rule) (empty, gen []*rule.Rule) {
- // Extract information about proto files. We need this to exclude .pb.go
- // files and generate go_proto_library rules.
- gc := getGoConfig(c)
- pc := proto.GetProtoConfig(c)
- var protoRuleNames []string
- protoPackages := make(map[string]proto.Package)
- protoFileInfo := make(map[string]proto.FileInfo)
- for _, r := range otherGen {
- if r.Kind() != "proto_library" {
- continue
- }
- pkg := r.PrivateAttr(proto.PackageKey).(proto.Package)
- protoPackages[r.Name()] = pkg
- for name, info := range pkg.Files {
- protoFileInfo[name] = info
- }
- protoRuleNames = append(protoRuleNames, r.Name())
- }
- sort.Strings(protoRuleNames)
- var emptyProtoRuleNames []string
- for _, r := range otherEmpty {
- if r.Kind() == "proto_library" {
- emptyProtoRuleNames = append(emptyProtoRuleNames, r.Name())
- }
- }
- // If proto rule generation is enabled, exclude .pb.go files that correspond
- // to any .proto files present.
- if !pc.Mode.ShouldIncludePregeneratedFiles() {
- keep := func(f string) bool {
- if strings.HasSuffix(f, ".pb.go") {
- _, ok := protoFileInfo[strings.TrimSuffix(f, ".pb.go")+".proto"]
- return !ok
- }
- return true
- }
- filterFiles(®ularFiles, keep)
- filterFiles(&genFiles, keep)
- }
- // Split regular files into files which can determine the package name and
- // import path and other files.
- var goFiles, otherFiles []string
- for _, f := range regularFiles {
- if strings.HasSuffix(f, ".go") {
- goFiles = append(goFiles, f)
- } else {
- otherFiles = append(otherFiles, f)
- }
- }
- // Look for a subdirectory named testdata. Only treat it as data if it does
- // not contain a buildable package.
- var hasTestdata bool
- for _, sub := range subdirs {
- if sub == "testdata" {
- hasTestdata = !gl.goPkgRels[path.Join(rel, "testdata")]
- break
- }
- }
- // Build a set of packages from files in this directory.
- goPackageMap, goFilesWithUnknownPackage := buildPackages(c, dir, rel, goFiles, hasTestdata)
- // Select a package to generate rules for. If there is no package, create
- // an empty package so we can generate empty rules.
- var protoName string
- pkg, err := selectPackage(c, dir, goPackageMap)
- if err != nil {
- if _, ok := err.(*build.NoGoError); ok {
- if len(protoPackages) == 1 {
- for name, ppkg := range protoPackages {
- pkg = &goPackage{
- name: goProtoPackageName(ppkg),
- importPath: goProtoImportPath(gc, ppkg, rel),
- proto: protoTargetFromProtoPackage(name, ppkg),
- }
- protoName = name
- break
- }
- } else {
- pkg = emptyPackage(c, dir, rel)
- }
- } else {
- log.Print(err)
- }
- }
- // Try to link the selected package with a proto package.
- if pkg != nil {
- if pkg.importPath == "" {
- if err := pkg.inferImportPath(c); err != nil && pkg.firstGoFile() != "" {
- inferImportPathErrorOnce.Do(func() { log.Print(err) })
- }
- }
- for _, name := range protoRuleNames {
- ppkg := protoPackages[name]
- if pkg.importPath == goProtoImportPath(gc, ppkg, rel) {
- protoName = name
- pkg.proto = protoTargetFromProtoPackage(name, ppkg)
- break
- }
- }
- }
- // Generate rules for proto packages. These should come before the other
- // Go rules.
- g := newGenerator(c, f, rel)
- var rules []*rule.Rule
- var protoEmbed string
- for _, name := range protoRuleNames {
- ppkg := protoPackages[name]
- var rs []*rule.Rule
- if name == protoName {
- protoEmbed, rs = g.generateProto(pc.Mode, pkg.proto, pkg.importPath)
- } else {
- target := protoTargetFromProtoPackage(name, ppkg)
- importPath := goProtoImportPath(gc, ppkg, rel)
- _, rs = g.generateProto(pc.Mode, target, importPath)
- }
- rules = append(rules, rs...)
- }
- for _, name := range emptyProtoRuleNames {
- goProtoName := strings.TrimSuffix(name, "_proto") + "_go_proto"
- empty = append(empty, rule.NewRule("go_proto_library", goProtoName))
- }
- if pkg != nil && pc.Mode == proto.PackageMode && pkg.firstGoFile() == "" {
- // In proto package mode, don't generate a go_library embedding a
- // go_proto_library unless there are actually go files.
- protoEmbed = ""
- }
- // Complete the Go package and generate rules for that.
- if pkg != nil {
- // Add files with unknown packages. This happens when there are parse
- // or I/O errors. We should keep the file in the srcs list and let the
- // compiler deal with the error.
- cgo := pkg.haveCgo()
- for _, info := range goFilesWithUnknownPackage {
- if err := pkg.addFile(c, info, cgo); err != nil {
- log.Print(err)
- }
- }
- // Process the other static files.
- for _, file := range otherFiles {
- info := otherFileInfo(filepath.Join(dir, file))
- if err := pkg.addFile(c, info, cgo); err != nil {
- log.Print(err)
- }
- }
- // Process generated files. Note that generated files may have the same names
- // as static files. Bazel will use the generated files, but we will look at
- // the content of static files, assuming they will be the same.
- regularFileSet := make(map[string]bool)
- for _, f := range regularFiles {
- regularFileSet[f] = true
- }
- for _, f := range genFiles {
- if regularFileSet[f] {
- continue
- }
- info := fileNameInfo(filepath.Join(dir, f))
- if err := pkg.addFile(c, info, cgo); err != nil {
- log.Print(err)
- }
- }
- // Generate Go rules.
- if protoName == "" {
- // Empty proto rules for deletion.
- _, rs := g.generateProto(pc.Mode, pkg.proto, pkg.importPath)
- rules = append(rules, rs...)
- }
- lib := g.generateLib(pkg, protoEmbed)
- var libName string
- if !lib.IsEmpty(goKinds[lib.Kind()]) {
- libName = lib.Name()
- }
- rules = append(rules, lib)
- rules = append(rules,
- g.generateBin(pkg, libName),
- g.generateTest(pkg, libName))
- }
- for _, r := range rules {
- if r.IsEmpty(goKinds[r.Kind()]) {
- empty = append(empty, r)
- } else {
- gen = append(gen, r)
- }
- }
- if f != nil || len(gen) > 0 {
- gl.goPkgRels[rel] = true
- } else {
- for _, sub := range subdirs {
- if gl.goPkgRels[path.Join(rel, sub)] {
- gl.goPkgRels[rel] = true
- break
- }
- }
- }
- return empty, gen
- }
- func filterFiles(files *[]string, pred func(string) bool) {
- w := 0
- for r := 0; r < len(*files); r++ {
- f := (*files)[r]
- if pred(f) {
- (*files)[w] = f
- w++
- }
- }
- *files = (*files)[:w]
- }
- func buildPackages(c *config.Config, dir, rel string, goFiles []string, hasTestdata bool) (packageMap map[string]*goPackage, goFilesWithUnknownPackage []fileInfo) {
- // Process .go and .proto files first, since these determine the package name.
- packageMap = make(map[string]*goPackage)
- for _, f := range goFiles {
- path := filepath.Join(dir, f)
- info := goFileInfo(path, rel)
- if info.packageName == "" {
- goFilesWithUnknownPackage = append(goFilesWithUnknownPackage, info)
- continue
- }
- if info.packageName == "documentation" {
- // go/build ignores this package
- continue
- }
- if _, ok := packageMap[info.packageName]; !ok {
- packageMap[info.packageName] = &goPackage{
- name: info.packageName,
- dir: dir,
- rel: rel,
- hasTestdata: hasTestdata,
- }
- }
- if err := packageMap[info.packageName].addFile(c, info, false); err != nil {
- log.Print(err)
- }
- }
- return packageMap, goFilesWithUnknownPackage
- }
- var inferImportPathErrorOnce sync.Once
- // selectPackages selects one Go packages out of the buildable packages found
- // in a directory. If multiple packages are found, it returns the package
- // whose name matches the directory if such a package exists.
- func selectPackage(c *config.Config, dir string, packageMap map[string]*goPackage) (*goPackage, error) {
- buildablePackages := make(map[string]*goPackage)
- for name, pkg := range packageMap {
- if pkg.isBuildable(c) {
- buildablePackages[name] = pkg
- }
- }
- if len(buildablePackages) == 0 {
- return nil, &build.NoGoError{Dir: dir}
- }
- if len(buildablePackages) == 1 {
- for _, pkg := range buildablePackages {
- return pkg, nil
- }
- }
- if pkg, ok := buildablePackages[defaultPackageName(c, dir)]; ok {
- return pkg, nil
- }
- err := &build.MultiplePackageError{Dir: dir}
- for name, pkg := range buildablePackages {
- // Add the first file for each package for the error message.
- // Error() method expects these lists to be the same length. File
- // lists must be non-empty. These lists are only created by
- // buildPackage for packages with .go files present.
- err.Packages = append(err.Packages, name)
- err.Files = append(err.Files, pkg.firstGoFile())
- }
- return nil, err
- }
- func emptyPackage(c *config.Config, dir, rel string) *goPackage {
- pkg := &goPackage{
- name: defaultPackageName(c, dir),
- dir: dir,
- rel: rel,
- }
- pkg.inferImportPath(c)
- return pkg
- }
- func defaultPackageName(c *config.Config, rel string) string {
- gc := getGoConfig(c)
- return pathtools.RelBaseName(rel, gc.prefix, "")
- }
- // hasDefaultVisibility returns whether oldFile contains a "package" rule with
- // a "default_visibility" attribute. Rules generated by Gazelle should not
- // have their own visibility attributes if this is the case.
- func hasDefaultVisibility(oldFile *rule.File) bool {
- for _, r := range oldFile.Rules {
- if r.Kind() == "package" && r.Attr("default_visibility") != nil {
- return true
- }
- }
- return false
- }
- // checkInternalVisibility overrides the given visibility if the package is
- // internal.
- func checkInternalVisibility(rel, visibility string) string {
- if i := strings.LastIndex(rel, "/internal/"); i >= 0 {
- visibility = fmt.Sprintf("//%s:__subpackages__", rel[:i])
- } else if strings.HasPrefix(rel, "internal/") {
- visibility = "//:__subpackages__"
- }
- return visibility
- }
- type generator struct {
- c *config.Config
- rel string
- shouldSetVisibility bool
- }
- func newGenerator(c *config.Config, f *rule.File, rel string) *generator {
- shouldSetVisibility := f == nil || !hasDefaultVisibility(f)
- return &generator{c: c, rel: rel, shouldSetVisibility: shouldSetVisibility}
- }
- func (g *generator) generateProto(mode proto.Mode, target protoTarget, importPath string) (string, []*rule.Rule) {
- if !mode.ShouldGenerateRules() && mode != proto.LegacyMode {
- // Don't create or delete proto rules in this mode. Any existing rules
- // are likely hand-written.
- return "", nil
- }
- filegroupName := config.DefaultProtosName
- protoName := target.name
- if protoName == "" {
- importPath := inferImportPath(getGoConfig(g.c), g.rel)
- protoName = proto.RuleName(importPath)
- }
- goProtoName := strings.TrimSuffix(protoName, "_proto") + "_go_proto"
- visibility := []string{checkInternalVisibility(g.rel, "//visibility:public")}
- if mode == proto.LegacyMode {
- filegroup := rule.NewRule("filegroup", filegroupName)
- if target.sources.isEmpty() {
- return "", []*rule.Rule{filegroup}
- }
- filegroup.SetAttr("srcs", target.sources.build())
- if g.shouldSetVisibility {
- filegroup.SetAttr("visibility", visibility)
- }
- return "", []*rule.Rule{filegroup}
- }
- if target.sources.isEmpty() {
- return "", []*rule.Rule{
- rule.NewRule("filegroup", filegroupName),
- rule.NewRule("go_proto_library", goProtoName),
- }
- }
- goProtoLibrary := rule.NewRule("go_proto_library", goProtoName)
- goProtoLibrary.SetAttr("proto", ":"+protoName)
- g.setImportAttrs(goProtoLibrary, importPath)
- if target.hasServices {
- goProtoLibrary.SetAttr("compilers", []string{"@io_bazel_rules_go//proto:go_grpc"})
- }
- if g.shouldSetVisibility {
- goProtoLibrary.SetAttr("visibility", visibility)
- }
- goProtoLibrary.SetPrivateAttr(config.GazelleImportsKey, target.imports.build())
- return goProtoName, []*rule.Rule{goProtoLibrary}
- }
- func (g *generator) generateLib(pkg *goPackage, embed string) *rule.Rule {
- goLibrary := rule.NewRule("go_library", config.DefaultLibName)
- if !pkg.library.sources.hasGo() && embed == "" {
- return goLibrary // empty
- }
- var visibility string
- if pkg.isCommand() {
- // Libraries made for a go_binary should not be exposed to the public.
- visibility = "//visibility:private"
- } else {
- visibility = checkInternalVisibility(pkg.rel, "//visibility:public")
- }
- g.setCommonAttrs(goLibrary, pkg.rel, visibility, pkg.library, embed)
- g.setImportAttrs(goLibrary, pkg.importPath)
- return goLibrary
- }
- func (g *generator) generateBin(pkg *goPackage, library string) *rule.Rule {
- name := pathtools.RelBaseName(pkg.rel, getGoConfig(g.c).prefix, g.c.RepoRoot)
- goBinary := rule.NewRule("go_binary", name)
- if !pkg.isCommand() || pkg.binary.sources.isEmpty() && library == "" {
- return goBinary // empty
- }
- visibility := checkInternalVisibility(pkg.rel, "//visibility:public")
- g.setCommonAttrs(goBinary, pkg.rel, visibility, pkg.binary, library)
- return goBinary
- }
- func (g *generator) generateTest(pkg *goPackage, library string) *rule.Rule {
- goTest := rule.NewRule("go_test", config.DefaultTestName)
- if !pkg.test.sources.hasGo() {
- return goTest // empty
- }
- g.setCommonAttrs(goTest, pkg.rel, "", pkg.test, library)
- if pkg.hasTestdata {
- goTest.SetAttr("data", rule.GlobValue{Patterns: []string{"testdata/**"}})
- }
- return goTest
- }
- func (g *generator) setCommonAttrs(r *rule.Rule, pkgRel, visibility string, target goTarget, embed string) {
- if !target.sources.isEmpty() {
- r.SetAttr("srcs", target.sources.buildFlat())
- }
- if target.cgo {
- r.SetAttr("cgo", true)
- }
- if !target.clinkopts.isEmpty() {
- r.SetAttr("clinkopts", g.options(target.clinkopts.build(), pkgRel))
- }
- if !target.copts.isEmpty() {
- r.SetAttr("copts", g.options(target.copts.build(), pkgRel))
- }
- if g.shouldSetVisibility && visibility != "" {
- r.SetAttr("visibility", []string{visibility})
- }
- if embed != "" {
- r.SetAttr("embed", []string{":" + embed})
- }
- r.SetPrivateAttr(config.GazelleImportsKey, target.imports.build())
- }
- func (g *generator) setImportAttrs(r *rule.Rule, importPath string) {
- r.SetAttr("importpath", importPath)
- goConf := getGoConfig(g.c)
- if goConf.importMapPrefix != "" {
- fromPrefixRel := pathtools.TrimPrefix(g.rel, goConf.importMapPrefixRel)
- importMap := path.Join(goConf.importMapPrefix, fromPrefixRel)
- if importMap != importPath {
- r.SetAttr("importmap", importMap)
- }
- }
- }
- var (
- // shortOptPrefixes are strings that come at the beginning of an option
- // argument that includes a path, e.g., -Ifoo/bar.
- shortOptPrefixes = []string{"-I", "-L", "-F"}
- // longOptPrefixes are separate arguments that come before a path argument,
- // e.g., -iquote foo/bar.
- longOptPrefixes = []string{"-I", "-L", "-F", "-iquote", "-isystem"}
- )
- // options transforms package-relative paths in cgo options into repository-
- // root-relative paths that Bazel can understand. For example, if a cgo file
- // in //foo declares an include flag in its copts: "-Ibar", this method
- // will transform that flag into "-Ifoo/bar".
- func (g *generator) options(opts rule.PlatformStrings, pkgRel string) rule.PlatformStrings {
- fixPath := func(opt string) string {
- if strings.HasPrefix(opt, "/") {
- return opt
- }
- return path.Clean(path.Join(pkgRel, opt))
- }
- fixGroups := func(groups []string) ([]string, error) {
- fixedGroups := make([]string, len(groups))
- for i, group := range groups {
- opts := strings.Split(group, optSeparator)
- fixedOpts := make([]string, len(opts))
- isPath := false
- for j, opt := range opts {
- if isPath {
- opt = fixPath(opt)
- isPath = false
- goto next
- }
- for _, short := range shortOptPrefixes {
- if strings.HasPrefix(opt, short) && len(opt) > len(short) {
- opt = short + fixPath(opt[len(short):])
- goto next
- }
- }
- for _, long := range longOptPrefixes {
- if opt == long {
- isPath = true
- goto next
- }
- }
- next:
- fixedOpts[j] = escapeOption(opt)
- }
- fixedGroups[i] = strings.Join(fixedOpts, " ")
- }
- return fixedGroups, nil
- }
- opts, errs := opts.MapSlice(fixGroups)
- if errs != nil {
- log.Panicf("unexpected error when transforming options with pkg %q: %v", pkgRel, errs)
- }
- return opts
- }
- func escapeOption(opt string) string {
- return strings.NewReplacer(
- `\`, `\\`,
- `'`, `\'`,
- `"`, `\"`,
- ` `, `\ `,
- "\t", "\\\t",
- "\n", "\\\n",
- "\r", "\\\r",
- ).Replace(opt)
- }
|