blob: f58b38d7dd1d8d364bcdcb24db4eb92deceb39a6 [file]
/* 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 packages
import (
"go/build"
"io/ioutil"
"log"
"os"
"path"
"path/filepath"
"strings"
bf "github.com/bazelbuild/buildtools/build"
"github.com/bazelbuild/rules_go/go/tools/gazelle/config"
)
// A WalkFunc is a callback called by Walk for each package.
type WalkFunc func(c *config.Config, pkg *Package, oldFile *bf.File)
// Walk walks through directories under "root".
// It calls back "f" for each package. If an existing BUILD file is present
// in the directory, it will be parsed and passed to "f" as well.
//
// Walk is similar to "golang.org/x/tools/go/buildutil".ForEachPackage, but
// it does not assume the standard Go tree because Bazel rules_go uses
// go_prefix instead of the standard tree.
//
// If a directory contains no buildable Go code, "f" is not called. If a
// directory contains one package with any name, "f" will be called with that
// package. If a directory contains multiple packages and one of the package
// names matches the directory name, "f" will be called on that package and the
// other packages will be silently ignored. If none of the package names match
// the directory name, or if some other error occurs, an error will be logged,
// and "f" will not be called.
func Walk(c *config.Config, dir string, f WalkFunc) {
// visit walks the directory tree in post-order. It returns whether the
// the directory it was called on or any subdirectory contains a Bazel
// package. This affects whether "testdata" directories are considered
// data dependencies.
var visit func(string) bool
visit = func(path string) bool {
// Look for an existing BUILD file.
var oldFile *bf.File
haveError := false
for _, base := range c.ValidBuildFileNames {
oldPath := filepath.Join(path, base)
st, err := os.Stat(oldPath)
if os.IsNotExist(err) || err == nil && st.IsDir() {
continue
}
oldData, err := ioutil.ReadFile(oldPath)
if err != nil {
log.Print(err)
haveError = true
continue
}
if oldFile != nil {
log.Printf("in directory %s, multiple Bazel files are present: %s, %s",
path, filepath.Base(oldFile.Path), base)
haveError = true
continue
}
oldFile, err = bf.Parse(oldPath, oldData)
if err != nil {
log.Print(err)
haveError = true
continue
}
}
// Process directives in the build file.
excluded := make(map[string]bool)
if oldFile != nil {
directives := config.ParseDirectives(oldFile)
c = config.ApplyDirectives(c, directives)
for _, d := range directives {
if d.Key == "exclude" {
excluded[d.Value] = true
}
}
}
// List files and subdirectories.
files, err := ioutil.ReadDir(path)
if err != nil {
log.Print(err)
return false
}
var goFiles, otherFiles, subdirs []string
for _, f := range files {
base := f.Name()
switch {
case base == "" || base[0] == '.' || base[0] == '_' ||
excluded != nil && excluded[base] ||
base == "vendor" && f.IsDir() && c.DepMode != config.VendorMode:
continue
case f.IsDir():
subdirs = append(subdirs, base)
case strings.HasSuffix(base, ".go"):
goFiles = append(goFiles, base)
default:
otherFiles = append(otherFiles, base)
}
}
// Recurse into subdirectories.
hasTestdata := false
subdirHasPackage := false
for _, sub := range subdirs {
hasPackage := visit(filepath.Join(path, sub))
if sub == "testdata" && !hasPackage {
hasTestdata = true
}
subdirHasPackage = subdirHasPackage || hasPackage
}
hasPackage := subdirHasPackage || oldFile != nil
if haveError {
return hasPackage
}
// Build a package from files in this directory.
var genFiles []string
if oldFile != nil {
genFiles = findGenFiles(oldFile, excluded)
}
pkg := buildPackage(c, path, goFiles, otherFiles, genFiles, hasTestdata)
if pkg != nil {
f(c, pkg, oldFile)
hasPackage = true
}
return hasPackage
}
visit(dir)
}
// buildPackage reads source files in a given directory and returns a Package
// containing information about those files and how to build them.
//
// If no buildable .go files are found in the directory, nil will be returned.
// If the directory contains multiple buildable packages, the package whose
// name matches the directory base name will be returned. If there is no such
// package or if an error occurs, an error will be logged, and nil will be
// returned.
func buildPackage(c *config.Config, dir string, goFiles, otherFiles, genFiles []string, hasTestdata bool) *Package {
rel, err := filepath.Rel(c.RepoRoot, dir)
if err != nil {
log.Print(err)
return nil
}
rel = filepath.ToSlash(rel)
if rel == "." {
rel = ""
}
// Process the .go files first.
packageMap := make(map[string]*Package)
cgo := false
var goFilesWithUnknownPackage []fileInfo
for _, goFile := range goFiles {
info := goFileInfo(c, dir, rel, goFile)
if info.packageName == "" {
goFilesWithUnknownPackage = append(goFilesWithUnknownPackage, info)
continue
}
if info.packageName == "documentation" {
// go/build ignores this package
continue
}
cgo = cgo || info.isCgo
if _, ok := packageMap[info.packageName]; !ok {
packageMap[info.packageName] = &Package{
Name: info.packageName,
Dir: dir,
Rel: rel,
HasTestdata: hasTestdata,
}
}
err = packageMap[info.packageName].addFile(c, info, false)
if err != nil {
log.Print(err)
}
}
// Select a package to generate rules for.
pkg, err := selectPackage(c, dir, packageMap)
if err != nil {
if _, ok := err.(*build.NoGoError); !ok {
log.Print(err)
}
return nil
}
// Add .go 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.
for _, goFile := range goFilesWithUnknownPackage {
pkg.addFile(c, goFile, cgo)
}
// Process the other static files.
for _, file := range otherFiles {
info := otherFileInfo(dir, rel, file)
err = pkg.addFile(c, info, cgo)
if 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.
staticFiles := make(map[string]bool)
for _, f := range goFiles {
staticFiles[f] = true
}
for _, f := range otherFiles {
staticFiles[f] = true
}
for _, f := range genFiles {
if staticFiles[f] {
continue
}
info := fileNameInfo(dir, rel, f)
err := pkg.addFile(c, info, cgo)
if err != nil {
log.Print(err)
}
}
return pkg
}
func selectPackage(c *config.Config, dir string, packageMap map[string]*Package) (*Package, error) {
packagesWithGo := make(map[string]*Package)
for name, pkg := range packageMap {
if pkg.HasGo() {
packagesWithGo[name] = pkg
}
}
if len(packagesWithGo) == 0 {
return nil, &build.NoGoError{Dir: dir}
}
if len(packagesWithGo) == 1 {
for _, pkg := range packagesWithGo {
return pkg, nil
}
}
if pkg, ok := packagesWithGo[defaultPackageName(c, dir)]; ok {
return pkg, nil
}
err := &build.MultiplePackageError{Dir: dir}
for name, pkg := range packagesWithGo {
// 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 defaultPackageName(c *config.Config, dir string) string {
if dir != c.RepoRoot {
return filepath.Base(dir)
}
name := path.Base(c.GoPrefix)
if name == "." || name == "/" {
// This can happen if go_prefix is empty or is all slashes.
return "unnamed"
}
return name
}
func findGenFiles(f *bf.File, excluded map[string]bool) []string {
var strs []string
for _, r := range f.Rules("") {
for _, key := range []string{"out", "outs"} {
switch e := r.Attr(key).(type) {
case *bf.StringExpr:
strs = append(strs, e.Value)
case *bf.ListExpr:
for _, elem := range e.List {
if s, ok := elem.(*bf.StringExpr); ok {
strs = append(strs, s.Value)
}
}
}
}
}
var genFiles []string
for _, s := range strs {
if !excluded[s] {
genFiles = append(genFiles, s)
}
}
return genFiles
}