use miette::{Context, IntoDiagnostic};
use std::sync::Arc;
use crate::{
archive::{extract_tar_gz, extract_zip},
fs::save_file,
reporter::{ProgressReporter, Reporter},
toolchain::ToolchainSpec,
utils::{build_dist_server_api, build_http_client_with_retry, path_to_reader, url_to_reader},
};
use super::index::InstallRecipe;
pub async fn populate_install(recipe: &InstallRecipe) -> miette::Result<()> {
let mut download_dir = crate::moonup_home();
download_dir.push("downloads");
let mut install_dir_root = crate::moonup_home();
install_dir_root.push("toolchains");
let tag = match recipe.spec {
ToolchainSpec::Bleeding => {
download_dir.push("bleeding");
install_dir_root.push("bleeding");
"bleeding".to_string()
}
ToolchainSpec::Nightly => {
let date = recipe.release.date.as_deref().expect("should have date");
download_dir.push("nightly");
download_dir.push(date);
install_dir_root.push("nightly");
format!("nightly-{date}")
}
ToolchainSpec::Latest => {
let version = recipe.release.version.as_str();
download_dir.push("latest");
download_dir.push(version);
install_dir_root.push("latest");
format!("v{}", version)
}
ToolchainSpec::Version(ref v) => {
if v.starts_with("nightly-") {
let date = recipe.release.date.as_deref().expect("should have date");
download_dir.push("nightly");
download_dir.push(date);
install_dir_root.push(v);
v.to_owned()
} else {
download_dir.push("latest");
download_dir.push(v);
install_dir_root.push(v);
format!("v{}", v)
}
}
};
crate::fs::empty_dir(&install_dir_root)
.into_diagnostic()
.wrap_err(format!("Failed to delete {}", install_dir_root.display()))
.wrap_err("Unable to clean up existing installation, files may be in use")?;
let is_bleeding = recipe.spec.is_bleeding();
for component in recipe.components.iter() {
let name = component.name.as_str();
let file = component.file.as_str();
let sha256_expected = component.sha256.as_str();
let local_file = download_dir.join(file);
if is_bleeding || !local_file.exists() {
tracing::debug!("downloading {} to {}", name, local_file.display());
let client = build_http_client_with_retry();
let pathname = format!("/download/{}/{}", tag, file);
let url = build_dist_server_api(&pathname)?;
let progress_reporter = ProgressReporter::new(format!("Downloading {}", name));
let reporter = Some(Arc::new(progress_reporter) as Arc<dyn Reporter>);
let reader = url_to_reader(url, &client, reporter.clone()).await?;
let sha256_actual = format!("{:x}", save_file(reader, &local_file).await?);
if let Some(reporter) = &reporter {
reporter.on_complete();
}
if sha256_actual != sha256_expected {
let msg = format!(
"Checksum mismatch for file {}\nExpected: {}\n Actual: {}\n\nPlease try again.",
file, sha256_expected, sha256_actual
);
let _ = std::fs::remove_file(&local_file).inspect_err(|e| {
tracing::debug!("failed to remove invalid download: {}", e);
});
let err = std::io::Error::new(std::io::ErrorKind::InvalidData, msg);
return Err(err).into_diagnostic();
}
}
}
for component in recipe.components.iter() {
let mut component_install_dir = install_dir_root.clone();
let name = component.name.as_str();
let file = component.file.as_str();
let sha256_expected = component.sha256.as_str();
let local_file = download_dir.join(file);
tracing::debug!("installing {} from {}", name, local_file.display());
let reader = path_to_reader(&local_file)
.await
.wrap_err("failed to read local file")?;
if name == "toolchain" && recipe.release.layout_version1.unwrap_or(false) {
let version = recipe.release.version.as_str();
tracing::debug!("old toolchain archive layout detected (version: {version})");
component_install_dir.push("bin");
}
if name == "libcore" {
component_install_dir.push("lib");
}
let is_zip = file.ends_with(".zip");
let sha256 = match is_zip {
true => extract_zip(reader, &component_install_dir).await?,
false => extract_tar_gz(reader, &component_install_dir).await?,
};
let sha256_actual = format!("{:x}", sha256);
if sha256_actual != sha256_expected {
let msg = format!(
"Checksum mismatch for file {}\nExpected: {}\n Actual: {}\n\nPlease try again.",
file, sha256_expected, sha256_actual
);
let _ = std::fs::remove_file(&local_file).inspect_err(|e| {
tracing::debug!("failed to remove invalid component download: {}", e);
});
let _ = crate::fs::remove_dir_all(&install_dir_root).inspect_err(|e| {
tracing::debug!("failed to clean up invalid installation: {}", e);
});
let err = std::io::Error::new(std::io::ErrorKind::InvalidData, msg);
return Err(err).into_diagnostic();
}
}
if recipe.spec.is_latest() || recipe.spec.is_bleeding() {
let actual_version = recipe.release.version.as_str();
install_dir_root.push("version");
tokio::fs::write(&install_dir_root, format!("{}\n", actual_version))
.await
.into_diagnostic()?;
} else if recipe.spec.is_nightly() {
let actual_date = recipe.release.date.as_ref().expect("should have a date");
install_dir_root.push("version");
tokio::fs::write(&install_dir_root, format!("{}\n", actual_date))
.await
.into_diagnostic()?;
}
Ok(())
}