pub mod availability;
pub mod fitness;
use crate::handlers::{HANDLER_HOMEBREW, HANDLER_INSTALL, HANDLER_NIX};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ManifestArgPosition(usize);
impl ManifestArgPosition {
pub const fn at(index: usize) -> Self {
Self(index)
}
pub const fn index(self) -> usize {
self.0
}
pub fn resolve(self, arguments: &[String]) -> Option<&str> {
arguments.get(self.0).map(String::as_str)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExecutableLocation {
Candidates(&'static [CandidatePath]),
Path,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CandidatePath {
Absolute(&'static str),
UnderHome(&'static str),
UnderEnv {
var: &'static str,
suffix: &'static str,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ProvisionerDescriptor {
pub handler: &'static str,
pub env: &'static [(&'static str, &'static str)],
pub manifest_arg: ManifestArgPosition,
pub location: ExecutableLocation,
pub project_url: Option<&'static str>,
pub version_floor: Option<fitness::VersionFloor>,
}
pub const HOMEBREW_CANDIDATES: &[CandidatePath] = &[
CandidatePath::UnderEnv {
var: "HOMEBREW_PREFIX",
suffix: "bin/brew",
},
CandidatePath::Absolute("/opt/homebrew/bin/brew"),
CandidatePath::Absolute("/home/linuxbrew/.linuxbrew/bin/brew"),
CandidatePath::Absolute("/usr/local/bin/brew"),
CandidatePath::UnderHome(".linuxbrew/bin/brew"),
];
pub const NIX_CANDIDATES: &[CandidatePath] = &[
CandidatePath::UnderHome(".nix-profile/bin/nix"),
CandidatePath::Absolute("/nix/var/nix/profiles/default/bin/nix"),
CandidatePath::UnderHome(".local/state/nix/profiles/profile/bin/nix"),
CandidatePath::Absolute("/run/current-system/sw/bin/nix"),
];
pub const HOMEBREW_PROJECT_URL: &str = "https://brew.sh";
pub const NIX_PROJECT_URL: &str = "https://nixos.org/download";
pub const PROVISIONERS: &[ProvisionerDescriptor] = &[
ProvisionerDescriptor {
handler: HANDLER_INSTALL,
env: &[],
manifest_arg: ManifestArgPosition::at(1),
location: ExecutableLocation::Path,
project_url: None,
version_floor: None,
},
ProvisionerDescriptor {
handler: HANDLER_HOMEBREW,
env: &[("HOMEBREW_NO_AUTO_UPDATE", "1")],
manifest_arg: ManifestArgPosition::at(3),
location: ExecutableLocation::Candidates(HOMEBREW_CANDIDATES),
project_url: Some(HOMEBREW_PROJECT_URL),
version_floor: Some(fitness::HOMEBREW_VERSION_FLOOR),
},
ProvisionerDescriptor {
handler: HANDLER_NIX,
env: &[],
manifest_arg: ManifestArgPosition::at(7),
location: ExecutableLocation::Candidates(NIX_CANDIDATES),
project_url: Some(NIX_PROJECT_URL),
version_floor: None,
},
];
pub fn descriptor_for(handler: &str) -> Option<&'static ProvisionerDescriptor> {
PROVISIONERS.iter().find(|d| d.handler == handler)
}
pub fn manifest_argument<'a>(handler: &str, arguments: &'a [String]) -> Option<&'a str> {
descriptor_for(handler)?.manifest_arg.resolve(arguments)
}
pub fn environment_for(handler: &str) -> &'static [(&'static str, &'static str)] {
descriptor_for(handler).map_or(&[], |d| d.env)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::handlers::homebrew::BrewfileCommand;
use crate::handlers::install::InstallCommand;
use crate::handlers::nix::NixCommand;
use crate::handlers::run_once::RunOnceCommand;
use std::path::Path;
#[test]
fn registry_holds_one_row_per_provisioning_handler() {
let names: Vec<&str> = PROVISIONERS.iter().map(|d| d.handler).collect();
assert_eq!(names, vec![HANDLER_INSTALL, HANDLER_HOMEBREW, HANDLER_NIX]);
}
#[test]
fn only_homebrew_declares_an_environment() {
assert_eq!(environment_for(HANDLER_INSTALL), &[]);
assert_eq!(environment_for(HANDLER_NIX), &[]);
assert_eq!(
environment_for(HANDLER_HOMEBREW),
&[("HOMEBREW_NO_AUTO_UPDATE", "1")]
);
}
#[test]
fn a_handler_with_no_descriptor_declares_no_environment() {
assert_eq!(environment_for("symlink"), &[]);
assert_eq!(environment_for(""), &[]);
}
#[test]
fn descriptor_for_is_none_outside_provisioning() {
assert!(descriptor_for("symlink").is_none());
assert!(descriptor_for("shell").is_none());
assert!(descriptor_for("external").is_none());
assert!(descriptor_for("").is_none());
}
#[test]
fn descriptor_matches_command() {
let manifest = Path::new("/dotfiles/tools/manifest");
let cases: Vec<(&str, Vec<String>)> = vec![
(
HANDLER_INSTALL,
InstallCommand
.command_for(&manifest.with_file_name("install.sh"))
.1,
),
(
HANDLER_HOMEBREW,
BrewfileCommand
.command_for(&manifest.with_file_name("Brewfile"))
.1,
),
(
HANDLER_NIX,
NixCommand
.command_for(&manifest.with_file_name("packages.nix"))
.1,
),
];
for (handler, arguments) in cases {
let expected = manifest
.with_file_name(match handler {
HANDLER_INSTALL => "install.sh",
HANDLER_HOMEBREW => "Brewfile",
_ => "packages.nix",
})
.to_string_lossy()
.into_owned();
assert_eq!(
manifest_argument(handler, &arguments),
Some(expected.as_str()),
"descriptor for {handler} does not point at the manifest in {arguments:?}"
);
}
}
#[test]
fn descriptor_environment_reaches_each_command() {
assert!(InstallCommand.environment().is_empty());
assert!(NixCommand.environment().is_empty());
assert_eq!(
BrewfileCommand.environment(),
vec![("HOMEBREW_NO_AUTO_UPDATE".to_string(), "1".to_string())]
);
}
#[test]
fn nix_manifest_is_not_the_last_argument() {
let (_, arguments) = NixCommand.command_for(Path::new("/dotfiles/tools/packages.nix"));
assert_ne!(
arguments.last().map(String::as_str),
Some("/dotfiles/tools/packages.nix")
);
assert_eq!(
manifest_argument(HANDLER_NIX, &arguments),
Some("/dotfiles/tools/packages.nix")
);
}
#[test]
fn homebrew_candidates_track_the_bootstrap_prefixes() {
use crate::shell::homebrew::{DEFAULT_PREFIXES, HOME_RELATIVE_PREFIX};
let mut expected: Vec<String> = vec!["$HOMEBREW_PREFIX/bin/brew".to_string()];
expected.extend(DEFAULT_PREFIXES.iter().map(|p| format!("{p}/bin/brew")));
expected.push(format!("~/{HOME_RELATIVE_PREFIX}/bin/brew"));
let actual: Vec<String> = HOMEBREW_CANDIDATES
.iter()
.map(|c| match c {
CandidatePath::Absolute(p) => (*p).to_string(),
CandidatePath::UnderHome(suffix) => format!("~/{suffix}"),
CandidatePath::UnderEnv { var, suffix } => format!("${var}/{suffix}"),
})
.collect();
assert_eq!(actual, expected);
}
#[test]
fn every_probed_candidate_is_absolute_or_anchored() {
for descriptor in PROVISIONERS {
let ExecutableLocation::Candidates(candidates) = descriptor.location else {
continue;
};
assert!(
!candidates.is_empty(),
"{} declares Candidates with an empty list, which probes as present",
descriptor.handler
);
for candidate in candidates {
match candidate {
CandidatePath::Absolute(p) => {
assert!(Path::new(p).is_absolute(), "{p} is not an absolute path")
}
CandidatePath::UnderHome(suffix) | CandidatePath::UnderEnv { suffix, .. } => {
assert!(
!Path::new(suffix).is_absolute(),
"{suffix} is anchored and must not also be absolute"
)
}
}
}
}
}
#[test]
fn every_probed_provisioner_names_its_project_page() {
for descriptor in PROVISIONERS {
match descriptor.location {
ExecutableLocation::Candidates(_) => assert!(
descriptor.project_url.is_some(),
"{} can report absence and must name where to get it",
descriptor.handler
),
ExecutableLocation::Path => {
assert_eq!(descriptor.project_url, None, "{}", descriptor.handler)
}
}
}
}
#[test]
fn install_is_the_path_exception() {
assert_eq!(
descriptor_for(HANDLER_INSTALL).unwrap().location,
ExecutableLocation::Path
);
}
#[test]
fn short_arguments_resolve_to_none() {
let arguments = vec!["--".to_string()];
assert_eq!(manifest_argument(HANDLER_INSTALL, &arguments), None);
assert_eq!(manifest_argument(HANDLER_INSTALL, &[]), None);
}
}