blob: 53d3b7b6cc19ddf5881caf95654304b7ae9cf365 [file] [log] [blame] [edit]
/* Copyright 2018 The Bazel Authors. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package repo
import (
"bytes"
"encoding/json"
"io"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"regexp"
"runtime"
"strings"
"github.com/bazelbuild/bazel-gazelle/label"
)
type module struct {
Path, Version string
Main bool
}
// Per the `go help modules` documentation:
// There are three pseudo-version forms:
//
// vX.0.0-yyyymmddhhmmss-abcdefabcdef is used when there is no earlier
// versioned commit with an appropriate major version before the target commit.
// (This was originally the only form, so some older go.mod files use this form
// even for commits that do follow tags.)
//
// vX.Y.Z-pre.0.yyyymmddhhmmss-abcdefabcdef is used when the most
// recent versioned commit before the target commit is vX.Y.Z-pre.
//
// vX.Y.(Z+1)-0.yyyymmddhhmmss-abcdefabcdef is used when the most
// recent versioned commit before the target commit is vX.Y.Z.
//
// We need to match all three of these with the following regexp.
var regexMixedVersioning = regexp.MustCompile(`^(.*?)[-.]((?:0\.|)[0-9]{14})-([a-fA-F0-9]{12})$`)
func toRepoRule(mod module) Repo {
var tag, commit string
if gr := regexMixedVersioning.FindStringSubmatch(mod.Version); gr != nil {
commit = gr[3]
} else {
tag = strings.TrimSuffix(mod.Version, "+incompatible")
}
return Repo{
Name: label.ImportPathToBazelRepoName(mod.Path),
GoPrefix: mod.Path,
Commit: commit,
Tag: tag,
}
}
func importRepoRulesModules(filename string, _ *RemoteCache) (repos []Repo, err error) {
tempDir, err := copyGoModToTemp(filename)
if err != nil {
return nil, err
}
defer os.RemoveAll(tempDir)
data, err := goListModulesFn(tempDir)
if err != nil {
return nil, err
}
dec := json.NewDecoder(bytes.NewReader(data))
for dec.More() {
var mod module
if err := dec.Decode(&mod); err != nil {
return nil, err
}
if mod.Main {
continue
}
repos = append(repos, toRepoRule(mod))
}
return repos, nil
}
// goListModulesFn may be overridden by tests.
var goListModulesFn = goListModules
// goListModules invokes "go list" in a directory containing a go.mod file.
func goListModules(dir string) ([]byte, error) {
goTool := findGoTool()
cmd := exec.Command(goTool, "list", "-m", "-json", "all")
cmd.Stderr = os.Stderr
cmd.Dir = dir
data, err := cmd.Output()
return data, err
}
// copyGoModToTemp copies to given go.mod file to a temporary directory.
// go list tends to mutate go.mod files, but gazelle shouldn't do that.
func copyGoModToTemp(filename string) (tempDir string, err error) {
goModOrig, err := os.Open(filename)
if err != nil {
return "", err
}
defer goModOrig.Close()
tempDir, err = ioutil.TempDir("", "gazelle-temp-gomod")
if err != nil {
return "", err
}
goModCopy, err := os.Create(filepath.Join(tempDir, "go.mod"))
if err != nil {
os.Remove(tempDir)
return "", err
}
defer func() {
if cerr := goModCopy.Close(); err == nil && cerr != nil {
err = cerr
}
}()
_, err = io.Copy(goModCopy, goModOrig)
if err != nil {
os.RemoveAll(tempDir)
return "", err
}
return tempDir, err
}
// findGoTool attempts to locate the go executable. If GOROOT is set, we'll
// prefer the one in there; otherwise, we'll rely on PATH. If the wrapper
// script generated by the gazelle rule is invoked by Bazel, it will set
// GOROOT to the configured SDK. We don't want to rely on the host SDK in
// that situation.
func findGoTool() string {
path := "go" // rely on PATH by default
if goroot, ok := os.LookupEnv("GOROOT"); ok {
path = filepath.Join(goroot, "bin", "go")
}
if runtime.GOOS == "windows" {
path += ".exe"
}
return path
}