Add Windows path updates for uv tool (#5029)

## Summary

Largely based on rustup's implementation (linked in the source).

Closes #5027.

## Test Plan

- Changed the executable directory to `uv/foo`.
- Ran script; verified that I could access executables in `foo`.
This commit is contained in:
Charlie Marsh 2024-07-12 21:55:05 -04:00 committed by GitHub
parent a61464e802
commit 4eb19c76bd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 164 additions and 1 deletions

View file

@ -10,5 +10,10 @@ workspace = true
[dependencies]
uv-fs = { workspace = true }
anyhow = { workspace = true }
home = { workspace = true }
same-file = { workspace = true }
tracing = { workspace = true }
[target.'cfg(windows)'.dependencies]
winreg = { workspace = true }

View file

@ -1,3 +1,5 @@
pub mod windows;
use std::path::{Path, PathBuf};
use uv_fs::Simplified;
@ -150,9 +152,11 @@ impl Shell {
// On Csh, we need to update both `.cshrc` and `.login`, like Bash.
vec![home_dir.join(".cshrc"), home_dir.join(".login")]
}
// TODO(charlie): Add support for Nushell, PowerShell, and Cmd.
// TODO(charlie): Add support for Nushell.
Shell::Nushell => vec![],
// See: [`crate::windows::prepend_path`].
Shell::Powershell => vec![],
// See: [`crate::windows::prepend_path`].
Shell::Cmd => vec![],
}
}

View file

@ -0,0 +1,128 @@
//! Windows-specific utilities for manipulating the environment.
//!
//! Based on rustup's Windows implementation: <https://github.com/rust-lang/rustup/blob/fede22fea7b160868cece632bd213e6d72f8912f/src/cli/self_update/windows.rs>
#![cfg(windows)]
use std::ffi::OsString;
use std::io;
use std::os::windows::ffi::OsStrExt;
use std::path::Path;
use std::slice;
use anyhow::Context;
use winreg::enums::{RegType, HKEY_CURRENT_USER, KEY_READ, KEY_WRITE};
use winreg::{RegKey, RegValue};
/// 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, OsString::from(path).encode_wide().collect())
});
// 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: Vec<u16>) -> anyhow::Result<()> {
let root = RegKey::predef(HKEY_CURRENT_USER);
let environment = root.open_subkey_with_flags("Environment", KEY_READ | KEY_WRITE)?;
if path.is_empty() {
environment.delete_value("PATH")?;
} else {
let reg_value = RegValue {
bytes: to_winreg_bytes(path),
vtype: RegType::REG_EXPAND_SZ,
};
environment.set_raw_value("PATH", &reg_value)?;
}
Ok(())
}
/// 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<Vec<u16>>> {
let root = RegKey::predef(HKEY_CURRENT_USER);
let environment = root
.open_subkey_with_flags("Environment", KEY_READ | KEY_WRITE)
.context("Failed to open `Environment` key")?;
let reg_value = environment.get_raw_value("PATH");
match reg_value {
Ok(reg_value) => {
if let Some(reg_value) = from_winreg_value(&reg_value) {
Ok(Some(reg_value))
} else {
tracing::warn!("`HKEY_CURRENT_USER\\Environment\\PATH` is a non-string");
Ok(None)
}
}
Err(ref err) if err.kind() == io::ErrorKind::NotFound => Ok(Some(Vec::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: Vec<u16>, path: Vec<u16>) -> Option<Vec<u16>> {
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;
new_path.push(u16::from(b';'));
new_path.extend(existing_path);
Some(new_path)
}
}
/// Convert a vector UCS-2 chars to a null-terminated UCS-2 string in bytes.
fn to_winreg_bytes(mut value: Vec<u16>) -> Vec<u8> {
value.push(0);
#[allow(unsafe_code)]
unsafe {
slice::from_raw_parts(value.as_ptr().cast::<u8>(), value.len() * 2).to_vec()
}
}
/// Decode the `HKCU\Environment\PATH` value.
///
/// If the key is not `REG_SZ` or `REG_EXPAND_SZ`, returns `None`.
/// The `winreg` library itself does a lossy unicode conversion.
fn from_winreg_value(val: &RegValue) -> Option<Vec<u16>> {
match val.vtype {
RegType::REG_SZ | RegType::REG_EXPAND_SZ => {
#[allow(unsafe_code)]
let mut words = unsafe {
#[allow(clippy::cast_ptr_alignment)]
slice::from_raw_parts(val.bytes.as_ptr().cast::<u16>(), val.bytes.len() / 2)
.to_owned()
};
while words.last() == Some(&0) {
words.pop();
}
Some(words)
}
_ => None,
}
}

View file

@ -1,3 +1,5 @@
#![cfg_attr(windows, allow(unreachable_code))]
use std::fmt::Write;
use anyhow::Result;
@ -26,6 +28,26 @@ pub(crate) async fn update_shell(preview: PreviewMode, printer: Printer) -> Resu
executable_directory.simplified_display()
);
#[cfg(windows)]
{
if uv_shell::windows::prepend_path(&executable_directory)? {
writeln!(
printer.stderr(),
"Updated PATH to include executable directory {}",
executable_directory.simplified_display().cyan()
)?;
writeln!(printer.stderr(), "Restart your shell to apply changes.")?;
} else {
writeln!(
printer.stderr(),
"Executable directory {} is already in PATH",
executable_directory.simplified_display().cyan()
)?;
}
return Ok(ExitStatus::Success);
}
if Shell::contains_path(&executable_directory) {
writeln!(
printer.stderr(),