forked from bazelbuild/bazelisk
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add an implementation of Bazelisk in Go.
It has feature parity with the Python version and is tested against the same test suite.
- Loading branch information
Showing
7 changed files
with
391 additions
and
19 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,11 +1,33 @@ | ||
py_binary( | ||
name = "bazelisk", | ||
srcs = ["bazelisk.py"], | ||
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") | ||
load("@bazel_gazelle//:def.bzl", "gazelle") | ||
|
||
# gazelle:prefix github.com/philwo/bazelisk | ||
gazelle(name = "gazelle") | ||
|
||
sh_test( | ||
name = "py_bazelisk_test", | ||
srcs = ["bazelisk_test.sh"], | ||
data = ["bazelisk.py"], | ||
deps = ["@bazel_tools//tools/bash/runfiles"], | ||
) | ||
|
||
sh_test( | ||
name = "bazelisk_test", | ||
name = "go_bazelisk_test", | ||
srcs = ["bazelisk_test.sh"], | ||
data = [":bazelisk"], | ||
deps = ["@bazel_tools//tools/bash/runfiles"], | ||
) | ||
|
||
go_library( | ||
name = "go_default_library", | ||
srcs = ["bazelisk.go"], | ||
importpath = "github.com/philwo/bazelisk", | ||
visibility = ["//visibility:private"], | ||
deps = ["@com_github_hashicorp_go_version//:go_default_library"], | ||
) | ||
|
||
go_binary( | ||
name = "bazelisk", | ||
embed = [":go_default_library"], | ||
visibility = ["//visibility:public"], | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") | ||
|
||
# Download the Go rules. | ||
http_archive( | ||
name = "io_bazel_rules_go", | ||
urls = ["https://github.com/bazelbuild/rules_go/releases/download/0.16.5/rules_go-0.16.5.tar.gz"], | ||
sha256 = "7be7dc01f1e0afdba6c8eb2b43d2fa01c743be1b9273ab1eaf6c233df078d705", | ||
) | ||
|
||
# Load and call the dependencies. | ||
load("@io_bazel_rules_go//go:def.bzl", "go_rules_dependencies", "go_register_toolchains") | ||
|
||
go_rules_dependencies() | ||
|
||
go_register_toolchains() | ||
|
||
# Download Gazelle. | ||
http_archive( | ||
name = "bazel_gazelle", | ||
urls = ["https://github.com/bazelbuild/bazel-gazelle/releases/download/0.16.0/bazel-gazelle-0.16.0.tar.gz"], | ||
sha256 = "7949fc6cc17b5b191103e97481cf8889217263acf52e00b560683413af204fcb", | ||
) | ||
|
||
# Load and call Gazelle dependencies. | ||
load("@bazel_gazelle//:deps.bzl", "gazelle_dependencies", "go_repository") | ||
|
||
gazelle_dependencies() | ||
|
||
go_repository( | ||
name = "com_github_hashicorp_go_version", | ||
importpath = "github.com/hashicorp/go-version", | ||
tag = "v1.1.0", | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,293 @@ | ||
package main | ||
|
||
import ( | ||
"bufio" | ||
"encoding/json" | ||
"fmt" | ||
"io" | ||
"io/ioutil" | ||
"log" | ||
"net/http" | ||
"os" | ||
"os/exec" | ||
"os/signal" | ||
"path" | ||
"runtime" | ||
"sort" | ||
"strings" | ||
"syscall" | ||
"time" | ||
|
||
version "github.com/hashicorp/go-version" | ||
) | ||
|
||
func findWorkspaceRoot(root string) string { | ||
if _, err := os.Stat(path.Join(root, "WORKSPACE")); err == nil { | ||
return root | ||
} | ||
|
||
parentDirectory := path.Dir(root) | ||
if parentDirectory == root { | ||
return "" | ||
} | ||
|
||
return findWorkspaceRoot(parentDirectory) | ||
} | ||
|
||
func getBazelVersion() string { | ||
// Check in this order: | ||
// - env var "USE_BAZEL_VERSION" is set to a specific version. | ||
// - env var "USE_NIGHTLY_BAZEL" or "USE_BAZEL_NIGHTLY" is set -> latest | ||
// nightly. (TODO) | ||
// - env var "USE_CANARY_BAZEL" or "USE_BAZEL_CANARY" is set -> latest | ||
// rc. (TODO) | ||
// - the file workspace_root/tools/bazel exists -> that version. (TODO) | ||
// - workspace_root/.bazelversion exists -> read contents, that version. | ||
// - workspace_root/WORKSPACE contains a version -> that version. (TODO) | ||
// - fallback: latest release | ||
bazelVersion := os.Getenv("USE_BAZEL_VERSION") | ||
if len(bazelVersion) != 0 { | ||
return bazelVersion | ||
} | ||
|
||
workingDirectory, err := os.Getwd() | ||
if err != nil { | ||
log.Fatalf("Could not get working directory: %v", err) | ||
} | ||
|
||
workspaceRoot := findWorkspaceRoot(workingDirectory) | ||
if len(workspaceRoot) != 0 { | ||
bazelVersionPath := path.Join(workspaceRoot, ".bazelversion") | ||
if _, err := os.Stat(bazelVersionPath); err == nil { | ||
f, err := os.Open(bazelVersionPath) | ||
if err != nil { | ||
log.Fatalf("Could not read %s: %v", bazelVersionPath, err) | ||
} | ||
defer f.Close() | ||
|
||
scanner := bufio.NewScanner(f) | ||
scanner.Scan() | ||
bazelVersion := scanner.Text() | ||
if err := scanner.Err(); err != nil { | ||
log.Fatalf("Could not read version from file %s: %v", bazelVersion, err) | ||
} | ||
|
||
return bazelVersion | ||
} | ||
} | ||
|
||
return "latest" | ||
} | ||
|
||
type release struct { | ||
TagName string `json:"tag_name"` | ||
Prerelease bool `json:"prerelease"` | ||
} | ||
|
||
func resolveLatestVersion() string { | ||
// We could also use go-github here, but I can't get it to build with Bazel's rules_go and it pulls in a lot of dependencies. | ||
res, err := http.Get("https://api.github.com/repos/bazelbuild/bazel/releases") | ||
if err != nil { | ||
log.Fatalf("Could not fetch list of Bazel releases from GitHub: %v", err) | ||
} | ||
defer res.Body.Close() | ||
|
||
body, err := ioutil.ReadAll(res.Body) | ||
if err != nil { | ||
log.Fatalf("Could not read list of Bazel releases from GitHub: %v", err) | ||
} | ||
|
||
if res.StatusCode != 200 { | ||
log.Fatalf("Unexpected status code while reading list of Bazel releases from GitHub: %v", res.StatusCode) | ||
} | ||
|
||
var releases []release | ||
err = json.Unmarshal(body, &releases) | ||
if err != nil { | ||
log.Fatalf("Could not parse JSON into list of releases: %v", err) | ||
} | ||
|
||
var versions []*version.Version | ||
for _, release := range releases { | ||
if release.Prerelease { | ||
continue | ||
} | ||
v, err := version.NewVersion(release.TagName) | ||
if err != nil { | ||
log.Printf("WARN: Could not parse version: %s", release.TagName) | ||
} | ||
versions = append(versions, v) | ||
} | ||
sort.Sort(version.Collection(versions)) | ||
return versions[len(versions)-1].Original() | ||
} | ||
|
||
func resolveVersionLabel(bazeliskHome, bazelVersion string) string { | ||
if bazelVersion != "latest" { | ||
return bazelVersion | ||
} | ||
|
||
latestCache := path.Join(bazeliskHome, "latest_bazel") | ||
if cacheStat, err := os.Stat(latestCache); err == nil { | ||
if time.Since(cacheStat.ModTime()).Hours() < 1 { | ||
f, err := os.Open(latestCache) | ||
if err != nil { | ||
log.Fatalf("Could not read %s: %v", latestCache, err) | ||
} | ||
defer f.Close() | ||
|
||
scanner := bufio.NewScanner(f) | ||
scanner.Scan() | ||
latestVersion := scanner.Text() | ||
if err := scanner.Err(); err != nil { | ||
log.Fatalf("Could not read latest version from file %s: %v", latestCache, err) | ||
} | ||
|
||
return latestVersion | ||
} | ||
} | ||
|
||
latestVersion := resolveLatestVersion() | ||
f, err := os.Create(latestCache) | ||
if err != nil { | ||
log.Fatalf("Could not create %s: %v", latestCache, err) | ||
} | ||
defer f.Close() | ||
if _, err := f.WriteString(latestVersion); err != nil { | ||
log.Fatalf("Could not write latest version to %s: %v", latestCache, err) | ||
} | ||
|
||
return latestVersion | ||
} | ||
|
||
func determineBazelFilename(version string) string { | ||
var machineName string | ||
switch runtime.GOARCH { | ||
case "amd64": | ||
machineName = "x86_64" | ||
default: | ||
log.Fatalf("Unsupported machine architecture \"%s\". Bazel currently only supports x86_64.", runtime.GOARCH) | ||
} | ||
|
||
var osName string | ||
switch runtime.GOOS { | ||
case "darwin", "linux", "windows": | ||
osName = runtime.GOOS | ||
default: | ||
log.Fatalf("Unsupported operating system \"%s\". Bazel currently only supports Linux, macOS and Windows.", runtime.GOOS) | ||
} | ||
|
||
filenameSuffix := "" | ||
if runtime.GOOS == "windows" { | ||
filenameSuffix = ".exe" | ||
} | ||
|
||
return fmt.Sprintf("bazel-%s-%s-%s%s", version, osName, machineName, filenameSuffix) | ||
} | ||
|
||
func determineURL(version, filename string) string { | ||
kind := "release" | ||
if strings.Contains(version, "rc") { | ||
kind = strings.SplitAfter(version, "rc")[1] | ||
} | ||
|
||
return fmt.Sprintf("https://releases.bazel.build/%s/%s/%s", version, kind, filename) | ||
} | ||
|
||
func downloadBazel(version, directory string) string { | ||
filename := determineBazelFilename(version) | ||
url := determineURL(version, filename) | ||
destinationPath := path.Join(directory, filename) | ||
|
||
if _, err := os.Stat(destinationPath); err != nil { | ||
log.Printf("Downloading %s...", url) | ||
f, err := os.Create(destinationPath) | ||
if err != nil { | ||
log.Fatalf("Could not create %s: %v", destinationPath, err) | ||
} | ||
defer f.Close() | ||
|
||
resp, err := http.Get(url) | ||
if err != nil { | ||
log.Fatalf("Could not download %s: %v", url, err) | ||
} | ||
defer resp.Body.Close() | ||
|
||
_, err = io.Copy(f, resp.Body) | ||
if err != nil { | ||
log.Fatalf("Could not download %s: %v", url, err) | ||
} | ||
} | ||
|
||
os.Chmod(destinationPath, 0755) | ||
return destinationPath | ||
} | ||
|
||
func runBazel(bazel string, args []string) int { | ||
cmd := exec.Command(bazel, args...) | ||
cmd.Stdin = os.Stdin | ||
cmd.Stdout = os.Stdout | ||
cmd.Stderr = os.Stderr | ||
err := cmd.Start() | ||
if err != nil { | ||
log.Fatalf("Could not start Bazel: %v", err) | ||
} | ||
|
||
c := make(chan os.Signal) | ||
signal.Notify(c, os.Interrupt, syscall.SIGTERM) | ||
go func() { | ||
s := <-c | ||
if runtime.GOOS != "windows" { | ||
cmd.Process.Signal(s) | ||
} else { | ||
cmd.Process.Kill() | ||
} | ||
}() | ||
|
||
err = cmd.Wait() | ||
if err != nil { | ||
if exitError, ok := err.(*exec.ExitError); ok { | ||
waitStatus := exitError.Sys().(syscall.WaitStatus) | ||
return waitStatus.ExitStatus() | ||
} | ||
log.Fatalf("Could not launch Bazel: %v", err) | ||
} | ||
return 0 | ||
} | ||
|
||
func main() { | ||
bazeliskHome := os.Getenv("BAZELISK_HOME") | ||
if len(bazeliskHome) == 0 { | ||
userCacheDir, err := os.UserCacheDir() | ||
if err != nil { | ||
log.Fatalf("Could not get the user's cache directory: %v", err) | ||
} | ||
|
||
bazeliskHome = path.Join(userCacheDir, "bazelisk") | ||
} | ||
|
||
err := os.MkdirAll(bazeliskHome, 0755) | ||
if err != nil { | ||
log.Fatalf("Could not create directory %s: %v", bazeliskHome, err) | ||
} | ||
|
||
bazelVersion := getBazelVersion() | ||
resolvedBazelVersion := resolveVersionLabel(bazeliskHome, bazelVersion) | ||
if err != nil { | ||
log.Fatalf("Could not resolve the version '%s' to an actual version number: %v", bazelVersion, err) | ||
} | ||
|
||
bazelDirectory := path.Join(bazeliskHome, "bin") | ||
err = os.MkdirAll(bazelDirectory, 0755) | ||
if err != nil { | ||
log.Fatalf("Could not create directory %s: %v", bazelDirectory, err) | ||
} | ||
|
||
bazelPath := downloadBazel(resolvedBazelVersion, bazelDirectory) | ||
if err != nil { | ||
log.Fatalf("Could not download Bazel: %v", err) | ||
} | ||
|
||
exitCode := runBazel(bazelPath, os.Args[1:]) | ||
os.Exit(exitCode) | ||
} |
Oops, something went wrong.