use crate::crate_name::CrateName;
use camino::Utf8PathBuf;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum InstallerError {
#[error("toolchain detection failed: {reason}")]
ToolchainDetection {
reason: String,
},
#[error("rust-toolchain.toml not found at {path}")]
ToolchainFileNotFound {
path: Utf8PathBuf,
},
#[error("invalid rust-toolchain.toml: {reason}")]
InvalidToolchainFile {
reason: String,
},
#[error("toolchain {toolchain} not installed; run: rustup toolchain install {toolchain}")]
ToolchainNotInstalled {
toolchain: String,
},
#[error("toolchain {toolchain} installation failed: {message}")]
ToolchainInstallFailed {
toolchain: String,
message: String,
},
#[error("toolchain {toolchain} component install failed for {components}: {message}")]
ToolchainComponentInstallFailed {
toolchain: String,
components: String,
message: String,
},
#[error("cargo build failed for {crate_name}: {reason}")]
BuildFailed {
crate_name: CrateName,
reason: String,
},
#[error("staging failed: {reason}")]
StagingFailed {
reason: String,
},
#[error("target directory {path} is not writable: {reason}")]
TargetNotWritable {
path: Utf8PathBuf,
reason: String,
},
#[error("lint crate {name} not found in workspace")]
LintCrateNotFound {
name: CrateName,
},
#[error("workspace not found: {reason}")]
WorkspaceNotFound {
reason: String,
},
#[error("invalid Cargo.toml at {path}: {reason}")]
InvalidCargoToml {
path: camino::Utf8PathBuf,
reason: String,
},
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("git {operation} failed: {message}")]
Git {
operation: &'static str,
message: String,
},
#[error("failed to install {tool}: {message}")]
DependencyInstall {
tool: &'static str,
message: String,
},
#[error("wrapper script generation failed: {0}")]
WrapperGeneration(String),
#[error("failed to scan staging directory")]
ScanFailed {
#[source]
source: std::io::Error,
},
#[error("failed to write output")]
WriteFailed {
#[source]
source: std::io::Error,
},
#[cfg(any(test, feature = "test-support"))]
#[error("stub mismatch: {message}")]
StubMismatch {
message: String,
},
}
impl Clone for InstallerError {
fn clone(&self) -> Self {
fn clone_io_error(source: &std::io::Error) -> std::io::Error {
std::io::Error::new(source.kind(), source.to_string())
}
match self {
InstallerError::ToolchainDetection { reason } => InstallerError::ToolchainDetection {
reason: reason.clone(),
},
InstallerError::ToolchainFileNotFound { path } => {
InstallerError::ToolchainFileNotFound { path: path.clone() }
}
InstallerError::InvalidToolchainFile { reason } => {
InstallerError::InvalidToolchainFile {
reason: reason.clone(),
}
}
InstallerError::ToolchainNotInstalled { toolchain } => {
InstallerError::ToolchainNotInstalled {
toolchain: toolchain.clone(),
}
}
InstallerError::ToolchainInstallFailed { toolchain, message } => {
InstallerError::ToolchainInstallFailed {
toolchain: toolchain.clone(),
message: message.clone(),
}
}
InstallerError::ToolchainComponentInstallFailed {
toolchain,
components,
message,
} => InstallerError::ToolchainComponentInstallFailed {
toolchain: toolchain.clone(),
components: components.clone(),
message: message.clone(),
},
InstallerError::BuildFailed { crate_name, reason } => InstallerError::BuildFailed {
crate_name: crate_name.clone(),
reason: reason.clone(),
},
InstallerError::StagingFailed { reason } => InstallerError::StagingFailed {
reason: reason.clone(),
},
InstallerError::TargetNotWritable { path, reason } => {
InstallerError::TargetNotWritable {
path: path.clone(),
reason: reason.clone(),
}
}
InstallerError::LintCrateNotFound { name } => {
InstallerError::LintCrateNotFound { name: name.clone() }
}
InstallerError::WorkspaceNotFound { reason } => InstallerError::WorkspaceNotFound {
reason: reason.clone(),
},
InstallerError::InvalidCargoToml { path, reason } => InstallerError::InvalidCargoToml {
path: path.clone(),
reason: reason.clone(),
},
InstallerError::Io(source) => InstallerError::Io(clone_io_error(source)),
InstallerError::Git { operation, message } => InstallerError::Git {
operation,
message: message.clone(),
},
InstallerError::DependencyInstall { tool, message } => {
InstallerError::DependencyInstall {
tool,
message: message.clone(),
}
}
InstallerError::WrapperGeneration(message) => {
InstallerError::WrapperGeneration(message.clone())
}
InstallerError::ScanFailed { source } => InstallerError::ScanFailed {
source: clone_io_error(source),
},
InstallerError::WriteFailed { source } => InstallerError::WriteFailed {
source: clone_io_error(source),
},
#[cfg(any(test, feature = "test-support"))]
InstallerError::StubMismatch { message } => InstallerError::StubMismatch {
message: message.clone(),
},
}
}
}
pub type Result<T> = std::result::Result<T, InstallerError>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn toolchain_not_installed_suggests_install_command() {
let err = InstallerError::ToolchainNotInstalled {
toolchain: "nightly-2025-09-18".to_owned(),
};
let msg = err.to_string();
assert!(msg.contains("rustup toolchain install"));
assert!(msg.contains("nightly-2025-09-18"));
}
#[test]
fn toolchain_install_failed_includes_toolchain_and_message() {
let err = InstallerError::ToolchainInstallFailed {
toolchain: "nightly-2025-09-18".to_owned(),
message: "network error".to_owned(),
};
let msg = err.to_string();
assert!(msg.contains("nightly-2025-09-18"));
assert!(msg.contains("network error"));
}
#[test]
fn toolchain_component_install_failed_includes_components() {
let err = InstallerError::ToolchainComponentInstallFailed {
toolchain: "nightly-2025-09-18".to_owned(),
components: "rust-src, rustc-dev".to_owned(),
message: "component error".to_owned(),
};
let msg = err.to_string();
assert!(msg.contains("nightly-2025-09-18"));
assert!(msg.contains("rust-src, rustc-dev"));
assert!(msg.contains("component error"));
}
#[test]
fn build_failed_includes_crate_name() {
let err = InstallerError::BuildFailed {
crate_name: CrateName::from("module_max_lines"),
reason: "compilation error".to_owned(),
};
let msg = err.to_string();
assert!(msg.contains("module_max_lines"));
assert!(msg.contains("compilation error"));
}
#[test]
fn git_error_includes_operation_and_message() {
let err = InstallerError::Git {
operation: "clone",
message: "network error".to_owned(),
};
let msg = err.to_string();
assert!(msg.contains("clone"));
assert!(msg.contains("network error"));
}
#[test]
fn dependency_install_error_includes_tool_name() {
let err = InstallerError::DependencyInstall {
tool: "cargo-dylint",
message: "network error".to_owned(),
};
let msg = err.to_string();
assert!(msg.contains("cargo-dylint"));
assert!(msg.contains("network error"));
}
#[test]
fn wrapper_generation_error_includes_message() {
let err = InstallerError::WrapperGeneration("permission denied".to_owned());
let msg = err.to_string();
assert!(msg.contains("permission denied"));
}
#[test]
fn scan_failed_includes_reason() {
let source = std::io::Error::other("directory not found");
let err = InstallerError::ScanFailed { source };
let msg = err.to_string();
assert!(msg.contains("scan"));
let source_err = std::error::Error::source(&err);
assert!(source_err.is_some());
}
#[test]
fn write_failed_includes_reason() {
let source = std::io::Error::other("permission denied");
let err = InstallerError::WriteFailed { source };
let msg = err.to_string();
assert!(msg.contains("write"));
let source_err = std::error::Error::source(&err);
assert!(source_err.is_some());
}
}