Skip to content

Commit 76873b5

Browse files
feat: support platform-specific SHA256 verification
Adds support for platform-specific BAZELISK_VERIFY_SHA256 variables, enabling SHA256 verification in multiplatform projects. Bazelisk now checks for configuration in the following order: 1. BAZELISK_VERIFY_SHA256_NOJDK_<OS>_<ARCH> (when BAZELISK_NOJDK is enabled) 2. BAZELISK_VERIFY_SHA256_<OS>_<ARCH> 3. BAZELISK_VERIFY_SHA256 (existing behavior, fallback) Implementation notes: - Refactored downloadBazelIfNecessary to compute platform info once upfront - Added FormatBazelFilename helper to avoid redundant OS/arch computation - Hash values normalized to lowercase for case-insensitive comparison - Full backward compatibility maintained Fixes #522
1 parent ac241ef commit 76873b5

6 files changed

Lines changed: 266 additions & 14 deletions

File tree

README.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,13 @@ Note: `last_downstream_green` support has been removed, please use `last_green`
7777

7878
## Where does Bazelisk get Bazel from?
7979

80-
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.
80+
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.
81+
82+
Since each platform has a different binary with a different hash, Bazelisk supports platform-specific hash variables:
83+
84+
- `BAZELISK_VERIFY_SHA256_<OS>_<ARCH>` - hash for a specific platform (e.g., `BAZELISK_VERIFY_SHA256_LINUX_X86_64`)
85+
- `BAZELISK_VERIFY_SHA256_NOJDK_<OS>_<ARCH>` - hash for nojdk builds (when `BAZELISK_NOJDK` is enabled)
86+
- `BAZELISK_VERIFY_SHA256` - fallback used when no platform-specific hash is set
8187

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

@@ -270,6 +276,8 @@ The following variables can be set:
270276
- `BAZELISK_SKIP_WRAPPER`
271277
- `BAZELISK_USER_AGENT`
272278
- `BAZELISK_VERIFY_SHA256`
279+
- `BAZELISK_VERIFY_SHA256_<OS>_<ARCH>` (e.g., `BAZELISK_VERIFY_SHA256_DARWIN_ARM64`)
280+
- `BAZELISK_VERIFY_SHA256_NOJDK_<OS>_<ARCH>` (e.g., `BAZELISK_VERIFY_SHA256_NOJDK_LINUX_X86_64`)
273281
- `USE_BAZEL_VERSION`
274282

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

281-
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`.
289+
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`.
282290

283291
## Requirements
284292

bazelisk_test.sh

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -448,6 +448,78 @@ function test_bazel_verify_sha256() {
448448
(echo "FAIL: Expected to find 'Build label' in the output of 'bazelisk version'"; exit 1)
449449
}
450450

451+
function test_bazel_verify_sha256_platform_specific() {
452+
setup
453+
454+
echo "9.0.0" > .bazelversion
455+
456+
local os="$(uname -s | tr A-Z a-z)"
457+
local arch="$(uname -m)"
458+
459+
# Map to bazelisk naming convention
460+
case "${arch}" in
461+
x86_64|amd64)
462+
arch="X86_64"
463+
;;
464+
arm64|aarch64)
465+
arch="ARM64"
466+
;;
467+
esac
468+
469+
# SHA256 hashes for Bazel 9.0.0 binaries.
470+
# Mixed case is intentional to test hash normalization.
471+
case "${os}" in
472+
darwin)
473+
os_upper="DARWIN"
474+
if [[ "${arch}" == "ARM64" ]]; then
475+
expected_sha256="2c3cce548a4b6a97A2A5267712187b784b52714c4a2b0613e7386b15669d783c"
476+
else
477+
expected_sha256="aa7e5fc364eaaba7f4f271dbf8c14172a5433f663cca6b130325df4b6569b3f0"
478+
fi
479+
;;
480+
linux)
481+
os_upper="LINUX"
482+
if [[ "${arch}" == "ARM64" ]]; then
483+
expected_sha256="cab23c59d3d39c5E5382F12cd116b47445afdff9813516c18ae3ee8836b3037f"
484+
else
485+
expected_sha256="C44A93f25398c68f904fa1d19b61d321de6c0d2f09dca375d7bc0dc9b9428403"
486+
fi
487+
;;
488+
msys*|mingw*|cygwin*)
489+
os_upper="WINDOWS"
490+
if [[ "${arch}" == "ARM64" ]]; then
491+
expected_sha256="ab3db0b1f129436180927Baa0f1f38e6d86a88fc9ee802572a76c9519a06f550"
492+
else
493+
expected_sha256="463faee497df2913854D80776784137cb47f42960b4ef4e4f85068c8da4849a8"
494+
fi
495+
;;
496+
*)
497+
echo "FAIL: Unknown OS ${os} in test"
498+
exit 1
499+
;;
500+
esac
501+
502+
local platform_var="BAZELISK_VERIFY_SHA256_${os_upper}_${arch}"
503+
504+
# Test 1: Platform-specific variable should work
505+
env BAZELISK_HOME="$BAZELISK_HOME" "${platform_var}=${expected_sha256}" \
506+
bazelisk version 2>&1 | tee log
507+
508+
grep "Build label:" log || \
509+
(echo "FAIL: Expected to find 'Build label' with platform-specific hash"; exit 1)
510+
511+
# Test 2: Platform-specific should take precedence over generic
512+
rm -rf "$BAZELISK_HOME/downloads"
513+
514+
env BAZELISK_HOME="$BAZELISK_HOME" \
515+
BAZELISK_VERIFY_SHA256="wrong-generic-hash" \
516+
"${platform_var}=${expected_sha256}" \
517+
bazelisk version 2>&1 | tee log
518+
519+
grep "Build label:" log || \
520+
(echo "FAIL: Platform-specific should take precedence over generic"; exit 1)
521+
}
522+
451523
function test_bazel_download_path_py() {
452524
setup
453525

@@ -570,6 +642,10 @@ if [[ $BAZELISK_VERSION == "GO" ]]; then
570642
test_bazel_verify_sha256
571643
echo
572644

645+
echo '# test_bazel_verify_sha256_platform_specific'
646+
test_bazel_verify_sha256_platform_specific
647+
echo
648+
573649
echo "# test_bazel_prepend_binary_directory_to_path_go"
574650
test_bazel_prepend_binary_directory_to_path_go
575651
echo

core/core.go

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -441,11 +441,17 @@ func downloadBazel(bazelVersionString string, bazeliskHome string, repos *Reposi
441441
// 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.
442442
// downloads/sha256/[sha256]/bin/bazel[extension] contains the bazel with a particular sha256.
443443
func downloadBazelIfNecessary(version string, bazeliskHome string, bazelForkOrURLDirName string, repos *Repositories, config config.Config, downloader DownloadFunc) (string, error) {
444-
pathSegment, err := platforms.DetermineBazelFilename(version, false, config)
444+
osName, err := platforms.DetermineOperatingSystem()
445445
if err != nil {
446-
return "", fmt.Errorf("could not determine path segment to use for Bazel binary: %v", err)
446+
return "", fmt.Errorf("could not determine operating system: %v", err)
447447
}
448+
arch, err := platforms.DetermineArchitecture(osName, version)
449+
if err != nil {
450+
return "", fmt.Errorf("could not determine architecture: %v", err)
451+
}
452+
isNojdk := platforms.IsNojdkEnabled(config)
448453

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

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

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

15091515
return completionScripts, nil
15101516
}
1517+
1518+
// getExpectedSHA256 returns the expected SHA256 hash for verification.
1519+
// It checks platform-specific keys first, then falls back to the generic key.
1520+
// Returns empty string if no hash is configured.
1521+
func getExpectedSHA256(cfg config.Config, osName, arch string, nojdk bool) string {
1522+
suffix := strings.ToUpper(osName) + "_" + strings.ToUpper(arch)
1523+
1524+
if nojdk {
1525+
if hash := cfg.Get("BAZELISK_VERIFY_SHA256_NOJDK_" + suffix); hash != "" {
1526+
return strings.ToLower(hash)
1527+
}
1528+
}
1529+
if hash := cfg.Get("BAZELISK_VERIFY_SHA256_" + suffix); hash != "" {
1530+
return strings.ToLower(hash)
1531+
}
1532+
return strings.ToLower(cfg.Get("BAZELISK_VERIFY_SHA256"))
1533+
}

core/core_test.go

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -888,3 +888,100 @@ func TestRunBazeliskWithStderrRedirection(t *testing.T) {
888888
t.Error("stdout content should not appear in stderr")
889889
}
890890
}
891+
892+
func TestGetExpectedSHA256(t *testing.T) {
893+
testCases := []struct {
894+
name string
895+
config map[string]string
896+
osName string
897+
arch string
898+
nojdk bool
899+
expected string
900+
}{
901+
{
902+
name: "platform-specific takes precedence",
903+
config: map[string]string{
904+
"BAZELISK_VERIFY_SHA256": "generic-hash",
905+
"BAZELISK_VERIFY_SHA256_LINUX_X86_64": "platform-hash",
906+
},
907+
osName: "linux",
908+
arch: "x86_64",
909+
nojdk: false,
910+
expected: "platform-hash",
911+
},
912+
{
913+
name: "falls back to generic when platform-specific not set",
914+
config: map[string]string{
915+
"BAZELISK_VERIFY_SHA256": "generic-hash",
916+
},
917+
osName: "darwin",
918+
arch: "arm64",
919+
nojdk: false,
920+
expected: "generic-hash",
921+
},
922+
{
923+
name: "empty when neither set",
924+
config: map[string]string{},
925+
osName: "windows",
926+
arch: "x86_64",
927+
nojdk: false,
928+
expected: "",
929+
},
930+
{
931+
name: "hash is normalized to lowercase",
932+
config: map[string]string{
933+
"BAZELISK_VERIFY_SHA256_LINUX_ARM64": "AbCdEf123456",
934+
},
935+
osName: "linux",
936+
arch: "arm64",
937+
nojdk: false,
938+
expected: "abcdef123456",
939+
},
940+
// nojdk test cases
941+
{
942+
name: "nojdk-specific takes precedence when nojdk enabled",
943+
config: map[string]string{
944+
"BAZELISK_VERIFY_SHA256": "generic-hash",
945+
"BAZELISK_VERIFY_SHA256_LINUX_X86_64": "platform-hash",
946+
"BAZELISK_VERIFY_SHA256_NOJDK_LINUX_X86_64": "nojdk-hash",
947+
},
948+
osName: "linux",
949+
arch: "x86_64",
950+
nojdk: true,
951+
expected: "nojdk-hash",
952+
},
953+
{
954+
name: "nojdk falls back to platform-specific when nojdk key not set",
955+
config: map[string]string{
956+
"BAZELISK_VERIFY_SHA256": "generic-hash",
957+
"BAZELISK_VERIFY_SHA256_LINUX_X86_64": "platform-hash",
958+
},
959+
osName: "linux",
960+
arch: "x86_64",
961+
nojdk: true,
962+
expected: "platform-hash",
963+
},
964+
{
965+
name: "nojdk key ignored when nojdk disabled",
966+
config: map[string]string{
967+
"BAZELISK_VERIFY_SHA256": "generic-hash",
968+
"BAZELISK_VERIFY_SHA256_LINUX_X86_64": "platform-hash",
969+
"BAZELISK_VERIFY_SHA256_NOJDK_LINUX_X86_64": "nojdk-hash",
970+
},
971+
osName: "linux",
972+
arch: "x86_64",
973+
nojdk: false,
974+
expected: "platform-hash",
975+
},
976+
}
977+
978+
for _, tc := range testCases {
979+
t.Run(tc.name, func(t *testing.T) {
980+
cfg := config.Static(tc.config)
981+
result := getExpectedSHA256(cfg, tc.osName, tc.arch, tc.nojdk)
982+
if result != tc.expected {
983+
t.Errorf("getExpectedSHA256() = %q, want %q", result, tc.expected)
984+
}
985+
})
986+
}
987+
}

platforms/platforms.go

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@ var supportedPlatforms = map[string]*platform{
3232
},
3333
}
3434

35+
// IsNojdkEnabled returns true if BAZELISK_NOJDK is set to a truthy value.
36+
func IsNojdkEnabled(config config.Config) bool {
37+
v := config.Get("BAZELISK_NOJDK")
38+
return v != "" && v != "0"
39+
}
40+
3541
// GetPlatform returns a Bazel CI-compatible platform identifier for the current operating system.
3642
// TODO(fweikert): raise an error for unsupported platforms
3743
func GetPlatform() (string, error) {
@@ -87,14 +93,6 @@ func DetermineOperatingSystem() (string, error) {
8793

8894
// DetermineBazelFilename returns the correct file name of a local Bazel binary.
8995
func DetermineBazelFilename(version string, includeSuffix bool, config config.Config) (string, error) {
90-
flavor := "bazel"
91-
92-
bazeliskNojdk := config.Get("BAZELISK_NOJDK")
93-
94-
if len(bazeliskNojdk) != 0 && bazeliskNojdk != "0" {
95-
flavor = "bazel_nojdk"
96-
}
97-
9896
osName, err := DetermineOperatingSystem()
9997
if err != nil {
10098
return "", err
@@ -105,12 +103,22 @@ func DetermineBazelFilename(version string, includeSuffix bool, config config.Co
105103
return "", err
106104
}
107105

106+
return FormatBazelFilename(version, includeSuffix, osName, machineName, IsNojdkEnabled(config)), nil
107+
}
108+
109+
// FormatBazelFilename formats a Bazel binary filename from pre-computed platform values.
110+
func FormatBazelFilename(version string, includeSuffix bool, osName, machineName string, nojdk bool) string {
111+
flavor := "bazel"
112+
if nojdk {
113+
flavor = "bazel_nojdk"
114+
}
115+
108116
var filenameSuffix string
109117
if includeSuffix {
110118
filenameSuffix = DetermineExecutableFilenameSuffix()
111119
}
112120

113-
return fmt.Sprintf("%s-%s-%s-%s%s", flavor, version, osName, machineName, filenameSuffix), nil
121+
return fmt.Sprintf("%s-%s-%s-%s%s", flavor, version, osName, machineName, filenameSuffix)
114122
}
115123

116124
// DetermineBazelInstallerFilename returns the correct file name of a Bazel installer script.

platforms/platforms_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,46 @@ package platforms
22

33
import "testing"
44

5+
func TestFormatBazelFilename(t *testing.T) {
6+
tests := []struct {
7+
name string
8+
version string
9+
includeSuffix bool
10+
osName string
11+
machineName string
12+
nojdk bool
13+
want string
14+
}{
15+
{
16+
name: "standard bazel binary",
17+
version: "7.1.1",
18+
includeSuffix: false,
19+
osName: "linux",
20+
machineName: "x86_64",
21+
nojdk: false,
22+
want: "bazel-7.1.1-linux-x86_64",
23+
},
24+
{
25+
name: "nojdk binary",
26+
version: "7.1.1",
27+
includeSuffix: false,
28+
osName: "linux",
29+
machineName: "x86_64",
30+
nojdk: true,
31+
want: "bazel_nojdk-7.1.1-linux-x86_64",
32+
},
33+
}
34+
35+
for _, tt := range tests {
36+
t.Run(tt.name, func(t *testing.T) {
37+
got := FormatBazelFilename(tt.version, tt.includeSuffix, tt.osName, tt.machineName, tt.nojdk)
38+
if got != tt.want {
39+
t.Errorf("FormatBazelFilename() = %v, want %v", got, tt.want)
40+
}
41+
})
42+
}
43+
}
44+
545
func TestDarwinFallback(t *testing.T) {
646
type args struct {
747
machineName string

0 commit comments

Comments
 (0)