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  }