github.phpd.cn/thought-machine/please@v12.2.0+incompatible/src/core/utils.go (about) 1 package core 2 3 import ( 4 "bytes" 5 "context" 6 "crypto/rand" 7 "crypto/sha1" 8 "encoding/hex" 9 "fmt" 10 "io" 11 "os" 12 "os/exec" 13 "path" 14 "path/filepath" 15 "runtime" 16 "strings" 17 "sync" 18 "syscall" 19 "time" 20 21 "cli" 22 "fs" 23 ) 24 25 // RepoRoot is the root of the Please repository 26 var RepoRoot string 27 28 // initialWorkingDir is the directory we began in. Early on we chdir() to the repo root but for 29 // some things we need to remember this. 30 var initialWorkingDir string 31 32 // initialPackage is the initial subdir of the working directory, ie. what package did we start in. 33 // This is similar but not identical to initialWorkingDir. 34 var initialPackage string 35 36 // usingBazelWorkspace is true if we detected a Bazel WORKSPACE file to find our repo root. 37 var usingBazelWorkspace bool 38 39 // DirPermissions are the default permission bits we apply to directories. 40 const DirPermissions = os.ModeDir | 0775 41 42 // FindRepoRoot returns the root directory of the current repo and sets the initial working dir. 43 // It returns true if the repo root was found. 44 func FindRepoRoot() bool { 45 initialWorkingDir, _ = os.Getwd() 46 RepoRoot, initialPackage = getRepoRoot(ConfigFileName, false) 47 return RepoRoot != "" 48 } 49 50 // MustFindRepoRoot returns the root directory of the current repo and sets the initial working dir. 51 // It dies on failure, although will fall back to looking for a Bazel WORKSPACE file first. 52 func MustFindRepoRoot() string { 53 if RepoRoot != "" { 54 return RepoRoot 55 } 56 if !FindRepoRoot() { 57 RepoRoot, initialPackage = getRepoRoot("WORKSPACE", true) 58 log.Warning("No .plzconfig file found to define the repo root.") 59 log.Warning("Falling back to Bazel WORKSPACE at %s", path.Join(RepoRoot, "WORKSPACE")) 60 usingBazelWorkspace = true 61 } 62 return RepoRoot 63 } 64 65 // InitialPackage returns a label corresponding to the initial package we started in. 66 func InitialPackage() []BuildLabel { 67 // It's possible to start off in directories that aren't legal package names, because 68 // our package naming is stricter than directory naming requirements. 69 // In that case move up until we find somewhere we can run from. 70 dir := initialPackage 71 for dir != "." { 72 if label, err := TryNewBuildLabel(dir, "test"); err == nil { 73 label.Name = "..." 74 return []BuildLabel{label} 75 } 76 dir = filepath.Dir(dir) 77 } 78 return WholeGraph 79 } 80 81 // getRepoRoot returns the root directory of the current repo and the initial package. 82 func getRepoRoot(filename string, die bool) (string, string) { 83 dir, err := os.Getwd() 84 if err != nil { 85 log.Fatalf("Couldn't determine working directory: %s", err) 86 } 87 // Walk up directories looking for a .plzconfig file, which we use to identify the root. 88 initial := dir 89 for dir != "" { 90 if PathExists(path.Join(dir, filename)) { 91 return dir, strings.TrimLeft(initial[len(dir):], "/") 92 } 93 dir, _ = path.Split(dir) 94 dir = strings.TrimRight(dir, "/") 95 } 96 if die { 97 log.Fatalf("Couldn't locate the repo root. Are you sure you're inside a plz repo?") 98 } 99 return "", "" 100 } 101 102 // StartedAtRepoRoot returns true if the build was initiated from the repo root. 103 // Used to provide slightly nicer output in some places. 104 func StartedAtRepoRoot() bool { 105 return RepoRoot == initialWorkingDir 106 } 107 108 // RecursiveCopyFile copies either a single file or a directory. 109 // If 'link' is true then we'll hardlink files instead of copying them. 110 // If 'fallback' is true then we'll fall back to a copy if linking fails. 111 func RecursiveCopyFile(from string, to string, mode os.FileMode, link, fallback bool) error { 112 if info, err := os.Stat(from); err == nil && info.IsDir() { 113 return fs.WalkMode(from, func(name string, isDir bool, fileMode os.FileMode) error { 114 dest := path.Join(to, name[len(from):]) 115 if isDir { 116 return os.MkdirAll(dest, DirPermissions) 117 } 118 return fs.CopyOrLinkFile(name, dest, mode, link, fallback) 119 }) 120 } 121 return fs.CopyOrLinkFile(from, to, mode, link, fallback) 122 } 123 124 // safeBuffer is an io.Writer that ensures that only one thread writes to it at a time. 125 // This is important because we potentially have both stdout and stderr writing to the same 126 // buffer, and os.exec only guarantees goroutine-safety if both are the same writer, which in 127 // our case they're not (but are both ultimately causing writes to the same buffer) 128 type safeBuffer struct { 129 sync.Mutex 130 buf bytes.Buffer 131 } 132 133 func (sb *safeBuffer) Write(b []byte) (int, error) { 134 sb.Lock() 135 defer sb.Unlock() 136 return sb.buf.Write(b) 137 } 138 139 func (sb *safeBuffer) Bytes() []byte { 140 return sb.buf.Bytes() 141 } 142 143 func (sb *safeBuffer) String() string { 144 return sb.buf.String() 145 } 146 147 // logProgress logs a message once a minute until the given context has expired. 148 // Used to provide some notion of progress while waiting for external commands. 149 func logProgress(ctx context.Context, target *BuildTarget) { 150 t := time.NewTicker(1 * time.Minute) 151 defer t.Stop() 152 for i := 1; i < 1000000; i++ { 153 select { 154 case <-ctx.Done(): 155 return 156 case <-t.C: 157 if i == 1 { 158 log.Notice("%s still running after 1 minute %s", target.Label, progressMessage(target)) 159 } else { 160 log.Notice("%s still running after %d minutes %s", target.Label, i, progressMessage(target)) 161 } 162 } 163 } 164 } 165 166 // progressMessage displays a progress message for a target, if it tracks progress. 167 func progressMessage(target *BuildTarget) string { 168 if target.ShowProgress { 169 return fmt.Sprintf("(%0.1f%% done)", target.Progress) 170 } 171 return "" 172 } 173 174 // ExecWithTimeout runs an external command with a timeout. 175 // If the command times out the returned error will be a context.DeadlineExceeded error. 176 // If showOutput is true then output will be printed to stderr as well as returned. 177 // It returns the stdout only, combined stdout and stderr and any error that occurred. 178 func ExecWithTimeout(target *BuildTarget, dir string, env []string, timeout time.Duration, defaultTimeout cli.Duration, showOutput, attachStdStreams bool, argv []string) ([]byte, []byte, error) { 179 if timeout == 0 { 180 if defaultTimeout == 0 { 181 timeout = 10 * time.Minute 182 } else { 183 timeout = time.Duration(defaultTimeout) 184 } 185 } 186 ctx, cancel := context.WithTimeout(context.Background(), timeout) 187 defer cancel() 188 cmd := ExecCommand(argv[0], argv[1:]...) 189 cmd.Dir = dir 190 cmd.Env = env 191 192 var out bytes.Buffer 193 var outerr safeBuffer 194 if showOutput { 195 cmd.Stdout = io.MultiWriter(os.Stderr, &out, &outerr) 196 cmd.Stderr = io.MultiWriter(os.Stderr, &outerr) 197 } else { 198 cmd.Stdout = io.MultiWriter(&out, &outerr) 199 cmd.Stderr = &outerr 200 } 201 if target != nil && target.ShowProgress { 202 cmd.Stdout = newProgressWriter(target, cmd.Stdout) 203 cmd.Stderr = newProgressWriter(target, cmd.Stderr) 204 } 205 if attachStdStreams { 206 cmd.Stdin = os.Stdin 207 cmd.Stdout = os.Stdout 208 cmd.Stderr = os.Stderr 209 } 210 if target != nil { 211 go logProgress(ctx, target) 212 } 213 // Start the command, wait for the timeout & then kill it. 214 // We deliberately don't use CommandContext because it will only send SIGKILL which 215 // child processes can't handle themselves. 216 err := cmd.Start() 217 if err != nil { 218 return nil, nil, err 219 } 220 ch := make(chan error) 221 go runCommand(cmd, ch) 222 select { 223 case err = <-ch: 224 // Do nothing. 225 case <-time.After(timeout): 226 // Send a relatively gentle signal that it can catch. 227 if err := cmd.Process.Signal(syscall.SIGTERM); err != nil { 228 log.Notice("Failed to kill subprocess: %s", err) 229 } 230 time.Sleep(10 * time.Millisecond) 231 // Send a more forceful signal. 232 cmd.Process.Kill() 233 err = fmt.Errorf("Timeout exceeded: %s", outerr.String()) 234 } 235 return out.Bytes(), outerr.Bytes(), err 236 } 237 238 // runCommand runs a command and signals on the given channel when it's done. 239 func runCommand(cmd *exec.Cmd, ch chan error) { 240 ch <- cmd.Wait() 241 } 242 243 // ExecWithTimeoutShell runs an external command within a Bash shell. 244 // Other arguments are as ExecWithTimeout. 245 // Note that the command is deliberately a single string. 246 func ExecWithTimeoutShell(state *BuildState, target *BuildTarget, dir string, env []string, timeout time.Duration, defaultTimeout cli.Duration, showOutput bool, cmd string, sandbox bool) ([]byte, []byte, error) { 247 return ExecWithTimeoutShellStdStreams(state, target, dir, env, timeout, defaultTimeout, showOutput, cmd, sandbox, false) 248 } 249 250 // ExecWithTimeoutShellStdStreams is as ExecWithTimeoutShell but optionally attaches stdin to the subprocess. 251 func ExecWithTimeoutShellStdStreams(state *BuildState, target *BuildTarget, dir string, env []string, timeout time.Duration, defaultTimeout cli.Duration, showOutput bool, cmd string, sandbox, attachStdStreams bool) ([]byte, []byte, error) { 252 c := append([]string{"bash", "-u", "-o", "pipefail", "-c"}, cmd) 253 // Runtime check is a little ugly, but we know this only works on Linux right now. 254 if sandbox && runtime.GOOS == "linux" { 255 tool, err := LookPath(state.Config.Build.PleaseSandboxTool, state.Config.Build.Path) 256 if err != nil { 257 return nil, nil, err 258 } 259 c = append([]string{tool}, c...) 260 } 261 return ExecWithTimeout(target, dir, env, timeout, defaultTimeout, showOutput, attachStdStreams, c) 262 } 263 264 // ExecWithTimeoutSimple runs an external command with a timeout. 265 // It's a simpler version of ExecWithTimeout that gives less control. 266 func ExecWithTimeoutSimple(timeout cli.Duration, cmd ...string) ([]byte, error) { 267 _, out, err := ExecWithTimeout(nil, "", nil, time.Duration(timeout), timeout, false, false, cmd) 268 return out, err 269 } 270 271 // A SourcePair represents a source file with its source and temporary locations. 272 // This isn't typically used much by callers; it's just useful to have a single type for channels. 273 type SourcePair struct{ Src, Tmp string } 274 275 // IterSources returns all the sources for a function, allowing for sources that are other rules 276 // and rules that require transitive dependencies. 277 // Yielded values are pairs of the original source location and its temporary location for this rule. 278 func IterSources(graph *BuildGraph, target *BuildTarget) <-chan SourcePair { 279 ch := make(chan SourcePair) 280 done := map[BuildLabel]bool{} 281 donePaths := map[string]bool{} 282 tmpDir := target.TmpDir() 283 var inner func(dependency *BuildTarget) 284 inner = func(dependency *BuildTarget) { 285 sources := dependency.AllSources() 286 if target == dependency { 287 // This is the current build rule, so link its sources. 288 for _, source := range sources { 289 for _, providedSource := range recursivelyProvideSource(graph, target, source) { 290 fullPaths := providedSource.FullPaths(graph) 291 for i, sourcePath := range providedSource.Paths(graph) { 292 tmpPath := path.Join(tmpDir, sourcePath) 293 ch <- SourcePair{fullPaths[i], tmpPath} 294 donePaths[tmpPath] = true 295 } 296 } 297 } 298 } else { 299 // This is a dependency of the rule, so link its outputs. 300 outDir := dependency.OutDir() 301 for _, dep := range dependency.Outputs() { 302 depPath := path.Join(outDir, dep) 303 pkgName := dependency.Subrepo.MakeRelativeName(dependency.Label.PackageName) 304 tmpPath := path.Join(tmpDir, pkgName, dep) 305 if !donePaths[tmpPath] { 306 ch <- SourcePair{depPath, tmpPath} 307 donePaths[tmpPath] = true 308 } 309 } 310 // Mark any label-type outputs as done. 311 for _, out := range dependency.DeclaredOutputs() { 312 if LooksLikeABuildLabel(out) { 313 label := ParseBuildLabel(out, target.Label.PackageName) 314 done[label] = true 315 } 316 } 317 } 318 // All the sources of this rule now count as done. 319 for _, source := range sources { 320 if label := source.Label(); label != nil && dependency.IsSourceOnlyDep(*label) { 321 done[*label] = true 322 } 323 } 324 325 done[dependency.Label] = true 326 if target == dependency || (target.NeedsTransitiveDependencies && !dependency.OutputIsComplete) { 327 for _, dep := range dependency.BuildDependencies() { 328 for _, dep2 := range recursivelyProvideFor(graph, target, dependency, dep.Label) { 329 if !done[dep2] && !dependency.IsTool(dep2) { 330 inner(graph.TargetOrDie(dep2)) 331 } 332 } 333 } 334 } else { 335 for _, dep := range dependency.ExportedDependencies() { 336 for _, dep2 := range recursivelyProvideFor(graph, target, dependency, dep) { 337 if !done[dep2] { 338 inner(graph.TargetOrDie(dep2)) 339 } 340 } 341 } 342 } 343 } 344 go func() { 345 inner(target) 346 close(ch) 347 }() 348 return ch 349 } 350 351 // recursivelyProvideFor recursively applies ProvideFor to a target. 352 func recursivelyProvideFor(graph *BuildGraph, target, dependency *BuildTarget, dep BuildLabel) []BuildLabel { 353 depTarget := graph.TargetOrDie(dep) 354 ret := depTarget.ProvideFor(dependency) 355 if len(ret) == 1 && ret[0] == dep { 356 // Dependency doesn't have a require/provide directly on this guy, up to the top-level 357 // target. We have to check the dep first to keep things consistent with what targets 358 // have actually been built. 359 ret = depTarget.ProvideFor(target) 360 if len(ret) == 1 && ret[0] == dep { 361 return ret 362 } 363 } 364 ret2 := make([]BuildLabel, 0, len(ret)) 365 for _, r := range ret { 366 if r == dep { 367 ret2 = append(ret2, r) // Providing itself, don't recurse 368 } else { 369 ret2 = append(ret2, recursivelyProvideFor(graph, target, dependency, r)...) 370 } 371 } 372 return ret2 373 } 374 375 // recursivelyProvideSource is similar to recursivelyProvideFor but operates on a BuildInput. 376 func recursivelyProvideSource(graph *BuildGraph, target *BuildTarget, src BuildInput) []BuildInput { 377 if label := src.nonOutputLabel(); label != nil { 378 dep := graph.TargetOrDie(*label) 379 provided := recursivelyProvideFor(graph, target, target, dep.Label) 380 ret := make([]BuildInput, len(provided)) 381 for i, p := range provided { 382 ret[i] = p 383 } 384 return ret 385 } 386 return []BuildInput{src} 387 } 388 389 // IterRuntimeFiles yields all the runtime files for a rule (outputs & data files), similar to above. 390 func IterRuntimeFiles(graph *BuildGraph, target *BuildTarget, absoluteOuts bool) <-chan SourcePair { 391 done := map[string]bool{} 392 ch := make(chan SourcePair) 393 394 makeOut := func(out string) string { 395 if absoluteOuts { 396 return path.Join(RepoRoot, target.TestDir(), out) 397 } 398 return out 399 } 400 401 pushOut := func(src, out string) { 402 out = makeOut(out) 403 if !done[out] { 404 ch <- SourcePair{src, out} 405 done[out] = true 406 } 407 } 408 409 var inner func(*BuildTarget) 410 inner = func(target *BuildTarget) { 411 outDir := target.OutDir() 412 for _, out := range target.Outputs() { 413 pushOut(path.Join(outDir, out), out) 414 } 415 for _, data := range target.Data { 416 var subrepo *Subrepo 417 label := data.Label() 418 if label != nil { 419 subrepo = graph.TargetOrDie(*label).Subrepo 420 } 421 fullPaths := data.FullPaths(graph) 422 for i, dataPath := range data.Paths(graph) { 423 pushOut(fullPaths[i], subrepo.MakeRelativeName(dataPath)) 424 } 425 if label != nil { 426 for _, dep := range graph.TargetOrDie(*label).ExportedDependencies() { 427 inner(graph.TargetOrDie(dep)) 428 } 429 } 430 } 431 for _, dep := range target.ExportedDependencies() { 432 inner(graph.TargetOrDie(dep)) 433 } 434 } 435 go func() { 436 inner(target) 437 close(ch) 438 }() 439 return ch 440 } 441 442 // IterInputPaths yields all the transitive input files for a rule (sources & data files), similar to above (again). 443 func IterInputPaths(graph *BuildGraph, target *BuildTarget) <-chan string { 444 // Use a couple of maps to protect us from dep-graph loops and to stop parsing the same target 445 // multiple times. We also only want to push files to the channel that it has not already seen. 446 donePaths := map[string]bool{} 447 doneTargets := map[*BuildTarget]bool{} 448 ch := make(chan string) 449 var inner func(*BuildTarget) 450 inner = func(target *BuildTarget) { 451 if !doneTargets[target] { 452 // First yield all the sources of the target only ever pushing declared paths to 453 // the channel to prevent us outputting any intermediate files. 454 for _, source := range target.AllSources() { 455 // If the label is nil add any input paths contained here. 456 if label := source.nonOutputLabel(); label == nil { 457 for _, sourcePath := range source.FullPaths(graph) { 458 if !donePaths[sourcePath] { 459 ch <- sourcePath 460 donePaths[sourcePath] = true 461 } 462 } 463 // Otherwise we should recurse for this build label (and gather its sources) 464 } else { 465 inner(graph.TargetOrDie(*label)) 466 } 467 } 468 469 // Now yield all the data deps of this rule. 470 for _, data := range target.Data { 471 // If the label is nil add any input paths contained here. 472 if label := data.Label(); label == nil { 473 for _, sourcePath := range data.FullPaths(graph) { 474 if !donePaths[sourcePath] { 475 ch <- sourcePath 476 donePaths[sourcePath] = true 477 } 478 } 479 // Otherwise we should recurse for this build label (and gather its sources) 480 } else { 481 inner(graph.TargetOrDie(*label)) 482 } 483 } 484 485 // Finally recurse for all the deps of this rule. 486 for _, dep := range target.Dependencies() { 487 inner(dep) 488 } 489 doneTargets[target] = true 490 } 491 } 492 go func() { 493 inner(target) 494 close(ch) 495 }() 496 return ch 497 } 498 499 // PrepareSource symlinks a single source file for a build rule. 500 func PrepareSource(sourcePath string, tmpPath string) error { 501 dir := path.Dir(tmpPath) 502 if !PathExists(dir) { 503 if err := os.MkdirAll(dir, DirPermissions); err != nil { 504 return err 505 } 506 } 507 if !PathExists(sourcePath) { 508 return fmt.Errorf("Source file %s doesn't exist", sourcePath) 509 } 510 return RecursiveCopyFile(sourcePath, tmpPath, 0, true, true) 511 } 512 513 // PrepareSourcePair prepares a source file for a build. 514 func PrepareSourcePair(pair SourcePair) error { 515 if path.IsAbs(pair.Src) { 516 return PrepareSource(pair.Src, pair.Tmp) 517 } 518 return PrepareSource(path.Join(RepoRoot, pair.Src), pair.Tmp) 519 } 520 521 // CollapseHash combines our usual four-part hash into one by XOR'ing them together. 522 // This helps keep things short in places where sometimes we get complaints about filenames being 523 // too long (this is most noticeable on e.g. Ubuntu with an encrypted home directory, but 524 // not an entire encrypted disk) and where we don't especially care about breaking out the 525 // individual parts of hashes, which is important for many parts of the system. 526 func CollapseHash(key []byte) []byte { 527 short := [sha1.Size]byte{} 528 // We store the rule hash twice, if it's repeated we must make sure not to xor it 529 // against itself. 530 if bytes.Equal(key[0:sha1.Size], key[sha1.Size:2*sha1.Size]) { 531 for i := 0; i < sha1.Size; i++ { 532 short[i] = key[i] ^ key[i+2*sha1.Size] ^ key[i+3*sha1.Size] 533 } 534 } else { 535 for i := 0; i < sha1.Size; i++ { 536 short[i] = key[i] ^ key[i+sha1.Size] ^ key[i+2*sha1.Size] ^ key[i+3*sha1.Size] 537 } 538 } 539 return short[:] 540 } 541 542 // LookPath does roughly the same as exec.LookPath, i.e. looks for the named file on the path. 543 // The main difference is that it looks based on our config which isn't necessarily the same 544 // as the external environment variable. 545 func LookPath(filename string, paths []string) (string, error) { 546 for _, p := range paths { 547 for _, p2 := range strings.Split(p, ":") { 548 p3 := path.Join(p2, filename) 549 if _, err := os.Stat(p3); err == nil { 550 return p3, nil 551 } 552 } 553 } 554 return "", fmt.Errorf("%s not found in PATH %s", filename, strings.Join(paths, ":")) 555 } 556 557 // AsyncDeleteDir deletes a directory asynchronously. 558 // First it renames the directory to something temporary and then forks to delete it. 559 // The rename is done synchronously but the actual deletion is async (after fork) so 560 // you don't have to wait for large directories to be removed. 561 // Conversely there is obviously no guarantee about at what point it will actually cease to 562 // be on disk any more. 563 func AsyncDeleteDir(dir string) error { 564 rm, err := exec.LookPath("rm") 565 if err != nil { 566 return err 567 } else if !PathExists(dir) { 568 return nil // not an error, just don't need to do anything. 569 } 570 newDir, err := moveDir(dir) 571 if err != nil { 572 return err 573 } 574 // Note that we can't fork() directly and continue running Go code, but ForkExec() works okay. 575 // Hence why we're using rm rather than fork() + os.RemoveAll. 576 _, err = syscall.ForkExec(rm, []string{rm, "-rf", newDir}, nil) 577 return err 578 } 579 580 // moveDir moves a directory to a new location and returns that new location. 581 func moveDir(dir string) (string, error) { 582 b := make([]byte, 16) 583 rand.Read(b) 584 name := path.Join(path.Dir(dir), ".plz_clean_"+hex.EncodeToString(b)) 585 log.Notice("Moving %s to %s", dir, name) 586 return name, os.Rename(dir, name) 587 } 588 589 // PathExists is an alias to fs.PathExists. 590 // TODO(peterebden): Remove and migrate everything over. 591 func PathExists(filename string) bool { 592 return fs.PathExists(filename) 593 }