feat(venv): add relocatable flag (#5515)

## Summary

Adds a `--relocatable` CLI arg to `uv venv`. This flag does two things:

* ensures that the associated activation scripts do not rely on a
hardcoded
absolute path to the virtual environment (to the extent possible; `.csh`
and
  `.nu` left as-is)
* persists a `relocatable` flag in `pyvenv.cfg`.

The flag in `pyvenv.cfg` in turn instructs the wheel `Installer` to
create script
entrypoints in a relocatable way (use `exec` trick + `dirname $0` on
POSIX;
use relative path to `python[w].exe` on Windows).

Fixes: #3863

## Test Plan

* Relocatable console scripts covered as additional scenarios in
existing test cases.
* Integration testing of boilerplate generation in `venv`.
* Manual testing of `uv venv` with and without `--relocatable`
This commit is contained in:
Pavel Dikov 2024-07-29 01:10:11 +01:00 committed by GitHub
parent 3626d08cca
commit cb47aed9de
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 299 additions and 26 deletions

View file

@ -162,6 +162,11 @@ impl PythonEnvironment {
Ok(PyVenvConfiguration::parse(self.0.root.join("pyvenv.cfg"))?)
}
/// Returns `true` if the environment is "relocatable".
pub fn relocatable(&self) -> bool {
self.cfg().is_ok_and(|cfg| cfg.is_relocatable())
}
/// Returns the location of the Python executable.
pub fn python_executable(&self) -> &Path {
self.0.interpreter.sys_executable()

View file

@ -28,6 +28,8 @@ pub struct PyVenvConfiguration {
pub(crate) virtualenv: bool,
/// If the uv package was used to create the virtual environment.
pub(crate) uv: bool,
/// Is the virtual environment relocatable?
pub(crate) relocatable: bool,
}
#[derive(Debug, Error)]
@ -136,6 +138,7 @@ impl PyVenvConfiguration {
pub fn parse(cfg: impl AsRef<Path>) -> Result<Self, Error> {
let mut virtualenv = false;
let mut uv = false;
let mut relocatable = 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
@ -143,7 +146,7 @@ impl PyVenvConfiguration {
let content = fs::read_to_string(&cfg)
.map_err(|err| Error::ParsePyVenvCfg(cfg.as_ref().to_path_buf(), err))?;
for line in content.lines() {
let Some((key, _value)) = line.split_once('=') else {
let Some((key, value)) = line.split_once('=') else {
continue;
};
match key.trim() {
@ -153,11 +156,18 @@ impl PyVenvConfiguration {
"uv" => {
uv = true;
}
"relocatable" => {
relocatable = value.trim().to_lowercase() == "true";
}
_ => {}
}
}
Ok(Self { virtualenv, uv })
Ok(Self {
virtualenv,
uv,
relocatable,
})
}
/// Returns true if the virtual environment was created with the `virtualenv` package.
@ -169,4 +179,9 @@ impl PyVenvConfiguration {
pub fn is_uv(&self) -> bool {
self.uv
}
/// Returns true if the virtual environment is relocatable.
pub fn is_relocatable(&self) -> bool {
self.relocatable
}
}