350 lines
11 KiB
Go
350 lines
11 KiB
Go
/* Copyright 2020 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 bzl generates a `bzl_library` target for every `.bzl` file in
|
|
// each package.
|
|
//
|
|
// The `bzl_library` rule is provided by
|
|
// https://github.com/bazelbuild/bazel-skylib.
|
|
//
|
|
// This extension is experimental and subject to change. It is not included
|
|
// in the default Gazelle binary.
|
|
package bzl
|
|
|
|
import (
|
|
"flag"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"log"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/bazelbuild/bazel-gazelle/config"
|
|
"github.com/bazelbuild/bazel-gazelle/label"
|
|
"github.com/bazelbuild/bazel-gazelle/language"
|
|
"github.com/bazelbuild/bazel-gazelle/pathtools"
|
|
"github.com/bazelbuild/bazel-gazelle/repo"
|
|
"github.com/bazelbuild/bazel-gazelle/resolve"
|
|
"github.com/bazelbuild/bazel-gazelle/rule"
|
|
|
|
"github.com/bazelbuild/buildtools/build"
|
|
)
|
|
|
|
const languageName = "starlark"
|
|
const fileType = ".bzl"
|
|
|
|
var ignoreSuffix = suffixes{
|
|
"_tests.bzl",
|
|
"_test.bzl",
|
|
}
|
|
|
|
type suffixes []string
|
|
|
|
func (s suffixes) Matches(test string) bool {
|
|
for _, v := range s {
|
|
if strings.HasSuffix(test, v) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
type bzlLibraryLang struct{}
|
|
|
|
// NewLanguage is called by Gazelle to install this language extension in a binary.
|
|
func NewLanguage() language.Language {
|
|
return &bzlLibraryLang{}
|
|
}
|
|
|
|
// Name returns the name of the language. This should be a prefix of the
|
|
// kinds of rules generated by the language, e.g., "go" for the Go extension
|
|
// since it generates "go_library" rules.
|
|
func (*bzlLibraryLang) Name() string { return languageName }
|
|
|
|
// The following methods are implemented to satisfy the
|
|
// https://pkg.go.dev/github.com/bazelbuild/bazel-gazelle/resolve?tab=doc#Resolver
|
|
// interface, but are otherwise unused.
|
|
func (*bzlLibraryLang) RegisterFlags(fs *flag.FlagSet, cmd string, c *config.Config) {}
|
|
func (*bzlLibraryLang) CheckFlags(fs *flag.FlagSet, c *config.Config) error { return nil }
|
|
func (*bzlLibraryLang) KnownDirectives() []string { return nil }
|
|
func (*bzlLibraryLang) Configure(c *config.Config, rel string, f *rule.File) {}
|
|
|
|
// Kinds returns a map of maps rule names (kinds) and information on how to
|
|
// match and merge attributes that may be found in rules of those kinds. All
|
|
// kinds of rules generated for this language may be found here.
|
|
func (*bzlLibraryLang) Kinds() map[string]rule.KindInfo {
|
|
return kinds
|
|
}
|
|
|
|
// Loads returns .bzl files and symbols they define. Every rule generated by
|
|
// GenerateRules, now or in the past, should be loadable from one of these
|
|
// files.
|
|
func (*bzlLibraryLang) Loads() []rule.LoadInfo {
|
|
return []rule.LoadInfo{{
|
|
Name: "@bazel_skylib//:bzl_library.bzl",
|
|
Symbols: []string{"bzl_library"},
|
|
}}
|
|
}
|
|
|
|
// Fix repairs deprecated usage of language-specific rules in f. This is
|
|
// called before the file is indexed. Unless c.ShouldFix is true, fixes
|
|
// that delete or rename rules should not be performed.
|
|
func (*bzlLibraryLang) Fix(c *config.Config, f *rule.File) {}
|
|
|
|
// Imports returns a list of ImportSpecs that can be used to import the rule
|
|
// r. This is used to populate RuleIndex.
|
|
//
|
|
// If nil is returned, the rule will not be indexed. If any non-nil slice is
|
|
// returned, including an empty slice, the rule will be indexed.
|
|
func (b *bzlLibraryLang) Imports(c *config.Config, r *rule.Rule, f *rule.File) []resolve.ImportSpec {
|
|
srcs := r.AttrStrings("srcs")
|
|
imports := make([]resolve.ImportSpec, 0, len(srcs))
|
|
|
|
for _, src := range srcs {
|
|
spec := resolve.ImportSpec{
|
|
// Lang is the language in which the import string appears (this should
|
|
// match Resolver.Name).
|
|
Lang: languageName,
|
|
// Imp is an import string for the library.
|
|
Imp: fmt.Sprintf("//%s:%s", f.Pkg, src),
|
|
}
|
|
|
|
imports = append(imports, spec)
|
|
}
|
|
|
|
return imports
|
|
}
|
|
|
|
// Embeds returns a list of labels of rules that the given rule embeds. If
|
|
// a rule is embedded by another importable rule of the same language, only
|
|
// the embedding rule will be indexed. The embedding rule will inherit
|
|
// the imports of the embedded rule.
|
|
// Since SkyLark doesn't support embedding this should always return nil.
|
|
func (*bzlLibraryLang) Embeds(r *rule.Rule, from label.Label) []label.Label { return nil }
|
|
|
|
// Resolve translates imported libraries for a given rule into Bazel
|
|
// dependencies. Information about imported libraries is returned for each
|
|
// rule generated by language.GenerateRules in
|
|
// language.GenerateResult.Imports. Resolve generates a "deps" attribute (or
|
|
// the appropriate language-specific equivalent) for each import according to
|
|
// language-specific rules and heuristics.
|
|
func (*bzlLibraryLang) Resolve(c *config.Config, ix *resolve.RuleIndex, rc *repo.RemoteCache, r *rule.Rule, importsRaw interface{}, from label.Label) {
|
|
imports := importsRaw.([]string)
|
|
|
|
r.DelAttr("deps")
|
|
|
|
if len(imports) == 0 {
|
|
return
|
|
}
|
|
|
|
deps := make([]string, 0, len(imports))
|
|
for _, imp := range imports {
|
|
impLabel, err := label.Parse(imp)
|
|
if err != nil {
|
|
log.Printf("%s: import of %q is invalid: %v", from.String(), imp, err)
|
|
continue
|
|
}
|
|
|
|
// the index only contains absolute labels, not relative
|
|
impLabel = impLabel.Abs(from.Repo, from.Pkg)
|
|
|
|
if impLabel.Repo == "bazel_tools" {
|
|
// The @bazel_tools repo is tricky because it is a part of the "shipped
|
|
// with bazel" core library for interacting with the outside world.
|
|
// This means that it can not depend on skylib. Fortunately there is a
|
|
// fairly simple workaround for this, which is that you can add those
|
|
// bzl files as `deps` entries.
|
|
deps = append(deps, imp)
|
|
continue
|
|
}
|
|
|
|
if impLabel.Repo != "" || !c.IndexLibraries {
|
|
// This is a dependency that is external to the current repo, or indexing
|
|
// is disabled so take a guess at what hte target name should be.
|
|
deps = append(deps, strings.TrimSuffix(imp, fileType))
|
|
continue
|
|
}
|
|
|
|
res := resolve.ImportSpec{
|
|
Lang: languageName,
|
|
Imp: impLabel.String(),
|
|
}
|
|
matches := ix.FindRulesByImport(res, languageName)
|
|
|
|
if len(matches) == 0 {
|
|
log.Printf("%s: %q (%s) was not found in dependency index. Skipping. This may result in an incomplete deps section and require manual BUILD file intervention.\n", from.String(), imp, impLabel.String())
|
|
}
|
|
|
|
for _, m := range matches {
|
|
depLabel := m.Label
|
|
depLabel = depLabel.Rel(from.Repo, from.Pkg)
|
|
deps = append(deps, depLabel.String())
|
|
}
|
|
}
|
|
|
|
sort.Strings(deps)
|
|
if len(deps) > 0 {
|
|
r.SetAttr("deps", deps)
|
|
}
|
|
}
|
|
|
|
var kinds = map[string]rule.KindInfo{
|
|
"bzl_library": {
|
|
NonEmptyAttrs: map[string]bool{"srcs": true, "deps": true},
|
|
MergeableAttrs: map[string]bool{"srcs": true},
|
|
},
|
|
}
|
|
|
|
// GenerateRules extracts build metadata from source files in a directory.
|
|
// GenerateRules is called in each directory where an update is requested
|
|
// in depth-first post-order.
|
|
//
|
|
// args contains the arguments for GenerateRules. This is passed as a
|
|
// struct to avoid breaking implementations in the future when new
|
|
// fields are added.
|
|
//
|
|
// A GenerateResult struct is returned. Optional fields may be added to this
|
|
// type in the future.
|
|
//
|
|
// Any non-fatal errors this function encounters should be logged using
|
|
// log.Print.
|
|
func (*bzlLibraryLang) GenerateRules(args language.GenerateArgs) language.GenerateResult {
|
|
var rules []*rule.Rule
|
|
var imports []interface{}
|
|
for _, f := range append(args.RegularFiles, args.GenFiles...) {
|
|
if !isBzlSourceFile(f) {
|
|
continue
|
|
}
|
|
name := strings.TrimSuffix(f, fileType)
|
|
r := rule.NewRule("bzl_library", name)
|
|
|
|
r.SetAttr("srcs", []string{f})
|
|
|
|
shouldSetVisibility := args.File == nil || !args.File.HasDefaultVisibility()
|
|
if shouldSetVisibility {
|
|
vis := checkInternalVisibility(args.Rel, "//visibility:public")
|
|
r.SetAttr("visibility", []string{vis})
|
|
}
|
|
|
|
fullPath := filepath.Join(args.Dir, f)
|
|
loads, err := getBzlFileLoads(fullPath)
|
|
if err != nil {
|
|
log.Printf("%s: contains syntax errors: %v", fullPath, err)
|
|
// Don't `continue` since it is reasonable to create a target even
|
|
// without deps.
|
|
}
|
|
|
|
rules = append(rules, r)
|
|
imports = append(imports, loads)
|
|
}
|
|
|
|
return language.GenerateResult{
|
|
Gen: rules,
|
|
Imports: imports,
|
|
Empty: generateEmpty(args),
|
|
}
|
|
}
|
|
|
|
func getBzlFileLoads(path string) ([]string, error) {
|
|
f, err := ioutil.ReadFile(path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("ioutil.ReadFile(%q) error: %v", path, err)
|
|
}
|
|
ast, err := build.ParseBuild(path, f)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("build.Parse(%q) error: %v", f, err)
|
|
}
|
|
|
|
var loads []string
|
|
build.WalkOnce(ast, func(expr *build.Expr) {
|
|
n := *expr
|
|
if l, ok := n.(*build.LoadStmt); ok {
|
|
loads = append(loads, l.Module.Value)
|
|
}
|
|
})
|
|
sort.Strings(loads)
|
|
|
|
return loads, nil
|
|
}
|
|
|
|
func isBzlSourceFile(f string) bool {
|
|
return strings.HasSuffix(f, fileType) && !ignoreSuffix.Matches(f)
|
|
}
|
|
|
|
// generateEmpty generates the list of rules that don't need to exist in the
|
|
// BUILD file any more.
|
|
// For each bzl_library rule in args.File that only has srcs that aren't in
|
|
// args.RegularFiles or args.GenFiles, add a bzl_library with no srcs or deps.
|
|
// That will let Gazelle delete bzl_library rules after the corresponding .bzl
|
|
// files are deleted.
|
|
func generateEmpty(args language.GenerateArgs) []*rule.Rule {
|
|
var ret []*rule.Rule
|
|
if args.File == nil {
|
|
return ret
|
|
}
|
|
for _, r := range args.File.Rules {
|
|
if r.Kind() != "bzl_library" {
|
|
continue
|
|
}
|
|
name := r.AttrString("name")
|
|
|
|
exists := make(map[string]bool)
|
|
for _, f := range args.RegularFiles {
|
|
exists[f] = true
|
|
}
|
|
for _, f := range args.GenFiles {
|
|
exists[f] = true
|
|
}
|
|
for _, r := range args.File.Rules {
|
|
srcsExist := false
|
|
for _, f := range r.AttrStrings("srcs") {
|
|
if exists[f] {
|
|
srcsExist = true
|
|
break
|
|
}
|
|
}
|
|
if !srcsExist {
|
|
ret = append(ret, rule.NewRule("bzl_library", name))
|
|
}
|
|
}
|
|
}
|
|
return ret
|
|
}
|
|
|
|
type srcsList []string
|
|
|
|
func (s srcsList) Contains(m string) bool {
|
|
for _, e := range s {
|
|
if e == m {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// checkInternalVisibility overrides the given visibility if the package is
|
|
// internal.
|
|
func checkInternalVisibility(rel, visibility string) string {
|
|
if i := pathtools.Index(rel, "internal"); i > 0 {
|
|
visibility = fmt.Sprintf("//%s:__subpackages__", rel[:i-1])
|
|
} else if i := pathtools.Index(rel, "private"); i > 0 {
|
|
visibility = fmt.Sprintf("//%s:__subpackages__", rel[:i-1])
|
|
} else if pathtools.HasPrefix(rel, "internal") || pathtools.HasPrefix(rel, "private") {
|
|
visibility = "//:__subpackages__"
|
|
}
|
|
return visibility
|
|
}
|