mirror of
https://github.com/astral-sh/uv.git
synced 2025-08-04 10:58:28 +00:00
Allow --force
to overwrite existing virtualenv (#2548)
## Summary Closes https://github.com/astral-sh/uv/issues/2529. ## Test Plan - `mkdir .venv` - `touch .venv/foo` - `cargo run venv` (ensure failure) - `cargo run venv --force` (ensure success) - `cargo run venv --force` (ensure success again)
This commit is contained in:
parent
630d3fde5c
commit
614c07329b
11 changed files with 127 additions and 41 deletions
|
@ -414,6 +414,7 @@ impl SourceBuild {
|
|||
interpreter.clone(),
|
||||
uv_virtualenv::Prompt::None,
|
||||
false,
|
||||
false,
|
||||
)?,
|
||||
BuildIsolation::Shared(venv) => venv.clone(),
|
||||
};
|
||||
|
|
|
@ -86,19 +86,26 @@ pub fn replace_symlink(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io:
|
|||
)
|
||||
}
|
||||
|
||||
/// Create a symlink from `src` to `dst`, replacing any existing symlink.
|
||||
/// Create a symlink from `src` to `dst`, replacing any existing symlink if necessary.
|
||||
#[cfg(unix)]
|
||||
pub fn replace_symlink(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
|
||||
// Create a symlink to the directory store.
|
||||
let temp_dir =
|
||||
tempfile::tempdir_in(dst.as_ref().parent().expect("Cache entry to have parent"))?;
|
||||
let temp_file = temp_dir.path().join("link");
|
||||
std::os::unix::fs::symlink(src, &temp_file)?;
|
||||
// Attempt to create the symlink directly.
|
||||
match std::os::unix::fs::symlink(src.as_ref(), dst.as_ref()) {
|
||||
Ok(()) => Ok(()),
|
||||
Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => {
|
||||
// Create a symlink to the directory store, using a temporary file to ensure atomicity.
|
||||
let temp_dir =
|
||||
tempfile::tempdir_in(dst.as_ref().parent().expect("Cache entry to have parent"))?;
|
||||
let temp_file = temp_dir.path().join("link");
|
||||
std::os::unix::fs::symlink(src, &temp_file)?;
|
||||
|
||||
// Move the symlink into the wheel cache.
|
||||
fs_err::rename(&temp_file, dst.as_ref())?;
|
||||
// Move the symlink into the wheel cache.
|
||||
fs_err::rename(&temp_file, dst.as_ref())?;
|
||||
|
||||
Ok(())
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
|
||||
/// Write `data` to `path` atomically using a temporary file and atomic rename.
|
||||
|
|
|
@ -47,6 +47,7 @@ pub fn create_bare_venv(
|
|||
interpreter: &Interpreter,
|
||||
prompt: Prompt,
|
||||
system_site_packages: bool,
|
||||
force: bool,
|
||||
) -> Result<Virtualenv, Error> {
|
||||
// Determine the base Python executable; that is, the Python executable that should be
|
||||
// considered the "base" for the virtual environment. This is typically the Python executable
|
||||
|
@ -88,7 +89,9 @@ pub fn create_bare_venv(
|
|||
format!("File exists at `{}`", location.user_display()),
|
||||
)));
|
||||
} else if metadata.is_dir() {
|
||||
if location.join("pyvenv.cfg").is_file() {
|
||||
if force {
|
||||
info!("Overwriting existing directory");
|
||||
} else if location.join("pyvenv.cfg").is_file() {
|
||||
info!("Removing existing directory");
|
||||
fs::remove_dir_all(location)?;
|
||||
fs::create_dir_all(location)?;
|
||||
|
@ -147,19 +150,17 @@ pub fn create_bare_venv(
|
|||
})?;
|
||||
|
||||
// Different names for the python interpreter
|
||||
fs::create_dir(&scripts)?;
|
||||
fs::create_dir_all(&scripts)?;
|
||||
let executable = scripts.join(format!("python{EXE_SUFFIX}"));
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use fs_err::os::unix::fs::symlink;
|
||||
|
||||
symlink(&base_python, &executable)?;
|
||||
symlink(
|
||||
uv_fs::replace_symlink(&base_python, &executable)?;
|
||||
uv_fs::replace_symlink(
|
||||
"python",
|
||||
scripts.join(format!("python{}", interpreter.python_major())),
|
||||
)?;
|
||||
symlink(
|
||||
uv_fs::replace_symlink(
|
||||
"python",
|
||||
scripts.join(format!(
|
||||
"python{}.{}",
|
||||
|
|
|
@ -51,9 +51,10 @@ pub fn create_venv(
|
|||
interpreter: Interpreter,
|
||||
prompt: Prompt,
|
||||
system_site_packages: bool,
|
||||
force: bool,
|
||||
) -> Result<PythonEnvironment, Error> {
|
||||
// Create the virtualenv at the given location.
|
||||
let virtualenv = create_bare_venv(location, &interpreter, prompt, system_site_packages)?;
|
||||
let virtualenv = create_bare_venv(location, &interpreter, prompt, system_site_packages, force)?;
|
||||
|
||||
// Create the corresponding `PythonEnvironment`.
|
||||
let interpreter = interpreter.with_virtualenv(virtualenv);
|
||||
|
|
|
@ -46,6 +46,7 @@ fn run() -> Result<(), uv_virtualenv::Error> {
|
|||
&interpreter,
|
||||
Prompt::from_args(cli.prompt),
|
||||
cli.system_site_packages,
|
||||
false,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -1679,6 +1679,15 @@ pub(crate) struct VenvArgs {
|
|||
#[arg(long)]
|
||||
pub(crate) seed: bool,
|
||||
|
||||
/// Overwrite the directory at the specified path when creating the virtual environment.
|
||||
///
|
||||
/// By default, `uv venv` will remove an existing virtual environment at the given path, and
|
||||
/// exit with an error if the path is non-empty but _not_ a virtual environment. The `--force`
|
||||
/// option will instead write to the given path, regardless of its contents, and without
|
||||
/// clearing it beforehand.
|
||||
#[clap(long)]
|
||||
pub(crate) force: bool,
|
||||
|
||||
/// The path to the virtual environment to create.
|
||||
#[arg(default_value = ".venv")]
|
||||
pub(crate) name: PathBuf,
|
||||
|
|
|
@ -268,6 +268,7 @@ async fn environment_for_run(
|
|||
python_env.into_interpreter(),
|
||||
uv_virtualenv::Prompt::None,
|
||||
false,
|
||||
false,
|
||||
)?;
|
||||
|
||||
// Determine the tags, markers, and interpreter to use for resolution.
|
||||
|
|
|
@ -29,7 +29,11 @@ use crate::printer::Printer;
|
|||
use crate::shell::Shell;
|
||||
|
||||
/// Create a virtual environment.
|
||||
#[allow(clippy::unnecessary_wraps, clippy::too_many_arguments)]
|
||||
#[allow(
|
||||
clippy::unnecessary_wraps,
|
||||
clippy::too_many_arguments,
|
||||
clippy::fn_params_excessive_bools
|
||||
)]
|
||||
pub(crate) async fn venv(
|
||||
path: &Path,
|
||||
python_request: Option<&str>,
|
||||
|
@ -41,6 +45,7 @@ pub(crate) async fn venv(
|
|||
system_site_packages: bool,
|
||||
connectivity: Connectivity,
|
||||
seed: bool,
|
||||
force: bool,
|
||||
exclude_newer: Option<ExcludeNewer>,
|
||||
native_tls: bool,
|
||||
cache: &Cache,
|
||||
|
@ -57,6 +62,7 @@ pub(crate) async fn venv(
|
|||
system_site_packages,
|
||||
connectivity,
|
||||
seed,
|
||||
force,
|
||||
exclude_newer,
|
||||
native_tls,
|
||||
cache,
|
||||
|
@ -92,7 +98,7 @@ enum VenvError {
|
|||
}
|
||||
|
||||
/// Create a virtual environment.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
#[allow(clippy::too_many_arguments, clippy::fn_params_excessive_bools)]
|
||||
async fn venv_impl(
|
||||
path: &Path,
|
||||
python_request: Option<&str>,
|
||||
|
@ -104,6 +110,7 @@ async fn venv_impl(
|
|||
system_site_packages: bool,
|
||||
connectivity: Connectivity,
|
||||
seed: bool,
|
||||
force: bool,
|
||||
exclude_newer: Option<ExcludeNewer>,
|
||||
native_tls: bool,
|
||||
cache: &Cache,
|
||||
|
@ -140,7 +147,7 @@ async fn venv_impl(
|
|||
.into_diagnostic()?;
|
||||
|
||||
// Create the virtual environment.
|
||||
let venv = uv_virtualenv::create_venv(path, interpreter, prompt, system_site_packages)
|
||||
let venv = uv_virtualenv::create_venv(path, interpreter, prompt, system_site_packages, force)
|
||||
.map_err(VenvError::Creation)?;
|
||||
|
||||
// Install seed packages.
|
||||
|
|
|
@ -478,6 +478,7 @@ async fn run() -> Result<ExitStatus> {
|
|||
args.system_site_packages,
|
||||
args.shared.connectivity,
|
||||
args.seed,
|
||||
args.force,
|
||||
args.shared.exclude_newer,
|
||||
globals.native_tls,
|
||||
&cache,
|
||||
|
|
|
@ -736,6 +736,7 @@ impl PipCheckSettings {
|
|||
pub(crate) struct VenvSettings {
|
||||
// CLI-only settings.
|
||||
pub(crate) seed: bool,
|
||||
pub(crate) force: bool,
|
||||
pub(crate) name: PathBuf,
|
||||
pub(crate) prompt: Option<String>,
|
||||
pub(crate) system_site_packages: bool,
|
||||
|
@ -752,6 +753,7 @@ impl VenvSettings {
|
|||
system,
|
||||
no_system,
|
||||
seed,
|
||||
force,
|
||||
name,
|
||||
prompt,
|
||||
system_site_packages,
|
||||
|
@ -770,6 +772,7 @@ impl VenvSettings {
|
|||
Self {
|
||||
// CLI-only settings.
|
||||
seed,
|
||||
force,
|
||||
name,
|
||||
prompt,
|
||||
system_site_packages,
|
||||
|
|
|
@ -53,6 +53,7 @@ impl VenvTestContext {
|
|||
.arg("--exclude-newer")
|
||||
.arg(EXCLUDE_NEWER)
|
||||
.env("UV_TEST_PYTHON_PATH", self.python_path.clone())
|
||||
.env("UV_NO_WRAP", "1")
|
||||
.current_dir(self.temp_dir.path());
|
||||
command
|
||||
}
|
||||
|
@ -126,8 +127,7 @@ fn create_venv() {
|
|||
uv_snapshot!(context.filters(), context.venv_command()
|
||||
.arg(context.venv.as_os_str())
|
||||
.arg("--python")
|
||||
.arg("3.12")
|
||||
.env("UV_NO_WRAP", "1"), @r###"
|
||||
.arg("3.12"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
@ -147,8 +147,7 @@ fn create_venv_defaults_to_cwd() {
|
|||
let context = VenvTestContext::new(&["3.12"]);
|
||||
uv_snapshot!(context.filters(), context.venv_command()
|
||||
.arg("--python")
|
||||
.arg("3.12")
|
||||
.env("UV_NO_WRAP", "1"), @r###"
|
||||
.arg("3.12"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
@ -170,8 +169,7 @@ fn seed() {
|
|||
.arg(context.venv.as_os_str())
|
||||
.arg("--seed")
|
||||
.arg("--python")
|
||||
.arg("3.12")
|
||||
.env("UV_NO_WRAP", "1"), @r###"
|
||||
.arg("3.12"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
@ -194,8 +192,7 @@ fn seed_older_python_version() {
|
|||
.arg(context.venv.as_os_str())
|
||||
.arg("--seed")
|
||||
.arg("--python")
|
||||
.arg("3.10")
|
||||
.env("UV_NO_WRAP", "1"), @r###"
|
||||
.arg("3.10"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
@ -221,8 +218,7 @@ fn create_venv_unknown_python_minor() {
|
|||
command
|
||||
.arg(context.venv.as_os_str())
|
||||
.arg("--python")
|
||||
.arg("3.15")
|
||||
.env("UV_NO_WRAP", "1");
|
||||
.arg("3.15");
|
||||
if cfg!(windows) {
|
||||
uv_snapshot!(&mut command, @r###"
|
||||
success: false
|
||||
|
@ -265,8 +261,7 @@ fn create_venv_unknown_python_patch() {
|
|||
uv_snapshot!(filters, context.venv_command()
|
||||
.arg(context.venv.as_os_str())
|
||||
.arg("--python")
|
||||
.arg("3.8.0")
|
||||
.env("UV_NO_WRAP", "1"), @r###"
|
||||
.arg("3.8.0"), @r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
|
@ -287,8 +282,7 @@ fn create_venv_python_patch() {
|
|||
uv_snapshot!(context.filters(), context.venv_command()
|
||||
.arg(context.venv.as_os_str())
|
||||
.arg("--python")
|
||||
.arg("3.12.1")
|
||||
.env("UV_NO_WRAP", "1"), @r###"
|
||||
.arg("3.12.1"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
@ -313,8 +307,7 @@ fn file_exists() -> Result<()> {
|
|||
uv_snapshot!(context.filters(), context.venv_command()
|
||||
.arg(context.venv.as_os_str())
|
||||
.arg("--python")
|
||||
.arg("3.12")
|
||||
.env("UV_NO_WRAP", "1"), @r###"
|
||||
.arg("3.12"), @r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
|
@ -341,8 +334,7 @@ fn empty_dir_exists() -> Result<()> {
|
|||
uv_snapshot!(context.filters(), context.venv_command()
|
||||
.arg(context.venv.as_os_str())
|
||||
.arg("--python")
|
||||
.arg("3.12")
|
||||
.env("UV_NO_WRAP", "1"), @r###"
|
||||
.arg("3.12"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
@ -370,8 +362,7 @@ fn non_empty_dir_exists() -> Result<()> {
|
|||
uv_snapshot!(context.filters(), context.venv_command()
|
||||
.arg(context.venv.as_os_str())
|
||||
.arg("--python")
|
||||
.arg("3.12")
|
||||
.env("UV_NO_WRAP", "1"), @r###"
|
||||
.arg("3.12"), @r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
|
@ -389,6 +380,70 @@ fn non_empty_dir_exists() -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_empty_dir_exists_force() -> Result<()> {
|
||||
let context = VenvTestContext::new(&["3.12"]);
|
||||
|
||||
// Create a non-empty directory at `.venv`. Creating a virtualenv at the same path should
|
||||
// succeed when `--force` is specified, but fail when it is not.
|
||||
context.venv.create_dir_all()?;
|
||||
context.venv.child("file").touch()?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.venv_command()
|
||||
.arg(context.venv.as_os_str())
|
||||
.arg("--python")
|
||||
.arg("3.12"), @r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Using Python 3.12.[X] interpreter at: [PATH]
|
||||
Creating virtualenv at: .venv
|
||||
uv::venv::creation
|
||||
|
||||
× Failed to create virtualenv
|
||||
╰─▶ The directory `.venv` exists, but it's not a virtualenv
|
||||
"###
|
||||
);
|
||||
|
||||
uv_snapshot!(context.filters(), context.venv_command()
|
||||
.arg(context.venv.as_os_str())
|
||||
.arg("--force")
|
||||
.arg("--python")
|
||||
.arg("3.12"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Using Python 3.12.[X] interpreter at: [PATH]
|
||||
Creating virtualenv at: .venv
|
||||
Activate with: source .venv/bin/activate
|
||||
"###
|
||||
);
|
||||
|
||||
// Running again should _also_ succeed, overwriting existing symlinks and respecting existing
|
||||
// directories.
|
||||
uv_snapshot!(context.filters(), context.venv_command()
|
||||
.arg(context.venv.as_os_str())
|
||||
.arg("--force")
|
||||
.arg("--python")
|
||||
.arg("3.12"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Using Python 3.12.[X] interpreter at: [PATH]
|
||||
Creating virtualenv at: .venv
|
||||
Activate with: source .venv/bin/activate
|
||||
"###
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(windows)]
|
||||
fn windows_shims() -> Result<()> {
|
||||
|
@ -509,7 +564,6 @@ fn verify_nested_pyvenv_cfg() -> Result<()> {
|
|||
.arg("--python")
|
||||
.arg("3.12")
|
||||
.env("VIRTUAL_ENV", context.venv.as_os_str())
|
||||
.env("UV_NO_WRAP", "1")
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue