Change:
build.run(&mut cargo); } <del> /// Updates the mtime of a stamp file if necessary, only changing it if it's /// older than some other library file in the same directory. /// /// We don't know what file Cargo is going to output (because there's a hash in /// the file name) but we know where it's going to put it. We use this helper to /// detect changes to that output file by looking at the modification time for /// all files in a directory and updating the stamp if any are newer. /// /// Note that we only consider Rust libraries as that's what we're interested in /// propagating changes from. Files like executables are tracked elsewhere. fn update_mtime(build: &Build, path: &Path) { let entries = match path.parent().unwrap().join("deps").read_dir() { Ok(entries) => entries, Err(_) => return, }; let files = entries.map(|e| t!(e)).filter(|e| t!(e.file_type()).is_file()); let files = files.filter(|e| { let filename = e.file_name(); let filename = filename.to_str().unwrap(); filename.ends_with(".rlib") || filename.ends_with(".lib") || is_dylib(&filename) }); let max = files.max_by_key(|entry| { let meta = t!(entry.metadata()); FileTime::from_last_modification_time(&meta) }); let max = match max { Some(max) => max, None => return, </del> <ins> fn run_cargo(build: &Build, cargo: &mut Command, stamp: &Path) { // Instruct Cargo to give us json messages on stdout, critically leaving // stderr as piped so we can get those pretty colors. cargo.arg("--message-format").arg("json") .stdout(Stdio::piped()); build.verbose(&format!("running: {:?}", cargo)); let mut child = match cargo.spawn() { Ok(child) => child, Err(e) => panic!("failed to execute command: {:?}nerror: {}", cargo, e), </ins> }; <del> if mtime(&max.path()) > mtime(path) { build.verbose(&format!("updating {:?} as {:?} changed", path, max.path())); t!(File::create(path)); </del> <ins> // `target_root_dir` looks like $dir/$target/release let target_root_dir = stamp.parent().unwrap(); // `target_deps_dir` looks like $dir/$target/release/deps let target_deps_dir = target_root_dir.join("deps"); // `host_root_dir` looks like $dir/release let host_root_dir = target_root_dir.parent().unwrap() // chop off `release` .parent().unwrap() // chop off `$target` .join(target_root_dir.file_name().unwrap()); // Spawn Cargo slurping up its JSON output. We'll start building up the // `deps` array of all files it generated along with a `toplevel` array of // files we need to probe for later. let mut deps = Vec::new(); let mut toplevel = Vec::new(); let stdout = BufReader::new(child.stdout.take().unwrap()); for line in stdout.lines() { let line = t!(line); let json = if line.starts_with("{") { t!(line.parse::<json::Json>()) } else { // If this was informational, just print it out and continue println!("{}", line); continue }; if json.find("reason").and_then(|j| j.as_string()) != Some("compiler-artifact") { continue } for filename in json["filenames"].as_array().unwrap() { let filename = filename.as_string().unwrap(); // Skip files like executables if !filename.ends_with(".rlib") && !filename.ends_with(".lib") && !is_dylib(&filename) { continue } let filename = Path::new(filename); // If this was an output file in the "host dir" we don't actually // worry about it, it's not relevant for us. if filename.starts_with(&host_root_dir) { continue // If this was output in the `deps` dir then this is a precise file // name (hash included) so we start tracking it. } else if filename.starts_with(&target_deps_dir) { deps.push(filename.to_path_buf()); // Otherwise this was a "top level artifact" which right now doesn't // have a hash in the name, but there's a version of this file in // the `deps` folder which *does* have a hash in the name. That's // the one we'll want to we'll probe for it later. } else { toplevel.push((filename.file_stem().unwrap() .to_str().unwrap().to_string(), filename.extension().unwrap().to_owned() .to_str().unwrap().to_string())); } } } // Make sure Cargo actually succeeded after we read all of its stdout. let status = t!(child.wait()); if !status.success() { panic!("command did not execute successfully: {:?}n expected success, got: {}", cargo, status); } // Ok now we need to actually find all the files listed in `toplevel`. We've // got a list of prefix/extensions and we basically just need to find the // most recent file in the `deps` folder corresponding to each one. let contents = t!(target_deps_dir.read_dir()) .map(|e| t!(e)) .map(|e| (e.path(), e.file_name().into_string().unwrap(), t!(e.metadata()))) .collect::<Vec<_>>(); for (prefix, extension) in toplevel { let candidates = contents.iter().filter(|&&(_, ref filename, _)| { filename.starts_with(&prefix[..]) && filename[prefix.len()..].starts_with("-") && filename.ends_with(&extension[..]) }); let max = candidates.max_by_key(|&&(_, _, ref metadata)| { FileTime::from_last_modification_time(metadata) }); let path_to_add = match max { Some(triple) => triple.0.to_str().unwrap(), None => panic!("no output generated for {:?} {:?}", prefix, extension), }; if is_dylib(path_to_add) { let candidate = format!("{}.lib", path_to_add); let candidate = PathBuf::from(candidate); if candidate.exists() { deps.push(candidate); } } deps.push(path_to_add.into()); } // Now we want to update the contents of the stamp file, if necessary. First // we read off the previous contents along with its mtime. If our new // contents (the list of files to copy) is different or if any dep's mtime // is newer then we rewrite the stamp file. deps.sort(); let mut stamp_contents = Vec::new(); if let Ok(mut f) = File::open(stamp) { t!(f.read_to_end(&mut stamp_contents)); } let stamp_mtime = mtime(&stamp); let mut new_contents = Vec::new(); let mut max = None; let mut max_path = None; for dep in deps { let mtime = mtime(&dep); if Some(mtime) > max { max = Some(mtime); max_path = Some(dep.clone()); } new_contents.extend(dep.to_str().unwrap().as_bytes()); new_contents.extend(b"0"); } let max = max.unwrap(); let max_path = max_path.unwrap(); if stamp_contents == new_contents && max <= stamp_mtime { return } if max > stamp_mtime { build.verbose(&format!("updating {:?} as {:?} changed", stamp, max_path)); } else { build.verbose(&format!("updating {:?} as deps changed", stamp)); </ins> } <ins> t!(t!(File::create(stamp)).write_all(&new_contents)); </ins> }

Discussion:
Rustbuild compiles the compiler in a particular location and is then responsible for copying artifacts over to the actual structure the compiler is expecting (e.g. rustbuild assembles a sysroot). In order to handle stale folders rustbuild , deduplicated by hash. This pretty obviously, though, doesn't work with multiple versions of the same library! We should probably call Cargo with and learn about compiled artifacts, that way we can copy precisely what we need to into the sysroot.