Skip to content

Commit

Permalink
Use reparse points to detect Windows installer shims (#2284)
Browse files Browse the repository at this point in the history
## Summary

This PR enables use of the Windows Store Pythons even with `py` is not
installed. Specifically, we need to ensure that the `python.exe` and
`python3.exe` executables installed into the
`C:\Users\crmar\AppData\Local\Microsoft\WindowsApp` directory _are_ used
when they're not "App execution aliases" (which merely open the Windows
Store, to help you install Python).

When `py` is installed, this isn't strictly necessary, since the
"resolved" executables are discovered via `py`. These look like
`C:\Users\crmar\AppData\Local\Microsoft\WindowsApps\PythonSoftwareFoundation.Python.3.11_qbs5n2kfra8p0\python.exe`.

Closes #2264.

## Test Plan

- Removed all Python installations from my Windows machine.
- Uninstalled `py`.
- Enabled "App execution aliases".
- Verified that for both `cargo run venv --python python.exe` and `cargo
run venv --python python3.exe`, `uv` exited with a failure that no
Python could be found.
- Installed Python 3.10 via the Windows Store.
- Verified that the above commands succeeded without error.
- Verified that `cargo run venv --python python3.10.exe` _also_
succeeded.
  • Loading branch information
charliermarsh authored Mar 7, 2024
1 parent fd03362 commit 996a859
Show file tree
Hide file tree
Showing 5 changed files with 173 additions and 78 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.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ url = { version = "2.5.0" }
urlencoding = { version = "2.1.3" }
walkdir = { version = "2.4.0" }
which = { version = "6.0.0" }
winapi = { version = "0.3.9" }
zip = { version = "0.6.6", default-features = false, features = ["deflate"] }

[patch.crates-io]
Expand Down
112 changes: 78 additions & 34 deletions crates/uv-fs/src/path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,55 +94,99 @@ pub fn canonicalize_executable(path: impl AsRef<Path>) -> std::io::Result<PathBu
}
}

/// Returns `true` if this is a Python executable installed via the Windows Store, like:
/// Returns `true` if this is a Python executable or shim installed via the Windows Store, based on
/// the path.
///
/// `C:\Users\crmar\AppData\Local\Microsoft\WindowsApps\PythonSoftwareFoundation.Python.3.11_qbs5n2kfra8p0\python.exe`.
/// This method does _not_ introspect the filesystem to determine if the shim is a redirect to the
/// Windows Store installer. In other words, it assumes that the path represents a Python
/// executable, not a redirect.
fn is_windows_store_python(path: &Path) -> bool {
if !cfg!(windows) {
return false;
}
/// Returns `true` if this is a Python executable shim installed via the Windows Store, like:
///
/// `C:\Users\crmar\AppData\Local\Microsoft\WindowsApps\python3.exe`
fn is_windows_store_python_shim(path: &Path) -> bool {
let mut components = path.components().rev();

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

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

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

// 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;
true
}

// 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;
/// 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_executable(path: &Path) -> bool {
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
}

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

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

true
is_windows_store_python_shim(path) || is_windows_store_python_executable(path)
}

#[cfg(test)]
Expand Down
1 change: 1 addition & 0 deletions crates/uv-interpreter/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ thiserror = { workspace = true }
tokio = { workspace = true }
tracing = { workspace = true }
which = { workspace = true}
winapi = { workspace = true }

[dev-dependencies]
anyhow = { version = "1.0.80" }
Expand Down
136 changes: 92 additions & 44 deletions crates/uv-interpreter/src/python_query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ fn find_python(
for name in possible_names.iter().flatten() {
if let Ok(paths) = which::which_in_global(&**name, Some(&path)) {
for path in paths {
if cfg!(windows) && windows::is_windows_store_shim(&path) {
if windows::is_windows_store_shim(&path) {
continue;
}

Expand Down Expand Up @@ -226,15 +226,13 @@ fn find_executable<R: AsRef<OsStr> + Into<OsString> + Copy>(
// binary is executable and exists. It also has some extra logic that handles inconsistent casing on Windows
// and expands `~`.
for path in env::split_paths(&PATH) {
let paths = match which::which_in_global(requested, Some(&path)) {
let mut paths = match which::which_in_global(requested, Some(&path)) {
Ok(paths) => paths,
Err(which::Error::CannotFindBinaryPath) => continue,
Err(err) => return Err(Error::WhichError(requested.into(), err)),
};
for path in paths {
if cfg!(windows) && windows::is_windows_store_shim(&path) {
continue;
}

if let Some(path) = paths.find(|path| !windows::is_windows_store_shim(path)) {
return Ok(Some(path));
}
}
Expand Down Expand Up @@ -476,34 +474,47 @@ mod windows {
.collect())
}

/// On Windows we might encounter the windows store proxy shim (Enabled in Settings/Apps/Advanced app settings/App execution aliases).
/// This requires quite a bit of custom logic to figure out what this thing does.
/// On Windows we might encounter the Windows Store proxy shim (enabled in:
/// Settings/Apps/Advanced app settings/App execution aliases). When Python is _not_ installed
/// via the Windows Store, but the proxy shim is enabled, then executing `python.exe` or
/// `python3.exe` will redirect to the Windows Store installer.
///
/// We need to detect that these `python.exe` and `python3.exe` files are _not_ Python
/// executables.
///
/// This method is taken from Rye:
///
/// This is a pretty dumb way. We know how to parse this reparse point, but Microsoft
/// does not want us to do this as the format is unstable. So this is a best effort way.
/// we just hope that the reparse point has the python redirector in it, when it's not
/// pointing to a valid Python.
/// > This is a pretty dumb way. We know how to parse this reparse point, but Microsoft
/// > does not want us to do this as the format is unstable. So this is a best effort way.
/// > we just hope that the reparse point has the python redirector in it, when it's not
/// > pointing to a valid Python.
///
/// Matches against paths like:
/// `C:\Users\crmar\AppData\Local\Microsoft\WindowsApps\python.exe`
/// See: <https://github.com/astral-sh/rye/blob/b0e9eccf05fe4ff0ae7b0250a248c54f2d780b4d/rye/src/cli/shim.rs#L108>
#[cfg(windows)]
pub(super) fn is_windows_store_shim(path: &Path) -> bool {
// Rye uses a more sophisticated test to identify the windows store shim.
// Unfortunately, it only works with the `python.exe` shim but not `python3.exe`.
// What we do here is a very naive implementation but probably sufficient for all we need.
// There's the risk of false positives but I consider it rare, considering how specific
// the path is.
use std::os::windows::fs::MetadataExt;
use std::os::windows::prelude::OsStrExt;
use winapi::um::fileapi::{CreateFileW, OPEN_EXISTING};
use winapi::um::handleapi::{CloseHandle, INVALID_HANDLE_VALUE};
use winapi::um::ioapiset::DeviceIoControl;
use winapi::um::winbase::{FILE_FLAG_BACKUP_SEMANTICS, FILE_FLAG_OPEN_REPARSE_POINT};
use winapi::um::winioctl::FSCTL_GET_REPARSE_POINT;
use winapi::um::winnt::{FILE_ATTRIBUTE_REPARSE_POINT, MAXIMUM_REPARSE_DATA_BUFFER_SIZE};

// The path must be absolute.
if !path.is_absolute() {
return false;
}

// The path must point to something like:
// `C:\Users\crmar\AppData\Local\Microsoft\WindowsApps\python3.exe`
let mut components = path.components().rev();

// Ex) `python.exe` or `python3.exe` or `python3.12.exe`
// Ex) `python.exe` or `python3.exe`
if !components
.next()
.and_then(|component| component.as_os_str().to_str())
.and_then(|component| component.rsplit_once('.'))
.is_some_and(|(name, extension)| name.starts_with("python") && extension == "exe")
.is_some_and(|component| component == "python.exe" || component == "python3.exe")
{
return false;
}
Expand All @@ -524,15 +535,68 @@ mod windows {
return false;
}

// Ex) `Local`
if !components
.next()
.is_some_and(|component| component.as_os_str() == "Local")
{
// The file is only relevant if it's a reparse point.
let Ok(md) = fs_err::symlink_metadata(path) else {
return false;
};
if md.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT == 0 {
return false;
}

true
let mut path_encoded = path
.as_os_str()
.encode_wide()
.chain(std::iter::once(0))
.collect::<Vec<_>>();

// SAFETY: The path is null-terminated.
#[allow(unsafe_code)]
let reparse_handle = unsafe {
CreateFileW(
path_encoded.as_mut_ptr(),
0,
0,
std::ptr::null_mut(),
OPEN_EXISTING,
FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT,
std::ptr::null_mut(),
)
};

if reparse_handle == INVALID_HANDLE_VALUE {
return false;
}

let mut buf = [0u16; MAXIMUM_REPARSE_DATA_BUFFER_SIZE as usize];
let mut bytes_returned = 0;

// SAFETY: The buffer is large enough to hold the reparse point.
#[allow(unsafe_code, clippy::cast_possible_truncation)]
let success = unsafe {
DeviceIoControl(
reparse_handle,
FSCTL_GET_REPARSE_POINT,
std::ptr::null_mut(),
0,
buf.as_mut_ptr().cast(),
buf.len() as u32 * 2,
&mut bytes_returned,
std::ptr::null_mut(),
) != 0
};

// SAFETY: The handle is valid.
#[allow(unsafe_code)]
unsafe {
CloseHandle(reparse_handle);
}

success && String::from_utf16_lossy(&buf).contains("\\AppInstallerPythonRedirector.exe")
}

#[cfg(not(windows))]
pub(super) fn is_windows_store_shim(_: &Path) -> bool {
false
}

#[cfg(test)]
Expand Down Expand Up @@ -574,22 +638,6 @@ mod windows {
"###);
});
}

#[test]
fn detect_shim() {
assert!(super::is_windows_store_shim(
r"C:\Users\crmar\AppData\Local\Microsoft\WindowsApps\python.exe".as_ref()
));
assert!(super::is_windows_store_shim(
r"C:\Users\crmar\AppData\Local\Microsoft\WindowsApps\python3.exe".as_ref()
));
assert!(super::is_windows_store_shim(
r"C:\Users\crmar\AppData\Local\Microsoft\WindowsApps\python3.12.exe".as_ref()
));
assert!(!super::is_windows_store_shim(
r"C:\Users\crmar\AppData\Local\Microsoft\WindowsApps\PythonSoftwareFoundation.Python.3.11_qbs5n2kfra8p0\python.exe".as_ref()
));
}
}
}

Expand Down

0 comments on commit 996a859

Please sign in to comment.