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,
|
2023-11-21 16:29:32 +01:00
|
|
|
output: Option<&Output>,
|
2023-08-19 20:53:47 +02:00
|
|
|
) -> Result<(), String> {
|
|
|
|
if exit_status.success() {
|
2023-11-21 16:29:32 +01:00
|
|
|
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()
|
|
|
|
);
|
|
|
|
if let Some(output) = output {
|
|
|
|
unsafe {
|
|
|
|
let stdout = std::str::from_utf8_unchecked(&output.stdout);
|
|
|
|
if !stdout.is_empty() {
|
|
|
|
error.push_str("\n==== STDOUT ====\n");
|
|
|
|
error.push_str(stdout);
|
|
|
|
}
|
|
|
|
let stderr = std::str::from_utf8_unchecked(&output.stderr);
|
|
|
|
if !stderr.is_empty() {
|
|
|
|
error.push_str("\n==== STDERR ====\n");
|
|
|
|
error.push_str(stderr);
|
|
|
|
}
|
|
|
|
}
|
2023-08-19 20:53:47 +02:00
|
|
|
}
|
2023-11-21 16:29:32 +01: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-11-21 16:29:32 +01:00
|
|
|
check_exit_status(input, cwd, output.status, Some(&output))?;
|
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-11-21 16:29:32 +01:00
|
|
|
check_exit_status(input, cwd, exit_status, None)?;
|
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-11-21 16:29:32 +01:00
|
|
|
check_exit_status(input, cwd, exit_status, None)?;
|
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-11-08 17:31:56 +01:00
|
|
|
#[derive(Default)]
|
|
|
|
pub struct RustcVersionInfo {
|
|
|
|
pub version: String,
|
|
|
|
pub host: Option<String>,
|
|
|
|
pub commit_hash: Option<String>,
|
|
|
|
pub commit_date: Option<String>,
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn rustc_version_info(rustc: Option<&str>) -> Result<RustcVersionInfo, String> {
|
|
|
|
let output = 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-09-26 16:09:51 +02:00
|
|
|
for line in content.split('\n').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
|
|
|
}
|
|
|
|
|
|
|
|
pub fn get_gcc_path() -> Result<String, String> {
|
|
|
|
let content = match fs::read_to_string("gcc_path") {
|
2023-10-04 16:01:02 +02:00
|
|
|
Ok(content) => content,
|
2023-09-26 16:09:51 +02:00
|
|
|
Err(_) => {
|
|
|
|
return Err(
|
|
|
|
"Please put the path to your custom build of libgccjit in the file \
|
|
|
|
`gcc_path`, see Readme.md for details"
|
|
|
|
.into(),
|
|
|
|
)
|
|
|
|
}
|
|
|
|
};
|
|
|
|
match content
|
|
|
|
.split('\n')
|
2023-10-04 16:01:02 +02:00
|
|
|
.map(|line| line.trim())
|
|
|
|
.filter(|line| !line.is_empty())
|
2023-09-26 16:09:51 +02:00
|
|
|
.next()
|
|
|
|
{
|
|
|
|
Some(gcc_path) => {
|
|
|
|
let path = Path::new(gcc_path);
|
|
|
|
if !path.exists() {
|
|
|
|
Err(format!(
|
2023-10-04 16:01:02 +02:00
|
|
|
"Path `{}` contained in the `gcc_path` file doesn't exist",
|
|
|
|
gcc_path,
|
2023-09-26 16:09:51 +02:00
|
|
|
))
|
|
|
|
} else {
|
|
|
|
Ok(gcc_path.into())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
None => Err("No path found in `gcc_path` file".into()),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-08-18 16:06:20 +02:00
|
|
|
pub struct CloneResult {
|
|
|
|
pub ran_clone: bool,
|
|
|
|
pub repo_name: String,
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn git_clone(to_clone: &str, dest: Option<&Path>) -> Result<CloneResult, String> {
|
|
|
|
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,
|
|
|
|
});
|
2023-08-18 16:06:20 +02:00
|
|
|
}
|
|
|
|
|
2023-10-04 16:01:02 +02:00
|
|
|
run_command_with_output(&[&"git", &"clone", &to_clone, &dest], None)?;
|
2023-09-26 16:09:51 +02:00
|
|
|
Ok(CloneResult {
|
|
|
|
ran_clone: true,
|
|
|
|
repo_name,
|
|
|
|
})
|
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
|
|
|
|
|
|
|
pub fn split_args(args: &str) -> Vec<String> {
|
|
|
|
let mut out = Vec::new();
|
|
|
|
let mut start = 0;
|
|
|
|
let mut iter = args.char_indices().peekable();
|
|
|
|
|
|
|
|
while iter.peek().is_some() {
|
|
|
|
while let Some((pos, c)) = iter.next() {
|
|
|
|
if c == ' ' {
|
|
|
|
if pos != 0 {
|
|
|
|
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 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 {
|
|
|
|
out.push(args[start..].to_string());
|
|
|
|
return out;
|
|
|
|
}
|
|
|
|
} else if c == '\\' {
|
|
|
|
// We skip the escaped character.
|
|
|
|
iter.next();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
let s = args[start..].trim();
|
|
|
|
if !s.is_empty() {
|
|
|
|
out.push(s.to_string());
|
|
|
|
}
|
|
|
|
out
|
|
|
|
}
|