Skip to content
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
7 changes: 6 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1916,7 +1916,7 @@ jobs:
persist-credentials: false

- name: "Install Python"
run: apt-get update && apt-get install -y python3.11 python3-pip python3.11-venv
run: apt-get update && apt-get install -y python3.11 python3-pip python3.11-venv python3-debian

- name: "Download binary"
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
Expand All @@ -1932,6 +1932,11 @@ jobs:
- name: "Validate global Python install"
run: python3.11 scripts/check_system_python.py --uv ./uv --externally-managed

- name: "Test `uv run` with system Python"
run: |
./uv run -p python3.11 -v python -c "import debian"
./uv run -p python3.11 -v --with anyio python -c "import debian"
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would fail without this change


system-test-fedora:
timeout-minutes: 10
needs: build-binary-linux-libc
Expand Down
2 changes: 2 additions & 0 deletions crates/uv-python/python/get_interpreter_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
The script will exit with status 0 on known error that are turned into rust errors.
"""

import site
import sys

import json
Expand Down Expand Up @@ -637,6 +638,7 @@ def main() -> None:
# temporary path to `sys.path` so we can import it, which we have to strip later
# to avoid having this now-deleted path around.
"sys_path": sys.path[1:],
"site_packages": site.getsitepackages(),
"stdlib": sysconfig.get_path("stdlib"),
# Prior to the introduction of `sysconfig` patching, python-build-standalone installations would always use
# "/install" as the prefix. With `sysconfig` patching, we rewrite the prefix to match the actual installation
Expand Down
22 changes: 21 additions & 1 deletion crates/uv-python/src/interpreter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ pub struct Interpreter {
sys_base_executable: Option<PathBuf>,
sys_executable: PathBuf,
sys_path: Vec<PathBuf>,
site_packages: Vec<PathBuf>,
stdlib: PathBuf,
standalone: bool,
tags: OnceLock<Tags>,
Expand Down Expand Up @@ -86,6 +87,7 @@ impl Interpreter {
sys_base_executable: info.sys_base_executable,
sys_executable: info.sys_executable,
sys_path: info.sys_path,
site_packages: info.site_packages,
stdlib: info.stdlib,
standalone: info.standalone,
tags: OnceLock::new(),
Expand Down Expand Up @@ -439,10 +441,21 @@ impl Interpreter {
}

/// Return the `sys.path` for this Python interpreter.
pub fn sys_path(&self) -> &Vec<PathBuf> {
pub fn sys_path(&self) -> &[PathBuf] {
&self.sys_path
}

/// Return the `site.getsitepackages` for this Python interpreter.
///
/// These are the paths Python will search for packages in at runtime. We use this for
/// environment layering, but not for checking for installed packages. We could use these paths
/// to check for installed packages, but it introduces a lot of complexity, so instead we use a
/// simplified version that does not respect customized site-packages. See
/// [`Interpreter::site_packages`].
pub fn runtime_site_packages(&self) -> &[PathBuf] {
&self.site_packages
}

/// Return the `stdlib` path for this Python interpreter, as returned by `sysconfig.get_paths()`.
pub fn stdlib(&self) -> &Path {
&self.stdlib
Expand Down Expand Up @@ -567,6 +580,9 @@ impl Interpreter {
///
/// Some distributions also create symbolic links from `purelib` to `platlib`; in such cases, we
/// still deduplicate the entries, returning a single path.
///
/// Note this does not include all runtime site-packages directories if the interpreter has been
/// customized. See [`Interpreter::runtime_site_packages`].
pub fn site_packages(&self) -> impl Iterator<Item = Cow<'_, Path>> {
let target = self.target().map(Target::site_packages);

Expand Down Expand Up @@ -870,6 +886,7 @@ struct InterpreterInfo {
sys_base_executable: Option<PathBuf>,
sys_executable: PathBuf,
sys_path: Vec<PathBuf>,
site_packages: Vec<PathBuf>,
stdlib: PathBuf,
standalone: bool,
pointer_size: PointerSize,
Expand Down Expand Up @@ -1247,6 +1264,9 @@ mod tests {
"/home/ferris/.pyenv/versions/3.12.0/lib/python3.12/lib/python3.12",
"/home/ferris/.pyenv/versions/3.12.0/lib/python3.12/site-packages"
],
"site_packages": [
"/home/ferris/.pyenv/versions/3.12.0/lib/python3.12/site-packages"
],
"stdlib": "/home/ferris/.pyenv/versions/3.12.0/lib/python3.12",
"scheme": {
"data": "/home/ferris/.pyenv/versions/3.12.0",
Expand Down
3 changes: 3 additions & 0 deletions crates/uv-python/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,9 @@ mod tests {
"/home/ferris/.pyenv/versions/{FULL_VERSION}/lib/python{VERSION}/lib/python{VERSION}",
"/home/ferris/.pyenv/versions/{FULL_VERSION}/lib/python{VERSION}/site-packages"
],
"site_packages": [
"/home/ferris/.pyenv/versions/{FULL_VERSION}/lib/python{VERSION}/site-packages"
],
"stdlib": "/home/ferris/.pyenv/versions/{FULL_VERSION}/lib/python{VERSION}",
"scheme": {
"data": "/home/ferris/.pyenv/versions/{FULL_VERSION}",
Expand Down
32 changes: 22 additions & 10 deletions crates/uv/src/commands/project/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1075,16 +1075,28 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl
requirements_env.site_packages().next().ok_or_else(|| {
anyhow!("Requirements environment has no site packages directory")
})?;
let base_site_packages = base_interpreter
.site_packages()
.next()
.ok_or_else(|| anyhow!("Base environment has no site packages directory"))?;

ephemeral_env.set_overlay(format!(
"import site; site.addsitedir(\"{}\"); site.addsitedir(\"{}\");",
requirements_site_packages.escape_for_python(),
base_site_packages.escape_for_python(),
))?;
let mut base_site_packages = base_interpreter
.runtime_site_packages()
.iter()
.map(|path| Cow::Borrowed(path.as_path()))
.chain(base_interpreter.site_packages())
.peekable();
if base_site_packages.peek().is_none() {
return Err(anyhow!("Base environment has no site packages directory"));
}

let overlay_content = format!(
"import site; {}",
std::iter::once(requirements_site_packages)
.chain(base_site_packages)
.dedup()
.inspect(|path| debug!("Adding `{}` to site packages", path.display()))
.map(|path| format!("site.addsitedir(\"{}\")", path.escape_for_python()))
.collect::<Vec<_>>()
.join("; ")
);

ephemeral_env.set_overlay(overlay_content)?;

// N.B. The order here matters — earlier interpreters take precedence over the
// later ones.
Expand Down
3 changes: 1 addition & 2 deletions crates/uv/tests/it/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5163,8 +5163,7 @@ fn run_repeated() -> Result<()> {
Resolved 1 package in [TIME]
"###);

// Re-running as a tool doesn't require reinstalling `typing-extensions`, since the environment
// is cached.
// Import `iniconfig` in the context of a `tool run` command, which should fail.
uv_snapshot!(
context.filters(),
context.tool_run().arg("--with").arg("typing-extensions").arg("python").arg("-c").arg("import typing_extensions; import iniconfig"), @r#"
Expand Down
Loading