use crate::dependency_binaries::{
DependencyBinaryInstaller, RepositoryDependencyBinaryInstaller, find_dependency_binary,
host_target,
};
use crate::dirs::{BaseDirs, SystemBaseDirs};
use crate::error::{InstallerError, Result};
use std::io;
use std::io::Write;
use std::path::Path;
use std::process::{Command, Output};
mod install;
use install::*;
pub trait CommandExecutor {
fn run(&self, cmd: &str, args: &[&str]) -> Result<Output>;
}
#[derive(Debug, Clone, Copy, Default)]
pub struct SystemCommandExecutor;
impl CommandExecutor for SystemCommandExecutor {
fn run(&self, cmd: &str, args: &[&str]) -> Result<Output> {
Command::new(cmd)
.args(args)
.output()
.map_err(InstallerError::from)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct DependencyTool {
package: &'static str,
command: &'static str,
args: &'static [&'static str],
}
const CARGO_DYLINT_TOOL: DependencyTool = DependencyTool {
package: "cargo-dylint",
command: "cargo",
args: &["dylint", "--version"],
};
const DYLINT_LINK_TOOL: DependencyTool = DependencyTool {
package: "dylint-link",
command: "dylint-link",
args: &["--version"],
};
const DEPENDENCY_TOOLS: [DependencyTool; 2] = [CARGO_DYLINT_TOOL, DYLINT_LINK_TOOL];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DylintToolStatus {
pub cargo_dylint: bool,
pub dylint_link: bool,
}
impl DylintToolStatus {
#[must_use]
pub fn all_installed(&self) -> bool {
self.cargo_dylint && self.dylint_link
}
}
#[cfg(any(test, feature = "test-support"))]
pub struct DependencyInstallOptions<'a> {
pub dirs: &'a dyn BaseDirs,
pub repository_installer: &'a dyn DependencyBinaryInstaller,
pub target: Option<crate::installer_packaging::TargetTriple>,
pub quiet: bool,
}
#[must_use]
pub fn check_dylint_tools(executor: &dyn CommandExecutor) -> DylintToolStatus {
DylintToolStatus {
cargo_dylint: is_tool_installed(executor, &CARGO_DYLINT_TOOL),
dylint_link: is_tool_installed(executor, &DYLINT_LINK_TOOL),
}
}
pub fn install_dylint_tools(
executor: &dyn CommandExecutor,
status: &DylintToolStatus,
) -> Result<()> {
let mut sink = io::sink();
install_dylint_tools_with_output(executor, status, true, &mut sink)
}
pub fn install_dylint_tools_with_output(
executor: &dyn CommandExecutor,
status: &DylintToolStatus,
quiet: bool,
stderr: &mut dyn Write,
) -> Result<()> {
let repository_installer = RepositoryDependencyBinaryInstaller;
let system_dirs = SystemBaseDirs::new();
let target = host_target();
let dirs = system_dirs.as_ref().map(|dirs| dirs as &dyn BaseDirs);
let cargo_fallback_mode = cargo_fallback_mode(executor);
install_missing_tools(
executor,
status,
stderr,
&InstallContext {
repo: repository_install_context(
dirs,
Some(&repository_installer as &dyn DependencyBinaryInstaller),
target.as_ref(),
),
cargo_fallback_mode,
quiet,
},
)
}
#[cfg(any(test, feature = "test-support"))]
#[doc(hidden)]
pub fn install_dylint_tools_with_options(
executor: &dyn CommandExecutor,
status: &DylintToolStatus,
stderr: &mut dyn Write,
options: DependencyInstallOptions<'_>,
) -> Result<()> {
let cargo_fallback_mode = cargo_fallback_mode(executor);
install_missing_tools(
executor,
status,
stderr,
&InstallContext {
repo: repository_install_context(
Some(options.dirs),
Some(options.repository_installer),
options.target.as_ref(),
),
cargo_fallback_mode,
quiet: options.quiet,
},
)
}
const CARGO_INSTALL_LIST_ARGS: [&str; 2] = ["install", "--list"];
fn is_tool_installed(executor: &dyn CommandExecutor, tool: &DependencyTool) -> bool {
let expected_version = find_dependency_binary(tool.package)
.ok()
.flatten()
.map(|dependency| dependency.version());
if tool == &DYLINT_LINK_TOOL {
return is_dylint_link_installed(executor, expected_version);
}
is_versioned_tool_installed(executor, tool, expected_version)
}
fn is_dylint_link_installed(
executor: &dyn CommandExecutor,
expected_version: Option<&str>,
) -> bool {
if find_binary_on_path(DYLINT_LINK_TOOL.command).is_none() {
return false;
}
let Some(expected_version) = expected_version else {
return true;
};
cargo_installed_version(executor, DYLINT_LINK_TOOL.package)
.is_some_and(|version| version == expected_version)
}
fn is_versioned_tool_installed(
executor: &dyn CommandExecutor,
tool: &DependencyTool,
expected_version: Option<&str>,
) -> bool {
let Some(expected_version) = expected_version else {
return command_succeeds(executor, tool.command, tool.args);
};
executor.run(tool.command, tool.args).is_ok_and(|output| {
output.status.success()
&& first_semver_token(&String::from_utf8_lossy(&output.stdout))
.is_some_and(|version| version == expected_version)
})
}
fn cargo_installed_version(executor: &dyn CommandExecutor, package: &str) -> Option<String> {
let output = executor.run("cargo", &CARGO_INSTALL_LIST_ARGS).ok()?;
if !output.status.success() {
return None;
}
let stdout = String::from_utf8_lossy(&output.stdout);
stdout.lines().find_map(|line| {
let mut tokens = line.split_whitespace();
if tokens.next() != Some(package) {
return None;
}
tokens
.next()
.and_then(|token| token.strip_prefix('v'))
.map(|version| version.trim_end_matches(':').to_owned())
})
}
fn first_semver_token(text: &str) -> Option<&str> {
text.split_whitespace().find(|token| {
let components: Vec<&str> = token.split('.').collect();
components.len() == 3
&& components.iter().all(|component| {
!component.is_empty()
&& component
.chars()
.all(|character| character.is_ascii_digit())
})
})
}
fn is_binstall_available(executor: &dyn CommandExecutor) -> bool {
command_succeeds(executor, "cargo", &["binstall", "--version"])
}
#[cfg(test)]
fn is_binary_on_path(binary_name: &str) -> bool {
find_binary_on_path(binary_name).is_some()
}
fn find_binary_on_path(binary_name: &str) -> Option<std::path::PathBuf> {
let path_var = std::env::var_os("PATH")?;
std::env::split_paths(&path_var)
.find_map(|directory| find_binary_in_directory(&directory, binary_name))
}
fn find_binary_in_directory(directory: &Path, binary_name: &str) -> Option<std::path::PathBuf> {
binary_candidates(directory, binary_name)
.into_iter()
.find(|candidate| is_executable_file(candidate))
}
fn binary_candidates(directory: &Path, binary_name: &str) -> Vec<std::path::PathBuf> {
#[cfg(windows)]
let mut candidates = Vec::new();
#[cfg(not(windows))]
let candidates = vec![directory.join(binary_name)];
#[cfg(windows)]
{
if Path::new(binary_name).extension().is_some() {
candidates.push(directory.join(binary_name));
} else {
let lowercase_name = binary_name.to_ascii_lowercase();
candidates.extend(
windows_path_extensions()
.into_iter()
.filter(|extension| !lowercase_name.ends_with(&extension.to_ascii_lowercase()))
.map(|extension| directory.join(format!("{binary_name}{extension}"))),
);
}
}
candidates
}
#[cfg(windows)]
fn windows_path_extensions() -> Vec<String> {
let path_ext = std::env::var_os("PATHEXT")
.filter(|value| !value.is_empty())
.unwrap_or_else(|| std::ffi::OsString::from(".COM;.EXE;.BAT;.CMD"));
path_ext
.to_string_lossy()
.split(';')
.filter_map(|extension| {
let trimmed = extension.trim();
if trimmed.is_empty() {
None
} else if trimmed.starts_with('.') {
Some(trimmed.to_owned())
} else {
Some(format!(".{trimmed}"))
}
})
.collect()
}
#[cfg(unix)]
fn is_executable_file(path: &Path) -> bool {
use std::os::unix::fs::PermissionsExt;
std::fs::metadata(path)
.map(|metadata| metadata.is_file() && metadata.permissions().mode() & 0o111 != 0)
.unwrap_or(false)
}
#[cfg(not(unix))]
fn is_executable_file(path: &Path) -> bool {
path.is_file()
}
#[cfg(test)]
mod path_tests;
#[cfg(test)]
mod tests;