Respect include-system-site-packages in layered environments (#11873)

## Summary

We use a similar strategy to the ephemeral overlay: set
`include-system-site-packages` in the `pyvenv.cfg`, and clear it
whenever we access a new environment.

Closes https://github.com/astral-sh/uv/issues/11829.

## Test Plan

Difficult to test because we don't really have support for system
packages in our test infrastructure. But...

```
> uv venv --system-site-packages
> ['', '/Users/crmarsh/.local/share/uv/python/cpython-3.13.0-macos-aarch64-none/lib/python313.zip', '/Users/crmarsh/.local/share/uv/python/cpython-3.13.0-macos-aarch64-none/lib/python3.13', '/Users/crmarsh/.local/share/uv/python/cpython-3.13.0-macos-aarch64-none/lib/python3.13/lib-dynload', '/Users/crmarsh/.cache/uv/archive-v0/AhKcORkaCdbBl31VweRtG/lib/python3.13/site-packages', '/Users/crmarsh/workspace/uv/foo/.venv/lib/python3.13/site-packages', '/Users/crmarsh/.local/share/uv/python/cpython-3.13.0-macos-aarch64-none/lib/python3.13/site-packages']
```

```
> uv venv
> ['', '/Users/crmarsh/.local/share/uv/python/cpython-3.13.0-macos-aarch64-none/lib/python313.zip', '/Users/crmarsh/.local/share/uv/python/cpython-3.13.0-macos-aarch64-none/lib/python3.13', '/Users/crmarsh/.local/share/uv/python/cpython-3.13.0-macos-aarch64-none/lib/python3.13/lib-dynload', '/Users/crmarsh/.cache/uv/archive-v0/AhKcORkaCdbBl31VweRtG/lib/python3.13/site-packages', '/Users/crmarsh/workspace/uv/foo/.venv/lib/python3.13/site-packages']
```
This commit is contained in:
Charlie Marsh 2025-02-28 22:22:37 -05:00 committed by GitHub
parent 3017b82ecc
commit 3398cb1902
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 169 additions and 2 deletions

View file

@ -270,6 +270,16 @@ impl PythonEnvironment {
Ok(PyVenvConfiguration::parse(self.0.root.join("pyvenv.cfg"))?)
}
/// Set a key-value pair in the `pyvenv.cfg` file.
pub fn set_pyvenv_cfg(&self, key: &str, value: &str) -> Result<(), Error> {
let content = fs_err::read_to_string(self.0.root.join("pyvenv.cfg"))?;
fs_err::write(
self.0.root.join("pyvenv.cfg"),
PyVenvConfiguration::set(&content, key, value),
)?;
Ok(())
}
/// Returns `true` if the environment is "relocatable".
pub fn relocatable(&self) -> bool {
self.cfg().is_ok_and(|cfg| cfg.is_relocatable())

View file

@ -1,3 +1,4 @@
use std::borrow::Cow;
use std::{
env, io,
path::{Path, PathBuf},
@ -5,6 +6,7 @@ use std::{
use fs_err as fs;
use thiserror::Error;
use uv_pypi_types::Scheme;
use uv_static::EnvVars;
@ -37,6 +39,8 @@ pub struct PyVenvConfiguration {
pub(crate) relocatable: bool,
/// Was the virtual environment populated with seed packages?
pub(crate) seed: bool,
/// Should the virtual environment include system site packages?
pub(crate) include_system_side_packages: bool,
}
#[derive(Debug, Error)]
@ -188,6 +192,7 @@ impl PyVenvConfiguration {
let mut uv = false;
let mut relocatable = false;
let mut seed = false;
let mut include_system_side_packages = false;
// Per https://snarky.ca/how-virtual-environments-work/, the `pyvenv.cfg` file is not a
// valid INI file, and is instead expected to be parsed by partitioning each line on the
@ -211,6 +216,9 @@ impl PyVenvConfiguration {
"seed" => {
seed = value.trim().to_lowercase() == "true";
}
"include-system-site-packages" => {
include_system_side_packages = value.trim().to_lowercase() == "true";
}
_ => {}
}
}
@ -220,6 +228,7 @@ impl PyVenvConfiguration {
uv,
relocatable,
seed,
include_system_side_packages,
})
}
@ -242,4 +251,119 @@ impl PyVenvConfiguration {
pub fn is_seed(&self) -> bool {
self.seed
}
/// Returns true if the virtual environment should include system site packages.
pub fn include_system_side_packages(&self) -> bool {
self.include_system_side_packages
}
/// Set the key-value pair in the `pyvenv.cfg` file.
pub fn set(content: &str, key: &str, value: &str) -> String {
let mut lines = content.lines().map(Cow::Borrowed).collect::<Vec<_>>();
let mut found = false;
for line in &mut lines {
if let Some((lhs, _)) = line.split_once('=') {
if lhs.trim() == key {
*line = Cow::Owned(format!("{key} = {value}"));
found = true;
break;
}
}
}
if !found {
lines.push(Cow::Owned(format!("{key} = {value}")));
}
if lines.is_empty() {
String::new()
} else {
format!("{}\n", lines.join("\n"))
}
}
}
#[cfg(test)]
mod tests {
use indoc::indoc;
use super::*;
#[test]
fn test_set_existing_key() {
let content = indoc! {"
home = /path/to/python
version = 3.8.0
include-system-site-packages = false
"};
let result = PyVenvConfiguration::set(content, "version", "3.9.0");
assert_eq!(
result,
indoc! {"
home = /path/to/python
version = 3.9.0
include-system-site-packages = false
"}
);
}
#[test]
fn test_set_new_key() {
let content = indoc! {"
home = /path/to/python
version = 3.8.0
"};
let result = PyVenvConfiguration::set(content, "include-system-site-packages", "false");
assert_eq!(
result,
indoc! {"
home = /path/to/python
version = 3.8.0
include-system-site-packages = false
"}
);
}
#[test]
fn test_set_key_no_spaces() {
let content = indoc! {"
home=/path/to/python
version=3.8.0
"};
let result = PyVenvConfiguration::set(content, "include-system-site-packages", "false");
assert_eq!(
result,
indoc! {"
home=/path/to/python
version=3.8.0
include-system-site-packages = false
"}
);
}
#[test]
fn test_set_key_prefix() {
let content = indoc! {"
home = /path/to/python
home_dir = /other/path
"};
let result = PyVenvConfiguration::set(content, "home", "new/path");
assert_eq!(
result,
indoc! {"
home = new/path
home_dir = /other/path
"}
);
}
#[test]
fn test_set_empty_content() {
let content = "";
let result = PyVenvConfiguration::set(content, "version", "3.9.0");
assert_eq!(
result,
indoc! {"
version = 3.9.0
"}
);
}
}

View file

@ -150,6 +150,22 @@ impl CachedEnvironment {
Ok(())
}
/// Enable system site packages for a Python environment.
#[allow(clippy::result_large_err)]
pub(crate) fn set_system_site_packages(&self) -> Result<(), ProjectError> {
self.0
.set_pyvenv_cfg("include-system-site-packages", "true")?;
Ok(())
}
/// Disable system site packages for a Python environment.
#[allow(clippy::result_large_err)]
pub(crate) fn clear_system_site_packages(&self) -> Result<(), ProjectError> {
self.0
.set_pyvenv_cfg("include-system-site-packages", "false")?;
Ok(())
}
/// Return the [`Interpreter`] to use for the cached environment, based on a given
/// [`Interpreter`].
///

View file

@ -25,8 +25,9 @@ use uv_fs::{PythonExt, Simplified};
use uv_installer::{SatisfiesResult, SitePackages};
use uv_normalize::PackageName;
use uv_python::{
EnvironmentPreference, Interpreter, PythonDownloads, PythonEnvironment, PythonInstallation,
PythonPreference, PythonRequest, PythonVersionFile, VersionFileDiscoveryOptions,
EnvironmentPreference, Interpreter, PyVenvConfiguration, PythonDownloads, PythonEnvironment,
PythonInstallation, PythonPreference, PythonRequest, PythonVersionFile,
VersionFileDiscoveryOptions,
};
use uv_requirements::{RequirementsSource, RequirementsSpecification};
use uv_resolver::Lock;
@ -923,6 +924,21 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl
"import site; site.addsitedir(\"{}\")",
site_packages.escape_for_python()
))?;
// If `--system-site-packages` is enabled, add the system site packages to the ephemeral
// environment.
if base_interpreter
.is_virtualenv()
.then(|| {
PyVenvConfiguration::parse(base_interpreter.sys_prefix().join("pyvenv.cfg"))
.is_ok_and(|cfg| cfg.include_system_side_packages())
})
.unwrap_or(false)
{
ephemeral_env.set_system_site_packages()?;
} else {
ephemeral_env.clear_system_site_packages()?;
}
}
// Cast from `CachedEnvironment` to `PythonEnvironment`.

View file

@ -801,6 +801,7 @@ async fn get_or_create_environment(
// Clear any existing overlay.
environment.clear_overlay()?;
environment.clear_system_site_packages()?;
Ok((from, environment.into()))
}