Repository: bazelbuild/bazelisk
Pull request #345 (2022-08-08 15:27:09)

Allow any execute bits to count as executable
If the `tools/bazel` wrapper is executable by the user, but not everyone the current behavior is that `bazelisk` assumes it's not executable and silently pretends that the wrapper script doesn't exist, which was very confusing to me.

After this change, someone could run into the case where `bazelisk` thinks the user can execute the wrapper, but doesn't actually have permission. If that does happen, the error `could not run Bazel: could not start Bazel: fork/exec /repo/tools/bazel: permission denied` makes it pretty clear what's wrong.

This now matches how the Go standard library checks if a file is executable. https://cs.opensource.google/go/go/+/refs/tags/go1.19:src/os/exec/lp_unix.go;l=26


Patch:
diff --git a/.bazelversion b/.bazelversion
index ac14c3d..03f488b 100644
--- a/.bazelversion
+++ b/.bazelversion
@@ -1,1 +1,1 @@
-5.1.1
+5.3.0
diff --git a/BUILD b/BUILD
index 394ed85..eb7e9e1 100644
--- a/BUILD
+++ b/BUILD
@@ -193,6 +193,7 @@ genrule(
 
 pkg_npm(
     name = "npm_package",
+    package_name = "@bazel/bazelisk",
     srcs = [
         "LICENSE",
         "README.md",
diff --git a/README.md b/README.md
index f5838d5..7d4a3aa 100644
--- a/README.md
+++ b/README.md
@@ -16,8 +16,7 @@ On Windows: `choco install bazelisk`.
 
 Each adds bazelisk to the `PATH` as both `bazelisk` and `bazel`.
 
-Binary and source releases are also provided on our [Releases](https://github.com/bazelbuild/bazelisk/releases) page.
-You can download one of these and add it to your `PATH` manually.
+On Linux: You can download Bazelisk binary on our [Releases](https://github.com/bazelbuild/bazelisk/releases) page and add it to your `PATH` manually, which also works on macOS and Windows.
 
 Bazelisk is also published to npm.
 Frontend developers may want to install it with `npm install -g @bazel/bazelisk`.
@@ -113,6 +112,8 @@ bazelisk --strict build //...
 If the code fails with `--strict`, the flag `--migrate` will run Bazel with each one of the flag separately, and print a report at the end.
 This will show you which flags can safely enabled, and which flags require a migration.
 
+You can set `BAZELISK_INCOMPATIBLE_FLAGS` to set a list of incompatible flags (separated by `,`) to be tested, otherwise Bazelisk tests all flags starting with `--incompatible_`.
+
 You can set `BAZELISK_GITHUB_TOKEN` to set a GitHub access token to use for API requests to avoid rate limiting when on shared networks.
 
 You can set `BAZELISK_SHUTDOWN` to run `shutdown` between builds when migrating if you suspect this affects your results.
@@ -144,6 +145,7 @@ The following variables can be set:
 - `BAZELISK_CLEAN`
 - `BAZELISK_GITHUB_TOKEN`
 - `BAZELISK_HOME`
+- `BAZELISK_INCOMPATIBLE_FLAGS`
 - `BAZELISK_SHUTDOWN`
 - `BAZELISK_SKIP_WRAPPER`
 - `BAZELISK_USER_AGENT`
diff --git a/WORKSPACE b/WORKSPACE
index 8298330..089caf6 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -43,9 +43,11 @@ go_repository(
 )
 
 go_repository(
-    name = "com_github_jdxcode_netrc",
-    importpath = "github.com/jdxcode/netrc",
-    commit = "926c7f70242abe00179235c2b06bb647c0c53a12",
+    name = "com_github_bgentry_go_netrc",
+    importpath = "github.com/bgentry/go-netrc/netrc",
+    urls = ["https://github.com/bgentry/go-netrc/archive/9fd32a8.zip"],
+    type = "zip",
+    strip_prefix = "go-netrc-9fd32a8b3d3d3f9d43c341bfe098430e07609480/netrc",
 )
 
 go_rules_dependencies()
@@ -57,10 +59,14 @@ gazelle_dependencies()
 # We don't use any nodejs but this includes a rule for publishing releases to npm
 http_archive(
     name = "build_bazel_rules_nodejs",
-    sha256 = "84abf7ac4234a70924628baa9a73a5a5cbad944c4358cf9abdb4aab29c9a5b77",
-    urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/1.7.0/rules_nodejs-1.7.0.tar.gz"],
+    sha256 = "f10a3a12894fc3c9bf578ee5a5691769f6805c4be84359681a785a0c12e8d2b6",
+    urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/5.5.3/rules_nodejs-5.5.3.tar.gz"],
 )
 
+load("@build_bazel_rules_nodejs//:repositories.bzl", "build_bazel_rules_nodejs_dependencies")
+
+build_bazel_rules_nodejs_dependencies()
+
 load("@build_bazel_rules_nodejs//:index.bzl", "node_repositories")
 
 node_repositories()
diff --git a/bazelisk.py b/bazelisk.py
index 0e94323..0b8e953 100755
--- a/bazelisk.py
+++ b/bazelisk.py
@@ -17,7 +17,6 @@ limitations under the License.
 
 import base64
 from contextlib import closing
-from distutils.version import LooseVersion
 import hashlib
 import json
 import netrc
@@ -171,15 +170,18 @@ def read_remote_text_file(url):
 
 
 def get_version_history(bazelisk_directory):
-    ordered = sorted(
+    return sorted(
         (
-            LooseVersion(release["tag_name"])
+            release["tag_name"]
             for release in get_releases_json(bazelisk_directory)
             if not release["prerelease"]
         ),
+        # This only handles versions with numeric components, but that is fine
+        # since prerelease versions have been excluded.
+        key=lambda version: tuple(int(component)
+                                  for component in version.split('.')),
         reverse=True,
     )
-    return [str(v) for v in ordered]
 
 
 def resolve_latest_version(version_history, offset):
diff --git a/core/core.go b/core/core.go
index cbc5fd5..625e8b6 100644
--- a/core/core.go
+++ b/core/core.go
@@ -39,8 +39,18 @@ var (
 	fileConfigOnce sync.Once
 )
 
+// ArgsFunc is a function that receives a resolved Bazel version and returns the arguments to invoke
+// Bazel with.
+type ArgsFunc func(resolvedBazelVersion string) []string
+
 // RunBazelisk runs the main Bazelisk logic for the given arguments and Bazel repositories.
 func RunBazelisk(args []string, repos *Repositories) (int, error) {
+	return RunBazeliskWithArgsFunc(func(_ string) []string { return args }, repos)
+}
+
+// RunBazeliskWithArgsFunc runs the main Bazelisk logic for the given ArgsFunc and Bazel
+// repositories.
+func RunBazeliskWithArgsFunc(argsFunc ArgsFunc, repos *Repositories) (int, error) {
 	httputil.UserAgent = getUserAgent()
 
 	bazeliskHome := GetEnvOrConfig("BAZELISK_HOME")
@@ -100,10 +110,12 @@ func RunBazelisk(args []string, repos *Repositories) (int, error) {
 		baseDirectory := filepath.Join(bazeliskHome, "local")
 		bazelPath, err = linkLocalBazel(baseDirectory, bazelPath)
 		if err != nil {
-			return -1, fmt.Errorf("cound not link local Bazel: %v", err)
+			return -1, fmt.Errorf("could not link local Bazel: %v", err)
 		}
 	}
 
+	args := argsFunc(resolvedBazelVersion)
+
 	// --print_env must be the first argument.
 	if len(args) > 0 && args[0] == "--print_env" {
 		// print environment variables for sub-processes
@@ -391,7 +403,7 @@ func linkLocalBazel(baseDirectory string, bazelPath string) (string, error) {
 		if err != nil {
 			err = copyFile(bazelPath, destinationPath, 0755)
 			if err != nil {
-				return "", fmt.Errorf("cound not copy file from %s to %s: %v", bazelPath, destinationPath, err)
+				return "", fmt.Errorf("could not copy file from %s to %s: %v", bazelPath, destinationPath, err)
 			}
 		}
 	}
@@ -410,7 +422,7 @@ func maybeDelegateToWrapper(bazel string) string {
 
 	root := findWorkspaceRoot(wd)
 	wrapper := filepath.Join(root, wrapperPath)
-	if stat, err := os.Stat(wrapper); err != nil || stat.IsDir() || stat.Mode().Perm()&0001 == 0 {
+	if stat, err := os.Stat(wrapper); err != nil || stat.IsDir() || stat.Mode().Perm()&0111 == 0 {
 		return bazel
 	}
 
@@ -486,6 +498,11 @@ func runBazel(bazel string, args []string, out io.Writer) (int, error) {
 
 // getIncompatibleFlags returns all incompatible flags for the current Bazel command in alphabetical order.
 func getIncompatibleFlags(bazelPath, cmd string) ([]string, error) {
+	var incompatibleFlagsStr = GetEnvOrConfig("BAZELISK_INCOMPATIBLE_FLAGS")
+	if len(incompatibleFlagsStr) > 0 {
+		return strings.Split(incompatibleFlagsStr, ","), nil
+	}
+
 	out := strings.Builder{}
 	if _, err := runBazel(bazelPath, []string{"help", cmd, "--short"}, &out); err != nil {
 		return nil, fmt.Errorf("unable to determine incompatible flags with binary %s: %v", bazelPath, err)
@@ -520,14 +537,50 @@ func insertArgs(baseArgs []string, newArgs []string) []string {
 	return result
 }
 
-func shutdownIfNeeded(bazelPath string) {
+func parseStartupOptions(baseArgs []string) []string {
+	var result []string
+	var BAZEL_COMMANDS = map[string]bool{
+		"analyze-profile": true,
+		"aquery": true,
+		"build": true,
+		"canonicalize-flags": true,
+		"clean": true,
+		"coverage": true,
+		"cquery": true,
+		"dump": true,
+		"fetch": true,
+		"help": true,
+		"info": true,
+		"license": true,
+		"mobile-install": true,
+		"mod": true,
+		"print_action": true,
+		"query": true,
+		"run": true,
+		"shutdown": true,
+		"sync": true,
+		"test": true,
+		"version": true,
+	}
+	// Arguments before a Bazel command are startup options.
+	for _, arg := range baseArgs {
+		if _, ok := BAZEL_COMMANDS[arg]; ok {
+			return result
+		}
+		result = append(result, arg)
+	}
+	return result
+}
+
+func shutdownIfNeeded(bazelPath string, startupOptions []string) {
 	bazeliskClean := GetEnvOrConfig("BAZELISK_SHUTDOWN")
 	if len(bazeliskClean) == 0 {
 		return
 	}
 
-	fmt.Printf("bazel shutdown\n")
-	exitCode, err := runBazel(bazelPath, []string{"shutdown"}, nil)
+	args := append(startupOptions, "shutdown")
+	fmt.Printf("bazel %s\n", strings.Join(args, " "))
+	exitCode, err := runBazel(bazelPath, args, nil)
 	fmt.Printf("\n")
 	if err != nil {
 		log.Fatalf("failed to run bazel shutdown: %v", err)
@@ -538,14 +591,15 @@ func shutdownIfNeeded(bazelPath string) {
 	}
 }
 
-func cleanIfNeeded(bazelPath string) {
+func cleanIfNeeded(bazelPath string, startupOptions []string) {
 	bazeliskClean := GetEnvOrConfig("BAZELISK_CLEAN")
 	if len(bazeliskClean) == 0 {
 		return
 	}
 
-	fmt.Printf("bazel clean --expunge\n")
-	exitCode, err := runBazel(bazelPath, []string{"clean", "--expunge"}, nil)
+	args := append(startupOptions, "clean", "--expunge")
+	fmt.Printf("bazel %s\n", strings.Join(args, " "))
+	exitCode, err := runBazel(bazelPath, args, nil)
 	fmt.Printf("\n")
 	if err != nil {
 		log.Fatalf("failed to run clean: %v", err)
@@ -558,11 +612,13 @@ func cleanIfNeeded(bazelPath string) {
 
 // migrate will run Bazel with each flag separately and report which ones are failing.
 func migrate(bazelPath string, baseArgs []string, flags []string) {
+	var startupOptions = parseStartupOptions(baseArgs)
+
 	// 1. Try with all the flags.
 	args := insertArgs(baseArgs, flags)
 	fmt.Printf("\n\n--- Running Bazel with all incompatible flags\n\n")
-	shutdownIfNeeded(bazelPath)
-	cleanIfNeeded(bazelPath)
+	shutdownIfNeeded(bazelPath, startupOptions)
+	cleanIfNeeded(bazelPath, startupOptions)
 	fmt.Printf("bazel %s\n", strings.Join(args, " "))
 	exitCode, err := runBazel(bazelPath, args, nil)
 	if err != nil {
@@ -576,8 +632,8 @@ func migrate(bazelPath string, baseArgs []string, flags []string) {
 	// 2. Try with no flags, as a sanity check.
 	args = baseArgs
 	fmt.Printf("\n\n--- Running Bazel with no incompatible flags\n\n")
-	shutdownIfNeeded(bazelPath)
-	cleanIfNeeded(bazelPath)
+	shutdownIfNeeded(bazelPath, startupOptions)
+	cleanIfNeeded(bazelPath, startupOptions)
 	fmt.Printf("bazel %s\n", strings.Join(args, " "))
 	exitCode, err = runBazel(bazelPath, args, nil)
 	if err != nil {
@@ -594,8 +650,8 @@ func migrate(bazelPath string, baseArgs []string, flags []string) {
 	for _, arg := range flags {
 		args = insertArgs(baseArgs, []string{arg})
 		fmt.Printf("\n\n--- Running Bazel with %s\n\n", arg)
-		shutdownIfNeeded(bazelPath)
-		cleanIfNeeded(bazelPath)
+		shutdownIfNeeded(bazelPath, startupOptions)
+		cleanIfNeeded(bazelPath, startupOptions)
 		fmt.Printf("bazel %s\n", strings.Join(args, " "))
 		exitCode, err = runBazel(bazelPath, args, nil)
 		if err != nil {
diff --git a/core/repositories.go b/core/repositories.go
index 8a2919c..f0ab732 100644
--- a/core/repositories.go
+++ b/core/repositories.go
@@ -18,10 +18,28 @@ const (
 // DownloadFunc downloads a specific Bazel binary to the given location and returns the absolute path.
 type DownloadFunc func(destDir, destFile string) (string, error)
 
+// ReleaseFilter filters Bazel versions based on specific criteria.
+type ReleaseFilter func(matchesSoFar int, currentVersion string) bool
+
+func lastNReleases(max int) ReleaseFilter {
+	return func(matchesSoFar int, currentVersion string) bool {
+		return max < 1 || matchesSoFar < max
+	}
+}
+
+// filterReleasesByTrack only works reliably if iterating on Bazel versions in descending order.
+func filterReleasesByTrack(track int) ReleaseFilter {
+	prefix := fmt.Sprintf("%d.", track)
+	return func(matchesSoFar int, currentVersion string) bool {
+		return matchesSoFar == 0 && strings.HasPrefix(currentVersion, prefix)
+	}
+}
+
 // ReleaseRepo represents a repository that stores LTS Bazel releases.
 type ReleaseRepo interface {
-	// GetReleaseVersions returns a list of all available release versions. If lastN is smaller than 1, all available versions are being returned.
-	GetReleaseVersions(bazeliskHome string, lastN int) ([]string, error)
+	// GetReleaseVersions returns a list of all available release versions that match the given filter function.
+	// Warning: the filter only works reliably if the versions are processed in descending order!
+	GetReleaseVersions(bazeliskHome string, filter ReleaseFilter) ([]string, error)
 
 	// DownloadRelease downloads the given Bazel version into the specified location and returns the absolute path.
 	DownloadRelease(version, destDir, destFile string) (string, error)
@@ -117,13 +135,15 @@ func (r *Repositories) resolveFork(bazeliskHome string, vi *versions.Info) (stri
 
 func (r *Repositories) resolveRelease(bazeliskHome string, vi *versions.Info) (string, DownloadFunc, error) {
 	lister := func(bazeliskHome string) ([]string, error) {
-		var lastN int
-		// Optimization: only fetch last (x+1) releases if the version is "latest-x".
-		// This does not work if the version is "4.x", i.e. if TrackRestriction is set.
-		if vi.TrackRestriction == 0 {
-			lastN = vi.LatestOffset + 1
+		var filter ReleaseFilter
+		if vi.TrackRestriction > 0 {
+			// Optimization: only fetch matching releases if an LTS track is specified.
+			filter = filterReleasesByTrack(vi.TrackRestriction)
+		} else {
+			// Optimization: only fetch last (x+1) releases if the version is "latest-x".
+			filter = lastNReleases(vi.LatestOffset + 1)
 		}
-		return r.Releases.GetReleaseVersions(bazeliskHome, lastN)
+		return r.Releases.GetReleaseVersions(bazeliskHome, filter)
 	}
 	version, err := resolvePotentiallyRelativeVersion(bazeliskHome, lister, vi)
 	if err != nil {
@@ -187,29 +207,14 @@ func resolvePotentiallyRelativeVersion(bazeliskHome string, lister listVersionsF
 		return "", fmt.Errorf("unable to determine latest version: %v", err)
 	}
 
-	if vi.TrackRestriction > 0 {
-		available = restrictToTrack(available, vi.TrackRestriction)
-	}
-
 	index := len(available) - 1 - vi.LatestOffset
 	if index < 0 {
-		return "", fmt.Errorf("cannot resolve version \"%s\": There are only %d Bazel versions", vi.Value, len(available))
+		return "", fmt.Errorf("cannot resolve version \"%s\": There are not enough matching Bazel releases (%d)", vi.Value, len(available))
 	}
 	sorted := versions.GetInAscendingOrder(available)
 	return sorted[index], nil
 }
 
-func restrictToTrack(versions []string, track int) []string {
-	filtered := make([]string, 0)
-	prefix := fmt.Sprintf("%d.", track)
-	for _, v := range versions {
-		if strings.HasPrefix(v, prefix) {
-			filtered = append(filtered, v)
-		}
-	}
-	return filtered
-}
-
 // DownloadFromBaseURL can download Bazel binaries from a specific URL while ignoring the predefined repositories.
 func (r *Repositories) DownloadFromBaseURL(baseURL, version, destDir, destFile string) (string, error) {
 	if !r.supportsBaseURL {
@@ -271,7 +276,7 @@ type noReleaseRepo struct {
 	err error
 }
 
-func (nrr *noReleaseRepo) GetReleaseVersions(bazeliskHome string, lastN int) ([]string, error) {
+func (nrr *noReleaseRepo) GetReleaseVersions(bazeliskHome string, filter ReleaseFilter) ([]string, error) {
 	return nil, nrr.err
 }
 
diff --git a/go.mod b/go.mod
index 752c475..b433331 100644
--- a/go.mod
+++ b/go.mod
@@ -4,7 +4,7 @@ go 1.15
 
 require (
 	github.com/bazelbuild/rules_go v0.34.0
+	github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d
 	github.com/hashicorp/go-version v1.6.0
-	github.com/jdxcode/netrc v0.0.0-20210204082910-926c7f70242a
 	github.com/mitchellh/go-homedir v1.1.0
 )
diff --git a/go.sum b/go.sum
index e52a172..e5ef5fd 100644
--- a/go.sum
+++ b/go.sum
@@ -1,15 +1,8 @@
 github.com/bazelbuild/rules_go v0.34.0 h1:cmObMtgIOaEU944SqXtJ9DnlS8IPGGa7pdRnsrpQzXM=
 github.com/bazelbuild/rules_go v0.34.0/go.mod h1:MC23Dc/wkXEyk3Wpq6lCqz0ZAYOZDw2DR5y3N1q2i7M=
+github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1UJrqV3uuy861HCTo708pDMbjHHdCas=
+github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4=
 github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek=
 github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
-github.com/jdxcode/netrc v0.0.0-20210204082910-926c7f70242a h1:d4+I1YEKVmWZrgkt6jpXBnLgV2ZjO0YxEtLDdfIZfH4=
-github.com/jdxcode/netrc v0.0.0-20210204082910-926c7f70242a/go.mod h1:Zi/ZFkEqFHTm7qkjyNJjaWH4LQA9LQhGJyF0lTYGpxw=
-github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
-github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
-github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
 github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
-github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
-github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
-gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b h1:QRR6H1YWRnHb4Y/HeNFCTJLFVxaq6wH4YuVdsUOr75U=
-gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
diff --git a/httputil/BUILD b/httputil/BUILD
index 89113ca..f168cff 100644
--- a/httputil/BUILD
+++ b/httputil/BUILD
@@ -12,7 +12,7 @@ go_library(
     ],
     deps = [
         "@com_github_mitchellh_go_homedir//:go_default_library",
-        "@com_github_jdxcode_netrc//:go_default_library"
+        "@com_github_bgentry_go_netrc//:go_default_library"
     ],
     importpath = "github.com/bazelbuild/bazelisk/httputil",
     visibility = ["//visibility:public"],
diff --git a/httputil/httputil.go b/httputil/httputil.go
index cbebf98..36db678 100644
--- a/httputil/httputil.go
+++ b/httputil/httputil.go
@@ -16,7 +16,7 @@ import (
 	"strconv"
 	"time"
 
-	netrc "github.com/jdxcode/netrc"
+	netrc "github.com/bgentry/go-netrc/netrc"
 	homedir "github.com/mitchellh/go-homedir"
 )
 
@@ -146,13 +146,13 @@ func tryFindNetrcFileCreds(host string) (string, error) {
 	}
 
 	var file = filepath.Join(dir, ".netrc")
-	n, err := netrc.Parse(file)
+	n, err := netrc.ParseFile(file)
 	if err != nil {
 		// netrc does not exist or we can't read it
 		return "", err
 	}
 
-	m := n.Machine(host)
+	m := n.FindMachine(host)
 	if m == nil {
 		// if host is not found, we should proceed without providing any Authorization header,
 		// because remote host may not have auth at all.
@@ -162,9 +162,7 @@ func tryFindNetrcFileCreds(host string) (string, error) {
 
 	log.Printf("Using basic authentication credentials for host %s from %s", host, file)
 
-	login := m.Get("login")
-	pwd := m.Get("password")
-	token := b64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", login, pwd)))
+	token := b64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", m.Login, m.Password)))
 	return fmt.Sprintf("Basic %s", token), nil
 }
 
diff --git a/platforms/BUILD b/platforms/BUILD
index 6b8f925..dcb655f 100644
--- a/platforms/BUILD
+++ b/platforms/BUILD
@@ -6,6 +6,7 @@ go_library(
     importpath = "github.com/bazelbuild/bazelisk/platforms",
     visibility = ["//visibility:public"],
     deps = [
+        "//versions:go_default_library",
         "@com_github_hashicorp_go_version//:go_default_library",
     ],
 )
diff --git a/platforms/platforms.go b/platforms/platforms.go
index 6c6b4e8..3c0fcbe 100644
--- a/platforms/platforms.go
+++ b/platforms/platforms.go
@@ -1,24 +1,48 @@
-// Package platforms determins file names and extensions based on the current operating system.
+// Package platforms determines file names and extensions based on the current operating system.
 package platforms
 
 import (
 	"fmt"
-	semver "github.com/hashicorp/go-version"
 	"log"
 	"runtime"
+
+	"github.com/bazelbuild/bazelisk/versions"
+	semver "github.com/hashicorp/go-version"
 )
 
-var platforms = map[string]string{"darwin": "macos", "linux": "ubuntu1404", "windows": "windows"}
+type platform struct {
+	Name           string
+	HasArm64Binary bool
+}
+
+var supportedPlatforms = map[string]*platform{
+	"darwin": {
+		Name:           "macos",
+		HasArm64Binary: true,
+	},
+	"linux": {
+		Name:           "centos7",
+		HasArm64Binary: false,
+	},
+	"windows": {
+		Name:           "windows",
+		HasArm64Binary: true,
+	},
+}
 
 // GetPlatform returns a Bazel CI-compatible platform identifier for the current operating system.
 // TODO(fweikert): raise an error for unsupported platforms
-func GetPlatform() string {
-	platform := platforms[runtime.GOOS]
+func GetPlatform() (string, error) {
+	platform := supportedPlatforms[runtime.GOOS]
 	arch := runtime.GOARCH
 	if arch == "arm64" {
-		platform = platform + "_arm64"
+		if platform.HasArm64Binary {
+			return platform.Name + "_arm64", nil
+		}
+		return "", fmt.Errorf("arm64 %s is unsupported", runtime.GOOS)
 	}
-	return platform
+
+	return platform.Name, nil
 }
 
 // DetermineExecutableFilenameSuffix returns the extension for binaries on the current operating system.
@@ -64,6 +88,11 @@ func DetermineBazelFilename(version string, includeSuffix bool) (string, error)
 
 // 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) {
+	// Do not use fallback for commits since they are likely newer than Bazel 4.1
+	if versions.IsCommit(version) {
+		return machineName
+	}
+
 	v, err := semver.NewVersion(version)
 	if err != nil {
 		return machineName
diff --git a/repositories/BUILD b/repositories/BUILD
index 7747adf..c612bf4 100644
--- a/repositories/BUILD
+++ b/repositories/BUILD
@@ -9,6 +9,7 @@ go_library(
     importpath = "github.com/bazelbuild/bazelisk/repositories",
     visibility = ["//visibility:public"],
     deps = [
+        "//core:go_default_library",
         "//httputil:go_default_library",
         "//platforms:go_default_library",
         "//versions:go_default_library",
diff --git a/repositories/gcs.go b/repositories/gcs.go
index 170f172..542e781 100644
--- a/repositories/gcs.go
+++ b/repositories/gcs.go
@@ -10,6 +10,7 @@ import (
 	"log"
 	"strings"
 
+	"github.com/bazelbuild/bazelisk/core"
 	"github.com/bazelbuild/bazelisk/httputil"
 	"github.com/bazelbuild/bazelisk/platforms"
 	"github.com/bazelbuild/bazelisk/versions"
@@ -35,13 +36,13 @@ type GCSRepo struct{}
 
 // ReleaseRepo
 
-// GetReleaseVersions returns the versions of all available Bazel releases in this repository.
-func (gcs *GCSRepo) GetReleaseVersions(bazeliskHome string, lastN int) ([]string, error) {
+// GetReleaseVersions returns the versions of all available Bazel releases in this repository that match the given filter.
+func (gcs *GCSRepo) GetReleaseVersions(bazeliskHome string, filter core.ReleaseFilter) ([]string, error) {
 	history, err := getVersionHistoryFromGCS()
 	if err != nil {
 		return []string{}, err
 	}
-	releases, err := gcs.removeCandidates(history, lastN)
+	releases, err := gcs.removeCandidates(history, filter)
 	if err != nil {
 		return []string{}, err
 	}
@@ -130,17 +131,31 @@ func (gcs *GCSRepo) DownloadRelease(version, destDir, destFile string) (string,
 	return httputil.DownloadBinary(url, destDir, destFile)
 }
 
-func (gcs *GCSRepo) removeCandidates(history []string, lastN int) ([]string, error) {
-	var resolvedLimit int
-	if lastN < 1 {
-		resolvedLimit = len(history)
-	} else {
-		resolvedLimit = lastN
-	}
-
+func (gcs *GCSRepo) removeCandidates(history []string, filter core.ReleaseFilter) ([]string, error) {
 	descendingReleases := make([]string, 0)
-	for hpos := len(history) - 1; hpos >= 0 && len(descendingReleases) < resolvedLimit; hpos-- {
+	filterPassed := false
+	// Iteration in descending order is important:
+	// - ReleaseFilter won't work correctly otherwise
+	// - It makes more sense in the "latest-n" use case
+	for hpos := len(history) - 1; hpos >= 0; hpos-- {
 		latestVersion := history[hpos]
+		pass := filter(len(descendingReleases), latestVersion)
+		if pass {
+			filterPassed = true
+		} else {
+			// Underlying assumption: all matching versions are in a single continuous sequence.
+			// Consequently, if the filter returns false, this means:
+			if filterPassed {
+				// a) "break" if we've seen a match before (because all future version won't match either)
+				break
+			} else {
+				// b) "continue looking" if we've never seen a match before
+				continue
+			}
+		}
+		// Obviously this only works for the existing filters (lastN and track-based) and
+		// because versions in history are sorted.
+
 		_, isRelease, err := listDirectoriesInReleaseBucket(latestVersion + "/release/")
 		if err != nil {
 			return []string{}, fmt.Errorf("could not list available releases for %v: %v", latestVersion, err)
@@ -149,10 +164,6 @@ func (gcs *GCSRepo) removeCandidates(history []string, lastN int) ([]string, err
 			descendingReleases = append(descendingReleases, latestVersion)
 		}
 	}
-
-	if lastN > 0 && len(descendingReleases) < lastN {
-		return []string{}, fmt.Errorf("requested %d latest releases, but only found %d", lastN, len(descendingReleases))
-	}
 	return reverseInPlace(descendingReleases), nil
 }
 
@@ -236,7 +247,11 @@ func (gcs *GCSRepo) GetLastGreenCommit(bazeliskHome string, downstreamGreen bool
 // DownloadAtCommit downloads a Bazel binary built at the given commit into the specified location and returns the absolute path.
 func (gcs *GCSRepo) DownloadAtCommit(commit, destDir, destFile string) (string, error) {
 	log.Printf("Using unreleased version at commit %s", commit)
-	url := fmt.Sprintf("%s/%s/%s/bazel", nonCandidateBaseURL, platforms.GetPlatform(), commit)
+	platform, err := platforms.GetPlatform()
+	if err != nil {
+		return "", err
+	}
+	url := fmt.Sprintf("%s/%s/%s/bazel", nonCandidateBaseURL, platform, commit)
 	return httputil.DownloadBinary(url, destDir, destFile)
 }
 
@@ -272,7 +287,7 @@ func (gcs *GCSRepo) DownloadRolling(version, destDir, destFile string) (string,
 		return "", err
 	}
 
-	release_version := strings.Split(version, "-")[0]
-	url := fmt.Sprintf("%s/%s/rolling/%s/%s", candidateBaseURL, release_version, version, srcFile)
+	releaseVersion := strings.Split(version, "-")[0]
+	url := fmt.Sprintf("%s/%s/rolling/%s/%s", candidateBaseURL, releaseVersion, version, srcFile)
 	return httputil.DownloadBinary(url, destDir, destFile)
 }
diff --git a/repositories/github.go b/repositories/github.go
index b75edcd..f03fd6a 100644
--- a/repositories/github.go
+++ b/repositories/github.go
@@ -54,7 +54,7 @@ func (gh *GitHubRepo) getFilteredVersions(bazeliskHome, bazelFork string, wantPr
 	url := fmt.Sprintf("https://api.github.com/repos/%s/bazel/releases", bazelFork)
 	releasesJSON, err := httputil.MaybeDownload(bazeliskHome, url, bazelFork+"-releases.json", "list of Bazel releases from github.com/"+bazelFork, fmt.Sprintf("token %s", gh.token), merger)
 	if err != nil {
-		return []string{}, fmt.Errorf("unable to dermine '%s' releases: %v", bazelFork, err)
+		return []string{}, fmt.Errorf("unable to determine '%s' releases: %v", bazelFork, err)
 	}
 
 	if len(releases) == 0 {
diff --git a/versions/versions.go b/versions/versions.go
index 0c4d5f9..5f5547e 100644
--- a/versions/versions.go
+++ b/versions/versions.go
@@ -105,3 +105,8 @@ func GetInAscendingOrder(versions []string) []string {
 	}
 	return sorted
 }
+
+// IsCommit returns whether the given version refers to a commit.
+func IsCommit(version string) bool {
+	return version == "last_green" || version == "last_downstream_green" || commitPattern.MatchString(version)
+}
\ No newline at end of file


Test patch:
diff --git a/bazelisk_version_test.go b/bazelisk_version_test.go
index bd83cf7..e542372 100644
--- a/bazelisk_version_test.go
+++ b/bazelisk_version_test.go
@@ -166,7 +166,7 @@ func TestResolveLatestVersion_ShouldFailIfNotEnoughReleases(t *testing.T) {
 	if err == nil {
 		t.Fatal("Expected ResolveVersion() to fail.")
 	}
-	expectedError := "unable to determine latest version: requested 2 latest releases, but only found 1"
+	expectedError := "cannot resolve version \"latest-1\": There are not enough matching Bazel releases (1)"
 	if err.Error() != expectedError {
 		t.Fatalf("Expected error message %q, but got '%v'", expectedError, err)
 	}
@@ -201,7 +201,7 @@ func TestResolveLatestVersion_GitHubIsDown(t *testing.T) {
 	if err == nil {
 		t.Fatal("Expected resolveLatestVersion() to fail.")
 	}
-	expectedPrefix := "unable to determine latest version: unable to dermine 'some_fork' releases: could not download list of Bazel releases from github.com/some_fork"
+	expectedPrefix := "unable to determine latest version: unable to determine 'some_fork' releases: could not download list of Bazel releases from github.com/some_fork"
 	if !strings.HasPrefix(err.Error(), expectedPrefix) {
 		t.Fatalf("Expected error message that starts with %q, but got '%v'", expectedPrefix, err)
 	}