use {
anyhow::anyhow,
reqwest::Url,
std::{
io::Read,
path::{Path, PathBuf},
},
trycmd_indygreg_fork::{schema::TryCmd, Error, TestCases},
};
const COREUTILS_VERSION: &str = "0.0.22";
const COREUTILS_BINARIES: [&str; 11] = [
"cat", "cp", "hashsum", "ln", "ls", "mkdir", "mv", "rm", "sort", "test", "touch",
];
const COREUTILS_ARTIFACT_URL: &str = "https://github.com/uutils/coreutils/releases/download";
const COREUTILS_TAR_TRIPLES: [&str; 4] = [
"aarch64-unknown-linux-gnu",
"i686-unknown-linux-musl",
"x86_64-apple-darwin",
"x86_64-unknown-linux-musl",
];
const COREUTILS_ZIP_TRIPLES: [&str; 2] = ["i686-pc-windows-msvc", "x86_64-pc-windows-msvc"];
fn ensure_coreutils_multicall() -> anyhow::Result<PathBuf> {
let current_exe = std::env::current_exe()?;
let target_dir = current_exe
.parent()
.ok_or_else(|| anyhow!("unable to determine current exe parent"))?
.parent()
.ok_or_else(|| anyhow!("unable to determine parent directory of current exe directory"))?
.parent()
.ok_or_else(|| anyhow!("unable to parent grandparent of current exe directory"))?;
let coreutils_dir = target_dir.join("coreutils");
let coreutils_bin_dir = coreutils_dir.join("bin");
let multicall_bin = coreutils_bin_dir.join("coreutils");
let multicall_bin = materialize_coreutils(&coreutils_dir, &multicall_bin)?;
Ok(multicall_bin)
}
fn materialize_coreutils(coreutils_dir: &Path, multicall_bin: &Path) -> anyhow::Result<PathBuf> {
let triple = if cfg!(all(target_os = "linux", target_arch = "x86")) {
Some("i686-unknown-linux-musl")
} else if cfg!(all(target_os = "linux", target_arch = "x86_64")) {
Some("x86_64-unknown-linux-musl")
} else if cfg!(all(target_os = "macos", target_arch = "x86_64")) {
Some("x86_64-apple-darwin")
} else if cfg!(all(target_os = "windows", target_arch = "x86")) {
Some("i686-pc-windows-msvc")
} else if cfg!(all(target_os = "windows", target_arch = "x86_64")) {
Some("x86_64-pc-windows-msvc")
} else {
None
};
let mut multicall_bin = multicall_bin.to_path_buf();
if let Some(triple) = triple {
if triple.contains("-windows-") {
multicall_bin.set_extension("exe");
}
}
if multicall_bin.exists() {
return Ok(multicall_bin);
}
match triple {
Some(triple) => {
let suffix = if COREUTILS_TAR_TRIPLES.contains(&triple) {
"tar.gz"
} else if COREUTILS_ZIP_TRIPLES.contains(&triple) {
"zip"
} else {
panic!("unhandled triple")
};
let filename = multicall_bin
.file_name()
.unwrap()
.to_string_lossy()
.to_string();
let exe_data = download_coreutils_artifact(triple, suffix, &filename)?;
eprintln!("writing {}", multicall_bin.display());
if let Some(parent) = multicall_bin.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(&multicall_bin, exe_data)?;
simple_file_manifest::set_executable(&mut std::fs::File::open(&multicall_bin)?)?;
Ok(multicall_bin)
}
None => {
let cargo_bin = std::env::var_os("CARGO")
.ok_or_else(|| anyhow!("unable to resolve CARGO environment variable"))?;
eprintln!("installing Rust coreutils to {}", coreutils_dir.display());
let output = std::process::Command::new(cargo_bin)
.args(vec![
"install".to_string(),
"--root".to_string(),
coreutils_dir.display().to_string(),
"--version".to_string(),
COREUTILS_VERSION.to_string(),
"coreutils".to_string(),
])
.output()?;
if !output.status.success() {
return Err(anyhow!(
"error installing coreutils: stdout: {}; stderr: {}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
));
}
Ok(multicall_bin)
}
}
}
fn download_coreutils_artifact(
triple: &str,
suffix: &str,
multicall_filename: &str,
) -> anyhow::Result<Vec<u8>> {
let url = format!("{COREUTILS_ARTIFACT_URL}/{COREUTILS_VERSION}/coreutils-{COREUTILS_VERSION}-{triple}.{suffix}");
let client = get_http_client()?;
eprintln!("downloading {}", url);
let mut res = client.get(url).send()?;
let mut data = vec![];
res.read_to_end(&mut data)?;
match suffix {
"tar.gz" => {
eprintln!("looking for {} in tar.gz", multicall_filename);
let d = flate2::read::GzDecoder::new(std::io::Cursor::new(data));
let mut ar = tar::Archive::new(d);
for entry in ar.entries()? {
let mut entry = entry?;
let path = entry.path()?;
if !path.display().to_string().ends_with(multicall_filename) {
continue;
}
eprintln!("extracting {} from tar.gz", path.display());
let mut buf = vec![];
entry.read_to_end(&mut buf)?;
return Ok(buf);
}
Err(anyhow!("could not find multicall binary in archive"))
}
"zip" => {
eprintln!("looking for {} in zip file", multicall_filename);
let mut archive = zip::ZipArchive::new(std::io::Cursor::new(data))?;
let archive_name = archive
.file_names()
.find(|f| f.ends_with(multicall_filename))
.ok_or_else(|| anyhow!("could not find multicall binary in zip file"))?
.to_string();
eprintln!("extracting {} from zip file", archive_name);
let mut zf = archive.by_name(&archive_name)?;
let mut buf = vec![];
zf.read_to_end(&mut buf)?;
Ok(buf)
}
_ => panic!("unhandled coreutils file extension"),
}
}
pub fn get_http_client() -> reqwest::Result<reqwest::blocking::Client> {
let mut builder = reqwest::blocking::ClientBuilder::new();
for (key, value) in std::env::vars() {
let key = key.to_lowercase();
if key.ends_with("_proxy") {
let end = key.len() - "_proxy".len();
let schema = &key[..end];
if let Ok(url) = Url::parse(&value) {
if let Some(Ok(proxy)) = match schema {
"http" => Some(reqwest::Proxy::http(url.as_str())),
"https" => Some(reqwest::Proxy::https(url.as_str())),
_ => None,
} {
builder = builder.proxy(proxy);
}
}
}
}
builder.build()
}
#[cfg(unix)]
fn install_coreutils_bin(multicall_bin: &Path, bin: &Path) -> Result<(), std::io::Error> {
std::os::unix::fs::symlink(multicall_bin, bin)
}
#[cfg(windows)]
fn install_coreutils_bin(multicall_bin: &Path, bin: &Path) -> Result<(), std::io::Error> {
std::fs::copy(multicall_bin, bin).map(|_| ())
}
fn load_trycmd(path: &Path) -> Result<TryCmd, Error> {
let mut cmd = TryCmd::load_trycmd(path)?;
let cwd = std::env::current_dir().map_err(Error::new)?;
cmd.fs.base = Some(cwd.clone());
cmd.fs.cwd = Some(cwd.clone());
cmd.fs.sandbox = Some(true);
Ok(cmd)
}
#[test]
fn cli_tests() {
let coreutils_multicall = ensure_coreutils_multicall().unwrap();
let coreutils_bin = coreutils_multicall.parent().unwrap();
let cases = TestCases::new();
for bin in COREUTILS_BINARIES {
let mut bin_path = coreutils_bin.join(bin);
if cfg!(windows) {
bin_path.set_extension("exe");
}
if bin_path.symlink_metadata().is_err() {
install_coreutils_bin(&coreutils_multicall, &bin_path).unwrap();
}
cases.register_bin(bin, bin_path);
}
cases.file_extension_loader("trycmd", load_trycmd);
cases.case("tests/cmd/*.trycmd").case("tests/cmd/*.toml");
if cfg!(not(feature = "notarize")) {
cases.skip("tests/cmd/encode-app-store-connect-api-key.trycmd");
cases.skip("tests/cmd/help.trycmd");
cases.skip("tests/cmd/notary*.trycmd");
}
if cfg!(windows) {
cases.skip("tests/cmd/sign-bundle-framework.trycmd");
cases.skip("tests/cmd/sign-bundle-with-nested-framework.trycmd");
cases.skip("tests/cmd/sign-bundle-electron.trycmd");
cases.skip("tests/cmd/sign-bundle-exclude.trycmd");
cases.skip("tests/cmd/sign-bundle-nested-symlinks.trycmd");
cases.skip("tests/cmd/sign-bundle-symlink-overwrite.trycmd");
}
}