xref: /aosp_15_r20/external/boringssl/src/util/pregenerate/pregenerate.go (revision 8fb009dc861624b67b6cdb62ea21f0f22d0c584b)
1// Copyright (c) 2024, Google Inc.
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// pregenerate manages generated files in BoringSSL
16package main
17
18import (
19	"bytes"
20	"encoding/json"
21	"errors"
22	"flag"
23	"fmt"
24	"os"
25	"path/filepath"
26	"runtime"
27	"slices"
28	"strings"
29	"sync"
30
31	"boringssl.googlesource.com/boringssl/util/build"
32)
33
34var (
35	check      = flag.Bool("check", false, "Check whether any files need to be updated, without actually updating them")
36	numWorkers = flag.Int("num-workers", runtime.NumCPU(), "Runs the given number of workers")
37	dryRun     = flag.Bool("dry-run", false, "Skip actually writing any files")
38	perlPath   = flag.String("perl", "perl", "Path to the perl command")
39	list       = flag.Bool("list", false, "List all generated files, rather than actually run them")
40)
41
42func runTask(t Task) error {
43	expected, err := t.Run()
44	if err != nil {
45		return err
46	}
47
48	dst := t.Destination()
49	dstPath := filepath.FromSlash(dst)
50	if *check {
51		actual, err := os.ReadFile(dstPath)
52		if err != nil {
53			if os.IsNotExist(err) {
54				err = errors.New("missing file")
55			}
56			return err
57		}
58
59		if !bytes.Equal(expected, actual) {
60			return errors.New("file out of date")
61		}
62		return nil
63	}
64
65	if *dryRun {
66		fmt.Printf("Would write %d bytes to %q\n", len(expected), dst)
67		return nil
68	}
69
70	if err := os.MkdirAll(filepath.Dir(dstPath), 0777); err != nil {
71		return err
72	}
73	return os.WriteFile(dstPath, expected, 0666)
74}
75
76type taskError struct {
77	dst string
78	err error
79}
80
81func worker(taskChan <-chan Task, errorChan chan<- taskError, wg *sync.WaitGroup) {
82	defer wg.Done()
83	for t := range taskChan {
84		if err := runTask(t); err != nil {
85			errorChan <- taskError{t.Destination(), err}
86		}
87	}
88}
89
90func run() error {
91	if _, err := os.Stat("BUILDING.md"); err != nil {
92		return fmt.Errorf("must be run from BoringSSL source root")
93	}
94
95	buildJSON, err := os.ReadFile("build.json")
96	if err != nil {
97		return err
98	}
99
100	// Remove comments. For now, just do a very basic preprocessing step. If
101	// needed, we can switch to something well-defined like one of the many
102	// dozen different extended JSONs like JSON5.
103	lines := bytes.Split(buildJSON, []byte("\n"))
104	for i := range lines {
105		if idx := bytes.Index(lines[i], []byte("//")); idx >= 0 {
106			lines[i] = lines[i][:idx]
107		}
108	}
109	buildJSON = bytes.Join(lines, []byte("\n"))
110
111	var targetsIn map[string]InputTarget
112	if err := json.Unmarshal(buildJSON, &targetsIn); err != nil {
113		return fmt.Errorf("error decoding build config: %s", err)
114	}
115
116	var tasks []Task
117	targetsOut := make(map[string]build.Target)
118	for name, targetIn := range targetsIn {
119		targetOut, targetTasks, err := targetIn.Pregenerate(name)
120		if err != nil {
121			return err
122		}
123		targetsOut[name] = targetOut
124		tasks = append(tasks, targetTasks...)
125	}
126
127	tasks = append(tasks, MakeBuildFiles(targetsOut)...)
128	tasks = append(tasks, NewSimpleTask("gen/README.md", func() ([]byte, error) {
129		return []byte(readme), nil
130	}))
131
132	// Filter tasks by command-line argument.
133	if args := flag.Args(); len(args) != 0 {
134		var filtered []Task
135		for _, t := range tasks {
136			dst := t.Destination()
137			for _, arg := range args {
138				if strings.Contains(dst, arg) {
139					filtered = append(filtered, t)
140					break
141				}
142			}
143		}
144		tasks = filtered
145	}
146
147	if *list {
148		paths := make([]string, len(tasks))
149		for i, t := range tasks {
150			paths[i] = t.Destination()
151		}
152		slices.Sort(paths)
153		for _, p := range paths {
154			fmt.Println(p)
155		}
156		return nil
157	}
158
159	// Schedule tasks in parallel. Perlasm benefits from running in parallel. The
160	// others likely do not, but it is simpler to parallelize them all.
161	var wg sync.WaitGroup
162	taskChan := make(chan Task, *numWorkers)
163	errorChan := make(chan taskError, *numWorkers)
164	for i := 0; i < *numWorkers; i++ {
165		wg.Add(1)
166		go worker(taskChan, errorChan, &wg)
167	}
168
169	go func() {
170		for _, t := range tasks {
171			taskChan <- t
172		}
173		close(taskChan)
174		wg.Wait()
175		close(errorChan)
176	}()
177
178	var failed bool
179	for err := range errorChan {
180		fmt.Fprintf(os.Stderr, "Error in file %q: %s\n", err.dst, err.err)
181		failed = true
182	}
183	if failed {
184		return errors.New("some files had errors")
185	}
186	return nil
187}
188
189func main() {
190	flag.Parse()
191	if err := run(); err != nil {
192		fmt.Fprintf(os.Stderr, "Error: %s\n", err)
193		os.Exit(1)
194	}
195}
196
197const readme = `# Pre-generated files
198
199This directory contains a number of pre-generated build artifacts. To simplify
200downstream builds, they are checked into the repository, rather than dynamically
201generated as part of the build.
202
203When developing on BoringSSL, if any inputs to these files are modified, callers
204must run the following command to update the generated files:
205
206    go run ./util/pregenerate
207
208To check that files are up-to-date without updating files, run:
209
210    go run ./util/pregenerate -check
211
212This is run on CI to ensure the generated files remain up-to-date.
213
214To speed up local iteration, the tool accepts additional arguments to filter the
215files generated. For example, if editing ` + "`aesni-x86_64.pl`" + `, this
216command will only update files with "aesni-x86_64" as a substring.
217
218    go run ./util/pregenerate aesni-x86_64
219
220For convenience, all files in this directory, including this README, are managed
221by the tool. This means the whole directory may be deleted and regenerated from
222scratch at any time.
223`
224