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