• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2019 The Chromium OS Authors. All rights reserved.
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	"strconv"
16	"strings"
17)
18
19const numWErrorEstimate = 30
20
21func shouldForceDisableWerror(env env, cfg *config, ty compilerType) bool {
22	if cfg.isAndroidWrapper {
23		return cfg.useLlvmNext
24	}
25
26	// We only want this functionality for clang.
27	if ty != clangType {
28		return false
29	}
30	value, _ := env.getenv("FORCE_DISABLE_WERROR")
31	return value != ""
32}
33
34func disableWerrorFlags(originalArgs []string) []string {
35	extraArgs := []string{"-Wno-error"}
36	newArgs := make([]string, 0, len(originalArgs)+numWErrorEstimate)
37	for _, flag := range originalArgs {
38		if strings.HasPrefix(flag, "-Werror=") {
39			extraArgs = append(extraArgs, strings.Replace(flag, "-Werror", "-Wno-error", 1))
40		}
41		if !strings.Contains(flag, "-warnings-as-errors") {
42			newArgs = append(newArgs, flag)
43		}
44	}
45	return append(newArgs, extraArgs...)
46}
47
48func isLikelyAConfTest(cfg *config, cmd *command) bool {
49	// Android doesn't do mid-build `configure`s, so we don't need to worry about this there.
50	if cfg.isAndroidWrapper {
51		return false
52	}
53
54	for _, a := range cmd.Args {
55		// The kernel, for example, will do configure tests with /dev/null as a source file.
56		if a == "/dev/null" || strings.HasPrefix(a, "conftest.c") {
57			return true
58		}
59	}
60	return false
61}
62
63func doubleBuildWithWNoError(env env, cfg *config, originalCmd *command) (exitCode int, err error) {
64	originalStdoutBuffer := &bytes.Buffer{}
65	originalStderrBuffer := &bytes.Buffer{}
66	// TODO: This is a bug in the old wrapper that it drops the ccache path
67	// during double build. Fix this once we don't compare to the old wrapper anymore.
68	if originalCmd.Path == "/usr/bin/ccache" {
69		originalCmd.Path = "ccache"
70	}
71
72	getStdin, err := prebufferStdinIfNeeded(env, originalCmd)
73	if err != nil {
74		return 0, wrapErrorwithSourceLocf(err, "prebuffering stdin: %v", err)
75	}
76
77	var originalExitCode int
78	commitOriginalRusage, err := maybeCaptureRusage(env, originalCmd, func(willLogRusage bool) error {
79		originalExitCode, err = wrapSubprocessErrorWithSourceLoc(originalCmd,
80			env.run(originalCmd, getStdin(), originalStdoutBuffer, originalStderrBuffer))
81		return err
82	})
83	if err != nil {
84		return 0, err
85	}
86
87	// The only way we can do anything useful is if it looks like the failure
88	// was -Werror-related.
89	originalStdoutBufferBytes := originalStdoutBuffer.Bytes()
90	shouldRetry := originalExitCode != 0 &&
91		!isLikelyAConfTest(cfg, originalCmd) &&
92		(bytes.Contains(originalStderrBuffer.Bytes(), []byte("-Werror")) ||
93			bytes.Contains(originalStdoutBufferBytes, []byte("warnings-as-errors")) ||
94			bytes.Contains(originalStdoutBufferBytes, []byte("clang-diagnostic-")))
95	if !shouldRetry {
96		if err := commitOriginalRusage(originalExitCode); err != nil {
97			return 0, fmt.Errorf("commiting rusage: %v", err)
98		}
99		originalStdoutBuffer.WriteTo(env.stdout())
100		originalStderrBuffer.WriteTo(env.stderr())
101		return originalExitCode, nil
102	}
103
104	retryStdoutBuffer := &bytes.Buffer{}
105	retryStderrBuffer := &bytes.Buffer{}
106	retryCommand := &command{
107		Path:       originalCmd.Path,
108		Args:       disableWerrorFlags(originalCmd.Args),
109		EnvUpdates: originalCmd.EnvUpdates,
110	}
111
112	var retryExitCode int
113	commitRetryRusage, err := maybeCaptureRusage(env, retryCommand, func(willLogRusage bool) error {
114		retryExitCode, err = wrapSubprocessErrorWithSourceLoc(retryCommand,
115			env.run(retryCommand, getStdin(), retryStdoutBuffer, retryStderrBuffer))
116		return err
117	})
118	if err != nil {
119		return 0, err
120	}
121
122	// If -Wno-error fixed us, pretend that we never ran without -Wno-error. Otherwise, pretend
123	// that we never ran the second invocation.
124	if retryExitCode != 0 {
125		originalStdoutBuffer.WriteTo(env.stdout())
126		originalStderrBuffer.WriteTo(env.stderr())
127		if err := commitOriginalRusage(originalExitCode); err != nil {
128			return 0, fmt.Errorf("commiting rusage: %v", err)
129		}
130		return originalExitCode, nil
131	}
132
133	if err := commitRetryRusage(retryExitCode); err != nil {
134		return 0, fmt.Errorf("commiting rusage: %v", err)
135	}
136
137	retryStdoutBuffer.WriteTo(env.stdout())
138	retryStderrBuffer.WriteTo(env.stderr())
139
140	lines := []string{}
141	if originalStderrBuffer.Len() > 0 {
142		lines = append(lines, originalStderrBuffer.String())
143	}
144	if originalStdoutBuffer.Len() > 0 {
145		lines = append(lines, originalStdoutBuffer.String())
146	}
147	outputToLog := strings.Join(lines, "\n")
148
149	// Ignore the error here; we can't do anything about it. The result is always valid (though
150	// perhaps incomplete) even if this returns an error.
151	parentProcesses, _ := collectAllParentProcesses()
152	jsonData := warningsJSONData{
153		Cwd:             env.getwd(),
154		Command:         append([]string{originalCmd.Path}, originalCmd.Args...),
155		Stdout:          outputToLog,
156		ParentProcesses: parentProcesses,
157	}
158
159	// Write warning report to stdout for Android.  On Android,
160	// double-build can be requested on remote builds as well, where there
161	// is no canonical place to write the warnings report.
162	if cfg.isAndroidWrapper {
163		stdout := env.stdout()
164		io.WriteString(stdout, "<LLVM_NEXT_ERROR_REPORT>")
165		if err := json.NewEncoder(stdout).Encode(jsonData); err != nil {
166			return 0, wrapErrorwithSourceLocf(err, "error in json.Marshal")
167		}
168		io.WriteString(stdout, "</LLVM_NEXT_ERROR_REPORT>")
169		return retryExitCode, nil
170	}
171
172	// All of the below is basically logging. If we fail at any point, it's
173	// reasonable for that to fail the build. This is all meant for FYI-like
174	// builders in the first place.
175
176	// Buildbots use a nonzero umask, which isn't quite what we want: these directories should
177	// be world-readable and world-writable.
178	oldMask := env.umask(0)
179	defer env.umask(oldMask)
180
181	// Allow root and regular users to write to this without issue.
182	if err := os.MkdirAll(cfg.newWarningsDir, 0777); err != nil {
183		return 0, wrapErrorwithSourceLocf(err, "error creating warnings directory %s", cfg.newWarningsDir)
184	}
185
186	// Have some tag to show that files aren't fully written. It would be sad if
187	// an interrupted build (or out of disk space, or similar) caused tools to
188	// have to be overly-defensive.
189	incompleteSuffix := ".incomplete"
190
191	// Coming up with a consistent name for this is difficult (compiler command's
192	// SHA can clash in the case of identically named files in different
193	// directories, or similar); let's use a random one.
194	tmpFile, err := ioutil.TempFile(cfg.newWarningsDir, "warnings_report*.json"+incompleteSuffix)
195	if err != nil {
196		return 0, wrapErrorwithSourceLocf(err, "error creating warnings file")
197	}
198
199	if err := tmpFile.Chmod(0666); err != nil {
200		return 0, wrapErrorwithSourceLocf(err, "error chmoding the file to be world-readable/writeable")
201	}
202
203	enc := json.NewEncoder(tmpFile)
204	if err := enc.Encode(jsonData); err != nil {
205		_ = tmpFile.Close()
206		return 0, wrapErrorwithSourceLocf(err, "error writing warnings data")
207	}
208
209	if err := tmpFile.Close(); err != nil {
210		return 0, wrapErrorwithSourceLocf(err, "error closing warnings file")
211	}
212
213	if err := os.Rename(tmpFile.Name(), tmpFile.Name()[:len(tmpFile.Name())-len(incompleteSuffix)]); err != nil {
214		return 0, wrapErrorwithSourceLocf(err, "error removing incomplete suffix from warnings file")
215	}
216
217	return retryExitCode, nil
218}
219
220func parseParentPidFromPidStat(pidStatContents string) (parentPid int, ok bool) {
221	// The parent's pid is the fourth field of /proc/[pid]/stat. Sadly, the second field can
222	// have spaces in it. It ends at the last ')' in the contents of /proc/[pid]/stat.
223	lastParen := strings.LastIndex(pidStatContents, ")")
224	if lastParen == -1 {
225		return 0, false
226	}
227
228	thirdFieldAndBeyond := strings.TrimSpace(pidStatContents[lastParen+1:])
229	fields := strings.Fields(thirdFieldAndBeyond)
230	if len(fields) < 2 {
231		return 0, false
232	}
233
234	fourthField := fields[1]
235	parentPid, err := strconv.Atoi(fourthField)
236	if err != nil {
237		return 0, false
238	}
239	return parentPid, true
240}
241
242func collectProcessData(pid int) (args, env []string, parentPid int, err error) {
243	procDir := fmt.Sprintf("/proc/%d", pid)
244
245	readFile := func(fileName string) (string, error) {
246		s, err := ioutil.ReadFile(path.Join(procDir, fileName))
247		if err != nil {
248			return "", fmt.Errorf("reading %s: %v", fileName, err)
249		}
250		return string(s), nil
251	}
252
253	statStr, err := readFile("stat")
254	if err != nil {
255		return nil, nil, 0, err
256	}
257
258	parentPid, ok := parseParentPidFromPidStat(statStr)
259	if !ok {
260		return nil, nil, 0, fmt.Errorf("no parseable parent PID found in %q", statStr)
261	}
262
263	argsStr, err := readFile("cmdline")
264	if err != nil {
265		return nil, nil, 0, err
266	}
267	args = strings.Split(argsStr, "\x00")
268
269	envStr, err := readFile("environ")
270	if err != nil {
271		return nil, nil, 0, err
272	}
273	env = strings.Split(envStr, "\x00")
274	return args, env, parentPid, nil
275}
276
277// The returned []processData is valid even if this returns an error. The error is just the first we
278// encountered when trying to collect parent process data.
279func collectAllParentProcesses() ([]processData, error) {
280	results := []processData{}
281	for parent := os.Getppid(); parent != 1; {
282		args, env, p, err := collectProcessData(parent)
283		if err != nil {
284			return results, fmt.Errorf("inspecting parent %d: %v", parent, err)
285		}
286		results = append(results, processData{Args: args, Env: env})
287		parent = p
288	}
289	return results, nil
290}
291
292type processData struct {
293	Args []string `json:"invocation"`
294	Env  []string `json:"env"`
295}
296
297// Struct used to write JSON. Fields have to be uppercase for the json encoder to read them.
298type warningsJSONData struct {
299	Cwd             string        `json:"cwd"`
300	Command         []string      `json:"command"`
301	Stdout          string        `json:"stdout"`
302	ParentProcesses []processData `json:"parent_process_data"`
303}
304