use std::collections::HashMap; #[cfg(unix)] use std::ffi::c_int; 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}; #[cfg(unix)] extern "C" { fn raise(signal: c_int) -> c_int; } fn exec_command( input: &[&dyn AsRef], cwd: Option<&Path>, env: Option<&HashMap>, ) -> Result { 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() { unsafe { raise(signal as _); } // In case the signal didn't kill the current process. return Err(command_error(input, &cwd, format!("Process received signal {}", signal))); } } Ok(status) } fn get_command_inner( input: &[&dyn AsRef], cwd: Option<&Path>, env: Option<&HashMap>, ) -> 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], 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::>().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::>(); if show_err { eprintln!("Command `{:?}` failed", input); } 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(input: &[&dyn AsRef], cwd: &Option<&Path>, error: D) -> String { format!( "Command `{}`{} failed to run: {error:?}", input.iter().map(|s| s.as_ref().to_str().unwrap()).collect::>().join(" "), cwd.as_ref() .map(|cwd| format!(" (running in folder `{}`)", cwd.display(),)) .unwrap_or_default(), ) } pub fn run_command(input: &[&dyn AsRef], cwd: Option<&Path>) -> Result { run_command_with_env(input, cwd, None) } pub fn run_command_with_env( input: &[&dyn AsRef], cwd: Option<&Path>, env: Option<&HashMap>, ) -> Result { 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], 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], cwd: Option<&Path>, env: Option<&HashMap>, ) -> Result<(), String> { let exit_status = exec_command(input, cwd, env)?; check_exit_status(input, cwd, exit_status, None, true)?; Ok(()) } pub fn run_command_with_output_and_env_no_err( input: &[&dyn AsRef], cwd: Option<&Path>, env: Option<&HashMap>, ) -> 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 { 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, pub commit_hash: Option, pub commit_date: Option, } pub fn rustc_toolchain_version_info(toolchain: &str) -> Result { rustc_version_info_inner(None, Some(toolchain)) } pub fn rustc_version_info(rustc: Option<&str>) -> Result { rustc_version_info_inner(rustc, None) } fn rustc_version_info_inner( rustc: Option<&str>, toolchain: Option<&str>, ) -> Result { 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 { 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('"').skip(1).next() }) .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 { if dest.is_dir() { return Ok(CloneResult { ran_clone: false, repo_name, repo_dir: dest.display().to_string(), }); } let mut command: Vec<&dyn AsRef> = 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('/').last().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 { 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>(path: P) -> Result<(), String> { fs::create_dir_all(&path).map_err(|error| { format!("Failed to create directory `{}`: {:?}", path.as_ref().display(), error) }) } pub fn copy_file, T: AsRef>(from: F, to: T) -> Result<(), String> { fs::copy(&from, &to) .map_err(|error| { format!( "Failed to copy file `{}` into `{}`: {:?}", from.as_ref().display(), to.as_ref().display(), error ) }) .map(|_| ()) } /// 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 { 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( dir: P, dir_cb: &mut D, file_cb: &mut F, recursive: bool, ) -> Result<(), String> where P: AsRef, 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, 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 + ?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, Q: AsRef>(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()]) ); } }