339e97baa0
## Summary Closes https://github.com/astral-sh/uv/issues/17331 Certain applications on windows expect to be notified when environment variables change such as conhost.exe (traditional cmd.exe host). Without this notification conhost.exe will not pick up changes to environment variables regardless of how many times conhost.exe is re-launched after running `uv tool update-shell`. ## Test Plan Before this change 1. Removed `%USERPROFILE%\.local\bin` from environment variables via UI (which sends `WM_SETTINGCHANGE`) 2. Launched `%SYSTEMROOT%\System32\conhost.exe` and attempted to run any tool preivously installed. It fails to find any. 3. Ran `uv tool update-shell`. Confirmed `HKEY_CURRENT_USER\Environment\Path` was updated in registry. 4. Launched new `%SYSTEMROOT%\System32\conhost.exe` session. **Fails to find installed tools**. After this change 1. Removed `%USERPROFILE%\.local\bin` from environment variables via UI (which sends `WM_SETTINGCHANGE`) 2. Launched `%SYSTEMROOT%\System32\conhost.exe` and attempted to run any tool preivously installed. It fails to find any. 3. Ran `uv tool update-shell`. Confirmed `HKEY_CURRENT_USER\Environment\Path` was updated in registry. 4. Launched new `%SYSTEMROOT%\System32\conhost.exe` session. **Finds the installed tools**.
116 lines
3.9 KiB
Rust
116 lines
3.9 KiB
Rust
//! Windows-specific utilities for manipulating the environment.
|
|
//!
|
|
//! Based on rustup's Windows implementation: <https://github.com/rust-lang/rustup/commit/bce3ed67d219a2b754857f9e231287794d8c770d>
|
|
|
|
#![cfg(windows)]
|
|
|
|
use std::path::Path;
|
|
|
|
use anyhow::Context;
|
|
use tracing::warn;
|
|
use windows::Win32::Foundation::{ERROR_FILE_NOT_FOUND, ERROR_INVALID_DATA, LPARAM, WPARAM};
|
|
use windows::Win32::UI::WindowsAndMessaging::{
|
|
HWND_BROADCAST, SMTO_ABORTIFHUNG, SendMessageTimeoutW, WM_SETTINGCHANGE,
|
|
};
|
|
use windows::core::{HRESULT, w};
|
|
use windows_registry::{CURRENT_USER, HSTRING};
|
|
|
|
use uv_static::EnvVars;
|
|
|
|
/// Append the given [`Path`] to the `PATH` environment variable in the Windows registry.
|
|
///
|
|
/// Returns `Ok(true)` if the path was successfully appended, and `Ok(false)` if the path was
|
|
/// already in `PATH`.
|
|
pub fn prepend_path(path: &Path) -> anyhow::Result<bool> {
|
|
// Get the existing `PATH` variable from the registry.
|
|
let windows_path = get_windows_path_var()?;
|
|
|
|
// Add the new path to the existing `PATH` variable.
|
|
let windows_path =
|
|
windows_path.and_then(|windows_path| prepend_to_path(&windows_path, HSTRING::from(path)));
|
|
// If the path didn't change, then we don't need to do anything.
|
|
let Some(windows_path) = windows_path else {
|
|
return Ok(false);
|
|
};
|
|
|
|
// Set the `PATH` variable in the registry.
|
|
apply_windows_path_var(&windows_path)?;
|
|
|
|
Ok(true)
|
|
}
|
|
|
|
/// Set the windows `PATH` variable in the registry.
|
|
fn apply_windows_path_var(path: &HSTRING) -> anyhow::Result<()> {
|
|
let environment = CURRENT_USER.create("Environment")?;
|
|
|
|
if path.is_empty() {
|
|
environment.remove_value(EnvVars::PATH)?;
|
|
} else {
|
|
environment.set_expand_hstring(EnvVars::PATH, path)?;
|
|
}
|
|
|
|
// Notify WM_SETTINGCHANGE listeners
|
|
broadcast_environment_changes();
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Broadcast `WM_SETTINGCHANGE` to notify listeners about environment changes.
|
|
///
|
|
/// SAFETY: `SendMessageTimeoutW` is safe to call with these set of parameters.
|
|
/// When modifying environment variables we need to broadcast a `WM_SETTINGCHANGE`
|
|
/// message with lparam set to the string "Environment" to allow processes such
|
|
/// as conhost.exe to pick up the changes made on new sessions.
|
|
///
|
|
/// See <https://learn.microsoft.com/en-us/windows/win32/procthread/environment-variables>
|
|
#[allow(unsafe_code)]
|
|
fn broadcast_environment_changes() {
|
|
unsafe {
|
|
SendMessageTimeoutW(
|
|
HWND_BROADCAST,
|
|
WM_SETTINGCHANGE,
|
|
WPARAM(0),
|
|
LPARAM(w!("Environment").as_ptr() as isize), // null terminated
|
|
SMTO_ABORTIFHUNG,
|
|
5000,
|
|
None,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Retrieve the windows `PATH` variable from the registry.
|
|
///
|
|
/// Returns `Ok(None)` if the `PATH` variable is not a string.
|
|
fn get_windows_path_var() -> anyhow::Result<Option<HSTRING>> {
|
|
let environment = CURRENT_USER
|
|
.create("Environment")
|
|
.context("Failed to open `Environment` key")?;
|
|
|
|
let reg_value = environment.get_hstring(EnvVars::PATH);
|
|
match reg_value {
|
|
Ok(reg_value) => Ok(Some(reg_value)),
|
|
Err(err) if err.code() == HRESULT::from(ERROR_INVALID_DATA) => {
|
|
warn!("`HKEY_CURRENT_USER\\Environment\\PATH` is a non-string");
|
|
Ok(None)
|
|
}
|
|
Err(err) if err.code() == HRESULT::from(ERROR_FILE_NOT_FOUND) => Ok(Some(HSTRING::new())),
|
|
Err(err) => Err(err.into()),
|
|
}
|
|
}
|
|
|
|
/// Prepend a path to the `PATH` variable in the Windows registry.
|
|
///
|
|
/// Returns `Ok(None)` if the given path is already in `PATH`.
|
|
fn prepend_to_path(existing_path: &HSTRING, path: HSTRING) -> Option<HSTRING> {
|
|
if existing_path.is_empty() {
|
|
Some(path)
|
|
} else if existing_path.windows(path.len()).any(|p| *p == *path) {
|
|
None
|
|
} else {
|
|
let mut new_path = path.to_os_string();
|
|
new_path.push(";");
|
|
new_path.push(existing_path.to_os_string());
|
|
Some(HSTRING::from(new_path))
|
|
}
|
|
}
|