• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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