use camino::Utf8Path;
use camino::Utf8PathBuf;
use std::collections::HashSet;
use std::fs;
use std::io;
use std::io::Write;
use std::time::Duration;
use whitaker_installer::builder::{library_extension, library_prefix};
use whitaker_installer::cli::InstallArgs;
use whitaker_installer::crate_name::CrateName;
use whitaker_installer::deps::{
CommandExecutor, check_dylint_tools, install_dylint_tools_with_output,
};
#[cfg(test)]
use whitaker_installer::deps::{DependencyInstallOptions, install_dylint_tools_with_options};
use whitaker_installer::dirs::BaseDirs;
use whitaker_installer::error::{InstallerError, Result};
use whitaker_installer::install_metrics::{InstallMode, RecordOutcome, record_install};
use whitaker_installer::output::write_stderr_line;
use whitaker_installer::prebuilt::{PrebuiltConfig, PrebuiltResult, attempt_prebuilt};
use whitaker_installer::prebuilt_path::prebuilt_library_dir;
use whitaker_installer::resolution::{EXPERIMENTAL_LINT_CRATES, LINT_CRATES, SUITE_CRATE};
pub(crate) fn ensure_dylint_tools_core(
quiet: bool,
stderr: &mut dyn Write,
is_all_installed: bool,
do_install: impl FnOnce(&mut dyn Write) -> Result<()>,
) -> Result<()> {
if is_all_installed {
return Ok(());
}
if !quiet {
write_stderr_line(stderr, "Installing required Dylint tools...");
}
do_install(stderr)?;
if !quiet {
write_stderr_line(stderr, "Dylint tools installed successfully.");
write_stderr_line(stderr, "");
}
Ok(())
}
pub(crate) fn ensure_dylint_tools_with_executor(
executor: &dyn CommandExecutor,
quiet: bool,
stderr: &mut dyn Write,
) -> Result<()> {
let status = check_dylint_tools(executor);
ensure_dylint_tools_core(quiet, stderr, status.all_installed(), |stderr| {
install_dylint_tools_with_output(executor, &status, quiet, stderr)
})
}
#[cfg(test)]
pub(crate) fn ensure_dylint_tools_with_options(
executor: &dyn CommandExecutor,
stderr: &mut dyn Write,
options: DependencyInstallOptions<'_>,
) -> Result<()> {
let status = check_dylint_tools(executor);
ensure_dylint_tools_core(options.quiet, stderr, status.all_installed(), |stderr| {
install_dylint_tools_with_options(executor, &status, stderr, options)
})
}
pub(crate) struct PrebuiltInstallationContext<'a> {
pub(crate) args: &'a InstallArgs,
pub(crate) dirs: &'a dyn BaseDirs,
pub(crate) requested_crates: &'a [CrateName],
pub(crate) toolchain_channel: &'a str,
}
pub(crate) struct MetricsWriteContext<'a> {
pub(crate) quiet: bool,
pub(crate) dirs: &'a dyn BaseDirs,
pub(crate) install_mode: InstallMode,
pub(crate) elapsed: Duration,
}
pub(crate) fn write_prebuilt_fallback_message(
quiet: bool,
error: &dyn std::fmt::Display,
stderr: &mut dyn Write,
) {
if quiet {
return;
}
write_stderr_line(stderr, format!("Prebuilt download unavailable: {error}"));
write_stderr_line(stderr, "Falling back to local compilation.");
write_stderr_line(stderr, "");
}
pub(crate) fn try_prebuilt_installation(
context: &PrebuiltInstallationContext<'_>,
stderr: &mut dyn Write,
) -> Result<Option<Utf8PathBuf>> {
try_prebuilt_installation_with(
context,
stderr,
PrebuiltInstallationHooks {
detect_host_target,
resolve_destination_dir: prebuilt_library_dir,
attempt_prebuilt,
prune_prebuilt_libraries,
},
)
}
type DetectHostTargetFn = fn() -> Result<String>;
type ResolveDestinationDirFn = fn(&dyn BaseDirs, &str, &str) -> Result<Utf8PathBuf>;
type AttemptPrebuiltFn = fn(&PrebuiltConfig<'_>, &mut dyn Write) -> PrebuiltResult;
type PruneLibrariesFn = fn(&Utf8Path, &str, &[CrateName]) -> Result<()>;
struct PrebuiltInstallationHooks {
detect_host_target: DetectHostTargetFn,
resolve_destination_dir: ResolveDestinationDirFn,
attempt_prebuilt: AttemptPrebuiltFn,
prune_prebuilt_libraries: PruneLibrariesFn,
}
fn try_prebuilt_installation_with(
context: &PrebuiltInstallationContext<'_>,
stderr: &mut dyn Write,
hooks: PrebuiltInstallationHooks,
) -> Result<Option<Utf8PathBuf>> {
let PrebuiltInstallationHooks {
detect_host_target,
resolve_destination_dir,
attempt_prebuilt,
prune_prebuilt_libraries,
} = hooks;
if !context
.args
.should_attempt_prebuilt(context.requested_crates)
{
return Ok(None);
}
let host_target = match detect_host_target() {
Ok(target) => target,
Err(error) => {
write_prebuilt_fallback_message(context.args.quiet, &error, stderr);
return Ok(None);
}
};
let destination_dir =
match resolve_destination_dir(context.dirs, context.toolchain_channel, &host_target) {
Ok(destination) => destination,
Err(error) => {
write_prebuilt_fallback_message(context.args.quiet, &error, stderr);
return Ok(None);
}
};
let prebuilt_config = PrebuiltConfig {
target: &host_target,
toolchain: context.toolchain_channel,
destination_dir: &destination_dir,
quiet: context.args.quiet,
};
let PrebuiltResult::Success { staging_path } = attempt_prebuilt(&prebuilt_config, stderr)
else {
return Ok(None);
};
if let Err(error) = prune_prebuilt_libraries(
&staging_path,
context.toolchain_channel,
context.requested_crates,
) {
write_prebuilt_fallback_message(context.args.quiet, &error, stderr);
return Ok(None);
}
Ok(Some(staging_path))
}
fn requested_crate_names(requested_crates: &[CrateName]) -> HashSet<&str> {
if requested_crates.is_empty() {
return HashSet::from([SUITE_CRATE]);
}
requested_crates.iter().map(CrateName::as_str).collect()
}
fn staged_library_filename(crate_name: &str, toolchain_channel: &str) -> String {
let normalized = crate_name.replace('-', "_");
format!(
"{}{}@{}{}",
library_prefix(),
normalized,
toolchain_channel,
library_extension()
)
}
fn prune_prebuilt_libraries(
staging_path: &Utf8Path,
toolchain_channel: &str,
requested_crates: &[CrateName],
) -> Result<()> {
let requested = requested_crate_names(requested_crates);
let known_crates = LINT_CRATES
.iter()
.copied()
.chain(EXPERIMENTAL_LINT_CRATES.iter().copied())
.chain(std::iter::once(SUITE_CRATE));
for crate_name in known_crates {
if requested.contains(crate_name) {
continue;
}
let stale_path = staging_path.join(staged_library_filename(crate_name, toolchain_channel));
match fs::remove_file(stale_path.as_std_path()) {
Ok(()) => {}
Err(error) if error.kind() == io::ErrorKind::NotFound => {}
Err(error) => {
return Err(InstallerError::StagingFailed {
reason: format!(
"failed to remove stale prebuilt library {stale_path}: {error}"
),
});
}
}
}
Ok(())
}
pub(crate) fn detect_host_target() -> Result<String> {
let output = std::process::Command::new("rustc")
.args(["-vV"])
.output()
.map_err(|error| InstallerError::ToolchainDetection {
reason: format!("failed to run `rustc -vV`: {error}"),
})?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(InstallerError::ToolchainDetection {
reason: format!(
"`rustc -vV` exited with {}: {}",
output.status,
stderr.trim()
),
});
}
let stdout = String::from_utf8_lossy(&output.stdout);
for line in stdout.lines() {
if let Some(host) = line.strip_prefix("host: ") {
return Ok(host.trim().to_owned());
}
}
Err(InstallerError::ToolchainDetection {
reason: "could not determine host target from `rustc -vV`".to_owned(),
})
}
pub(crate) fn write_install_metrics(context: &MetricsWriteContext<'_>, stderr: &mut dyn Write) {
match record_install(context.dirs, context.install_mode, context.elapsed) {
Ok(record_outcome) => write_metrics_summary(context.quiet, &record_outcome, stderr),
Err(error) => {
if !context.quiet {
write_stderr_line(
stderr,
format!("Warning: could not record install metrics: {error}"),
);
}
}
}
}
fn write_metrics_summary(quiet: bool, record_outcome: &RecordOutcome, stderr: &mut dyn Write) {
if quiet {
return;
}
if record_outcome.recovered_from_corrupt_file() {
write_stderr_line(
stderr,
"Install metrics file was invalid and has been reset.",
);
}
write_stderr_line(stderr, record_outcome.metrics().summary_line());
}
#[cfg(test)]
mod tests;