mod install_flow;
mod staged_suite;
#[cfg(test)]
use crate::install_flow::ensure_dylint_tools_with_options;
use crate::install_flow::{
MetricsWriteContext, PrebuiltInstallationContext, detect_host_target,
ensure_dylint_tools_with_executor, try_prebuilt_installation, write_install_metrics,
};
use camino::{Utf8Path, Utf8PathBuf};
use clap::Parser;
use std::io::Write;
use std::time::Instant;
use whitaker_installer::cli::{Cli, Command, InstallArgs};
use whitaker_installer::crate_name::CrateName;
use whitaker_installer::deps::SystemCommandExecutor;
use whitaker_installer::dirs::{BaseDirs, SystemBaseDirs};
use whitaker_installer::error::{InstallerError, Result};
use whitaker_installer::install_metrics::InstallMode;
use whitaker_installer::list::{determine_target_dir, run_list};
use whitaker_installer::output::{DryRunInfo, ShellSnippet, write_stderr_line};
use whitaker_installer::pipeline::{PipelineContext, perform_build, stage_libraries};
use whitaker_installer::prebuilt_path::prebuilt_library_dir;
use whitaker_installer::resolution::{
CrateResolutionOptions, resolve_crates, validate_crate_names,
};
use whitaker_installer::toolchain::Toolchain;
use whitaker_installer::wrapper::{generate_wrapper_scripts, path_instructions};
fn main() {
let cli = Cli::parse();
let mut stdout = std::io::stdout();
let mut stderr = std::io::stderr();
let run_result = run(&cli, &mut stdout, &mut stderr);
let exit_code = exit_code_for_run_result(run_result, &mut stderr);
if exit_code != 0 {
std::process::exit(exit_code);
}
}
fn run(cli: &Cli, stdout: &mut dyn Write, stderr: &mut dyn Write) -> Result<()> {
match &cli.command {
Some(Command::List(args)) => run_list(args, stdout),
Some(Command::Install(args)) => run_install(args, stderr),
None => run_install(cli.install_args(), stderr),
}
}
fn run_install(args: &InstallArgs, stderr: &mut dyn Write) -> Result<()> {
let dirs = SystemBaseDirs::new().ok_or_else(|| InstallerError::WorkspaceNotFound {
reason: "could not determine platform directories".to_owned(),
})?;
if args.dry_run {
return run_dry(args, &dirs, stderr);
}
let install_started = Instant::now();
if !args.skip_deps {
ensure_dylint_tools(args.quiet, stderr)?;
}
let workspace_root = ensure_whitaker_workspace(args, &dirs, stderr)?;
let requested_crates = resolve_requested_crates(args)?;
let toolchain = resolve_toolchain(&workspace_root, args.toolchain.as_deref())?;
ensure_toolchain_installed(&toolchain, args.quiet, stderr)?;
let target_dir = determine_target_dir(args.target_dir.as_deref())?;
let prebuilt_context = PrebuiltInstallationContext {
args,
dirs: &dirs,
requested_crates: &requested_crates,
toolchain_channel: toolchain.channel(),
};
if let Some(staging_path) = try_prebuilt_installation(&prebuilt_context, stderr)? {
let finish_context = FinishInstallContext {
args,
dirs: &dirs,
staging_path: &staging_path,
install_mode: InstallMode::Download,
install_started,
};
return finish_install_and_record_metrics(&finish_context, stderr);
}
if let Some(staging_path) = staged_suite::try_test_staged_suite_installation(
&requested_crates,
&toolchain,
&target_dir,
)? {
let finish_context = FinishInstallContext {
args,
dirs: &dirs,
staging_path: &staging_path,
install_mode: InstallMode::Build,
install_started,
};
return finish_install_and_record_metrics(&finish_context, stderr);
}
let context = PipelineContext {
workspace_root: &workspace_root,
toolchain: &toolchain,
target_dir: &target_dir,
jobs: args.jobs,
verbosity: args.verbosity,
experimental: args.experimental,
quiet: args.quiet,
};
let build_results = perform_build(&context, &requested_crates, stderr)?;
let staging_path = stage_libraries(&context, &build_results, stderr)?;
let finish_context = FinishInstallContext {
args,
dirs: &dirs,
staging_path: &staging_path,
install_mode: InstallMode::Build,
install_started,
};
finish_install_and_record_metrics(&finish_context, stderr)
}
fn run_dry(args: &InstallArgs, dirs: &dyn BaseDirs, stderr: &mut dyn Write) -> Result<()> {
use whitaker_installer::workspace::resolve_workspace_path;
let workspace_root = resolve_workspace_path(dirs)?;
let requested_crates = resolve_requested_crates(args)?;
let toolchain = resolve_toolchain(&workspace_root, args.toolchain.as_deref())?;
toolchain.verify_installed()?;
let target_dir = determine_dry_run_target_dir(args, dirs, &toolchain, &requested_crates)?;
let info = DryRunInfo {
workspace_root: &workspace_root,
toolchain: toolchain.channel(),
target_dir: &target_dir,
verbosity: args.verbosity,
quiet: args.quiet,
skip_deps: args.skip_deps,
skip_wrapper: args.skip_wrapper,
no_update: args.no_update,
jobs: args.jobs,
crates: &requested_crates,
};
write_stderr_line(stderr, info.display_text());
Ok(())
}
fn determine_dry_run_target_dir(
args: &InstallArgs,
dirs: &dyn BaseDirs,
toolchain: &Toolchain,
requested_crates: &[CrateName],
) -> Result<Utf8PathBuf> {
let build_target_dir = determine_target_dir(args.target_dir.as_deref())?;
if !args.should_attempt_prebuilt(requested_crates) {
return Ok(build_target_dir);
}
let Ok(host_target) = detect_host_target() else {
return Ok(build_target_dir);
};
Ok(prebuilt_library_dir(dirs, toolchain.channel(), &host_target).unwrap_or(build_target_dir))
}
fn ensure_dylint_tools(quiet: bool, stderr: &mut dyn Write) -> Result<()> {
let executor = SystemCommandExecutor;
ensure_dylint_tools_with_executor(&executor, quiet, stderr)
}
fn ensure_whitaker_workspace(
args: &InstallArgs,
dirs: &dyn BaseDirs,
stderr: &mut dyn Write,
) -> Result<Utf8PathBuf> {
use whitaker_installer::workspace::{
WorkspaceAction, clone_directory, decide_workspace_action, ensure_workspace,
};
if !args.quiet
&& let Some(clone_dir) = clone_directory(dirs)
{
let cwd = std::env::current_dir()
.ok()
.and_then(|p| Utf8PathBuf::try_from(p).ok());
let Some(cwd) = cwd else {
return ensure_workspace(dirs, !args.no_update);
};
match decide_workspace_action(&cwd, &clone_dir, !args.no_update) {
WorkspaceAction::CloneTo(dir) => {
write_stderr_line(stderr, format!("Cloning Whitaker repository to {dir}..."));
}
WorkspaceAction::UpdateAt(dir) => {
write_stderr_line(stderr, format!("Updating Whitaker repository at {dir}..."));
}
WorkspaceAction::UseCurrentDir(_) | WorkspaceAction::UseExisting(_) => {}
}
}
ensure_workspace(dirs, !args.no_update)
}
fn resolve_toolchain(
workspace_root: &Utf8Path,
override_channel: Option<&str>,
) -> Result<Toolchain> {
match override_channel {
Some(channel) => Ok(Toolchain::with_override(workspace_root, channel)),
None => Toolchain::detect(workspace_root),
}
}
fn ensure_toolchain_installed(
toolchain: &Toolchain,
quiet: bool,
stderr: &mut dyn Write,
) -> Result<()> {
let status = toolchain.ensure_installed()?;
if status.installed_toolchain() && !quiet {
write_stderr_line(
stderr,
format!("Toolchain {} installed successfully.", toolchain.channel()),
);
write_stderr_line(stderr, "");
}
Ok(())
}
fn finish_install(
args: &InstallArgs,
dirs: &dyn BaseDirs,
staging_path: &Utf8Path,
stderr: &mut dyn Write,
) -> Result<()> {
if args.skip_wrapper {
write_stderr_line(stderr, "");
write_stderr_line(stderr, ShellSnippet::new(staging_path).display_text());
} else {
generate_and_report_wrapper(dirs, staging_path, stderr)?;
}
Ok(())
}
struct FinishInstallContext<'a> {
args: &'a InstallArgs,
dirs: &'a dyn BaseDirs,
staging_path: &'a Utf8Path,
install_mode: InstallMode,
install_started: Instant,
}
fn finish_install_and_record_metrics(
context: &FinishInstallContext<'_>,
stderr: &mut dyn Write,
) -> Result<()> {
finish_install(context.args, context.dirs, context.staging_path, stderr)?;
let metrics_context = MetricsWriteContext {
quiet: context.args.quiet,
dirs: context.dirs,
install_mode: context.install_mode,
elapsed: context.install_started.elapsed(),
};
write_install_metrics(&metrics_context, stderr);
Ok(())
}
fn resolve_requested_crates(args: &InstallArgs) -> Result<Vec<CrateName>> {
let lint_crates: Vec<CrateName> = args
.lint
.iter()
.map(|name| CrateName::from(name.as_str()))
.collect();
if !lint_crates.is_empty() {
validate_crate_names(&lint_crates)?;
}
let options = CrateResolutionOptions {
individual_lints: args.individual_lints,
experimental: args.experimental,
};
Ok(resolve_crates(&lint_crates, &options))
}
fn generate_and_report_wrapper(
dirs: &dyn BaseDirs,
staging_path: &Utf8Path,
stderr: &mut dyn Write,
) -> Result<()> {
let result = generate_wrapper_scripts(dirs, staging_path)?;
write_stderr_line(stderr, "");
write_stderr_line(stderr, "Wrapper scripts created:");
write_stderr_line(stderr, format!(" - {}", result.whitaker_path.display()));
write_stderr_line(stderr, format!(" - {}", result.whitaker_ls_path.display()));
write_stderr_line(stderr, "");
if result.in_path {
write_stderr_line(stderr, "You can now run:");
write_stderr_line(stderr, " whitaker --all");
write_stderr_line(stderr, " whitaker-ls");
} else {
let bin_dir =
result
.whitaker_path
.parent()
.ok_or_else(|| InstallerError::StagingFailed {
reason: "wrapper script path has no parent directory".to_owned(),
})?;
write_stderr_line(stderr, path_instructions(bin_dir));
write_stderr_line(stderr, "");
write_stderr_line(stderr, "Then run:");
write_stderr_line(stderr, " whitaker --all");
write_stderr_line(stderr, " whitaker-ls");
}
Ok(())
}
fn exit_code_for_run_result(result: Result<()>, stderr: &mut dyn Write) -> i32 {
match result {
Ok(()) => 0,
Err(err) => {
write_stderr_line(stderr, err);
1
}
}
}
#[cfg(test)]
mod tests;