config: add per-command and global impure knobs; bump deps
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: Icadc94f1e1ca1c007feee7766c60847c6a6a6964
This commit is contained in:
parent
8836eacb95
commit
e385c74b57
6 changed files with 321 additions and 25 deletions
99
Cargo.lock
generated
99
Cargo.lock
generated
|
|
@ -25,9 +25,9 @@ checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
|||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.11.0"
|
||||
version = "2.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
|
||||
checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
|
|
@ -37,9 +37,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
|||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.6.0"
|
||||
version = "4.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351"
|
||||
checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
|
|
@ -57,18 +57,18 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "clap_complete"
|
||||
version = "4.6.0"
|
||||
version = "4.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19c9f1dde76b736e3681f28cec9d5a61299cbaae0fce80a68e43724ad56031eb"
|
||||
checksum = "3ff7a1dccbdd8b078c2bdebff47e404615151534d5043da397ec50286816f9cb"
|
||||
dependencies = [
|
||||
"clap",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.6.0"
|
||||
version = "4.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a"
|
||||
checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
|
|
@ -117,6 +117,7 @@ dependencies = [
|
|||
"tempfile",
|
||||
"textwrap",
|
||||
"thiserror",
|
||||
"toml",
|
||||
"walkdir",
|
||||
"yansi",
|
||||
]
|
||||
|
|
@ -152,9 +153,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.3.0"
|
||||
version = "2.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
||||
checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
|
||||
|
||||
[[package]]
|
||||
name = "foldhash"
|
||||
|
|
@ -186,9 +187,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.16.1"
|
||||
version = "0.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
|
||||
checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
|
|
@ -204,12 +205,12 @@ checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
|
|||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.13.0"
|
||||
version = "2.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
|
||||
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.16.1",
|
||||
"hashbrown 0.17.0",
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
|
|
@ -228,9 +229,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
|||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.184"
|
||||
version = "0.2.185"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af"
|
||||
checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f"
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
|
|
@ -343,9 +344,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.27"
|
||||
version = "1.0.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
|
||||
checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
|
|
@ -390,6 +391,15 @@ dependencies = [
|
|||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_spanned"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shell-words"
|
||||
version = "1.1.1"
|
||||
|
|
@ -457,6 +467,37 @@ dependencies = [
|
|||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "1.1.2+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"toml_parser",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "1.1.1+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_parser"
|
||||
version = "1.1.2+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526"
|
||||
dependencies = [
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
|
|
@ -493,11 +534,11 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "wasip2"
|
||||
version = "1.0.2+wasi-0.2.9"
|
||||
version = "1.0.3+wasi-0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5"
|
||||
checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6"
|
||||
dependencies = [
|
||||
"wit-bindgen",
|
||||
"wit-bindgen 0.57.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -506,7 +547,7 @@ version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
|
||||
dependencies = [
|
||||
"wit-bindgen",
|
||||
"wit-bindgen 0.51.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -567,6 +608,12 @@ dependencies = [
|
|||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0"
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen"
|
||||
version = "0.51.0"
|
||||
|
|
@ -576,6 +623,12 @@ dependencies = [
|
|||
"wit-bindgen-rust-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen"
|
||||
version = "0.57.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-core"
|
||||
version = "0.51.0"
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ serde_json = "1.0.149"
|
|||
tempfile = "3.27.0"
|
||||
textwrap = "0.16.2"
|
||||
thiserror = "2.0.18"
|
||||
toml = { default-features = false, features = [ "parse", "serde" ], version = "1.1.2" }
|
||||
walkdir = "2.5.0"
|
||||
yansi = "1.0.1"
|
||||
|
||||
|
|
|
|||
|
|
@ -20,5 +20,6 @@ serde_json.workspace = true
|
|||
tempfile.workspace = true
|
||||
textwrap.workspace = true
|
||||
thiserror.workspace = true
|
||||
toml.workspace = true
|
||||
walkdir.workspace = true
|
||||
yansi.workspace = true
|
||||
|
|
|
|||
236
eh/src/config.rs
Normal file
236
eh/src/config.rs
Normal file
|
|
@ -0,0 +1,236 @@
|
|||
use std::{
|
||||
collections::HashMap,
|
||||
env,
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Debug, Deserialize, Default)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Config {
|
||||
/// When `Some(true)`, pass `--impure` to every Nix command.
|
||||
/// When `Some(false)`, block automatic impure retries for every command.
|
||||
/// When absent (`None`), retry behaviour is automatic (default).
|
||||
#[serde(default)]
|
||||
pub impure: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub commands: HashMap<String, CommandConfig>,
|
||||
}
|
||||
|
||||
/// Per-command configuration.
|
||||
#[derive(Debug, Deserialize, Default, Clone)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct CommandConfig {
|
||||
/// When `Some(true)`, pass `--impure` to the underlying Nix command.
|
||||
/// When `Some(false)`, block automatic impure retries for this command.
|
||||
/// When absent (`None`), the global setting is used; if that is also absent,
|
||||
/// retry behaviour is automatic (default).
|
||||
#[serde(default)]
|
||||
pub impure: Option<bool>,
|
||||
/// Additional environment variables to set for the Nix command.
|
||||
#[serde(default)]
|
||||
pub env: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// Return the [`CommandConfig`] for `command`.
|
||||
///
|
||||
/// Resolution order: per-command `impure` takes precedence over the global
|
||||
/// `impure`. Neither being set means automatic retry behaviour.
|
||||
pub fn for_command(&self, command: &str) -> CommandConfig {
|
||||
let mut cmd = self.commands.get(command).cloned().unwrap_or_default();
|
||||
// Per-command setting wins; fall back to global.
|
||||
if cmd.impure.is_none() {
|
||||
cmd.impure = self.impure;
|
||||
}
|
||||
cmd
|
||||
}
|
||||
}
|
||||
|
||||
/// Load configuration from the first `.eh.toml` found by walking up from the
|
||||
/// current directory, or from `~/.config/eh/config.toml` as a global
|
||||
/// fallback. Returns a default (empty) config if no file is found or if
|
||||
/// parsing fails.
|
||||
pub fn load() -> Config {
|
||||
if let Some(path) = find_project_config()
|
||||
&& let Some(cfg) = load_from_file(&path)
|
||||
{
|
||||
return cfg;
|
||||
}
|
||||
|
||||
if let Some(path) = global_config_path()
|
||||
&& let Some(cfg) = load_from_file(&path)
|
||||
{
|
||||
return cfg;
|
||||
}
|
||||
|
||||
Config::default()
|
||||
}
|
||||
|
||||
fn find_project_config() -> Option<PathBuf> {
|
||||
let mut dir = env::current_dir().ok()?;
|
||||
loop {
|
||||
let candidate = dir.join(".eh.toml");
|
||||
if candidate.exists() {
|
||||
return Some(candidate);
|
||||
}
|
||||
if !dir.pop() {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn global_config_path() -> Option<PathBuf> {
|
||||
let home = env::var("HOME").ok()?;
|
||||
Some(
|
||||
PathBuf::from(home)
|
||||
.join(".config")
|
||||
.join("eh")
|
||||
.join("config.toml"),
|
||||
)
|
||||
}
|
||||
|
||||
fn load_from_file(path: &Path) -> Option<Config> {
|
||||
let content = fs::read_to_string(path).ok()?;
|
||||
match toml::de::from_str::<Config>(&content) {
|
||||
Ok(cfg) => Some(cfg),
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"eh: warning: failed to parse config file {}: {}",
|
||||
path.display(),
|
||||
e
|
||||
);
|
||||
None
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_empty_config_defaults() {
|
||||
let cfg: Config = toml::from_str("").unwrap();
|
||||
assert!(cfg.impure.is_none());
|
||||
assert!(cfg.commands.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_command_config_impure_true() {
|
||||
let cfg: Config = toml::from_str(
|
||||
r#"
|
||||
[commands.build]
|
||||
impure = true
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(cfg.for_command("build").impure, Some(true));
|
||||
assert_eq!(cfg.for_command("run").impure, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_command_config_impure_false() {
|
||||
let cfg: Config = toml::from_str(
|
||||
r#"
|
||||
[commands.build]
|
||||
impure = false
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(cfg.for_command("build").impure, Some(false));
|
||||
assert_eq!(cfg.for_command("run").impure, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_global_impure_propagates_to_unconfigured_commands() {
|
||||
let cfg: Config = toml::from_str("impure = true").unwrap();
|
||||
// Commands with no per-command entry inherit global.
|
||||
assert_eq!(cfg.for_command("build").impure, Some(true));
|
||||
assert_eq!(cfg.for_command("nonexistent").impure, Some(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_global_impure_false_propagates_to_unconfigured_commands() {
|
||||
let cfg: Config = toml::from_str("impure = false").unwrap();
|
||||
assert_eq!(cfg.for_command("build").impure, Some(false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_per_command_impure_overrides_global() {
|
||||
// Per-command setting wins over global.
|
||||
let cfg: Config = toml::from_str(
|
||||
r#"
|
||||
impure = false
|
||||
|
||||
[commands.build]
|
||||
impure = true
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(cfg.for_command("build").impure, Some(true));
|
||||
// Command without per-command entry falls back to global false.
|
||||
assert_eq!(cfg.for_command("run").impure, Some(false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_command_config_env() {
|
||||
let cfg: Config = toml::from_str(
|
||||
r#"
|
||||
[commands.develop]
|
||||
env = { FOO = "bar", BAZ = "1" }
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let dev = cfg.for_command("develop");
|
||||
assert_eq!(dev.env.get("FOO").map(String::as_str), Some("bar"));
|
||||
assert_eq!(dev.env.get("BAZ").map(String::as_str), Some("1"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_command_config_env_table_syntax() {
|
||||
let cfg: Config = toml::from_str(
|
||||
r#"
|
||||
[commands.shell]
|
||||
impure = true
|
||||
|
||||
[commands.shell.env]
|
||||
MY_VAR = "hello"
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let shell = cfg.for_command("shell");
|
||||
assert_eq!(shell.impure, Some(true));
|
||||
assert_eq!(shell.env.get("MY_VAR").map(String::as_str), Some("hello"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_for_command_missing_returns_default() {
|
||||
let cfg = Config::default();
|
||||
let cc = cfg.for_command("nonexistent");
|
||||
assert_eq!(cc.impure, None);
|
||||
assert!(cc.env.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unknown_top_level_key_is_rejected() {
|
||||
let result = toml::de::from_str::<Config>("unknown_key = true");
|
||||
assert!(result.is_err(), "unknown top-level keys should be rejected");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unknown_command_key_is_rejected() {
|
||||
let result = toml::de::from_str::<Config>(
|
||||
r#"
|
||||
[commands.build]
|
||||
typo_key = true
|
||||
"#,
|
||||
);
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"unknown per-command keys should be rejected"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
pub mod commands;
|
||||
pub mod config;
|
||||
pub mod error;
|
||||
pub mod util;
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ use eh::{Cli, Command, CommandFactory, Parser};
|
|||
use yansi::Paint;
|
||||
|
||||
mod commands;
|
||||
mod config;
|
||||
mod error;
|
||||
mod util;
|
||||
|
||||
|
|
@ -29,11 +30,13 @@ fn handle_command(command: &str, args: &[String]) -> error::Result<i32> {
|
|||
let hash_extractor = util::RegexHashExtractor;
|
||||
let fixer = util::DefaultNixFileFixer;
|
||||
let classifier = util::DefaultNixErrorClassifier;
|
||||
let cfg = config::load();
|
||||
let cmd_cfg = cfg.for_command(command);
|
||||
|
||||
match command {
|
||||
"info" => commands::info::handle_info(args),
|
||||
"info" => commands::info::handle_info(args, &cmd_cfg),
|
||||
|
||||
"update" => commands::update::handle_update(args),
|
||||
"update" => commands::update::handle_update(args, &cmd_cfg),
|
||||
"run" | "shell" | "build" | "develop" => {
|
||||
commands::handle_nix_command(
|
||||
command,
|
||||
|
|
@ -41,6 +44,7 @@ fn handle_command(command: &str, args: &[String]) -> error::Result<i32> {
|
|||
&hash_extractor,
|
||||
&fixer,
|
||||
&classifier,
|
||||
&cmd_cfg,
|
||||
)
|
||||
},
|
||||
_ => unreachable!(),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue