Skip to content

Commit

Permalink
Add support for Windows
Browse files Browse the repository at this point in the history
  • Loading branch information
charliermarsh committed Mar 3, 2024
1 parent 61b267c commit 42bbeba
Show file tree
Hide file tree
Showing 5 changed files with 98 additions and 4 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

26 changes: 26 additions & 0 deletions crates/uv-fs/src/path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,32 @@ pub fn normalize_path(path: impl AsRef<Path>) -> 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<Path>) -> std::io::Result<PathBuf> {
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::*;
Expand Down
2 changes: 1 addition & 1 deletion crates/uv-interpreter/src/interpreter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions crates/uv-virtualenv/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
72 changes: 69 additions & 3 deletions crates/uv-virtualenv/src/bare.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -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")
Expand Down Expand Up @@ -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: <https://github.com/python-poetry/poetry/pull/5931>
fn real_path(path: &Path) -> io::Result<PathBuf> {
#[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<PathBuf> {
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<PathBuf> {
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<u16> = 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;
}

0 comments on commit 42bbeba

Please sign in to comment.