Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Patch embedded install path for Python dylib on macOS during python install #10629

Merged
merged 5 commits into from
Jan 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions crates/uv-python/src/installation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,9 @@ impl PythonInstallation {
installed.ensure_externally_managed()?;
installed.ensure_sysconfig_patched()?;
installed.ensure_canonical_executables()?;
if let Err(e) = installed.ensure_dylib_patched() {
e.warn_user(&installed);
}

Ok(Self {
source: PythonSource::Managed,
Expand Down
1 change: 1 addition & 0 deletions crates/uv-python/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ mod implementation;
mod installation;
mod interpreter;
mod libc;
pub mod macos_dylib;
pub mod managed;
#[cfg(windows)]
mod microsoft_store;
Expand Down
63 changes: 63 additions & 0 deletions crates/uv-python/src/macos_dylib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
use std::{io::ErrorKind, path::PathBuf};

use uv_fs::Simplified as _;
use uv_warnings::warn_user;

use crate::managed::ManagedPythonInstallation;

pub fn patch_dylib_install_name(dylib: PathBuf) -> Result<(), Error> {
let output = match std::process::Command::new("install_name_tool")
.arg("-id")
.arg(&dylib)
.arg(&dylib)
.output()
{
Ok(output) => output,
Err(e) => {
let e = if e.kind() == ErrorKind::NotFound {
Error::MissingInstallNameTool
} else {
e.into()
};
return Err(e);
}
};

if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr).into_owned();
return Err(Error::RenameError { dylib, stderr });
}

Ok(())
}

#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error(transparent)]
Io(#[from] std::io::Error),
#[error("`install_name_tool` is not available on this system.
This utility is part of macOS Developer Tools. Please ensure that the Xcode Command Line Tools are installed by running:

xcode-select --install

For more information, see: https://developer.apple.com/xcode/")]
MissingInstallNameTool,
zanieb marked this conversation as resolved.
Show resolved Hide resolved
#[error("Failed to update the install name of the Python dynamic library located at `{}`", dylib.user_display())]
RenameError { dylib: PathBuf, stderr: String },
}

impl Error {
/// Emit a user-friendly warning about the patching failure.
pub fn warn_user(&self, installation: &ManagedPythonInstallation) {
let error = if tracing::enabled!(tracing::Level::DEBUG) {
format!("\nUnderlying error: {self}")
} else {
String::new()
};
warn_user!(
"Failed to patch the install name of the dynamic library for {}. This may cause issues when building Python native extensions.{}",
installation.executable().simplified_display(),
error
);
}
}
29 changes: 27 additions & 2 deletions crates/uv-python/src/managed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ use crate::libc::LibcDetectionError;
use crate::platform::Error as PlatformError;
use crate::platform::{Arch, Libc, Os};
use crate::python_version::PythonVersion;
use crate::{sysconfig, PythonRequest, PythonVariant};
use crate::{macos_dylib, sysconfig, PythonRequest, PythonVariant};

#[derive(Error, Debug)]
pub enum Error {
#[error(transparent)]
Expand Down Expand Up @@ -88,6 +89,8 @@ pub enum Error {
NameParseError(#[from] installation::PythonInstallationKeyError),
#[error(transparent)]
LibcDetection(#[from] LibcDetectionError),
#[error(transparent)]
MacOsDylib(#[from] macos_dylib::Error),
}
/// A collection of uv-managed Python installations installed on the current system.
#[derive(Debug, Clone, Eq, PartialEq)]
Expand Down Expand Up @@ -508,6 +511,28 @@ impl ManagedPythonInstallation {
Ok(())
}

/// On macOS, ensure that the `install_name` for the Python dylib is set
/// correctly, rather than pointing at `/install/lib/libpython{version}.dylib`.
/// This is necessary to ensure that native extensions written in Rust
/// link to the correct location for the Python library.
///
/// See <https://github.com/astral-sh/uv/issues/10598> for more information.
pub fn ensure_dylib_patched(&self) -> Result<(), macos_dylib::Error> {
if cfg!(target_os = "macos") {
if *self.implementation() == ImplementationName::CPython {
let dylib_path = self.python_dir().join("lib").join(format!(
"{}python{}{}{}",
std::env::consts::DLL_PREFIX,
self.key.version().python_version(),
self.key.variant().suffix(),
std::env::consts::DLL_SUFFIX
));
macos_dylib::patch_dylib_install_name(dylib_path)?;
}
}
Ok(())
}

/// Create a link to the managed Python executable.
///
/// If the file already exists at the target path, an error will be returned.
Expand Down Expand Up @@ -603,7 +628,7 @@ impl ManagedPythonInstallation {
}

/// Generate a platform portion of a key from the environment.
fn platform_key_from_env() -> Result<String, Error> {
pub fn platform_key_from_env() -> Result<String, Error> {
let os = Os::from_env();
let arch = Arch::from_env();
let libc = Libc::from_env()?;
Expand Down
3 changes: 3 additions & 0 deletions crates/uv/src/commands/python/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,9 @@ pub(crate) async fn install(
installation.ensure_externally_managed()?;
installation.ensure_sysconfig_patched()?;
installation.ensure_canonical_executables()?;
if let Err(e) = installation.ensure_dylib_patched() {
e.warn_user(installation);
}

if preview.is_disabled() {
debug!("Skipping installation of Python executables, use `--preview` to enable.");
Expand Down
44 changes: 44 additions & 0 deletions crates/uv/tests/it/python_install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -876,3 +876,47 @@ fn python_install_preview_broken_link() {
);
});
}

#[cfg(target_os = "macos")]
#[test]
fn python_dylib_install_name_is_patched_on_install() {
use assert_cmd::assert::OutputAssertExt;
use uv_python::managed::platform_key_from_env;

let context: TestContext = TestContext::new_with_versions(&[]).with_filtered_python_keys();

// Install the latest version
context
.python_install()
.arg("--preview")
.arg("3.13.1")
.assert()
.success();

let dylib = context
.temp_dir
.child("managed")
.child(format!(
"cpython-3.13.1-{}",
platform_key_from_env().unwrap()
))
.child("lib")
.child(format!(
"{}python3.13{}",
std::env::consts::DLL_PREFIX,
std::env::consts::DLL_SUFFIX
));

let mut cmd = std::process::Command::new("otool");
cmd.arg("-D").arg(dylib.as_ref());

uv_snapshot!(context.filters(), cmd, @r###"
success: true
exit_code: 0
----- stdout -----
[TEMP_DIR]/managed/cpython-3.13.1-[PLATFORM]/lib/libpython3.13.dylib:
[TEMP_DIR]/managed/cpython-3.13.1-[PLATFORM]/lib/libpython3.13.dylib

----- stderr -----
"###);
}
Loading