Skip to content

Commit 23767e5

Browse files
committed
fix: avoid set_env panic on Windows doctor
1 parent 7c6da8e commit 23767e5

1 file changed

Lines changed: 135 additions & 5 deletions

File tree

src/commands/doctor.rs

Lines changed: 135 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,20 @@ pub fn exec(meta: &mut DvmMeta) -> Result<()> {
1111
// Init enviroments if need
1212
// actually set DVM_DIR env var if not exist.
1313
let home_path = dvm_root();
14-
set_env::check_or_set("DVM_DIR", home_path.to_str().unwrap()).unwrap();
15-
let path = set_env::get("PATH").unwrap();
14+
check_or_set_env("DVM_DIR", home_path.to_str().unwrap())?;
15+
let path = get_env("PATH")?;
1616
let looking_for = deno_bin_path().parent().unwrap().to_str().unwrap().to_string();
1717
let current = which::which("deno");
1818

1919
if let Ok(current) = current {
2020
if current.to_str().unwrap().starts_with(&looking_for) {
2121
println!("{}", "DVM deno bin is already set correctly.".green());
2222
} else {
23-
set_env::prepend("PATH", looking_for.as_str()).unwrap();
23+
prepend_env_path(looking_for.as_str())?;
2424
println!("{}", "Please restart your shell of choice to take effects.".red());
2525
}
26-
} else if !path.contains(looking_for.as_str()) {
27-
set_env::prepend("PATH", looking_for.as_str()).unwrap();
26+
} else if !env_path_contains(&path, looking_for.as_str()) {
27+
prepend_env_path(looking_for.as_str())?;
2828
println!("{}", "Please restart your shell of choice to take effects.".red());
2929
}
3030

@@ -65,3 +65,133 @@ pub fn exec(meta: &mut DvmMeta) -> Result<()> {
6565
println!("{}", "All fixes applied, DVM is ready to use.".green());
6666
Ok(())
6767
}
68+
69+
#[cfg(not(windows))]
70+
fn check_or_set_env(name: &str, value: &str) -> Result<()> {
71+
set_env::check_or_set(name, value).map_err(Into::into)
72+
}
73+
74+
#[cfg(not(windows))]
75+
fn get_env(name: &str) -> Result<String> {
76+
set_env::get(name).map_err(Into::into)
77+
}
78+
79+
#[cfg(not(windows))]
80+
fn prepend_env_path(value: &str) -> Result<()> {
81+
set_env::prepend("PATH", value).map_err(Into::into)
82+
}
83+
84+
#[cfg(not(windows))]
85+
fn env_path_contains(path: &str, value: &str) -> bool {
86+
path.contains(value)
87+
}
88+
89+
#[cfg(windows)]
90+
fn check_or_set_env(name: &str, value: &str) -> Result<()> {
91+
if std::env::var_os(name).is_none() {
92+
set_user_env(name, value)?;
93+
}
94+
Ok(())
95+
}
96+
97+
#[cfg(windows)]
98+
fn get_env(name: &str) -> Result<String> {
99+
std::env::var(name).map_err(Into::into)
100+
}
101+
102+
#[cfg(windows)]
103+
fn prepend_env_path(value: &str) -> Result<()> {
104+
let user_path = get_user_env("Path")?.unwrap_or_default();
105+
let new_user_path = prepend_path_value(&user_path, value);
106+
107+
set_user_env("Path", &new_user_path)?;
108+
109+
let process_path = std::env::var("PATH").unwrap_or_default();
110+
std::env::set_var("PATH", prepend_path_value(&process_path, value));
111+
112+
Ok(())
113+
}
114+
115+
#[cfg(windows)]
116+
fn get_user_env(name: &str) -> Result<Option<String>> {
117+
let output = std::process::Command::new("powershell.exe")
118+
.arg("-NoLogo")
119+
.arg("-NoProfile")
120+
.arg("-NonInteractive")
121+
.arg("-Command")
122+
.arg("[Environment]::GetEnvironmentVariable($args[0], 'User')")
123+
.arg(name)
124+
.output()?;
125+
126+
if !output.status.success() {
127+
anyhow::bail!("Failed to read user environment variable {}", name);
128+
}
129+
130+
let value = String::from_utf8(output.stdout)?.trim().to_string();
131+
Ok((!value.is_empty()).then_some(value))
132+
}
133+
134+
#[cfg(windows)]
135+
fn set_user_env(name: &str, value: &str) -> Result<()> {
136+
let status = std::process::Command::new("powershell.exe")
137+
.arg("-NoLogo")
138+
.arg("-NoProfile")
139+
.arg("-NonInteractive")
140+
.arg("-Command")
141+
.arg("[Environment]::SetEnvironmentVariable($args[0], $args[1], 'User')")
142+
.arg(name)
143+
.arg(value)
144+
.status()?;
145+
146+
if !status.success() {
147+
anyhow::bail!("Failed to set user environment variable {}", name);
148+
}
149+
150+
std::env::set_var(name, value);
151+
Ok(())
152+
}
153+
154+
#[cfg(windows)]
155+
fn path_contains(path: &str, value: &str) -> bool {
156+
path.split(';').any(|item| item.eq_ignore_ascii_case(value))
157+
}
158+
159+
#[cfg(windows)]
160+
fn env_path_contains(path: &str, value: &str) -> bool {
161+
path_contains(path, value)
162+
}
163+
164+
#[cfg(windows)]
165+
fn prepend_path_value(path: &str, value: &str) -> String {
166+
let rest = path
167+
.split(';')
168+
.filter(|item| !item.is_empty() && !item.eq_ignore_ascii_case(value))
169+
.collect::<Vec<_>>()
170+
.join(";");
171+
172+
if rest.is_empty() {
173+
value.to_string()
174+
} else {
175+
format!("{};{}", value, rest)
176+
}
177+
}
178+
179+
#[cfg(all(test, windows))]
180+
mod tests {
181+
use super::*;
182+
183+
#[test]
184+
fn prepend_path_value_moves_existing_entry_to_front() {
185+
assert_eq!(
186+
prepend_path_value(
187+
"C:\\Windows;C:\\Users\\me\\.dvm\\bin;C:\\Tools",
188+
"C:\\Users\\me\\.dvm\\bin"
189+
),
190+
"C:\\Users\\me\\.dvm\\bin;C:\\Windows;C:\\Tools"
191+
);
192+
assert_eq!(
193+
prepend_path_value("C:\\Windows;C:\\Tools", "C:\\Users\\me\\.dvm\\bin"),
194+
"C:\\Users\\me\\.dvm\\bin;C:\\Windows;C:\\Tools"
195+
);
196+
}
197+
}

0 commit comments

Comments
 (0)