pub(crate) mod condition;
mod error;
mod git;
pub(crate) mod pkg;
mod pkg_config_files;
pub(crate) mod shell;
mod single_key_de;
pub(crate) mod ssh;
mod stored_relative_path;
mod user;
pub use error::Error as ConfigError;
use std::{
path::{Path, PathBuf},
sync::Arc,
};
use indexmap::IndexMap;
use smol_str::SmolStr;
use xshell::cmd;
use zenops_safe_relative_path::srpath;
pub use crate::config::pkg::PkgConfig;
use crate::{
config::{
condition::{Condition, Conditions, HostContext},
git::StoredGitConfig,
pkg::{Shell, ShellInitAction},
shell::StoredShellEnvironment,
ssh::{CurlGithubKeyFetcher, StoredSshConfig},
user::StoredUserConfig,
},
config_files::{ConfigFileDirs, ConfigFilePath, ConfigFiles},
error::Error,
git::Git,
os::Os,
output::{Event, Output, PkgStatus, ResolvedConfigFilePath, Status},
pkg_manager,
utils::which::SearchPath,
};
#[derive(serde::Deserialize, schemars::JsonSchema, Debug, Clone, PartialEq, Default)]
#[serde(deny_unknown_fields, default)]
pub(crate) struct StoredConfig {
shell: StoredShellEnvironment,
pkg: IndexMap<SmolStr, PkgConfig>,
ssh: StoredSshConfig,
user: StoredUserConfig,
git: StoredGitConfig,
conditions: IndexMap<SmolStr, Condition>,
}
pub struct Config<'dirs> {
dirs: &'dirs ConfigFileDirs,
zenops_repo: ResolvedConfigFilePath,
stored: StoredConfig,
system_inputs: IndexMap<SmolStr, SmolStr>,
conditions: Conditions,
hostname: String,
path: SearchPath,
}
fn detect_brew_prefix() -> Result<Option<PathBuf>, Error> {
const CANDIDATES: &[&str] = &["/opt/homebrew", "/usr/local", "/home/linuxbrew/.linuxbrew"];
for prefix in CANDIDATES.iter().map(Path::new) {
let brew = prefix.join("bin/brew");
if brew
.try_exists()
.map_err(|e| ConfigError::BrewProbeFailed(brew.clone(), e))?
{
return Ok(Some(prefix.to_path_buf()));
}
}
Ok(None)
}
fn build_system_inputs(
brew_prefix: Option<&Path>,
user: &StoredUserConfig,
) -> IndexMap<SmolStr, SmolStr> {
let mut m = IndexMap::new();
if let Some(p) = brew_prefix {
m.insert(
SmolStr::new_static("brew_prefix"),
SmolStr::new(p.to_string_lossy()),
);
}
m.insert(
SmolStr::new_static("os"),
SmolStr::new_static(std::env::consts::OS),
);
if let Some(name) = &user.name {
m.insert(SmolStr::new_static("user.name"), name.clone());
}
if let Some(email) = &user.email {
m.insert(SmolStr::new_static("user.email"), email.clone());
}
m
}
static DEFAULT_PKGS: &[(&str, &str)] = &[
("brew-macos", include_str!("pkgs/brew-macos.toml")),
("brew-linux", include_str!("pkgs/brew-linux.toml")),
("bashrc-chain", include_str!("pkgs/bashrc-chain.toml")),
("local-bin", include_str!("pkgs/local-bin.toml")),
("brew-python", include_str!("pkgs/brew-python.toml")),
("cargo", include_str!("pkgs/cargo.toml")),
("bash-completion", include_str!("pkgs/bash-completion.toml")),
("zsh-completions", include_str!("pkgs/zsh-completions.toml")),
("sk", include_str!("pkgs/sk.toml")),
("starship", include_str!("pkgs/starship.toml")),
("zenops", include_str!("pkgs/zenops.toml")),
("llvm", include_str!("pkgs/llvm.toml")),
];
static BUILTIN_CONDITIONS: &str = include_str!("condition_builtins.toml");
fn deep_merge(base: &mut toml::Value, overlay: toml::Value) {
match (base, overlay) {
(toml::Value::Table(b), toml::Value::Table(o)) => {
for (k, v) in o {
deep_merge(
b.entry(k).or_insert(toml::Value::Table(Default::default())),
v,
);
}
}
(base, overlay) => *base = overlay,
}
}
impl<'dirs> Config<'dirs> {
pub fn load(
dirs: &'dirs ConfigFileDirs,
path: SearchPath,
sh: &xshell::Shell,
update_self: bool,
) -> Result<Self, Error> {
if update_self {
let zenops_dir = dirs.zenops();
cmd!(sh, "git -C {zenops_dir} pull --rebase").run()?;
}
let zenops_repo =
ResolvedConfigFilePath::resolve(ConfigFilePath::Zenops(Arc::from(srpath!(""))), dirs);
let cfg_path = dirs.zenops().join("config.toml");
let mut merged = toml::Value::Table(Default::default());
let builtin_conditions: toml::Value = toml::from_str(BUILTIN_CONDITIONS).map_err(|e| {
ConfigError::ParseDb(std::path::PathBuf::from("<defaults:conditions>"), e)
})?;
deep_merge(&mut merged, builtin_conditions);
for (name, src) in DEFAULT_PKGS {
let v: toml::Value = toml::from_str(src).map_err(|e| {
ConfigError::ParseDb(std::path::PathBuf::from(format!("<defaults:{name}>")), e)
})?;
deep_merge(&mut merged, v);
}
let user_bytes =
std::fs::read(&cfg_path).map_err(|e| ConfigError::OpenDb(cfg_path.clone(), e))?;
let user_val: toml::Value = toml::from_slice(&user_bytes)
.map_err(|e| ConfigError::ParseDb(cfg_path.to_path_buf(), e))?;
deep_merge(&mut merged, user_val);
let stored: StoredConfig = merged
.try_into()
.map_err(|e| ConfigError::ParseDb(cfg_path.to_path_buf(), e))?;
let conditions = Conditions::compile(stored.conditions.clone())
.map_err(ConfigError::CompileConditions)?;
let hostname = gethostname::gethostname().to_string_lossy().into_owned();
let brew_prefix = detect_brew_prefix()?;
let system_inputs = build_system_inputs(brew_prefix.as_deref(), &stored.user);
Ok(Self {
dirs,
zenops_repo,
stored,
system_inputs,
conditions,
hostname,
path,
})
}
pub fn pkgs(&self) -> &IndexMap<SmolStr, PkgConfig> {
&self.stored.pkg
}
pub fn home(&self) -> &Path {
self.dirs.home()
}
pub fn system_inputs(&self) -> &IndexMap<SmolStr, SmolStr> {
&self.system_inputs
}
pub(crate) fn conditions(&self) -> &Conditions {
&self.conditions
}
pub(crate) fn shell(&self) -> Option<Shell> {
self.stored.shell.shell()
}
pub(crate) fn host_context(&self, shell: Option<Shell>) -> Result<HostContext<'_>, Error> {
Ok(HostContext {
os: Os::current()?,
shell: shell.or_else(|| self.shell()),
hostname: &self.hostname,
home: self.dirs.home(),
system_inputs: &self.system_inputs,
path: &self.path,
})
}
pub(crate) fn env_pkg_inits(
&self,
shell: Shell,
) -> Result<Vec<(&SmolStr, &PkgConfig, &ShellInitAction)>, Error> {
let ctx = self.host_context(Some(shell))?;
let mut inits = Vec::new();
for (name, p) in &self.stored.pkg {
if p.is_installed(&self.conditions, &ctx)? {
for a in p.shell.env_init.for_shell(shell).iter() {
inits.push((name, p, a));
}
}
}
Ok(inits)
}
pub(crate) fn login_pkg_inits(
&self,
shell: Shell,
) -> Result<Vec<(&SmolStr, &PkgConfig, &ShellInitAction)>, Error> {
let ctx = self.host_context(Some(shell))?;
let mut inits = Vec::new();
for (name, p) in &self.stored.pkg {
if p.is_installed(&self.conditions, &ctx)? {
for a in p.shell.login_init.for_shell(shell).iter() {
inits.push((name, p, a));
}
}
}
Ok(inits)
}
pub(crate) fn interactive_pkg_inits(
&self,
shell: Shell,
) -> Result<Vec<(&SmolStr, &PkgConfig, &ShellInitAction)>, Error> {
let ctx = self.host_context(Some(shell))?;
let mut inits = Vec::new();
for (name, p) in &self.stored.pkg {
if p.is_installed(&self.conditions, &ctx)? {
for a in p.shell.interactive_init.for_shell(shell).iter() {
inits.push((name, p, a));
}
}
}
Ok(inits)
}
pub fn update_config_files(
&self,
_sh: &xshell::Shell,
config_files: &mut ConfigFiles<'_>,
) -> Result<(), Error> {
self.stored.shell.update_config_files(self, config_files)?;
self.stored
.ssh
.update_config_files(config_files, &CurlGithubKeyFetcher::new(&self.path))?;
self.stored.git.update_config_files(
&self.stored.user,
!self.stored.ssh.allowed_signers.is_empty(),
config_files,
)?;
let ctx = self.host_context(None)?;
for (pkg_key, pkg) in &self.stored.pkg {
if !pkg.is_installed(&self.conditions, &ctx)? {
continue;
}
for cfg in pkg.configs() {
cfg.update_config_files(pkg_key, self, config_files)?;
}
}
Ok(())
}
pub fn check_own_status(
&self,
sh: &xshell::Shell,
output: &mut dyn Output,
) -> Result<(), Error> {
let git = Git::new(self.dirs.zenops(), sh);
if git.is_git_repo()? {
let statuses = git.status()?;
if statuses.is_empty() {
output.push(Event::Status(Status::GitRepoClean {
repo: self.zenops_repo.clone(),
}))?;
} else {
for status in statuses {
output.push(Event::Status(Status::Git {
repo: self.zenops_repo.clone(),
status,
}))?;
}
}
}
Ok(())
}
pub fn push_pkg_health(&self, output: &mut dyn Output) -> Result<(), Error> {
let ctx = self.host_context(None)?;
let manager = pkg_manager::detect(ctx.path, &ctx.os)?;
for (key, pkg) in &self.stored.pkg {
let label = pkg.name.clone().unwrap_or_else(|| key.clone());
if pkg.enable_on_but_detect_missing(&self.conditions, &ctx)? {
let install_command = manager.and_then(|m| {
let pkgs = m.packages_for(&pkg.install_hint);
(!pkgs.is_empty()).then(|| m.install_command(pkgs))
});
output.push(Event::Status(Status::Pkg {
pkg: label,
status: PkgStatus::Missing { install_command },
}))?;
} else if pkg.enable_on_and_detect_matches(&self.conditions, &ctx)? {
output.push(Event::Status(Status::Pkg {
pkg: label,
status: PkgStatus::Ok,
}))?;
}
}
Ok(())
}
}
#[cfg(test)]
mod readme_tests {
use super::StoredConfig;
use std::path::{Path, PathBuf};
#[test]
fn doc_toml_blocks_parse_as_stored_config() {
let root = Path::new(env!("CARGO_MANIFEST_DIR"));
let mut files: Vec<PathBuf> = vec![root.join("README.md")];
let docs_dir = root.join("docs");
if docs_dir.is_dir() {
for entry in std::fs::read_dir(&docs_dir).expect("read docs/") {
let path = entry.expect("docs/ entry").path();
if path.extension().is_some_and(|e| e == "md") {
files.push(path);
}
}
}
files.sort();
let mut total_blocks = 0usize;
for file in &files {
let body = std::fs::read_to_string(file)
.unwrap_or_else(|e| panic!("read {}: {e}", file.display()));
let blocks = extract_toml_blocks(&body);
for (i, block) in blocks.iter().enumerate() {
toml::from_str::<StoredConfig>(block).unwrap_or_else(|e| {
panic!(
"{} ```toml block #{i} failed to parse: {e}\n---\n{block}---",
file.display()
)
});
}
total_blocks += blocks.len();
}
assert!(
total_blocks > 0,
"no ```toml blocks found across README.md + docs/*.md"
);
}
fn extract_toml_blocks(body: &str) -> Vec<String> {
let mut blocks = Vec::new();
let mut in_toml = false;
let mut current = String::new();
for line in body.lines() {
if in_toml {
if line.trim_start().starts_with("```") {
blocks.push(std::mem::take(&mut current));
in_toml = false;
} else {
current.push_str(line);
current.push('\n');
}
} else if line.trim_start().starts_with("```toml") {
in_toml = true;
}
}
blocks
}
}
#[cfg(test)]
mod fedora_defaults_tests {
use super::*;
use crate::os::{Distro, Fedora, FedoraVersion, Linux, Os};
use crate::pkg_manager::{self, DetectedPackageManager};
fn fedora42() -> Os {
Os::Linux(Linux {
distro: Distro::Fedora(Fedora {
version: FedoraVersion::F42,
}),
})
}
fn load_defaults() -> StoredConfig {
let mut merged = toml::Value::Table(Default::default());
let builtin_conditions: toml::Value = toml::from_str(BUILTIN_CONDITIONS).unwrap();
deep_merge(&mut merged, builtin_conditions);
for (_name, src) in DEFAULT_PKGS {
let v: toml::Value = toml::from_str(src).unwrap();
deep_merge(&mut merged, v);
}
merged.try_into().unwrap()
}
fn install_cmd_for(pkg: &PkgConfig, mgr: DetectedPackageManager) -> Option<String> {
let pkgs = mgr.packages_for(&pkg.install_hint);
(!pkgs.is_empty()).then(|| mgr.install_command(pkgs))
}
#[test]
fn detect_native_on_fedora_42_is_dnf5() {
assert_eq!(
pkg_manager::detect_native(&fedora42()),
Some(DetectedPackageManager::Dnf5),
);
}
#[test]
fn starship_dnf_install_command_matches_research() {
let cfg = load_defaults();
let pkg = cfg.pkg.get("starship").expect("starship default present");
assert_eq!(
install_cmd_for(pkg, DetectedPackageManager::Dnf5).as_deref(),
Some("sudo dnf install starship"),
);
}
#[test]
fn starship_cargo_install_command_present() {
let cfg = load_defaults();
let pkg = cfg.pkg.get("starship").expect("starship default present");
assert_eq!(
install_cmd_for(pkg, DetectedPackageManager::Cargo).as_deref(),
Some("cargo install starship"),
);
}
#[test]
fn sk_only_installable_via_cargo() {
let cfg = load_defaults();
let pkg = cfg.pkg.get("sk").expect("sk default present");
assert!(install_cmd_for(pkg, DetectedPackageManager::Dnf5).is_none());
assert_eq!(
install_cmd_for(pkg, DetectedPackageManager::Cargo).as_deref(),
Some("cargo install skim"),
);
}
#[test]
fn cargo_dnf_install_command_uses_rustup() {
let cfg = load_defaults();
let pkg = cfg.pkg.get("cargo").expect("cargo default present");
assert_eq!(
install_cmd_for(pkg, DetectedPackageManager::Dnf5).as_deref(),
Some("sudo dnf install rustup"),
);
}
#[test]
fn cargo_pkg_has_no_cargo_install_path() {
let cfg = load_defaults();
let pkg = cfg.pkg.get("cargo").expect("cargo default present");
assert!(install_cmd_for(pkg, DetectedPackageManager::Cargo).is_none());
}
#[test]
fn brew_bound_pkgs_have_no_dnf5_install_path() {
let cfg = load_defaults();
for key in [
"brew-macos",
"brew-linux",
"brew-python",
"bash-completion",
"llvm",
"sk",
] {
let pkg = cfg.pkg.get(key).unwrap_or_else(|| panic!("{key} missing"));
assert!(
pkg.install_hint.dnf5.packages.is_empty(),
"{key}: expected empty dnf5 packages, got {:?}",
pkg.install_hint.dnf5.packages,
);
}
}
fn ubuntu_2404() -> Os {
Os::Linux(crate::os::Linux {
distro: crate::os::Distro::Ubuntu(crate::os::Ubuntu {
version: crate::os::UbuntuVersion::U2404,
}),
})
}
fn arch_host() -> Os {
Os::Linux(crate::os::Linux {
distro: crate::os::Distro::Arch,
})
}
#[test]
fn detect_native_on_arch_is_pacman() {
assert_eq!(
pkg_manager::detect_native(&arch_host()),
Some(DetectedPackageManager::Pacman),
);
}
#[test]
fn starship_pacman_install_command_matches_research() {
let cfg = load_defaults();
let pkg = cfg.pkg.get("starship").expect("starship default present");
assert_eq!(
install_cmd_for(pkg, DetectedPackageManager::Pacman).as_deref(),
Some("sudo pacman -S starship"),
);
}
#[test]
fn cargo_pacman_install_command_uses_rustup() {
let cfg = load_defaults();
let pkg = cfg.pkg.get("cargo").expect("cargo default present");
assert_eq!(
install_cmd_for(pkg, DetectedPackageManager::Pacman).as_deref(),
Some("sudo pacman -S rustup"),
);
}
#[test]
fn sk_pacman_install_command_uses_skim() {
let cfg = load_defaults();
let pkg = cfg.pkg.get("sk").expect("sk default present");
assert_eq!(
install_cmd_for(pkg, DetectedPackageManager::Pacman).as_deref(),
Some("sudo pacman -S skim"),
);
}
#[test]
fn detect_native_on_ubuntu_2404_is_apt() {
assert_eq!(
pkg_manager::detect_native(&ubuntu_2404()),
Some(DetectedPackageManager::Apt),
);
}
#[test]
fn starship_apt_install_command_matches_research() {
let cfg = load_defaults();
let pkg = cfg.pkg.get("starship").expect("starship default present");
assert_eq!(
install_cmd_for(pkg, DetectedPackageManager::Apt).as_deref(),
Some("sudo apt install starship"),
);
}
#[test]
fn cargo_apt_install_command_uses_rustup() {
let cfg = load_defaults();
let pkg = cfg.pkg.get("cargo").expect("cargo default present");
assert_eq!(
install_cmd_for(pkg, DetectedPackageManager::Apt).as_deref(),
Some("sudo apt install rustup"),
);
}
#[test]
fn sk_only_installable_via_cargo_on_ubuntu_too() {
let cfg = load_defaults();
let pkg = cfg.pkg.get("sk").expect("sk default present");
assert!(install_cmd_for(pkg, DetectedPackageManager::Apt).is_none());
assert_eq!(
install_cmd_for(pkg, DetectedPackageManager::Cargo).as_deref(),
Some("cargo install skim"),
);
}
#[test]
fn brew_macos_when_gate_excludes_fedora_host() {
let cfg = load_defaults();
let conds = Conditions::compile(cfg.conditions.clone()).unwrap();
let pkg = cfg
.pkg
.get("brew-macos")
.expect("brew-macos default present");
let tmp = tempfile::tempdir().unwrap();
let sys = IndexMap::new();
let path = SearchPath::default();
let ctx = HostContext {
os: fedora42(),
shell: None,
hostname: "fedora42-test",
home: tmp.path(),
system_inputs: &sys,
path: &path,
};
assert!(
!pkg.evaluate_when(&conds, &ctx).unwrap(),
"brew-macos should not match a Fedora host"
);
}
#[test]
fn brew_linux_when_gate_matches_fedora_host() {
let cfg = load_defaults();
let conds = Conditions::compile(cfg.conditions.clone()).unwrap();
let pkg = cfg
.pkg
.get("brew-linux")
.expect("brew-linux default present");
let tmp = tempfile::tempdir().unwrap();
let sys = IndexMap::new();
let path = SearchPath::default();
let ctx = HostContext {
os: fedora42(),
shell: None,
hostname: "fedora42-test",
home: tmp.path(),
system_inputs: &sys,
path: &path,
};
assert!(
pkg.evaluate_when(&conds, &ctx).unwrap(),
"brew-linux should match a Fedora host (linux is any-distro)"
);
}
}