| // Copyright 2026 The BoringSSL Authors |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // https://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| // update_bazel_deps updates dependencies in MODULE.bazel files to their latest |
| // stable versions from the Bazel Central Registry (BCR). |
| package main |
| |
| import ( |
| "bytes" |
| "cmp" |
| "encoding/json" |
| "errors" |
| "flag" |
| "fmt" |
| "net/http" |
| "os" |
| "os/exec" |
| "path/filepath" |
| "regexp" |
| "strconv" |
| "strings" |
| ) |
| |
| // Version represents a parsed non-prerelease Bazel module version. See |
| // https://bazel.build/external/module#version_format. |
| type Version struct { |
| components []string |
| raw string |
| } |
| |
| var bazelVersionRE = regexp.MustCompile(`^([a-zA-Z0-9.]+)(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.-]+)?$`) |
| |
| var errPrerelease = errors.New("prerelease versions are not supported") |
| |
| func parseVersion(v string) (Version, error) { |
| matches := bazelVersionRE.FindStringSubmatch(v) |
| if matches == nil { |
| return Version{}, fmt.Errorf("invalid version %q", v) |
| } |
| if matches[2] != "" { |
| return Version{}, fmt.Errorf("invalid version %q: %w", v, errPrerelease) |
| } |
| return Version{ |
| components: strings.Split(matches[1], "."), |
| raw: v, |
| }, nil |
| } |
| |
| func compareComponents(c1, c2 string) int { |
| // Numeric identifiers are compared numerically. |
| n1, err1 := strconv.Atoi(c1) |
| n2, err2 := strconv.Atoi(c2) |
| if err1 == nil && err2 == nil { |
| return cmp.Compare(n1, n2) |
| } |
| |
| // Numeric has lower precedence than non-numeric. |
| if err1 == nil { |
| return -1 |
| } |
| if err2 == nil { |
| return 1 |
| } |
| |
| // Non-numeric identifiers are compared lexically. |
| return cmp.Compare(c1, c2) |
| } |
| |
| func compareVersions(v1, v2 Version) int { |
| minLen := min(len(v1.components), len(v2.components)) |
| for i := 0; i < minLen; i++ { |
| if ret := compareComponents(v1.components[i], v2.components[i]); ret != 0 { |
| return ret |
| } |
| } |
| // If one version is a prefix of the other, the shorter compares first. |
| return cmp.Compare(len(v1.components), len(v2.components)) |
| } |
| |
| type moduleMetadata struct { |
| Versions []string `json:"versions"` |
| YankedVersions map[string]string `json:"yanked_versions"` |
| } |
| |
| func getLatestVersion(dep string) (Version, error) { |
| url := fmt.Sprintf("https://bcr.bazel.build/modules/%s/metadata.json", dep) |
| resp, err := http.Get(url) |
| if err != nil { |
| return Version{}, err |
| } |
| defer resp.Body.Close() |
| if resp.StatusCode != 200 { |
| return Version{}, fmt.Errorf("bad status %d fetching metadata for %s", resp.StatusCode, dep) |
| } |
| var meta moduleMetadata |
| if err := json.NewDecoder(resp.Body).Decode(&meta); err != nil { |
| return Version{}, err |
| } |
| |
| var latest Version |
| var found bool |
| for _, vStr := range meta.Versions { |
| if _, yanked := meta.YankedVersions[vStr]; yanked { |
| continue |
| } |
| v, err := parseVersion(vStr) |
| if err != nil { |
| if errors.Is(err, errPrerelease) { |
| // Ignore prerelease versions. |
| continue |
| } |
| return Version{}, err |
| } |
| if !found || compareVersions(v, latest) > 0 { |
| latest = v |
| found = true |
| } |
| } |
| if !found { |
| return Version{}, fmt.Errorf("no suitable version found for %s", dep) |
| } |
| return latest, nil |
| } |
| |
| // Group 1: prefix up to name=" |
| // Group 2: dep name |
| // Group 3: middle part between name and version |
| // Group 4: current version |
| // Group 5: suffix |
| var bazelDepRE = regexp.MustCompile(`^(\s*bazel_dep\(\s*name\s*=\s*")([^"]+)("\s*,\s*version\s*=\s*")([^"]+)("\s*.*)$`) |
| |
| func updateFile(path string) (changed bool, err error) { |
| content, err := os.ReadFile(path) |
| if err != nil { |
| return false, err |
| } |
| |
| lines := bytes.Split(content, []byte("\n")) |
| // Account for the trailing newline. |
| if len(lines) > 0 && len(lines[len(lines)-1]) == 0 { |
| lines = lines[:len(lines)-1] |
| } |
| |
| var out bytes.Buffer |
| for _, line := range lines { |
| var lineChanged bool |
| matches := bazelDepRE.FindSubmatch(line) |
| if matches != nil { |
| depName := string(matches[2]) |
| currentVerStr := string(matches[4]) |
| |
| currentVer, err := parseVersion(currentVerStr) |
| if err != nil { |
| return false, fmt.Errorf("could not parse current version for %s: %s", depName, err) |
| } |
| |
| latestVer, err := getLatestVersion(depName) |
| if err != nil { |
| return false, fmt.Errorf("could not get latest version for %s: %s", depName, err) |
| } |
| |
| if compareVersions(latestVer, currentVer) > 0 { |
| out.Write(matches[1]) |
| out.Write(matches[2]) |
| out.Write(matches[3]) |
| out.WriteString(latestVer.raw) |
| out.Write(matches[5]) |
| out.WriteByte('\n') |
| |
| fmt.Printf("%s: Updating %s: %s -> %s\n", path, depName, currentVerStr, latestVer.raw) |
| lineChanged = true |
| changed = true |
| } |
| } |
| |
| if !lineChanged { |
| out.Write(line) |
| out.WriteByte('\n') |
| } |
| } |
| |
| if changed { |
| if err := os.WriteFile(path, out.Bytes(), 0666); err != nil { |
| return false, err |
| } |
| return true, nil |
| } |
| |
| fmt.Printf("%s: All dependencies up to date.\n", path) |
| return false, nil |
| } |
| |
| func updateLockfile(dir string) error { |
| cmd := exec.Command("bazelisk", "mod", "deps", "--lockfile_mode=refresh") |
| cmd.Dir = dir |
| cmd.Stdout = os.Stdout |
| cmd.Stderr = os.Stderr |
| fmt.Printf("Running bazelisk mod deps --lockfile_mode=refresh in %s...\n", dir) |
| return cmd.Run() |
| } |
| |
| func main() { |
| flag.Parse() |
| |
| files := flag.Args() |
| if len(files) == 0 { |
| files = []string{"MODULE.bazel", "util/bazel-example/MODULE.bazel"} |
| } |
| |
| var anyChanged bool |
| for _, file := range files { |
| changed, err := updateFile(file) |
| if err != nil { |
| fmt.Fprintf(os.Stderr, "Error updating %s: %s\n", file, err) |
| os.Exit(1) |
| } |
| anyChanged = anyChanged || changed |
| } |
| |
| // If any dependencies changed, update all lock files. The lockfiles for |
| // the example project depend on the root project. |
| if anyChanged { |
| for _, file := range files { |
| dir := filepath.Dir(file) |
| if err := updateLockfile(dir); err != nil { |
| fmt.Fprintf(os.Stderr, "Error updating lockfile in %s: %s\n", dir, err) |
| os.Exit(1) |
| } |
| } |
| } |
| } |