| // 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 |
| } |