github.com/afking/bazel-gazelle@v0.0.0-20180301150245-c02bc0f529e8/internal/repos/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 repos
    17  
    18  import (
    19  	"fmt"
    20  	"os"
    21  	"path/filepath"
    22  	"sort"
    23  	"strings"
    24  
    25  	"github.com/bazelbuild/bazel-gazelle/internal/rules"
    26  	bf "github.com/bazelbuild/buildtools/build"
    27  )
    28  
    29  // Repo describes an external repository rule declared in a Bazel
    30  // WORKSPACE file.
    31  type Repo struct {
    32  	// Name is the value of the "name" attribute of the repository rule.
    33  	Name string
    34  
    35  	// GoPrefix is the portion of the Go import path for the root of this
    36  	// repository. Usually the same as Remote.
    37  	GoPrefix string
    38  
    39  	// Commit is the revision at which a repository is checked out (for example,
    40  	// a Git commit id).
    41  	Commit string
    42  
    43  	// Tag is the name of the version at which a repository is checked out.
    44  	Tag string
    45  
    46  	// Remote is the URL the repository can be cloned or checked out from.
    47  	Remote string
    48  
    49  	// VCS is the version control system used to check out the repository.
    50  	// May also be "http" for HTTP archives.
    51  	VCS string
    52  }
    53  
    54  type byName []Repo
    55  
    56  func (s byName) Len() int           { return len(s) }
    57  func (s byName) Less(i, j int) bool { return s[i].Name < s[j].Name }
    58  func (s byName) Swap(i, j int)      { s[i], s[j] = s[j], s[i] }
    59  
    60  type lockFileFormat int
    61  
    62  const (
    63  	unknownFormat lockFileFormat = iota
    64  	depFormat
    65  )
    66  
    67  var lockFileParsers = map[lockFileFormat]func(string) ([]Repo, error){
    68  	depFormat: importRepoRulesDep,
    69  }
    70  
    71  // ImportRepoRules reads the lock file of a vendoring tool and returns
    72  // a list of equivalent repository rules that can be merged into a WORKSPACE
    73  // file. The format of the file is inferred from its basename. Currently,
    74  // only Gopkg.lock is supported.
    75  func ImportRepoRules(filename string) ([]bf.Expr, error) {
    76  	format := getLockFileFormat(filename)
    77  	if format == unknownFormat {
    78  		return nil, fmt.Errorf(`%s: unrecognized lock file format. Expected "Gopkg.lock"`, filename)
    79  	}
    80  	parser := lockFileParsers[format]
    81  	repos, err := parser(filename)
    82  	if err != nil {
    83  		return nil, fmt.Errorf("error parsing %q: %v", filename, err)
    84  	}
    85  	sort.Stable(byName(repos))
    86  
    87  	rules := make([]bf.Expr, 0, len(repos))
    88  	for _, repo := range repos {
    89  		rules = append(rules, GenerateRule(repo))
    90  	}
    91  	return rules, nil
    92  }
    93  
    94  func getLockFileFormat(filename string) lockFileFormat {
    95  	switch filepath.Base(filename) {
    96  	case "Gopkg.lock":
    97  		return depFormat
    98  	default:
    99  		return unknownFormat
   100  	}
   101  }
   102  
   103  // GenerateRule returns a repository rule for the given repository that can
   104  // be written in a WORKSPACE file.
   105  func GenerateRule(repo Repo) bf.Expr {
   106  	attrs := []rules.KeyValue{
   107  		{Key: "name", Value: repo.Name},
   108  		{Key: "commit", Value: repo.Commit},
   109  		{Key: "importpath", Value: repo.GoPrefix},
   110  	}
   111  	if repo.Remote != "" {
   112  		attrs = append(attrs, rules.KeyValue{Key: "remote", Value: repo.Remote})
   113  	}
   114  	if repo.VCS != "" {
   115  		attrs = append(attrs, rules.KeyValue{Key: "vcs", Value: repo.VCS})
   116  	}
   117  	return rules.NewRule("go_repository", attrs)
   118  }
   119  
   120  // FindExternalRepo attempts to locate the directory where Bazel has fetched
   121  // the external repository with the given name. An error is returned if the
   122  // repository directory cannot be located.
   123  func FindExternalRepo(repoRoot, name string) (string, error) {
   124  	// See https://docs.bazel.build/versions/master/output_directories.html
   125  	// for documentation on Bazel directory layout.
   126  	// We expect the bazel-out symlink in the workspace root directory to point to
   127  	// <output-base>/execroot/<workspace-name>/bazel-out
   128  	// We expect the external repository to be checked out at
   129  	// <output-base>/external/<name>
   130  	// Note that users can change the prefix for most of the Bazel symlinks with
   131  	// --symlink_prefix, but this does not include bazel-out.
   132  	externalPath := strings.Join([]string{repoRoot, "bazel-out", "..", "..", "..", "external", name}, string(os.PathSeparator))
   133  	cleanPath, err := filepath.EvalSymlinks(externalPath)
   134  	if err != nil {
   135  		return "", err
   136  	}
   137  	st, err := os.Stat(cleanPath)
   138  	if err != nil {
   139  		return "", err
   140  	}
   141  	if !st.IsDir() {
   142  		return "", fmt.Errorf("%s: not a directory", externalPath)
   143  	}
   144  	return cleanPath, nil
   145  }
   146  
   147  // ListRepositories extracts metadata about repositories declared in a
   148  // WORKSPACE file.
   149  //
   150  // The set of repositories returned is necessarily incomplete, since we don't
   151  // evaluate the file, and repositories may be declared in macros in other files.
   152  func ListRepositories(workspace *bf.File) []Repo {
   153  	var repos []Repo
   154  	for _, e := range workspace.Stmt {
   155  		call, ok := e.(*bf.CallExpr)
   156  		if !ok {
   157  			continue
   158  		}
   159  		r := bf.Rule{Call: call}
   160  		name := r.Name()
   161  		if name == "" {
   162  			continue
   163  		}
   164  		var repo Repo
   165  		switch r.Kind() {
   166  		case "go_repository":
   167  			// TODO(jayconrod): extract other fields needed by go_repository.
   168  			// Currently, we don't use the result of this function to produce new
   169  			// go_repository rules, so it doesn't matter.
   170  			goPrefix := r.AttrString("importpath")
   171  			revision := r.AttrString("commit")
   172  			remote := r.AttrString("remote")
   173  			vcs := r.AttrString("vcs")
   174  			if goPrefix == "" {
   175  				continue
   176  			}
   177  			repo = Repo{
   178  				Name:     name,
   179  				GoPrefix: goPrefix,
   180  				Commit:   revision,
   181  				Remote:   remote,
   182  				VCS:      vcs,
   183  			}
   184  
   185  			// TODO(jayconrod): infer from {new_,}git_repository, {new_,}http_archive,
   186  			// local_repository.
   187  
   188  		default:
   189  			continue
   190  		}
   191  		repos = append(repos, repo)
   192  	}
   193  
   194  	// TODO(jayconrod): look for directives that describe repositories that
   195  	// aren't declared in the top-level of WORKSPACE (e.g., behind a macro).
   196  
   197  	return repos
   198  }