github.phpd.cn/thought-machine/please@v12.2.0+incompatible/src/output/shell_output.go (about) 1 // Package for displaying output on the command line of the current build state. 2 3 package output 4 5 import ( 6 "bufio" 7 "encoding/hex" 8 "fmt" 9 "math/rand" 10 "os" 11 "os/exec" 12 "path" 13 "path/filepath" 14 "regexp" 15 "strings" 16 "sync" 17 "time" 18 19 "gopkg.in/op/go-logging.v1" 20 21 "build" 22 "cli" 23 "core" 24 "test" 25 ) 26 27 var log = logging.MustGetLogger("output") 28 29 // durationGranularity is the granularity that we build durations at. 30 const durationGranularity = 10 * time.Millisecond 31 const testDurationGranularity = time.Millisecond 32 33 // SetColouredOutput forces on or off coloured output in logging and other console output. 34 func SetColouredOutput(on bool) { 35 cli.StdErrIsATerminal = on 36 } 37 38 // Used to track currently building targets. 39 type buildingTarget struct { 40 sync.Mutex 41 buildingTargetData 42 } 43 44 type buildingTargetData struct { 45 Label core.BuildLabel 46 Started time.Time 47 Finished time.Time 48 Description string 49 Active bool 50 Failed bool 51 Cached bool 52 Err error 53 Colour string 54 Target *core.BuildTarget 55 LastProgress float32 56 Eta time.Duration 57 } 58 59 // MonitorState monitors the build while it's running (essentially until state.Results is closed) 60 // and prints output while it's happening. 61 func MonitorState(state *core.BuildState, numThreads int, plainOutput, keepGoing, shouldBuild, shouldTest, shouldRun, showStatus bool, traceFile string) bool { 62 failedTargetMap := map[core.BuildLabel]error{} 63 buildingTargets := make([]buildingTarget, numThreads) 64 65 if len(state.Config.Please.Motd) != 0 { 66 printf("%s\n", state.Config.Please.Motd[rand.Intn(len(state.Config.Please.Motd))]) 67 } 68 69 displayDone := make(chan struct{}) 70 stop := make(chan struct{}) 71 if plainOutput { 72 go logProgress(state, &buildingTargets, stop, displayDone) 73 } else { 74 go display(state, &buildingTargets, stop, displayDone) 75 } 76 aggregatedResults := core.TestResults{} 77 failedTargets := []core.BuildLabel{} 78 failedNonTests := []core.BuildLabel{} 79 for result := range state.Results { 80 if state.DebugTests && result.Status == core.TargetTesting { 81 stop <- struct{}{} 82 <-displayDone 83 // Ensure that this works again later and we don't deadlock 84 // TODO(peterebden): this does not seem like a gloriously elegant synchronisation mechanism... 85 go func() { 86 <-stop 87 displayDone <- struct{}{} 88 }() 89 } 90 processResult(state, result, buildingTargets, &aggregatedResults, plainOutput, keepGoing, &failedTargets, &failedNonTests, failedTargetMap, traceFile != "") 91 } 92 stop <- struct{}{} 93 <-displayDone 94 if traceFile != "" { 95 writeTrace(traceFile) 96 } 97 duration := time.Since(state.StartTime).Round(durationGranularity) 98 if len(failedNonTests) > 0 { // Something failed in the build step. 99 if state.Verbosity > 0 { 100 printFailedBuildResults(failedNonTests, failedTargetMap, duration) 101 } 102 // Die immediately and unsuccessfully, this avoids awkward interactions with 103 // --failing_tests_ok later on. 104 os.Exit(-1) 105 } 106 // Check all the targets we wanted to build actually have been built. 107 for _, label := range state.ExpandOriginalTargets() { 108 if target := state.Graph.Target(label); target == nil { 109 log.Fatalf("Target %s doesn't exist in build graph", label) 110 } else if (state.NeedHashesOnly || state.PrepareOnly) && target.State() == core.Stopped { 111 // Do nothing, we will output about this shortly. 112 } else if shouldBuild && target != nil && target.State() < core.Built && len(failedTargetMap) == 0 && !target.AddedPostBuild { 113 // N.B. Currently targets that are added post-build are excluded here, because in some legit cases this 114 // check can fail incorrectly. It'd be better to verify this more precisely though. 115 cycle := graphCycleMessage(state.Graph, target) 116 log.Fatalf("Target %s hasn't built but we have no pending tasks left.\n%s", label, cycle) 117 } 118 } 119 if state.Verbosity > 0 && shouldBuild { 120 if shouldTest { // Got to the test phase, report their results. 121 printTestResults(state, aggregatedResults, failedTargets, duration) 122 } else if state.NeedHashesOnly { 123 printHashes(state, duration) 124 } else if state.PrepareOnly { 125 printTempDirs(state, duration) 126 } else if !shouldRun { // Must be plz build or similar, report build outputs. 127 printBuildResults(state, duration, showStatus) 128 } 129 } 130 return len(failedTargetMap) == 0 131 } 132 133 // PrintConnectionMessage prints the message when we're initially connected to a remote server. 134 func PrintConnectionMessage(url string, targets []core.BuildLabel, tests, coverage bool) { 135 printf("${WHITE}Connection established to remote plz server at ${BOLD_WHITE}%s${RESET}.\n", url) 136 printf("${WHITE}It's building the following %s: ", pluralise(len(targets), "target", "targets")) 137 for i, t := range targets { 138 if i > 5 { 139 printf("${BOLD_WHITE}...${RESET}") 140 break 141 } else { 142 if i > 0 { 143 printf(", ") 144 } 145 printf("${BOLD_WHITE}%s${RESET}", t) 146 } 147 } 148 printf("\n${WHITE}Running tests: ${BOLD_WHITE}%s${RESET}\n", yesNo(tests)) 149 printf("${WHITE}Coverage: ${BOLD_WHITE}%s${RESET}\n", yesNo(coverage)) 150 printf("${BOLD_WHITE}Ctrl+C${RESET}${WHITE} to disconnect from it; that will ${BOLD_WHITE}not${RESET}${WHITE} stop the remote build.${RESET}\n") 151 } 152 153 // PrintDisconnectionMessage prints the message when we're disconnected from the remote server. 154 func PrintDisconnectionMessage(success, closed, disconnected bool) { 155 printf("${BOLD_WHITE}Disconnected from remote plz server.\nStatus: ") 156 if disconnected { 157 printf("${BOLD_YELLOW}Disconnected${RESET}\n") 158 } else if !closed { 159 printf("${BOLD_MAGENTA}Unknown${RESET}\n") 160 } else if success { 161 printf("${BOLD_GREEN}Success${RESET}\n") 162 } else { 163 printf("${BOLD_RED}Failure${RESET}\n") 164 } 165 } 166 167 func yesNo(b bool) string { 168 if b { 169 return "yes" 170 } 171 return "no" 172 } 173 174 func processResult(state *core.BuildState, result *core.BuildResult, buildingTargets []buildingTarget, aggregatedResults *core.TestResults, plainOutput bool, 175 keepGoing bool, failedTargets, failedNonTests *[]core.BuildLabel, failedTargetMap map[core.BuildLabel]error, shouldTrace bool) { 176 label := result.Label 177 active := result.Status == core.PackageParsing || result.Status == core.TargetBuilding || result.Status == core.TargetTesting 178 failed := result.Status == core.ParseFailed || result.Status == core.TargetBuildFailed || result.Status == core.TargetTestFailed 179 cached := result.Status == core.TargetCached || result.Tests.Cached 180 stopped := result.Status == core.TargetBuildStopped 181 parse := result.Status == core.PackageParsing || result.Status == core.PackageParsed || result.Status == core.ParseFailed 182 if shouldTrace { 183 addTrace(result, buildingTargets[result.ThreadID].Label, active) 184 } 185 if failed && result.Tests.NumTests == 0 && result.Tests.Failed == 0 { 186 result.Tests.NumTests = 1 187 result.Tests.Failed = 1 // Ensure there's one test failure when there're no results to parse. 188 } 189 // Only aggregate test results the first time it finishes. 190 if buildingTargets[result.ThreadID].Active && !active { 191 aggregatedResults.Aggregate(&result.Tests) 192 } 193 target := state.Graph.Target(label) 194 if !parse { // Parse tasks happen on a different set of threads. 195 updateTarget(state, plainOutput, &buildingTargets[result.ThreadID], label, active, failed, cached, result.Description, result.Err, targetColour(target), target) 196 } 197 if failed { 198 failedTargetMap[label] = result.Err 199 // Don't stop here after test failure, aggregate them for later. 200 if !keepGoing && result.Status != core.TargetTestFailed { 201 // Reset colour so the entire compiler error output doesn't appear red. 202 log.Errorf("%s failed:${RESET}\n%s", result.Label, shortError(result.Err)) 203 state.KillAll() 204 } else if !plainOutput { // plain output will have already logged this 205 log.Errorf("%s failed: %s", result.Label, shortError(result.Err)) 206 } 207 *failedTargets = append(*failedTargets, label) 208 if result.Status != core.TargetTestFailed { 209 *failedNonTests = append(*failedNonTests, label) 210 } 211 } else if stopped { 212 failedTargetMap[result.Label] = nil 213 } else if plainOutput && state.ShowTestOutput && result.Status == core.TargetTested && target != nil { 214 // If using interactive output we'll print it afterwards. 215 printf("Finished test %s:\n%s\n", label, target.Results.Output) 216 } 217 } 218 219 func printTestResults(state *core.BuildState, aggregatedResults core.TestResults, failedTargets []core.BuildLabel, duration time.Duration) { 220 if len(failedTargets) > 0 { 221 for _, failed := range failedTargets { 222 target := state.Graph.TargetOrDie(failed) 223 if len(target.Results.Failures) == 0 { 224 if target.Results.TimedOut { 225 printf("${WHITE_ON_RED}Fail:${RED_NO_BG} %s ${WHITE_ON_RED}Timed out${RESET}\n", target.Label) 226 } else { 227 printf("${WHITE_ON_RED}Fail:${RED_NO_BG} %s ${WHITE_ON_RED}Failed to run test${RESET}\n", target.Label) 228 } 229 } else { 230 printf("${WHITE_ON_RED}Fail:${RED_NO_BG} %s ${BOLD_GREEN}%3d passed ${BOLD_YELLOW}%3d skipped ${BOLD_RED}%3d failed ${BOLD_WHITE}Took %s${RESET}\n", 231 target.Label, target.Results.Passed, target.Results.Skipped, target.Results.Failed, target.Results.Duration.Round(durationGranularity)) 232 for _, failure := range target.Results.Failures { 233 printf("${BOLD_RED}Failure: %s in %s${RESET}\n", failure.Type, failure.Name) 234 printf("%s\n", failure.Traceback) 235 if len(failure.Stdout) > 0 { 236 printf("${BOLD_RED}Standard output:${RESET}\n%s\n", failure.Stdout) 237 } 238 if len(failure.Stderr) > 0 { 239 printf("${BOLD_RED}Standard error:${RESET}\n%s\n", failure.Stderr) 240 } 241 } 242 } 243 if len(target.Results.Output) > 0 { 244 printf("${BOLD_RED}Full output:${RESET}\n%s\n", target.Results.Output) 245 } 246 if target.Results.Flakes > 0 { 247 printf("${BOLD_MAGENTA}Flaky target; made %s before giving up${RESET}\n", pluralise(target.Results.Flakes, "attempt", "attempts")) 248 } 249 } 250 } 251 // Print individual test results 252 i := 0 253 for _, target := range state.Graph.AllTargets() { 254 if target.IsTest && target.Results.NumTests > 0 { 255 if target.Results.Failed > 0 { 256 printf("${RED}%s${RESET} %s\n", target.Label, testResultMessage(target.Results, failedTargets)) 257 } else { 258 printf("${GREEN}%s${RESET} %s\n", target.Label, testResultMessage(target.Results, failedTargets)) 259 } 260 if state.ShowTestOutput && target.Results.Output != "" { 261 printf("Test output:\n%s\n", target.Results.Output) 262 } 263 i++ 264 } 265 } 266 aggregatedResults.Duration = -100 * time.Millisecond // Exclude this from being displayed later. 267 printf(fmt.Sprintf("${BOLD_WHITE}%s and %s${BOLD_WHITE}. Total time %s.${RESET}\n", 268 pluralise(i, "test target", "test targets"), testResultMessage(aggregatedResults, failedTargets), duration)) 269 } 270 271 // logProgress continually logs progress messages every 10s explaining where we're up to. 272 func logProgress(state *core.BuildState, buildingTargets *[]buildingTarget, stop <-chan struct{}, done chan<- struct{}) { 273 t := time.NewTicker(10 * time.Second) 274 defer t.Stop() 275 for { 276 select { 277 case <-t.C: 278 busy := 0 279 for i := 0; i < len(*buildingTargets); i++ { 280 if (*buildingTargets)[i].Active { 281 busy++ 282 } 283 } 284 log.Notice("Build running for %s, %d / %d tasks done, %s busy", time.Since(state.StartTime).Round(time.Second), state.NumDone(), state.NumActive(), pluralise(busy, "worker", "workers")) 285 case <-stop: 286 done <- struct{}{} 287 return 288 } 289 } 290 } 291 292 // Produces a string describing the results of one test (or a single aggregation). 293 func testResultMessage(results core.TestResults, failedTargets []core.BuildLabel) string { 294 if results.NumTests == 0 { 295 if len(failedTargets) > 0 { 296 return "Tests failed" 297 } 298 return "No tests found" 299 } 300 msg := fmt.Sprintf("%s run", pluralise(results.NumTests, "test", "tests")) 301 if results.Duration >= 0.0 { 302 msg += fmt.Sprintf(" in %s", results.Duration.Round(testDurationGranularity)) 303 } 304 msg += fmt.Sprintf("; ${BOLD_GREEN}%d passed${RESET}", results.Passed) 305 if results.Failed > 0 { 306 msg += fmt.Sprintf(", ${BOLD_RED}%d failed${RESET}", results.Failed) 307 } 308 if results.Skipped > 0 { 309 msg += fmt.Sprintf(", ${BOLD_YELLOW}%d skipped${RESET}", results.Skipped) 310 } 311 if results.Flakes > 0 { 312 msg += fmt.Sprintf(", ${BOLD_MAGENTA}%s${RESET}", pluralise(results.Flakes, "flake", "flakes")) 313 } 314 if results.Cached { 315 msg += " ${GREEN}[cached]${RESET}" 316 } 317 return msg 318 } 319 320 func printBuildResults(state *core.BuildState, duration time.Duration, showStatus bool) { 321 // Count incrementality. 322 totalBuilt := 0 323 totalReused := 0 324 for _, target := range state.Graph.AllTargets() { 325 if target.State() == core.Built { 326 totalBuilt++ 327 } else if target.State() == core.Reused { 328 totalReused++ 329 } 330 } 331 incrementality := 100.0 * float64(totalReused) / float64(totalBuilt+totalReused) 332 if totalBuilt+totalReused == 0 { 333 incrementality = 100 // avoid NaN 334 } 335 // Print this stuff so we always see it. 336 printf("Build finished; total time %s, incrementality %.1f%%. Outputs:\n", duration, incrementality) 337 for _, label := range state.ExpandVisibleOriginalTargets() { 338 target := state.Graph.TargetOrDie(label) 339 if showStatus { 340 fmt.Printf("%s [%s]:\n", label, target.State()) 341 } else { 342 fmt.Printf("%s:\n", label) 343 } 344 for _, result := range buildResult(target) { 345 fmt.Printf(" %s\n", result) 346 } 347 } 348 } 349 350 func printHashes(state *core.BuildState, duration time.Duration) { 351 fmt.Printf("Hashes calculated, total time %s:\n", duration) 352 for _, label := range state.ExpandVisibleOriginalTargets() { 353 hash, err := build.OutputHash(state.Graph.TargetOrDie(label)) 354 if err != nil { 355 fmt.Printf(" %s: cannot calculate: %s\n", label, err) 356 } else { 357 fmt.Printf(" %s: %s\n", label, hex.EncodeToString(hash)) 358 } 359 } 360 } 361 362 func printTempDirs(state *core.BuildState, duration time.Duration) { 363 fmt.Printf("Temp directories prepared, total time %s:\n", duration) 364 for _, label := range state.ExpandVisibleOriginalTargets() { 365 target := state.Graph.TargetOrDie(label) 366 cmd := build.ReplaceSequences(state, target, target.GetCommand(state)) 367 env := core.BuildEnvironment(state, target, false) 368 fmt.Printf(" %s: %s\n", label, target.TmpDir()) 369 fmt.Printf(" Command: %s\n", cmd) 370 if !state.PrepareShell { 371 // This isn't very useful if we're opening a shell (since then the vars will be set anyway) 372 fmt.Printf(" Expanded: %s\n", os.Expand(cmd, env.ReplaceEnvironment)) 373 } else { 374 fmt.Printf("\n") 375 cmd := exec.Command("bash", "--noprofile", "--norc", "-o", "pipefail") // plz requires bash, some commands contain bashisms. 376 cmd.Dir = target.TmpDir() 377 cmd.Env = env 378 cmd.Stdin = os.Stdin 379 cmd.Stdout = os.Stdout 380 cmd.Stderr = os.Stderr 381 cmd.Run() // Ignore errors, it will typically end by the user killing it somehow. 382 } 383 } 384 } 385 386 func buildResult(target *core.BuildTarget) []string { 387 results := []string{} 388 if target != nil { 389 for _, out := range target.Outputs() { 390 if core.StartedAtRepoRoot() { 391 results = append(results, path.Join(target.OutDir(), out)) 392 } else { 393 results = append(results, path.Join(core.RepoRoot, target.OutDir(), out)) 394 } 395 } 396 } 397 return results 398 } 399 400 func printFailedBuildResults(failedTargets []core.BuildLabel, failedTargetMap map[core.BuildLabel]error, duration time.Duration) { 401 printf("${WHITE_ON_RED}Build stopped after %s. %s failed:${RESET}\n", duration, pluralise(len(failedTargetMap), "target", "targets")) 402 for _, label := range failedTargets { 403 err := failedTargetMap[label] 404 if err != nil { 405 printf(" ${BOLD_RED}%s\n${RESET}%s${RESET}\n", label, colouriseError(err)) 406 } else { 407 printf(" ${BOLD_RED}%s${RESET}\n", label) 408 } 409 } 410 } 411 412 func updateTarget(state *core.BuildState, plainOutput bool, buildingTarget *buildingTarget, label core.BuildLabel, 413 active bool, failed bool, cached bool, description string, err error, colour string, target *core.BuildTarget) { 414 updateTarget2(buildingTarget, label, active, failed, cached, description, err, colour, target) 415 if plainOutput { 416 if failed { 417 log.Errorf("%s: %s: %s", label.String(), description, shortError(err)) 418 } else { 419 if !active { 420 active := pluralise(state.NumActive(), "task", "tasks") 421 log.Info("[%d/%s] %s: %s [%3.1fs]", state.NumDone(), active, label.String(), description, time.Since(buildingTarget.Started).Seconds()) 422 } else { 423 log.Info("%s: %s", label.String(), description) 424 } 425 } 426 } 427 } 428 429 func updateTarget2(target *buildingTarget, label core.BuildLabel, active bool, failed bool, cached bool, description string, err error, colour string, t *core.BuildTarget) { 430 target.Lock() 431 defer target.Unlock() 432 target.Label = label 433 target.Description = description 434 if !target.Active { 435 // Starting to build now. 436 target.Started = time.Now() 437 target.Finished = target.Started 438 } else if !active { 439 // finished building 440 target.Finished = time.Now() 441 } 442 target.Active = active 443 target.Failed = failed 444 target.Cached = cached 445 target.Err = err 446 target.Colour = colour 447 target.Target = t 448 } 449 450 func targetColour(target *core.BuildTarget) string { 451 if target == nil { 452 return "${BOLD_CYAN}" // unknown 453 } else if target.IsBinary { 454 return "${BOLD}" + targetColour2(target) 455 } else { 456 return targetColour2(target) 457 } 458 } 459 460 func targetColour2(target *core.BuildTarget) string { 461 // Quick heuristic on language types. May want to make this configurable. 462 for _, require := range target.Requires { 463 if require == "py" { 464 return "${GREEN}" 465 } else if require == "java" { 466 return "${RED}" 467 } else if require == "go" { 468 return "${YELLOW}" 469 } else if require == "js" { 470 return "${BLUE}" 471 } 472 } 473 if strings.Contains(target.Label.PackageName, "third_party") { 474 return "${MAGENTA}" 475 } 476 return "${WHITE}" 477 } 478 479 // Since this is a gentleman's build tool, we'll make an effort to get plurals correct 480 // in at least this one place. 481 func pluralise(num int, singular, plural string) string { 482 if num == 1 { 483 return fmt.Sprintf("1 %s", singular) 484 } 485 return fmt.Sprintf("%d %s", num, plural) 486 } 487 488 // PrintCoverage writes out coverage metrics after a test run in a file tree setup. 489 // Only files that were covered by tests and not excluded are shown. 490 func PrintCoverage(state *core.BuildState, includeFiles []string) { 491 printf("${BOLD_WHITE}Coverage results:${RESET}\n") 492 totalCovered := 0 493 totalTotal := 0 494 lastDir := "_" 495 for _, file := range state.Coverage.OrderedFiles() { 496 if !shouldInclude(file, includeFiles) { 497 continue 498 } 499 dir := filepath.Dir(file) 500 if dir != lastDir { 501 printf("${WHITE}%s:${RESET}\n", strings.TrimRight(dir, "/")) 502 } 503 lastDir = dir 504 covered, total := test.CountCoverage(state.Coverage.Files[file]) 505 printf(" %s\n", coveragePercentage(covered, total, file[len(dir)+1:])) 506 totalCovered += covered 507 totalTotal += total 508 } 509 printf("${BOLD_WHITE}Total coverage: %s${RESET}\n", coveragePercentage(totalCovered, totalTotal, "")) 510 } 511 512 // PrintLineCoverageReport writes out line-by-line coverage metrics after a test run. 513 func PrintLineCoverageReport(state *core.BuildState, includeFiles []string) { 514 coverageColours := map[core.LineCoverage]string{ 515 core.NotExecutable: "${GREY}", 516 core.Unreachable: "${YELLOW}", 517 core.Uncovered: "${RED}", 518 core.Covered: "${GREEN}", 519 } 520 521 printf("${BOLD_WHITE}Covered files:${RESET}\n") 522 for _, file := range state.Coverage.OrderedFiles() { 523 if !shouldInclude(file, includeFiles) { 524 continue 525 } 526 coverage := state.Coverage.Files[file] 527 covered, total := test.CountCoverage(coverage) 528 printf("${BOLD_WHITE}%s: %s${RESET}\n", file, coveragePercentage(covered, total, "")) 529 f, err := os.Open(file) 530 if err != nil { 531 printf("${BOLD_RED}Can't open: %s${RESET}\n", err) 532 continue 533 } 534 defer f.Close() 535 scanner := bufio.NewScanner(f) 536 i := 0 537 for scanner.Scan() { 538 if i < len(coverage) { 539 printf("${WHITE}%4d %s%s\n", i, coverageColours[coverage[i]], scanner.Text()) 540 } else { 541 // Assume the lines are not executable. This happens for python, for example. 542 printf("${WHITE}%4d ${GREY}%s\n", i, scanner.Text()) 543 } 544 i++ 545 } 546 printf("${RESET}\n") 547 } 548 } 549 550 // shouldInclude returns true if we should include a file in the coverage display. 551 func shouldInclude(file string, files []string) bool { 552 if len(files) == 0 { 553 return true 554 } 555 for _, f := range files { 556 if file == f { 557 return true 558 } 559 } 560 return false 561 } 562 563 // Returns some appropriate ANSI colour code for a coverage percentage. 564 func coverageColour(percentage float32) string { 565 // TODO(pebers): consider making these configurable? 566 if percentage < 20.0 { 567 return "${MAGENTA}" 568 } else if percentage < 60.0 { 569 return "${BOLD_RED}" 570 } else if percentage < 80.0 { 571 return "${BOLD_YELLOW}" 572 } 573 return "${BOLD_GREEN}" 574 } 575 576 func coveragePercentage(covered, total int, label string) string { 577 if total == 0 { 578 return fmt.Sprintf("${BOLD_MAGENTA}%s No data${RESET}", label) 579 } 580 percentage := 100.0 * float32(covered) / float32(total) 581 return fmt.Sprintf("%s%s %d/%s, %2.1f%%${RESET}", coverageColour(percentage), label, covered, pluralise(total, "line", "lines"), percentage) 582 } 583 584 // colouriseError adds a splash of colour to a compiler error message. 585 // This is a similar effect to -fcolor-diagnostics in Clang, but we attempt to apply it fairly generically. 586 func colouriseError(err error) error { 587 msg := []string{} 588 for _, line := range strings.Split(err.Error(), "\n") { 589 if groups := errorMessageRe.FindStringSubmatch(line); groups != nil { 590 if groups[3] != "" { 591 groups[3] = ", column " + groups[3] 592 } 593 if groups[4] != "" { 594 groups[4] += ": " 595 } 596 msg = append(msg, fmt.Sprintf("${BOLD_WHITE}%s, line %s%s:${RESET} ${BOLD_RED}%s${RESET}${BOLD_WHITE}%s${RESET}", groups[1], groups[2], groups[3], groups[4], groups[5])) 597 } else { 598 msg = append(msg, line) 599 } 600 } 601 return fmt.Errorf("%s", strings.Join(msg, "\n")) 602 } 603 604 // errorMessageRe is a regex to find lines that look like they're specifying a file. 605 var errorMessageRe = regexp.MustCompile(`^([^ ]+\.[^: /]+):([0-9]+):(?:([0-9]+):)? *(?:([a-z-_ ]+):)? (.*)$`) 606 607 // graphCycleMessage attempts to detect graph cycles and produces a readable message from it. 608 func graphCycleMessage(graph *core.BuildGraph, target *core.BuildTarget) string { 609 if cycle := findGraphCycle(graph, target); len(cycle) > 0 { 610 msg := "Dependency cycle found:\n" 611 msg += fmt.Sprintf(" %s\n", cycle[len(cycle)-1].Label) 612 for i := len(cycle) - 2; i >= 0; i-- { 613 msg += fmt.Sprintf(" -> %s\n", cycle[i].Label) 614 } 615 msg += fmt.Sprintf(" -> %s\n", cycle[len(cycle)-1].Label) 616 return msg + fmt.Sprintf("Sorry, but you'll have to refactor your build files to avoid this cycle.") 617 } 618 return unbuiltTargetsMessage(graph) 619 } 620 621 // Attempts to detect cycles in the build graph. Returns an empty slice if none is found, 622 // otherwise returns a slice of labels describing the cycle. 623 func findGraphCycle(graph *core.BuildGraph, target *core.BuildTarget) []*core.BuildTarget { 624 index := func(haystack []*core.BuildTarget, needle *core.BuildTarget) int { 625 for i, straw := range haystack { 626 if straw == needle { 627 return i 628 } 629 } 630 return -1 631 } 632 633 done := map[core.BuildLabel]bool{} 634 var detectCycle func(*core.BuildTarget, []*core.BuildTarget) []*core.BuildTarget 635 detectCycle = func(target *core.BuildTarget, deps []*core.BuildTarget) []*core.BuildTarget { 636 if i := index(deps, target); i != -1 { 637 return deps[i:] 638 } else if done[target.Label] { 639 return nil 640 } 641 done[target.Label] = true 642 deps = append(deps, target) 643 for _, dep := range target.Dependencies() { 644 if cycle := detectCycle(dep, deps); len(cycle) > 0 { 645 return cycle 646 } 647 } 648 return nil 649 } 650 return detectCycle(target, nil) 651 } 652 653 // unbuiltTargetsMessage returns a message for any targets that are supposed to build but haven't yet. 654 func unbuiltTargetsMessage(graph *core.BuildGraph) string { 655 msg := "" 656 for _, target := range graph.AllTargets() { 657 if target.State() == core.Active { 658 if graph.AllDepsBuilt(target) { 659 msg += fmt.Sprintf(" %s (waiting for deps to build)\n", target.Label) 660 } else { 661 msg += fmt.Sprintf(" %s\n", target.Label) 662 } 663 } else if target.State() == core.Pending { 664 msg += fmt.Sprintf(" %s (pending build)\n", target.Label) 665 } 666 } 667 if msg != "" { 668 return "\nThe following targets have not yet built:\n" + msg 669 } 670 return "" 671 } 672 673 // shortError returns the message for an error, shortening it if the error supports that. 674 func shortError(err error) string { 675 if se, ok := err.(shortenableError); ok { 676 return se.ShortError() 677 } 678 return err.Error() 679 } 680 681 // A shortenableError describes any error type that can communicate a short-form error. 682 type shortenableError interface { 683 ShortError() string 684 }