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