rust/build_system/src/utils.rs

415 lines
12 KiB
Rust
Raw Normal View History

2023-09-26 16:09:51 +02:00
use std::collections::HashMap;
2023-08-18 16:06:20 +02:00
use std::ffi::OsStr;
2023-08-19 20:53:47 +02:00
use std::fmt::Debug;
2023-08-18 16:06:20 +02:00
use std::fs;
use std::path::Path;
2023-08-19 20:53:47 +02:00
use std::process::{Command, ExitStatus, Output};
2023-08-18 16:06:20 +02:00
2023-09-26 16:09:51 +02:00
fn get_command_inner(
input: &[&dyn AsRef<OsStr>],
cwd: Option<&Path>,
env: Option<&HashMap<String, String>>,
) -> Command {
2023-08-18 16:06:20 +02:00
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);
}
2023-09-26 16:09:51 +02:00
if let Some(env) = env {
command.envs(env.iter().map(|(k, v)| (k.as_str(), v.as_str())));
}
2023-08-19 18:24:01 +02:00
command
}
2023-08-19 20:53:47 +02:00
fn check_exit_status(
input: &[&dyn AsRef<OsStr>],
cwd: Option<&Path>,
exit_status: ExitStatus,
output: Option<&Output>,
2023-12-29 21:27:36 +01:00
show_err: bool,
2023-08-19 20:53:47 +02:00
) -> 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()
);
2023-11-24 22:23:08 +01:00
let input = input.iter().map(|i| i.as_ref()).collect::<Vec<&OsStr>>();
2023-12-29 21:27:36 +01:00
if show_err {
eprintln!("Command `{:?}` failed", input);
}
if let Some(output) = output {
2023-12-01 23:57:16 +01:00
let stdout = String::from_utf8_lossy(&output.stdout);
if !stdout.is_empty() {
error.push_str("\n==== STDOUT ====\n");
error.push_str(&*stdout);
2023-12-01 23:57:16 +01:00
}
let stderr = String::from_utf8_lossy(&output.stderr);
if !stderr.is_empty() {
error.push_str("\n==== STDERR ====\n");
error.push_str(&*stderr);
}
2023-08-19 20:53:47 +02:00
}
Err(error)
2023-08-19 20:53:47 +02:00
}
fn command_error<D: Debug>(input: &[&dyn AsRef<OsStr>], cwd: &Option<&Path>, error: D) -> String {
format!(
"Command `{}`{} failed to run: {error:?}",
2023-09-26 16:09:51 +02:00
input
.iter()
2023-08-19 20:53:47 +02:00
.map(|s| s.as_ref().to_str().unwrap())
.collect::<Vec<_>>()
.join(" "),
cwd.as_ref()
2023-09-26 16:09:51 +02:00
.map(|cwd| format!(" (running in folder `{}`)", cwd.display(),))
2023-08-19 20:53:47 +02:00
.unwrap_or_default(),
)
}
pub fn run_command(input: &[&dyn AsRef<OsStr>], cwd: Option<&Path>) -> Result<Output, String> {
2023-09-26 16:09:51 +02:00
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)
2023-08-19 20:53:47 +02:00
.output()
.map_err(|e| command_error(input, &cwd, e))?;
2023-12-29 21:27:36 +01:00
check_exit_status(input, cwd, output.status, Some(&output), true)?;
2023-08-19 20:53:47 +02:00
Ok(output)
2023-08-18 16:06:20 +02:00
}
2023-08-19 18:24:01 +02:00
pub fn run_command_with_output(
input: &[&dyn AsRef<OsStr>],
cwd: Option<&Path>,
2023-10-04 16:01:02 +02:00
) -> Result<(), String> {
let exit_status = get_command_inner(input, cwd, None)
.spawn()
.map_err(|e| command_error(input, &cwd, e))?
.wait()
.map_err(|e| command_error(input, &cwd, e))?;
2023-12-29 21:27:36 +01:00
check_exit_status(input, cwd, exit_status, None, true)?;
2023-10-04 16:01:02 +02:00
Ok(())
}
pub fn run_command_with_output_and_env(
input: &[&dyn AsRef<OsStr>],
cwd: Option<&Path>,
2023-09-26 16:09:51 +02:00
env: Option<&HashMap<String, String>>,
2023-08-19 18:24:01 +02:00
) -> Result<(), String> {
2023-09-26 16:09:51 +02:00
let exit_status = get_command_inner(input, cwd, env)
.spawn()
2023-08-19 20:53:47 +02:00
.map_err(|e| command_error(input, &cwd, e))?
2023-08-19 18:24:01 +02:00
.wait()
2023-08-19 20:53:47 +02:00
.map_err(|e| command_error(input, &cwd, e))?;
2023-12-29 21:27:36 +01:00
check_exit_status(input, cwd, exit_status, None, true)?;
Ok(())
}
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 = get_command_inner(input, cwd, env)
.spawn()
.map_err(|e| command_error(input, &cwd, e))?
.wait()
.map_err(|e| command_error(input, &cwd, e))?;
check_exit_status(input, cwd, exit_status, None, false)?;
2023-08-19 18:24:01 +02:00
Ok(())
}
2023-08-18 16:06:20 +02:00
pub fn cargo_install(to_install: &str) -> Result<(), String> {
let output = run_command(&[&"cargo", &"install", &"--list"], None)?;
2023-08-19 20:53:47 +02:00
let to_install_needle = format!("{to_install} ");
2023-08-18 16:06:20 +02:00
// 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()
2023-08-19 20:53:47 +02:00
.any(|line| line.ends_with(':') && line.starts_with(&to_install_needle))
2023-08-18 16:06:20 +02:00
{
return Ok(());
}
2023-08-19 20:53:47 +02:00
// We voluntarily ignore this error.
2023-10-04 16:01:02 +02:00
if run_command_with_output(&[&"cargo", &"install", &to_install], None).is_err() {
2023-08-19 20:53:47 +02:00
println!("Skipping installation of `{to_install}`");
}
2023-08-18 16:06:20 +02:00
Ok(())
}
2023-09-26 16:09:51 +02:00
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()
2023-10-04 16:01:02 +02:00
.to_string();
2023-09-26 16:09:51 +02:00
if !name.is_empty() {
Ok(name)
} else {
2023-10-04 16:01:02 +02:00
Err("Failed to retrieve the OS name".to_string())
2023-09-26 16:09:51 +02:00
}
}
2023-12-21 23:46:41 +01:00
#[derive(Default, PartialEq)]
2023-11-08 17:31:56 +01:00
pub struct RustcVersionInfo {
2023-12-21 23:46:41 +01:00
pub short: String,
2023-11-08 17:31:56 +01:00
pub version: String,
pub host: Option<String>,
pub commit_hash: Option<String>,
pub commit_date: Option<String>,
}
2023-12-21 23:46:41 +01:00
pub fn rustc_toolchain_version_info(toolchain: &str) -> Result<RustcVersionInfo, String> {
rustc_version_info_inner(None, Some(toolchain))
}
2023-11-08 17:31:56 +01:00
pub fn rustc_version_info(rustc: Option<&str>) -> Result<RustcVersionInfo, String> {
2023-12-21 23:46:41 +01:00
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)
}?;
2023-09-26 16:09:51 +02:00
let content = std::str::from_utf8(&output.stdout).unwrap_or("");
2023-11-08 17:31:56 +01:00
let mut info = RustcVersionInfo::default();
2023-12-21 23:46:41 +01:00
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()),
};
2023-11-08 17:31:56 +01:00
2023-12-21 23:46:41 +01:00
for line in lines.map(|line| line.trim()) {
2023-11-08 17:31:56 +01:00
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()),
_ => {}
2023-09-26 16:09:51 +02:00
}
}
2023-11-08 17:31:56 +01:00
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('"').skip(1).next()
})
.next()
{
Some(toolchain) => Ok(toolchain.to_string()),
None => Err("Couldn't find `channel` in `rust-toolchain` file".to_string()),
}
2023-09-26 16:09:51 +02:00
}
2023-08-18 16:06:20 +02:00
pub struct CloneResult {
pub ran_clone: bool,
pub repo_name: String,
pub repo_dir: String,
2023-08-18 16:06:20 +02:00
}
2024-02-12 14:41:18 +01:00
pub fn git_clone(
to_clone: &str,
dest: Option<&Path>,
shallow_clone: bool,
) -> Result<CloneResult, String> {
2023-08-18 16:06:20 +02:00
let repo_name = to_clone.split('/').last().unwrap();
let repo_name = match repo_name.strip_suffix(".git") {
2023-10-04 16:01:02 +02:00
Some(n) => n.to_string(),
None => repo_name.to_string(),
2023-08-18 16:06:20 +02:00
};
2023-08-19 20:53:47 +02:00
let dest = dest
.map(|dest| dest.join(&repo_name))
.unwrap_or_else(|| Path::new(&repo_name).into());
2023-08-18 16:06:20 +02:00
if dest.is_dir() {
2023-09-26 16:09:51 +02:00
return Ok(CloneResult {
ran_clone: false,
repo_name,
repo_dir: dest.display().to_string(),
2023-09-26 16:09:51 +02:00
});
2023-08-18 16:06:20 +02:00
}
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)?;
2023-09-26 16:09:51 +02:00
Ok(CloneResult {
ran_clone: true,
repo_name,
repo_dir: dest.display().to_string(),
2023-09-26 16:09:51 +02:00
})
2023-08-18 16:06:20 +02:00
}
2023-08-19 20:53:47 +02:00
pub fn walk_dir<P, D, F>(dir: P, mut dir_cb: D, mut file_cb: F) -> Result<(), String>
2023-08-18 16:06:20 +02:00
where
P: AsRef<Path>,
2023-08-19 20:53:47 +02:00
D: FnMut(&Path) -> Result<(), String>,
F: FnMut(&Path) -> Result<(), String>,
2023-08-18 16:06:20 +02:00
{
let dir = dir.as_ref();
2023-10-04 16:01:02 +02:00
for entry in fs::read_dir(dir)
.map_err(|error| format!("Failed to read dir `{}`: {:?}", dir.display(), error))?
2023-09-26 16:09:51 +02:00
{
2023-10-04 16:01:02 +02:00
let entry = entry
.map_err(|error| format!("Failed to read entry in `{}`: {:?}", dir.display(), error))?;
2023-08-18 16:06:20 +02:00
let entry_path = entry.path();
if entry_path.is_dir() {
dir_cb(&entry_path)?;
} else {
file_cb(&entry_path)?;
}
}
Ok(())
}
2023-11-08 17:31:56 +01:00
2023-12-01 23:57:16 +01:00
pub fn split_args(args: &str) -> Result<Vec<String>, String> {
2023-11-08 17:31:56 +01:00
let mut out = Vec::new();
let mut start = 0;
2023-12-01 23:57:16 +01:00
let args = args.trim();
2023-11-08 17:31:56 +01:00
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();
2023-11-08 17:31:56 +01:00
}
}
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();
2023-11-08 17:31:56 +01:00
}
}
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();
2023-11-08 17:31:56 +01:00
}
}
let s = args[start..].trim();
if !s.is_empty() {
out.push(s.to_string());
}
2023-12-01 23:57:16 +01:00
Ok(out)
}
2023-12-20 14:58:43 +01:00
pub fn remove_file<P: AsRef<Path> + ?Sized>(file_path: &P) -> Result<(), String> {
2023-12-01 23:57:16 +01:00
std::fs::remove_file(file_path).map_err(|error| {
format!(
"Failed to remove `{}`: {:?}",
file_path.as_ref().display(),
error
)
})
2023-11-08 17:31:56 +01:00
}
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,
)
})
}
#[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()])
);
}
}