mirror of
https://github.com/astral-sh/uv.git
synced 2025-10-03 23:34:35 +00:00
feat: allow passing extra config k,v pairs for pyvenv.cfg when creating a venv (#1852)
<!-- Thank you for contributing to uv! To help us out with reviewing, please consider the following: - Does this pull request include a summary of the change? (See below.) - Does this pull request include a descriptive title? - Does this pull request include references to any relevant issues? --> ## Summary This modifies `gourgeist` to allow passing additional k,v pairs to add to the `pyvenv.cfg` file as proposed in #1697. I made it allow an arbitrary set of pairs (to decouple from `uv` since this is mainly a change to `gourgeist`) , but I can slim it down to just allow just a name and version strings if that's desired. The `pyvenv.cfg` will also have a `uv = <uv-crate-version>` when a venv is created via `uv venv` ~~and `uv-build = <uv-build-crate-version>` when it's created via `SourceBuild::setup`~~. Example below via `uv venv`: ```ini home = ... implementation = CPython version_info = 3.12 include-system-site-packages = false base-prefix = ... base-exec-prefix = ... base-executable = ... uv = 0.1.6 prompt = uv ``` Open to any suggestions, thanks! Closes #1697 ## Test Plan Added new test in `tests/venv.rs` called `verify_pyvenv_cfg` to verify that it contains the right uv version string. I didn't see tests configured in `gourgeist` itself, so I didn't add any there.
This commit is contained in:
parent
f0b39a36b4
commit
2fa67eae6f
6 changed files with 79 additions and 31 deletions
|
@ -13,7 +13,7 @@ use uv_fs::Normalized;
|
||||||
|
|
||||||
use uv_interpreter::Interpreter;
|
use uv_interpreter::Interpreter;
|
||||||
|
|
||||||
use crate::Prompt;
|
use crate::{Error, Prompt};
|
||||||
|
|
||||||
/// The bash activate scripts with the venv dependent paths patches out
|
/// The bash activate scripts with the venv dependent paths patches out
|
||||||
const ACTIVATE_TEMPLATES: &[(&str, &str)] = &[
|
const ACTIVATE_TEMPLATES: &[(&str, &str)] = &[
|
||||||
|
@ -33,17 +33,10 @@ const ACTIVATE_TEMPLATES: &[(&str, &str)] = &[
|
||||||
const VIRTUALENV_PATCH: &str = include_str!("_virtualenv.py");
|
const VIRTUALENV_PATCH: &str = include_str!("_virtualenv.py");
|
||||||
|
|
||||||
/// Very basic `.cfg` file format writer.
|
/// Very basic `.cfg` file format writer.
|
||||||
fn write_cfg(
|
fn write_cfg(f: &mut impl Write, data: &[(String, String)]) -> io::Result<()> {
|
||||||
f: &mut impl Write,
|
|
||||||
data: &[(&str, String); 8],
|
|
||||||
prompt: Option<String>,
|
|
||||||
) -> io::Result<()> {
|
|
||||||
for (key, value) in data {
|
for (key, value) in data {
|
||||||
writeln!(f, "{key} = {value}")?;
|
writeln!(f, "{key} = {value}")?;
|
||||||
}
|
}
|
||||||
if let Some(prompt) = prompt {
|
|
||||||
writeln!(f, "prompt = {prompt}")?;
|
|
||||||
}
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -73,7 +66,8 @@ pub fn create_bare_venv(
|
||||||
location: &Utf8Path,
|
location: &Utf8Path,
|
||||||
interpreter: &Interpreter,
|
interpreter: &Interpreter,
|
||||||
prompt: Prompt,
|
prompt: Prompt,
|
||||||
) -> io::Result<VenvPaths> {
|
extra_cfg: Vec<(String, String)>,
|
||||||
|
) -> Result<VenvPaths, Error> {
|
||||||
// We have to canonicalize the interpreter path, otherwise the home is set to the venv dir instead of the real root.
|
// We have to canonicalize the interpreter path, otherwise the home is set to the venv dir instead of the real root.
|
||||||
// This would make python-build-standalone fail with the encodings module not being found because its home is wrong.
|
// This would make python-build-standalone fail with the encodings module not being found because its home is wrong.
|
||||||
let base_python: Utf8PathBuf = fs_err::canonicalize(interpreter.sys_executable())?
|
let base_python: Utf8PathBuf = fs_err::canonicalize(interpreter.sys_executable())?
|
||||||
|
@ -84,10 +78,10 @@ pub fn create_bare_venv(
|
||||||
match location.metadata() {
|
match location.metadata() {
|
||||||
Ok(metadata) => {
|
Ok(metadata) => {
|
||||||
if metadata.is_file() {
|
if metadata.is_file() {
|
||||||
return Err(io::Error::new(
|
return Err(Error::IO(io::Error::new(
|
||||||
io::ErrorKind::AlreadyExists,
|
io::ErrorKind::AlreadyExists,
|
||||||
format!("File exists at `{location}`"),
|
format!("File exists at `{location}`"),
|
||||||
));
|
)));
|
||||||
} else if metadata.is_dir() {
|
} else if metadata.is_dir() {
|
||||||
if location.join("pyvenv.cfg").is_file() {
|
if location.join("pyvenv.cfg").is_file() {
|
||||||
info!("Removing existing directory");
|
info!("Removing existing directory");
|
||||||
|
@ -99,17 +93,17 @@ pub fn create_bare_venv(
|
||||||
{
|
{
|
||||||
info!("Ignoring empty directory");
|
info!("Ignoring empty directory");
|
||||||
} else {
|
} else {
|
||||||
return Err(io::Error::new(
|
return Err(Error::IO(io::Error::new(
|
||||||
io::ErrorKind::AlreadyExists,
|
io::ErrorKind::AlreadyExists,
|
||||||
format!("The directory `{location}` exists, but it's not a virtualenv"),
|
format!("The directory `{location}` exists, but it's not a virtualenv"),
|
||||||
));
|
)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(err) if err.kind() == io::ErrorKind::NotFound => {
|
Err(err) if err.kind() == io::ErrorKind::NotFound => {
|
||||||
fs::create_dir_all(location)?;
|
fs::create_dir_all(location)?;
|
||||||
}
|
}
|
||||||
Err(err) => return Err(err),
|
Err(err) => return Err(Error::IO(err)),
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(konstin): I bet on windows we'll have to strip the prefix again
|
// TODO(konstin): I bet on windows we'll have to strip the prefix again
|
||||||
|
@ -226,31 +220,58 @@ pub fn create_bare_venv(
|
||||||
} else {
|
} else {
|
||||||
unimplemented!("Only Windows and Unix are supported")
|
unimplemented!("Only Windows and Unix are supported")
|
||||||
};
|
};
|
||||||
let pyvenv_cfg_data = &[
|
|
||||||
("home", python_home),
|
// Validate extra_cfg
|
||||||
|
let reserved_keys = [
|
||||||
|
"home",
|
||||||
|
"implementation",
|
||||||
|
"version_info",
|
||||||
|
"include-system-site-packages",
|
||||||
|
"base-prefix",
|
||||||
|
"base-exec-prefix",
|
||||||
|
"base-executable",
|
||||||
|
"prompt",
|
||||||
|
];
|
||||||
|
for (key, _) in &extra_cfg {
|
||||||
|
if reserved_keys.contains(&key.as_str()) {
|
||||||
|
return Err(Error::ReservedConfigKey(key.to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut pyvenv_cfg_data: Vec<(String, String)> = vec![
|
||||||
|
("home".to_string(), python_home),
|
||||||
(
|
(
|
||||||
"implementation",
|
"implementation".to_string(),
|
||||||
interpreter.markers().platform_python_implementation.clone(),
|
interpreter.markers().platform_python_implementation.clone(),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"version_info",
|
"version_info".to_string(),
|
||||||
interpreter.markers().python_version.string.clone(),
|
interpreter.markers().python_version.string.clone(),
|
||||||
),
|
),
|
||||||
("gourgeist", env!("CARGO_PKG_VERSION").to_string()),
|
|
||||||
// I wouldn't allow this option anyway
|
|
||||||
("include-system-site-packages", "false".to_string()),
|
|
||||||
(
|
(
|
||||||
"base-prefix",
|
"include-system-site-packages".to_string(),
|
||||||
|
"false".to_string(),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"base-prefix".to_string(),
|
||||||
interpreter.base_prefix().to_string_lossy().to_string(),
|
interpreter.base_prefix().to_string_lossy().to_string(),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"base-exec-prefix",
|
"base-exec-prefix".to_string(),
|
||||||
interpreter.base_exec_prefix().to_string_lossy().to_string(),
|
interpreter.base_exec_prefix().to_string_lossy().to_string(),
|
||||||
),
|
),
|
||||||
("base-executable", base_python.to_string()),
|
("base-executable".to_string(), base_python.to_string()),
|
||||||
];
|
]
|
||||||
|
.into_iter()
|
||||||
|
.chain(extra_cfg)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if let Some(prompt) = prompt {
|
||||||
|
pyvenv_cfg_data.push(("prompt".to_string(), prompt));
|
||||||
|
}
|
||||||
|
|
||||||
let mut pyvenv_cfg = BufWriter::new(File::create(location.join("pyvenv.cfg"))?);
|
let mut pyvenv_cfg = BufWriter::new(File::create(location.join("pyvenv.cfg"))?);
|
||||||
write_cfg(&mut pyvenv_cfg, pyvenv_cfg_data, prompt)?;
|
write_cfg(&mut pyvenv_cfg, &pyvenv_cfg_data)?;
|
||||||
drop(pyvenv_cfg);
|
drop(pyvenv_cfg);
|
||||||
|
|
||||||
let site_packages = if cfg!(unix) {
|
let site_packages = if cfg!(unix) {
|
||||||
|
|
|
@ -21,6 +21,8 @@ pub enum Error {
|
||||||
InvalidPythonInterpreter(#[source] Box<dyn std::error::Error + Sync + Send>),
|
InvalidPythonInterpreter(#[source] Box<dyn std::error::Error + Sync + Send>),
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Platform(#[from] PlatformError),
|
Platform(#[from] PlatformError),
|
||||||
|
#[error("Reserved key used for pyvenv.cfg: {0}")]
|
||||||
|
ReservedConfigKey(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The value to use for the shell prompt when inside a virtual environment.
|
/// The value to use for the shell prompt when inside a virtual environment.
|
||||||
|
@ -51,11 +53,12 @@ pub fn create_venv(
|
||||||
location: &Path,
|
location: &Path,
|
||||||
interpreter: Interpreter,
|
interpreter: Interpreter,
|
||||||
prompt: Prompt,
|
prompt: Prompt,
|
||||||
|
extra_cfg: Vec<(String, String)>,
|
||||||
) -> Result<Virtualenv, Error> {
|
) -> Result<Virtualenv, Error> {
|
||||||
let location: &Utf8Path = location
|
let location: &Utf8Path = location
|
||||||
.try_into()
|
.try_into()
|
||||||
.map_err(|err: FromPathError| err.into_io_error())?;
|
.map_err(|err: FromPathError| err.into_io_error())?;
|
||||||
let paths = create_bare_venv(location, &interpreter, prompt)?;
|
let paths = create_bare_venv(location, &interpreter, prompt, extra_cfg)?;
|
||||||
Ok(Virtualenv::from_interpreter(
|
Ok(Virtualenv::from_interpreter(
|
||||||
interpreter,
|
interpreter,
|
||||||
paths.root.as_std_path(),
|
paths.root.as_std_path(),
|
||||||
|
|
|
@ -36,7 +36,7 @@ fn run() -> Result<(), gourgeist::Error> {
|
||||||
Cache::from_path(".gourgeist_cache")?
|
Cache::from_path(".gourgeist_cache")?
|
||||||
};
|
};
|
||||||
let info = Interpreter::query(python.as_std_path(), &platform, &cache).unwrap();
|
let info = Interpreter::query(python.as_std_path(), &platform, &cache).unwrap();
|
||||||
create_bare_venv(&location, &info, Prompt::from_args(cli.prompt))?;
|
create_bare_venv(&location, &info, Prompt::from_args(cli.prompt), Vec::new())?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -329,6 +329,7 @@ impl SourceBuild {
|
||||||
&temp_dir.path().join(".venv"),
|
&temp_dir.path().join(".venv"),
|
||||||
interpreter.clone(),
|
interpreter.clone(),
|
||||||
gourgeist::Prompt::None,
|
gourgeist::Prompt::None,
|
||||||
|
Vec::new(),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
// Setup the build environment.
|
// Setup the build environment.
|
||||||
|
|
|
@ -119,8 +119,12 @@ async fn venv_impl(
|
||||||
)
|
)
|
||||||
.into_diagnostic()?;
|
.into_diagnostic()?;
|
||||||
|
|
||||||
|
// Extra cfg for pyvenv.cfg to specify uv version
|
||||||
|
let extra_cfg = vec![("uv".to_string(), env!("CARGO_PKG_VERSION").to_string())];
|
||||||
|
|
||||||
// Create the virtual environment.
|
// Create the virtual environment.
|
||||||
let venv = gourgeist::create_venv(path, interpreter, prompt).map_err(VenvError::Creation)?;
|
let venv = gourgeist::create_venv(path, interpreter, prompt, extra_cfg)
|
||||||
|
.map_err(VenvError::Creation)?;
|
||||||
|
|
||||||
// Install seed packages.
|
// Install seed packages.
|
||||||
if seed {
|
if seed {
|
||||||
|
|
|
@ -7,7 +7,9 @@ use assert_fs::prelude::*;
|
||||||
|
|
||||||
use uv_fs::Normalized;
|
use uv_fs::Normalized;
|
||||||
|
|
||||||
use crate::common::{create_bin_with_executables, get_bin, uv_snapshot, EXCLUDE_NEWER};
|
use crate::common::{
|
||||||
|
create_bin_with_executables, get_bin, uv_snapshot, TestContext, EXCLUDE_NEWER,
|
||||||
|
};
|
||||||
|
|
||||||
mod common;
|
mod common;
|
||||||
|
|
||||||
|
@ -644,3 +646,20 @@ fn virtualenv_compatibility() -> Result<()> {
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn verify_pyvenv_cfg() {
|
||||||
|
let context = TestContext::new("3.12");
|
||||||
|
let venv = context.temp_dir.child(".venv");
|
||||||
|
let pyvenv_cfg = venv.child("pyvenv.cfg");
|
||||||
|
|
||||||
|
venv.assert(predicates::path::is_dir());
|
||||||
|
|
||||||
|
// Check pyvenv.cfg exists
|
||||||
|
pyvenv_cfg.assert(predicates::path::is_file());
|
||||||
|
|
||||||
|
// Check if "uv = version" is present in the file
|
||||||
|
let version = env!("CARGO_PKG_VERSION").to_string();
|
||||||
|
let search_string = format!("uv = {version}");
|
||||||
|
pyvenv_cfg.assert(predicates::str::contains(search_string));
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue