From 42bbebac67292bbd2a8d7f750a89c28c22f4de1d Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 1 Mar 2024 14:15:17 -0500 Subject: [PATCH] Add support for Windows --- Cargo.lock | 1 + crates/uv-fs/src/path.rs | 26 +++++++++ crates/uv-interpreter/src/interpreter.rs | 2 +- crates/uv-virtualenv/Cargo.toml | 1 + crates/uv-virtualenv/src/bare.rs | 72 +++++++++++++++++++++++- 5 files changed, 98 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2f0ccd82dbe51..ebdb9a9940330 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4618,6 +4618,7 @@ dependencies = [ "uv-fs", "uv-interpreter", "which", + "windows-sys 0.52.0", ] [[package]] diff --git a/crates/uv-fs/src/path.rs b/crates/uv-fs/src/path.rs index a890e3f872015..bd3ac6aadcc59 100644 --- a/crates/uv-fs/src/path.rs +++ b/crates/uv-fs/src/path.rs @@ -76,6 +76,32 @@ pub fn normalize_path(path: impl AsRef) -> PathBuf { ret } +/// Like `fs_err::canonicalize`, but with permissive failures on Windows. +/// +/// On Windows, we can't canonicalize the resolved path to Pythons that are installed via the +/// Windows Store. For example, if you install Python via the Windows Store, then run `python` +/// and print the `sys.executable` path, you'll get a path like: +/// +/// `C:\Users\crmar\AppData\Local\Microsoft\WindowsApps\PythonSoftwareFoundation.Python.3.11_qbs5n2kfra8p0\python.exe`. +/// +/// Attempting to canonicalize this path will fail with `ErrorKind::Uncategorized`. +pub fn canonicalize_executable(path: impl AsRef) -> std::io::Result { + let path = path.as_ref(); + match fs_err::canonicalize(path) { + Ok(path) => Ok(path), + Err(err) => { + if cfg!(windows) { + if path.is_absolute() { + if path.components().any(|component| component.as_os_str() == "Microsoft") { + return Ok(path.to_path_buf()); + } + } + } + Err(err) + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/uv-interpreter/src/interpreter.rs b/crates/uv-interpreter/src/interpreter.rs index de0e0e57b43c0..d240b8c4c2d92 100644 --- a/crates/uv-interpreter/src/interpreter.rs +++ b/crates/uv-interpreter/src/interpreter.rs @@ -562,7 +562,7 @@ impl InterpreterInfo { format!("{}.msgpack", digest(&executable_bytes)), ); - let modified = Timestamp::from_path(fs_err::canonicalize(executable)?)?; + let modified = Timestamp::from_path(uv_fs::canonicalize_executable(executable)?)?; // Read from the cache. if cache diff --git a/crates/uv-virtualenv/Cargo.toml b/crates/uv-virtualenv/Cargo.toml index 9cf6d09813486..5403f2a351d20 100644 --- a/crates/uv-virtualenv/Cargo.toml +++ b/crates/uv-virtualenv/Cargo.toml @@ -38,6 +38,7 @@ thiserror = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true, optional = true } which = { workspace = true } +windows-sys = "0.52.0" [features] cli = ["clap", "tracing-subscriber"] diff --git a/crates/uv-virtualenv/src/bare.rs b/crates/uv-virtualenv/src/bare.rs index 7c3ad74d02a00..7f1bf0bd10025 100644 --- a/crates/uv-virtualenv/src/bare.rs +++ b/crates/uv-virtualenv/src/bare.rs @@ -4,7 +4,7 @@ use std::env; use std::env::consts::EXE_SUFFIX; use std::io; use std::io::{BufWriter, Write}; -use std::path::Path; +use std::path::{Path, PathBuf}; use fs_err as fs; use fs_err::File; @@ -55,7 +55,7 @@ pub fn create_bare_venv( let base_python = if cfg!(unix) { // On Unix, follow symlinks to resolve the base interpreter, since the Python executable in // a virtual environment is a symlink to the base interpreter. - fs_err::canonicalize(interpreter.sys_executable())? + uv_fs::canonicalize_executable(interpreter.sys_executable())? } else if cfg!(windows) { // On Windows, follow `virtualenv`. If we're in a virtual environment, use // `sys._base_executable` if it exists; if not, use `sys.base_prefix`. For example, with @@ -73,7 +73,7 @@ pub fn create_bare_venv( interpreter.base_prefix().join("python.exe") } } else { - fs_err::canonicalize(interpreter.sys_executable())? + uv_fs::canonicalize_executable(interpreter.sys_executable())? } } else { unimplemented!("Only Windows and Unix are supported") @@ -330,3 +330,69 @@ pub fn create_bare_venv( executable, }) } + +/// Given a path, return the "real" path. +/// +/// The only modifications applied here are for the Windows Store. Specifically, the Windows Store +/// Python executable is located at a path like: +/// +/// `C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.11_qbs5n2kfra8p0`. +/// +/// However, users actually aren't allowed to run executables from this path directly. So, instead, +/// we need to use a path like: +/// +/// `C:\Users\crmar\AppData\Local\Microsoft\WindowsApps\PythonSoftwareFoundation.Python.3.11_qbs5n2kfra8p0` +/// +/// In practice, this is achieved by replacing `%ProgramFiles%\WindowsApps` with `%LocalAppData%\Microsoft\WindowsApps`. +/// +/// See: +fn real_path(path: &Path) -> io::Result { + #[cfg(windows)] + if path.is_absolute() { + if path.components().any(|component| component.as_os_str() == "Microsoft") { + return real_windows_path(path); + } + } + + Ok(path.to_path_buf()) +} + +#[cfg(windows)] +fn real_windows_path(path: &Path) -> io::Result { + use windows_sys::Win32::UI::Shell::{CSIDL_LOCAL_APPDATA, CSIDL_PROFILE}; + + let program_files = windows_folder(CSIDL_PROGRAM_FILES)?; + let local_appdata = windows_folder(CSIDL_LOCAL_APPDATA)?; + + let suffix = path.strip_prefix(&program_files)?; + Ok(local_appdata.join(suffix)) +} + +#[cfg(windows)] +fn windows_folder(csidl: u32) -> Option { + use std::env; + use std::ffi::OsString; + use std::os::windows::ffi::OsStringExt; + use std::path::PathBuf; + + use windows_sys::Win32::Foundation::{MAX_PATH, S_OK}; + use windows_sys::Win32::UI::Shell::{SHGetFolderPathW}; + + unsafe { + let mut path: Vec = Vec::with_capacity(MAX_PATH as usize); + match SHGetFolderPathW(0, csidl as i32, 0, 0, path.as_mut_ptr()) { + S_OK => { + let len = wcslen(path.as_ptr()); + path.set_len(len); + let s = OsString::from_wide(&path); + Some(PathBuf::from(s)) + } + _ => None, + } + } +} + +#[cfg(windows)] +extern "C" { + fn wcslen(buf: *const u16) -> usize; +}