Repository: bazelbuild/bazelisk
Pull request #299 (2022-02-07 16:36:02)

feat(platforms): fallback to x86_64 before bazel version 4.1.0 on App…
like #272 , add support to Apple Silicon.

fallback to x86_64 before bazel version 4.1.0 on Apple Silicon

xinnjie: /cla
xinnjie: ping @philwo 
xinnjie: ping again @philwo
philwo: This LGTM to me, @fweikert could you have a look and merge if you're happy, too?

Patch:
diff --git a/bazelisk.py b/bazelisk.py
index b6fab96..0e94323 100755
--- a/bazelisk.py
+++ b/bazelisk.py
@@ -18,6 +18,7 @@ limitations under the License.
 import base64
 from contextlib import closing
 from distutils.version import LooseVersion
+import hashlib
 import json
 import netrc
 import os
@@ -33,11 +34,14 @@ import time
 try:
     from urllib.parse import urlparse
     from urllib.request import urlopen, Request
+    from urllib.error import HTTPError
 except ImportError:
     # Python 2.x compatibility hack.
     # http://python-future.org/compatible_idioms.html?highlight=urllib#urllib-module
     from urlparse import urlparse
-    from urllib2 import urlopen, Request
+    from urllib2 import urlopen, Request, HTTPError
+
+    FileNotFoundError = IOError
 
 ONE_HOUR = 1 * 60 * 60
 
@@ -206,16 +210,16 @@ def determine_executable_filename_suffix():
 
 
 def determine_bazel_filename(version):
+    operating_system = get_operating_system()
+    supported_machines = get_supported_machine_archs(version, operating_system)
     machine = normalized_machine_arch_name()
-    if machine != "x86_64":
+    if machine not in supported_machines:
         raise Exception(
-            'Unsupported machine architecture "{}". Bazel currently only supports x86_64.'.format(
-                machine
+            'Unsupported machine architecture "{}". Bazel {} only supports {} on {}.'.format(
+                machine, version, ", ".join(supported_machines), operating_system.capitalize()
             )
         )
 
-    operating_system = get_operating_system()
-
     filename_suffix = determine_executable_filename_suffix()
     bazel_flavor = "bazel"
     if os.environ.get("BAZELISK_NOJDK", "0") != "0":
@@ -223,6 +227,31 @@ def determine_bazel_filename(version):
     return "{}-{}-{}-{}{}".format(bazel_flavor, version, operating_system, machine, filename_suffix)
 
 
+def get_supported_machine_archs(version, operating_system):
+    supported_machines = ["x86_64"]
+    versions = version.split(".")[:2]
+    if len(versions) == 2:
+        # released version
+        major, minor = int(versions[0]), int(versions[1])
+        if (
+            operating_system == "darwin"
+            and (major > 4 or major == 4 and minor >= 1)
+            or operating_system == "linux"
+            and (major > 3 or major == 3 and minor >= 4)
+        ):
+            # Linux arm64 was supported since 3.4.0.
+            # Darwin arm64 was supported since 4.1.0.
+            supported_machines.append("arm64")
+    elif operating_system in ("darwin", "linux"):
+        # This is needed to run bazelisk_test.sh on Linux and Darwin arm64 machines, which are
+        # becoming more and more popular.
+        # It works because all recent commits of Bazel support arm64 on Darwin and Linux.
+        # However, this would add arm64 by mistake if the commit is too old, which should be
+        # a rare scenario.
+        supported_machines.append("arm64")
+    return supported_machines
+
+
 def normalized_machine_arch_name():
     machine = platform.machine().lower()
     if machine == "amd64":
@@ -259,7 +288,7 @@ def trim_suffix(string, suffix):
 
 def download_bazel_into_directory(version, is_commit, directory):
     bazel_filename = determine_bazel_filename(version)
-    url = determine_url(version, is_commit, bazel_filename)
+    bazel_url = determine_url(version, is_commit, bazel_filename)
 
     filename_suffix = determine_executable_filename_suffix()
     bazel_directory_name = trim_suffix(bazel_filename, filename_suffix)
@@ -268,30 +297,59 @@ def download_bazel_into_directory(version, is_commit, directory):
 
     destination_path = os.path.join(destination_dir, "bazel" + filename_suffix)
     if not os.path.exists(destination_path):
-        sys.stderr.write("Downloading {}...\n".format(url))
-        with tempfile.NamedTemporaryFile(prefix="bazelisk", dir=destination_dir, delete=False) as t:
-            # https://github.com/bazelbuild/bazelisk/issues/247
-            request = Request(url)
-            if "BAZELISK_BASE_URL" in os.environ:
-                parts = urlparse(url)
-                creds = None
-                try:
-                    creds = netrc.netrc().hosts.get(parts.netloc)
-                except:
-                    pass
-                if creds is not None:
-                    auth = base64.b64encode(("%s:%s" % (creds[0], creds[2])).encode("ascii"))
-                    request.add_header("Authorization", "Basic %s" % auth.decode("utf-8"))
-            with closing(urlopen(request)) as response:
-                shutil.copyfileobj(response, t)
-            t.flush()
-            os.fsync(t.fileno())
-        os.rename(t.name, destination_path)
+        download(bazel_url, destination_path)
         os.chmod(destination_path, 0o755)
 
+    sha256_path = destination_path + ".sha256"
+    expected_hash = ""
+    if not os.path.exists(sha256_path):
+        try:
+            download(bazel_url + ".sha256", sha256_path)
+        except HTTPError as e:
+            if e.code == 404:
+                sys.stderr.write(
+                    "The Bazel mirror does not have a checksum file; skipping checksum verification."
+                )
+                return destination_path
+            raise e
+    with open(sha256_path, "r") as sha_file:
+        expected_hash = sha_file.read().split()[0]
+    sha256_hash = hashlib.sha256()
+    with open(destination_path, "rb") as bazel_file:
+        for byte_block in iter(lambda: bazel_file.read(4096), b""):
+            sha256_hash.update(byte_block)
+    actual_hash = sha256_hash.hexdigest()
+    if actual_hash != expected_hash:
+        os.remove(destination_path)
+        os.remove(sha256_path)
+        print(
+            "The downloaded Bazel binary is corrupted. Expected SHA-256 {}, got {}. Please try again.".format(
+                expected_hash, actual_hash
+            )
+        )
+        # Exiting with a special exit code not used by Bazel, so the calling process may retry based on that.
+        # https://docs.bazel.build/versions/0.21.0/guide.html#what-exit-code-will-i-get
+        sys.exit(22)
     return destination_path
 
 
+def download(url, destination_path):
+    sys.stderr.write("Downloading {}...\n".format(url))
+    request = Request(url)
+    if "BAZELISK_BASE_URL" in os.environ:
+        parts = urlparse(url)
+        creds = None
+        try:
+            creds = netrc.netrc().hosts.get(parts.netloc)
+        except Exception:
+            pass
+        if creds is not None:
+            auth = base64.b64encode(("%s:%s" % (creds[0], creds[2])).encode("ascii"))
+            request.add_header("Authorization", "Basic %s" % auth.decode("utf-8"))
+    with closing(urlopen(request)) as response, open(destination_path, "wb") as file:
+        shutil.copyfileobj(response, file)
+
+
 def get_bazelisk_directory():
     bazelisk_home = os.environ.get("BAZELISK_HOME")
     if bazelisk_home is not None:
diff --git a/platforms/BUILD b/platforms/BUILD
index 52cc5ed..6b8f925 100644
--- a/platforms/BUILD
+++ b/platforms/BUILD
@@ -1,8 +1,17 @@
-load("@io_bazel_rules_go//go:def.bzl", "go_library")
+load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
 
 go_library(
     name = "go_default_library",
     srcs = ["platforms.go"],
     importpath = "github.com/bazelbuild/bazelisk/platforms",
     visibility = ["//visibility:public"],
+    deps = [
+        "@com_github_hashicorp_go_version//:go_default_library",
+    ],
+)
+
+go_test(
+    name = "platforms_test",
+    srcs = ["platforms_test.go"],
+    embed = [":go_default_library"],
 )
diff --git a/platforms/platforms.go b/platforms/platforms.go
index 14efc07..8e51ec7 100644
--- a/platforms/platforms.go
+++ b/platforms/platforms.go
@@ -3,6 +3,8 @@ package platforms
 
 import (
 	"fmt"
+	semver "github.com/hashicorp/go-version"
+	"log"
 	"runtime"
 )
 
@@ -43,6 +45,10 @@ func DetermineBazelFilename(version string, includeSuffix bool) (string, error)
 		return "", fmt.Errorf("unsupported operating system \"%s\", must be Linux, macOS or Windows", runtime.GOOS)
 	}
 
+	if osName == "darwin" {
+		machineName = DarwinFallback(machineName, version)
+	}
+
 	var filenameSuffix string
 	if includeSuffix {
 		filenameSuffix = DetermineExecutableFilenameSuffix()
@@ -50,3 +56,19 @@ func DetermineBazelFilename(version string, includeSuffix bool) (string, error)
 
 	return fmt.Sprintf("bazel-%s-%s-%s%s", version, osName, machineName, filenameSuffix), nil
 }
+
+// DarwinFallback Darwin arm64 was supported since 4.1.0, before 4.1.0, fall back to x86_64
+func DarwinFallback(machineName string, version string) (alterMachineName string) {
+	v, err := semver.NewVersion(version)
+	if err != nil {
+		return machineName
+	}
+
+	armSupportVer, _ := semver.NewVersion("4.1.0")
+
+	if machineName == "arm64" && v.LessThan(armSupportVer) {
+		log.Printf("WARN: Fallback to x86_64 because arm64 is not supported on Apple Silicon until 4.1.0")
+		return "x86_64"
+	}
+	return machineName
+}


Test patch:
diff --git a/bazelisk_test.sh b/bazelisk_test.sh
index 9002979..09733ba 100755
--- a/bazelisk_test.sh
+++ b/bazelisk_test.sh
@@ -108,12 +108,12 @@ function test_bazel_version_go() {
 function test_bazel_version_from_environment() {
   setup
 
-  USE_BAZEL_VERSION="0.20.0" \
+  USE_BAZEL_VERSION="5.0.0" \
       BAZELISK_HOME="$BAZELISK_HOME" \
       bazelisk version 2>&1 | tee log
 
-  grep "Build label: 0.20.0" log || \
-      (echo "FAIL: Expected to find 'Build label: 0.20.0' in the output of 'bazelisk version'"; exit 1)
+  grep "Build label: 5.0.0" log || \
+      (echo "FAIL: Expected to find 'Build label: 5.0.0' in the output of 'bazelisk version'"; exit 1)
 }
 
 function test_bazel_version_prefer_environment_to_bazeliskrc() {
@@ -157,13 +157,13 @@ function test_bazel_version_prefer_bazeliskrc_to_bazelversion_file() {
 function test_bazel_version_from_file() {
   setup
 
-  echo "0.19.0" > .bazelversion
+  echo "5.0.0" > .bazelversion
 
   BAZELISK_HOME="$BAZELISK_HOME" \
       bazelisk version 2>&1 | tee log
 
-  grep "Build label: 0.19.0" log || \
-      (echo "FAIL: Expected to find 'Build label: 0.19.0' in the output of 'bazelisk version'"; exit 1)
+  grep "Build label: 5.0.0" log || \
+      (echo "FAIL: Expected to find 'Build label: 5.0.0' in the output of 'bazelisk version'"; exit 1)
 }
 
 function test_bazel_version_from_url() {
diff --git a/platforms/platforms_test.go b/platforms/platforms_test.go
new file mode 100644
index 0000000..7b09f10
--- /dev/null
+++ b/platforms/platforms_test.go
@@ -0,0 +1,56 @@
+package platforms
+
+import "testing"
+
+func TestDarwinFallback(t *testing.T) {
+	type args struct {
+		machineName string
+		version     string
+	}
+	tests := []struct {
+		name                 string
+		args                 args
+		wantAlterMachineName string
+	}{
+		{
+			name: "before 4.1.0, x86_64 do not fallback",
+			args: args{
+				machineName: "x86_64",
+				version:     "4.0.1",
+			},
+			wantAlterMachineName: "x86_64",
+		},
+		{
+			name: "since 4.1.0, x86_64 do not fallback either",
+			args: args{
+				machineName: "x86_64",
+				version:     "4.1.0",
+			},
+			wantAlterMachineName: "x86_64",
+		},
+		{
+			name: "before 4.1.0, arm64 not supported, fallback to x86_64 on arm64",
+			args: args{
+				machineName: "arm64",
+				version:     "4.0.1",
+			},
+			wantAlterMachineName: "x86_64",
+		},
+		{
+			name: "since 4.1.0, arm64 supported, do not fallback",
+			args: args{
+				machineName: "arm64",
+				version:     "4.1.0",
+			},
+			wantAlterMachineName: "arm64",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			gotAlterMachineName := DarwinFallback(tt.args.machineName, tt.args.version)
+			if gotAlterMachineName != tt.wantAlterMachineName {
+				t.Errorf("DarwinFallback() gotAlterMachineName = %v, want %v", gotAlterMachineName, tt.wantAlterMachineName)
+			}
+		})
+	}
+}