Skip to content

Commit

Permalink
Add support for Python installed from Windows Store (#2122)
Browse files Browse the repository at this point in the history
## Summary

After #2121, the only remaining
issue is that calling `canonicalize` on these Pythons returns an error.

Closes #2105.

## Test Plan

Uninstalled all python.org Pythons on my Windows machine, then created a
virtualenv. The resulting config file:

```
Using Python 3.11.8 interpreter at: C:\Users\crmar\AppData\Local\Microsoft\WindowsApps\PythonSoftwareFoundation.Python.3.11_qbz5n2kfra8p0\python.exe
Creating virtualenv at: .venv
Activate with: .venv\Scripts\activate
PS C:\Users\crmar\workspace\puffin> cat .\.venv\pyvenv.cfg
home = C:\Users\crmar\AppData\Local\Microsoft\WindowsApps\PythonSoftwareFoundation.Python.3.11_qbz5n2kfra8p0
implementation = CPython
version_info = 3.11.8
include-system-site-packages = false
uv = 0.1.13
prompt = puffin
```

Prior to this PR, it would fail with a canonicalization error.

Prior to #2121, it would leave a "bad" Python in the config file:

```
Using Python 3.11.8 interpreter at: C:\Users\crmar\AppData\Local\Microsoft\WindowsApps\PythonSoftwareFoundation.Python.3.11_qbz5n2kfra8p0\python.exe
Creating virtualenv at: .venv
Activate with: .venv\Scripts\activate
PS C:\Users\crmar\workspace\puffin> cat .\.venv\pyvenv.cfg
home = C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.11_3.11.2288.0_x64__qbz5n2kfra8p0
implementation = CPython
version_info = 3.11.8
include-system-site-packages = false
uv = 0.1.13
prompt = puffin
```

Which, once activated, would fail with:

```
(venv) PS C:\Users\crmar\workspace\puffin> python
No Python at '"C:\Users\crmar\AppData\Local\Programs\Python\Python312\python.exe'
```
  • Loading branch information
charliermarsh authored Mar 3, 2024
1 parent 46265b7 commit 6a53798
Show file tree
Hide file tree
Showing 3 changed files with 72 additions and 3 deletions.
69 changes: 69 additions & 0 deletions crates/uv-fs/src/path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,75 @@ 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();
if is_windows_store_python(path) {
Ok(path.to_path_buf())
} else {
fs_err::canonicalize(path)
}
}

/// Returns `true` if this is a Python executable installed via the Windows Store, like:
///
/// `C:\Users\crmar\AppData\Local\Microsoft\WindowsApps\PythonSoftwareFoundation.Python.3.11_qbs5n2kfra8p0\python.exe`.
fn is_windows_store_python(path: &Path) -> bool {
if !cfg!(windows) {
return false;
}

if !path.is_absolute() {
return false;
}

let mut components = path.components().rev();

// Ex) `python.exe`
if !components
.next()
.and_then(|component| component.as_os_str().to_str())
.is_some_and(|component| component.starts_with("python"))
{
return false;
}

// Ex) `PythonSoftwareFoundation.Python.3.11_qbs5n2kfra8p0`
if !components
.next()
.and_then(|component| component.as_os_str().to_str())
.is_some_and(|component| component.starts_with("PythonSoftwareFoundation.Python.3."))
{
return false;
}

// Ex) `WindowsApps`
if !components
.next()
.is_some_and(|component| component.as_os_str() == "WindowsApps")
{
return false;
}

// Ex) `Microsoft`
if !components
.next()
.is_some_and(|component| component.as_os_str() == "Microsoft")
{
return false;
}

true
}

#[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
4 changes: 2 additions & 2 deletions crates/uv-virtualenv/src/bare.rs
Original file line number Diff line number Diff line change
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

0 comments on commit 6a53798

Please sign in to comment.