1// Copyright 2024 The BoringSSL Authors 2// 3// Permission to use, copy, modify, and/or distribute this software for any 4// purpose with or without fee is hereby granted, provided that the above 5// copyright notice and this permission notice appear in all copies. 6// 7// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 10// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION 12// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN 13// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 15// prepare_bcr_module prepares for a BCR release. It outputs a JSON 16// configuration file that may be used by BCR's add_module tool. 17package main 18 19import ( 20 "archive/tar" 21 "bytes" 22 "compress/gzip" 23 "crypto/sha256" 24 "encoding/json" 25 "flag" 26 "fmt" 27 "io" 28 "net/http" 29 "os" 30 "path/filepath" 31 "runtime" 32 "strings" 33) 34 35var ( 36 outDir = flag.String("out-dir", "", "The directory to place the script output, or a temporary directory if unspecified.") 37 numWorkers = flag.Int("num-workers", runtime.NumCPU(), "Runs the given number of workers") 38 39 moduleOverride = flag.String("module-override", "", "The path to a file that overrides the MODULE.bazel file in the archve.") 40 presubmitOverride = flag.String("presubmit-override", "", "The path to a file that overrides the presubmit.yml file in the archve.") 41 skipArchiveCheck = flag.Bool("skip-archive-check", false, "Skips checking the release tarball against the (potentially unstable) archive tarball.") 42 pipe = flag.Bool("pipe", false, "Prints output suitable for writing to a pipe instead of a terminal") 43 44 githubOrg = flag.String("github-org", "google", "The organization where the GitHub repository lives") 45 githubRepo = flag.String("github-repo", "boringssl", "The name of the GitHub repository") 46 moduleName = flag.String("module-name", "boringssl", "The name of the BCR module") 47 compatibilityLevel = flag.String("compatibility-level", "2", "The compatibility_level setting for the BCR module") 48) 49 50// A bcrConfig is a configuration file for BCR's add_module tool. This is 51// undocumented but can be seen in the Module Python class. (The JSON struct is 52// simply the object's __dict__.) 53type bcrConfig struct { 54 Name string `json:"name"` 55 Version string `json:"version"` 56 CompatibilityLevel string `json:"compatibility_level"` 57 ModuleDotBazel *string `json:"module_dot_bazel"` 58 URL *string `json:"url"` 59 StripPrefix *string `json:"strip_prefix"` 60 Deps []string `json:"deps"` 61 Patches []string `json:"patches"` 62 PatchStrip int `json:"patch_strip"` 63 BuildFile *string `json:"build_file"` 64 PresubmitYml *string `json:"presubmit_yml"` 65 BuildTargets []string `json:"build_targets"` 66 TestModulePath *string `json:"test_module_path"` 67 TestModuleBuildTargets []string `json:"test_module_build_targets"` 68 TestModuleTestTargets []string `json:"test_module_test_targets"` 69} 70 71func ptr[T any](t T) *T { return &t } 72 73func archiveURL(tag string) string { 74 return fmt.Sprintf("https://github.com/%s/%s/archive/refs/tags/%s.tar.gz", *githubOrg, *githubRepo, tag) 75} 76 77func releaseURL(tag string) string { 78 return fmt.Sprintf("https://github.com/%s/%s/releases/download/%s/%s-%s.tar.gz", *githubOrg, *githubRepo, tag, *githubRepo, tag) 79} 80 81func releaseViewURL(tag string) string { 82 return fmt.Sprintf("https://github.com/%s/%s/releases/tag/%s", *githubOrg, *githubRepo, tag) 83} 84 85func releaseEditURL(tag string) string { 86 return fmt.Sprintf("https://github.com/%s/%s/releases/edit/%s", *githubOrg, *githubRepo, tag) 87} 88 89func fetch(url string) (*http.Response, error) { 90 resp, err := http.Get(url) 91 if err != nil { 92 return nil, err 93 } 94 if resp.StatusCode != 200 { 95 resp.Body.Close() 96 return nil, fmt.Errorf("got status code of %d from %q instead of 200", resp.StatusCode, url) 97 } 98 return resp, nil 99} 100 101type releaseFetchError struct{ error } 102type releaseMismatchError struct{ error } 103 104func sha256Reader(r io.Reader) ([]byte, error) { 105 h := sha256.New() 106 if _, err := io.Copy(h, r); err != nil { 107 return nil, err 108 } 109 return h.Sum(nil), nil 110} 111 112func run(tag string) error { 113 // Check the tag does not contain any characters that would break the URL 114 // or filesystem. 115 for _, c := range tag { 116 if c != '.' && !('0' <= c && c <= '9') && !('a' <= c && c <= 'z') && !('A' <= c && c <= 'Z') { 117 return fmt.Errorf("invalid tag %q", tag) 118 } 119 } 120 121 // Read the tag from git. We will use this to ensure the archive is correct. 122 var expectedTree []treeEntry 123 if err := step("Hashing tree from git", func(s *stepPrinter) error { 124 var err error 125 expectedTree, err = gitHashTree(s, tag) 126 return err 127 }); err != nil { 128 return err 129 } 130 131 // Hash the archive tarball. 132 // 133 // BCR does not accept archive tarballs, due to concerns that GitHub may 134 // change the hash, and instead prefers release tarballs. Release tarballs, 135 // however, are uploaded by individual developers, with no guaranteed they 136 // match the contents of the tag. 137 // 138 // This script checks the release tarball against the tag in the on-disk git 139 // repository, so we validate the contents independent of GitHub. We 140 // additionally check that release tarball matches the archive tarball. The 141 // archive tarballs are stable in practice, and this is an easy, though 142 // still GitHub-dependent, property that anyone can check. (This script 143 // assumes GitHub did not change their tarballs in the short window between 144 // when the release tarball was uploaded and this script runs.) 145 var archiveSHA256 []byte 146 if !*skipArchiveCheck { 147 if err := step("Fetching archive tarball", func(s *stepPrinter) error { 148 archive, err := fetch(archiveURL(tag)) 149 if err != nil { 150 return err 151 } 152 defer archive.Body.Close() 153 archiveSHA256, err = sha256Reader(s.httpBodyWithProgress(archive)) 154 return err 155 }); err != nil { 156 return err 157 } 158 } 159 160 // Prepare an output directory. 161 var dir string 162 var err error 163 if len(*outDir) != 0 { 164 dir, err = filepath.Abs(*outDir) 165 } else { 166 dir, err = os.MkdirTemp("", "boringssl_bcr") 167 } 168 if err != nil { 169 return err 170 } 171 172 // Fetch the release tarball. As we stream it, we do three things: 173 // 174 // 1. Compute the overall SHA-256 sum. This hash must be saved in the BCR 175 // configuration. 176 // 177 // 2. Hash the contents of each file in the tarball, to compare against the 178 // contents in git. 179 // 180 // 3. Extract MODULE.bazel and presubmit.yml, to save in the temporary 181 // directory. This is needed to work around limitations in BCR's tooling. 182 // See https://github.com/bazelbuild/bazel-central-registry/issues/2781 183 var releaseTree []treeEntry 184 releaseHash := sha256.New() 185 stripPrefix := fmt.Sprintf("%s-%s/", *githubRepo, tag) 186 if err := step("Fetching release tarball", func(s *stepPrinter) error { 187 release, err := fetch(releaseURL(tag)) 188 if err != nil { 189 return releaseFetchError{err} 190 } 191 defer release.Body.Close() 192 193 // Hash the tarball as we read it. 194 reader := s.httpBodyWithProgress(release) 195 reader = io.TeeReader(reader, releaseHash) 196 197 zlibReader, err := gzip.NewReader(reader) 198 if err != nil { 199 return fmt.Errorf("error reading release tarball: %w", err) 200 } 201 202 tarReader := tar.NewReader(zlibReader) 203 var seenModule, seenPresubmit bool 204 for { 205 header, err := tarReader.Next() 206 if err == io.EOF { 207 break 208 } 209 if err != nil { 210 return fmt.Errorf("error reading release tarball: %w", err) 211 } 212 213 var mode treeEntryMode 214 var fileReader io.Reader 215 switch header.Typeflag { 216 case tar.TypeDir: 217 // Check directories have a suitable prefix, but otherwise ignore 218 // them. 219 if !strings.HasPrefix(header.Name, stripPrefix) { 220 return fmt.Errorf("release tarball contained path %q which did not begin with %q", header.Name, stripPrefix) 221 } 222 continue 223 case tar.TypeXGlobalHeader: 224 continue 225 case tar.TypeReg: 226 if header.Mode&1 != 0 { 227 mode = treeEntryExecutable 228 } else { 229 mode = treeEntryRegular 230 } 231 fileReader = tarReader 232 case tar.TypeSymlink: 233 mode = treeEntrySymlink 234 fileReader = strings.NewReader(header.Linkname) 235 default: 236 return fmt.Errorf("path %q in release archive had unknown type %d", header.Name, header.Typeflag) 237 } 238 239 path, ok := strings.CutPrefix(header.Name, stripPrefix) 240 if !ok { 241 return fmt.Errorf("release tarball contained path %q which did not begin with %q", header.Name, stripPrefix) 242 } 243 244 var saveFile *os.File 245 if mode == treeEntryRegular && path == "MODULE.bazel" { 246 if seenModule { 247 return fmt.Errorf("release tarball contained duplicate MODULE.bazel file") 248 } 249 saveFile, err = os.Create(filepath.Join(dir, "MODULE.bazel")) 250 if err != nil { 251 return err 252 } 253 seenModule = true 254 } else if mode == treeEntryRegular && path == ".bcr/presubmit.yml" { 255 if seenPresubmit { 256 return fmt.Errorf("release tarball contained duplicate .bcr/presubmit.yml file") 257 } 258 saveFile, err = os.Create(filepath.Join(dir, "presubmit.yml")) 259 if err != nil { 260 return err 261 } 262 seenPresubmit = true 263 } 264 265 if saveFile != nil { 266 fileReader = io.TeeReader(fileReader, saveFile) 267 } 268 269 sha256, err := sha256Reader(fileReader) 270 saveFile.Close() 271 if err != nil { 272 return fmt.Errorf("error reading %q in release archive: %w", header.Name, err) 273 } 274 275 releaseTree = append(releaseTree, treeEntry{path: path, mode: mode, sha256: sha256}) 276 } 277 278 sortTree(releaseTree) 279 280 // Check the zlib checksum is correct. 281 if err := zlibReader.Close(); err != nil { 282 return fmt.Errorf("error reading release tarball: %w", err) 283 } 284 285 // Ensure we have read (and thus hashed) the entire archive. 286 if _, err := io.Copy(io.Discard, reader); err != nil { 287 return fmt.Errorf("error reading release archive: %w", err) 288 } 289 290 if !seenModule && len(*moduleOverride) == 0 { 291 return fmt.Errorf("could not find MODULE.bazel in release tarball") 292 } 293 if !seenPresubmit && len(*presubmitOverride) == 0 { 294 return fmt.Errorf("could not find .bcr/presubmit.yml in release tarball") 295 } 296 return nil 297 }); err != nil { 298 return err 299 } 300 301 releaseSHA256 := releaseHash.Sum(nil) 302 if !*skipArchiveCheck && !bytes.Equal(archiveSHA256, releaseSHA256) { 303 return releaseMismatchError{fmt.Errorf("release hash was %x, which did not match archive hash was %x", archiveSHA256, releaseSHA256)} 304 } 305 306 if err := compareTrees(releaseTree, expectedTree); err != nil { 307 return err 308 } 309 310 config := bcrConfig{ 311 Name: *moduleName, 312 Version: tag, 313 CompatibilityLevel: *compatibilityLevel, 314 ModuleDotBazel: ptr(filepath.Join(dir, "MODULE.bazel")), 315 URL: ptr(releaseURL(tag)), 316 StripPrefix: &stripPrefix, 317 PresubmitYml: ptr(filepath.Join(dir, "presubmit.yml")), 318 // encoding/json will encode nil slices as null instead of the empty array. 319 Deps: []string{}, 320 Patches: []string{}, 321 BuildTargets: []string{}, 322 TestModuleBuildTargets: []string{}, 323 TestModuleTestTargets: []string{}, 324 } 325 326 if len(*moduleOverride) != 0 { 327 override, err := filepath.Abs(*moduleOverride) 328 if err != nil { 329 return err 330 } 331 config.ModuleDotBazel = &override 332 } 333 if len(*presubmitOverride) != 0 { 334 override, err := filepath.Abs(*presubmitOverride) 335 if err != nil { 336 return err 337 } 338 config.PresubmitYml = &override 339 } 340 341 configJSON, err := json.Marshal(config) 342 if err != nil { 343 return err 344 } 345 346 jsonPath := filepath.Join(dir, "bcr.json") 347 if err := os.WriteFile(jsonPath, configJSON, 0666); err != nil { 348 return err 349 } 350 351 fmt.Printf("\n") 352 fmt.Printf("BCR configuration written to %q\n", dir) 353 fmt.Printf("\n") 354 fmt.Printf("Clone the BCR repository at:\n") 355 fmt.Printf(" https://github.com/bazelbuild/bazel-central-registry\n") 356 fmt.Printf("\n") 357 fmt.Printf("Then, run the following command to prepare the module update:\n") 358 fmt.Printf(" bazelisk run //tools:add_module -- --input %s\n", jsonPath) 359 fmt.Printf("\n") 360 fmt.Printf("Finally, commit the result and send the BCR repository a PR.\n") 361 return nil 362} 363 364func main() { 365 flag.Usage = func() { 366 fmt.Fprint(os.Stderr, "Usage: go run ./util/prepare_bcr_module [FLAGS...] TAG\n") 367 flag.PrintDefaults() 368 } 369 flag.Parse() 370 if flag.NArg() != 1 { 371 fmt.Fprintf(os.Stderr, "Expected exactly one tag specified.\n") 372 flag.Usage() 373 os.Exit(1) 374 } 375 376 tag := flag.Arg(0) 377 if err := run(tag); err != nil { 378 if _, ok := err.(releaseFetchError); ok { 379 fmt.Fprintf(os.Stderr, "Error fetching release URL for %q: %s\n", tag, err) 380 fmt.Fprintf(os.Stderr, "\n") 381 fmt.Fprintf(os.Stderr, "To fix this, follow the following steps:\n") 382 fmt.Fprintf(os.Stderr, "1. Open %s in a browser.\n", releaseViewURL(tag)) 383 fmt.Fprintf(os.Stderr, "2. Download the \"Source code (tar.gz)\" archive.\n") 384 fmt.Fprintf(os.Stderr, "3. Click the edit icon, or open %s in your browser.\n", releaseEditURL(tag)) 385 fmt.Fprintf(os.Stderr, "4. Attach the downloaded boringssl-%s.tar.gz to the release.\n", tag) 386 fmt.Fprintf(os.Stderr, "\n") 387 } else if _, ok := err.(releaseMismatchError); ok { 388 fmt.Fprintf(os.Stderr, "Invalid release tarball for %q: %s\n", tag, err) 389 fmt.Fprintf(os.Stderr, "\n") 390 fmt.Fprintf(os.Stderr, "To fix this, follow the following steps:\n") 391 fmt.Fprintf(os.Stderr, "1. Open %s in a browser.\n", releaseViewURL(tag)) 392 fmt.Fprintf(os.Stderr, "2. Download the \"Source code (tar.gz)\" archive.\n") 393 fmt.Fprintf(os.Stderr, "3. Click the edit icon, or open %s in your browser.\n", releaseEditURL(tag)) 394 fmt.Fprintf(os.Stderr, "4. Delete the old boringssl-%s.tar.gz from the release.\n", tag) 395 fmt.Fprintf(os.Stderr, "5. Re-attach the downloaded boringssl-%s.tar.gz to the release.\n", tag) 396 fmt.Fprintf(os.Stderr, "\n") 397 } else { 398 fmt.Fprintf(os.Stderr, "Error preparing release %q: %s\n", tag, err) 399 } 400 os.Exit(1) 401 } 402} 403