Skip to content

Commit

Permalink
Add an implementation of Bazelisk in Go.
Browse files Browse the repository at this point in the history
It has feature parity with the Python version and is tested against the
same test suite.
  • Loading branch information
philwo committed Jan 20, 2019
1 parent 5dbd91a commit 38afa12
Show file tree
Hide file tree
Showing 7 changed files with 391 additions and 19 deletions.
15 changes: 10 additions & 5 deletions .bazelci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,26 @@
platforms:
ubuntu1404:
test_targets:
- //:bazelisk_test
- //:go_bazelisk_test
- //:py_bazelisk_test
ubuntu1604:
test_targets:
- //:bazelisk_test
- //:go_bazelisk_test
- //:py_bazelisk_test
ubuntu1804:
test_targets:
- //:bazelisk_test
- //:go_bazelisk_test
- //:py_bazelisk_test
macos:
test_targets:
- //:bazelisk_test
- //:go_bazelisk_test
- //:py_bazelisk_test
test_flags:
- --test_output=streamed
windows:
test_targets:
- //:bazelisk_test
- //:go_bazelisk_test
- //:py_bazelisk_test
test_flags:
- --flaky_test_attempts=1
- --test_env=PATH
Expand Down
30 changes: 26 additions & 4 deletions BUILD
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"],
)
33 changes: 33 additions & 0 deletions WORKSPACE
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",
)
293 changes: 293 additions & 0 deletions bazelisk.go
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)
}
Loading

0 comments on commit 38afa12

Please sign in to comment.