| use std::collections::HashMap; |
| use std::ffi::OsStr; |
| use std::fmt::Debug; |
| use std::fs; |
| #[cfg(unix)] |
| use std::os::unix::process::ExitStatusExt; |
| use std::path::{Path, PathBuf}; |
| use std::process::{Command, ExitStatus, Output}; |
| |
| fn exec_command( |
| input: &[&dyn AsRef<OsStr>], |
| cwd: Option<&Path>, |
| env: Option<&HashMap<String, String>>, |
| ) -> Result<ExitStatus, String> { |
| let status = get_command_inner(input, cwd, env) |
| .spawn() |
| .map_err(|e| command_error(input, &cwd, e))? |
| .wait() |
| .map_err(|e| command_error(input, &cwd, e))?; |
| #[cfg(unix)] |
| { |
| if let Some(signal) = status.signal() { |
| // In case the signal didn't kill the current process. |
| return Err(command_error(input, &cwd, format!("Process received signal {signal}"))); |
| } |
| } |
| Ok(status) |
| } |
| |
| pub(crate) fn get_command_inner( |
| input: &[&dyn AsRef<OsStr>], |
| cwd: Option<&Path>, |
| env: Option<&HashMap<String, String>>, |
| ) -> Command { |
| let (cmd, args) = match input { |
| [] => panic!("empty command"), |
| [cmd, args @ ..] => (cmd, args), |
| }; |
| let mut command = Command::new(cmd); |
| command.args(args); |
| if let Some(cwd) = cwd { |
| command.current_dir(cwd); |
| } |
| if let Some(env) = env { |
| command.envs(env.iter().map(|(k, v)| (k.as_str(), v.as_str()))); |
| } |
| command |
| } |
| |
| fn check_exit_status( |
| input: &[&dyn AsRef<OsStr>], |
| cwd: Option<&Path>, |
| exit_status: ExitStatus, |
| output: Option<&Output>, |
| show_err: bool, |
| ) -> Result<(), String> { |
| if exit_status.success() { |
| return Ok(()); |
| } |
| let mut error = format!( |
| "Command `{}`{} exited with status {:?}", |
| input.iter().map(|s| s.as_ref().to_str().unwrap()).collect::<Vec<_>>().join(" "), |
| cwd.map(|cwd| format!(" (running in folder `{}`)", cwd.display())).unwrap_or_default(), |
| exit_status.code() |
| ); |
| let input = input.iter().map(|i| i.as_ref()).collect::<Vec<&OsStr>>(); |
| if show_err { |
| eprintln!("Command `{input:?}` failed"); |
| } |
| if let Some(output) = output { |
| let stdout = String::from_utf8_lossy(&output.stdout); |
| if !stdout.is_empty() { |
| error.push_str("\n==== STDOUT ====\n"); |
| error.push_str(&stdout); |
| } |
| let stderr = String::from_utf8_lossy(&output.stderr); |
| if !stderr.is_empty() { |
| error.push_str("\n==== STDERR ====\n"); |
| error.push_str(&stderr); |
| } |
| } |
| Err(error) |
| } |
| |
| fn command_error<D: Debug>(input: &[&dyn AsRef<OsStr>], cwd: &Option<&Path>, error: D) -> String { |
| format!( |
| "Command `{}`{} failed to run: {error:?}", |
| input.iter().map(|s| s.as_ref().to_str().unwrap()).collect::<Vec<_>>().join(" "), |
| cwd.as_ref() |
| .map(|cwd| format!(" (running in folder `{}`)", cwd.display(),)) |
| .unwrap_or_default(), |
| ) |
| } |
| |
| pub fn run_command(input: &[&dyn AsRef<OsStr>], cwd: Option<&Path>) -> Result<Output, String> { |
| run_command_with_env(input, cwd, None) |
| } |
| |
| pub fn run_command_with_env( |
| input: &[&dyn AsRef<OsStr>], |
| cwd: Option<&Path>, |
| env: Option<&HashMap<String, String>>, |
| ) -> Result<Output, String> { |
| let output = |
| get_command_inner(input, cwd, env).output().map_err(|e| command_error(input, &cwd, e))?; |
| check_exit_status(input, cwd, output.status, Some(&output), true)?; |
| Ok(output) |
| } |
| |
| pub fn run_command_with_output( |
| input: &[&dyn AsRef<OsStr>], |
| cwd: Option<&Path>, |
| ) -> Result<(), String> { |
| let exit_status = exec_command(input, cwd, None)?; |
| check_exit_status(input, cwd, exit_status, None, true)?; |
| Ok(()) |
| } |
| |
| pub fn run_command_with_output_and_env( |
| input: &[&dyn AsRef<OsStr>], |
| cwd: Option<&Path>, |
| env: Option<&HashMap<String, String>>, |
| ) -> Result<(), String> { |
| let exit_status = exec_command(input, cwd, env)?; |
| check_exit_status(input, cwd, exit_status, None, true)?; |
| Ok(()) |
| } |
| |
| #[cfg(not(unix))] |
| pub fn run_command_with_output_and_env_no_err( |
| input: &[&dyn AsRef<OsStr>], |
| cwd: Option<&Path>, |
| env: Option<&HashMap<String, String>>, |
| ) -> Result<(), String> { |
| let exit_status = exec_command(input, cwd, env)?; |
| check_exit_status(input, cwd, exit_status, None, false)?; |
| Ok(()) |
| } |
| |
| pub fn cargo_install(to_install: &str) -> Result<(), String> { |
| let output = run_command(&[&"cargo", &"install", &"--list"], None)?; |
| |
| let to_install_needle = format!("{to_install} "); |
| // cargo install --list returns something like this: |
| // |
| // mdbook-toc v0.8.0: |
| // mdbook-toc |
| // rust-reduce v0.1.0: |
| // rust-reduce |
| // |
| // We are only interested into the command name so we only look for lines ending with `:`. |
| if String::from_utf8(output.stdout) |
| .unwrap() |
| .lines() |
| .any(|line| line.ends_with(':') && line.starts_with(&to_install_needle)) |
| { |
| return Ok(()); |
| } |
| // We voluntarily ignore this error. |
| if run_command_with_output(&[&"cargo", &"install", &to_install], None).is_err() { |
| println!("Skipping installation of `{to_install}`"); |
| } |
| Ok(()) |
| } |
| |
| pub fn get_os_name() -> Result<String, String> { |
| let output = run_command(&[&"uname"], None)?; |
| let name = std::str::from_utf8(&output.stdout).unwrap_or("").trim().to_string(); |
| if !name.is_empty() { Ok(name) } else { Err("Failed to retrieve the OS name".to_string()) } |
| } |
| |
| #[derive(Default, PartialEq)] |
| pub struct RustcVersionInfo { |
| pub short: String, |
| pub version: String, |
| pub host: Option<String>, |
| pub commit_hash: Option<String>, |
| pub commit_date: Option<String>, |
| } |
| |
| pub fn rustc_toolchain_version_info(toolchain: &str) -> Result<RustcVersionInfo, String> { |
| rustc_version_info_inner(None, Some(toolchain)) |
| } |
| |
| pub fn rustc_version_info(rustc: Option<&str>) -> Result<RustcVersionInfo, String> { |
| rustc_version_info_inner(rustc, None) |
| } |
| |
| fn rustc_version_info_inner( |
| rustc: Option<&str>, |
| toolchain: Option<&str>, |
| ) -> Result<RustcVersionInfo, String> { |
| let output = if let Some(toolchain) = toolchain { |
| run_command(&[&rustc.unwrap_or("rustc"), &toolchain, &"-vV"], None) |
| } else { |
| run_command(&[&rustc.unwrap_or("rustc"), &"-vV"], None) |
| }?; |
| let content = std::str::from_utf8(&output.stdout).unwrap_or(""); |
| |
| let mut info = RustcVersionInfo::default(); |
| let mut lines = content.split('\n'); |
| info.short = match lines.next() { |
| Some(s) => s.to_string(), |
| None => return Err("failed to retrieve rustc version".to_string()), |
| }; |
| |
| for line in lines.map(|line| line.trim()) { |
| match line.split_once(':') { |
| Some(("host", data)) => info.host = Some(data.trim().to_string()), |
| Some(("release", data)) => info.version = data.trim().to_string(), |
| Some(("commit-hash", data)) => info.commit_hash = Some(data.trim().to_string()), |
| Some(("commit-date", data)) => info.commit_date = Some(data.trim().to_string()), |
| _ => {} |
| } |
| } |
| if info.version.is_empty() { |
| Err("failed to retrieve rustc version".to_string()) |
| } else { |
| Ok(info) |
| } |
| } |
| |
| pub fn get_toolchain() -> Result<String, String> { |
| let content = match fs::read_to_string("rust-toolchain") { |
| Ok(content) => content, |
| Err(_) => return Err("No `rust-toolchain` file found".to_string()), |
| }; |
| match content |
| .split('\n') |
| .map(|line| line.trim()) |
| .filter(|line| !line.is_empty()) |
| .filter_map(|line| { |
| if !line.starts_with("channel") { |
| return None; |
| } |
| line.split('"').nth(1) |
| }) |
| .next() |
| { |
| Some(toolchain) => Ok(toolchain.to_string()), |
| None => Err("Couldn't find `channel` in `rust-toolchain` file".to_string()), |
| } |
| } |
| |
| pub struct CloneResult { |
| pub ran_clone: bool, |
| pub repo_name: String, |
| pub repo_dir: String, |
| } |
| |
| fn git_clone_inner( |
| to_clone: &str, |
| dest: &Path, |
| shallow_clone: bool, |
| repo_name: String, |
| ) -> Result<CloneResult, String> { |
| if dest.is_dir() { |
| return Ok(CloneResult { |
| ran_clone: false, |
| repo_name, |
| repo_dir: dest.display().to_string(), |
| }); |
| } |
| |
| let mut command: Vec<&dyn AsRef<OsStr>> = vec![&"git", &"clone", &to_clone, &dest]; |
| if shallow_clone { |
| command.push(&"--depth"); |
| command.push(&"1"); |
| } |
| run_command_with_output(&command, None)?; |
| Ok(CloneResult { ran_clone: true, repo_name, repo_dir: dest.display().to_string() }) |
| } |
| |
| fn get_repo_name(url: &str) -> String { |
| let repo_name = url.split('/').next_back().unwrap(); |
| match repo_name.strip_suffix(".git") { |
| Some(n) => n.to_string(), |
| None => repo_name.to_string(), |
| } |
| } |
| |
| pub fn git_clone( |
| to_clone: &str, |
| dest: Option<&Path>, |
| shallow_clone: bool, |
| ) -> Result<CloneResult, String> { |
| let repo_name = get_repo_name(to_clone); |
| let tmp: PathBuf; |
| |
| let dest = match dest { |
| Some(dest) => dest, |
| None => { |
| tmp = repo_name.clone().into(); |
| &tmp |
| } |
| }; |
| git_clone_inner(to_clone, dest, shallow_clone, repo_name) |
| } |
| |
| pub fn create_dir<P: AsRef<Path>>(path: P) -> Result<(), String> { |
| fs::create_dir_all(&path).map_err(|error| { |
| format!("Failed to create directory `{}`: {:?}", path.as_ref().display(), error) |
| }) |
| } |
| |
| /// This function differs from `git_clone` in how it handles *where* the repository will be cloned. |
| /// In `git_clone`, it is cloned in the provided path. In this function, the path you provide is |
| /// the parent folder. So if you pass "a" as folder and try to clone "b.git", it will be cloned into |
| /// `a/b`. |
| pub fn git_clone_root_dir( |
| to_clone: &str, |
| dest_parent_dir: &Path, |
| shallow_clone: bool, |
| ) -> Result<CloneResult, String> { |
| let repo_name = get_repo_name(to_clone); |
| |
| git_clone_inner(to_clone, &dest_parent_dir.join(&repo_name), shallow_clone, repo_name) |
| } |
| |
| pub fn walk_dir<P, D, F>( |
| dir: P, |
| dir_cb: &mut D, |
| file_cb: &mut F, |
| recursive: bool, |
| ) -> Result<(), String> |
| where |
| P: AsRef<Path>, |
| D: FnMut(&Path) -> Result<(), String>, |
| F: FnMut(&Path) -> Result<(), String>, |
| { |
| let dir = dir.as_ref(); |
| for entry in fs::read_dir(dir) |
| .map_err(|error| format!("Failed to read dir `{}`: {:?}", dir.display(), error))? |
| { |
| let entry = entry |
| .map_err(|error| format!("Failed to read entry in `{}`: {:?}", dir.display(), error))?; |
| let entry_path = entry.path(); |
| if entry_path.is_dir() { |
| dir_cb(&entry_path)?; |
| if recursive { |
| walk_dir(entry_path, dir_cb, file_cb, recursive)?; // Recursive call |
| } |
| } else { |
| file_cb(&entry_path)?; |
| } |
| } |
| Ok(()) |
| } |
| |
| pub fn split_args(args: &str) -> Result<Vec<String>, String> { |
| let mut out = Vec::new(); |
| let mut start = 0; |
| let args = args.trim(); |
| let mut iter = args.char_indices().peekable(); |
| |
| while let Some((pos, c)) = iter.next() { |
| if c == ' ' { |
| out.push(args[start..pos].to_string()); |
| let mut found_start = false; |
| while let Some((pos, c)) = iter.peek() { |
| if *c != ' ' { |
| start = *pos; |
| found_start = true; |
| break; |
| } else { |
| iter.next(); |
| } |
| } |
| if !found_start { |
| return Ok(out); |
| } |
| } else if c == '"' || c == '\'' { |
| let end = c; |
| let mut found_end = false; |
| while let Some((_, c)) = iter.next() { |
| if c == end { |
| found_end = true; |
| break; |
| } else if c == '\\' { |
| // We skip the escaped character. |
| iter.next(); |
| } |
| } |
| if !found_end { |
| return Err(format!("Didn't find `{}` at the end of `{}`", end, &args[start..])); |
| } |
| } else if c == '\\' { |
| // We skip the escaped character. |
| iter.next(); |
| } |
| } |
| let s = args[start..].trim(); |
| if !s.is_empty() { |
| out.push(s.to_string()); |
| } |
| Ok(out) |
| } |
| |
| pub fn remove_file<P: AsRef<Path> + ?Sized>(file_path: &P) -> Result<(), String> { |
| std::fs::remove_file(file_path).map_err(|error| { |
| format!("Failed to remove `{}`: {:?}", file_path.as_ref().display(), error) |
| }) |
| } |
| |
| pub fn create_symlink<P: AsRef<Path>, Q: AsRef<Path>>(original: P, link: Q) -> Result<(), String> { |
| #[cfg(windows)] |
| let symlink = std::os::windows::fs::symlink_file; |
| #[cfg(not(windows))] |
| let symlink = std::os::unix::fs::symlink; |
| |
| symlink(&original, &link).map_err(|err| { |
| format!( |
| "failed to create a symlink `{}` to `{}`: {:?}", |
| original.as_ref().display(), |
| link.as_ref().display(), |
| err, |
| ) |
| }) |
| } |
| |
| pub fn get_sysroot_dir() -> PathBuf { |
| Path::new(crate::BUILD_DIR).join("build_sysroot") |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| |
| #[test] |
| fn test_split_args() { |
| // Missing `"` at the end. |
| assert!(split_args("\"tada").is_err()); |
| // Missing `'` at the end. |
| assert!(split_args("\'tada").is_err()); |
| |
| assert_eq!( |
| split_args("a \"b\" c"), |
| Ok(vec!["a".to_string(), "\"b\"".to_string(), "c".to_string()]) |
| ); |
| // Trailing whitespace characters. |
| assert_eq!( |
| split_args(" a \"b\" c "), |
| Ok(vec!["a".to_string(), "\"b\"".to_string(), "c".to_string()]) |
| ); |
| } |
| } |