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