blob: 1b17ca8f833b8b94f7f03f1fcc413a782ea752b3 [file] [log] [blame]
/* Copyright 2016 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 generator
import (
"fmt"
"log"
"path"
"strings"
"github.com/bazelbuild/bazel-gazelle/internal/config"
"github.com/bazelbuild/bazel-gazelle/internal/label"
"github.com/bazelbuild/bazel-gazelle/internal/packages"
"github.com/bazelbuild/bazel-gazelle/internal/pathtools"
bf "github.com/bazelbuild/buildtools/build"
)
// NewGenerator returns a new instance of Generator.
// "oldFile" is the existing build file. May be nil.
func NewGenerator(c *config.Config, l *label.Labeler, oldFile *bf.File) *Generator {
shouldSetVisibility := oldFile == nil || !hasDefaultVisibility(oldFile)
return &Generator{c: c, l: l, shouldSetVisibility: shouldSetVisibility}
}
// Generator generates Bazel build rules for Go build targets.
type Generator struct {
c *config.Config
l *label.Labeler
shouldSetVisibility bool
}
// GenerateRules generates a list of rules for targets in "pkg". It also returns
// a list of empty rules that may be deleted from an existing file.
func (g *Generator) GenerateRules(pkg *packages.Package) (rules []bf.Expr, empty []bf.Expr, err error) {
var rs []bf.Expr
protoLibName, protoRules := g.generateProto(pkg)
rs = append(rs, protoRules...)
libName, libRule := g.generateLib(pkg, protoLibName)
rs = append(rs, libRule)
rs = append(rs,
g.generateBin(pkg, libName),
g.generateTest(pkg, libName))
for _, r := range rs {
if isEmpty(r) {
empty = append(empty, r)
} else {
rules = append(rules, r)
}
}
return rules, empty, nil
}
func (g *Generator) generateProto(pkg *packages.Package) (string, []bf.Expr) {
if g.c.ProtoMode == config.DisableProtoMode {
// Don't create or delete proto rules in this mode. Any existing rules
// are likely hand-written.
return "", nil
}
filegroupName := config.DefaultProtosName
protoName := g.l.ProtoLabel(pkg.Rel, pkg.Name).Name
goProtoName := g.l.GoProtoLabel(pkg.Rel, pkg.Name).Name
if g.c.ProtoMode == config.LegacyProtoMode {
if !pkg.Proto.HasProto() {
return "", []bf.Expr{EmptyRule("filegroup", filegroupName)}
}
attrs := []KeyValue{
{Key: "name", Value: filegroupName},
{Key: "srcs", Value: pkg.Proto.Sources},
}
if g.shouldSetVisibility {
attrs = append(attrs, KeyValue{"visibility", []string{checkInternalVisibility(pkg.Rel, "//visibility:public")}})
}
return "", []bf.Expr{NewRule("filegroup", attrs)}
}
if !pkg.Proto.HasProto() {
return "", []bf.Expr{
EmptyRule("filegroup", filegroupName),
EmptyRule("proto_library", protoName),
EmptyRule("go_proto_library", goProtoName),
}
}
var rules []bf.Expr
visibility := []string{checkInternalVisibility(pkg.Rel, "//visibility:public")}
protoAttrs := []KeyValue{
{"name", protoName},
{"srcs", pkg.Proto.Sources},
}
if g.shouldSetVisibility {
protoAttrs = append(protoAttrs, KeyValue{"visibility", visibility})
}
imports := pkg.Proto.Imports
if !imports.IsEmpty() {
protoAttrs = append(protoAttrs, KeyValue{config.GazelleImportsKey, imports})
}
rules = append(rules, NewRule("proto_library", protoAttrs))
goProtoAttrs := []KeyValue{
{"name", goProtoName},
{"proto", ":" + protoName},
}
goProtoAttrs = append(goProtoAttrs, g.importAttrs(pkg)...)
if pkg.Proto.HasServices {
goProtoAttrs = append(goProtoAttrs, KeyValue{"compilers", []string{"@io_bazel_rules_go//proto:go_grpc"}})
}
if g.shouldSetVisibility {
goProtoAttrs = append(goProtoAttrs, KeyValue{"visibility", visibility})
}
if !imports.IsEmpty() {
goProtoAttrs = append(goProtoAttrs, KeyValue{config.GazelleImportsKey, imports})
}
rules = append(rules, NewRule("go_proto_library", goProtoAttrs))
return goProtoName, rules
}
func (g *Generator) generateBin(pkg *packages.Package, library string) bf.Expr {
name := g.l.BinaryLabel(pkg.Rel).Name
if !pkg.IsCommand() || pkg.Binary.Sources.IsEmpty() && library == "" {
return EmptyRule("go_binary", name)
}
visibility := checkInternalVisibility(pkg.Rel, "//visibility:public")
attrs := g.commonAttrs(pkg.Rel, name, visibility, pkg.Binary)
if library != "" {
attrs = append(attrs, KeyValue{"embed", []string{":" + library}})
}
return NewRule("go_binary", attrs)
}
func (g *Generator) generateLib(pkg *packages.Package, goProtoName string) (string, *bf.CallExpr) {
name := g.l.LibraryLabel(pkg.Rel).Name
if !pkg.Library.HasGo() && goProtoName == "" {
return "", EmptyRule("go_library", name)
}
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")
}
attrs := g.commonAttrs(pkg.Rel, name, visibility, pkg.Library)
attrs = append(attrs, g.importAttrs(pkg)...)
if goProtoName != "" {
attrs = append(attrs, KeyValue{"embed", []string{":" + goProtoName}})
}
rule := NewRule("go_library", attrs)
return name, rule
}
// 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 *bf.File) bool {
for _, s := range oldFile.Stmt {
c, ok := s.(*bf.CallExpr)
if !ok {
continue
}
r := bf.Rule{Call: c}
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
}
func (g *Generator) generateTest(pkg *packages.Package, library string) bf.Expr {
name := g.l.TestLabel(pkg.Rel).Name
if !pkg.Test.HasGo() {
return EmptyRule("go_test", name)
}
attrs := g.commonAttrs(pkg.Rel, name, "", pkg.Test)
if library != "" {
attrs = append(attrs, KeyValue{"embed", []string{":" + library}})
}
if pkg.HasTestdata {
glob := GlobValue{Patterns: []string{"testdata/**"}}
attrs = append(attrs, KeyValue{"data", glob})
}
return NewRule("go_test", attrs)
}
func (g *Generator) commonAttrs(pkgRel, name, visibility string, target packages.GoTarget) []KeyValue {
attrs := []KeyValue{{"name", name}}
if !target.Sources.IsEmpty() {
attrs = append(attrs, KeyValue{"srcs", target.Sources.Flat()})
}
if target.Cgo {
attrs = append(attrs, KeyValue{"cgo", true})
}
if !target.CLinkOpts.IsEmpty() {
attrs = append(attrs, KeyValue{"clinkopts", g.options(target.CLinkOpts, pkgRel)})
}
if !target.COpts.IsEmpty() {
attrs = append(attrs, KeyValue{"copts", g.options(target.COpts, pkgRel)})
}
if g.shouldSetVisibility && visibility != "" {
attrs = append(attrs, KeyValue{"visibility", []string{visibility}})
}
imports := target.Imports
if !imports.IsEmpty() {
attrs = append(attrs, KeyValue{config.GazelleImportsKey, imports})
}
return attrs
}
func (g *Generator) importAttrs(pkg *packages.Package) []KeyValue {
attrs := []KeyValue{{"importpath", pkg.ImportPath}}
if g.c.GoImportMapPrefix != "" {
fromPrefixRel := pathtools.TrimPrefix(pkg.Rel, g.c.GoImportMapPrefixRel)
importMap := path.Join(g.c.GoImportMapPrefix, fromPrefixRel)
if importMap != pkg.ImportPath {
attrs = append(attrs, KeyValue{"importmap", importMap})
}
}
return attrs
}
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 packages.PlatformStrings, pkgRel string) packages.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, packages.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)
}
func isEmpty(r bf.Expr) bool {
c, ok := r.(*bf.CallExpr)
return ok && len(c.List) == 1 // name
}