| // Copyright (c) 2024, Google Inc. |
| // |
| // Permission to use, copy, modify, and/or distribute this software for any |
| // purpose with or without fee is hereby granted, provided that the above |
| // copyright notice and this permission notice appear in all copies. |
| // |
| // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES |
| // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF |
| // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY |
| // SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES |
| // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION |
| // OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN |
| // CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. |
| |
| // prepare_bcr_module prepares for a BCR release. It outputs a JSON |
| // configuration file that may be used by BCR's add_module tool. |
| package main |
| |
| import ( |
| "archive/tar" |
| "bytes" |
| "compress/gzip" |
| "crypto/sha256" |
| "encoding/json" |
| "flag" |
| "fmt" |
| "io" |
| "net/http" |
| "os" |
| "path/filepath" |
| "runtime" |
| "strings" |
| ) |
| |
| var ( |
| outDir = flag.String("out-dir", "", "The directory to place the script output, or a temporary directory if unspecified.") |
| numWorkers = flag.Int("num-workers", runtime.NumCPU(), "Runs the given number of workers") |
| |
| moduleOverride = flag.String("module-override", "", "The path to a file that overrides the MODULE.bazel file in the archve.") |
| presubmitOverride = flag.String("presubmit-override", "", "The path to a file that overrides the presubmit.yml file in the archve.") |
| skipArchiveCheck = flag.Bool("skip-archive-check", false, "Skips checking the release tarball against the (potentially unstable) archive tarball.") |
| pipe = flag.Bool("pipe", false, "Prints output suitable for writing to a pipe instead of a terminal") |
| |
| githubOrg = flag.String("github-org", "google", "The organization where the GitHub repository lives") |
| githubRepo = flag.String("github-repo", "boringssl", "The name of the GitHub repository") |
| moduleName = flag.String("module-name", "boringssl", "The name of the BCR module") |
| compatibilityLevel = flag.String("compatibility-level", "2", "The compatibility_level setting for the BCR module") |
| ) |
| |
| // A bcrConfig is a configuration file for BCR's add_module tool. This is |
| // undocumented but can be seen in the Module Python class. (The JSON struct is |
| // simply the object's __dict__.) |
| type bcrConfig struct { |
| Name string `json:"name"` |
| Version string `json:"version"` |
| CompatibilityLevel string `json:"compatibility_level"` |
| ModuleDotBazel *string `json:"module_dot_bazel"` |
| URL *string `json:"url"` |
| StripPrefix *string `json:"strip_prefix"` |
| Deps []string `json:"deps"` |
| Patches []string `json:"patches"` |
| PatchStrip int `json:"patch_strip"` |
| BuildFile *string `json:"build_file"` |
| PresubmitYml *string `json:"presubmit_yml"` |
| BuildTargets []string `json:"build_targets"` |
| TestModulePath *string `json:"test_module_path"` |
| TestModuleBuildTargets []string `json:"test_module_build_targets"` |
| TestModuleTestTargets []string `json:"test_module_test_targets"` |
| } |
| |
| func ptr[T any](t T) *T { return &t } |
| |
| func archiveURL(tag string) string { |
| return fmt.Sprintf("https://github.com/%s/%s/archive/refs/tags/%s.tar.gz", *githubOrg, *githubRepo, tag) |
| } |
| |
| func releaseURL(tag string) string { |
| return fmt.Sprintf("https://github.com/%s/%s/releases/download/%s/%s-%s.tar.gz", *githubOrg, *githubRepo, tag, *githubRepo, tag) |
| } |
| |
| func releaseViewURL(tag string) string { |
| return fmt.Sprintf("https://github.com/%s/%s/releases/tag/%s", *githubOrg, *githubRepo, tag) |
| } |
| |
| func releaseEditURL(tag string) string { |
| return fmt.Sprintf("https://github.com/%s/%s/releases/edit/%s", *githubOrg, *githubRepo, tag) |
| } |
| |
| func fetch(url string) (*http.Response, error) { |
| resp, err := http.Get(url) |
| if err != nil { |
| return nil, err |
| } |
| if resp.StatusCode != 200 { |
| resp.Body.Close() |
| return nil, fmt.Errorf("got status code of %d from %q instead of 200", resp.StatusCode, url) |
| } |
| return resp, nil |
| } |
| |
| type releaseFetchError struct{ error } |
| type releaseMismatchError struct{ error } |
| |
| func sha256Reader(r io.Reader) ([]byte, error) { |
| h := sha256.New() |
| if _, err := io.Copy(h, r); err != nil { |
| return nil, err |
| } |
| return h.Sum(nil), nil |
| } |
| |
| func run(tag string) error { |
| // Check the tag does not contain any characters that would break the URL |
| // or filesystem. |
| for _, c := range tag { |
| if c != '.' && !('0' <= c && c <= '9') && !('a' <= c && c <= 'z') && !('A' <= c && c <= 'Z') { |
| return fmt.Errorf("invalid tag %q", tag) |
| } |
| } |
| |
| // Read the tag from git. We will use this to ensure the archive is correct. |
| var expectedTree []treeEntry |
| if err := step("Hashing tree from git", func(s *stepPrinter) error { |
| var err error |
| expectedTree, err = gitHashTree(s, tag) |
| return err |
| }); err != nil { |
| return err |
| } |
| |
| // Hash the archive tarball. |
| // |
| // BCR does not accept archive tarballs, due to concerns that GitHub may |
| // change the hash, and instead prefers release tarballs. Release tarballs, |
| // however, are uploaded by individual developers, with no guaranteed they |
| // match the contents of the tag. |
| // |
| // This script checks the release tarball against the tag in the on-disk git |
| // repository, so we validate the contents independent of GitHub. We |
| // additionally check that release tarball matches the archive tarball. The |
| // archive tarballs are stable in practice, and this is an easy, though |
| // still GitHub-dependent, property that anyone can check. (This script |
| // assumes GitHub did not change their tarballs in the short window between |
| // when the release tarball was uploaded and this script runs.) |
| var archiveSHA256 []byte |
| if !*skipArchiveCheck { |
| if err := step("Fetching archive tarball", func(s *stepPrinter) error { |
| archive, err := fetch(archiveURL(tag)) |
| if err != nil { |
| return err |
| } |
| defer archive.Body.Close() |
| archiveSHA256, err = sha256Reader(s.httpBodyWithProgress(archive)) |
| return err |
| }); err != nil { |
| return err |
| } |
| } |
| |
| // Prepare an output directory. |
| var dir string |
| var err error |
| if len(*outDir) != 0 { |
| dir, err = filepath.Abs(*outDir) |
| } else { |
| dir, err = os.MkdirTemp("", "boringssl_bcr") |
| } |
| if err != nil { |
| return err |
| } |
| |
| // Fetch the release tarball. As we stream it, we do three things: |
| // |
| // 1. Compute the overall SHA-256 sum. This hash must be saved in the BCR |
| // configuration. |
| // |
| // 2. Hash the contents of each file in the tarball, to compare against the |
| // contents in git. |
| // |
| // 3. Extract MODULE.bazel and presubmit.yml, to save in the temporary |
| // directory. This is needed to work around limitations in BCR's tooling. |
| // See https://github.com/bazelbuild/bazel-central-registry/issues/2781 |
| var releaseTree []treeEntry |
| releaseHash := sha256.New() |
| stripPrefix := fmt.Sprintf("%s-%s/", *githubRepo, tag) |
| if err := step("Fetching release tarball", func(s *stepPrinter) error { |
| release, err := fetch(releaseURL(tag)) |
| if err != nil { |
| return releaseFetchError{err} |
| } |
| defer release.Body.Close() |
| |
| // Hash the tarball as we read it. |
| reader := s.httpBodyWithProgress(release) |
| reader = io.TeeReader(reader, releaseHash) |
| |
| zlibReader, err := gzip.NewReader(reader) |
| if err != nil { |
| return fmt.Errorf("error reading release tarball: %w", err) |
| } |
| |
| tarReader := tar.NewReader(zlibReader) |
| var seenModule, seenPresubmit bool |
| for { |
| header, err := tarReader.Next() |
| if err == io.EOF { |
| break |
| } |
| if err != nil { |
| return fmt.Errorf("error reading release tarball: %w", err) |
| } |
| |
| var mode treeEntryMode |
| var fileReader io.Reader |
| switch header.Typeflag { |
| case tar.TypeDir: |
| // Check directories have a suitable prefix, but otherwise ignore |
| // them. |
| if !strings.HasPrefix(header.Name, stripPrefix) { |
| return fmt.Errorf("release tarball contained path %q which did not begin with %q", header.Name, stripPrefix) |
| } |
| continue |
| case tar.TypeXGlobalHeader: |
| continue |
| case tar.TypeReg: |
| if header.Mode&1 != 0 { |
| mode = treeEntryExecutable |
| } else { |
| mode = treeEntryRegular |
| } |
| fileReader = tarReader |
| case tar.TypeSymlink: |
| mode = treeEntrySymlink |
| fileReader = strings.NewReader(header.Linkname) |
| default: |
| return fmt.Errorf("path %q in release archive had unknown type %d", header.Name, header.Typeflag) |
| } |
| |
| path, ok := strings.CutPrefix(header.Name, stripPrefix) |
| if !ok { |
| return fmt.Errorf("release tarball contained path %q which did not begin with %q", header.Name, stripPrefix) |
| } |
| |
| var saveFile *os.File |
| if mode == treeEntryRegular && path == "MODULE.bazel" { |
| if seenModule { |
| return fmt.Errorf("release tarball contained duplicate MODULE.bazel file") |
| } |
| saveFile, err = os.Create(filepath.Join(dir, "MODULE.bazel")) |
| if err != nil { |
| return err |
| } |
| seenModule = true |
| } else if mode == treeEntryRegular && path == ".bcr/presubmit.yml" { |
| if seenPresubmit { |
| return fmt.Errorf("release tarball contained duplicate .bcr/presubmit.yml file") |
| } |
| saveFile, err = os.Create(filepath.Join(dir, "presubmit.yml")) |
| if err != nil { |
| return err |
| } |
| seenPresubmit = true |
| } |
| |
| if saveFile != nil { |
| fileReader = io.TeeReader(fileReader, saveFile) |
| } |
| |
| sha256, err := sha256Reader(fileReader) |
| saveFile.Close() |
| if err != nil { |
| return fmt.Errorf("error reading %q in release archive: %w", header.Name, err) |
| } |
| |
| releaseTree = append(releaseTree, treeEntry{path: path, mode: mode, sha256: sha256}) |
| } |
| |
| sortTree(releaseTree) |
| |
| // Check the zlib checksum is correct. |
| if err := zlibReader.Close(); err != nil { |
| return fmt.Errorf("error reading release tarball: %w", err) |
| } |
| |
| // Ensure we have read (and thus hashed) the entire archive. |
| if _, err := io.Copy(io.Discard, reader); err != nil { |
| return fmt.Errorf("error reading release archive: %w", err) |
| } |
| |
| if !seenModule && len(*moduleOverride) == 0 { |
| return fmt.Errorf("could not find MODULE.bazel in release tarball") |
| } |
| if !seenPresubmit && len(*presubmitOverride) == 0 { |
| return fmt.Errorf("could not find .bcr/presubmit.yml in release tarball") |
| } |
| return nil |
| }); err != nil { |
| return err |
| } |
| |
| releaseSHA256 := releaseHash.Sum(nil) |
| if !*skipArchiveCheck && !bytes.Equal(archiveSHA256, releaseSHA256) { |
| return releaseMismatchError{fmt.Errorf("release hash was %x, which did not match archive hash was %x", archiveSHA256, releaseSHA256)} |
| } |
| |
| if err := compareTrees(releaseTree, expectedTree); err != nil { |
| return err |
| } |
| |
| config := bcrConfig{ |
| Name: *moduleName, |
| Version: tag, |
| CompatibilityLevel: *compatibilityLevel, |
| ModuleDotBazel: ptr(filepath.Join(dir, "MODULE.bazel")), |
| URL: ptr(releaseURL(tag)), |
| StripPrefix: &stripPrefix, |
| PresubmitYml: ptr(filepath.Join(dir, "presubmit.yml")), |
| // encoding/json will encode nil slices as null instead of the empty array. |
| Deps: []string{}, |
| Patches: []string{}, |
| BuildTargets: []string{}, |
| TestModuleBuildTargets: []string{}, |
| TestModuleTestTargets: []string{}, |
| } |
| |
| if len(*moduleOverride) != 0 { |
| override, err := filepath.Abs(*moduleOverride) |
| if err != nil { |
| return err |
| } |
| config.ModuleDotBazel = &override |
| } |
| if len(*presubmitOverride) != 0 { |
| override, err := filepath.Abs(*presubmitOverride) |
| if err != nil { |
| return err |
| } |
| config.PresubmitYml = &override |
| } |
| |
| configJSON, err := json.Marshal(config) |
| if err != nil { |
| return err |
| } |
| |
| jsonPath := filepath.Join(dir, "bcr.json") |
| if err := os.WriteFile(jsonPath, configJSON, 0666); err != nil { |
| return err |
| } |
| |
| fmt.Printf("\n") |
| fmt.Printf("BCR configuration written to %q\n", dir) |
| fmt.Printf("\n") |
| fmt.Printf("Clone the BCR repository at:\n") |
| fmt.Printf(" https://github.com/bazelbuild/bazel-central-registry\n") |
| fmt.Printf("\n") |
| fmt.Printf("Then, run the following command to prepare the module update:\n") |
| fmt.Printf(" bazelisk run //tools:add_module -- --input %s\n", jsonPath) |
| fmt.Printf("\n") |
| fmt.Printf("Finally, commit the result and send the BCR repository a PR.\n") |
| return nil |
| } |
| |
| func main() { |
| flag.Usage = func() { |
| fmt.Fprint(os.Stderr, "Usage: go run ./util/prepare_bcr_module [FLAGS...] TAG\n") |
| flag.PrintDefaults() |
| } |
| flag.Parse() |
| if flag.NArg() != 1 { |
| fmt.Fprintf(os.Stderr, "Expected exactly one tag specified.\n") |
| flag.Usage() |
| os.Exit(1) |
| } |
| |
| tag := flag.Arg(0) |
| if err := run(tag); err != nil { |
| if _, ok := err.(releaseFetchError); ok { |
| fmt.Fprintf(os.Stderr, "Error fetching release URL for %q: %s\n", tag, err) |
| fmt.Fprintf(os.Stderr, "\n") |
| fmt.Fprintf(os.Stderr, "To fix this, follow the following steps:\n") |
| fmt.Fprintf(os.Stderr, "1. Open %s in a browser.\n", releaseViewURL(tag)) |
| fmt.Fprintf(os.Stderr, "2. Download the \"Source code (tar.gz)\" archive.\n") |
| fmt.Fprintf(os.Stderr, "3. Click the edit icon, or open %s in your browser.\n", releaseEditURL(tag)) |
| fmt.Fprintf(os.Stderr, "4. Attach the downloaded boringssl-%s.tar.gz to the release.\n", tag) |
| fmt.Fprintf(os.Stderr, "\n") |
| } else if _, ok := err.(releaseMismatchError); ok { |
| fmt.Fprintf(os.Stderr, "Invalid release tarball for %q: %s\n", tag, err) |
| fmt.Fprintf(os.Stderr, "\n") |
| fmt.Fprintf(os.Stderr, "To fix this, follow the following steps:\n") |
| fmt.Fprintf(os.Stderr, "1. Open %s in a browser.\n", releaseViewURL(tag)) |
| fmt.Fprintf(os.Stderr, "2. Download the \"Source code (tar.gz)\" archive.\n") |
| fmt.Fprintf(os.Stderr, "3. Click the edit icon, or open %s in your browser.\n", releaseEditURL(tag)) |
| fmt.Fprintf(os.Stderr, "4. Delete the old boringssl-%s.tar.gz from the release.\n", tag) |
| fmt.Fprintf(os.Stderr, "5. Re-attach the downloaded boringssl-%s.tar.gz to the release.\n", tag) |
| fmt.Fprintf(os.Stderr, "\n") |
| } else { |
| fmt.Fprintf(os.Stderr, "Error preparing release %q: %s\n", tag, err) |
| } |
| os.Exit(1) |
| } |
| } |