// 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@[^/]+)?/(?P[^:]+)(?P:[^:]+)?$` locationPattern = `^(?P[^:]+):(?P[^:]+):(?P[^:]+)$` ) 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 }