Skip to content

Commit 3bbd3a5

Browse files
committed
Support file:// protocol for BAZELISK_BASE_URL
Fixes #715
1 parent 166e156 commit 3bbd3a5

3 files changed

Lines changed: 83 additions & 0 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ The URL format looks like `https://github.com/<FORK>/bazel/releases/download/<VE
8686

8787
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.
8888

89+
If you want to use the releases stored on the local disk, set the URL as `file://` followed by the local disk path. On Windows, escape `\` in the path by `%5C`.
90+
8991
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:
9092

9193
- `%e`: Extension suffix, such as the empty string or `.exe`.

httputil/httputil.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"path/filepath"
1515
"regexp"
1616
"strconv"
17+
"strings"
1718
"time"
1819

1920
netrc "github.com/bgentry/go-netrc/netrc"
@@ -78,7 +79,41 @@ func ReadRemoteFile(url string, auth string) ([]byte, http.Header, error) {
7879
return body, res.Header, nil
7980
}
8081

82+
type LocalFileError struct{ err error }
83+
84+
func (e *LocalFileError) Error() string { return e.err.Error() }
85+
func (e *LocalFileError) Unwrap() error { return e.err }
86+
87+
// Handles file:// URLs by reading files from disk.
88+
func readLocalFile(urlStr string) (*http.Response, error) {
89+
urlStr = strings.TrimPrefix(urlStr, "file://")
90+
path, err := url.PathUnescape(urlStr)
91+
if err != nil {
92+
return nil, &LocalFileError{err: fmt.Errorf("invalid file url %q: %w", urlStr, err)}
93+
}
94+
f, err := os.Open(path)
95+
if err != nil {
96+
return nil, &LocalFileError{err: fmt.Errorf("could not open %q: %w", path, err)}
97+
}
98+
var size int64 = -1
99+
if fi, statErr := f.Stat(); statErr == nil {
100+
size = fi.Size()
101+
}
102+
return &http.Response{
103+
StatusCode: 200,
104+
Status: "200 OK",
105+
Header: make(http.Header),
106+
Body: f,
107+
ContentLength: size,
108+
Request: &http.Request{Method: "GET", URL: &url.URL{Scheme: "file", Path: path}},
109+
}, nil
110+
}
111+
81112
func get(url, auth string) (*http.Response, error) {
113+
if strings.HasPrefix(url, "file://") {
114+
return readLocalFile(url)
115+
}
116+
82117
req, err := http.NewRequest("GET", url, nil)
83118
if err != nil {
84119
return nil, fmt.Errorf("could not create request: %v", err)
@@ -127,6 +162,10 @@ func get(url, auth string) (*http.Response, error) {
127162
func shouldRetry(res *http.Response, err error) bool {
128163
// Retry if the client failed to speak HTTP.
129164
if err != nil {
165+
var nre *LocalFileError
166+
if errors.As(err, &nre) {
167+
return false
168+
}
130169
return true
131170
}
132171
// For HTTP: only retry on non-permanent/fatal errors.

httputil/httputil_test.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ package httputil
33
import (
44
"errors"
55
"net/http"
6+
"net/url"
7+
"os"
8+
"path/filepath"
69
"strconv"
710
"strings"
811
"testing"
@@ -251,3 +254,42 @@ func TestNoRetryOnPermanentError(t *testing.T) {
251254
t.Fatalf("Expected no retries for permanent error, but got %d", clock.TimesSlept())
252255
}
253256
}
257+
258+
func TestReadLocalFile(t *testing.T) {
259+
tmpDir := t.TempDir()
260+
path := filepath.Join(tmpDir, "payload.txt")
261+
want := "hello from disk"
262+
if err := os.WriteFile(path, []byte(want), 0644); err != nil {
263+
t.Fatalf("failed to write temp file: %v", err)
264+
}
265+
fileURL := (&url.URL{Scheme: "file", Path: path}).String()
266+
267+
body, _, err := ReadRemoteFile(fileURL, "")
268+
if err != nil {
269+
t.Fatalf("unexpected error: %v", err)
270+
}
271+
if got := string(body); got != want {
272+
t.Fatalf("expected body %q, but got %q", want, got)
273+
}
274+
}
275+
276+
func TestReadLocalFileNotFound(t *testing.T) {
277+
clock := newFakeClock()
278+
RetryClock = clock
279+
MaxRetries = 10
280+
MaxRequestDuration = time.Hour
281+
282+
missingPath := filepath.Join(t.TempDir(), "does-not-exist.txt")
283+
fileURL := (&url.URL{Scheme: "file", Path: missingPath}).String()
284+
285+
_, _, err := ReadRemoteFile(fileURL, "")
286+
if err == nil {
287+
t.Fatal("expected error for missing file")
288+
}
289+
if !errors.Is(err, os.ErrNotExist) {
290+
t.Fatalf("expected os.ErrNotExist, got %v", err)
291+
}
292+
if clock.TimesSlept() != 0 {
293+
t.Fatalf("expected no retries for file:// error, but slept %d times", clock.TimesSlept())
294+
}
295+
}

0 commit comments

Comments
 (0)