Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,13 @@ Note: `last_downstream_green` support has been removed, please use `last_green`

## Where does Bazelisk get Bazel from?

By default Bazelisk retrieves Bazel releases, release candidates and binaries built at green commits from Google Cloud Storage. The downloaded artifacts are validated against the SHA256 value recorded in `BAZELISK_VERIFY_SHA256` if this variable is set in the configuration file.
By default Bazelisk retrieves Bazel releases, release candidates and binaries built at green commits from Google Cloud Storage. The downloaded artifacts can be validated against an expected SHA256 hash.

Since each platform has a different binary with a different hash, Bazelisk supports platform-specific hash variables:

- `BAZELISK_VERIFY_SHA256_<OS>_<ARCH>` - hash for a specific platform (e.g., `BAZELISK_VERIFY_SHA256_LINUX_X86_64`)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: can you please enumerate all valid values here? There might be some confusion regarding darwin vs macos or arm64 vs aarch64.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Absolutely! And agreed. Thanks for the feedback and review Florian. I should resume working in this during the weekend.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the feature (and the tests)!

- `BAZELISK_VERIFY_SHA256_NOJDK_<OS>_<ARCH>` - hash for nojdk builds (when `BAZELISK_NOJDK` is enabled)
- `BAZELISK_VERIFY_SHA256` - fallback used when no platform-specific hash is set

As mentioned in the previous section, the `<FORK>/<VERSION>` version format allows you to use your own Bazel fork hosted on GitHub:

Expand Down Expand Up @@ -270,6 +276,8 @@ The following variables can be set:
- `BAZELISK_SKIP_WRAPPER`
- `BAZELISK_USER_AGENT`
- `BAZELISK_VERIFY_SHA256`
- `BAZELISK_VERIFY_SHA256_<OS>_<ARCH>` (e.g., `BAZELISK_VERIFY_SHA256_DARWIN_ARM64`)
- `BAZELISK_VERIFY_SHA256_NOJDK_<OS>_<ARCH>` (e.g., `BAZELISK_VERIFY_SHA256_NOJDK_LINUX_X86_64`)
- `USE_BAZEL_VERSION`

Configuration variables are evaluated with precedence order. The preferred values are derived in order from highest to lowest precedence as follows:
Expand All @@ -278,7 +286,7 @@ Configuration variables are evaluated with precedence order. The preferred value
* Variables defined in the workspace root `.bazeliskrc`
* Variables defined in the user home `.bazeliskrc`

Additionally, the Bazelisk home directory is also evaluated in precedence order. The preferred value is OS-specific e.g. `BAZELISK_HOME_LINUX`, then we fall back to `BAZELISK_HOME`.
Some variables support platform-specific variants. For example, `BAZELISK_HOME_LINUX` takes precedence over `BAZELISK_HOME`, and `BAZELISK_VERIFY_SHA256_DARWIN_ARM64` takes precedence over `BAZELISK_VERIFY_SHA256`.

## Requirements

Expand Down
76 changes: 76 additions & 0 deletions bazelisk_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,78 @@ function test_bazel_verify_sha256() {
(echo "FAIL: Expected to find 'Build label' in the output of 'bazelisk version'"; exit 1)
}

function test_bazel_verify_sha256_platform_specific() {
setup

echo "9.0.0" > .bazelversion

local os="$(uname -s | tr A-Z a-z)"
local arch="$(uname -m)"

# Map to bazelisk naming convention
case "${arch}" in
x86_64|amd64)
arch="X86_64"
;;
arm64|aarch64)
arch="ARM64"
;;
esac

# SHA256 hashes for Bazel 9.0.0 binaries.
# Mixed case is intentional to test hash normalization.
case "${os}" in
darwin)
os_upper="DARWIN"
if [[ "${arch}" == "ARM64" ]]; then
expected_sha256="2c3cce548a4b6a97A2A5267712187b784b52714c4a2b0613e7386b15669d783c"
else
expected_sha256="aa7e5fc364eaaba7f4f271dbf8c14172a5433f663cca6b130325df4b6569b3f0"
fi
;;
linux)
os_upper="LINUX"
if [[ "${arch}" == "ARM64" ]]; then
expected_sha256="cab23c59d3d39c5E5382F12cd116b47445afdff9813516c18ae3ee8836b3037f"
else
expected_sha256="C44A93f25398c68f904fa1d19b61d321de6c0d2f09dca375d7bc0dc9b9428403"
fi
;;
msys*|mingw*|cygwin*)
os_upper="WINDOWS"
if [[ "${arch}" == "ARM64" ]]; then
expected_sha256="ab3db0b1f129436180927Baa0f1f38e6d86a88fc9ee802572a76c9519a06f550"
else
expected_sha256="463faee497df2913854D80776784137cb47f42960b4ef4e4f85068c8da4849a8"
fi
;;
*)
echo "FAIL: Unknown OS ${os} in test"
exit 1
;;
esac

local platform_var="BAZELISK_VERIFY_SHA256_${os_upper}_${arch}"

# Test 1: Platform-specific variable should work
env BAZELISK_HOME="$BAZELISK_HOME" "${platform_var}=${expected_sha256}" \
bazelisk version 2>&1 | tee log

grep "Build label:" log || \
(echo "FAIL: Expected to find 'Build label' with platform-specific hash"; exit 1)

# Test 2: Platform-specific should take precedence over generic
rm -rf "$BAZELISK_HOME/downloads"

env BAZELISK_HOME="$BAZELISK_HOME" \
BAZELISK_VERIFY_SHA256="wrong-generic-hash" \
"${platform_var}=${expected_sha256}" \
bazelisk version 2>&1 | tee log

grep "Build label:" log || \
(echo "FAIL: Platform-specific should take precedence over generic"; exit 1)
}

function test_bazel_download_path_py() {
setup

Expand Down Expand Up @@ -578,6 +650,10 @@ if [[ $BAZELISK_VERSION == "GO" ]]; then
test_bazel_verify_sha256
echo

echo '# test_bazel_verify_sha256_platform_specific'
test_bazel_verify_sha256_platform_specific
echo

echo "# test_bazel_prepend_binary_directory_to_path_go"
test_bazel_prepend_binary_directory_to_path_go
echo
Expand Down
30 changes: 27 additions & 3 deletions core/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -441,11 +441,17 @@ func downloadBazel(bazelVersionString string, bazeliskHome string, repos *Reposi
// downloads/metadata/[fork-or-url]/bazel-[version-os-etc] is a text file containing a hex sha256 of the contents of the downloaded bazel file.
// downloads/sha256/[sha256]/bin/bazel[extension] contains the bazel with a particular sha256.
func downloadBazelIfNecessary(version string, bazeliskHome string, bazelForkOrURLDirName string, repos *Repositories, config config.Config, downloader DownloadFunc) (string, error) {
pathSegment, err := platforms.DetermineBazelFilename(version, false, config)
osName, err := platforms.DetermineOperatingSystem()
if err != nil {
return "", fmt.Errorf("could not determine path segment to use for Bazel binary: %v", err)
return "", fmt.Errorf("could not determine operating system: %v", err)
}
arch, err := platforms.DetermineArchitecture(osName, version)
if err != nil {
return "", fmt.Errorf("could not determine architecture: %v", err)
}
isNojdk := platforms.IsNojdkEnabled(config)

pathSegment := platforms.FormatBazelFilename(version, false, osName, arch, isNojdk)
destFile := "bazel" + platforms.DetermineExecutableFilenameSuffix()

mappingPath := filepath.Join(bazeliskHome, "downloads", "metadata", bazelForkOrURLDirName, pathSegment)
Expand All @@ -462,7 +468,7 @@ func downloadBazelIfNecessary(version string, bazeliskHome string, bazelForkOrUR
return "", fmt.Errorf("failed to download bazel: %w", err)
}

expectedSha256 := strings.ToLower(config.Get("BAZELISK_VERIFY_SHA256"))
expectedSha256 := getExpectedSHA256(config, osName, arch, isNojdk)
if len(expectedSha256) > 0 {
if expectedSha256 != downloadedDigest {
return "", fmt.Errorf("%s has sha256=%s but need sha256=%s", pathToBazelInCAS, downloadedDigest, expectedSha256)
Expand Down Expand Up @@ -1508,3 +1514,21 @@ func extractCompletionScriptsFromZip(zipData []byte) (map[string]string, error)

return completionScripts, nil
}

// getExpectedSHA256 returns the expected SHA256 hash for verification.
// It checks platform-specific keys first, then falls back to the generic key.
// Returns empty string if no hash is configured.
// TODO(#767): Normalize hash values at the config layer to avoid repetitive ToLower calls.
func getExpectedSHA256(cfg config.Config, osName, arch string, nojdk bool) string {
suffix := strings.ToUpper(osName) + "_" + strings.ToUpper(arch)

if nojdk {
if hash := cfg.Get("BAZELISK_VERIFY_SHA256_NOJDK_" + suffix); hash != "" {
return strings.ToLower(hash)
}
}
if hash := cfg.Get("BAZELISK_VERIFY_SHA256_" + suffix); hash != "" {
return strings.ToLower(hash)
}
return strings.ToLower(cfg.Get("BAZELISK_VERIFY_SHA256"))
}
97 changes: 97 additions & 0 deletions core/core_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -888,3 +888,100 @@ func TestRunBazeliskWithStderrRedirection(t *testing.T) {
t.Error("stdout content should not appear in stderr")
}
}

func TestGetExpectedSHA256(t *testing.T) {
testCases := []struct {
name string
config map[string]string
osName string
arch string
nojdk bool
expected string
}{
{
name: "platform-specific takes precedence",
config: map[string]string{
"BAZELISK_VERIFY_SHA256": "generic-hash",
"BAZELISK_VERIFY_SHA256_LINUX_X86_64": "platform-hash",
},
osName: "linux",
arch: "x86_64",
nojdk: false,
expected: "platform-hash",
},
{
name: "falls back to generic when platform-specific not set",
config: map[string]string{
"BAZELISK_VERIFY_SHA256": "generic-hash",
},
osName: "darwin",
arch: "arm64",
nojdk: false,
expected: "generic-hash",
},
{
name: "empty when neither set",
config: map[string]string{},
osName: "windows",
arch: "x86_64",
nojdk: false,
expected: "",
},
{
name: "hash is normalized to lowercase",
config: map[string]string{
"BAZELISK_VERIFY_SHA256_LINUX_ARM64": "AbCdEf123456",
},
osName: "linux",
arch: "arm64",
nojdk: false,
expected: "abcdef123456",
},
// nojdk test cases
{
name: "nojdk-specific takes precedence when nojdk enabled",
config: map[string]string{
"BAZELISK_VERIFY_SHA256": "generic-hash",
"BAZELISK_VERIFY_SHA256_LINUX_X86_64": "platform-hash",
"BAZELISK_VERIFY_SHA256_NOJDK_LINUX_X86_64": "nojdk-hash",
},
osName: "linux",
arch: "x86_64",
nojdk: true,
expected: "nojdk-hash",
},
{
name: "nojdk falls back to platform-specific when nojdk key not set",
config: map[string]string{
"BAZELISK_VERIFY_SHA256": "generic-hash",
"BAZELISK_VERIFY_SHA256_LINUX_X86_64": "platform-hash",
},
osName: "linux",
arch: "x86_64",
nojdk: true,
expected: "platform-hash",
},
{
name: "nojdk key ignored when nojdk disabled",
config: map[string]string{
"BAZELISK_VERIFY_SHA256": "generic-hash",
"BAZELISK_VERIFY_SHA256_LINUX_X86_64": "platform-hash",
"BAZELISK_VERIFY_SHA256_NOJDK_LINUX_X86_64": "nojdk-hash",
},
osName: "linux",
arch: "x86_64",
nojdk: false,
expected: "platform-hash",
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
cfg := config.Static(tc.config)
result := getExpectedSHA256(cfg, tc.osName, tc.arch, tc.nojdk)
if result != tc.expected {
t.Errorf("getExpectedSHA256() = %q, want %q", result, tc.expected)
}
})
}
}
26 changes: 17 additions & 9 deletions platforms/platforms.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ var supportedPlatforms = map[string]*platform{
},
}

// IsNojdkEnabled returns true if BAZELISK_NOJDK is set to a truthy value.
func IsNojdkEnabled(config config.Config) bool {
v := config.Get("BAZELISK_NOJDK")
return v != "" && v != "0"
}

// GetPlatform returns a Bazel CI-compatible platform identifier for the current operating system.
// TODO(fweikert): raise an error for unsupported platforms
func GetPlatform() (string, error) {
Expand Down Expand Up @@ -87,14 +93,6 @@ func DetermineOperatingSystem() (string, error) {

// DetermineBazelFilename returns the correct file name of a local Bazel binary.
func DetermineBazelFilename(version string, includeSuffix bool, config config.Config) (string, error) {
flavor := "bazel"

bazeliskNojdk := config.Get("BAZELISK_NOJDK")

if len(bazeliskNojdk) != 0 && bazeliskNojdk != "0" {
flavor = "bazel_nojdk"
}

osName, err := DetermineOperatingSystem()
if err != nil {
return "", err
Expand All @@ -105,12 +103,22 @@ func DetermineBazelFilename(version string, includeSuffix bool, config config.Co
return "", err
}

return FormatBazelFilename(version, includeSuffix, osName, machineName, IsNojdkEnabled(config)), nil
}

// FormatBazelFilename formats a Bazel binary filename from pre-computed platform values.
func FormatBazelFilename(version string, includeSuffix bool, osName, machineName string, nojdk bool) string {
flavor := "bazel"
if nojdk {
flavor = "bazel_nojdk"
}

var filenameSuffix string
if includeSuffix {
filenameSuffix = DetermineExecutableFilenameSuffix()
}

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

// DetermineBazelInstallerFilename returns the correct file name of a Bazel installer script.
Expand Down
40 changes: 40 additions & 0 deletions platforms/platforms_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,46 @@ package platforms

import "testing"

func TestFormatBazelFilename(t *testing.T) {
tests := []struct {
name string
version string
includeSuffix bool
osName string
machineName string
nojdk bool
want string
}{
{
name: "standard bazel binary",
version: "7.1.1",
includeSuffix: false,
osName: "linux",
machineName: "x86_64",
nojdk: false,
want: "bazel-7.1.1-linux-x86_64",
},
{
name: "nojdk binary",
version: "7.1.1",
includeSuffix: false,
osName: "linux",
machineName: "x86_64",
nojdk: true,
want: "bazel_nojdk-7.1.1-linux-x86_64",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := FormatBazelFilename(tt.version, tt.includeSuffix, tt.osName, tt.machineName, tt.nojdk)
if got != tt.want {
t.Errorf("FormatBazelFilename() = %v, want %v", got, tt.want)
}
})
}
}

func TestDarwinFallback(t *testing.T) {
type args struct {
machineName string
Expand Down