xref: /aosp_15_r20/external/toolchain-utils/compiler_wrapper/disable_werror_flag.go (revision 760c253c1ed00ce9abd48f8546f08516e57485fe)
1// Copyright 2019 The ChromiumOS Authors
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5package main
6
7import (
8	"bytes"
9	"encoding/json"
10	"fmt"
11	"io"
12	"io/ioutil"
13	"os"
14	"path"
15	"regexp"
16	"strconv"
17	"strings"
18)
19
20const numWErrorEstimate = 30
21
22func getForceDisableWerrorDir(env env, cfg *config) string {
23	return path.Join(getCompilerArtifactsDir(env), "toolchain/fatal_clang_warnings")
24}
25
26type forceDisableWerrorConfig struct {
27	// If reportToStdout is true, we'll write -Werror reports to stdout. Otherwise, they'll be
28	// written to reportDir. If reportDir is empty, it will be determined via
29	// `getForceDisableWerrorDir`.
30	//
31	// Neither of these have specified values if `enabled == false`.
32	reportDir      string
33	reportToStdout bool
34
35	// If true, `-Werror` reporting should be used.
36	enabled bool
37}
38
39func processForceDisableWerrorFlag(env env, cfg *config, builder *commandBuilder) forceDisableWerrorConfig {
40	if cfg.isAndroidWrapper {
41		return forceDisableWerrorConfig{
42			reportToStdout: true,
43			enabled:        cfg.useLlvmNext,
44		}
45	}
46
47	// CrOS supports two modes for enabling this flag:
48	// 1 (preferred). A CFLAG that specifies the directory to write reports to. e.g.,
49	//   `-D_CROSTC_FORCE_DISABLE_WERROR=/path/to/directory`. This flag will be removed from the
50	//   command before the compiler is invoked. If multiple of these are passed, the last one
51	//   wins, but all are removed from the build command.
52	// 2 (dispreferred, but supported). An environment variable, FORCE_DISABLE_WERROR, set to
53	//   any nonempty value. In this case, the wrapper will write to either
54	//   ${CROS_ARTIFACTS_TMP_DIR}/toolchain/fatal_clang_warnings, or to
55	//   /tmp/toolchain/fatal_clang_warnings.
56	//
57	// Two modes are supported because some ebuilds filter the env, while others will filter
58	// CFLAGS. Vanishingly few (none?) filter both, though.
59	const cflagPrefix = "-D_CROSTC_FORCE_DISABLE_WERROR="
60
61	argDir := ""
62	sawArg := false
63	builder.transformArgs(func(arg builderArg) string {
64		value := arg.value
65		if !strings.HasPrefix(value, cflagPrefix) {
66			return value
67		}
68		argDir = value[len(cflagPrefix):]
69		sawArg = true
70		return ""
71	})
72
73	// CrOS only wants this functionality to apply to clang, though flags should also be removed
74	// for GCC.
75	if builder.target.compilerType != clangType {
76		return forceDisableWerrorConfig{enabled: false}
77	}
78
79	if sawArg {
80		return forceDisableWerrorConfig{
81			reportDir: argDir,
82			// Skip this when in src_configure: some build systems ignore CFLAGS
83			// modifications after configure, so this flag must be specified before
84			// src_configure, but we only want the flag to apply to actual builds.
85			enabled: !isInConfigureStage(env),
86		}
87	}
88
89	envValue, _ := env.getenv("FORCE_DISABLE_WERROR")
90	return forceDisableWerrorConfig{enabled: envValue != ""}
91}
92
93func disableWerrorFlags(originalArgs, extraFlags []string) []string {
94	allExtraFlags := append([]string{}, extraFlags...)
95	newArgs := make([]string, 0, len(originalArgs)+numWErrorEstimate)
96	for _, flag := range originalArgs {
97		if strings.HasPrefix(flag, "-Werror=") {
98			allExtraFlags = append(allExtraFlags, strings.Replace(flag, "-Werror", "-Wno-error", 1))
99		}
100		if !strings.Contains(flag, "-warnings-as-errors") {
101			newArgs = append(newArgs, flag)
102		}
103	}
104	return append(newArgs, allExtraFlags...)
105}
106
107func isLikelyAConfTest(cfg *config, cmd *command) bool {
108	// Android doesn't do mid-build `configure`s, so we don't need to worry about this there.
109	if cfg.isAndroidWrapper {
110		return false
111	}
112
113	for _, a := range cmd.Args {
114		// The kernel, for example, will do configure tests with /dev/null as a source file.
115		if a == "/dev/null" || strings.HasPrefix(a, "conftest.c") {
116			return true
117		}
118	}
119	return false
120}
121
122func getWnoErrorFlags(stdout, stderr []byte) []string {
123	needWnoError := false
124	extraFlags := []string{}
125	for _, submatches := range regexp.MustCompile(`error:.* \[(-W[^\]]+)\]`).FindAllSubmatch(stderr, -1) {
126		bracketedMatch := submatches[1]
127
128		// Some warnings are promoted to errors by -Werror. These contain `-Werror` in the
129		// brackets specifying the warning name. A broad, follow-up `-Wno-error` should
130		// disable those.
131		//
132		// _Others_ are implicitly already errors, and will not be disabled by `-Wno-error`.
133		// These do not have `-Wno-error` in their brackets. These need to explicitly have
134		// `-Wno-error=${warning_name}`. See b/325463152 for an example.
135		if bytes.HasPrefix(bracketedMatch, []byte("-Werror,")) || bytes.HasSuffix(bracketedMatch, []byte(",-Werror")) {
136			needWnoError = true
137		} else {
138			// In this case, the entire bracketed match is the warning flag. Trim the
139			// first two chars off to account for the `-W` matched in the regex.
140			warningName := string(bracketedMatch[2:])
141			extraFlags = append(extraFlags, "-Wno-error="+warningName)
142		}
143	}
144	needWnoError = needWnoError || bytes.Contains(stdout, []byte("warnings-as-errors")) || bytes.Contains(stdout, []byte("clang-diagnostic-"))
145
146	if len(extraFlags) == 0 && !needWnoError {
147		return nil
148	}
149	return append(extraFlags, "-Wno-error")
150}
151
152func doubleBuildWithWNoError(env env, cfg *config, originalCmd *command, werrorConfig forceDisableWerrorConfig) (exitCode int, err error) {
153	originalStdoutBuffer := &bytes.Buffer{}
154	originalStderrBuffer := &bytes.Buffer{}
155	// TODO: This is a bug in the old wrapper that it drops the ccache path
156	// during double build. Fix this once we don't compare to the old wrapper anymore.
157	if originalCmd.Path == "/usr/bin/ccache" {
158		originalCmd.Path = "ccache"
159	}
160
161	getStdin, err := prebufferStdinIfNeeded(env, originalCmd)
162	if err != nil {
163		return 0, wrapErrorwithSourceLocf(err, "prebuffering stdin: %v", err)
164	}
165
166	var originalExitCode int
167	commitOriginalRusage, err := maybeCaptureRusage(env, originalCmd, func(willLogRusage bool) error {
168		originalExitCode, err = wrapSubprocessErrorWithSourceLoc(originalCmd,
169			env.run(originalCmd, getStdin(), originalStdoutBuffer, originalStderrBuffer))
170		return err
171	})
172	if err != nil {
173		return 0, err
174	}
175
176	// The only way we can do anything useful is if it looks like the failure
177	// was -Werror-related.
178	retryWithExtraFlags := []string{}
179	if originalExitCode != 0 && !isLikelyAConfTest(cfg, originalCmd) {
180		retryWithExtraFlags = getWnoErrorFlags(originalStdoutBuffer.Bytes(), originalStderrBuffer.Bytes())
181	}
182	if len(retryWithExtraFlags) == 0 {
183		if err := commitOriginalRusage(originalExitCode); err != nil {
184			return 0, fmt.Errorf("commiting rusage: %v", err)
185		}
186		originalStdoutBuffer.WriteTo(env.stdout())
187		originalStderrBuffer.WriteTo(env.stderr())
188		return originalExitCode, nil
189	}
190
191	retryStdoutBuffer := &bytes.Buffer{}
192	retryStderrBuffer := &bytes.Buffer{}
193	retryCommand := &command{
194		Path:       originalCmd.Path,
195		Args:       disableWerrorFlags(originalCmd.Args, retryWithExtraFlags),
196		EnvUpdates: originalCmd.EnvUpdates,
197	}
198
199	var retryExitCode int
200	commitRetryRusage, err := maybeCaptureRusage(env, retryCommand, func(willLogRusage bool) error {
201		retryExitCode, err = wrapSubprocessErrorWithSourceLoc(retryCommand,
202			env.run(retryCommand, getStdin(), retryStdoutBuffer, retryStderrBuffer))
203		return err
204	})
205	if err != nil {
206		return 0, err
207	}
208
209	// If -Wno-error fixed us, pretend that we never ran without -Wno-error. Otherwise, pretend
210	// that we never ran the second invocation.
211	if retryExitCode != 0 {
212		originalStdoutBuffer.WriteTo(env.stdout())
213		originalStderrBuffer.WriteTo(env.stderr())
214		if err := commitOriginalRusage(originalExitCode); err != nil {
215			return 0, fmt.Errorf("commiting rusage: %v", err)
216		}
217		return originalExitCode, nil
218	}
219
220	if err := commitRetryRusage(retryExitCode); err != nil {
221		return 0, fmt.Errorf("commiting rusage: %v", err)
222	}
223
224	retryStdoutBuffer.WriteTo(env.stdout())
225	retryStderrBuffer.WriteTo(env.stderr())
226
227	lines := []string{}
228	if originalStderrBuffer.Len() > 0 {
229		lines = append(lines, originalStderrBuffer.String())
230	}
231	if originalStdoutBuffer.Len() > 0 {
232		lines = append(lines, originalStdoutBuffer.String())
233	}
234	outputToLog := strings.Join(lines, "\n")
235
236	// Ignore the error here; we can't do anything about it. The result is always valid (though
237	// perhaps incomplete) even if this returns an error.
238	parentProcesses, _ := collectAllParentProcesses()
239	jsonData := warningsJSONData{
240		Cwd:             env.getwd(),
241		Command:         append([]string{originalCmd.Path}, originalCmd.Args...),
242		Stdout:          outputToLog,
243		ParentProcesses: parentProcesses,
244	}
245
246	// Write warning report to stdout for Android.  On Android,
247	// double-build can be requested on remote builds as well, where there
248	// is no canonical place to write the warnings report.
249	if werrorConfig.reportToStdout {
250		stdout := env.stdout()
251		io.WriteString(stdout, "<LLVM_NEXT_ERROR_REPORT>")
252		if err := json.NewEncoder(stdout).Encode(jsonData); err != nil {
253			return 0, wrapErrorwithSourceLocf(err, "error in json.Marshal")
254		}
255		io.WriteString(stdout, "</LLVM_NEXT_ERROR_REPORT>")
256		return retryExitCode, nil
257	}
258
259	// All of the below is basically logging. If we fail at any point, it's
260	// reasonable for that to fail the build. This is all meant for FYI-like
261	// builders in the first place.
262
263	// Buildbots use a nonzero umask, which isn't quite what we want: these directories should
264	// be world-readable and world-writable.
265	oldMask := env.umask(0)
266	defer env.umask(oldMask)
267
268	reportDir := werrorConfig.reportDir
269	if reportDir == "" {
270		reportDir = getForceDisableWerrorDir(env, cfg)
271	}
272
273	// Allow root and regular users to write to this without issue.
274	if err := os.MkdirAll(reportDir, 0777); err != nil {
275		return 0, wrapErrorwithSourceLocf(err, "error creating warnings directory %s", reportDir)
276	}
277
278	// Have some tag to show that files aren't fully written. It would be sad if
279	// an interrupted build (or out of disk space, or similar) caused tools to
280	// have to be overly-defensive.
281	const incompleteSuffix = ".incomplete"
282
283	// Coming up with a consistent name for this is difficult (compiler command's
284	// SHA can clash in the case of identically named files in different
285	// directories, or similar); let's use a random one.
286	tmpFile, err := ioutil.TempFile(reportDir, "warnings_report*.json"+incompleteSuffix)
287	if err != nil {
288		return 0, wrapErrorwithSourceLocf(err, "error creating warnings file")
289	}
290
291	if err := tmpFile.Chmod(0666); err != nil {
292		return 0, wrapErrorwithSourceLocf(err, "error chmoding the file to be world-readable/writeable")
293	}
294
295	enc := json.NewEncoder(tmpFile)
296	if err := enc.Encode(jsonData); err != nil {
297		_ = tmpFile.Close()
298		return 0, wrapErrorwithSourceLocf(err, "error writing warnings data")
299	}
300
301	if err := tmpFile.Close(); err != nil {
302		return 0, wrapErrorwithSourceLocf(err, "error closing warnings file")
303	}
304
305	if err := os.Rename(tmpFile.Name(), tmpFile.Name()[:len(tmpFile.Name())-len(incompleteSuffix)]); err != nil {
306		return 0, wrapErrorwithSourceLocf(err, "error removing incomplete suffix from warnings file")
307	}
308
309	return retryExitCode, nil
310}
311
312func parseParentPidFromPidStat(pidStatContents string) (parentPid int, ok bool) {
313	// The parent's pid is the fourth field of /proc/[pid]/stat. Sadly, the second field can
314	// have spaces in it. It ends at the last ')' in the contents of /proc/[pid]/stat.
315	lastParen := strings.LastIndex(pidStatContents, ")")
316	if lastParen == -1 {
317		return 0, false
318	}
319
320	thirdFieldAndBeyond := strings.TrimSpace(pidStatContents[lastParen+1:])
321	fields := strings.Fields(thirdFieldAndBeyond)
322	if len(fields) < 2 {
323		return 0, false
324	}
325
326	fourthField := fields[1]
327	parentPid, err := strconv.Atoi(fourthField)
328	if err != nil {
329		return 0, false
330	}
331	return parentPid, true
332}
333
334func collectProcessData(pid int) (args, env []string, parentPid int, err error) {
335	procDir := fmt.Sprintf("/proc/%d", pid)
336
337	readFile := func(fileName string) (string, error) {
338		s, err := ioutil.ReadFile(path.Join(procDir, fileName))
339		if err != nil {
340			return "", fmt.Errorf("reading %s: %v", fileName, err)
341		}
342		return string(s), nil
343	}
344
345	statStr, err := readFile("stat")
346	if err != nil {
347		return nil, nil, 0, err
348	}
349
350	parentPid, ok := parseParentPidFromPidStat(statStr)
351	if !ok {
352		return nil, nil, 0, fmt.Errorf("no parseable parent PID found in %q", statStr)
353	}
354
355	argsStr, err := readFile("cmdline")
356	if err != nil {
357		return nil, nil, 0, err
358	}
359	args = strings.Split(argsStr, "\x00")
360
361	envStr, err := readFile("environ")
362	if err != nil {
363		return nil, nil, 0, err
364	}
365	env = strings.Split(envStr, "\x00")
366	return args, env, parentPid, nil
367}
368
369// The returned []processData is valid even if this returns an error. The error is just the first we
370// encountered when trying to collect parent process data.
371func collectAllParentProcesses() ([]processData, error) {
372	results := []processData{}
373	for parent := os.Getppid(); parent != 1; {
374		args, env, p, err := collectProcessData(parent)
375		if err != nil {
376			return results, fmt.Errorf("inspecting parent %d: %v", parent, err)
377		}
378		results = append(results, processData{Args: args, Env: env})
379		parent = p
380	}
381	return results, nil
382}
383
384type processData struct {
385	Args []string `json:"invocation"`
386	Env  []string `json:"env"`
387}
388
389// Struct used to write JSON. Fields have to be uppercase for the json encoder to read them.
390type warningsJSONData struct {
391	Cwd             string        `json:"cwd"`
392	Command         []string      `json:"command"`
393	Stdout          string        `json:"stdout"`
394	ParentProcesses []processData `json:"parent_process_data"`
395}
396