Skip to content

Commit

Permalink
bazelisk.go: Correctly handle failed downloads. (bazelbuild#24)
Browse files Browse the repository at this point in the history
* bazelisk.go: Correctly handle failed downloads.

Progress towards bazelbuild#2.

* Fix error handling
  • Loading branch information
philwo authored Feb 1, 2019
1 parent 38961f1 commit a81fc5c
Showing 1 changed file with 85 additions and 52 deletions.
137 changes: 85 additions & 52 deletions bazelisk.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ func findWorkspaceRoot(root string) string {
return findWorkspaceRoot(parentDirectory)
}

func getBazelVersion() string {
func getBazelVersion() (string, error) {
// 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
Expand All @@ -49,12 +49,12 @@ func getBazelVersion() string {
// - fallback: latest release
bazelVersion := os.Getenv("USE_BAZEL_VERSION")
if len(bazelVersion) != 0 {
return bazelVersion
return bazelVersion, nil
}

workingDirectory, err := os.Getwd()
if err != nil {
log.Fatalf("Could not get working directory: %v", err)
return "", fmt.Errorf("could not get working directory: %v", err)
}

workspaceRoot := findWorkspaceRoot(workingDirectory)
Expand All @@ -63,72 +63,75 @@ func getBazelVersion() string {
if _, err := os.Stat(bazelVersionPath); err == nil {
f, err := os.Open(bazelVersionPath)
if err != nil {
log.Fatalf("Could not read %s: %v", bazelVersionPath, err)
return "", fmt.Errorf("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 "", fmt.Errorf("could not read version from file %s: %v", bazelVersion, err)
}

return bazelVersion
return bazelVersion, nil
}
}

return "latest"
return "latest", nil
}

type release struct {
TagName string `json:"tag_name"`
Prerelease bool `json:"prerelease"`
}

func getReleasesJSON(bazeliskHome string) []byte {
func getReleasesJSON(bazeliskHome string) ([]byte, error) {
cachePath := path.Join(bazeliskHome, "releases.json")

if cacheStat, err := os.Stat(cachePath); err == nil {
if time.Since(cacheStat.ModTime()).Hours() < 1 {
res, err := ioutil.ReadFile(cachePath)
if err != nil {
log.Fatalf("Could not read %s: %v", cachePath, err)
return nil, fmt.Errorf("could not read %s: %v", cachePath, err)
}
return res
return res, nil
}
}

// 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)
return nil, fmt.Errorf("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)
return nil, fmt.Errorf("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)
return nil, fmt.Errorf("unexpected status code while reading list of Bazel releases from GitHub: %v", res.StatusCode)
}

err = ioutil.WriteFile(cachePath, body, 0666)
if err != nil {
log.Fatalf("Could not create %s: %v", cachePath, err)
return nil, fmt.Errorf("could not create %s: %v", cachePath, err)
}

return body
return body, nil
}

func resolveLatestVersion(bazeliskHome string, offset int) string {
releasesJSON := getReleasesJSON(bazeliskHome)
func resolveLatestVersion(bazeliskHome string, offset int) (string, error) {
releasesJSON, err := getReleasesJSON(bazeliskHome)
if err != nil {
return "", fmt.Errorf("could not get releases from GitHub: %v", err)
}

var releases []release
if err := json.Unmarshal(releasesJSON, &releases); err != nil {
log.Fatalf("Could not parse JSON into list of releases: %v", err)
return "", fmt.Errorf("could not parse JSON into list of releases: %v", err)
}

var versions []*version.Version
Expand All @@ -144,12 +147,12 @@ func resolveLatestVersion(bazeliskHome string, offset int) string {
}
sort.Sort(version.Collection(versions))
if offset >= len(versions) {
log.Fatalf("Cannot resolve version \"latest-%d\": There are only %d Bazel releases!", offset, len(versions))
return "", fmt.Errorf("cannot resolve version \"latest-%d\": There are only %d Bazel releases", offset, len(versions))
}
return versions[len(versions)-1-offset].Original()
return versions[len(versions)-1-offset].Original(), nil
}

func resolveVersionLabel(bazeliskHome, bazelVersion string) string {
func resolveVersionLabel(bazeliskHome, bazelVersion string) (string, error) {
r := regexp.MustCompile(`^latest(?:-(?P<offset>\d+))?$`)

match := r.FindStringSubmatch(bazelVersion)
Expand All @@ -159,38 +162,38 @@ func resolveVersionLabel(bazeliskHome, bazelVersion string) string {
var err error
offset, err = strconv.Atoi(match[1])
if err != nil {
log.Fatalf("Invalid version \"%s\", could not parse offset: %v", bazelVersion, err)
return "", fmt.Errorf("invalid version \"%s\", could not parse offset: %v", bazelVersion, err)
}
}
return resolveLatestVersion(bazeliskHome, offset)
}

return bazelVersion
return bazelVersion, nil
}

func determineBazelFilename(version string) string {
func determineBazelFilename(version string) (string, error) {
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)
return "", fmt.Errorf("unsupported machine architecture \"%s\", must be 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)
return "", fmt.Errorf("unsupported operating system \"%s\", must be Linux, macOS or Windows", runtime.GOOS)
}

filenameSuffix := ""
if runtime.GOOS == "windows" {
filenameSuffix = ".exe"
}

return fmt.Sprintf("bazel-%s-%s-%s%s", version, osName, machineName, filenameSuffix)
return fmt.Sprintf("bazel-%s-%s-%s%s", version, osName, machineName, filenameSuffix), nil
}

func determineURL(version, filename string) string {
Expand All @@ -202,43 +205,66 @@ func determineURL(version, filename string) string {
return fmt.Sprintf("https://releases.bazel.build/%s/%s/%s", version, kind, filename)
}

func downloadBazel(version, directory string) string {
filename := determineBazelFilename(version)
func downloadBazel(version, directory string) (string, error) {
filename, err := determineBazelFilename(version)
if err != nil {
return "", fmt.Errorf("could not determine filename to use for Bazel binary: %v", err)
}

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)
tmpfile, err := ioutil.TempFile(directory, "download")
if err != nil {
log.Fatalf("Could not create %s: %v", destinationPath, err)
return "", fmt.Errorf("could not create temporary file: %v", err)
}
defer f.Close()
defer func() {
err := tmpfile.Close()
if err == nil {
os.Remove(tmpfile.Name())
}
}()

log.Printf("Downloading %s...", url)
resp, err := http.Get(url)
if err != nil {
log.Fatalf("Could not download %s: %v", url, err)
return "", fmt.Errorf("HTTP GET %s failed: %v", url, err)
}
defer resp.Body.Close()

_, err = io.Copy(f, resp.Body)
if resp.StatusCode != 200 {
return "", fmt.Errorf("HTTP GET %s failed with error %v", url, resp.StatusCode)
}

_, err = io.Copy(tmpfile, resp.Body)
if err != nil {
log.Fatalf("Could not download %s: %v", url, err)
return "", fmt.Errorf("could not copy from %s to %s: %v", url, tmpfile.Name(), err)
}

err = os.Chmod(tmpfile.Name(), 0755)
if err != nil {
return "", fmt.Errorf("could not chmod file %s: %v", tmpfile.Name(), err)
}

tmpfile.Close()
err = os.Rename(tmpfile.Name(), destinationPath)
if err != nil {
return "", fmt.Errorf("could not move %s to %s: %v", tmpfile.Name(), destinationPath, err)
}
}

os.Chmod(destinationPath, 0755)
return destinationPath
return destinationPath, nil
}

func runBazel(bazel string, args []string) int {
func runBazel(bazel string, args []string) (int, error) {
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)
return 1, fmt.Errorf("could not start Bazel: %v", err)
}

c := make(chan os.Signal)
Expand All @@ -256,46 +282,53 @@ func runBazel(bazel string, args []string) int {
if err != nil {
if exitError, ok := err.(*exec.ExitError); ok {
waitStatus := exitError.Sys().(syscall.WaitStatus)
return waitStatus.ExitStatus()
return waitStatus.ExitStatus(), nil
}
log.Fatalf("Could not launch Bazel: %v", err)
return 1, fmt.Errorf("could not launch Bazel: %v", err)
}
return 0
return 0, nil
}

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)
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)
log.Fatalf("could not create directory %s: %v", bazeliskHome, err)
}

bazelVersion := getBazelVersion()
resolvedBazelVersion := resolveVersionLabel(bazeliskHome, bazelVersion)
bazelVersion, err := getBazelVersion()
if err != nil {
log.Fatalf("Could not resolve the version '%s' to an actual version number: %v", bazelVersion, err)
log.Fatalf("could not get Bazel version: %v", err)
}

resolvedBazelVersion, err := 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)
log.Fatalf("could not create directory %s: %v", bazelDirectory, err)
}

bazelPath := downloadBazel(resolvedBazelVersion, bazelDirectory)
bazelPath, err := downloadBazel(resolvedBazelVersion, bazelDirectory)
if err != nil {
log.Fatalf("Could not download Bazel: %v", err)
log.Fatalf("could not download Bazel: %v", err)
}

exitCode := runBazel(bazelPath, os.Args[1:])
exitCode, err := runBazel(bazelPath, os.Args[1:])
if err != nil {
log.Fatalf("could not run Bazel: %v", err)
}
os.Exit(exitCode)
}

0 comments on commit a81fc5c

Please sign in to comment.