Skip to content

Commit

Permalink
Add PATH and registry information for adapting winget on Windows (#483)
Browse files Browse the repository at this point in the history
  • Loading branch information
zengqiu committed Jan 21, 2024
1 parent 95ba033 commit 6cab42d
Show file tree
Hide file tree
Showing 8 changed files with 271 additions and 10 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ target
/x
site
__pycache__
.idea
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ _Unreleased_
- Fixed an issue where `rye init` would pin a much too specific version in the `.python-version`
file that is generated. #545

- On Windows the `PATH` is now automatically adjusted on install and uninstall. This means that
manually adding the rye folder to the search path is no longer necessary. #483

<!-- released start -->

## 0.18.0
Expand Down
11 changes: 11 additions & 0 deletions Cargo.lock

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

3 changes: 2 additions & 1 deletion docs/guide/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,8 @@ rye self uninstall
```

Additionally you should delete the remaining `.rye` folder from your home directory and
remove `.rye/shims` from the `PATH` again. Rye itself does not place any data
remove `.rye/shims` from the `PATH` again (usually by removing the code that sources
the `env` file from the installation step). Rye itself does not place any data
in other locations. Note though that virtual environments created by rye will
no longer function after Rye was uninstalled.

Expand Down
1 change: 1 addition & 0 deletions rye/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,4 @@ whattheshell = "1.0.1"

[target."cfg(windows)".dependencies]
winapi = { version = "0.3.9", default-features = false, features = [] }
winreg = "0.51"
22 changes: 13 additions & 9 deletions rye/src/cli/rye.rs
Original file line number Diff line number Diff line change
Expand Up @@ -312,16 +312,19 @@ fn uninstall(args: UninstallCommand) -> Result<(), Error> {
let rye_home = env::var("RYE_HOME")
.map(Cow::Owned)
.unwrap_or(Cow::Borrowed(DEFAULT_HOME));
if cfg!(unix) {

#[cfg(unix)]
{
echo!(
"Don't forget to remove the sourcing of {} from your shell config.",
Path::new(&rye_home as &str).join("env").display()
Path::new(&*rye_home).join("env").display()
);
} else {
echo!(
"Don't forget to remove {} from your PATH",
Path::new(&rye_home as &str).join("shims").display()
)
}

#[cfg(windows)]
{
crate::utils::windows::remove_from_path(Path::new(&*rye_home))?;
crate::utils::windows::remove_from_programs()?;
}

Ok(())
Expand Down Expand Up @@ -462,8 +465,9 @@ fn perform_install(mode: InstallMode, toolchain_path: Option<&Path>) -> Result<(
}
#[cfg(windows)]
{
echo!();
echo!("Note: You need to manually add {DEFAULT_HOME} to your PATH.");
let rye_home = Path::new(&*rye_home);
crate::utils::windows::add_to_programs(rye_home)?;
crate::utils::windows::add_to_path(rye_home)?;
}

echo!("For more information read https://mitsuhiko.github.io/rye/guide/installation");
Expand Down
3 changes: 3 additions & 0 deletions rye/src/utils.rs → rye/src/utils/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ pub use std::os::windows::fs::symlink_file;
use crate::config::Config;
use crate::consts::VENV_BIN;

#[cfg(windows)]
pub(crate) mod windows;

#[cfg(windows)]
pub fn symlink_dir<P, Q>(original: P, link: Q) -> Result<(), std::io::Error>
where
Expand Down
237 changes: 237 additions & 0 deletions rye/src/utils/windows.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
use std::env;
use std::ffi::OsString;
use std::os::windows::ffi::{OsStrExt, OsStringExt};
use std::path::{Path, PathBuf};

use anyhow::{anyhow, Context, Error};
use winreg::enums::{RegType, HKEY_CURRENT_USER, KEY_READ, KEY_WRITE};
use winreg::{RegKey, RegValue};

const RYE_UNINSTALL_ENTRY: &str = r"Software\Microsoft\Windows\CurrentVersion\Uninstall\Rye";

pub(crate) fn add_to_path(rye_home: &Path) -> Result<(), Error> {
let target_path = reverse_resolve_user_profile(rye_home.join("shims"));
if let Some(old_path) = get_windows_path_var()? {
if let Some(new_path) =
append_entry_to_path(old_path, target_path.as_os_str().encode_wide().collect())
{
apply_new_path(new_path)?;
}
}
Ok(())
}

pub(crate) fn remove_from_path(rye_home: &Path) -> Result<(), Error> {
let target_path = reverse_resolve_user_profile(rye_home.join("shims"));
if let Some(old_path) = get_windows_path_var()? {
if let Some(new_path) =
remove_entry_from_path(old_path, target_path.as_os_str().encode_wide().collect())
{
apply_new_path(new_path)?;
}
}
Ok(())
}

/// If the target path is under the user profile, replace it with %USERPROFILE%. The
/// motivation here is that this was the path we documented originally so someone updating
/// Rye does not end up with two competing paths in the list for no reason.
fn reverse_resolve_user_profile(path: PathBuf) -> PathBuf {
if let Some(user_profile) = env::var_os("USERPROFILE").map(PathBuf::from) {
if let Ok(rest) = path.strip_prefix(&user_profile) {
return Path::new("%USERPROFILE%").join(rest);
}
}
path
}

fn apply_new_path(new_path: Vec<u16>) -> Result<(), Error> {
use std::ptr;
use winapi::shared::minwindef::*;
use winapi::um::winuser::{
SendMessageTimeoutA, HWND_BROADCAST, SMTO_ABORTIFHUNG, WM_SETTINGCHANGE,
};

let root = RegKey::predef(HKEY_CURRENT_USER);
let environment = root.open_subkey_with_flags("Environment", KEY_READ | KEY_WRITE)?;

if new_path.is_empty() {
environment.delete_value("PATH")?;
} else {
let reg_value = RegValue {
bytes: to_winreg_bytes(new_path),
vtype: RegType::REG_EXPAND_SZ,
};
environment.set_raw_value("PATH", &reg_value)?;
}

// Tell other processes to update their environment
#[allow(clippy::unnecessary_cast)]
unsafe {
SendMessageTimeoutA(
HWND_BROADCAST,
WM_SETTINGCHANGE,
0 as WPARAM,
"Environment\0".as_ptr() as LPARAM,
SMTO_ABORTIFHUNG,
5000,
ptr::null_mut(),
);
}

Ok(())
}

/// Get the windows PATH variable out of the registry as a String. If
/// this returns None then the PATH variable is not a string and we
/// should not mess with it.
fn get_windows_path_var() -> Result<Option<Vec<u16>>, Error> {
use std::io;

let root = RegKey::predef(HKEY_CURRENT_USER);
let environment = root
.open_subkey_with_flags("Environment", KEY_READ | KEY_WRITE)
.context("Failed opening Environment key")?;

let reg_value = environment.get_raw_value("PATH");
match reg_value {
Ok(val) => {
if let Some(s) = from_winreg_value(&val) {
Ok(Some(s))
} else {
warn!(
"the registry key HKEY_CURRENT_USER\\Environment\\PATH is not a string. \
Not modifying the PATH variable"
);
Ok(None)
}
}
Err(ref e) if e.kind() == io::ErrorKind::NotFound => Ok(Some(Vec::new())),
Err(e) => Err(e).context("failure during windows path manipulation"),
}
}

/// Returns None if the existing old_path does not need changing, otherwise
/// prepends the path_str to old_path, handling empty old_path appropriately.
fn append_entry_to_path(old_path: Vec<u16>, path_str: Vec<u16>) -> Option<Vec<u16>> {
if old_path.is_empty() {
Some(path_str)
} else if old_path
.windows(path_str.len())
.any(|path| path == path_str)
{
None
} else {
let mut new_path = path_str;
new_path.push(b';' as u16);
new_path.extend_from_slice(&old_path);
Some(new_path)
}
}

/// Returns None if the existing old_path does not need changing
fn remove_entry_from_path(old_path: Vec<u16>, path_str: Vec<u16>) -> Option<Vec<u16>> {
let idx = old_path
.windows(path_str.len())
.position(|path| path == path_str)?;
// If there's a trailing semicolon (likely, since we probably added one
// during install), include that in the substring to remove. We don't search
// for that to find the string, because if it's the last string in the path,
// there may not be.
let mut len = path_str.len();
if old_path.get(idx + path_str.len()) == Some(&(b';' as u16)) {
len += 1;
}

let mut new_path = old_path[..idx].to_owned();
new_path.extend_from_slice(&old_path[idx + len..]);
// Don't leave a trailing ; though, we don't want an empty string in the
// path.
if new_path.last() == Some(&(b';' as u16)) {
new_path.pop();
}
Some(new_path)
}

/// Registers rye as installed program.
pub(crate) fn add_to_programs(rye_home: &Path) -> Result<(), Error> {
let key = RegKey::predef(HKEY_CURRENT_USER)
.create_subkey(RYE_UNINSTALL_ENTRY)
.context("Failed creating uninstall key")?
.0;

// Don't overwrite registry if Rye is already installed
let prev = key
.get_raw_value("UninstallString")
.map(|val| from_winreg_value(&val));
if let Ok(Some(s)) = prev {
let mut path = PathBuf::from(OsString::from_wide(&s));
path.pop();
if path.exists() {
return Ok(());
}
}

let mut uninstall_cmd = OsString::from("\"");
uninstall_cmd.push(rye_home);
uninstall_cmd.push("\" self uninstall");

let reg_value = RegValue {
bytes: to_winreg_bytes(uninstall_cmd.encode_wide().collect()),
vtype: RegType::REG_SZ,
};

let current_version: &str = env!("CARGO_PKG_VERSION");

key.set_raw_value("UninstallString", &reg_value)
.context("Failed to set uninstall string")?;
key.set_value(
"DisplayName",
&"Rye: An Experimental Package Management Solution for Python",
)
.context("Failed to set display name")?;
key.set_value("DisplayVersion", &current_version)
.context("Failed to set display version")?;
key.set_value("Publisher", &"Rye")
.context("Failed to set publisher")?;

Ok(())
}

/// Removes the entry on uninstall from the program list.
pub(crate) fn remove_from_programs() -> Result<(), Error> {
match RegKey::predef(HKEY_CURRENT_USER).delete_subkey_all(RYE_UNINSTALL_ENTRY) {
Ok(()) => Ok(()),
Err(ref e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(e) => Err(anyhow!(e)),
}
}

/// Convert a vector UCS-2 chars to a null-terminated UCS-2 string in bytes
pub(crate) fn to_winreg_bytes(mut v: Vec<u16>) -> Vec<u8> {
v.push(0);
unsafe { std::slice::from_raw_parts(v.as_ptr().cast::<u8>(), v.len() * 2).to_vec() }
}

/// This is used to decode the value of HKCU\Environment\PATH. If that key is
/// not REG_SZ | REG_EXPAND_SZ then this returns None. The winreg library itself
/// does a lossy unicode conversion.
pub(crate) fn from_winreg_value(val: &winreg::RegValue) -> Option<Vec<u16>> {
use std::slice;

match val.vtype {
RegType::REG_SZ | RegType::REG_EXPAND_SZ => {
// Copied from winreg
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,
}
}

0 comments on commit 6cab42d

Please sign in to comment.