github.com/bazelbuild/bazel-gazelle@v0.36.1-0.20240520142334-61b277ba6fed/repo/repo.go (about) 1 /* Copyright 2017 The Bazel Authors. All rights reserved. 2 3 Licensed under the Apache License, Version 2.0 (the "License"); 4 you may not use this file except in compliance with the License. 5 You may obtain a copy of the License at 6 7 http://www.apache.org/licenses/LICENSE-2.0 8 9 Unless required by applicable law or agreed to in writing, software 10 distributed under the License is distributed on an "AS IS" BASIS, 11 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 See the License for the specific language governing permissions and 13 limitations under the License. 14 */ 15 16 // Package repo provides functionality for managing Go repository rules. 17 // 18 // UNSTABLE: The exported APIs in this package may change. In the future, 19 // language extensions should implement an interface for repository 20 // rule management. The update-repos command will call interface methods, 21 // and most if this package's functionality will move to language/go. 22 // Moving this package to an internal directory would break existing 23 // extensions, since RemoteCache is referenced through the resolve.Resolver 24 // interface, which extensions are required to implement. 25 package repo 26 27 import ( 28 "fmt" 29 "os" 30 "path/filepath" 31 "strings" 32 33 "github.com/bazelbuild/bazel-gazelle/rule" 34 ) 35 36 const gazelleFromDirectiveKey = "_gazelle_from_directive" 37 38 // FindExternalRepo attempts to locate the directory where Bazel has fetched 39 // the external repository with the given name. An error is returned if the 40 // repository directory cannot be located. 41 func FindExternalRepo(repoRoot, name string) (string, error) { 42 // See https://docs.bazel.build/versions/master/output_directories.html 43 // for documentation on Bazel directory layout. 44 // We expect the bazel-out symlink in the workspace root directory to point to 45 // <output-base>/execroot/<workspace-name>/bazel-out 46 // We expect the external repository to be checked out at 47 // <output-base>/external/<name> 48 externalPath := strings.Join([]string{repoRoot, "bazel-out", "..", "..", "..", "external", name}, string(os.PathSeparator)) 49 cleanPath, err := filepath.EvalSymlinks(externalPath) 50 if err != nil { 51 return "", err 52 } 53 st, err := os.Stat(cleanPath) 54 if err != nil { 55 return "", err 56 } 57 if !st.IsDir() { 58 return "", fmt.Errorf("%s: not a directory", externalPath) 59 } 60 return cleanPath, nil 61 } 62 63 type macroKey struct { 64 file, def string 65 } 66 67 type loader struct { 68 repos []*rule.Rule 69 repoRoot string 70 repoFileMap map[string]*rule.File // repo rule name => file that contains repo 71 repoIndexMap map[string]int // repo rule name => index of rule in "repos" slice 72 visited map[macroKey]struct{} 73 } 74 75 // IsFromDirective returns true if the repo rule was defined from a directive. 76 func IsFromDirective(repo *rule.Rule) bool { 77 b, ok := repo.PrivateAttr(gazelleFromDirectiveKey).(bool) 78 return ok && b 79 } 80 81 // add adds a repository rule to a file. 82 // In the case of duplicate rules, select the rule 83 // with the following prioritization: 84 // - rules that were provided as directives have precedence 85 // - rules that were provided last 86 func (l *loader) add(file *rule.File, repo *rule.Rule) { 87 name := repo.Name() 88 if name == "" { 89 return 90 } 91 92 if i, ok := l.repoIndexMap[repo.Name()]; ok { 93 if IsFromDirective(l.repos[i]) && !IsFromDirective(repo) { 94 // We always prefer directives over non-directives 95 return 96 } 97 // Update existing rule to new rule 98 l.repos[i] = repo 99 } else { 100 l.repos = append(l.repos, repo) 101 l.repoIndexMap[name] = len(l.repos) - 1 102 } 103 l.repoFileMap[name] = file 104 } 105 106 // visit returns true exactly once for each file,function key, and false for all future instances 107 func (l *loader) visit(file, function string) bool { 108 if _, ok := l.visited[macroKey{file, function}]; ok { 109 return false 110 } 111 l.visited[macroKey{file, function}] = struct{}{} 112 return true 113 } 114 115 // ListRepositories extracts metadata about repositories declared in a 116 // file. 117 func ListRepositories(workspace *rule.File) (repos []*rule.Rule, repoFileMap map[string]*rule.File, err error) { 118 l := &loader{ 119 repoRoot: filepath.Dir(workspace.Path), 120 repoIndexMap: make(map[string]int), 121 repoFileMap: make(map[string]*rule.File), 122 visited: make(map[macroKey]struct{}), 123 } 124 125 for _, repo := range workspace.Rules { 126 l.add(workspace, repo) 127 } 128 if err := l.loadExtraRepos(workspace); err != nil { 129 return nil, nil, err 130 } 131 132 for _, d := range workspace.Directives { 133 switch d.Key { 134 case "repository_macro": 135 parsed, err := ParseRepositoryMacroDirective(d.Value) 136 if err != nil { 137 return nil, nil, err 138 } 139 140 if err := l.loadRepositoriesFromMacro(parsed); err != nil { 141 return nil, nil, err 142 } 143 } 144 } 145 return l.repos, l.repoFileMap, nil 146 } 147 148 func (l *loader) loadRepositoriesFromMacro(macro *RepoMacro) error { 149 f := filepath.Join(l.repoRoot, macro.Path) 150 if !l.visit(f, macro.DefName) { 151 return nil 152 } 153 154 macroFile, err := rule.LoadMacroFile(f, "", macro.DefName) 155 if err != nil { 156 return fmt.Errorf("failed to load %s in repoRoot %s: %w", f, l.repoRoot, err) 157 } 158 loads := map[string]*rule.Load{} 159 for _, load := range macroFile.Loads { 160 for _, name := range load.Symbols() { 161 loads[name] = load 162 } 163 } 164 for _, rule := range macroFile.Rules { 165 // (Incorrectly) assume that anything with a name attribute is a rule, not a macro to recurse into 166 if rule.Name() != "" { 167 l.add(macroFile, rule) 168 continue 169 } 170 if !macro.Leveled { 171 continue 172 } 173 // If another repository macro is loaded that macro defName must be called. 174 // When a defName is called, the defName of the function is the rule's "kind". 175 // This then must be matched with the Load that it is imported with, so that 176 // file can be loaded 177 kind := rule.Kind() 178 load := loads[kind] 179 if load == nil { 180 continue 181 } 182 resolved := loadToMacroDef(load, l.repoRoot, kind) 183 // TODO: Also handle the case where one macro calls another macro in the same bzl file 184 if macro.Path == "" { 185 continue 186 } 187 188 if err := l.loadRepositoriesFromMacro(resolved); err != nil { 189 return err 190 } 191 } 192 return l.loadExtraRepos(macroFile) 193 } 194 195 // loadToMacroDef takes a load 196 // e.g. for if called on 197 // load("package_name:package_dir/file.bzl", alias_name="original_def_name") 198 // with defAlias = "alias_name", it will return: 199 // 200 // -> "/Path/to/package_name/package_dir/file.bzl" 201 // -> "original_def_name" 202 func loadToMacroDef(l *rule.Load, repoRoot, defAlias string) *RepoMacro { 203 rel := strings.Replace(filepath.Clean(l.Name()), ":", string(filepath.Separator), 1) 204 // A loaded macro may refer to the macro by a different name (alias) in the load, 205 // thus, the original name must be resolved to load the macro file properly. 206 defName := l.Unalias(defAlias) 207 return &RepoMacro{ 208 Path: rel, 209 DefName: defName, 210 } 211 } 212 213 func (l *loader) loadExtraRepos(f *rule.File) error { 214 extraRepos, err := parseRepositoryDirectives(f.Directives) 215 if err != nil { 216 return err 217 } 218 for _, repo := range extraRepos { 219 l.add(f, repo) 220 } 221 return nil 222 } 223 224 func parseRepositoryDirectives(directives []rule.Directive) (repos []*rule.Rule, err error) { 225 for _, d := range directives { 226 switch d.Key { 227 case "repository": 228 vals := strings.Fields(d.Value) 229 if len(vals) < 2 { 230 return nil, fmt.Errorf("failure parsing repository: %s, expected repository kind and attributes", d.Value) 231 } 232 kind := vals[0] 233 r := rule.NewRule(kind, "") 234 r.SetPrivateAttr(gazelleFromDirectiveKey, true) 235 for _, val := range vals[1:] { 236 kv := strings.SplitN(val, "=", 2) 237 if len(kv) != 2 { 238 return nil, fmt.Errorf("failure parsing repository: %s, expected format for attributes is attr1_name=attr1_value", d.Value) 239 } 240 r.SetAttr(kv[0], kv[1]) 241 } 242 if r.Name() == "" { 243 return nil, fmt.Errorf("failure parsing repository: %s, expected a name attribute for the given repository", d.Value) 244 } 245 repos = append(repos, r) 246 } 247 } 248 return repos, nil 249 } 250 251 type RepoMacro struct { 252 Path string 253 DefName string 254 Leveled bool 255 } 256 257 // ParseRepositoryMacroDirective checks the directive is in proper format, and splits 258 // path and defName. Repository_macros prepended with a "+" (e.g. "# gazelle:repository_macro +file%def") 259 // indicates a "leveled" macro, which loads other macro files. 260 func ParseRepositoryMacroDirective(directive string) (*RepoMacro, error) { 261 vals := strings.Split(directive, "%") 262 if len(vals) != 2 { 263 return nil, fmt.Errorf("Failure parsing repository_macro: %s, expected format is macroFile%%defName", directive) 264 } 265 f := vals[0] 266 if strings.HasPrefix(f, "..") { 267 return nil, fmt.Errorf("Failure parsing repository_macro: %s, macro file path %s should not start with \"..\"", directive, f) 268 } 269 return &RepoMacro{ 270 Path: strings.TrimPrefix(f, "+"), 271 DefName: vals[1], 272 Leveled: strings.HasPrefix(f, "+"), 273 }, nil 274 }