1// Copyright 2022 Google LLC 2// 3// Use of this source code is governed by a BSD-style license that can be 4// found in the LICENSE file. 5 6package exporter 7 8import ( 9 "fmt" 10 "path/filepath" 11 "regexp" 12 "strconv" 13 "strings" 14 15 "go.skia.org/infra/go/skerr" 16 "go.skia.org/infra/go/util" 17 "go.skia.org/skia/bazel/exporter/build_proto/analysis_v2" 18 "go.skia.org/skia/bazel/exporter/build_proto/build" 19) 20 21const ( 22 ruleOnlyRepoPattern = `^(@\w+)$` 23 rulePattern = `^(?P<repo>@[^/]+)?/(?P<path>[^:]+)(?P<target>:[^:]+)?$` 24 locationPattern = `^(?P<path>[^:]+):(?P<line>[^:]+):(?P<pos>[^:]+)$` 25) 26 27var ( 28 ruleOnlyRepoRegex = regexp.MustCompile(ruleOnlyRepoPattern) 29 ruleRegex = regexp.MustCompile(rulePattern) 30 locRegex = regexp.MustCompile(locationPattern) 31) 32 33// Return true if the given rule name represents an external repository. 34func isExternalRule(name string) bool { 35 return name[0] == '@' 36} 37 38// Given a Bazel rule name find that rule from within the 39// query results. Returns nil if the given rule is not present. 40func findRule(qr *analysis_v2.CqueryResult, name string) *build.Rule { 41 for _, result := range qr.GetResults() { 42 r := result.GetTarget().GetRule() 43 if r.GetName() == name { 44 return r 45 } 46 } 47 return nil 48} 49 50// Parse a rule into its constituent parts. 51// https://docs.bazel.build/versions/main/guide.html#specifying-targets-to-build 52// 53// For example, the input rule `//foo/bar:baz` will return: 54// 55// repo: "" 56// path: "/foo/bar" 57// target: "baz" 58func parseRule(rule string) (repo string, path string, target string, err error) { 59 match := ruleOnlyRepoRegex.FindStringSubmatch(rule) 60 if match != nil { 61 return match[1], "/", strings.TrimPrefix(match[1], "@"), nil 62 } 63 64 match = ruleRegex.FindStringSubmatch(rule) 65 if match == nil { 66 return "", "", "", skerr.Fmt(`Unable to match rule %q`, rule) 67 } 68 69 if len(match[3]) > 0 { 70 target = strings.TrimPrefix(match[3], ":") 71 } else { 72 // No explicit target, so use directory name as default target. 73 target = filepath.Base(match[2]) 74 } 75 76 return match[1], match[2], target, nil 77} 78 79// Parse a file location into its three constituent parts. 80// 81// A location is of the form: 82// 83// /full/path/to/BUILD.bazel:33:20 84func parseLocation(location string) (path string, line int, pos int, err error) { 85 match := locRegex.FindStringSubmatch(location) 86 if match == nil { 87 return "", 0, 0, skerr.Fmt(`unable to match file location %q`, location) 88 } 89 path = match[1] 90 line, err = strconv.Atoi(match[2]) 91 if err != nil { 92 return "", 0, 0, skerr.Fmt(`unable to parse line no. %q`, match[2]) 93 } 94 pos, err = strconv.Atoi(match[3]) 95 if err != nil { 96 return "", 0, 0, skerr.Fmt(`unable to parse pos. %q`, match[3]) 97 } 98 return path, line, pos, nil 99} 100 101// Return the directory containing the file in the location string. 102func getLocationDir(location string) (string, error) { 103 filePath, _, _, err := parseLocation(location) 104 if err != nil { 105 return "", skerr.Wrap(err) 106 } 107 return filepath.Dir(filePath), nil 108} 109 110func makeCanonicalRuleName(bazelRuleName string) (string, error) { 111 repo, path, target, err := parseRule(bazelRuleName) 112 if err != nil { 113 return "", skerr.Wrap(err) 114 } 115 return fmt.Sprintf("%s/%s:%s", repo, path, target), nil 116} 117 118// Determine if a target refers to a file, or a rule. target is of 119// the form: 120// 121// file: //include/private:SingleOwner.h 122// rule: //bazel/common_config_settings:has_gpu_backend 123func isFileTarget(target string) bool { 124 _, _, target, err := parseRule(target) 125 if err != nil { 126 return false 127 } 128 return strings.Contains(target, ".") 129} 130 131// Create a string that uniquely identifies the rule and can be used 132// in the exported project file as a valid name. 133func getRuleSimpleName(bazelRuleName string) (string, error) { 134 s, err := makeCanonicalRuleName(bazelRuleName) 135 if err != nil { 136 return "", skerr.Wrap(err) 137 } 138 s = strings.TrimPrefix(s, "//:") 139 s = strings.TrimPrefix(s, "//") 140 s = strings.ReplaceAll(s, "//", "_") 141 s = strings.ReplaceAll(s, "@", "at_") 142 s = strings.ReplaceAll(s, "/", "_") 143 s = strings.ReplaceAll(s, ":", "_") 144 s = strings.ReplaceAll(s, "__", "_") 145 return s, nil 146} 147 148// Append all elements to the slice if not already present in the slice. 149func appendUnique(slice []string, elems ...string) []string { 150 for _, elem := range elems { 151 if !util.In(elem, slice) { 152 slice = append(slice, elem) 153 } 154 } 155 return slice 156} 157 158// Retrieve (if present) a slice of string attribute values from the given 159// rule and attribute name. A nil slice will be returned if the attribute 160// does not exist in the rule. A slice of strings (possibly empty) will be 161// returned if the attribute is empty. An error will be returned if the 162// attribute is not a list type. 163func getRuleStringArrayAttribute(r *build.Rule, name string) ([]string, error) { 164 for _, attrib := range r.Attribute { 165 if attrib.GetName() != name { 166 continue 167 } 168 if attrib.GetType() != build.Attribute_LABEL_LIST && 169 attrib.GetType() != build.Attribute_STRING_LIST { 170 return nil, skerr.Fmt(`%s in rule %q is not a list`, name, r.GetName()) 171 } 172 return attrib.GetStringListValue(), nil 173 } 174 return nil, nil 175} 176 177// Given an input rule target return the workspace relative file path. 178// For example, an input of `//src/core:source.cpp` will return 179// `src/core/source.cpp`. 180func getFilePathFromFileTarget(target string) (string, error) { 181 _, path, t, err := parseRule(target) 182 if err != nil { 183 return "", skerr.Wrap(err) 184 } 185 if !isFileTarget(target) { 186 return "", skerr.Fmt("Target %q is not a file target.", target) 187 } 188 return filepath.Join(strings.TrimPrefix(path, "/"), t), nil 189} 190