From d5acbf60233823c787ee9bcc2485b622e294b582 Mon Sep 17 00:00:00 2001 From: Florian Weikert Date: Thu, 14 Nov 2024 20:11:03 +0100 Subject: [PATCH] Refactor repositories API This commit merges ReleaseRepo with CandidateRepo into a single type called LTSRepo. 1. The new behavior more closely resembles reality. 2. It will be much easier to implement https://github.com/bazelbuild/bazelisk/issues/630. Progress towards https://github.com/bazelbuild/bazelisk/issues/630. --- bazelisk.go | 4 +- bazelisk_version_test.go | 35 +++++--- core/repositories.go | 140 ++++++++++++------------------ repositories/gcs.go | 178 +++++++++++++++------------------------ versions/versions.go | 33 +++++--- 5 files changed, 164 insertions(+), 226 deletions(-) diff --git a/bazelisk.go b/bazelisk.go index a25041c1..45da21de 100644 --- a/bazelisk.go +++ b/bazelisk.go @@ -27,8 +27,8 @@ func main() { gcs := &repositories.GCSRepo{} config := core.MakeDefaultConfig() gitHub := repositories.CreateGitHubRepo(config.Get("BAZELISK_GITHUB_TOKEN")) - // Fetch LTS releases, release candidates, rolling releases and Bazel-at-commits from GCS, forks from GitHub. - repos := core.CreateRepositories(gcs, gcs, gitHub, gcs, gcs, true) + // Fetch LTS releases & candidates, rolling releases and Bazel-at-commits from GCS, forks from GitHub. + repos := core.CreateRepositories(gcs, gitHub, gcs, gcs, true) exitCode, err := core.RunBazeliskWithArgsFuncAndConfig(func(string) []string { return os.Args[1:] }, repos, config) if err != nil { diff --git a/bazelisk_version_test.go b/bazelisk_version_test.go index 58516401..81527f2f 100644 --- a/bazelisk_version_test.go +++ b/bazelisk_version_test.go @@ -5,6 +5,7 @@ import ( "fmt" "log" "os" + "slices" "strings" "testing" @@ -23,6 +24,16 @@ var ( tmpDir = "" ) +func TestGetInAscendingOrder(t *testing.T) { + input := []string{"7.0.1rc2", "6.1.0", "6.0.1", "10.11.12", "6.0.0", "7.0.1", "6.0.0rc2", "7.0.1rc1", "10.11.12rc1", "6.0.0rc1"} + want := []string{"6.0.0rc1", "6.0.0rc2", "6.0.0", "6.0.1", "6.1.0", "7.0.1rc1", "7.0.1rc2", "7.0.1", "10.11.12rc1", "10.11.12"} + + got := versions.GetInAscendingOrder(input) + if !slices.Equal(got, want) { + t.Errorf("GetInAscendingOrder(): got %s, want %s", got, want) + } +} + func TestMain(m *testing.M) { var err error tmpDir, err = os.MkdirTemp("", "version_test") @@ -41,7 +52,7 @@ func TestResolveVersion(t *testing.T) { s.Finish() gcs := &repositories.GCSRepo{} - repos := core.CreateRepositories(nil, gcs, nil, nil, nil, false) + repos := core.CreateRepositories(gcs, nil, nil, nil, false) version, _, err := repos.ResolveVersion(tmpDir, versions.BazelUpstream, "4.0.0", config.Null()) if err != nil { @@ -59,7 +70,7 @@ func TestResolvePatchVersion(t *testing.T) { s.Finish() gcs := &repositories.GCSRepo{} - repos := core.CreateRepositories(nil, gcs, nil, nil, nil, false) + repos := core.CreateRepositories(gcs, nil, nil, nil, false) version, _, err := repos.ResolveVersion(tmpDir, versions.BazelUpstream, "4.0.0-patch1", config.Null()) if err != nil { @@ -81,7 +92,7 @@ func TestResolveLatestRcVersion(t *testing.T) { s.Finish() gcs := &repositories.GCSRepo{} - repos := core.CreateRepositories(nil, gcs, nil, nil, nil, false) + repos := core.CreateRepositories(gcs, nil, nil, nil, false) version, _, err := repos.ResolveVersion(tmpDir, versions.BazelUpstream, "last_rc", config.Null()) if err != nil { @@ -99,7 +110,7 @@ func TestResolveLatestRcVersion_WithFullRelease(t *testing.T) { s.Finish() gcs := &repositories.GCSRepo{} - repos := core.CreateRepositories(nil, gcs, nil, nil, nil, false) + repos := core.CreateRepositories(gcs, nil, nil, nil, false) version, _, err := repos.ResolveVersion(tmpDir, versions.BazelUpstream, "last_rc", config.Null()) if err != nil { @@ -119,7 +130,7 @@ func TestResolveLatestVersion_TwoLatestVersionsDoNotHaveAReleaseYet(t *testing.T s.Finish() gcs := &repositories.GCSRepo{} - repos := core.CreateRepositories(gcs, nil, nil, nil, nil, false) + repos := core.CreateRepositories(gcs, nil, nil, nil, false) version, _, err := repos.ResolveVersion(tmpDir, versions.BazelUpstream, "latest", config.Null()) if err != nil { @@ -141,7 +152,7 @@ func TestResolveLatestVersion_ShouldOnlyReturnStableReleases(t *testing.T) { s.Finish() gcs := &repositories.GCSRepo{} - repos := core.CreateRepositories(gcs, nil, nil, nil, nil, false) + repos := core.CreateRepositories(gcs, nil, nil, nil, false) version, _, err := repos.ResolveVersion(tmpDir, versions.BazelUpstream, "latest-1", config.Null()) if err != nil { @@ -160,7 +171,7 @@ func TestResolveLatestVersion_ShouldFailIfNotEnoughReleases(t *testing.T) { s.Finish() gcs := &repositories.GCSRepo{} - repos := core.CreateRepositories(gcs, nil, nil, nil, nil, false) + repos := core.CreateRepositories(gcs, nil, nil, nil, false) _, _, err := repos.ResolveVersion(tmpDir, versions.BazelUpstream, "latest-1", config.Null()) if err == nil { @@ -177,7 +188,7 @@ func TestResolveLatestVersion_GCSIsDown(t *testing.T) { g.Transport.AddResponse("https://www.googleapis.com/storage/v1/b/bazel/o?delimiter=/", 500, "", nil) gcs := &repositories.GCSRepo{} - repos := core.CreateRepositories(gcs, nil, nil, nil, nil, false) + repos := core.CreateRepositories(gcs, nil, nil, nil, false) _, _, err := repos.ResolveVersion(tmpDir, versions.BazelUpstream, "latest", config.Null()) if err == nil { @@ -194,7 +205,7 @@ func TestResolveLatestVersion_GitHubIsDown(t *testing.T) { transport.AddResponse("https://api.github.com/repos/bazelbuild/bazel/releases", 500, "", nil) gh := repositories.CreateGitHubRepo("test_token") - repos := core.CreateRepositories(nil, nil, gh, nil, nil, false) + repos := core.CreateRepositories(nil, gh, nil, nil, false) _, _, err := repos.ResolveVersion(tmpDir, "some_fork", "latest", config.Null()) @@ -209,7 +220,7 @@ func TestResolveLatestVersion_GitHubIsDown(t *testing.T) { func TestAcceptRollingReleaseName(t *testing.T) { gcs := &repositories.GCSRepo{} - repos := core.CreateRepositories(nil, nil, nil, nil, gcs, false) + repos := core.CreateRepositories(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, config.Null()) @@ -231,7 +242,7 @@ func TestResolveLatestRollingRelease(t *testing.T) { s.Finish() gcs := &repositories.GCSRepo{} - repos := core.CreateRepositories(nil, nil, nil, nil, gcs, false) + repos := core.CreateRepositories(nil, nil, nil, gcs, false) version, _, err := repos.ResolveVersion(tmpDir, "", rollingReleaseIdentifier, config.Null()) @@ -256,7 +267,7 @@ func TestAcceptFloatingReleaseVersions(t *testing.T) { s.Finish() gcs := &repositories.GCSRepo{} - repos := core.CreateRepositories(gcs, nil, nil, nil, nil, false) + repos := core.CreateRepositories(gcs, nil, nil, nil, false) version, _, err := repos.ResolveVersion(tmpDir, versions.BazelUpstream, "4.x", config.Null()) if err != nil { diff --git a/core/repositories.go b/core/repositories.go index 489988b3..e7388390 100644 --- a/core/repositories.go +++ b/core/repositories.go @@ -22,40 +22,24 @@ const ( // DownloadFunc downloads a specific Bazel binary to the given location and returns the absolute path. type DownloadFunc func(destDir, destFile string) (string, error) -// ReleaseFilter filters Bazel versions based on specific criteria. -type ReleaseFilter func(matchesSoFar int, currentVersion string) bool +// LTSFilter filters Bazel versions based on specific criteria. +type LTSFilter func(string) bool -func lastNReleases(max int) ReleaseFilter { - return func(matchesSoFar int, currentVersion string) bool { - return max < 1 || matchesSoFar < max - } -} - -// filterReleasesByTrack only works reliably if iterating on Bazel versions in descending order. -func filterReleasesByTrack(track int) ReleaseFilter { - prefix := fmt.Sprintf("%d.", track) - return func(matchesSoFar int, currentVersion string) bool { - return matchesSoFar == 0 && strings.HasPrefix(currentVersion, prefix) - } -} - -// ReleaseRepo represents a repository that stores LTS Bazel releases. -type ReleaseRepo interface { - // GetReleaseVersions returns a list of all available release versions that match the given filter function. - // Warning: the filter only works reliably if the versions are processed in descending order! - 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, config config.Config) (string, error) +// FilterOpts represents options relevant to filtering Bazel versions. +type FilterOpts struct { + MaxResults int + Track int + Filter LTSFilter } -// CandidateRepo represents a repository that stores Bazel release candidates. -type CandidateRepo interface { - // GetCandidateVersions returns the versions of all available release candidates. - GetCandidateVersions(bazeliskHome string) ([]string, error) +// LTSRepo represents a repository that stores LTS Bazel releases and their candidates. +type LTSRepo interface { + // GetLTSVersions returns a list of all available LTS release (candidates) that match the given filter options. + // Warning: Filters only work reliably if the versions are processed in descending order! + GetLTSVersions(bazeliskHome string, opts *FilterOpts) ([]string, error) - // DownloadCandidate downloads the given Bazel release candidate into the specified location and returns the absolute path. - DownloadCandidate(version, destDir, destFile string, config config.Config) (string, error) + // DownloadLTS downloads the given Bazel version into the specified location and returns the absolute path. + DownloadLTS(version, destDir, destFile string, config config.Config) (string, error) } // ForkRepo represents a repository that stores a fork of Bazel (releases). @@ -88,8 +72,7 @@ type RollingRepo interface { // Repositories offers access to different types of Bazel repositories, mainly for finding and downloading the correct version of Bazel. type Repositories struct { - Releases ReleaseRepo - Candidates CandidateRepo + LTS LTSRepo Fork ForkRepo Commits CommitRepo Rolling RollingRepo @@ -105,17 +88,15 @@ func (r *Repositories) ResolveVersion(bazeliskHome, fork, version string, config if vi.IsFork { return r.resolveFork(bazeliskHome, vi, config) - } else if vi.IsRelease { - return r.resolveRelease(bazeliskHome, vi, config) - } else if vi.IsCandidate { - return r.resolveCandidate(bazeliskHome, vi, config) + } else if vi.IsLTS { + return r.resolveLTS(bazeliskHome, vi, config) } else if vi.IsCommit { return r.resolveCommit(bazeliskHome, vi, config) } else if vi.IsRolling { return r.resolveRolling(bazeliskHome, vi, config) } - return "", nil, fmt.Errorf("Unsupported version identifier '%s'", version) + return "", nil, fmt.Errorf("unsupported version identifier '%s'", version) } func (r *Repositories) resolveFork(bazeliskHome string, vi *versions.Info, config config.Config) (string, DownloadFunc, error) { @@ -135,35 +116,36 @@ func (r *Repositories) resolveFork(bazeliskHome string, vi *versions.Info, confi return version, downloader, nil } -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 { - // Optimization: only fetch matching releases if an LTS track is specified. - filter = filterReleasesByTrack(vi.TrackRestriction) - } else { - // Optimization: only fetch last (x+1) releases if the version is "latest-x". - filter = lastNReleases(vi.LatestOffset + 1) - } - return r.Releases.GetReleaseVersions(bazeliskHome, filter) - } - version, err := resolvePotentiallyRelativeVersion(bazeliskHome, lister, vi) - if err != nil { - return "", nil, err +var IsRelease = func(version string) bool { + return !strings.Contains(version, "rc") +} + +var IsCandidate = func(version string) bool { + return strings.Contains(version, "rc") +} + +func (r *Repositories) resolveLTS(bazeliskHome string, vi *versions.Info, config config.Config) (string, DownloadFunc, error) { + opts := &FilterOpts{ + // Optimization: only fetch last (x+1) releases if the version is "latest-x". + MaxResults: vi.LatestOffset + 1, + Track: vi.TrackRestriction, } - downloader := func(destDir, destFile string) (string, error) { - return r.Releases.DownloadRelease(version, destDir, destFile, config) + + if vi.IsRelease { + opts.Filter = IsRelease + } else { + opts.Filter = IsCandidate } - return version, downloader, nil -} -func (r *Repositories) resolveCandidate(bazeliskHome string, vi *versions.Info, config config.Config) (string, DownloadFunc, error) { - version, err := resolvePotentiallyRelativeVersion(bazeliskHome, r.Candidates.GetCandidateVersions, vi) + lister := func(bazeliskHome string) ([]string, error) { + return r.LTS.GetLTSVersions(bazeliskHome, opts) + } + version, err := resolvePotentiallyRelativeVersion(bazeliskHome, lister, vi) if err != nil { return "", nil, err } downloader := func(destDir, destFile string) (string, error) { - return r.Candidates.DownloadCandidate(version, destDir, destFile, config) + return r.LTS.DownloadLTS(version, destDir, destFile, config) } return version, downloader, nil } @@ -295,19 +277,13 @@ func (r *Repositories) DownloadFromFormatURL(config config.Config, formatURL, ve } // 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 { +func CreateRepositories(lts LTSRepo, fork ForkRepo, commits CommitRepo, rolling RollingRepo, supportsBaseURL bool) *Repositories { repos := &Repositories{supportsBaseURL: supportsBaseURL} - if releases == nil { - repos.Releases = &noReleaseRepo{err: errors.New("Bazel LTS releases are not supported")} - } else { - repos.Releases = releases - } - - if candidates == nil { - repos.Candidates = &noCandidateRepo{err: errors.New("Bazel release candidates are not supported")} + if lts == nil { + repos.LTS = &noLTSRepo{err: errors.New("Bazel LTS releases & candidates are not supported")} } else { - repos.Candidates = candidates + repos.LTS = lts } if fork == nil { @@ -331,31 +307,19 @@ func CreateRepositories(releases ReleaseRepo, candidates CandidateRepo, fork For return repos } -// The whole point of the structs below this line is that users can simply call repos.Releases.GetReleaseVersions() -// (etc) without having to worry whether `Releases` points at an actual repo. - -type noReleaseRepo struct { - err error -} - -func (nrr *noReleaseRepo) GetReleaseVersions(bazeliskHome string, filter ReleaseFilter) ([]string, error) { - return nil, nrr.err -} - -func (nrr *noReleaseRepo) DownloadRelease(version, destDir, destFile string, config config.Config) (string, error) { - return "", nrr.err -} +// The whole point of the structs below this line is that users can simply call repos.LTS.GetLTSVersions() +// (etc) without having to worry whether `LTS` points at an actual repo. -type noCandidateRepo struct { +type noLTSRepo struct { err error } -func (ncc *noCandidateRepo) GetCandidateVersions(bazeliskHome string) ([]string, error) { - return nil, ncc.err +func (nolts *noLTSRepo) GetLTSVersions(bazeliskHome string, opts *FilterOpts) ([]string, error) { + return nil, nolts.err } -func (ncc *noCandidateRepo) DownloadCandidate(version, destDir, destFile string, config config.Config) (string, error) { - return "", ncc.err +func (nolts *noLTSRepo) DownloadLTS(version, destDir, destFile string, config config.Config) (string, error) { + return "", nolts.err } type noForkRepo struct { diff --git a/repositories/gcs.go b/repositories/gcs.go index 7206899b..f5d3b6aa 100644 --- a/repositories/gcs.go +++ b/repositories/gcs.go @@ -8,6 +8,7 @@ import ( "errors" "fmt" "log" + "strconv" "strings" "time" @@ -19,35 +20,35 @@ import ( ) const ( - candidateBaseURL = "https://releases.bazel.build" - nonCandidateBaseURL = "https://storage.googleapis.com/bazel-builds/artifacts" - lastGreenCommitURL = "https://storage.googleapis.com/bazel-builds/last_green_commit/github.com/bazelbuild/bazel.git/publish-bazel-binaries" + ltsBaseURL = "https://releases.bazel.build" + commitBaseURL = "https://storage.googleapis.com/bazel-builds/artifacts" + lastGreenCommitURL = "https://storage.googleapis.com/bazel-builds/last_green_commit/github.com/bazelbuild/bazel.git/publish-bazel-binaries" ) // GCSRepo represents a Bazel repository on Google Cloud Storage that contains Bazel releases, release candidates and Bazel binaries built at arbitrary commits. // It can return all available Bazel versions, as well as downloading a specific version. type GCSRepo struct{} -// ReleaseRepo +// LTSRepo -// GetReleaseVersions returns the versions of all available Bazel releases in this repository that match the given filter. -func (gcs *GCSRepo) GetReleaseVersions(bazeliskHome string, filter core.ReleaseFilter) ([]string, error) { +// GetLTSVersions returns the versions of all available Bazel releases in this repository that match the given filter. +func (gcs *GCSRepo) GetLTSVersions(bazeliskHome string, opts *core.FilterOpts) ([]string, error) { history, err := getVersionHistoryFromGCS() if err != nil { return []string{}, err } - releases, err := gcs.removeCandidates(history, filter) + matches, err := gcs.matchingVersions(history, opts) if err != nil { return []string{}, err } - if len(releases) == 0 { - return []string{}, errors.New("there are no releases available") + if len(matches) == 0 { + return []string{}, errors.New("there are no LTS releases or candidates") } - return releases, nil + return matches, nil } func getVersionHistoryFromGCS() ([]string, error) { - prefixes, _, err := listDirectoriesInReleaseBucket("") + prefixes, err := listDirectoriesInBucket("") if err != nil { return []string{}, fmt.Errorf("could not list Bazel versions in GCS bucket: %v", err) } @@ -57,14 +58,13 @@ func getVersionHistoryFromGCS() ([]string, error) { return sorted, nil } -func listDirectoriesInReleaseBucket(prefix string) ([]string, bool, error) { +func listDirectoriesInBucket(prefix string) ([]string, error) { baseURL := "https://www.googleapis.com/storage/v1/b/bazel/o?delimiter=/" if prefix != "" { baseURL = fmt.Sprintf("%s&prefix=%s", baseURL, prefix) } var prefixes []string - var isRelease = false var nextPageToken = "" for { var url = baseURL @@ -88,32 +88,22 @@ func listDirectoriesInReleaseBucket(prefix string) ([]string, bool, error) { } if err != nil { - return nil, false, fmt.Errorf("could not list GCS objects at %s: %v", url, err) + return nil, fmt.Errorf("could not list GCS objects at %s: %v", url, err) } var response GcsListResponse if err := json.Unmarshal(content, &response); err != nil { - return nil, false, fmt.Errorf("could not parse GCS index JSON: %v", err) + return nil, fmt.Errorf("could not parse GCS index JSON: %v", err) } prefixes = append(prefixes, response.Prefixes...) - isRelease = isRelease || len(response.Items) > 0 if response.NextPageToken == "" { break } nextPageToken = response.NextPageToken } - return prefixes, isRelease, nil -} - -func getVersionsFromGCSPrefixes(versions []string) []string { - result := make([]string, len(versions)) - for i, v := range versions { - noSlashes := strings.Replace(v, "/", "", -1) - result[i] = strings.TrimSuffix(noSlashes, "release") - } - return result + return prefixes, nil } // GcsListResponse represents the result of listing the contents of a GCS bucket. @@ -129,113 +119,77 @@ type GcsListResponse struct { NextPageToken string `json:"nextPageToken"` } -// DownloadRelease downloads the given Bazel release into the specified location and returns the absolute path. -func (gcs *GCSRepo) DownloadRelease(version, destDir, destFile string, config config.Config) (string, error) { - srcFile, err := platforms.DetermineBazelFilename(version, true, config) - if err != nil { - return "", err +func getVersionsFromGCSPrefixes(versions []string) []string { + result := make([]string, len(versions)) + for i, v := range versions { + noSlashes := strings.Replace(v, "/", "", -1) + result[i] = strings.TrimSuffix(noSlashes, "release") } - - url := fmt.Sprintf("%s/%s/release/%s", candidateBaseURL, version, srcFile) - return httputil.DownloadBinary(url, destDir, destFile, config) + return result } -func (gcs *GCSRepo) removeCandidates(history []string, filter core.ReleaseFilter) ([]string, error) { - descendingReleases := make([]string, 0) - filterPassed := false - // Iteration in descending order is important: - // - ReleaseFilter won't work correctly otherwise - // - It makes more sense in the "latest-n" use case +func (gcs *GCSRepo) matchingVersions(history []string, opts *core.FilterOpts) ([]string, error) { + descendingMatches := make([]string, 0) + // history is a list of base versions in ascending order (i.e. X.Y.Z, no rolling releases or candidates). for hpos := len(history) - 1; hpos >= 0; hpos-- { - latestVersion := history[hpos] - pass := filter(len(descendingReleases), latestVersion) - if pass { - filterPassed = true - } else { - // Underlying assumption: all matching versions are in a single continuous sequence. - // Consequently, if the filter returns false, this means: - if filterPassed { - // a) "break" if we've seen a match before (because all future version won't match either) - break - } else { - // b) "continue looking" if we've never seen a match before + baseVersion := history[hpos] + + if opts.Track > 0 { + track, err := getTrack(baseVersion) + if err != nil { + continue // Ignore invalid GCS entries for now + } + if track > opts.Track { continue + } else if track < opts.Track { + break } } - // Obviously this only works for the existing filters (lastN and track-based) and - // because versions in history are sorted. - - _, isRelease, err := listDirectoriesInReleaseBucket(latestVersion + "/release/") - if err != nil { - return []string{}, fmt.Errorf("could not list available releases for %v: %v", latestVersion, err) - } - if isRelease { - descendingReleases = append(descendingReleases, latestVersion) - } - } - return reverseInPlace(descendingReleases), nil -} - -func reverseInPlace(values []string) []string { - for i := 0; i < len(values)/2; i++ { - j := len(values) - 1 - i - values[i], values[j] = values[j], values[i] - } - return values -} - -// CandidateRepo -// GetCandidateVersions returns all versions of available release candidates for the latest release in this repository. -func (gcs *GCSRepo) GetCandidateVersions(bazeliskHome string) ([]string, error) { - history, err := getVersionHistoryFromGCS() - if err != nil { - return []string{}, err - } - - if len(history) == 0 { - return []string{}, errors.New("could not find any Bazel versions") - } - - // Find most recent directory that contains any release candidates. - // Typically it should be the last or second-to-last, depending on whether there are new rolling releases. - for pos := len(history) - 1; pos >= 0; pos-- { // Append slash to match directories - bucket := fmt.Sprintf("%s/", history[pos]) - rcPrefixes, _, err := listDirectoriesInReleaseBucket(bucket) + bucket := fmt.Sprintf("%s/", history[hpos]) + prefixes, err := listDirectoriesInBucket(bucket) if err != nil { - return []string{}, fmt.Errorf("could not list release candidates for latest release: %v", err) + return []string{}, fmt.Errorf("could not list LTS releases/candidates: %v", err) } - rcs := make([]string, 0) - for _, v := range getVersionsFromGCSPrefixes(rcPrefixes) { - // Remove full and rolling releases - if strings.Contains(v, "rc") { - rcs = append(rcs, v) + // Ascending list of rc versions, followed by the release version (if it exists) and a rolling identifier (if there are rolling releases). + versions := getVersionsFromGCSPrefixes(prefixes) + for vpos := len(versions) - 1; vpos >= 0 && len(descendingMatches) < opts.MaxResults; vpos-- { + curr := versions[vpos] + if strings.Contains(curr, "rolling") || !opts.Filter(curr) { + continue } - } - if len(rcs) > 0 { - return rcs, nil + descendingMatches = append(descendingMatches, curr) } } - return nil, nil + return descendingMatches, nil } -// DownloadCandidate downloads the given release candidate into the specified location and returns the absolute path. -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) +func getTrack(version string) (int, error) { + major, _, _ := strings.Cut(version, ".") + if val, err := strconv.Atoi(major); err == nil { + return val, nil } + return 0, fmt.Errorf("invalid version %q", version) +} +// DownloadLTS downloads the given Bazel LTS release (candidate) into the specified location and returns the absolute path. +func (gcs *GCSRepo) DownloadLTS(version, destDir, destFile string, config config.Config) (string, error) { srcFile, err := platforms.DetermineBazelFilename(version, true, config) if err != nil { return "", err } - versionComponents := strings.Split(version, "rc") - baseVersion := versionComponents[0] - rcVersion := "rc" + versionComponents[1] - url := fmt.Sprintf("%s/%s/%s/%s", candidateBaseURL, baseVersion, rcVersion, srcFile) + var baseVersion, folder string + if strings.Contains(version, "rc") { + versionComponents := strings.Split(version, "rc") + baseVersion, folder = versionComponents[0], "rc"+versionComponents[1] + } else { + baseVersion, folder = version, "release" + } + + url := fmt.Sprintf("%s/%s/%s/%s", ltsBaseURL, baseVersion, folder, srcFile) return httputil.DownloadBinary(url, destDir, destFile, config) } @@ -264,7 +218,7 @@ func (gcs *GCSRepo) DownloadAtCommit(commit, destDir, destFile string, config co if err != nil { return "", err } - url := fmt.Sprintf("%s/%s/%s/bazel", nonCandidateBaseURL, platform, commit) + url := fmt.Sprintf("%s/%s/%s/bazel", commitBaseURL, platform, commit) return httputil.DownloadBinary(url, destDir, destFile, config) } @@ -278,7 +232,7 @@ func (gcs *GCSRepo) GetRollingVersions(bazeliskHome string) ([]string, error) { } newest := history[len(history)-1] - versions, _, err := listDirectoriesInReleaseBucket(newest + "/rolling/") + versions, err := listDirectoriesInBucket(newest + "/rolling/") if err != nil { return []string{}, err } @@ -301,6 +255,6 @@ func (gcs *GCSRepo) DownloadRolling(version, destDir, destFile string, config co } releaseVersion := strings.Split(version, "-")[0] - url := fmt.Sprintf("%s/%s/rolling/%s/%s", candidateBaseURL, releaseVersion, version, srcFile) + url := fmt.Sprintf("%s/%s/rolling/%s/%s", ltsBaseURL, releaseVersion, version, srcFile) return httputil.DownloadBinary(url, destDir, destFile, config) } diff --git a/versions/versions.go b/versions/versions.go index bba0ed75..3fb5f87d 100644 --- a/versions/versions.go +++ b/versions/versions.go @@ -17,7 +17,8 @@ const ( ) var ( - releasePattern = regexp.MustCompile(`^(\d+)\.(x|\d+\.\d+)$`) + releasePattern = regexp.MustCompile(`^(\d+)\.\d+\.\d+$`) + trackPattern = regexp.MustCompile(`^(\d+)\.x$`) patchPattern = regexp.MustCompile(`^(\d+\.\d+\.\d+)-([\w\d]+)$`) candidatePattern = regexp.MustCompile(`^(\d+\.\d+\.\d+)rc(\d+)$`) rollingPattern = regexp.MustCompile(`^\d+\.0\.0-pre\.\d{8}(\.\d+){1,2}$`) @@ -27,9 +28,11 @@ var ( // Info represents a structured Bazel version identifier. type Info struct { - IsRelease, IsCandidate, IsCommit, IsFork, IsRolling, IsRelative bool - Fork, Value string - LatestOffset, TrackRestriction int + IsRelease, IsCandidate bool + IsLTS, IsRolling bool + IsCommit, IsFork, IsRelative bool + Fork, Value string + LatestOffset, TrackRestriction int } // Parse extracts and returns structured information about the given Bazel version label. @@ -37,18 +40,22 @@ func Parse(fork, version string) (*Info, error) { vi := &Info{Fork: fork, Value: version, IsFork: isFork(fork)} if m := releasePattern.FindStringSubmatch(version); m != nil { + vi.IsLTS = true vi.IsRelease = true - if m[2] == "x" { - track, err := strconv.Atoi(m[1]) - if err != nil { - return nil, fmt.Errorf("invalid version %q, expected something like '5.2.1' or '5.x'", version) - } - vi.IsRelative = true - vi.TrackRestriction = track + } else if m := trackPattern.FindStringSubmatch(version); m != nil { + track, err := strconv.Atoi(m[1]) + if err != nil { + return nil, fmt.Errorf("invalid version %q, expected something like '5.x'", version) } + vi.IsLTS = true + vi.IsRelease = true + vi.IsRelative = true + vi.TrackRestriction = track } else if patchPattern.MatchString(version) { + vi.IsLTS = true vi.IsRelease = true } else if m := latestReleasePattern.FindStringSubmatch(version); m != nil { + vi.IsLTS = true vi.IsRelease = true vi.IsRelative = true if m[1] != "" { @@ -59,8 +66,10 @@ func Parse(fork, version string) (*Info, error) { vi.LatestOffset = offset } } else if candidatePattern.MatchString(version) { + vi.IsLTS = true vi.IsCandidate = true } else if version == "last_rc" { + vi.IsLTS = true vi.IsCandidate = true vi.IsRelative = true } else if commitPattern.MatchString(version) { @@ -74,7 +83,7 @@ func Parse(fork, version string) (*Info, error) { vi.IsRolling = true vi.IsRelative = true } else { - return nil, fmt.Errorf("Invalid version '%s'", version) + return nil, fmt.Errorf("invalid version '%s'", version) } return vi, nil }