From 48dd0865459265aa373dd321a082fdd018a4ddfb Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 5 May 2026 23:36:35 +0000 Subject: [PATCH] Strip em dashes and emojis, trim noisy comments Make output and source plain ASCII. Replace em dashes with normal punctuation across code, docs, and CLI strings. Drop the hammer from the CLI about text. Switch validate's check/cross markers to OK / WARN / FAIL so the output stays readable in any terminal. Drop comments that just restate the next line of code (e.g. "Load config", "Pre-resolve hooks", "Replace \${VERSION} with package version") and the decorative section banners in main.rs and context.rs. Keep the comments that explain non-obvious decisions (tempdir leak, cache salt, alias resolution, override warning, tilde-at-segment expansion, etc.). --- README.md | 6 +-- docs/walkthrough.md | 18 ++++----- src/cache.rs | 7 ++-- src/cli.rs | 11 ++---- src/config.rs | 12 ++++-- src/context.rs | 13 ++----- src/main.rs | 95 ++++++++++++++++++++++----------------------- src/package.rs | 77 ++++++++++++++++++------------------ src/resolver.rs | 23 +++++------ src/shell.rs | 47 ++++++++++------------ tests/cli.rs | 53 +++++++++++++++++++------ 11 files changed, 190 insertions(+), 172 deletions(-) diff --git a/README.md b/README.md index 874a6f5..b7c4423 100644 --- a/README.md +++ b/README.md @@ -135,7 +135,7 @@ only on the last hyphen when the suffix starts with a digit. Values resolve in this order: `${PACKAGE_ROOT}`, `${VERSION}`, `${NAME}`, `${PATHSEP}` (`:` on Unix / `;` on Windows), `${EXE_SUFFIX}` (`""` on Unix / `".exe"` on Windows), then any `${VAR}` set by previously resolved packages or -the inherited environment, and finally `~/` — which expands at every path +the inherited environment, and finally `~/`, which expands at every path segment, so `~/USD/bin${PATHSEP}~/USD/lib` works as expected. On Windows PowerShell sessions `~/` falls back to `USERPROFILE` when `HOME` is unset. @@ -147,7 +147,7 @@ through. The `commands` map lets `anvil run` pick a program from the package definition. Values expand the same way as `environment` values, and can include baked in -arguments with whitespace or tilde segments — anvil tokenises the value with +arguments with whitespace or tilde segments. Anvil tokenises the value with POSIX shell rules, expands `~/` in every token, then runs the first token with the remaining tokens prepended to whatever the user passes after `--`. @@ -167,7 +167,7 @@ commands: ``` `anvil run nukex -- --view` therefore exec's -`${NUKE_HOME}/Nuke${VERSION} --nukex --view` — packages can ship sane defaults +`${NUKE_HOME}/Nuke${VERSION} --nukex --view`. Packages can ship sane defaults for every tool they expose without forcing users to memorise flag soup. Quoted substrings are preserved as a single argv element, so paths with spaces work without escaping the whole value. diff --git a/docs/walkthrough.md b/docs/walkthrough.md index 0e17b47..a45f04b 100644 --- a/docs/walkthrough.md +++ b/docs/walkthrough.md @@ -62,13 +62,13 @@ anvil info houdini-20.5 ```bash anvil validate -# ✓ blender -# ✓ houdini -# ✓ nuke -# ✓ python -# ✓ studio-blender-tools -# ✓ studio-python -# ✓ usd +# OK blender +# OK houdini +# OK nuke +# OK python +# OK studio-blender-tools +# OK studio-python +# OK usd # All packages valid! ``` @@ -94,7 +94,7 @@ anvil env python-3.11 --json | python3 -c "import sys,json; print(json.load(sys. ### Dependency resolution -Houdini depends on python-3.11 — both are resolved automatically: +Houdini depends on python-3.11, and both are resolved automatically: ```bash anvil env houdini-20.5 | grep -E "PYTHON_VERSION|HOUDINI_VERSION" @@ -171,7 +171,7 @@ anvil run python-3.11 -e MY_VAR=hello -- env | grep MY_VAR ```bash # Start a shell with packages loaded -# (will replace the current process — type 'exit' to return) +# (will replace the current process; type 'exit' to return) anvil shell python-3.11 # Specify a different shell diff --git a/src/cache.rs b/src/cache.rs index fb4b308..185b0fc 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -41,14 +41,12 @@ pub fn compute_fingerprint(package_paths: &[PathBuf], config_salt: &str) -> u64 base.hash(&mut hasher); hash_dir_entries(base, &mut hasher); - // One level deeper for nested packages if let Ok(entries) = std::fs::read_dir(base) { for entry in entries.flatten() { let path = entry.path(); if path.is_dir() { hash_dir_entries(&path, &mut hasher); - // Check package.yaml files inside version dirs if let Ok(sub_entries) = std::fs::read_dir(&path) { for sub in sub_entries.flatten() { if sub.path().is_dir() { @@ -116,7 +114,10 @@ fn hash_file_mtime(path: &Path, hasher: &mut impl Hasher) { /// Try to load cached packages. Returns `Some(packages)` if the cache /// is valid (fingerprint matches), `None` otherwise. -pub fn load(package_paths: &[PathBuf], config_salt: &str) -> Option>> { +pub fn load( + package_paths: &[PathBuf], + config_salt: &str, +) -> Option>> { let path = cache_path()?; if !path.exists() { return None; diff --git a/src/cli.rs b/src/cli.rs index 23620df..aa772fb 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -6,7 +6,7 @@ use clap::{CommandFactory, Parser, Subcommand}; #[command(name = "anvil")] #[command(author = "Alejandro Cabrera ")] #[command(version)] -#[command(about = "Forge your environment 🔨 — Fast package resolution for VFX pipelines", long_about = None)] +#[command(about = "Forge your environment. Fast package resolution for VFX pipelines", long_about = None)] pub struct Cli { /// Ignore any cached package scan and re-read all package files. #[arg(long, global = true)] @@ -63,7 +63,7 @@ pub enum Commands { #[arg(short, long)] shell: Option, - /// Don't materialise `commands:` as PATH shims — compose the env only. + /// Don't materialise `commands:` as PATH shims; compose the env only. #[arg(long)] env_only: bool, @@ -170,12 +170,7 @@ pub enum Commands { impl Cli { /// Generate shell completions and write to stdout. pub fn print_completions(shell: clap_complete::Shell) { - clap_complete::generate( - shell, - &mut Self::command(), - "anvil", - &mut std::io::stdout(), - ); + clap_complete::generate(shell, &mut Self::command(), "anvil", &mut std::io::stdout()); } } diff --git a/src/config.rs b/src/config.rs index 0624294..96be324 100644 --- a/src/config.rs +++ b/src/config.rs @@ -182,7 +182,6 @@ impl Config { config.merge(project); } - // Apply platform overrides and expand paths after merging config.apply_platform_overrides(); config.expand_paths(); @@ -420,7 +419,10 @@ impl Config { } /// Run a list of hook commands. Returns Err if any hook exits non-zero. - pub fn run_hooks(hooks: &[String], env: &std::collections::HashMap) -> Result<()> { + pub fn run_hooks( + hooks: &[String], + env: &std::collections::HashMap, + ) -> Result<()> { for cmd in hooks { let shell = if cfg!(target_os = "windows") { "cmd" @@ -441,7 +443,11 @@ impl Config { .with_context(|| format!("Failed to run hook: {}", cmd))?; if !status.success() { - anyhow::bail!("Hook failed (exit {}): {}", status.code().unwrap_or(-1), cmd); + anyhow::bail!( + "Hook failed (exit {}): {}", + status.code().unwrap_or(-1), + cmd + ); } } Ok(()) diff --git a/src/context.rs b/src/context.rs index 8ffe40d..2c9c245 100644 --- a/src/context.rs +++ b/src/context.rs @@ -6,10 +6,6 @@ use std::path::{Path, PathBuf}; use anyhow::{Context as _, Result}; use serde::{Deserialize, Serialize}; -// --------------------------------------------------------------------------- -// Lockfile -// --------------------------------------------------------------------------- - /// Pins package versions for reproducible resolution. /// /// Stored as `anvil.lock` in the project directory. When present, the @@ -35,7 +31,10 @@ impl Lockfile { /// Write the lockfile to a YAML file. pub fn save(&self, path: &Path) -> Result<()> { let content = serde_yaml::to_string(self)?; - let output = format!("# anvil.lock — generated by `anvil lock`, do not edit\n{}", content); + let output = format!( + "# anvil.lock: generated by `anvil lock`, do not edit\n{}", + content + ); std::fs::write(path, output) .with_context(|| format!("Failed to write lockfile: {:?}", path)) } @@ -55,10 +54,6 @@ impl Lockfile { } } -// --------------------------------------------------------------------------- -// Saved context -// --------------------------------------------------------------------------- - /// A fully resolved environment that can be saved, shared, and re-loaded /// without re-running resolution. Useful for render farms, CI, and /// sharing reproducible environments across machines. diff --git a/src/main.rs b/src/main.rs index d349797..f4c22f7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -43,18 +43,30 @@ fn main() -> Result<()> { ) .init(); - // Load config let config = Config::load()?; let refresh = cli.refresh; match cli.command { - Commands::Env { packages, export, json } => { + Commands::Env { + packages, + export, + json, + } => { cmd_env(&config, &packages, export, json, refresh)?; } - Commands::Run { packages, env_vars, command } => { + Commands::Run { + packages, + env_vars, + command, + } => { cmd_run(&config, &packages, &env_vars, &command, refresh)?; } - Commands::Shell { packages, shell, env_only, no_sweep } => { + Commands::Shell { + packages, + shell, + env_only, + no_sweep, + } => { cmd_shell(&config, &packages, shell, refresh, env_only, no_sweep)?; } Commands::List { package } => { @@ -66,7 +78,10 @@ fn main() -> Result<()> { Commands::Validate { package, strict } => { cmd_validate(&config, package, strict, refresh)?; } - Commands::Lock { packages, update: _ } => { + Commands::Lock { + packages, + update: _, + } => { cmd_lock(&config, &packages, refresh)?; } Commands::Context { action } => match action { @@ -83,7 +98,12 @@ fn main() -> Result<()> { cmd_context_shell(&config, &file, shell)?; } }, - Commands::Init { name, version, flat, config: scaffold_config } => { + Commands::Init { + name, + version, + flat, + config: scaffold_config, + } => { if scaffold_config { cmd_init_config()?; } else { @@ -98,7 +118,11 @@ fn main() -> Result<()> { Commands::Completions { shell } => { Cli::print_completions(shell); } - Commands::Wrap { packages, dir, shell } => { + Commands::Wrap { + packages, + dir, + shell, + } => { cmd_wrap(&config, &packages, &dir, &shell, refresh)?; } Commands::Publish { target, path, flat } => { @@ -146,17 +170,14 @@ fn cmd_run( ) -> Result<()> { use std::process::Command; - // Pre-resolve hooks Config::run_hooks(&config.hooks.pre_resolve, &std::env::vars().collect())?; let resolver = Resolver::new(config, refresh)?; let resolved = resolver.resolve(packages)?; let mut env = resolved.environment(); - // Post-resolve hooks Config::run_hooks(&config.hooks.post_resolve, &env)?; - // Add user-specified env vars for var in env_vars { if let Some((key, value)) = var.split_once('=') { env.insert(key.to_string(), value.to_string()); @@ -188,7 +209,6 @@ fn cmd_run( let mut all_args = tokens; all_args.extend(command[1..].iter().cloned()); - // Pre-run hooks Config::run_hooks(&config.hooks.pre_run, &env)?; // Surface the resolved argv at `-v`/`-vv` so when an exec fails with @@ -256,14 +276,12 @@ fn cmd_list(config: &Config, package: Option, refresh: bool) -> Result<( let resolver = Resolver::new(config, refresh)?; if let Some(name) = package { - // List versions of specific package let versions = resolver.list_versions(&name)?; println!("{}:", name); for version in versions { println!(" - {}", version); } } else { - // List all packages let packages = resolver.list_packages()?; if packages.is_empty() { if let Some(hint) = config.first_run_hint() { @@ -356,9 +374,9 @@ fn cmd_validate( match report { Ok(cmd_problems) => { if cmd_problems.is_empty() { - println!("✓ {}", pkg_name); + println!("OK {}", pkg_name); } else { - let label = if strict { "✗" } else { "!" }; + let label = if strict { "FAIL" } else { "WARN" }; println!("{} {}: command problems:", label, pkg_name); for p in &cmd_problems { println!(" - {}", p); @@ -371,7 +389,7 @@ fn cmd_validate( } } Err(e) => { - println!("✗ {}: {}", pkg_name, e); + println!("FAIL {}: {}", pkg_name, e); errors += 1; } } @@ -383,7 +401,7 @@ fn cmd_validate( if warnings > 0 { println!( - "\nAll dependencies resolve ({} package(s) with command warnings — use --strict to fail on these).", + "\nAll dependencies resolve ({} package(s) with command warnings; use --strict to fail on these).", warnings ); } else { @@ -392,13 +410,9 @@ fn cmd_validate( Ok(()) } -// --------------------------------------------------------------------------- -// Lock -// --------------------------------------------------------------------------- - /// Resolve packages and write pinned versions to `anvil.lock`. fn cmd_lock(config: &Config, packages: &[String], refresh: bool) -> Result<()> { - // Always resolve fresh (ignore existing lockfile). + // Always resolve fresh, ignoring any existing lockfile. let resolver = Resolver::new_unlocked(config, refresh)?; let resolved = resolver.resolve(packages)?; @@ -415,7 +429,10 @@ fn cmd_lock(config: &Config, packages: &[String], refresh: bool) -> Result<()> { let lock_path = std::path::PathBuf::from("anvil.lock"); lockfile.save(&lock_path)?; - println!("Locked {} packages to anvil.lock:", resolved.packages().len()); + println!( + "Locked {} packages to anvil.lock:", + resolved.packages().len() + ); for pkg in resolved.packages() { println!(" {}-{}", pkg.name, pkg.version); } @@ -423,10 +440,6 @@ fn cmd_lock(config: &Config, packages: &[String], refresh: bool) -> Result<()> { Ok(()) } -// --------------------------------------------------------------------------- -// Context -// --------------------------------------------------------------------------- - /// Resolve packages and save the full environment to a context file. fn cmd_context_save( config: &Config, @@ -526,10 +539,6 @@ fn cmd_context_shell(config: &Config, file: &str, shell_override: Option Ok(()) } -// --------------------------------------------------------------------------- -// Init -// --------------------------------------------------------------------------- - /// Scaffold a new package definition. fn cmd_init(name: &str, version: &str, flat: bool) -> Result<()> { let template = format!( @@ -586,7 +595,7 @@ fn cmd_init_config() -> Result<()> { .with_context(|| format!("Failed to create {}", parent.display()))?; } - let template = r#"# Anvil global config — see https://github.com/voidreamer/anvil + let template = r#"# Anvil global config. See https://github.com/voidreamer/anvil # Where to look for package definitions, in priority order. # Each entry can be a directory of flat `-.yaml` files @@ -619,10 +628,6 @@ package_paths: Ok(()) } -// --------------------------------------------------------------------------- -// Wrap -// --------------------------------------------------------------------------- - /// Generate wrapper scripts for all commands defined by the resolved packages. fn cmd_wrap( config: &Config, @@ -644,7 +649,6 @@ fn cmd_wrap( let dir_path = std::path::Path::new(dir); std::fs::create_dir_all(dir_path)?; - // Build the package request string for the wrapper let pkg_args: Vec = resolved.packages().iter().map(|p| p.id()).collect(); let pkg_str = pkg_args.join(" "); @@ -654,7 +658,6 @@ fn cmd_wrap( let out_path = dir_path.join(alias); std::fs::write(&out_path, script)?; - // Make executable on Unix #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; @@ -691,10 +694,6 @@ fn generate_wrapper(shell: &str, packages: &str, command: &str) -> String { } } -// --------------------------------------------------------------------------- -// Publish -// --------------------------------------------------------------------------- - /// Publish a package to a target package path. fn cmd_publish(target: &str, source: Option<&str>, flat: bool) -> Result<()> { use crate::package::Package; @@ -704,7 +703,6 @@ fn cmd_publish(target: &str, source: Option<&str>, flat: bool) -> Result<()> { None => std::env::current_dir()?, }; - // Load and validate the package let pkg = if source_dir.is_file() { Package::load_from_file(&source_dir, None)? } else { @@ -717,23 +715,25 @@ fn cmd_publish(target: &str, source: Option<&str>, flat: bool) -> Result<()> { } if flat { - // Publish as flat YAML file let filename = format!("{}-{}.yaml", pkg.name, pkg.version); let dest = target_path.join(&filename); if dest.exists() { anyhow::bail!("{} already exists in target", filename); } - // Re-read the source YAML to publish it verbatim let src_file = if source_dir.is_file() { source_dir.clone() } else { source_dir.join("package.yaml") }; std::fs::copy(&src_file, &dest)?; - println!("Published {}-{} to {}", pkg.name, pkg.version, dest.display()); + println!( + "Published {}-{} to {}", + pkg.name, + pkg.version, + dest.display() + ); } else { - // Publish as nested directory let dest_dir = target_path.join(&pkg.name).join(&pkg.version); if dest_dir.exists() { anyhow::bail!( @@ -744,7 +744,6 @@ fn cmd_publish(target: &str, source: Option<&str>, flat: bool) -> Result<()> { ); } - // Copy the entire source directory tree let src = if source_dir.is_file() { source_dir .parent() diff --git a/src/package.rs b/src/package.rs index e3740ad..271106a 100644 --- a/src/package.rs +++ b/src/package.rs @@ -24,29 +24,29 @@ pub const EXE_SUFFIX: &str = ""; pub struct Package { /// Package name pub name: String, - + /// Package version pub version: String, - + /// Human-readable description pub description: Option, - + /// Required packages (dependencies) #[serde(default)] pub requires: Vec, - + /// Environment variables to set #[serde(default)] pub environment: IndexMap, - + /// Command aliases #[serde(default)] pub commands: HashMap, - + /// Platform-specific variants #[serde(default)] pub variants: Vec, - + /// Path to the package root (set after loading, omitted from package.yaml) #[serde(default)] pub root: PathBuf, @@ -56,11 +56,11 @@ pub struct Package { pub struct PackageVariant { /// Platform filter (linux, windows, macos) pub platform: Option, - + /// Additional requires for this variant #[serde(default)] pub requires: Vec, - + /// Additional environment for this variant #[serde(default)] pub environment: IndexMap, @@ -101,12 +101,12 @@ impl Package { Ok(package) } - + /// Get the full package identifier (name-version) pub fn id(&self) -> String { format!("{}-{}", self.name, self.version) } - + /// Apply the variant matching the current platform fn apply_current_variant(&mut self) { let current_platform = if cfg!(target_os = "linux") { @@ -118,31 +118,23 @@ impl Package { } else { return; }; - + for variant in &self.variants { if variant.platform.as_deref() == Some(current_platform) { - // Merge variant requires self.requires.extend(variant.requires.clone()); - - // Merge variant environment for (key, value) in &variant.environment { self.environment.insert(key.clone(), value.clone()); } } } } - + /// Expand environment variables and tilde in a value pub fn expand_env_value(&self, value: &str, env: &HashMap) -> String { let mut result = value.to_string(); - // Replace ${PACKAGE_ROOT} with actual path result = result.replace("${PACKAGE_ROOT}", &self.root.to_string_lossy()); - - // Replace ${VERSION} with package version result = result.replace("${VERSION}", &self.version); - - // Replace ${NAME} with package name result = result.replace("${NAME}", &self.name); // Platform-aware builtins so a single yaml line can compose path @@ -150,17 +142,18 @@ impl Package { result = result.replace("${PATHSEP}", PATHSEP); result = result.replace("${EXE_SUFFIX}", EXE_SUFFIX); - // Replace other ${VAR} references for (key, val) in env { result = result.replace(&format!("${{{}}}", key), val); } - // Replace remaining ${VAR} with current environment + // Anything still unresolved falls back to the current process env. let re = regex::Regex::new(r"\$\{([^}]+)\}").unwrap(); - result = re.replace_all(&result, |caps: ®ex::Captures| { - let var = &caps[1]; - std::env::var(var).unwrap_or_default() - }).to_string(); + result = re + .replace_all(&result, |caps: ®ex::Captures| { + let var = &caps[1]; + std::env::var(var).unwrap_or_default() + }) + .to_string(); // Expand `~/` everywhere it appears at a segment boundary // (start-of-value, or after `:` / `;`). Path-list values like @@ -179,23 +172,26 @@ impl Package { result } - + /// Get resolved environment for this package - pub fn resolved_environment(&self, base_env: &HashMap) -> HashMap { + pub fn resolved_environment( + &self, + base_env: &HashMap, + ) -> HashMap { let mut env = base_env.clone(); - + for (key, value) in &self.environment { let expanded = self.expand_env_value(value, &env); env.insert(key.clone(), expanded); } - + env } } /// Tokenize a command-alias value into `[program, args...]`. /// -/// If the whole value — after tilde expansion — names an existing file, it's +/// If the whole value, after tilde expansion, names an existing file, it's /// treated as a single executable path (so paths with spaces like /// `/Applications/Houdini 20/bin/hython` work without quoting). Otherwise the /// value is split with POSIX shell rules and each token is tilde-expanded. @@ -240,7 +236,6 @@ impl PackageRequest { /// with an ASCII digit). This allows hyphenated package names such as /// `studio-blender-tools` to be used without being misinterpreted. pub fn parse(s: &str) -> Result { - // Try to split name and version on the last '-' if let Some(idx) = s.rfind('-') { let name = &s[..idx]; let version_part = &s[idx + 1..]; @@ -254,7 +249,6 @@ impl PackageRequest { .map_or(false, |c| c.is_ascii_digit()); if starts_with_digit { - // Parse version constraint let constraint = if version_part.ends_with('+') { VersionConstraint::Minimum(version_part.trim_end_matches('+').to_string()) } else if version_part.contains("..") { @@ -285,7 +279,7 @@ impl PackageRequest { version_constraint: VersionConstraint::Any, }) } - + /// Check if a version matches this constraint pub fn matches(&self, version: &str) -> bool { match &self.version_constraint { @@ -345,14 +339,18 @@ mod tests { fn parse_range_version() { let req = PackageRequest::parse("maya-2024..2025").unwrap(); assert_eq!(req.name, "maya"); - assert!(matches!(req.version_constraint, VersionConstraint::Range(a, b) if a == "2024" && b == "2025")); + assert!( + matches!(req.version_constraint, VersionConstraint::Range(a, b) if a == "2024" && b == "2025") + ); } #[test] fn parse_oneof_version() { let req = PackageRequest::parse("python-3.10|3.11").unwrap(); assert_eq!(req.name, "python"); - assert!(matches!(req.version_constraint, VersionConstraint::OneOf(ref v) if v == &["3.10", "3.11"])); + assert!( + matches!(req.version_constraint, VersionConstraint::OneOf(ref v) if v == &["3.10", "3.11"]) + ); } #[test] @@ -475,7 +473,10 @@ mod tests { root: PathBuf::from("/opt/maya"), }; let env = HashMap::new(); - assert_eq!(pkg.expand_env_value("${NAME}-${VERSION}", &env), "maya-2024"); + assert_eq!( + pkg.expand_env_value("${NAME}-${VERSION}", &env), + "maya-2024" + ); } #[test] diff --git a/src/resolver.rs b/src/resolver.rs index c3ad983..75e59e2 100644 --- a/src/resolver.rs +++ b/src/resolver.rs @@ -119,7 +119,6 @@ impl Resolver { // don't share a cache (e.g. different filters or package paths). let salt = format!("{:?}{:?}", self.config.package_paths, self.config.filters); - // Try cache if !refresh { if let Some(cached) = cache::load(&paths, &salt) { self.package_cache = cached; @@ -131,11 +130,10 @@ impl Resolver { info!("Bypassing package scan cache (--refresh)"); } - // Full scan self.scan_packages()?; self.apply_filters(); - // Save to cache (best-effort, before filter so cache stores everything) + // Best-effort: a save failure shouldn't block resolution. if let Err(e) = cache::save(&paths, &salt, &self.package_cache) { debug!("Failed to save cache: {}", e); } @@ -150,8 +148,7 @@ impl Resolver { return; } - self.package_cache - .retain(|name, _| filters.allows(name)); + self.package_cache.retain(|name, _| filters.allows(name)); } /// Scan package paths and load all packages. @@ -223,7 +220,6 @@ impl Resolver { let mut resolved: Vec = Vec::new(); let mut seen: std::collections::HashSet = std::collections::HashSet::new(); - // Expand aliases let mut expanded_requests: Vec = Vec::new(); for req in requests { if let Some(alias_packages) = self.config.resolve_alias(req) { @@ -233,7 +229,6 @@ impl Resolver { } } - // Resolve each request for req_str in &expanded_requests { let request = PackageRequest::parse(req_str) .with_context(|| format!("Invalid package request: {}", req_str))?; @@ -273,7 +268,9 @@ impl Resolver { /// Find a package matching a request, preferring a pinned version. fn find_package(&self, request: &PackageRequest) -> Result { - let versions = self.package_cache.get(&request.name) + let versions = self + .package_cache + .get(&request.name) .ok_or_else(|| anyhow::anyhow!("Package not found: {}", request.name))?; // Lockfile pin takes priority @@ -324,7 +321,9 @@ impl Resolver { /// List versions of a specific package pub fn list_versions(&self, name: &str) -> Result> { - let versions = self.package_cache.get(name) + let versions = self + .package_cache + .get(name) .ok_or_else(|| anyhow::anyhow!("Package not found: {}", name))?; let mut version_list: Vec = versions.keys().cloned().collect(); @@ -407,8 +406,7 @@ fn check_executable(program: &str) -> std::result::Result<(), String> { #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; - let meta = std::fs::metadata(&resolved) - .map_err(|e| format!("stat failed: {}", e))?; + let meta = std::fs::metadata(&resolved).map_err(|e| format!("stat failed: {}", e))?; if !meta.is_file() { return Err("not a regular file".into()); } @@ -418,8 +416,7 @@ fn check_executable(program: &str) -> std::result::Result<(), String> { } #[cfg(not(unix))] { - let meta = std::fs::metadata(&resolved) - .map_err(|e| format!("stat failed: {}", e))?; + let meta = std::fs::metadata(&resolved).map_err(|e| format!("stat failed: {}", e))?; if !meta.is_file() { return Err("not a regular file".into()); } diff --git a/src/shell.rs b/src/shell.rs index 54c4eb9..18419ea 100644 --- a/src/shell.rs +++ b/src/shell.rs @@ -13,21 +13,17 @@ pub const SHIM_DIR_PREFIX: &str = "anvil-shell-"; /// Detect the user's preferred shell pub fn detect_shell() -> String { - // Check SHELL environment variable if let Ok(shell) = std::env::var("SHELL") { return shell; } - - // Platform defaults + if cfg!(target_os = "windows") { - // Check for PowerShell first if which::which("pwsh").is_ok() { return "pwsh".to_string(); } return "cmd".to_string(); } - - // Unix default + "bash".to_string() } @@ -37,34 +33,31 @@ pub fn spawn_shell(shell: &str, env: &HashMap) -> Result<()> { .file_name() .and_then(|s| s.to_str()) .unwrap_or(shell); - + println!("Starting {} shell with resolved environment...", shell_name); println!("Type 'exit' to return to your original shell.\n"); - + let mut cmd = Command::new(shell); - - // Set up environment + cmd.env_clear(); cmd.envs(env); - - // Add anvil indicator to prompt + + // Tag the prompt so users see when they're inside an anvil shell. if let Some(prompt) = env.get("PS1") { let new_prompt = format!("[anvil] {}", prompt); cmd.env("PS1", new_prompt); } else { - // Set a simple prompt for bash cmd.env("PS1", "[anvil] \\u@\\h:\\w\\$ "); } - - // Platform-specific setup + cfg_if::cfg_if! { if #[cfg(unix)] { use std::os::unix::process::CommandExt; - // Replace current process with shell + // Replace the current process with the shell so the user's exit + // returns them to their original parent. let err = cmd.exec(); Err(err.into()) } else { - // Windows: spawn and wait let status = cmd.status()?; if !status.success() { anyhow::bail!("Shell exited with status: {:?}", status.code()); @@ -80,12 +73,11 @@ pub fn generate_env_script(shell: &str, env: &HashMap) -> String .file_name() .and_then(|s| s.to_str()) .unwrap_or(shell); - + match shell_name { "bash" | "sh" | "zsh" => { let mut script = String::new(); for (key, value) in env { - // Escape special characters let escaped = value.replace('\\', "\\\\").replace('"', "\\\""); script.push_str(&format!("export {}=\"{}\"\n", key, escaped)); } @@ -115,7 +107,6 @@ pub fn generate_env_script(shell: &str, env: &HashMap) -> String script } _ => { - // Default to bash-style let mut script = String::new(); for (key, value) in env { let escaped = value.replace('\\', "\\\\").replace('"', "\\\""); @@ -127,8 +118,8 @@ pub fn generate_env_script(shell: &str, env: &HashMap) -> String } /// Write a PATH shim for each `(alias, command)` pair into a fresh tempdir -/// and return the path. The tempdir is *leaked* on purpose — its lifetime -/// is the interactive subshell the caller is about to spawn, and the sweeper +/// and return the path. The tempdir is *leaked* on purpose: its lifetime is +/// the interactive subshell the caller is about to spawn, and the sweeper /// reclaims it on the next `anvil shell` invocation. /// /// On POSIX the shim is a `chmod 755` shebang script; on Windows it's a @@ -155,8 +146,8 @@ pub fn materialize_commands(commands: &HashMap) -> Result Result<()> { use std::os::unix::fs::PermissionsExt; - // `target` is already the expanded command string — e.g. a bare path or - // `"/Applications/... Painter" --flag`. `exec "$@"` with the command + // `target` is already the expanded command string (e.g. a bare path or + // `"/Applications/... Painter" --flag`). `exec "$@"` with the command // embedded raw preserves any baked-in arguments and lets the user append // their own. let script = format!("#!/usr/bin/env bash\nexec {} \"$@\"\n", target); @@ -192,7 +183,9 @@ pub fn sweep_stale_shims_in(root: &Path, ttl: std::time::Duration) { for entry in entries.flatten() { let name = entry.file_name(); - let Some(name_str) = name.to_str() else { continue }; + let Some(name_str) = name.to_str() else { + continue; + }; if !name_str.starts_with(SHIM_DIR_PREFIX) { continue; } @@ -202,7 +195,9 @@ pub fn sweep_stale_shims_in(root: &Path, ttl: std::time::Duration) { continue; } let Ok(mtime) = meta.modified() else { continue }; - let Ok(age) = now.duration_since(mtime) else { continue }; + let Ok(age) = now.duration_since(mtime) else { + continue; + }; if age < ttl { continue; } diff --git a/tests/cli.rs b/tests/cli.rs index 39a8d73..89d6052 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -174,11 +174,10 @@ fn validate_all() { // (e.g. /usr/autodesk/maya2024/bin/maya), so validate reports // command warnings but still succeeds. Dependencies do resolve. let (_dir, cfg) = setup_env(); - anvil(&cfg) - .args(["validate"]) - .assert() - .success() - .stdout(predicate::str::contains("All dependencies resolve").or(predicate::str::contains("All packages valid!"))); + anvil(&cfg).args(["validate"]).assert().success().stdout( + predicate::str::contains("All dependencies resolve") + .or(predicate::str::contains("All packages valid!")), + ); } #[test] @@ -325,7 +324,7 @@ fn run_quoted_tokens_in_alias() { #[test] fn flat_and_nested_coexist() { let (_dir, cfg) = setup_env(); - // maya is flat, python is nested — both should appear + // maya is flat, python is nested; both should appear anvil(&cfg) .args(["list"]) .assert() @@ -423,7 +422,13 @@ fn context_save_and_show() { // Save anvil(&cfg) - .args(["context", "save", "maya-2024", "-o", ctx_path.to_str().unwrap()]) + .args([ + "context", + "save", + "maya-2024", + "-o", + ctx_path.to_str().unwrap(), + ]) .assert() .success() .stdout(predicate::str::contains("Saved context")); @@ -445,7 +450,13 @@ fn context_show_json() { let ctx_path = dir.path().join("test.ctx.json"); anvil(&cfg) - .args(["context", "save", "maya-2024", "-o", ctx_path.to_str().unwrap()]) + .args([ + "context", + "save", + "maya-2024", + "-o", + ctx_path.to_str().unwrap(), + ]) .assert() .success(); @@ -462,7 +473,13 @@ fn context_show_export() { let ctx_path = dir.path().join("test.ctx.json"); anvil(&cfg) - .args(["context", "save", "maya-2024", "-o", ctx_path.to_str().unwrap()]) + .args([ + "context", + "save", + "maya-2024", + "-o", + ctx_path.to_str().unwrap(), + ]) .assert() .success(); @@ -866,7 +883,12 @@ fn publish_refuses_overwrite() { Command::cargo_bin("anvil") .unwrap() .env("RUST_LOG", "anvil=error") - .args(["publish", target.to_str().unwrap(), "--path", src.to_str().unwrap()]) + .args([ + "publish", + target.to_str().unwrap(), + "--path", + src.to_str().unwrap(), + ]) .assert() .success(); @@ -874,7 +896,12 @@ fn publish_refuses_overwrite() { Command::cargo_bin("anvil") .unwrap() .env("RUST_LOG", "anvil=error") - .args(["publish", target.to_str().unwrap(), "--path", src.to_str().unwrap()]) + .args([ + "publish", + target.to_str().unwrap(), + "--path", + src.to_str().unwrap(), + ]) .assert() .failure(); } @@ -917,7 +944,9 @@ fn list_hint_when_package_paths_missing() { .args(["list"]) .assert() .success() - .stderr(predicate::str::contains("None of the configured package_paths exist")); + .stderr(predicate::str::contains( + "None of the configured package_paths exist", + )); } #[test]