Repository: bazelbuild/bazelisk
Pull request #500 (2023-09-21 11:16:57)

Make getBazelVersion func public
At the moment, `func getBazelVersion()` is scoped to the `core.go` package. I have made a small change to make this function public, so it can be called from other packages. 


Patch:
diff --git a/BUILD b/BUILD
index 906cbe2..3b3abec 100644
--- a/BUILD
+++ b/BUILD
@@ -58,6 +58,7 @@ go_test(
     embed = [":go_default_library"],
     importpath = "github.com/bazelbuild/bazelisk",
     deps = [
+        "//config:go_default_library",
         "//core:go_default_library",
         "//httputil:go_default_library",
         "//repositories:go_default_library",
@@ -154,7 +155,6 @@ go_binary(
 pkg_npm(
     name = "npm_package",
     package_name = "@bazel/bazelisk",
-    substitutions = {"0.0.0-PLACEHOLDER": "{BUILD_SCM_VERSION}"},
     srcs = [
         "LICENSE",
         "README.md",
@@ -162,6 +162,7 @@ pkg_npm(
         "bazelisk.js",
         "package.json",
     ],
+    substitutions = {"0.0.0-PLACEHOLDER": "{BUILD_SCM_VERSION}"},
     deps = [
         ":bazelisk-darwin-amd64",
         ":bazelisk-darwin-arm64",
diff --git a/README.md b/README.md
index 35d4622..ab59fc7 100644
--- a/README.md
+++ b/README.md
@@ -183,6 +183,7 @@ The following variables can be set:
 - `BAZELISK_GITHUB_TOKEN`
 - `BAZELISK_HOME`
 - `BAZELISK_INCOMPATIBLE_FLAGS`
+- `BAZELISK_SHOW_PROGRESS`
 - `BAZELISK_SHUTDOWN`
 - `BAZELISK_SKIP_WRAPPER`
 - `BAZELISK_USER_AGENT`
diff --git a/WORKSPACE b/WORKSPACE
index 73033ce..bdcca7f 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -2,10 +2,10 @@ load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
 
 http_archive(
     name = "io_bazel_rules_go",
-    sha256 = "56d8c5a5c91e1af73eca71a6fab2ced959b67c86d12ba37feedb0a2dfea441a6",
+    sha256 = "278b7ff5a826f3dc10f04feaf0b70d48b68748ccd512d7f98bf442077f043fe3",
     urls = [
-        "https://mirror.bazel.build/github.com/bazelbuild/rules_go/releases/download/v0.37.0/rules_go-v0.37.0.zip",
-        "https://github.com/bazelbuild/rules_go/releases/download/v0.37.0/rules_go-v0.37.0.zip",
+        "https://mirror.bazel.build/github.com/bazelbuild/rules_go/releases/download/v0.41.0/rules_go-v0.41.0.zip",
+        "https://github.com/bazelbuild/rules_go/releases/download/v0.41.0/rules_go-v0.41.0.zip",
     ],
 )
 
@@ -25,7 +25,7 @@ go_repository(
     name = "com_github_bazelbuild_rules_go",
     importpath = "github.com/bazelbuild/rules_go",
     sum = "h1:vbnESGv/t2WgGEbXatwbXAS95dTx93Lv6Uh5QkVF13s=",
-    version = "v0.37.0",
+    version = "v0.42.0",
 )
 
 go_repository(
@@ -49,9 +49,23 @@ go_repository(
     version = "v0.0.0-20140422174119-9fd32a8b3d3d",
 )
 
+go_repository(
+    name = "org_golang_x_term",
+    importpath = "golang.org/x/term",
+    sum = "h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek=",
+    version = "v0.13.0",
+)
+
+go_repository(
+    name = "org_golang_x_sys",
+    importpath = "golang.org/x/sys",
+    sum = "h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=",
+    version = "v0.13.0",
+)
+
 go_rules_dependencies()
 
-go_register_toolchains(version = "1.19.4")
+go_register_toolchains(version = "1.20.5")
 
 gazelle_dependencies()
 
diff --git a/bazelisk.py b/bazelisk.py
index 8a967c2..45eb3cc 100755
--- a/bazelisk.py
+++ b/bazelisk.py
@@ -95,10 +95,10 @@ def decide_which_bazel_version_to_use():
 def find_workspace_root(root=None):
     if root is None:
         root = os.getcwd()
-    if os.path.exists(os.path.join(root, "WORKSPACE")):
-        return root
-    if os.path.exists(os.path.join(root, "WORKSPACE.bazel")):
-        return root
+    for boundary in ["MODULE.bazel", "REPO.bazel", "WORKSPACE.bazel", "WORKSPACE"]:
+        path = os.path.join(root, boundary)
+        if os.path.exists(path) and not os.path.isdir(path):
+            return root
     new_root = os.path.dirname(root)
     return find_workspace_root(new_root) if new_root != root else None
 
diff --git a/core/core.go b/core/core.go
index cab98d9..f6ed200 100644
--- a/core/core.go
+++ b/core/core.go
@@ -106,7 +106,7 @@ func RunBazeliskWithArgsFuncAndConfig(argsFunc ArgsFunc, repos *Repositories, co
 		return -1, fmt.Errorf("could not create directory %s: %v", bazeliskHome, err)
 	}
 
-	bazelVersionString, err := getBazelVersion(config)
+	bazelVersionString, err := GetBazelVersion(config)
 	if err != nil {
 		return -1, fmt.Errorf("could not get Bazel version: %v", err)
 	}
@@ -230,7 +230,7 @@ func cutString(s, sep string) (before, after string, found bool) {
 	return s, "", false
 }
 
-func getBazelVersion(config config.Config) (string, error) {
+func GetBazelVersion(config config.Config) (string, error) {
 	// Check in this order:
 	// - env var "USE_BAZEL_VERSION" is set to a specific version.
 	// - workspace_root/.bazeliskrc exists -> read contents, in contents:
@@ -322,7 +322,7 @@ func downloadBazel(bazelVersionString string, bazeliskHome string, repos *Reposi
 		return "", fmt.Errorf("could not parse Bazel fork and version: %v", err)
 	}
 
-	resolvedBazelVersion, downloader, err := repos.ResolveVersion(bazeliskHome, bazelFork, bazelVersion)
+	resolvedBazelVersion, downloader, err := repos.ResolveVersion(bazeliskHome, bazelFork, bazelVersion, config)
 	if err != nil {
 		return "", fmt.Errorf("could not resolve the version '%s' to an actual version number: %v", bazelVersion, err)
 	}
@@ -415,7 +415,7 @@ func downloadBazelToCAS(version string, bazeliskHome string, repos *Repositories
 	} else if formatURL != "" {
 		tmpDestPath, err = repos.DownloadFromFormatURL(config, formatURL, version, temporaryDownloadDir, tmpDestFile)
 	} else if baseURL != "" {
-		tmpDestPath, err = repos.DownloadFromBaseURL(baseURL, version, temporaryDownloadDir, tmpDestFile)
+		tmpDestPath, err = repos.DownloadFromBaseURL(baseURL, version, temporaryDownloadDir, tmpDestFile, config)
 	} else {
 		tmpDestPath, err = downloader(temporaryDownloadDir, tmpDestFile)
 	}
diff --git a/core/repositories.go b/core/repositories.go
index e11e446..b84a1f4 100644
--- a/core/repositories.go
+++ b/core/repositories.go
@@ -46,7 +46,7 @@ type ReleaseRepo interface {
 	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)
+	DownloadRelease(version, destDir, destFile string, config config.Config) (string, error)
 }
 
 // CandidateRepo represents a repository that stores Bazel release candidates.
@@ -55,7 +55,7 @@ type CandidateRepo interface {
 	GetCandidateVersions(bazeliskHome string) ([]string, error)
 
 	// DownloadCandidate downloads the given Bazel release candidate into the specified location and returns the absolute path.
-	DownloadCandidate(version, destDir, destFile string) (string, error)
+	DownloadCandidate(version, destDir, destFile string, config config.Config) (string, error)
 }
 
 // ForkRepo represents a repository that stores a fork of Bazel (releases).
@@ -64,7 +64,7 @@ type ForkRepo interface {
 	GetVersions(bazeliskHome, fork string) ([]string, error)
 
 	// DownloadVersion downloads the given Bazel binary from the specified fork into the given location and returns the absolute path.
-	DownloadVersion(fork, version, destDir, destFile string) (string, error)
+	DownloadVersion(fork, version, destDir, destFile string, config config.Config) (string, error)
 }
 
 // CommitRepo represents a repository that stores Bazel binaries built at specific commits.
@@ -76,7 +76,7 @@ type CommitRepo interface {
 	GetLastGreenCommit(bazeliskHome string, downstreamGreen bool) (string, error)
 
 	// DownloadAtCommit downloads a Bazel binary built at the given commit into the specified location and returns the absolute path.
-	DownloadAtCommit(commit, destDir, destFile string) (string, error)
+	DownloadAtCommit(commit, destDir, destFile string, config config.Config) (string, error)
 }
 
 // RollingRepo represents a repository that stores rolling Bazel releases.
@@ -85,7 +85,7 @@ type RollingRepo interface {
 	GetRollingVersions(bazeliskHome string) ([]string, error)
 
 	// DownloadRolling downloads the given Bazel version into the specified location and returns the absolute path.
-	DownloadRolling(version, destDir, destFile string) (string, error)
+	DownloadRolling(version, destDir, destFile string, config config.Config) (string, error)
 }
 
 // Repositories offers access to different types of Bazel repositories, mainly for finding and downloading the correct version of Bazel.
@@ -99,28 +99,28 @@ type Repositories struct {
 }
 
 // ResolveVersion resolves a potentially relative Bazel version string such as "latest" to an absolute version identifier, and returns this identifier alongside a function to download said version.
-func (r *Repositories) ResolveVersion(bazeliskHome, fork, version string) (string, DownloadFunc, error) {
+func (r *Repositories) ResolveVersion(bazeliskHome, fork, version string, config config.Config) (string, DownloadFunc, error) {
 	vi, err := versions.Parse(fork, version)
 	if err != nil {
 		return "", nil, err
 	}
 
 	if vi.IsFork {
-		return r.resolveFork(bazeliskHome, vi)
+		return r.resolveFork(bazeliskHome, vi, config)
 	} else if vi.IsRelease {
-		return r.resolveRelease(bazeliskHome, vi)
+		return r.resolveRelease(bazeliskHome, vi, config)
 	} else if vi.IsCandidate {
-		return r.resolveCandidate(bazeliskHome, vi)
+		return r.resolveCandidate(bazeliskHome, vi, config)
 	} else if vi.IsCommit {
-		return r.resolveCommit(bazeliskHome, vi)
+		return r.resolveCommit(bazeliskHome, vi, config)
 	} else if vi.IsRolling {
-		return r.resolveRolling(bazeliskHome, vi)
+		return r.resolveRolling(bazeliskHome, vi, config)
 	}
 
 	return "", nil, fmt.Errorf("Unsupported version identifier '%s'", version)
 }
 
-func (r *Repositories) resolveFork(bazeliskHome string, vi *versions.Info) (string, DownloadFunc, error) {
+func (r *Repositories) resolveFork(bazeliskHome string, vi *versions.Info, config config.Config) (string, DownloadFunc, error) {
 	if vi.IsRelative && (vi.IsCandidate || vi.IsCommit) {
 		return "", nil, errors.New("forks do not support last_rc, last_green and last_downstream_green")
 	}
@@ -132,12 +132,12 @@ func (r *Repositories) resolveFork(bazeliskHome string, vi *versions.Info) (stri
 		return "", nil, err
 	}
 	downloader := func(destDir, destFile string) (string, error) {
-		return r.Fork.DownloadVersion(vi.Fork, version, destDir, destFile)
+		return r.Fork.DownloadVersion(vi.Fork, version, destDir, destFile, config)
 	}
 	return version, downloader, nil
 }
 
-func (r *Repositories) resolveRelease(bazeliskHome string, vi *versions.Info) (string, DownloadFunc, error) {
+func (r *Repositories) resolveRelease(bazeliskHome string, vi *versions.Info, config config.Config) (string, DownloadFunc, error) {
 	lister := func(bazeliskHome string) ([]string, error) {
 		var filter ReleaseFilter
 		if vi.TrackRestriction > 0 {
@@ -154,23 +154,23 @@ func (r *Repositories) resolveRelease(bazeliskHome string, vi *versions.Info) (s
 		return "", nil, err
 	}
 	downloader := func(destDir, destFile string) (string, error) {
-		return r.Releases.DownloadRelease(version, destDir, destFile)
+		return r.Releases.DownloadRelease(version, destDir, destFile, config)
 	}
 	return version, downloader, nil
 }
 
-func (r *Repositories) resolveCandidate(bazeliskHome string, vi *versions.Info) (string, DownloadFunc, error) {
+func (r *Repositories) resolveCandidate(bazeliskHome string, vi *versions.Info, config config.Config) (string, DownloadFunc, error) {
 	version, err := resolvePotentiallyRelativeVersion(bazeliskHome, r.Candidates.GetCandidateVersions, vi)
 	if err != nil {
 		return "", nil, err
 	}
 	downloader := func(destDir, destFile string) (string, error) {
-		return r.Candidates.DownloadCandidate(version, destDir, destFile)
+		return r.Candidates.DownloadCandidate(version, destDir, destFile, config)
 	}
 	return version, downloader, nil
 }
 
-func (r *Repositories) resolveCommit(bazeliskHome string, vi *versions.Info) (string, DownloadFunc, error) {
+func (r *Repositories) resolveCommit(bazeliskHome string, vi *versions.Info, config config.Config) (string, DownloadFunc, error) {
 	version := vi.Value
 	if vi.IsRelative {
 		var err error
@@ -180,12 +180,12 @@ func (r *Repositories) resolveCommit(bazeliskHome string, vi *versions.Info) (st
 		}
 	}
 	downloader := func(destDir, destFile string) (string, error) {
-		return r.Commits.DownloadAtCommit(version, destDir, destFile)
+		return r.Commits.DownloadAtCommit(version, destDir, destFile, config)
 	}
 	return version, downloader, nil
 }
 
-func (r *Repositories) resolveRolling(bazeliskHome string, vi *versions.Info) (string, DownloadFunc, error) {
+func (r *Repositories) resolveRolling(bazeliskHome string, vi *versions.Info, config config.Config) (string, DownloadFunc, error) {
 	lister := func(bazeliskHome string) ([]string, error) {
 		return r.Rolling.GetRollingVersions(bazeliskHome)
 	}
@@ -194,7 +194,7 @@ func (r *Repositories) resolveRolling(bazeliskHome string, vi *versions.Info) (s
 		return "", nil, err
 	}
 	downloader := func(destDir, destFile string) (string, error) {
-		return r.Rolling.DownloadRolling(version, destDir, destFile)
+		return r.Rolling.DownloadRolling(version, destDir, destFile, config)
 	}
 	return version, downloader, nil
 }
@@ -220,7 +220,7 @@ func resolvePotentiallyRelativeVersion(bazeliskHome string, lister listVersionsF
 }
 
 // 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) {
+func (r *Repositories) DownloadFromBaseURL(baseURL, version, destDir, destFile string, config config.Config) (string, error) {
 	if !r.supportsBaseURL {
 		return "", fmt.Errorf("downloads from %s are forbidden", BaseURLEnv)
 	} else if baseURL == "" {
@@ -233,7 +233,7 @@ func (r *Repositories) DownloadFromBaseURL(baseURL, version, destDir, destFile s
 	}
 
 	url := fmt.Sprintf("%s/%s/%s", baseURL, version, srcFile)
-	return httputil.DownloadBinary(url, destDir, destFile)
+	return httputil.DownloadBinary(url, destDir, destFile, config)
 }
 
 func BuildURLFromFormat(config config.Config, formatURL, version string) (string, error) {
@@ -292,7 +292,7 @@ func (r *Repositories) DownloadFromFormatURL(config config.Config, formatURL, ve
 		return "", err
 	}
 
-	return httputil.DownloadBinary(url, destDir, destFile)
+	return httputil.DownloadBinary(url, destDir, destFile, config)
 }
 
 // CreateRepositories creates a new Repositories instance with the given repositories. Any nil repository will be replaced by a dummy repository that raises an error whenever a download is attempted.
@@ -343,7 +343,7 @@ func (nrr *noReleaseRepo) GetReleaseVersions(bazeliskHome string, filter Release
 	return nil, nrr.err
 }
 
-func (nrr *noReleaseRepo) DownloadRelease(version, destDir, destFile string) (string, error) {
+func (nrr *noReleaseRepo) DownloadRelease(version, destDir, destFile string, config config.Config) (string, error) {
 	return "", nrr.err
 }
 
@@ -355,7 +355,7 @@ func (ncc *noCandidateRepo) GetCandidateVersions(bazeliskHome string) ([]string,
 	return nil, ncc.err
 }
 
-func (ncc *noCandidateRepo) DownloadCandidate(version, destDir, destFile string) (string, error) {
+func (ncc *noCandidateRepo) DownloadCandidate(version, destDir, destFile string, config config.Config) (string, error) {
 	return "", ncc.err
 }
 
@@ -367,7 +367,7 @@ func (nfr *noForkRepo) GetVersions(bazeliskHome, fork string) ([]string, error)
 	return nil, nfr.err
 }
 
-func (nfr *noForkRepo) DownloadVersion(fork, version, destDir, destFile string) (string, error) {
+func (nfr *noForkRepo) DownloadVersion(fork, version, destDir, destFile string, config config.Config) (string, error) {
 	return "", nfr.err
 }
 
@@ -379,7 +379,7 @@ func (nlgr *noCommitRepo) GetLastGreenCommit(bazeliskHome string, downstreamGree
 	return "", nlgr.err
 }
 
-func (nlgr *noCommitRepo) DownloadAtCommit(commit, destDir, destFile string) (string, error) {
+func (nlgr *noCommitRepo) DownloadAtCommit(commit, destDir, destFile string, config config.Config) (string, error) {
 	return "", nlgr.err
 }
 
@@ -391,6 +391,6 @@ func (nrr *noRollingRepo) GetRollingVersions(bazeliskHome string) ([]string, err
 	return nil, nrr.err
 }
 
-func (nrr *noRollingRepo) DownloadRolling(version, destDir, destFile string) (string, error) {
+func (nrr *noRollingRepo) DownloadRolling(version, destDir, destFile string, config config.Config) (string, error) {
 	return "", nrr.err
 }
diff --git a/go.mod b/go.mod
index f2d53e1..67e4f47 100644
--- a/go.mod
+++ b/go.mod
@@ -6,4 +6,6 @@ require (
 	github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d
 	github.com/hashicorp/go-version v1.6.0
 	github.com/mitchellh/go-homedir v1.1.0
+        golang.org/x/term v0.13.0
+	golang.org/x/sys v0.13.0
 )
diff --git a/go.sum b/go.sum
index cc4156d..24d669a 100644
--- a/go.sum
+++ b/go.sum
@@ -4,3 +4,7 @@ github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mO
 github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
 github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
 github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
+golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek=
+golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
diff --git a/httputil/BUILD b/httputil/BUILD
index 4c7ac89..d15189b 100644
--- a/httputil/BUILD
+++ b/httputil/BUILD
@@ -13,6 +13,8 @@ go_library(
     importpath = "github.com/bazelbuild/bazelisk/httputil",
     visibility = ["//visibility:public"],
     deps = [
+        "//config:go_default_library",
+        "//httputil/progress:go_default_library",
         "@com_github_bgentry_go_netrc//netrc:go_default_library",
         "@com_github_mitchellh_go_homedir//:go_default_library",
     ],
@@ -20,6 +22,8 @@ go_library(
 
 go_test(
     name = "go_default_test",
-    srcs = ["httputil_test.go"],
+    srcs = [
+        "httputil_test.go",
+    ],
     embed = [":go_default_library"],
 )
diff --git a/httputil/httputil.go b/httputil/httputil.go
index 36db678..07eb8c5 100644
--- a/httputil/httputil.go
+++ b/httputil/httputil.go
@@ -18,6 +18,9 @@ import (
 
 	netrc "github.com/bgentry/go-netrc/netrc"
 	homedir "github.com/mitchellh/go-homedir"
+
+	"github.com/bazelbuild/bazelisk/config"
+	"github.com/bazelbuild/bazelisk/httputil/progress"
 )
 
 var (
@@ -167,7 +170,7 @@ func tryFindNetrcFileCreds(host string) (string, error) {
 }
 
 // DownloadBinary downloads a file from the given URL into the specified location, marks it executable and returns its full path.
-func DownloadBinary(originURL, destDir, destFile string) (string, error) {
+func DownloadBinary(originURL, destDir, destFile string, config config.Config) (string, error) {
 	err := os.MkdirAll(destDir, 0755)
 	if err != nil {
 		return "", fmt.Errorf("could not create directory %s: %v", destDir, err)
@@ -211,7 +214,11 @@ func DownloadBinary(originURL, destDir, destFile string) (string, error) {
 			return "", fmt.Errorf("HTTP GET %s failed with error %v", originURL, resp.StatusCode)
 		}
 
-		_, err = io.Copy(tmpfile, resp.Body)
+		_, err = io.Copy(
+			// Add a progress bar during download.
+			progress.Writer(tmpfile, "Downloading", resp.ContentLength, config),
+			resp.Body)
+		progress.Finish(config)
 		if err != nil {
 			return "", fmt.Errorf("could not copy from %s to %s: %v", originURL, tmpfile.Name(), err)
 		}
diff --git a/httputil/progress/BUILD b/httputil/progress/BUILD
new file mode 100644
index 0000000..07bd8c7
--- /dev/null
+++ b/httputil/progress/BUILD
@@ -0,0 +1,26 @@
+load("@bazel_gazelle//:def.bzl", "gazelle")
+load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
+
+# gazelle:prefix github.com/bazelbuild/bazelisk/httputil/progress
+gazelle(name = "gazelle")
+
+go_library(
+    name = "go_default_library",
+    srcs = [
+        "progress.go",
+    ],
+    importpath = "github.com/bazelbuild/bazelisk/httputil/progress",
+    visibility = ["//visibility:public"],
+    deps = [
+        "//config:go_default_library",
+        "@org_golang_x_term//:go_default_library",
+    ],
+)
+
+go_test(
+    name = "go_default_test",
+    srcs = [
+        "progress_test.go",
+    ],
+    embed = [":go_default_library"],
+)
diff --git a/httputil/progress/progress.go b/httputil/progress/progress.go
new file mode 100644
index 0000000..2df6612
--- /dev/null
+++ b/httputil/progress/progress.go
@@ -0,0 +1,115 @@
+// Package progress makes it possible to dissplay download progress.
+package progress
+
+import (
+	"fmt"
+	"io"
+	"os"
+	"strings"
+
+	"golang.org/x/term"
+
+	"github.com/bazelbuild/bazelisk/config"
+)
+
+// Progress shows a download progress bar.
+type progress struct {
+	header      string
+	total       int64
+	current     int64
+	lastMessage string
+}
+
+func showProgress(config config.Config) bool {
+	// If stdout is a terminal, don't show progress.
+	if !term.IsTerminal(int(os.Stdout.Fd())) {
+		return false
+	}
+
+	// Check the config variable.
+	showProgress := config.Get("BAZELISK_SHOW_PROGRESS")
+	if len(showProgress) == 0 {
+		// Default to showing progress
+		return true
+	}
+	switch strings.ToLower(showProgress) {
+	case "":
+		return true // Default to on
+	case "yes":
+		return true
+	case "y":
+		return true
+	case "true":
+		return true
+	case "1":
+		return true
+	case "no":
+		return false
+	case "n":
+		return false
+	case "false":
+		return false
+	case "0":
+		return false
+	}
+	// TODO: default: error
+
+	return true
+}
+
+func Writer(w io.Writer, header string, total int64, config config.Config) io.Writer {
+	if !showProgress(config) {
+		return w
+	}
+	prog := &progress{
+		header: header,
+		total:  total,
+	}
+	out := io.MultiWriter(w, prog)
+	return out
+}
+
+func Finish(config config.Config) {
+	if showProgress(config) {
+		// Add a newline after the progress bar
+		fmt.Println()
+	}
+}
+
+func (p *progress) Write(buf []byte) (int, error) {
+	l := len(buf)
+	p.current += int64(l)
+	p.ShowProgress()
+	return l, nil
+}
+
+// Writes the current download progress to stdout.
+func (p *progress) ShowProgress() {
+	message := fmt.Sprintf("%s: %s out of %s (%s)",
+		p.header,
+		formatMb(p.current),
+		formatMb(p.total),
+		formatPercentage(p.current, p.total))
+
+	if message == p.lastMessage {
+		return
+	}
+
+	// Clear the line.
+	fmt.Printf("\r%s", strings.Repeat(" ", 39))
+
+	// Show a message, don't add a newline
+	fmt.Printf("\r%s", message)
+	p.lastMessage = message
+}
+
+func formatMb(size int64) string {
+	// TODO: Use units other than MB
+	inMb := size / (1024 * 1024)
+	return fmt.Sprintf("%d MB", inMb)
+}
+
+func formatPercentage(current, size int64) string {
+	percentage := current * 100 / size
+	return fmt.Sprintf("%d%%", percentage)
+}
diff --git a/repositories/BUILD b/repositories/BUILD
index c612bf4..6e04af4 100644
--- a/repositories/BUILD
+++ b/repositories/BUILD
@@ -9,6 +9,7 @@ go_library(
     importpath = "github.com/bazelbuild/bazelisk/repositories",
     visibility = ["//visibility:public"],
     deps = [
+        "//config:go_default_library",
         "//core:go_default_library",
         "//httputil:go_default_library",
         "//platforms:go_default_library",
diff --git a/repositories/gcs.go b/repositories/gcs.go
index db6f0b8..d5a107d 100644
--- a/repositories/gcs.go
+++ b/repositories/gcs.go
@@ -11,6 +11,7 @@ import (
 	"strings"
 	"time"
 
+	"github.com/bazelbuild/bazelisk/config"
 	"github.com/bazelbuild/bazelisk/core"
 	"github.com/bazelbuild/bazelisk/httputil"
 	"github.com/bazelbuild/bazelisk/platforms"
@@ -137,14 +138,14 @@ type GcsListResponse struct {
 }
 
 // DownloadRelease downloads the given Bazel release into the specified location and returns the absolute path.
-func (gcs *GCSRepo) DownloadRelease(version, destDir, destFile string) (string, error) {
+func (gcs *GCSRepo) DownloadRelease(version, destDir, destFile string, config config.Config) (string, error) {
 	srcFile, err := platforms.DetermineBazelFilename(version, true)
 	if err != nil {
 		return "", err
 	}
 
 	url := fmt.Sprintf("%s/%s/release/%s", candidateBaseURL, version, srcFile)
-	return httputil.DownloadBinary(url, destDir, destFile)
+	return httputil.DownloadBinary(url, destDir, destFile, config)
 }
 
 func (gcs *GCSRepo) removeCandidates(history []string, filter core.ReleaseFilter) ([]string, error) {
@@ -229,7 +230,7 @@ func (gcs *GCSRepo) GetCandidateVersions(bazeliskHome string) ([]string, error)
 }
 
 // DownloadCandidate downloads the given release candidate into the specified location and returns the absolute path.
-func (gcs *GCSRepo) DownloadCandidate(version, destDir, destFile string) (string, error) {
+func (gcs *GCSRepo) DownloadCandidate(version, destDir, destFile string, config config.Config) (string, error) {
 	if !strings.Contains(version, "rc") {
 		return "", fmt.Errorf("'%s' does not refer to a release candidate", version)
 	}
@@ -243,7 +244,7 @@ func (gcs *GCSRepo) DownloadCandidate(version, destDir, destFile string) (string
 	baseVersion := versionComponents[0]
 	rcVersion := "rc" + versionComponents[1]
 	url := fmt.Sprintf("%s/%s/%s/%s", candidateBaseURL, baseVersion, rcVersion, srcFile)
-	return httputil.DownloadBinary(url, destDir, destFile)
+	return httputil.DownloadBinary(url, destDir, destFile, config)
 }
 
 // CommitRepo
@@ -261,14 +262,14 @@ 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) {
+func (gcs *GCSRepo) DownloadAtCommit(commit, destDir, destFile string, config config.Config) (string, error) {
 	log.Printf("Using unreleased version at commit %s", 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)
+	return httputil.DownloadBinary(url, destDir, destFile, config)
 }
 
 // RollingRepo
@@ -297,7 +298,7 @@ func (gcs *GCSRepo) GetRollingVersions(bazeliskHome string) ([]string, error) {
 }
 
 // DownloadRolling downloads the given Bazel version into the specified location and returns the absolute path.
-func (gcs *GCSRepo) DownloadRolling(version, destDir, destFile string) (string, error) {
+func (gcs *GCSRepo) DownloadRolling(version, destDir, destFile string, config config.Config) (string, error) {
 	srcFile, err := platforms.DetermineBazelFilename(version, true)
 	if err != nil {
 		return "", err
@@ -305,5 +306,5 @@ func (gcs *GCSRepo) DownloadRolling(version, destDir, destFile string) (string,
 
 	releaseVersion := strings.Split(version, "-")[0]
 	url := fmt.Sprintf("%s/%s/rolling/%s/%s", candidateBaseURL, releaseVersion, version, srcFile)
-	return httputil.DownloadBinary(url, destDir, destFile)
+	return httputil.DownloadBinary(url, destDir, destFile, config)
 }
diff --git a/repositories/github.go b/repositories/github.go
index 0d02067..0dddd3c 100644
--- a/repositories/github.go
+++ b/repositories/github.go
@@ -4,6 +4,7 @@ import (
 	"encoding/json"
 	"fmt"
 
+	"github.com/bazelbuild/bazelisk/config"
 	"github.com/bazelbuild/bazelisk/httputil"
 	"github.com/bazelbuild/bazelisk/platforms"
 )
@@ -84,11 +85,11 @@ type gitHubRelease struct {
 }
 
 // DownloadVersion downloads a Bazel binary for the given version and fork to the specified location and returns the absolute path.
-func (gh *GitHubRepo) DownloadVersion(fork, version, destDir, destFile string) (string, error) {
+func (gh *GitHubRepo) DownloadVersion(fork, version, destDir, destFile string, config config.Config) (string, error) {
 	filename, err := platforms.DetermineBazelFilename(version, true)
 	if err != nil {
 		return "", err
 	}
 	url := fmt.Sprintf(urlPattern, fork, version, filename)
-	return httputil.DownloadBinary(url, destDir, destFile)
+	return httputil.DownloadBinary(url, destDir, destFile, config)
 }
diff --git a/ws/ws.go b/ws/ws.go
index 653090d..63b314d 100644
--- a/ws/ws.go
+++ b/ws/ws.go
@@ -7,12 +7,10 @@ import (
 
 // FindWorkspaceRoot returns the root directory of the Bazel workspace in which the passed root exists, if any.
 func FindWorkspaceRoot(root string) string {
-	if isValidWorkspace(filepath.Join(root, "WORKSPACE")) {
-		return root
-	}
-
-	if isValidWorkspace(filepath.Join(root, "WORKSPACE.bazel")) {
-		return root
+	for _, boundary := range [...]string{"MODULE.bazel", "REPO.bazel", "WORKSPACE.bazel", "WORKSPACE"} {
+		if isValidWorkspace(filepath.Join(root, boundary)) {
+			return root
+		}
 	}
 
 	parentDirectory := filepath.Dir(root)
@@ -23,9 +21,9 @@ func FindWorkspaceRoot(root string) string {
 	return FindWorkspaceRoot(parentDirectory)
 }
 
-// isValidWorkspace returns true iff the supplied path is the workspace root, defined by the presence of
-// a file named WORKSPACE or WORKSPACE.bazel
-// see https://github.com/bazelbuild/bazel/blob/8346ea4cfdd9fbd170d51a528fee26f912dad2d5/src/main/cpp/workspace_layout.cc#L37
+// isValidWorkspace returns true if the supplied path is the workspace root, defined by the presence of
+// a file named MODULE.bazel, REPO.bazel, WORKSPACE.bazel, or WORKSPACE
+// see https://github.com/bazelbuild/bazel/blob/6.3.0/src/main/cpp/workspace_layout.cc#L34
 func isValidWorkspace(path string) bool {
 	info, err := os.Stat(path)
 	if err != nil {


Test patch:
diff --git a/bazelisk_test.sh b/bazelisk_test.sh
index 95b51d6..fded3a7 100755
--- a/bazelisk_test.sh
+++ b/bazelisk_test.sh
@@ -544,9 +544,10 @@ if [[ $BAZELISK_VERSION == "GO" ]]; then
   test_bazel_prepend_binary_directory_to_path_go
   echo
 
-  echo "# test_path_is_consistent_regardless_of_base_url"
-  test_path_is_consistent_regardless_of_base_url
-  echo
+  # Disabled due to https://github.com/bazelbuild/bazelisk/pull/465#issuecomment-1805905082
+  # echo "# test_path_is_consistent_regardless_of_base_url"
+  # test_path_is_consistent_regardless_of_base_url
+  # echo
 
   case "$(uname -s)" in
     MSYS*)
diff --git a/bazelisk_version_test.go b/bazelisk_version_test.go
index e542372..61579f5 100644
--- a/bazelisk_version_test.go
+++ b/bazelisk_version_test.go
@@ -9,6 +9,7 @@ import (
 	"strings"
 	"testing"
 
+	"github.com/bazelbuild/bazelisk/config"
 	"github.com/bazelbuild/bazelisk/core"
 	"github.com/bazelbuild/bazelisk/httputil"
 	"github.com/bazelbuild/bazelisk/repositories"
@@ -42,7 +43,7 @@ func TestResolveVersion(t *testing.T) {
 
 	gcs := &repositories.GCSRepo{}
 	repos := core.CreateRepositories(nil, gcs, nil, nil, nil, false)
-	version, _, err := repos.ResolveVersion(tmpDir, versions.BazelUpstream, "4.0.0")
+	version, _, err := repos.ResolveVersion(tmpDir, versions.BazelUpstream, "4.0.0", config.Null())
 
 	if err != nil {
 		t.Fatalf("Version resolution failed unexpectedly: %v", err)
@@ -60,7 +61,7 @@ func TestResolvePatchVersion(t *testing.T) {
 
 	gcs := &repositories.GCSRepo{}
 	repos := core.CreateRepositories(nil, gcs, nil, nil, nil, false)
-	version, _, err := repos.ResolveVersion(tmpDir, versions.BazelUpstream, "4.0.0-patch1")
+	version, _, err := repos.ResolveVersion(tmpDir, versions.BazelUpstream, "4.0.0-patch1", config.Null())
 
 	if err != nil {
 		t.Fatalf("Version resolution failed unexpectedly: %v", err)
@@ -82,7 +83,7 @@ func TestResolveLatestRcVersion(t *testing.T) {
 
 	gcs := &repositories.GCSRepo{}
 	repos := core.CreateRepositories(nil, gcs, nil, nil, nil, false)
-	version, _, err := repos.ResolveVersion(tmpDir, versions.BazelUpstream, "last_rc")
+	version, _, err := repos.ResolveVersion(tmpDir, versions.BazelUpstream, "last_rc", config.Null())
 
 	if err != nil {
 		t.Fatalf("Version resolution failed unexpectedly: %v", err)
@@ -100,7 +101,7 @@ func TestResolveLatestRcVersion_WithFullRelease(t *testing.T) {
 
 	gcs := &repositories.GCSRepo{}
 	repos := core.CreateRepositories(nil, gcs, nil, nil, nil, false)
-	version, _, err := repos.ResolveVersion(tmpDir, versions.BazelUpstream, "last_rc")
+	version, _, err := repos.ResolveVersion(tmpDir, versions.BazelUpstream, "last_rc", config.Null())
 
 	if err != nil {
 		t.Fatalf("Version resolution failed unexpectedly: %v", err)
@@ -120,7 +121,7 @@ func TestResolveLatestVersion_TwoLatestVersionsDoNotHaveAReleaseYet(t *testing.T
 
 	gcs := &repositories.GCSRepo{}
 	repos := core.CreateRepositories(gcs, nil, nil, nil, nil, false)
-	version, _, err := repos.ResolveVersion(tmpDir, versions.BazelUpstream, "latest")
+	version, _, err := repos.ResolveVersion(tmpDir, versions.BazelUpstream, "latest", config.Null())
 
 	if err != nil {
 		t.Fatalf("Version resolution failed unexpectedly: %v", err)
@@ -142,7 +143,7 @@ func TestResolveLatestVersion_ShouldOnlyReturnStableReleases(t *testing.T) {
 
 	gcs := &repositories.GCSRepo{}
 	repos := core.CreateRepositories(gcs, nil, nil, nil, nil, false)
-	version, _, err := repos.ResolveVersion(tmpDir, versions.BazelUpstream, "latest-1")
+	version, _, err := repos.ResolveVersion(tmpDir, versions.BazelUpstream, "latest-1", config.Null())
 
 	if err != nil {
 		t.Fatalf("Version resolution failed unexpectedly: %v", err)
@@ -161,7 +162,7 @@ func TestResolveLatestVersion_ShouldFailIfNotEnoughReleases(t *testing.T) {
 
 	gcs := &repositories.GCSRepo{}
 	repos := core.CreateRepositories(gcs, nil, nil, nil, nil, false)
-	_, _, err := repos.ResolveVersion(tmpDir, versions.BazelUpstream, "latest-1")
+	_, _, err := repos.ResolveVersion(tmpDir, versions.BazelUpstream, "latest-1", config.Null())
 
 	if err == nil {
 		t.Fatal("Expected ResolveVersion() to fail.")
@@ -178,7 +179,7 @@ func TestResolveLatestVersion_GCSIsDown(t *testing.T) {
 
 	gcs := &repositories.GCSRepo{}
 	repos := core.CreateRepositories(gcs, nil, nil, nil, nil, false)
-	_, _, err := repos.ResolveVersion(tmpDir, versions.BazelUpstream, "latest")
+	_, _, err := repos.ResolveVersion(tmpDir, versions.BazelUpstream, "latest", config.Null())
 
 	if err == nil {
 		t.Fatal("Expected resolveLatestVersion() to fail.")
@@ -196,7 +197,7 @@ func TestResolveLatestVersion_GitHubIsDown(t *testing.T) {
 	gh := repositories.CreateGitHubRepo("test_token")
 	repos := core.CreateRepositories(nil, nil, gh, nil, nil, false)
 
-	_, _, err := repos.ResolveVersion(tmpDir, "some_fork", "latest")
+	_, _, err := repos.ResolveVersion(tmpDir, "some_fork", "latest", config.Null())
 
 	if err == nil {
 		t.Fatal("Expected resolveLatestVersion() to fail.")
@@ -212,7 +213,7 @@ func TestAcceptRollingReleaseName(t *testing.T) {
 	repos := core.CreateRepositories(nil, nil, nil, nil, gcs, false)
 
 	for _, version := range []string{"10.0.0-pre.20201103.4", "10.0.0-pre.20201103.4.2"} {
-		resolvedVersion, _, err := repos.ResolveVersion(tmpDir, "", version)
+		resolvedVersion, _, err := repos.ResolveVersion(tmpDir, "", version, config.Null())
 
 		if err != nil {
 			t.Fatalf("ResolveVersion(%q, \"\", %q): expected no error, but got %v", tmpDir, version, err)
@@ -233,7 +234,7 @@ func TestResolveLatestRollingRelease(t *testing.T) {
 	gcs := &repositories.GCSRepo{}
 	repos := core.CreateRepositories(nil, nil, nil, nil, gcs, false)
 
-	version, _, err := repos.ResolveVersion(tmpDir, "", rollingReleaseIdentifier)
+	version, _, err := repos.ResolveVersion(tmpDir, "", rollingReleaseIdentifier, config.Null())
 
 	if err != nil {
 		t.Fatalf("ResolveVersion(%q, \"\", %q): expected no error, but got %v", tmpDir, rollingReleaseIdentifier, err)
@@ -257,7 +258,7 @@ func TestAcceptFloatingReleaseVersions(t *testing.T) {
 
 	gcs := &repositories.GCSRepo{}
 	repos := core.CreateRepositories(gcs, nil, nil, nil, nil, false)
-	version, _, err := repos.ResolveVersion(tmpDir, versions.BazelUpstream, "4.x")
+	version, _, err := repos.ResolveVersion(tmpDir, versions.BazelUpstream, "4.x", config.Null())
 
 	if err != nil {
 		t.Fatalf("Version resolution failed unexpectedly: %v", err)
diff --git a/httputil/progress/progress_test.go b/httputil/progress/progress_test.go
new file mode 100644
index 0000000..0ff9b96
--- /dev/null
+++ b/httputil/progress/progress_test.go
@@ -0,0 +1,52 @@
+package progress
+
+import (
+	"testing"
+)
+
+func TestFormatMb(t *testing.T) {
+	type test struct {
+		input int64
+		want  string
+	}
+	tests := []test{
+		{input: 48*1024*1024 + 512, want: "48 MB"},
+		{input: 58538527, want: "55 MB"},
+		{input: 0, want: "0 MB"},
+		{input: 48*1024*1024 - 1, want: "47 MB"},
+		{input: 48 * 1024 * 1024, want: "48 MB"},
+		{input: 48 * 1024 * 1024 * 1024, want: "49152 MB"},
+	}
+
+	for _, tc := range tests {
+		name := tc.want
+		t.Run(name, func(t *testing.T) {
+			got := formatMb(tc.input)
+			if got != tc.want {
+				t.Fatalf("Expected %q, but got %q", tc.want, got)
+			}
+		})
+	}
+}
+
+func TestFormatPercentage(t *testing.T) {
+	type test struct {
+		curr, total int64
+		want        string
+	}
+	tests := []test{
+		{curr: 0, total: 1000, want: "0%"},
+		{curr: 1000, total: 1000, want: "100%"},
+		{curr: 500, total: 1000, want: "50%"},
+	}
+
+	for _, tc := range tests {
+		name := tc.want
+		t.Run(name, func(t *testing.T) {
+			got := formatPercentage(tc.curr, tc.total)
+			if got != tc.want {
+				t.Fatalf("Expected %q, but got %q", tc.want, got)
+			}
+		})
+	}
+}