github.phpd.cn/thought-machine/please@v12.2.0+incompatible/src/build/build_step.go (about) 1 // Package build houses the core functionality for actually building targets. 2 package build 3 4 import ( 5 "bytes" 6 "crypto/sha1" 7 "encoding/hex" 8 "fmt" 9 "io" 10 "net/http" 11 "os" 12 "path" 13 "runtime" 14 "strconv" 15 "strings" 16 "sync" 17 "time" 18 19 "github.com/hashicorp/go-multierror" 20 "gopkg.in/op/go-logging.v1" 21 22 "core" 23 "fs" 24 "metrics" 25 ) 26 27 var log = logging.MustGetLogger("build") 28 29 // Type that indicates that we're stopping the build of a target in a nonfatal way. 30 var errStop = fmt.Errorf("stopping build") 31 32 // goDirOnce guards the creation of plz-out/go, which we only attempt once per process. 33 var goDirOnce sync.Once 34 35 // httpClient is the shared http client that we use for fetching remote files. 36 var httpClient http.Client 37 38 // Build implements the core logic for building a single target. 39 func Build(tid int, state *core.BuildState, label core.BuildLabel) { 40 start := time.Now() 41 target := state.Graph.TargetOrDie(label) 42 state = state.ForTarget(target) 43 target.SetState(core.Building) 44 if err := buildTarget(tid, state, target); err != nil { 45 if err == errStop { 46 target.SetState(core.Stopped) 47 state.LogBuildResult(tid, target.Label, core.TargetBuildStopped, "Build stopped") 48 return 49 } 50 state.LogBuildError(tid, label, core.TargetBuildFailed, err, "Build failed: %s", err) 51 if err := RemoveOutputs(target); err != nil { 52 log.Errorf("Failed to remove outputs for %s: %s", target.Label, err) 53 } 54 target.SetState(core.Failed) 55 return 56 } 57 metrics.Record(target, time.Since(start)) 58 59 // Add any of the reverse deps that are now fully built to the queue. 60 for _, reverseDep := range state.Graph.ReverseDependencies(target) { 61 if reverseDep.State() == core.Active && state.Graph.AllDepsBuilt(reverseDep) && reverseDep.SyncUpdateState(core.Active, core.Pending) { 62 state.AddPendingBuild(reverseDep.Label, false) 63 } 64 } 65 if target.IsTest && state.NeedTests { 66 state.AddPendingTest(target.Label) 67 } 68 state.Parser.UndeferAnyParses(state, target) 69 } 70 71 // Builds a single target 72 func buildTarget(tid int, state *core.BuildState, target *core.BuildTarget) (err error) { 73 defer func() { 74 if r := recover(); r != nil { 75 if e, ok := r.(error); ok { 76 err = e 77 } else { 78 err = fmt.Errorf("%s", r) 79 } 80 } 81 }() 82 83 if err := target.CheckDependencyVisibility(state); err != nil { 84 return err 85 } 86 // We can't do this check until build time, until then we don't know what all the outputs 87 // will be (eg. for filegroups that collect outputs of other rules). 88 if err := target.CheckDuplicateOutputs(); err != nil { 89 return err 90 } 91 // This must run before we can leave this function successfully by any path. 92 if target.PreBuildFunction != nil { 93 log.Debug("Running pre-build function for %s", target.Label) 94 if err := state.Parser.RunPreBuildFunction(tid, state, target); err != nil { 95 return err 96 } 97 log.Debug("Finished pre-build function for %s", target.Label) 98 } 99 state.LogBuildResult(tid, target.Label, core.TargetBuilding, "Preparing...") 100 var postBuildOutput string 101 if state.PrepareOnly && state.IsOriginalTarget(target.Label) { 102 if target.IsFilegroup { 103 return fmt.Errorf("Filegroup targets don't have temporary directories") 104 } 105 if err := prepareDirectories(target); err != nil { 106 return err 107 } 108 if err := prepareSources(state.Graph, target); err != nil { 109 return err 110 } 111 return errStop 112 } 113 if target.IsHashFilegroup { 114 updateHashFilegroupPaths(state, target) 115 } 116 if !needsBuilding(state, target, false) { 117 log.Debug("Not rebuilding %s, nothing's changed", target.Label) 118 if postBuildOutput, err = runPostBuildFunctionIfNeeded(tid, state, target, ""); err != nil { 119 log.Warning("Missing post-build output for %s; will rebuild.", target.Label) 120 } else { 121 // If a post-build function ran it may modify the rule definition. In that case we 122 // need to check again whether the rule needs building. 123 if target.PostBuildFunction == nil || !needsBuilding(state, target, true) { 124 if target.IsFilegroup { 125 // Small optimisation to ensure we don't need to rehash things unnecessarily. 126 copyFilegroupHashes(state, target) 127 } 128 target.SetState(core.Reused) 129 state.LogBuildResult(tid, target.Label, core.TargetCached, "Unchanged") 130 return nil // Nothing needs to be done. 131 } 132 log.Debug("Rebuilding %s after post-build function", target.Label) 133 } 134 } 135 oldOutputHash, outputHashErr := OutputHash(target) 136 if target.IsFilegroup { 137 log.Debug("Building %s...", target.Label) 138 if err := buildFilegroup(tid, state, target); err != nil { 139 return err 140 } else if newOutputHash, err := calculateAndCheckRuleHash(state, target); err != nil { 141 return err 142 } else if !bytes.Equal(newOutputHash, oldOutputHash) { 143 target.SetState(core.Built) 144 state.LogBuildResult(tid, target.Label, core.TargetBuilt, "Built") 145 } else { 146 target.SetState(core.Unchanged) 147 state.LogBuildResult(tid, target.Label, core.TargetCached, "Unchanged") 148 } 149 return nil 150 } 151 if err := prepareDirectories(target); err != nil { 152 return fmt.Errorf("Error preparing directories for %s: %s", target.Label, err) 153 } 154 155 // Similarly to the createInitPy special-casing, this is not very nice, but makes it 156 // rather easier to have a consistent GOPATH setup. 157 if target.HasLabel("go") { 158 goDirOnce.Do(createPlzOutGo) 159 } 160 161 retrieveArtifacts := func() bool { 162 state.LogBuildResult(tid, target.Label, core.TargetBuilding, "Checking cache...") 163 if _, retrieved := retrieveFromCache(state, target); retrieved { 164 log.Debug("Retrieved artifacts for %s from cache", target.Label) 165 checkLicences(state, target) 166 newOutputHash, err := calculateAndCheckRuleHash(state, target) 167 if err != nil { // Most likely hash verification failure 168 log.Warning("Error retrieving cached artifacts for %s: %s", target.Label, err) 169 RemoveOutputs(target) 170 return false 171 } else if outputHashErr != nil || !bytes.Equal(oldOutputHash, newOutputHash) { 172 target.SetState(core.Cached) 173 state.LogBuildResult(tid, target.Label, core.TargetCached, "Cached") 174 } else { 175 target.SetState(core.Unchanged) 176 state.LogBuildResult(tid, target.Label, core.TargetCached, "Cached (unchanged)") 177 } 178 return true // got from cache 179 } 180 return false 181 } 182 cacheKey := mustShortTargetHash(state, target) 183 if state.Cache != nil { 184 // Note that ordering here is quite sensitive since the post-build function can modify 185 // what we would retrieve from the cache. 186 if target.PostBuildFunction != nil { 187 log.Debug("Checking for post-build output file for %s in cache...", target.Label) 188 if state.Cache.RetrieveExtra(target, cacheKey, target.PostBuildOutputFileName()) { 189 if postBuildOutput, err = runPostBuildFunctionIfNeeded(tid, state, target, postBuildOutput); err != nil { 190 panic(err) 191 } 192 if retrieveArtifacts() { 193 return nil 194 } 195 } 196 } else if retrieveArtifacts() { 197 return nil 198 } 199 } 200 if err := target.CheckSecrets(); err != nil { 201 return err 202 } 203 if err := prepareSources(state.Graph, target); err != nil { 204 return fmt.Errorf("Error preparing sources for %s: %s", target.Label, err) 205 } 206 207 state.LogBuildResult(tid, target.Label, core.TargetBuilding, target.BuildingDescription) 208 out, err := buildMaybeRemotely(state, target, cacheKey) 209 if err != nil { 210 return err 211 } 212 if target.PostBuildFunction != nil { 213 out = bytes.TrimSpace(out) 214 if err := runPostBuildFunction(tid, state, target, string(out), postBuildOutput); err != nil { 215 return err 216 } 217 storePostBuildOutput(state, target, out) 218 } 219 checkLicences(state, target) 220 state.LogBuildResult(tid, target.Label, core.TargetBuilding, "Collecting outputs...") 221 extraOuts, outputsChanged, err := moveOutputs(state, target) 222 if err != nil { 223 return fmt.Errorf("Error moving outputs for target %s: %s", target.Label, err) 224 } 225 if _, err = calculateAndCheckRuleHash(state, target); err != nil { 226 return err 227 } 228 if outputsChanged { 229 target.SetState(core.Built) 230 } else { 231 target.SetState(core.Unchanged) 232 } 233 if state.Cache != nil { 234 state.LogBuildResult(tid, target.Label, core.TargetBuilding, "Storing...") 235 newCacheKey := mustShortTargetHash(state, target) 236 if target.PostBuildFunction != nil { 237 if !bytes.Equal(newCacheKey, cacheKey) { 238 // NB. Important this is stored with the earlier hash - if we calculate the hash 239 // now, it might be different, and we could of course never retrieve it again. 240 state.Cache.StoreExtra(target, cacheKey, target.PostBuildOutputFileName()) 241 } else { 242 extraOuts = append(extraOuts, target.PostBuildOutputFileName()) 243 } 244 } 245 state.Cache.Store(target, newCacheKey, extraOuts...) 246 } 247 // Clean up the temporary directory once it's done. 248 if state.CleanWorkdirs { 249 if err := os.RemoveAll(target.TmpDir()); err != nil { 250 log.Warning("Failed to remove temporary directory for %s: %s", target.Label, err) 251 } 252 } 253 if outputsChanged { 254 state.LogBuildResult(tid, target.Label, core.TargetBuilt, "Built") 255 } else { 256 state.LogBuildResult(tid, target.Label, core.TargetBuilt, "Built (unchanged)") 257 } 258 return nil 259 } 260 261 // runBuildCommand runs the actual command to build a target. 262 // On success it returns the stdout of the target, otherwise an error. 263 func runBuildCommand(state *core.BuildState, target *core.BuildTarget, command string, inputHash []byte) ([]byte, error) { 264 if target.IsRemoteFile { 265 return nil, fetchRemoteFile(state, target) 266 } 267 env := core.StampedBuildEnvironment(state, target, false, inputHash) 268 log.Debug("Building target %s\nENVIRONMENT:\n%s\n%s", target.Label, env, command) 269 out, combined, err := core.ExecWithTimeoutShell(state, target, target.TmpDir(), env, target.BuildTimeout, state.Config.Build.Timeout, state.ShowAllOutput, command, target.Sandbox) 270 if err != nil { 271 if state.Verbosity >= 4 { 272 return nil, fmt.Errorf("Error building target %s: %s\nENVIRONMENT:\n%s\n%s\n%s", 273 target.Label, err, env, target.GetCommand(state), combined) 274 } 275 return nil, fmt.Errorf("Error building target %s: %s\n%s", target.Label, err, combined) 276 } 277 return out, nil 278 } 279 280 // Prepares the output directories for a target 281 func prepareDirectories(target *core.BuildTarget) error { 282 if err := prepareDirectory(target.TmpDir(), true); err != nil { 283 return err 284 } 285 if err := prepareDirectory(target.OutDir(), false); err != nil { 286 return err 287 } 288 // Nicety for the build rules: create any directories that it's 289 // declared it'll create files in. 290 for _, out := range target.Outputs() { 291 if dir := path.Dir(out); dir != "." { 292 outPath := path.Join(target.TmpDir(), dir) 293 if !core.PathExists(outPath) { 294 if err := os.MkdirAll(outPath, core.DirPermissions); err != nil { 295 return err 296 } 297 } 298 } 299 } 300 return nil 301 } 302 303 func prepareDirectory(directory string, remove bool) error { 304 if remove && core.PathExists(directory) { 305 if err := os.RemoveAll(directory); err != nil { 306 return err 307 } 308 } 309 err := os.MkdirAll(directory, core.DirPermissions) 310 if err != nil && checkForStaleOutput(directory, err) { 311 err = os.MkdirAll(directory, core.DirPermissions) 312 } 313 return err 314 } 315 316 // Symlinks the source files of this rule into its temp directory. 317 func prepareSources(graph *core.BuildGraph, target *core.BuildTarget) error { 318 for source := range core.IterSources(graph, target) { 319 if err := core.PrepareSourcePair(source); err != nil { 320 return err 321 } 322 } 323 return nil 324 } 325 326 func moveOutputs(state *core.BuildState, target *core.BuildTarget) ([]string, bool, error) { 327 // Before we write any outputs, we must remove the old hash file to avoid it being 328 // left in an inconsistent state. 329 if err := os.RemoveAll(ruleHashFileName(target)); err != nil { 330 return nil, true, err 331 } 332 changed := false 333 tmpDir := target.TmpDir() 334 outDir := target.OutDir() 335 for _, output := range target.Outputs() { 336 tmpOutput := path.Join(tmpDir, output) 337 realOutput := path.Join(outDir, output) 338 if !core.PathExists(tmpOutput) { 339 return nil, true, fmt.Errorf("Rule %s failed to create output %s", target.Label, tmpOutput) 340 } 341 outputChanged, err := moveOutput(target, tmpOutput, realOutput) 342 if err != nil { 343 return nil, true, err 344 } 345 changed = changed || outputChanged 346 } 347 if changed { 348 log.Debug("Outputs for %s have changed", target.Label) 349 } else { 350 log.Debug("Outputs for %s are unchanged", target.Label) 351 } 352 // Optional outputs get moved but don't contribute to the hash or for incrementality. 353 // Glob patterns are supported on these. 354 extraOuts := []string{} 355 for _, output := range fs.Glob(state.Config.Parse.BuildFileName, tmpDir, target.OptionalOutputs, nil, nil, true) { 356 log.Debug("Discovered optional output %s", output) 357 tmpOutput := path.Join(tmpDir, output) 358 realOutput := path.Join(outDir, output) 359 if _, err := moveOutput(target, tmpOutput, realOutput); err != nil { 360 return nil, changed, err 361 } 362 extraOuts = append(extraOuts, output) 363 } 364 return extraOuts, changed, nil 365 } 366 367 func moveOutput(target *core.BuildTarget, tmpOutput, realOutput string) (bool, error) { 368 // hash the file 369 newHash, err := pathHash(tmpOutput, false) 370 if err != nil { 371 return true, err 372 } 373 if fs.PathExists(realOutput) { 374 if oldHash, err := pathHash(realOutput, false); err != nil { 375 return true, err 376 } else if bytes.Equal(oldHash, newHash) { 377 // We already have the same file in the current location. Don't bother moving it. 378 log.Debug("Checking %s vs. %s, hashes match", tmpOutput, realOutput) 379 return false, nil 380 } 381 if err := os.RemoveAll(realOutput); err != nil { 382 return true, err 383 } 384 } 385 movePathHash(tmpOutput, realOutput, false) 386 // Check if we need a directory for this output. 387 dir := path.Dir(realOutput) 388 if !core.PathExists(dir) { 389 if err := os.MkdirAll(dir, core.DirPermissions); err != nil { 390 return true, err 391 } 392 } 393 // If the output file is in plz-out/tmp we can just move it to save time, otherwise we need 394 // to copy so we don't move files from other directories. 395 if strings.HasPrefix(tmpOutput, target.TmpDir()) { 396 if err := os.Rename(tmpOutput, realOutput); err != nil { 397 return true, err 398 } 399 } else { 400 if err := core.RecursiveCopyFile(tmpOutput, realOutput, target.OutMode(), false, false); err != nil { 401 return true, err 402 } 403 } 404 if target.IsBinary { 405 if err := os.Chmod(realOutput, target.OutMode()); err != nil { 406 return true, err 407 } 408 } 409 return true, nil 410 } 411 412 // RemoveOutputs removes all generated outputs for a rule. 413 func RemoveOutputs(target *core.BuildTarget) error { 414 if err := os.Remove(ruleHashFileName(target)); err != nil && !os.IsNotExist(err) { 415 if checkForStaleOutput(ruleHashFileName(target), err) { 416 return RemoveOutputs(target) // try again 417 } 418 return err 419 } 420 for _, output := range target.Outputs() { 421 if err := os.RemoveAll(path.Join(target.OutDir(), output)); err != nil { 422 return err 423 } 424 } 425 return nil 426 } 427 428 // checkForStaleOutput removes any parents of a file that are files themselves. 429 // This is a fix for a specific case where there are old file outputs in plz-out which 430 // have the same name as part of a package path. 431 // It returns true if something was removed. 432 func checkForStaleOutput(filename string, err error) bool { 433 if perr, ok := err.(*os.PathError); ok && perr.Err.Error() == "not a directory" { 434 for dir := path.Dir(filename); dir != "." && dir != "/" && path.Base(dir) != "plz-out"; dir = path.Dir(filename) { 435 if fs.FileExists(dir) { 436 log.Warning("Removing %s which appears to be a stale output file", dir) 437 os.Remove(dir) 438 return true 439 } 440 } 441 } 442 return false 443 } 444 445 // calculateAndCheckRuleHash checks the output hash for a rule. 446 func calculateAndCheckRuleHash(state *core.BuildState, target *core.BuildTarget) ([]byte, error) { 447 hash, err := OutputHash(target) 448 if err != nil { 449 return nil, err 450 } 451 if err = checkRuleHashes(target, hash); err != nil { 452 if state.NeedHashesOnly && (state.IsOriginalTarget(target.Label) || state.IsOriginalTarget(target.Label.Parent())) { 453 return nil, errStop 454 } else if state.VerifyHashes { 455 return nil, err 456 } else { 457 log.Warning("%s", err) 458 } 459 } 460 if err := writeRuleHashFile(state, target); err != nil { 461 return nil, fmt.Errorf("Attempting to create hash file: %s", err) 462 } 463 return hash, nil 464 } 465 466 // OutputHash calculates the hash of a target's outputs. 467 func OutputHash(target *core.BuildTarget) ([]byte, error) { 468 h := sha1.New() 469 for _, output := range target.Outputs() { 470 // NB. Always force a recalculation of the output hashes here. Memoisation is not 471 // useful because by definition we are rebuilding a target, and can actively hurt 472 // in cases where we compare the retrieved cache artifacts with what was there before. 473 filename := path.Join(target.OutDir(), output) 474 h2, err := pathHash(filename, true) 475 if err != nil { 476 return nil, err 477 } 478 h.Write(h2) 479 // Record the name of the file too, but not if the rule has hash verification 480 // (because this will change the hashes, and the cases it fixes are relatively rare 481 // and generally involve things like hash_filegroup that doesn't have hashes set). 482 // TODO(pebers): Find some more elegant way of unifying this behaviour. 483 if len(target.Hashes) == 0 { 484 h.Write([]byte(filename)) 485 } 486 } 487 return h.Sum(nil), nil 488 } 489 490 // mustOutputHash calculates the hash of a target's outputs. It panics on any errors. 491 func mustOutputHash(target *core.BuildTarget) []byte { 492 hash, err := OutputHash(target) 493 if err != nil { 494 panic(err) 495 } 496 return hash 497 } 498 499 // Verify the hash of output files for a rule match the ones set on it. 500 func checkRuleHashes(target *core.BuildTarget, hash []byte) error { 501 if len(target.Hashes) == 0 { 502 return nil // nothing to check 503 } 504 hashStr := hex.EncodeToString(hash) 505 for _, okHash := range target.Hashes { 506 // Hashes can have an arbitrary label prefix. Strip it off if present. 507 if index := strings.LastIndexByte(okHash, ':'); index != -1 { 508 okHash = strings.TrimSpace(okHash[index+1:]) 509 } 510 if okHash == hashStr { 511 return nil 512 } 513 } 514 if len(target.Hashes) == 1 { 515 return fmt.Errorf("Bad output hash for rule %s: was %s but expected %s", 516 target.Label, hashStr, target.Hashes[0]) 517 } 518 return fmt.Errorf("Bad output hash for rule %s: was %s but expected one of [%s]", 519 target.Label, hashStr, strings.Join(target.Hashes, ", ")) 520 } 521 522 func retrieveFromCache(state *core.BuildState, target *core.BuildTarget) ([]byte, bool) { 523 hash := mustShortTargetHash(state, target) 524 return hash, state.Cache.Retrieve(target, hash) 525 } 526 527 // Runs the post-build function for a target if it's got one. 528 func runPostBuildFunctionIfNeeded(tid int, state *core.BuildState, target *core.BuildTarget, prevOutput string) (string, error) { 529 if target.PostBuildFunction != nil { 530 out, err := loadPostBuildOutput(state, target) 531 if err != nil { 532 return "", err 533 } 534 return out, runPostBuildFunction(tid, state, target, out, prevOutput) 535 } 536 return "", nil 537 } 538 539 // Runs the post-build function for a target. 540 // In some cases it may have already run; if so we compare the previous output and warn 541 // if the two differ (they must be deterministic to ensure it's a pure function, since there 542 // are a few different paths through here and we guarantee to only run them once). 543 func runPostBuildFunction(tid int, state *core.BuildState, target *core.BuildTarget, output, prevOutput string) error { 544 if prevOutput != "" { 545 if output != prevOutput { 546 log.Warning("The build output for %s differs from what we got back from the cache earlier.\n"+ 547 "This implies your target's output is nondeterministic; Please won't re-run the\n"+ 548 "post-build function, which will *probably* be okay, but Please can't be sure.\n"+ 549 "See https://github.com/thought-machine/please/issues/113 for more information.", target.Label) 550 log.Debug("Cached build output for %s: %s\n\nNew build output: %s", target.Label, prevOutput, output) 551 } 552 return nil 553 } 554 return state.Parser.RunPostBuildFunction(tid, state, target, output) 555 } 556 557 // checkLicences checks the licences for the target match what we've accepted / rejected in the config 558 // and panics if they don't match. 559 func checkLicences(state *core.BuildState, target *core.BuildTarget) { 560 for _, licence := range target.Licences { 561 for _, reject := range state.Config.Licences.Reject { 562 if strings.EqualFold(reject, licence) { 563 panic(fmt.Sprintf("Target %s is licensed %s, which is explicitly rejected for this repository", target.Label, licence)) 564 } 565 } 566 for _, accept := range state.Config.Licences.Accept { 567 if strings.EqualFold(accept, licence) { 568 log.Info("Licence %s is accepted in this repository", licence) 569 return // Note licences are assumed to be an 'or', ie. any one of them can be accepted. 570 } 571 } 572 } 573 if len(target.Licences) > 0 && len(state.Config.Licences.Accept) > 0 { 574 panic(fmt.Sprintf("None of the licences for %s are accepted in this repository: %s", target.Label, strings.Join(target.Licences, ", "))) 575 } 576 } 577 578 // createPlzOutGo creates a directory plz-out/go that contains src / pkg links which 579 // make it easier to set up one's GOPATH appropriately. 580 func createPlzOutGo() { 581 dir := path.Join(core.RepoRoot, core.OutDir, "go") 582 genDir := path.Join(core.RepoRoot, core.GenDir) 583 srcDir := path.Join(dir, "src") 584 pkgDir := path.Join(dir, "pkg") 585 archDir := path.Join(pkgDir, runtime.GOOS+"_"+runtime.GOARCH) 586 if err := os.MkdirAll(pkgDir, core.DirPermissions); err != nil { 587 log.Warning("Failed to create %s: %s", pkgDir, err) 588 return 589 } 590 symlinkIfNotExists(genDir, srcDir) 591 symlinkIfNotExists(genDir, archDir) 592 } 593 594 // symlinkIfNotExists creates newDir as a link to oldDir if it doesn't already exist. 595 func symlinkIfNotExists(oldDir, newDir string) { 596 if !core.PathExists(newDir) { 597 if err := os.Symlink(oldDir, newDir); err != nil && !os.IsExist(err) { 598 log.Warning("Failed to create %s: %s", newDir, err) 599 } 600 } 601 } 602 603 // fetchRemoteFile fetches a remote file from a URL. 604 // This is a builtin for better efficiency and more control over the whole process. 605 func fetchRemoteFile(state *core.BuildState, target *core.BuildTarget) error { 606 if err := prepareDirectory(target.OutDir(), false); err != nil { 607 return err 608 } else if err := prepareDirectory(target.TmpDir(), false); err != nil { 609 return err 610 } else if err := os.RemoveAll(ruleHashFileName(target)); err != nil { 611 return err 612 } 613 httpClient.Timeout = time.Duration(state.Config.Build.Timeout) // Can't set this when we init the client because config isn't loaded then. 614 var err error 615 for _, src := range target.Sources { 616 if e := fetchOneRemoteFile(state, target, string(src.(core.URLLabel))); e != nil { 617 err = multierror.Append(err, e) 618 } else { 619 return nil 620 } 621 } 622 return err 623 } 624 625 func fetchOneRemoteFile(state *core.BuildState, target *core.BuildTarget, url string) error { 626 env := core.BuildEnvironment(state, target, false) 627 url = os.Expand(url, env.ReplaceEnvironment) 628 tmpPath := path.Join(target.TmpDir(), target.Outputs()[0]) 629 f, err := os.Create(tmpPath) 630 if err != nil { 631 return err 632 } 633 resp, err := httpClient.Get(url) 634 if err != nil { 635 return err 636 } 637 defer resp.Body.Close() 638 if resp.StatusCode < 200 || resp.StatusCode > 299 { 639 return fmt.Errorf("Error retrieving %s: %s", url, resp.Status) 640 } 641 var r io.Reader = resp.Body 642 if length := resp.Header.Get("Content-Length"); length != "" { 643 if i, err := strconv.Atoi(length); err == nil { 644 r = &progressReader{Reader: resp.Body, Target: target, Total: float32(i)} 645 } 646 } 647 target.ShowProgress = true // Required for it to actually display 648 h := sha1.New() 649 if _, err := io.Copy(io.MultiWriter(f, h), r); err != nil { 650 return err 651 } 652 setPathHash(tmpPath, h.Sum(nil)) 653 return f.Close() 654 } 655 656 // A progressReader tracks progress from a HTTP response and marks it on the given target. 657 type progressReader struct { 658 Reader io.Reader 659 Target *core.BuildTarget 660 Done, Total float32 661 } 662 663 // Read implements the io.Reader interface 664 func (r *progressReader) Read(b []byte) (int, error) { 665 n, err := r.Reader.Read(b) 666 r.Done += float32(n) 667 r.Target.Progress = 100.0 * r.Done / r.Total 668 return n, err 669 }