Skip to content

Commit e132fc4

Browse files
committed
refactor(shared): centralize directory overrides
1 parent d774ed5 commit e132fc4

6 files changed

Lines changed: 230 additions & 136 deletions

File tree

‎crates/vp_installer/src/main.rs‎

Lines changed: 14 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -616,43 +616,12 @@ fn prepare_dirs(opts: &cli::Options) -> Result<VpDirs, Box<dyn std::error::Error
616616
.ok_or("The installation directory must be an absolute path")?;
617617
// Safety: called in main() before any threads are spawned (or under
618618
// EnvConfig::with_vars in tests, which serializes env mutation).
619-
unsafe { std::env::set_var("VP_HOME", abs.as_path()) };
619+
unsafe { std::env::set_var(vp_shared::env_vars::VP_HOME, abs.as_path()) };
620620
}
621-
validate_dir_env()?;
621+
vp_shared::validate_vp_dir_env()?;
622622
Ok(vp_shared::EnvConfig::get().dirs.clone())
623623
}
624624

625-
fn validate_dir_env() -> Result<(), Box<dyn std::error::Error>> {
626-
use vp_shared::env_vars;
627-
628-
let env_path = |name| std::env::var_os(name).filter(|value| !value.is_empty());
629-
if let Some(home) = env_path(env_vars::VP_HOME)
630-
&& !std::path::Path::new(&home).is_absolute()
631-
{
632-
return Err(format!("{} must be an absolute path.", env_vars::VP_HOME).into());
633-
}
634-
635-
let split_dirs = [
636-
(env_vars::VP_BIN_DIR, env_path(env_vars::VP_BIN_DIR)),
637-
(env_vars::VP_DATA_DIR, env_path(env_vars::VP_DATA_DIR)),
638-
(env_vars::VP_CACHE_DIR, env_path(env_vars::VP_CACHE_DIR)),
639-
];
640-
let configured_count = split_dirs.iter().filter(|(_, value)| value.is_some()).count();
641-
if configured_count != 0 && configured_count != split_dirs.len() {
642-
return Err(
643-
"Set VP_BIN_DIR, VP_DATA_DIR, and VP_CACHE_DIR together, or leave all three unset."
644-
.into(),
645-
);
646-
}
647-
for (name, value) in split_dirs {
648-
if value.is_some_and(|path| !std::path::Path::new(&path).is_absolute()) {
649-
return Err(format!("{name} must be an absolute path.").into());
650-
}
651-
}
652-
653-
Ok(())
654-
}
655-
656625
#[allow(clippy::print_stdout)]
657626
fn modify_path(bin_dir: &str, quiet: bool) -> Result<(), Box<dyn std::error::Error>> {
658627
#[cfg(windows)]
@@ -961,11 +930,9 @@ mod tests {
961930
}
962931

963932
#[test]
964-
fn incomplete_vp_dir_groups_are_rejected_without_creating_roots() {
933+
fn invalid_vp_dir_env_is_rejected_without_creating_roots() {
965934
let tmp = tempfile::tempdir().unwrap();
966-
let bin = tmp.path().join("requested-bin");
967935
let data = tmp.path().join("requested-data");
968-
let cache = tmp.path().join("requested-cache");
969936

970937
with_clean_home(tmp.path(), || {
971938
let default_dirs = EnvConfig::get().dirs.clone();
@@ -977,91 +944,28 @@ mod tests {
977944
default_dirs.state.as_path().to_path_buf(),
978945
];
979946
let default_existed = default_roots.each_ref().map(|root| root.exists());
980-
let paths = [bin.as_os_str(), data.as_os_str(), cache.as_os_str()];
981-
let cases = [
982-
[Some(paths[0]), None, None],
983-
[None, Some(paths[1]), None],
984-
[None, None, Some(paths[2])],
985-
[Some(paths[0]), Some(paths[1]), None],
986-
[Some(paths[0]), None, Some(paths[2])],
987-
[None, Some(paths[1]), Some(paths[2])],
988-
];
989-
990-
for values in cases {
991-
EnvConfig::with_vars(
992-
[
993-
(env_vars::VP_HOME, None),
994-
(env_vars::VP_BIN_DIR, values[0]),
995-
(env_vars::VP_DATA_DIR, values[1]),
996-
(env_vars::VP_CACHE_DIR, values[2]),
997-
],
998-
|_| {
999-
let error = prepare_dirs(&opts(None)).unwrap_err().to_string();
1000-
assert_eq!(
1001-
error,
1002-
"Set VP_BIN_DIR, VP_DATA_DIR, and VP_CACHE_DIR together, or leave all three unset."
1003-
);
1004-
},
1005-
);
1006-
}
1007-
1008-
for requested in [&bin, &data, &cache] {
1009-
assert!(!requested.exists(), "validation created {}", requested.display());
1010-
}
1011-
for (root, existed) in default_roots.iter().zip(default_existed) {
1012-
if !existed {
1013-
assert!(!root.exists(), "validation created default root {}", root.display());
1014-
}
1015-
}
1016-
});
1017-
}
1018-
1019-
#[test]
1020-
fn relative_directory_overrides_are_rejected() {
1021-
let tmp = tempfile::tempdir().unwrap();
1022-
with_clean_home(tmp.path(), || {
1023947
EnvConfig::with_vars(
1024948
[
1025-
(env_vars::VP_HOME, Some(std::ffi::OsStr::new("relative-home"))),
949+
(env_vars::VP_HOME, None),
1026950
(env_vars::VP_BIN_DIR, None),
1027-
(env_vars::VP_DATA_DIR, None),
951+
(env_vars::VP_DATA_DIR, Some(data.as_os_str())),
1028952
(env_vars::VP_CACHE_DIR, None),
1029953
],
1030954
|_| {
1031955
let error = prepare_dirs(&opts(None)).unwrap_err().to_string();
1032-
assert_eq!(error, "VP_HOME must be an absolute path.");
956+
assert_eq!(
957+
error,
958+
"Set VP_BIN_DIR, VP_DATA_DIR, and VP_CACHE_DIR together, or leave all three unset."
959+
);
1033960
},
1034961
);
1035962

1036-
let bin = tmp.path().join("bin");
1037-
let data = tmp.path().join("data");
1038-
let cache = tmp.path().join("cache");
1039-
let relative = std::ffi::OsStr::new("relative-dir");
1040-
let cases = [
1041-
(env_vars::VP_BIN_DIR, [relative, data.as_os_str(), cache.as_os_str()]),
1042-
(env_vars::VP_DATA_DIR, [bin.as_os_str(), relative, cache.as_os_str()]),
1043-
(env_vars::VP_CACHE_DIR, [bin.as_os_str(), data.as_os_str(), relative]),
1044-
];
1045-
for (name, values) in cases {
1046-
EnvConfig::with_vars(
1047-
[
1048-
(env_vars::VP_HOME, None),
1049-
(env_vars::VP_BIN_DIR, Some(values[0])),
1050-
(env_vars::VP_DATA_DIR, Some(values[1])),
1051-
(env_vars::VP_CACHE_DIR, Some(values[2])),
1052-
],
1053-
|_| {
1054-
let error = prepare_dirs(&opts(None)).unwrap_err().to_string();
1055-
assert_eq!(error, format!("{name} must be an absolute path."));
1056-
},
1057-
);
1058-
}
1059-
1060-
for root in [&bin, &data, &cache] {
1061-
assert!(!root.exists(), "validation created {}", root.display());
963+
assert!(!data.exists(), "validation created {}", data.display());
964+
for (root, existed) in default_roots.iter().zip(default_existed) {
965+
if !existed {
966+
assert!(!root.exists(), "validation created default root {}", root.display());
967+
}
1062968
}
1063-
assert!(!tmp.path().join("relative-home").exists());
1064-
assert!(!tmp.path().join("relative-dir").exists());
1065969
});
1066970
}
1067971

‎crates/vp_shared/src/dirs.rs‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,17 @@
1010
//! `<CONFIG>/`, and `<STATE>/` for category roots. They do not use a concrete
1111
//! path for each layout. See `rfcs/directory-layout.md`.
1212
13+
mod env_overrides;
1314
mod resolution;
1415

16+
pub use env_overrides::{VpDirEnvError, validate_vp_dir_env};
1517
use vt_path::{AbsolutePath, AbsolutePathBuf};
1618

19+
/// Absolute `VP_HOME` override from the process environment, if set.
20+
pub(crate) fn vp_home_override() -> Option<AbsolutePathBuf> {
21+
env_overrides::DirEnvOverrides::from_env().home()
22+
}
23+
1724
/// Platform-specific binary name for the `vp` CLI.
1825
pub const VP_BINARY_NAME: &str = if cfg!(windows) { "vp.exe" } else { "vp" };
1926

@@ -146,8 +153,7 @@ impl VpDirs {
146153
/// payload cannot report category roots through `VP_DUMP_DIRS`.
147154
#[must_use]
148155
pub fn legacy_single_root(home: &AbsolutePath) -> Self {
149-
let root = resolution::vp_home_override()
150-
.unwrap_or_else(|| home.join(resolution::VP_HOME_DIR_NAME));
156+
let root = vp_home_override().unwrap_or_else(|| home.join(resolution::VP_HOME_DIR_NAME));
151157
resolution::single_root_dirs(root)
152158
}
153159

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
use thiserror::Error;
2+
use vt_path::AbsolutePathBuf;
3+
4+
use crate::env_vars;
5+
6+
enum PathOverride {
7+
Unset,
8+
Absolute(AbsolutePathBuf),
9+
Relative,
10+
}
11+
12+
impl PathOverride {
13+
fn from_env(name: &str) -> Self {
14+
let Some(path) = std::env::var_os(name).filter(|value| !value.is_empty()) else {
15+
return Self::Unset;
16+
};
17+
AbsolutePathBuf::new(path.into()).map_or(Self::Relative, Self::Absolute)
18+
}
19+
20+
fn absolute(&self) -> Option<&AbsolutePathBuf> {
21+
match self {
22+
Self::Absolute(path) => Some(path),
23+
Self::Unset | Self::Relative => None,
24+
}
25+
}
26+
27+
fn is_set(&self) -> bool {
28+
!matches!(self, Self::Unset)
29+
}
30+
31+
fn is_relative(&self) -> bool {
32+
matches!(self, Self::Relative)
33+
}
34+
}
35+
36+
/// One snapshot of the Vite+ directory override variables.
37+
pub(super) struct DirEnvOverrides {
38+
home: PathOverride,
39+
bin: PathOverride,
40+
data: PathOverride,
41+
cache: PathOverride,
42+
}
43+
44+
impl DirEnvOverrides {
45+
pub(super) fn from_env() -> Self {
46+
Self {
47+
home: PathOverride::from_env(env_vars::VP_HOME),
48+
bin: PathOverride::from_env(env_vars::VP_BIN_DIR),
49+
data: PathOverride::from_env(env_vars::VP_DATA_DIR),
50+
cache: PathOverride::from_env(env_vars::VP_CACHE_DIR),
51+
}
52+
}
53+
54+
pub(super) fn home(&self) -> Option<AbsolutePathBuf> {
55+
self.home.absolute().cloned()
56+
}
57+
58+
pub(super) fn split_dirs(&self) -> Option<(AbsolutePathBuf, AbsolutePathBuf, AbsolutePathBuf)> {
59+
Some((
60+
self.bin.absolute()?.clone(),
61+
self.data.absolute()?.clone(),
62+
self.cache.absolute()?.clone(),
63+
))
64+
}
65+
66+
fn validate(&self) -> Result<(), VpDirEnvError> {
67+
if self.home.is_relative() {
68+
return Err(VpDirEnvError::RelativePath { name: env_vars::VP_HOME });
69+
}
70+
71+
let split_dirs = [
72+
(env_vars::VP_BIN_DIR, &self.bin),
73+
(env_vars::VP_DATA_DIR, &self.data),
74+
(env_vars::VP_CACHE_DIR, &self.cache),
75+
];
76+
let configured_count = split_dirs.iter().filter(|(_, value)| value.is_set()).count();
77+
if configured_count != 0 && configured_count != split_dirs.len() {
78+
return Err(VpDirEnvError::IncompleteSplitGroup);
79+
}
80+
for (name, value) in split_dirs {
81+
if value.is_relative() {
82+
return Err(VpDirEnvError::RelativePath { name });
83+
}
84+
}
85+
86+
Ok(())
87+
}
88+
}
89+
90+
/// An invalid Vite+ directory override configuration.
91+
#[derive(Debug, Error, PartialEq, Eq)]
92+
pub enum VpDirEnvError {
93+
#[error("Set VP_BIN_DIR, VP_DATA_DIR, and VP_CACHE_DIR together, or leave all three unset.")]
94+
IncompleteSplitGroup,
95+
#[error("{name} must be an absolute path.")]
96+
RelativePath { name: &'static str },
97+
}
98+
99+
/// Validate Vite+ directory overrides for an installer.
100+
///
101+
/// Runtime resolution ignores relative or incomplete overrides and continues
102+
/// to the next source. Installers use this stricter check before they resolve
103+
/// or create installation roots.
104+
pub fn validate_vp_dir_env() -> Result<(), VpDirEnvError> {
105+
DirEnvOverrides::from_env().validate()
106+
}
107+
108+
#[cfg(test)]
109+
mod tests {
110+
use std::ffi::OsStr;
111+
112+
use super::*;
113+
114+
fn validate_with(
115+
home: Option<&OsStr>,
116+
[bin, data, cache]: [Option<&OsStr>; 3],
117+
) -> Result<(), VpDirEnvError> {
118+
temp_env::with_vars(
119+
[
120+
(env_vars::VP_HOME, home),
121+
(env_vars::VP_BIN_DIR, bin),
122+
(env_vars::VP_DATA_DIR, data),
123+
(env_vars::VP_CACHE_DIR, cache),
124+
],
125+
validate_vp_dir_env,
126+
)
127+
}
128+
129+
#[test]
130+
fn installer_validation_accepts_valid_overrides() {
131+
let root = tempfile::tempdir().unwrap();
132+
let bin = root.path().join("bin");
133+
let data = root.path().join("data");
134+
let cache = root.path().join("cache");
135+
136+
assert_eq!(validate_with(None, [None, None, None]), Ok(()));
137+
assert_eq!(validate_with(Some(root.path().as_os_str()), [None, None, None]), Ok(()));
138+
assert_eq!(
139+
validate_with(
140+
None,
141+
[Some(bin.as_os_str()), Some(data.as_os_str()), Some(cache.as_os_str())],
142+
),
143+
Ok(())
144+
);
145+
}
146+
147+
#[test]
148+
fn installer_validation_rejects_incomplete_split_groups() {
149+
let root = tempfile::tempdir().unwrap();
150+
let paths = [root.path().join("bin"), root.path().join("data"), root.path().join("cache")];
151+
let paths = paths.each_ref().map(|path| path.as_os_str());
152+
let cases = [
153+
[Some(paths[0]), None, None],
154+
[None, Some(paths[1]), None],
155+
[None, None, Some(paths[2])],
156+
[Some(paths[0]), Some(paths[1]), None],
157+
[Some(paths[0]), None, Some(paths[2])],
158+
[None, Some(paths[1]), Some(paths[2])],
159+
];
160+
161+
for dirs in cases {
162+
assert_eq!(validate_with(None, dirs), Err(VpDirEnvError::IncompleteSplitGroup));
163+
}
164+
}
165+
166+
#[test]
167+
fn installer_validation_rejects_relative_overrides() {
168+
let root = tempfile::tempdir().unwrap();
169+
let bin = root.path().join("bin");
170+
let data = root.path().join("data");
171+
let cache = root.path().join("cache");
172+
let relative = OsStr::new("relative-dir");
173+
174+
assert_eq!(
175+
validate_with(Some(relative), [None, None, None]),
176+
Err(VpDirEnvError::RelativePath { name: env_vars::VP_HOME })
177+
);
178+
179+
let cases = [
180+
(
181+
env_vars::VP_BIN_DIR,
182+
[Some(relative), Some(data.as_os_str()), Some(cache.as_os_str())],
183+
),
184+
(
185+
env_vars::VP_DATA_DIR,
186+
[Some(bin.as_os_str()), Some(relative), Some(cache.as_os_str())],
187+
),
188+
(
189+
env_vars::VP_CACHE_DIR,
190+
[Some(bin.as_os_str()), Some(data.as_os_str()), Some(relative)],
191+
),
192+
];
193+
for (name, dirs) in cases {
194+
assert_eq!(validate_with(None, dirs), Err(VpDirEnvError::RelativePath { name }));
195+
}
196+
}
197+
}

0 commit comments

Comments
 (0)