190 lines
5.4 KiB
Go
190 lines
5.4 KiB
Go
|
|
// Copyright 2022 Google LLC
|
||
|
|
//
|
||
|
|
// Use of this source code is governed by a BSD-style license that can be
|
||
|
|
// found in the LICENSE file.
|
||
|
|
|
||
|
|
package exporter
|
||
|
|
|
||
|
|
import (
|
||
|
|
"fmt"
|
||
|
|
"path/filepath"
|
||
|
|
"regexp"
|
||
|
|
"strconv"
|
||
|
|
"strings"
|
||
|
|
|
||
|
|
"go.skia.org/infra/go/skerr"
|
||
|
|
"go.skia.org/infra/go/util"
|
||
|
|
"go.skia.org/skia/bazel/exporter/build_proto/analysis_v2"
|
||
|
|
"go.skia.org/skia/bazel/exporter/build_proto/build"
|
||
|
|
)
|
||
|
|
|
||
|
|
const (
|
||
|
|
ruleOnlyRepoPattern = `^(@\w+)$`
|
||
|
|
rulePattern = `^(?P<repo>@[^/]+)?/(?P<path>[^:]+)(?P<target>:[^:]+)?$`
|
||
|
|
locationPattern = `^(?P<path>[^:]+):(?P<line>[^:]+):(?P<pos>[^:]+)$`
|
||
|
|
)
|
||
|
|
|
||
|
|
var (
|
||
|
|
ruleOnlyRepoRegex = regexp.MustCompile(ruleOnlyRepoPattern)
|
||
|
|
ruleRegex = regexp.MustCompile(rulePattern)
|
||
|
|
locRegex = regexp.MustCompile(locationPattern)
|
||
|
|
)
|
||
|
|
|
||
|
|
// Return true if the given rule name represents an external repository.
|
||
|
|
func isExternalRule(name string) bool {
|
||
|
|
return name[0] == '@'
|
||
|
|
}
|
||
|
|
|
||
|
|
// Given a Bazel rule name find that rule from within the
|
||
|
|
// query results. Returns nil if the given rule is not present.
|
||
|
|
func findRule(qr *analysis_v2.CqueryResult, name string) *build.Rule {
|
||
|
|
for _, result := range qr.GetResults() {
|
||
|
|
r := result.GetTarget().GetRule()
|
||
|
|
if r.GetName() == name {
|
||
|
|
return r
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// Parse a rule into its constituent parts.
|
||
|
|
// https://docs.bazel.build/versions/main/guide.html#specifying-targets-to-build
|
||
|
|
//
|
||
|
|
// For example, the input rule `//foo/bar:baz` will return:
|
||
|
|
//
|
||
|
|
// repo: ""
|
||
|
|
// path: "/foo/bar"
|
||
|
|
// target: "baz"
|
||
|
|
func parseRule(rule string) (repo string, path string, target string, err error) {
|
||
|
|
match := ruleOnlyRepoRegex.FindStringSubmatch(rule)
|
||
|
|
if match != nil {
|
||
|
|
return match[1], "/", strings.TrimPrefix(match[1], "@"), nil
|
||
|
|
}
|
||
|
|
|
||
|
|
match = ruleRegex.FindStringSubmatch(rule)
|
||
|
|
if match == nil {
|
||
|
|
return "", "", "", skerr.Fmt(`Unable to match rule %q`, rule)
|
||
|
|
}
|
||
|
|
|
||
|
|
if len(match[3]) > 0 {
|
||
|
|
target = strings.TrimPrefix(match[3], ":")
|
||
|
|
} else {
|
||
|
|
// No explicit target, so use directory name as default target.
|
||
|
|
target = filepath.Base(match[2])
|
||
|
|
}
|
||
|
|
|
||
|
|
return match[1], match[2], target, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// Parse a file location into its three constituent parts.
|
||
|
|
//
|
||
|
|
// A location is of the form:
|
||
|
|
//
|
||
|
|
// /full/path/to/BUILD.bazel:33:20
|
||
|
|
func parseLocation(location string) (path string, line int, pos int, err error) {
|
||
|
|
match := locRegex.FindStringSubmatch(location)
|
||
|
|
if match == nil {
|
||
|
|
return "", 0, 0, skerr.Fmt(`unable to match file location %q`, location)
|
||
|
|
}
|
||
|
|
path = match[1]
|
||
|
|
line, err = strconv.Atoi(match[2])
|
||
|
|
if err != nil {
|
||
|
|
return "", 0, 0, skerr.Fmt(`unable to parse line no. %q`, match[2])
|
||
|
|
}
|
||
|
|
pos, err = strconv.Atoi(match[3])
|
||
|
|
if err != nil {
|
||
|
|
return "", 0, 0, skerr.Fmt(`unable to parse pos. %q`, match[3])
|
||
|
|
}
|
||
|
|
return path, line, pos, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// Return the directory containing the file in the location string.
|
||
|
|
func getLocationDir(location string) (string, error) {
|
||
|
|
filePath, _, _, err := parseLocation(location)
|
||
|
|
if err != nil {
|
||
|
|
return "", skerr.Wrap(err)
|
||
|
|
}
|
||
|
|
return filepath.Dir(filePath), nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func makeCanonicalRuleName(bazelRuleName string) (string, error) {
|
||
|
|
repo, path, target, err := parseRule(bazelRuleName)
|
||
|
|
if err != nil {
|
||
|
|
return "", skerr.Wrap(err)
|
||
|
|
}
|
||
|
|
return fmt.Sprintf("%s/%s:%s", repo, path, target), nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// Determine if a target refers to a file, or a rule. target is of
|
||
|
|
// the form:
|
||
|
|
//
|
||
|
|
// file: //include/private:SingleOwner.h
|
||
|
|
// rule: //bazel/common_config_settings:has_gpu_backend
|
||
|
|
func isFileTarget(target string) bool {
|
||
|
|
_, _, target, err := parseRule(target)
|
||
|
|
if err != nil {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
return strings.Contains(target, ".")
|
||
|
|
}
|
||
|
|
|
||
|
|
// Create a string that uniquely identifies the rule and can be used
|
||
|
|
// in the exported project file as a valid name.
|
||
|
|
func getRuleSimpleName(bazelRuleName string) (string, error) {
|
||
|
|
s, err := makeCanonicalRuleName(bazelRuleName)
|
||
|
|
if err != nil {
|
||
|
|
return "", skerr.Wrap(err)
|
||
|
|
}
|
||
|
|
s = strings.TrimPrefix(s, "//:")
|
||
|
|
s = strings.TrimPrefix(s, "//")
|
||
|
|
s = strings.ReplaceAll(s, "//", "_")
|
||
|
|
s = strings.ReplaceAll(s, "@", "at_")
|
||
|
|
s = strings.ReplaceAll(s, "/", "_")
|
||
|
|
s = strings.ReplaceAll(s, ":", "_")
|
||
|
|
s = strings.ReplaceAll(s, "__", "_")
|
||
|
|
return s, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// Append all elements to the slice if not already present in the slice.
|
||
|
|
func appendUnique(slice []string, elems ...string) []string {
|
||
|
|
for _, elem := range elems {
|
||
|
|
if !util.In(elem, slice) {
|
||
|
|
slice = append(slice, elem)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return slice
|
||
|
|
}
|
||
|
|
|
||
|
|
// Retrieve (if present) a slice of string attribute values from the given
|
||
|
|
// rule and attribute name. A nil slice will be returned if the attribute
|
||
|
|
// does not exist in the rule. A slice of strings (possibly empty) will be
|
||
|
|
// returned if the attribute is empty. An error will be returned if the
|
||
|
|
// attribute is not a list type.
|
||
|
|
func getRuleStringArrayAttribute(r *build.Rule, name string) ([]string, error) {
|
||
|
|
for _, attrib := range r.Attribute {
|
||
|
|
if attrib.GetName() != name {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
if attrib.GetType() != build.Attribute_LABEL_LIST &&
|
||
|
|
attrib.GetType() != build.Attribute_STRING_LIST {
|
||
|
|
return nil, skerr.Fmt(`%s in rule %q is not a list`, name, r.GetName())
|
||
|
|
}
|
||
|
|
return attrib.GetStringListValue(), nil
|
||
|
|
}
|
||
|
|
return nil, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// Given an input rule target return the workspace relative file path.
|
||
|
|
// For example, an input of `//src/core:source.cpp` will return
|
||
|
|
// `src/core/source.cpp`.
|
||
|
|
func getFilePathFromFileTarget(target string) (string, error) {
|
||
|
|
_, path, t, err := parseRule(target)
|
||
|
|
if err != nil {
|
||
|
|
return "", skerr.Wrap(err)
|
||
|
|
}
|
||
|
|
if !isFileTarget(target) {
|
||
|
|
return "", skerr.Fmt("Target %q is not a file target.", target)
|
||
|
|
}
|
||
|
|
return filepath.Join(strings.TrimPrefix(path, "/"), t), nil
|
||
|
|
}
|