Skip to content

Commit 3900092

Browse files
authored
Merge pull request #7 from voidreamer/claude/windows-feedback-fixes
Release 0.4.2: cross-platform env expansion fixes
2 parents 3a9ece3 + abe1660 commit 3900092

4 files changed

Lines changed: 148 additions & 11 deletions

File tree

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "anvil-env"
3-
version = "0.4.1"
3+
version = "0.4.2"
44
edition = "2021"
55
authors = ["Alejandro Cabrera <voidreamer@gmail.com>"]
66
description = "A lightweight environment and configuration manager for VFX/Animation pipelines"

README.md

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -132,11 +132,16 @@ only on the last hyphen when the suffix starts with a digit.
132132

133133
### Environment expansion
134134

135-
Values resolve in this order: `${PACKAGE_ROOT}`, `${VERSION}`, `${NAME}`, then
136-
any `${VAR}` set by previously resolved packages or the inherited environment,
137-
and finally a leading `~/`. When two packages set the same variable without
138-
referencing `${VAR}` on the right, anvil emits a conflict warning so a silent
139-
overwrite does not slip through.
135+
Values resolve in this order: `${PACKAGE_ROOT}`, `${VERSION}`, `${NAME}`,
136+
`${PATHSEP}` (`:` on Unix / `;` on Windows), `${EXE_SUFFIX}` (`""` on Unix /
137+
`".exe"` on Windows), then any `${VAR}` set by previously resolved packages or
138+
the inherited environment, and finally `~/` — which expands at every path
139+
segment, so `~/USD/bin${PATHSEP}~/USD/lib` works as expected. On Windows
140+
PowerShell sessions `~/` falls back to `USERPROFILE` when `HOME` is unset.
141+
142+
When two packages set the same variable without referencing `${VAR}` on the
143+
right, anvil emits a conflict warning so a silent overwrite does not slip
144+
through.
140145

141146
### Command aliases
142147

src/main.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
55
use anyhow::{Context, Result};
66
use clap::Parser;
7+
use tracing::info;
78
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
89

910
mod cache;
@@ -190,6 +191,10 @@ fn cmd_run(
190191
// Pre-run hooks
191192
Config::run_hooks(&config.hooks.pre_run, &env)?;
192193

194+
// Surface the resolved argv at `-v`/`-vv` so when an exec fails with
195+
// "file not found" the user can see what anvil actually tried to run.
196+
info!("exec: {} {:?}", executable, all_args);
197+
193198
let status = Command::new(&executable)
194199
.args(&all_args)
195200
.envs(&env)

src/package.rs

Lines changed: 132 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,18 @@ use anyhow::{Context, Result};
77
use indexmap::IndexMap;
88
use serde::{Deserialize, Serialize};
99

10+
/// Platform-native path-list separator, exposed in yaml as `${PATHSEP}`.
11+
#[cfg(target_os = "windows")]
12+
pub const PATHSEP: &str = ";";
13+
#[cfg(not(target_os = "windows"))]
14+
pub const PATHSEP: &str = ":";
15+
16+
/// Platform-native executable suffix, exposed in yaml as `${EXE_SUFFIX}`.
17+
#[cfg(target_os = "windows")]
18+
pub const EXE_SUFFIX: &str = ".exe";
19+
#[cfg(not(target_os = "windows"))]
20+
pub const EXE_SUFFIX: &str = "";
21+
1022
/// A package definition
1123
#[derive(Debug, Clone, Serialize, Deserialize)]
1224
pub struct Package {
@@ -133,6 +145,11 @@ impl Package {
133145
// Replace ${NAME} with package name
134146
result = result.replace("${NAME}", &self.name);
135147

148+
// Platform-aware builtins so a single yaml line can compose path
149+
// lists or binary names without a `variants:` fork per platform.
150+
result = result.replace("${PATHSEP}", PATHSEP);
151+
result = result.replace("${EXE_SUFFIX}", EXE_SUFFIX);
152+
136153
// Replace other ${VAR} references
137154
for (key, val) in env {
138155
result = result.replace(&format!("${{{}}}", key), val);
@@ -145,11 +162,19 @@ impl Package {
145162
std::env::var(var).unwrap_or_default()
146163
}).to_string();
147164

148-
// Expand ~ to home directory
149-
if result.starts_with("~/") {
150-
if let Ok(home) = std::env::var("HOME") {
151-
result = format!("{}{}", home, &result[1..]);
152-
}
165+
// Expand `~/` everywhere it appears at a segment boundary
166+
// (start-of-value, or after `:` / `;`). Path-list values like
167+
// `~/USD/bin;~/USD/lib` need every occurrence expanded, not just
168+
// the first. `dirs::home_dir()` resolves via `USERPROFILE` on
169+
// Windows when `HOME` is unset (PowerShell sessions).
170+
if let Some(home) = dirs::home_dir() {
171+
let home_str = home.to_string_lossy();
172+
let tilde_re = regex::Regex::new(r"(^|[:;])~/").unwrap();
173+
result = tilde_re
174+
.replace_all(&result, |caps: &regex::Captures| {
175+
format!("{}{}/", &caps[1], home_str)
176+
})
177+
.to_string();
153178
}
154179

155180
result
@@ -453,6 +478,108 @@ mod tests {
453478
assert_eq!(pkg.expand_env_value("${NAME}-${VERSION}", &env), "maya-2024");
454479
}
455480

481+
#[test]
482+
fn expand_pathsep_builtin() {
483+
let pkg = Package {
484+
name: "test".into(),
485+
version: "1.0".into(),
486+
description: None,
487+
requires: vec![],
488+
environment: IndexMap::new(),
489+
commands: HashMap::new(),
490+
variants: vec![],
491+
root: PathBuf::from("/tmp"),
492+
};
493+
let env = HashMap::new();
494+
let expected = if cfg!(target_os = "windows") {
495+
"/a;/b;/c"
496+
} else {
497+
"/a:/b:/c"
498+
};
499+
assert_eq!(
500+
pkg.expand_env_value("/a${PATHSEP}/b${PATHSEP}/c", &env),
501+
expected
502+
);
503+
}
504+
505+
#[test]
506+
fn expand_exe_suffix_builtin() {
507+
let pkg = Package {
508+
name: "test".into(),
509+
version: "1.0".into(),
510+
description: None,
511+
requires: vec![],
512+
environment: IndexMap::new(),
513+
commands: HashMap::new(),
514+
variants: vec![],
515+
root: PathBuf::from("/tmp"),
516+
};
517+
let env = HashMap::new();
518+
let expected = if cfg!(target_os = "windows") {
519+
"blender.exe"
520+
} else {
521+
"blender"
522+
};
523+
assert_eq!(pkg.expand_env_value("blender${EXE_SUFFIX}", &env), expected);
524+
}
525+
526+
#[test]
527+
fn expand_tilde_at_every_segment() {
528+
// `~` should expand at the start of every path segment, not just the
529+
// first occurrence in the value. Path-list values like
530+
// `~/USD/bin;~/USD/lib` were leaving the second `~` literal before.
531+
let pkg = Package {
532+
name: "test".into(),
533+
version: "1.0".into(),
534+
description: None,
535+
requires: vec![],
536+
environment: IndexMap::new(),
537+
commands: HashMap::new(),
538+
variants: vec![],
539+
root: PathBuf::from("/tmp"),
540+
};
541+
let env = HashMap::new();
542+
let home = dirs::home_dir().expect("test needs a HOME");
543+
let home_str = home.to_string_lossy();
544+
545+
// Unix-style separator
546+
let unix_in = "~/a:~/b:~/c";
547+
let unix_out = pkg.expand_env_value(unix_in, &env);
548+
assert_eq!(
549+
unix_out,
550+
format!("{h}/a:{h}/b:{h}/c", h = home_str),
551+
"Unix-style path list should expand every ~"
552+
);
553+
554+
// Windows-style separator
555+
let win_in = "~/a;~/b;~/c";
556+
let win_out = pkg.expand_env_value(win_in, &env);
557+
assert_eq!(
558+
win_out,
559+
format!("{h}/a;{h}/b;{h}/c", h = home_str),
560+
"Windows-style path list should expand every ~"
561+
);
562+
}
563+
564+
#[test]
565+
fn expand_tilde_only_at_segment_boundary() {
566+
// A `~` that's not at a segment boundary (e.g. embedded in a word)
567+
// should be left alone.
568+
let pkg = Package {
569+
name: "test".into(),
570+
version: "1.0".into(),
571+
description: None,
572+
requires: vec![],
573+
environment: IndexMap::new(),
574+
commands: HashMap::new(),
575+
variants: vec![],
576+
root: PathBuf::from("/tmp"),
577+
};
578+
let env = HashMap::new();
579+
// No `~/` at start or after `:` / `;`, so nothing should change.
580+
assert_eq!(pkg.expand_env_value("backup~/file", &env), "backup~/file");
581+
}
582+
456583
#[test]
457584
fn expand_from_env_map() {
458585
let pkg = Package {

0 commit comments

Comments
 (0)