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