Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement support for BAZELISK_FORMAT_URL #427

Merged
merged 1 commit into from
May 16, 2023
Merged
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: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,21 @@ By default Bazelisk retrieves Bazel releases, release candidates and binaries bu

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

If you want to create a fork with your own releases, you have to follow the naming conventions that we use in `bazelbuild/bazel` for the binary file names.
If you want to create a fork with your own releases, you should follow the naming conventions that we use in `bazelbuild/bazel` for the binary file names as this results in predictable URLs that are similar to the official ones.
The URL format looks like `https://github.com/<FORK>/bazel/releases/download/<VERSION>/<FILENAME>`.

You can also override the URL by setting the environment variable `$BAZELISK_BASE_URL`. Bazelisk will then append `/<VERSION>/<FILENAME>` to the base URL instead of using the official release server. Bazelisk will read file [`~/.netrc`](https://everything.curl.dev/usingcurl/netrc) for credentials for Basic authentication.

If for any reason none of this works, you can also override the URL format altogether by setting the environment variable `$BAZELISK_FORMAT_URL`. This variable takes a format-like string with placeholders and performs the following replacements to compute the download URL:

- `%e`: Extension suffix, such as the empty string or `.exe`.
- `%h`: Value of `BAZELISK_VERIFY_SHA256`, respecting uppercase/lowercase characters.
- `%m`: Machine architecture name, such as `arm64` or `x86_64`.
- `%o`: Operating system name, such as `darwin` or `linux`.
- `%v`: Bazel version as determined by Bazelisk.
- `%%`: Literal `%` for escaping purposes.
- All other characters after `%` are reserved for future use and result in a processing error.

## Ensuring that your developers use Bazelisk rather than Bazel

Bazel installers typically provide Bazel's [shell wrapper script] as the `bazel` on the PATH.
Expand Down
23 changes: 20 additions & 3 deletions bazelisk_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,20 @@ function test_bazel_version_from_file() {
(echo "FAIL: Expected to find 'Build label: 5.0.0' in the output of 'bazelisk version'"; exit 1)
}

function test_bazel_version_from_url() {
function test_bazel_version_from_format_url() {
setup

echo "0.19.0" > .bazelversion

BAZELISK_FORMAT_URL="https://github.com/bazelbuild/bazel/releases/download/%v/bazel-%v-%o-%m%e" \
BAZELISK_HOME="$BAZELISK_HOME" \
bazelisk version 2>&1 | tee log

grep "Build label: 0.19.0" log || \
(echo "FAIL: Expected to find 'Build label: 0.19.0' in the output of 'bazelisk version'"; exit 1)
}

function test_bazel_version_from_base_url() {
setup

echo "0.19.0" > .bazelversion
Expand Down Expand Up @@ -427,8 +440,12 @@ if [[ $BAZELISK_VERSION == "GO" ]]; then
test_bazel_last_rc
echo

echo "# test_bazel_version_from_url"
test_bazel_version_from_url
echo "# test_bazel_version_from_format_url"
test_bazel_version_from_format_url
echo

echo "# test_bazel_version_from_base_url"
test_bazel_version_from_base_url
echo

echo "# test_bazel_version_prefer_environment_to_bazeliskrc"
Expand Down
5 changes: 4 additions & 1 deletion core/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ go_library(

go_test(
name = "go_default_test",
srcs = ["core_test.go"],
srcs = [
"core_test.go",
"repositories_test.go",
],
embed = [":go_default_library"],
)
10 changes: 8 additions & 2 deletions core/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -422,8 +422,14 @@ func downloadBazelIfNecessary(version string, baseDirectory string, repos *Repos
}

var tmpDestPath string
if url := GetEnvOrConfig(BaseURLEnv); url != "" {
tmpDestPath, err = repos.DownloadFromBaseURL(url, version, destDir, tmpDestFile)
baseURL := GetEnvOrConfig(BaseURLEnv)
formatURL := GetEnvOrConfig(FormatURLEnv)
if baseURL != "" && formatURL != "" {
return "", fmt.Errorf("cannot set %s and %s at once", BaseURLEnv, FormatURLEnv)
} else if formatURL != "" {
tmpDestPath, err = repos.DownloadFromFormatURL(formatURL, version, destDir, tmpDestFile)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I acknowledge that adding this feels very strange here because we have an input downloader function that should encapsulate this. However please note that the previous code was already hacking its way to handle the base URL case in the same way, so I just followed existing practice.

I think that this could be generalized to inject these behaviors via downloader functions... but I'd rather investigate that separately because there are lots of subtle consequences...

} else if baseURL != "" {
tmpDestPath, err = repos.DownloadFromBaseURL(baseURL, version, destDir, tmpDestFile)
} else {
tmpDestPath, err = downloader(destDir, tmpDestFile)
}
Expand Down
62 changes: 62 additions & 0 deletions core/repositories.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ import (
const (
// BaseURLEnv is the name of the environment variable that stores the base URL for downloads.
BaseURLEnv = "BAZELISK_BASE_URL"

// FormatURLEnv is the name of the environment variable that stores the format string to generate URLs for downloads.
FormatURLEnv = "BAZELISK_FORMAT_URL"
)

// DownloadFunc downloads a specific Bazel binary to the given location and returns the absolute path.
Expand Down Expand Up @@ -232,6 +235,65 @@ func (r *Repositories) DownloadFromBaseURL(baseURL, version, destDir, destFile s
return httputil.DownloadBinary(url, destDir, destFile)
}

func BuildURLFromFormat(formatURL, version string) (string, error) {
osName, err := platforms.DetermineOperatingSystem()
if err != nil {
return "", err
}

machineName, err := platforms.DetermineArchitecture(osName, version)
if err != nil {
return "", err
}

var b strings.Builder
b.Grow(len(formatURL) * 2) // Approximation.
for i := 0; i < len(formatURL); i++ {
ch := formatURL[i]
if ch == '%' {
i++
if i == len(formatURL) {
return "", errors.New("trailing %")
}

ch = formatURL[i]
switch ch {
case 'e':
b.WriteString(platforms.DetermineExecutableFilenameSuffix())
case 'h':
b.WriteString(GetEnvOrConfig("BAZELISK_VERIFY_SHA256"))
case 'm':
b.WriteString(machineName)
case 'o':
b.WriteString(osName)
case 'v':
b.WriteString(version)
case '%':
b.WriteByte('%')
default:
return "", fmt.Errorf("unknown placeholder %%%c", ch)
}
} else {
b.WriteByte(ch)
}
}
return b.String(), nil
}

// DownloadFromFormatURL can download Bazel binaries from a specific URL while ignoring the predefined repositories.
func (r *Repositories) DownloadFromFormatURL(formatURL, version, destDir, destFile string) (string, error) {
if formatURL == "" {
return "", fmt.Errorf("%s is not set", FormatURLEnv)
}

url, err := BuildURLFromFormat(formatURL, version)
if err != nil {
return "", err
}

return httputil.DownloadBinary(url, destDir, destFile)
}

// 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.
func CreateRepositories(releases ReleaseRepo, candidates CandidateRepo, fork ForkRepo, commits CommitRepo, rolling RollingRepo, supportsBaseURL bool) *Repositories {
repos := &Repositories{supportsBaseURL: supportsBaseURL}
Expand Down
78 changes: 78 additions & 0 deletions core/repositories_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package core

import (
"errors"
"fmt"
"os"
"testing"

"github.com/bazelbuild/bazelisk/platforms"
)

func TestBuildURLFromFormat(t *testing.T) {
osName, err := platforms.DetermineOperatingSystem()
if err != nil {
t.Fatalf("Cannot get operating system name: %v", err)
}

version := "6.0.0"

machineName, err := platforms.DetermineArchitecture(osName, version)
if err != nil {
t.Fatalf("Cannot get machine architecture name: %v", err)
}

suffix := platforms.DetermineExecutableFilenameSuffix()

previousSha256, hadSha256 := os.LookupEnv("BAZELISK_VERIFY_SHA256")
sha256 := "SomeSha256ValueThatIsIrrelevant"
if err := os.Setenv("BAZELISK_VERIFY_SHA256", sha256); err != nil {
t.Fatalf("Failed to set BAZELISK_VERIFY_SHA256")
}
defer func() {
if hadSha256 {
os.Setenv("BAZELISK_VERIFY_SHA256", previousSha256)
} else {
os.Unsetenv("BAZELISK_VERIFY_SHA256")
}
}()

type test struct {
format string
want string
wantErr error
}

tests := []test{
{format: "", want: ""},
{format: "no/placeholders", want: "no/placeholders"},

{format: "%", wantErr: errors.New("trailing %")},
{format: "%%", want: "%"},
{format: "%%%%", want: "%%"},
{format: "invalid/trailing/%", wantErr: errors.New("trailing %")},
{format: "escaped%%placeholder", want: "escaped%placeholder"},

{format: "foo-%e-bar", want: fmt.Sprintf("foo-%s-bar", suffix)},
{format: "foo-%h-bar", want: fmt.Sprintf("foo-%s-bar", sha256)},
{format: "foo-%m-bar", want: fmt.Sprintf("foo-%s-bar", machineName)},
{format: "foo-%o-bar", want: fmt.Sprintf("foo-%s-bar", osName)},
{format: "foo-%v-bar", want: fmt.Sprintf("foo-%s-bar", version)},

{format: "repeated %v %m %v", want: fmt.Sprintf("repeated %s %s %s", version, machineName, version)},

{format: "https://real.example.com/%e/%m/%o/%v#%%20trailing", want: fmt.Sprintf("https://real.example.com/%s/%s/%s/%s#%%20trailing", suffix, machineName, osName, version)},
}

for _, tc := range tests {
got, err := BuildURLFromFormat(tc.format, version)
if fmt.Sprintf("%v", err) != fmt.Sprintf("%v", tc.wantErr) {
if got != "" {
t.Errorf("format '%s': got non-empty '%s' on error", tc.format, got)
}
t.Errorf("format '%s': got error %v, want error %v", tc.format, err, tc.wantErr)
} else if got != tc.want {
t.Errorf("format '%s': got %s, want %s", tc.format, got, tc.want)
}
}
}
6 changes: 3 additions & 3 deletions httputil/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ go_library(
"fake.go",
"httputil.go",
],
importpath = "github.com/bazelbuild/bazelisk/httputil",
visibility = ["//visibility:public"],
deps = [
"@com_github_bgentry_go_netrc//:go_default_library",
"@com_github_mitchellh_go_homedir//:go_default_library",
"@com_github_bgentry_go_netrc//:go_default_library"
],
importpath = "github.com/bazelbuild/bazelisk/httputil",
visibility = ["//visibility:public"],
)

go_test(
Expand Down
6 changes: 6 additions & 0 deletions platforms/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,9 @@ go_test(
srcs = ["platforms_test.go"],
embed = [":go_default_library"],
)

go_test(
name = "go_default_test",
srcs = ["platforms_test.go"],
embed = [":go_default_library"],
)
27 changes: 21 additions & 6 deletions platforms/platforms.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,7 @@ func DetermineExecutableFilenameSuffix() string {
return filenameSuffix
}

// DetermineBazelFilename returns the correct file name of a local Bazel binary.
func DetermineBazelFilename(version string, includeSuffix bool) (string, error) {
func DetermineArchitecture(osName, version string) (string, error) {
var machineName string
switch runtime.GOARCH {
case "amd64":
Expand All @@ -66,16 +65,32 @@ func DetermineBazelFilename(version string, includeSuffix bool) (string, error)
return "", fmt.Errorf("unsupported machine architecture \"%s\", must be arm64 or x86_64", runtime.GOARCH)
}

var osName string
if osName == "darwin" {
machineName = DarwinFallback(machineName, version)
}

return machineName, nil
}

func DetermineOperatingSystem() (string, error) {
switch runtime.GOOS {
case "darwin", "linux", "windows":
osName = runtime.GOOS
return runtime.GOOS, nil
default:
return "", fmt.Errorf("unsupported operating system \"%s\", must be Linux, macOS or Windows", runtime.GOOS)
}
}

if osName == "darwin" {
machineName = DarwinFallback(machineName, version)
// DetermineBazelFilename returns the correct file name of a local Bazel binary.
func DetermineBazelFilename(version string, includeSuffix bool) (string, error) {
osName, err := DetermineOperatingSystem()
if err != nil {
return "", err
}

machineName, err := DetermineArchitecture(osName, version)
if err != nil {
return "", err
}

var filenameSuffix string
Expand Down