diff --git a/docs/reference/cli/pixi.md b/docs/reference/cli/pixi.md index 2c7a4f5169..12bacefe94 100644 --- a/docs/reference/cli/pixi.md +++ b/docs/reference/cli/pixi.md @@ -15,6 +15,7 @@ pixi [OPTIONS] | [`add`](pixi/add.md) | Adds dependencies to the workspace | | [`remove`](pixi/remove.md) | Removes dependencies from the workspace | | [`install`](pixi/install.md) | Install an environment, both updating the lockfile and installing the environment | +| [`reinstall`](pixi/reinstall.md) | Re-install an environment, both updating the lockfile and re-installing the environment | | [`update`](pixi/update.md) | The `update` command checks if there are newer versions of the dependencies and updates the `pixi.lock` file and environments accordingly | | [`upgrade`](pixi/upgrade.md) | Checks if there are newer versions of the dependencies and upgrades them in the lockfile and manifest file | | [`lock`](pixi/lock.md) | Solve environment and update the lock file without installing the environments | diff --git a/docs/reference/cli/pixi/install.md b/docs/reference/cli/pixi/install.md index 64fb0f4d7a..ecb3841cd1 100644 --- a/docs/reference/cli/pixi/install.md +++ b/docs/reference/cli/pixi/install.md @@ -52,7 +52,7 @@ If you want to install all environments, you can use the `--all` flag. Running `pixi install` is not required before running other commands like `pixi run` or `pixi shell`. These commands will automatically install the environment if it is not already installed. -You can use `pixi clean` to remove the installed environments and start fresh. +You can use `pixi reinstall` to reinstall all environments, one environment or just some packages of an environment. --8<-- "docs/reference/cli/pixi/install_extender.md:example" diff --git a/docs/reference/cli/pixi/reinstall.md b/docs/reference/cli/pixi/reinstall.md new file mode 100644 index 0000000000..bb3e442ef6 --- /dev/null +++ b/docs/reference/cli/pixi/reinstall.md @@ -0,0 +1,59 @@ + +# [pixi](../pixi.md) reinstall + +## About +Re-install an environment, both updating the lockfile and re-installing the environment + +--8<-- "docs/reference/cli/pixi/reinstall_extender.md:description" + +## Usage +``` +pixi reinstall [OPTIONS] [PACKAGE]... +``` + +## Arguments +- `` +: Specifies the package that should be reinstalled. If no package is given, the whole environment will be reinstalled +
May be provided more than once. + +## Config Options +- `--tls-no-verify` +: Do not verify the TLS certificate of the server +- `--auth-file ` +: Path to the file containing the authentication token +- `--pypi-keyring-provider ` +: Specifies whether to use the keyring to look up credentials for PyPI +
**options**: `disabled`, `subprocess` +- `--concurrent-solves ` +: Max concurrent solves, default is the number of CPUs +- `--concurrent-downloads ` +: Max concurrent network requests, default is `50` + +## Update Options +- `--frozen` +: Install the environment as defined in the lockfile, doesn't update lockfile if it isn't up-to-date with the manifest file +
**env**: `PIXI_FROZEN` +- `--locked` +: Check if lockfile is up-to-date before installing the environment, aborts when lockfile isn't up-to-date with the manifest file +
**env**: `PIXI_LOCKED` +- `--environment (-e) ` +: The environment to install +
May be provided more than once. +- `--all (-a)` +: Install all environments + +## Global Options +- `--manifest-path ` +: The path to `pixi.toml`, `pyproject.toml`, or the workspace directory + +## Description +Re-install an environment, both updating the lockfile and re-installing the environment. + +This command reinstalls an environment, if the lockfile is not up-to-date it will be updated. If packages are specified, only those packages will be reinstalled. Otherwise the whole environment will be reinstalled. + +`pixi reinstall` only re-installs one environment at a time, if you have multiple environments you can select the right one with the `--environment` flag. If you don't provide an environment, the `default` environment will be re-installed. + +If you want to re-install all environments, you can use the `--all` flag. + + +--8<-- "docs/reference/cli/pixi/reinstall_extender.md:example" diff --git a/src/build/mod.rs b/src/build/mod.rs index fb8e47b38d..9126068512 100644 --- a/src/build/mod.rs +++ b/src/build/mod.rs @@ -257,6 +257,7 @@ impl BuildContext { build_reporter: Arc, source_reporter: Option>, build_id: usize, + rebuild: bool, ) -> Result { let source_checkout = SourceCheckout { path: self @@ -287,10 +288,12 @@ impl BuildContext { .await?; // Check if there are already cached builds - if let Some(build) = cached_build { - if let Some(record) = Self::cached_build_source_record(build, &source_checkout)? { - build_reporter.on_build_cached(build_id); - return Ok(record); + if !rebuild { + if let Some(build) = cached_build { + if let Some(record) = Self::cached_build_source_record(build, &source_checkout)? { + build_reporter.on_build_cached(build_id); + return Ok(record); + } } } diff --git a/src/cli/install.rs b/src/cli/install.rs index 1e25e32b6f..1ab0a48d76 100644 --- a/src/cli/install.rs +++ b/src/cli/install.rs @@ -1,6 +1,6 @@ use crate::cli::cli_config::WorkspaceConfig; use crate::environment::get_update_lock_file_and_prefix; -use crate::lock_file::UpdateMode; +use crate::lock_file::{ReinstallPackages, UpdateMode}; use crate::{UpdateLockFileOptions, WorkspaceLocator}; use clap::Parser; use fancy_display::FancyDisplay; @@ -20,7 +20,7 @@ use pixi_config::ConfigCli; /// Running `pixi install` is not required before running other commands like `pixi run` or `pixi shell`. /// These commands will automatically install the environment if it is not already installed. /// -/// You can use `pixi clean` to remove the installed environments and start fresh. +/// You can use `pixi reinstall` to reinstall all environments, one environment or just some packages of an environment. #[derive(Parser, Debug)] pub struct Args { #[clap(flatten)] @@ -77,6 +77,7 @@ pub async fn execute(args: Args) -> miette::Result<()> { no_install: false, max_concurrent_solves: workspace.config().max_concurrent_solves(), }, + ReinstallPackages::default(), ) .await?; diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 576b3e3c5f..df93857f72 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -25,6 +25,7 @@ pub mod init; pub mod install; pub mod list; pub mod lock; +pub mod reinstall; pub mod remove; pub mod run; pub mod search; @@ -134,6 +135,7 @@ pub enum Command { Remove(remove::Args), #[clap(visible_alias = "i")] Install(install::Args), + Reinstall(reinstall::Args), Update(update::Args), Upgrade(upgrade::Args), Lock(lock::Args), @@ -270,6 +272,7 @@ pub async fn execute_command(command: Command) -> miette::Result<()> { Command::Global(cmd) => global::execute(cmd).await, Command::Auth(cmd) => rattler::cli::auth::execute(cmd).await.into_diagnostic(), Command::Install(cmd) => install::execute(cmd).await, + Command::Reinstall(cmd) => reinstall::execute(cmd).await, Command::Shell(cmd) => shell::execute(cmd).await, Command::ShellHook(cmd) => shell_hook::execute(cmd).await, Command::Task(cmd) => task::execute(cmd).await, diff --git a/src/cli/reinstall.rs b/src/cli/reinstall.rs new file mode 100644 index 0000000000..4bd4baf97a --- /dev/null +++ b/src/cli/reinstall.rs @@ -0,0 +1,120 @@ +use crate::cli::cli_config::WorkspaceConfig; +use crate::environment::get_update_lock_file_and_prefix; +use crate::lock_file::{ReinstallPackages, UpdateMode}; +use crate::{UpdateLockFileOptions, WorkspaceLocator}; +use clap::Parser; +use fancy_display::FancyDisplay; +use itertools::Itertools; +use pixi_config::ConfigCli; + +/// Re-install an environment, both updating the lockfile and re-installing the environment. +/// +/// This command reinstalls an environment, if the lockfile is not up-to-date it will be updated. +/// If packages are specified, only those packages will be reinstalled. +/// Otherwise the whole environment will be reinstalled. +/// +/// `pixi reinstall` only re-installs one environment at a time, +/// if you have multiple environments you can select the right one with the `--environment` flag. +/// If you don't provide an environment, the `default` environment will be re-installed. +/// +/// If you want to re-install all environments, you can use the `--all` flag. +#[derive(Parser, Debug)] +pub struct Args { + /// Specifies the package that should be reinstalled. + /// If no package is given, the whole environment will be reinstalled. + #[arg(value_name = "PACKAGE")] + packages: Option>, + + #[clap(flatten)] + pub project_config: WorkspaceConfig, + + #[clap(flatten)] + pub lock_file_usage: super::LockFileUsageConfig, + + /// The environment to install. + #[arg(long, short)] + pub environment: Option>, + + #[clap(flatten)] + pub config: ConfigCli, + + /// Install all environments. + #[arg(long, short, conflicts_with = "environment")] + pub all: bool, +} + +pub async fn execute(args: Args) -> miette::Result<()> { + let workspace = WorkspaceLocator::for_cli() + .with_search_start(args.project_config.workspace_locator_start()) + .locate()? + .with_cli_config(args.config); + + // Install either: + // + // 1. specific environments + // 2. all environments + // 3. default environment (if no environments are specified) + let envs = if let Some(envs) = args.environment { + envs + } else if args.all { + workspace + .environments() + .iter() + .map(|env| env.name().to_string()) + .collect() + } else { + vec![workspace.default_environment().name().to_string()] + }; + + let reinstall_packages = args + .packages + .map(|p| p.into_iter().collect()) + .map(ReinstallPackages::Some) + .unwrap_or(ReinstallPackages::All); + + let mut installed_envs = Vec::with_capacity(envs.len()); + for env in envs { + let environment = workspace.environment_from_name_or_env_var(Some(env))?; + + // Update the prefix by installing all packages + get_update_lock_file_and_prefix( + &environment, + UpdateMode::Revalidate, + UpdateLockFileOptions { + lock_file_usage: args.lock_file_usage.into(), + no_install: false, + max_concurrent_solves: workspace.config().max_concurrent_solves(), + }, + reinstall_packages.clone(), + ) + .await?; + + installed_envs.push(environment.name().clone()); + } + + // Message what's installed + let detached_envs_message = + if let Ok(Some(path)) = workspace.config().detached_environments().path() { + format!(" in '{}'", console::style(path.display()).bold()) + } else { + "".to_string() + }; + + if installed_envs.len() == 1 { + eprintln!( + "{}The {} environment has been re-installed{}.", + console::style(console::Emoji("✔ ", "")).green(), + installed_envs[0].fancy_display(), + detached_envs_message + ); + } else { + eprintln!( + "{}The following environments have been re-installed: {}\t{}", + console::style(console::Emoji("✔ ", "")).green(), + installed_envs.iter().map(|n| n.fancy_display()).join(", "), + detached_envs_message + ); + } + + Ok(()) +} diff --git a/src/cli/remove.rs b/src/cli/remove.rs index 695845bb73..68a1786342 100644 --- a/src/cli/remove.rs +++ b/src/cli/remove.rs @@ -2,7 +2,7 @@ use super::{cli_config::LockFileUpdateConfig, has_specs::HasSpecs}; use crate::{ cli::cli_config::{DependencyConfig, PrefixUpdateConfig, WorkspaceConfig}, environment::get_update_lock_file_and_prefix, - lock_file::UpdateMode, + lock_file::{ReinstallPackages, UpdateMode}, DependencyType, UpdateLockFileOptions, WorkspaceLocator, }; use clap::Parser; @@ -124,6 +124,7 @@ pub async fn execute(args: Args) -> miette::Result<()> { no_install: prefix_update_config.no_install, max_concurrent_solves: workspace.config().max_concurrent_solves(), }, + ReinstallPackages::default(), ) .await?; } diff --git a/src/cli/run.rs b/src/cli/run.rs index 2b27084ffb..6845e8f7b5 100644 --- a/src/cli/run.rs +++ b/src/cli/run.rs @@ -21,7 +21,7 @@ use tracing::Level; use crate::{ cli::cli_config::{PrefixUpdateConfig, WorkspaceConfig}, environment::sanity_check_project, - lock_file::UpdateLockFileOptions, + lock_file::{ReinstallPackages, UpdateLockFileOptions}, task::{ get_task_env, AmbiguousTask, CanSkip, ExecutableTask, FailedToParseShellScript, InvalidWorkingDirectory, SearchEnvironments, TaskAndEnvironment, TaskGraph, @@ -250,6 +250,7 @@ pub async fn execute(args: Args) -> miette::Result<()> { .prefix( &executable_task.run_environment, args.prefix_update_config.update_mode(), + ReinstallPackages::default(), ) .await?; diff --git a/src/cli/shell.rs b/src/cli/shell.rs index 3f7869d753..02dcfd6b3b 100644 --- a/src/cli/shell.rs +++ b/src/cli/shell.rs @@ -8,13 +8,16 @@ use rattler_shell::{ shell::{CmdExe, PowerShell, Shell, ShellEnum, ShellScript}, }; -use crate::cli::cli_config::{PrefixUpdateConfig, WorkspaceConfig}; use crate::lock_file::UpdateMode; use crate::workspace::get_activated_environment_variables; use crate::{ activation::CurrentEnvVarBehavior, environment::get_update_lock_file_and_prefix, prompt, UpdateLockFileOptions, WorkspaceLocator, }; +use crate::{ + cli::cli_config::{PrefixUpdateConfig, WorkspaceConfig}, + lock_file::ReinstallPackages, +}; use pixi_config::{ConfigCli, ConfigCliActivation, ConfigCliPrompt}; #[cfg(target_family = "unix")] use pixi_pty::unix::PtySession; @@ -284,6 +287,7 @@ pub async fn execute(args: Args) -> miette::Result<()> { && args.lock_file_update_config.no_lockfile_update, max_concurrent_solves: workspace.config().max_concurrent_solves(), }, + ReinstallPackages::default(), ) .await?; diff --git a/src/cli/shell_hook.rs b/src/cli/shell_hook.rs index 8fd376cefd..eb4979e70a 100644 --- a/src/cli/shell_hook.rs +++ b/src/cli/shell_hook.rs @@ -15,6 +15,7 @@ use crate::{ activation::{get_activator, CurrentEnvVarBehavior}, cli::cli_config::{PrefixUpdateConfig, WorkspaceConfig}, environment::get_update_lock_file_and_prefix, + lock_file::ReinstallPackages, prompt, workspace::{get_activated_environment_variables, Environment, HasWorkspaceRef}, UpdateLockFileOptions, Workspace, WorkspaceLocator, @@ -163,6 +164,7 @@ pub async fn execute(args: Args) -> miette::Result<()> { && args.lock_file_update_config.no_lockfile_update, max_concurrent_solves: workspace.config().max_concurrent_solves(), }, + ReinstallPackages::default(), ) .await?; diff --git a/src/cli/workspace/channel/add.rs b/src/cli/workspace/channel/add.rs index 6ffe32ac77..a54a722170 100644 --- a/src/cli/workspace/channel/add.rs +++ b/src/cli/workspace/channel/add.rs @@ -1,6 +1,6 @@ use crate::{ environment::{get_update_lock_file_and_prefix, LockFileUsage}, - lock_file::UpdateMode, + lock_file::{ReinstallPackages, UpdateMode}, UpdateLockFileOptions, WorkspaceLocator, }; use miette::IntoDiagnostic; @@ -31,6 +31,7 @@ pub async fn execute(args: AddRemoveArgs) -> miette::Result<()> { && args.lock_file_update_config.no_lockfile_update, max_concurrent_solves: workspace.workspace().config().max_concurrent_solves(), }, + ReinstallPackages::default(), ) .await?; diff --git a/src/cli/workspace/channel/remove.rs b/src/cli/workspace/channel/remove.rs index 0e0756015d..a1051f88dc 100644 --- a/src/cli/workspace/channel/remove.rs +++ b/src/cli/workspace/channel/remove.rs @@ -1,4 +1,4 @@ -use crate::lock_file::UpdateMode; +use crate::lock_file::{ReinstallPackages, UpdateMode}; use crate::{ environment::{get_update_lock_file_and_prefix, LockFileUsage}, UpdateLockFileOptions, WorkspaceLocator, @@ -29,6 +29,7 @@ pub async fn execute(args: AddRemoveArgs) -> miette::Result<()> { && args.lock_file_update_config.no_lockfile_update, max_concurrent_solves: workspace.workspace().config().max_concurrent_solves(), }, + ReinstallPackages::default(), ) .await?; let workspace = workspace.save().await.into_diagnostic()?; diff --git a/src/cli/workspace/platform/add.rs b/src/cli/workspace/platform/add.rs index 4a25d7ff61..75254e47a1 100644 --- a/src/cli/workspace/platform/add.rs +++ b/src/cli/workspace/platform/add.rs @@ -2,7 +2,7 @@ use std::str::FromStr; use crate::{ environment::{get_update_lock_file_and_prefix, LockFileUsage}, - lock_file::UpdateMode, + lock_file::{ReinstallPackages, UpdateMode}, UpdateLockFileOptions, Workspace, }; use clap::Parser; @@ -55,6 +55,7 @@ pub async fn execute(workspace: Workspace, args: Args) -> miette::Result<()> { no_install: args.no_install, max_concurrent_solves: workspace.workspace().config().max_concurrent_solves(), }, + ReinstallPackages::default(), ) .await?; workspace.save().await.into_diagnostic()?; diff --git a/src/cli/workspace/platform/remove.rs b/src/cli/workspace/platform/remove.rs index f9c10c8382..a14dbeb88d 100644 --- a/src/cli/workspace/platform/remove.rs +++ b/src/cli/workspace/platform/remove.rs @@ -5,7 +5,7 @@ use rattler_conda_types::Platform; use crate::{ environment::{get_update_lock_file_and_prefix, LockFileUsage}, - lock_file::UpdateMode, + lock_file::{ReinstallPackages, UpdateMode}, UpdateLockFileOptions, Workspace, }; @@ -45,6 +45,7 @@ pub async fn execute(workspace: Workspace, args: Args) -> miette::Result<()> { no_install: args.no_install, max_concurrent_solves: workspace.workspace().config().max_concurrent_solves(), }, + ReinstallPackages::default(), ) .await?; workspace.save().await.into_diagnostic()?; diff --git a/src/environment/conda_prefix.rs b/src/environment/conda_prefix.rs index f66a5c62f2..293980c3fa 100644 --- a/src/environment/conda_prefix.rs +++ b/src/environment/conda_prefix.rs @@ -1,3 +1,4 @@ +use std::collections::HashSet; use std::sync::{Arc, LazyLock}; use crate::build::{BuildContext, SourceCheckoutReporter}; @@ -16,7 +17,7 @@ use pixi_record::PixiRecord; use rattler::install::{DefaultProgressFormatter, IndicatifReporter, Installer}; use rattler::package_cache::PackageCache; use rattler_conda_types::{ - ChannelUrl, GenericVirtualPackage, Platform, PrefixRecord, RepoDataRecord, + ChannelUrl, GenericVirtualPackage, PackageName, Platform, PrefixRecord, RepoDataRecord, }; use reqwest_middleware::ClientWithMiddleware; use tokio::sync::Semaphore; @@ -178,6 +179,7 @@ impl CondaPrefixUpdater { pub async fn update( &self, pixi_records: Vec, + reinstall_packages: Option>, ) -> miette::Result<&CondaPrefixUpdated> { self.inner .created @@ -224,6 +226,7 @@ impl CondaPrefixUpdater { " ", self.inner.io_concurrency_limit.clone().into(), self.inner.build_context.clone(), + reinstall_packages, ) .await?; @@ -256,6 +259,7 @@ pub async fn update_prefix_conda( progress_bar_prefix: &str, io_concurrency_limit: Arc, build_context: BuildContext, + reinstall_packages: Option>, ) -> miette::Result { // Try to increase the rlimit to a sensible value for installation. try_increase_rlimit_to_sensible(); @@ -311,6 +315,11 @@ pub async fn update_prefix_conda( let build_context = &build_context; let channels = &channels; let virtual_packages = &virtual_packages; + let rebuild = reinstall_packages + .as_ref() + .map(|packages| packages.iter().any(|p| p == &record.package_record.name)) + .unwrap_or(false); + async move { build_context .build_source_record( @@ -322,6 +331,7 @@ pub async fn update_prefix_conda( progress_reporter.clone(), Some(source_reporter), build_id, + rebuild, ) .await } @@ -336,7 +346,7 @@ pub async fn update_prefix_conda( let result = await_in_progress( format!("{progress_bar_prefix}{progress_bar_message}",), |pb| async { - Installer::new() + let mut installer = Installer::new() .with_download_client(authenticated_client) .with_io_concurrency_semaphore(io_concurrency_limit) .with_execute_link_scripts(false) @@ -353,7 +363,12 @@ pub async fn update_prefix_conda( ) .clear_when_done(true) .finish(), - ) + ); + if let Some(reinstall_packages) = reinstall_packages { + installer.set_reinstall_packages(reinstall_packages); + } + + installer .install(prefix.root(), repodata_records) .await .into_diagnostic() diff --git a/src/environment/mod.rs b/src/environment/mod.rs index 786b6cd1cc..a78ee270f4 100644 --- a/src/environment/mod.rs +++ b/src/environment/mod.rs @@ -25,7 +25,7 @@ use serde::{Deserialize, Serialize}; use xxhash_rust::xxh3::Xxh3; use crate::{ - lock_file::{LockFileDerivedData, UpdateLockFileOptions, UpdateMode}, + lock_file::{LockFileDerivedData, ReinstallPackages, UpdateLockFileOptions, UpdateMode}, prefix::Prefix, rlimit::try_increase_rlimit_to_sensible, workspace::{grouped_environment::GroupedEnvironment, Environment, HasWorkspaceRef}, @@ -392,6 +392,7 @@ pub async fn get_update_lock_file_and_prefix<'env>( environment: &Environment<'env>, update_mode: UpdateMode, update_lock_file_options: UpdateLockFileOptions, + reinstall_packages: ReinstallPackages, ) -> miette::Result<(LockFileDerivedData<'env>, Prefix)> { let current_platform = environment.best_platform(); let project = environment.workspace(); @@ -423,7 +424,9 @@ pub async fn get_update_lock_file_and_prefix<'env>( let prefix = if no_install { Prefix::new(environment.dir()) } else { - lock_file.prefix(environment, update_mode).await? + lock_file + .prefix(environment, update_mode, reinstall_packages) + .await? }; Ok((lock_file, prefix)) diff --git a/src/lock_file/mod.rs b/src/lock_file/mod.rs index d8f93640c7..78d5feac43 100644 --- a/src/lock_file/mod.rs +++ b/src/lock_file/mod.rs @@ -20,7 +20,7 @@ pub use satisfiability::{ verify_environment_satisfiability, verify_platform_satisfiability, EnvironmentUnsat, PlatformUnsat, }; -pub(crate) use update::{LockFileDerivedData, UpdateContext}; +pub use update::{LockFileDerivedData, ReinstallPackages, UpdateContext}; pub use update::{UpdateLockFileOptions, UpdateMode}; pub(crate) use utils::filter_lock_file; diff --git a/src/lock_file/resolve/build_dispatch.rs b/src/lock_file/resolve/build_dispatch.rs index fe2960d6ab..14317a3a44 100644 --- a/src/lock_file/resolve/build_dispatch.rs +++ b/src/lock_file/resolve/build_dispatch.rs @@ -265,7 +265,7 @@ impl<'a> LazyBuildDispatch<'a> { ); let prefix = self .prefix_updater - .update(self.repodata_records.clone()) + .update(self.repodata_records.clone(), None) .await .map_err(|err| { LazyBuildDispatchError::InitializationError(format!( diff --git a/src/lock_file/resolve/uv_resolution_context.rs b/src/lock_file/resolve/uv_resolution_context.rs index eb88a87182..48afa492f7 100644 --- a/src/lock_file/resolve/uv_resolution_context.rs +++ b/src/lock_file/resolve/uv_resolution_context.rs @@ -75,4 +75,15 @@ impl UvResolutionContext { shared_state: SharedState::default(), }) } + + /// Set the cache refresh strategy. + pub fn set_cache_refresh( + mut self, + all: Option, + specific_packages: Option>, + ) -> Self { + let policy = uv_cache::Refresh::from_args(all, specific_packages.unwrap_or_default()); + self.cache = self.cache.with_refresh(policy); + self + } } diff --git a/src/lock_file/update.rs b/src/lock_file/update.rs index 710090778a..29dfe93cfe 100644 --- a/src/lock_file/update.rs +++ b/src/lock_file/update.rs @@ -4,6 +4,7 @@ use std::{ future::{ready, Future}, iter, path::PathBuf, + str::FromStr, sync::Arc, time::{Duration, Instant}, }; @@ -31,7 +32,7 @@ use pixi_uv_conversions::{ use pypi_mapping::{self}; use pypi_modifiers::pypi_marker_env::determine_marker_environment; use rattler::package_cache::PackageCache; -use rattler_conda_types::{Arch, MatchSpec, ParseStrictness, Platform}; +use rattler_conda_types::{Arch, MatchSpec, PackageName, ParseStrictness, Platform}; use rattler_lock::{ LockFile, ParseCondaLockError, PypiIndexes, PypiPackageData, PypiPackageEnvironmentData, }; @@ -212,6 +213,14 @@ pub struct UpdateLockFileOptions { pub max_concurrent_solves: usize, } +#[derive(Debug, Clone, Default)] +pub enum ReinstallPackages { + #[default] + None, + All, + Some(HashSet), +} + /// A struct that holds the lock-file and any potential derived data that was /// computed when calling `update_lock_file`. pub struct LockFileDerivedData<'p> { @@ -286,6 +295,7 @@ impl<'p> LockFileDerivedData<'p> { &mut self, environment: &Environment<'p>, update_mode: UpdateMode, + reinstall_packages: ReinstallPackages, ) -> miette::Result { // Check if the prefix is already up-to-date by validating the hash with the // environment file @@ -297,7 +307,7 @@ impl<'p> LockFileDerivedData<'p> { } // Get the up-to-date prefix - let prefix = self.update_prefix(environment).await?; + let prefix = self.update_prefix(environment, reinstall_packages).await?; // Save an environment file to the environment directory after the update. // Avoiding writing the cache away before the update is done. @@ -361,7 +371,11 @@ impl<'p> LockFileDerivedData<'p> { } /// Returns the up-to-date prefix for the given environment. - async fn update_prefix(&mut self, environment: &Environment<'p>) -> miette::Result { + async fn update_prefix( + &mut self, + environment: &Environment<'p>, + reinstall_packages: ReinstallPackages, + ) -> miette::Result { // If we previously updated this environment, early out. if let Some(prefix) = self.updated_pypi_prefixes.get(environment.name()) { return Ok(prefix.clone()); @@ -380,13 +394,29 @@ impl<'p> LockFileDerivedData<'p> { ))?; tracing::info!("Updating prefix: '{}'", environment.dir().display()); - // Get the prefix with the conda packages installed. + let platform = environment.best_platform(); - let (prefix, python_status) = self.conda_prefix(environment).await?; let pixi_records = self .pixi_records(environment, platform) .into_diagnostic()? .unwrap_or_default(); + + let conda_reinstall_packages = match reinstall_packages { + ReinstallPackages::None => None, + ReinstallPackages::Some(ref p) => Some( + p.iter() + .filter_map(|p| PackageName::from_str(p).ok()) + .filter(|name| pixi_records.iter().any(|r| r.name() == name)) + .collect(), + ), + ReinstallPackages::All => Some(pixi_records.iter().map(|r| r.name().clone()).collect()), + }; + + // Get the prefix with the conda packages installed. + let (prefix, python_status) = self + .conda_prefix(environment, conda_reinstall_packages) + .await?; + let pypi_records = self .pypi_records(environment, platform) .into_diagnostic()? @@ -397,9 +427,30 @@ impl<'p> LockFileDerivedData<'p> { return Ok(prefix); } + let pypi_lock_file_names = pypi_records + .iter() + .filter_map(|(data, _)| to_uv_normalize(&data.name).ok()) + .collect::>(); + + // Figure out uv reinstall + let (uv_reinstall, uv_packages) = match reinstall_packages { + ReinstallPackages::None => (Some(false), None), + ReinstallPackages::All => (Some(true), None), + ReinstallPackages::Some(pkgs) => ( + None, + Some( + pkgs.into_iter() + .filter_map(|pkg| uv_pep508::PackageName::new(pkg).ok()) + .filter(|name| pypi_lock_file_names.contains(name)) + .collect(), + ), + ), + }; + let uv_context = match &self.uv_context { None => { - let context = UvResolutionContext::from_workspace(self.workspace)?; + let context = UvResolutionContext::from_workspace(self.workspace)? + .set_cache_refresh(uv_reinstall, uv_packages); self.uv_context = Some(context.clone()); context } @@ -507,6 +558,7 @@ impl<'p> LockFileDerivedData<'p> { async fn conda_prefix( &mut self, environment: &Environment<'p>, + reinstall_packages: Option>, ) -> miette::Result<(Prefix, PythonStatus)> { // If we previously updated this environment, early out. if let Some((prefix, python_status)) = self.updated_conda_prefixes.get(environment.name()) { @@ -536,7 +588,9 @@ impl<'p> LockFileDerivedData<'p> { prefix, python_status, .. - } = conda_prefix_updater.update(records).await?; + } = conda_prefix_updater + .update(records, reinstall_packages) + .await?; // Store that we updated the environment, so we won't have to do it again. self.updated_conda_prefixes.insert( diff --git a/src/workspace/workspace_mut.rs b/src/workspace/workspace_mut.rs index ef258aead2..84e75107c3 100644 --- a/src/workspace/workspace_mut.rs +++ b/src/workspace/workspace_mut.rs @@ -25,7 +25,7 @@ use crate::{ cli::cli_config::{LockFileUpdateConfig, PrefixUpdateConfig}, diff::LockFileDiff, environment::LockFileUsage, - lock_file::{LockFileDerivedData, UpdateContext, UpdateMode}, + lock_file::{LockFileDerivedData, ReinstallPackages, UpdateContext, UpdateMode}, workspace::{ grouped_environment::GroupedEnvironment, MatchSpecs, PypiDeps, SourceSpecs, UpdateDeps, NON_SEMVER_PACKAGES, @@ -422,6 +422,7 @@ impl WorkspaceMut { .prefix( &self.workspace().default_environment(), UpdateMode::Revalidate, + ReinstallPackages::default(), ) .await?; } diff --git a/tests/data/mock-projects/test-rebuild/.gitattributes b/tests/data/mock-projects/test-rebuild/.gitattributes new file mode 100644 index 0000000000..887a2c18f0 --- /dev/null +++ b/tests/data/mock-projects/test-rebuild/.gitattributes @@ -0,0 +1,2 @@ +# SCM syntax highlighting & preventing 3-way merges +pixi.lock merge=binary linguist-language=YAML linguist-generated=true diff --git a/tests/data/mock-projects/test-rebuild/.gitignore b/tests/data/mock-projects/test-rebuild/.gitignore new file mode 100644 index 0000000000..740bb7d1ae --- /dev/null +++ b/tests/data/mock-projects/test-rebuild/.gitignore @@ -0,0 +1,4 @@ + +# pixi environments +.pixi +*.egg-info diff --git a/tests/data/mock-projects/test-rebuild/pixi.lock b/tests/data/mock-projects/test-rebuild/pixi.lock new file mode 100644 index 0000000000..f44ceb8eea --- /dev/null +++ b/tests/data/mock-projects/test-rebuild/pixi.lock @@ -0,0 +1,291 @@ +version: 6 +environments: + default: + channels: + - url: https://prefix.dev/conda-forge/ + indexes: + - https://pypi.org/simple + packages: + linux-64: + - conda: https://prefix.dev/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 + - conda: https://prefix.dev/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 + - conda: https://prefix.dev/conda-forge/linux-64/bzip2-1.0.8-h4bc722e_7.conda + - conda: https://prefix.dev/conda-forge/linux-64/ca-certificates-2025.1.31-hbcca054_0.conda + - conda: https://prefix.dev/conda-forge/linux-64/ld_impl_linux-64-2.43-h712a8e2_4.conda + - conda: https://prefix.dev/conda-forge/linux-64/libexpat-2.6.4-h5888daf_0.conda + - conda: https://prefix.dev/conda-forge/linux-64/libffi-3.4.6-h2dba641_0.conda + - conda: https://prefix.dev/conda-forge/linux-64/libgcc-14.2.0-h767d61c_2.conda + - conda: https://prefix.dev/conda-forge/linux-64/libgcc-ng-14.2.0-h69a702a_2.conda + - conda: https://prefix.dev/conda-forge/linux-64/libgomp-14.2.0-h767d61c_2.conda + - conda: https://prefix.dev/conda-forge/linux-64/liblzma-5.6.4-hb9d3cd8_0.conda + - conda: https://prefix.dev/conda-forge/linux-64/libmpdec-4.0.0-h4bc722e_0.conda + - conda: https://prefix.dev/conda-forge/linux-64/libsqlite-3.49.1-hee588c1_1.conda + - conda: https://prefix.dev/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda + - conda: https://prefix.dev/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda + - conda: https://prefix.dev/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda + - conda: https://prefix.dev/conda-forge/linux-64/openssl-3.4.1-h7b32b05_0.conda + - conda: https://prefix.dev/conda-forge/linux-64/python-3.13.2-hf636f53_101_cp313.conda + - conda: https://prefix.dev/conda-forge/linux-64/python_abi-3.13-5_cp313.conda + - conda: https://prefix.dev/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda + - conda: https://prefix.dev/conda-forge/linux-64/tk-8.6.13-noxft_h4845f30_101.conda + - conda: https://prefix.dev/conda-forge/noarch/tzdata-2025a-h78e105d_0.conda + - pypi: . +packages: +- conda: https://prefix.dev/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 + sha256: fe51de6107f9edc7aa4f786a70f4a883943bc9d39b3bb7307c04c41410990726 + md5: d7c89558ba9fa0495403155b64376d81 + license: None + purls: [] + size: 2562 + timestamp: 1578324546067 +- conda: https://prefix.dev/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 + build_number: 16 + sha256: fbe2c5e56a653bebb982eda4876a9178aedfc2b545f25d0ce9c4c0b508253d22 + md5: 73aaf86a425cc6e73fcf236a5a46396d + depends: + - _libgcc_mutex 0.1 conda_forge + - libgomp >=7.5.0 + constrains: + - openmp_impl 9999 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 23621 + timestamp: 1650670423406 +- conda: https://prefix.dev/conda-forge/linux-64/bzip2-1.0.8-h4bc722e_7.conda + sha256: 5ced96500d945fb286c9c838e54fa759aa04a7129c59800f0846b4335cee770d + md5: 62ee74e96c5ebb0af99386de58cf9553 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc-ng >=12 + license: bzip2-1.0.6 + license_family: BSD + purls: [] + size: 252783 + timestamp: 1720974456583 +- conda: https://prefix.dev/conda-forge/linux-64/ca-certificates-2025.1.31-hbcca054_0.conda + sha256: bf832198976d559ab44d6cdb315642655547e26d826e34da67cbee6624cda189 + md5: 19f3a56f68d2fd06c516076bff482c52 + license: ISC + purls: [] + size: 158144 + timestamp: 1738298224464 +- conda: https://prefix.dev/conda-forge/linux-64/ld_impl_linux-64-2.43-h712a8e2_4.conda + sha256: db73f38155d901a610b2320525b9dd3b31e4949215c870685fd92ea61b5ce472 + md5: 01f8d123c96816249efd255a31ad7712 + depends: + - __glibc >=2.17,<3.0.a0 + constrains: + - binutils_impl_linux-64 2.43 + license: GPL-3.0-only + license_family: GPL + purls: [] + size: 671240 + timestamp: 1740155456116 +- conda: https://prefix.dev/conda-forge/linux-64/libexpat-2.6.4-h5888daf_0.conda + sha256: 56541b98447b58e52d824bd59d6382d609e11de1f8adf20b23143e353d2b8d26 + md5: db833e03127376d461e1e13e76f09b6c + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + constrains: + - expat 2.6.4.* + license: MIT + license_family: MIT + purls: [] + size: 73304 + timestamp: 1730967041968 +- conda: https://prefix.dev/conda-forge/linux-64/libffi-3.4.6-h2dba641_0.conda + sha256: 67a6c95e33ebc763c1adc3455b9a9ecde901850eb2fceb8e646cc05ef3a663da + md5: e3eb7806380bc8bcecba6d749ad5f026 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + license: MIT + license_family: MIT + purls: [] + size: 53415 + timestamp: 1739260413716 +- conda: https://prefix.dev/conda-forge/linux-64/libgcc-14.2.0-h767d61c_2.conda + sha256: 3a572d031cb86deb541d15c1875aaa097baefc0c580b54dc61f5edab99215792 + md5: ef504d1acbd74b7cc6849ef8af47dd03 + depends: + - __glibc >=2.17,<3.0.a0 + - _openmp_mutex >=4.5 + constrains: + - libgomp 14.2.0 h767d61c_2 + - libgcc-ng ==14.2.0=*_2 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 847885 + timestamp: 1740240653082 +- conda: https://prefix.dev/conda-forge/linux-64/libgcc-ng-14.2.0-h69a702a_2.conda + sha256: fb7558c328b38b2f9d2e412c48da7890e7721ba018d733ebdfea57280df01904 + md5: a2222a6ada71fb478682efe483ce0f92 + depends: + - libgcc 14.2.0 h767d61c_2 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 53758 + timestamp: 1740240660904 +- conda: https://prefix.dev/conda-forge/linux-64/libgomp-14.2.0-h767d61c_2.conda + sha256: 1a3130e0b9267e781b89399580f3163632d59fe5b0142900d63052ab1a53490e + md5: 06d02030237f4d5b3d9a7e7d348fe3c6 + depends: + - __glibc >=2.17,<3.0.a0 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 459862 + timestamp: 1740240588123 +- conda: https://prefix.dev/conda-forge/linux-64/liblzma-5.6.4-hb9d3cd8_0.conda + sha256: cad52e10319ca4585bc37f0bc7cce99ec7c15dc9168e42ccb96b741b0a27db3f + md5: 42d5b6a0f30d3c10cd88cb8584fda1cb + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + license: 0BSD + purls: [] + size: 111357 + timestamp: 1738525339684 +- conda: https://prefix.dev/conda-forge/linux-64/libmpdec-4.0.0-h4bc722e_0.conda + sha256: d02d1d3304ecaf5c728e515eb7416517a0b118200cd5eacbe829c432d1664070 + md5: aeb98fdeb2e8f25d43ef71fbacbeec80 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc-ng >=12 + license: BSD-2-Clause + license_family: BSD + purls: [] + size: 89991 + timestamp: 1723817448345 +- conda: https://prefix.dev/conda-forge/linux-64/libsqlite-3.49.1-hee588c1_1.conda + sha256: 7a09eef804ef7cf4d88215c2297eabb72af8ad0bd5b012060111c289f14bbe7d + md5: 73cea06049cc4174578b432320a003b8 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - libzlib >=1.3.1,<2.0a0 + license: Unlicense + purls: [] + size: 915956 + timestamp: 1739953155793 +- conda: https://prefix.dev/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda + sha256: 787eb542f055a2b3de553614b25f09eefb0a0931b0c87dbcce6efdfd92f04f18 + md5: 40b61aab5c7ba9ff276c41cfffe6b80b + depends: + - libgcc-ng >=12 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 33601 + timestamp: 1680112270483 +- conda: https://prefix.dev/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda + sha256: d4bfe88d7cb447768e31650f06257995601f89076080e76df55e3112d4e47dc4 + md5: edb0dca6bc32e4f4789199455a1dbeb8 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + constrains: + - zlib 1.3.1 *_2 + license: Zlib + license_family: Other + purls: [] + size: 60963 + timestamp: 1727963148474 +- conda: https://prefix.dev/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda + sha256: 3fde293232fa3fca98635e1167de6b7c7fda83caf24b9d6c91ec9eefb4f4d586 + md5: 47e340acb35de30501a76c7c799c41d7 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + license: X11 AND BSD-3-Clause + purls: [] + size: 891641 + timestamp: 1738195959188 +- conda: https://prefix.dev/conda-forge/linux-64/openssl-3.4.1-h7b32b05_0.conda + sha256: cbf62df3c79a5c2d113247ddea5658e9ff3697b6e741c210656e239ecaf1768f + md5: 41adf927e746dc75ecf0ef841c454e48 + depends: + - __glibc >=2.17,<3.0.a0 + - ca-certificates + - libgcc >=13 + license: Apache-2.0 + license_family: Apache + purls: [] + size: 2939306 + timestamp: 1739301879343 +- pypi: . + name: pypi + version: 0.1.0 + sha256: ae72568434c2de5207d8d1394a4934a67a6fa897172a1036308bf5366d39c144 + requires_python: '>=3.11' +- conda: https://prefix.dev/conda-forge/linux-64/python-3.13.2-hf636f53_101_cp313.conda + build_number: 101 + sha256: cc1984ee54261cee6a2db75c65fc7d2967bc8c6e912d332614df15244d7730ef + md5: a7902a3611fe773da3921cbbf7bc2c5c + depends: + - __glibc >=2.17,<3.0.a0 + - bzip2 >=1.0.8,<2.0a0 + - ld_impl_linux-64 >=2.36.1 + - libexpat >=2.6.4,<3.0a0 + - libffi >=3.4,<4.0a0 + - libgcc >=13 + - liblzma >=5.6.4,<6.0a0 + - libmpdec >=4.0.0,<5.0a0 + - libsqlite >=3.48.0,<4.0a0 + - libuuid >=2.38.1,<3.0a0 + - libzlib >=1.3.1,<2.0a0 + - ncurses >=6.5,<7.0a0 + - openssl >=3.4.1,<4.0a0 + - python_abi 3.13.* *_cp313 + - readline >=8.2,<9.0a0 + - tk >=8.6.13,<8.7.0a0 + - tzdata + license: Python-2.0 + purls: [] + size: 33233150 + timestamp: 1739803603242 + python_site_packages_path: lib/python3.13/site-packages +- conda: https://prefix.dev/conda-forge/linux-64/python_abi-3.13-5_cp313.conda + build_number: 5 + sha256: 438225b241c5f9bddae6f0178a97f5870a89ecf927dfca54753e689907331442 + md5: 381bbd2a92c863f640a55b6ff3c35161 + constrains: + - python 3.13.* *_cp313 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 6217 + timestamp: 1723823393322 +- conda: https://prefix.dev/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda + sha256: 2d6d0c026902561ed77cd646b5021aef2d4db22e57a5b0178dfc669231e06d2c + md5: 283b96675859b20a825f8fa30f311446 + depends: + - libgcc >=13 + - ncurses >=6.5,<7.0a0 + license: GPL-3.0-only + license_family: GPL + purls: [] + size: 282480 + timestamp: 1740379431762 +- conda: https://prefix.dev/conda-forge/linux-64/tk-8.6.13-noxft_h4845f30_101.conda + sha256: e0569c9caa68bf476bead1bed3d79650bb080b532c64a4af7d8ca286c08dea4e + md5: d453b98d9c83e71da0741bb0ff4d76bc + depends: + - libgcc-ng >=12 + - libzlib >=1.2.13,<2.0.0a0 + license: TCL + license_family: BSD + purls: [] + size: 3318875 + timestamp: 1699202167581 +- conda: https://prefix.dev/conda-forge/noarch/tzdata-2025a-h78e105d_0.conda + sha256: c4b1ae8a2931fe9b274c44af29c5475a85b37693999f8c792dad0f8c6734b1de + md5: dbcace4706afdfb7eb891f7b37d07c04 + license: LicenseRef-Public-Domain + purls: [] + size: 122921 + timestamp: 1737119101255 diff --git a/tests/data/mock-projects/test-rebuild/pixi.toml b/tests/data/mock-projects/test-rebuild/pixi.toml new file mode 100644 index 0000000000..a36ca54be0 --- /dev/null +++ b/tests/data/mock-projects/test-rebuild/pixi.toml @@ -0,0 +1,26 @@ +[workspace] +authors = ["Julian Hofer "] +channels = ["https://prefix.dev/conda-forge"] +name = "test-rebuild" +platforms = ["linux-64", "osx-arm64", "win-64"] +preview = ["pixi-build"] +version = "0.1.0" + +[tasks] + +[dependencies] +pixi_build_package = { path = "pixi_build_package" } +python = ">=3.13.2,<3.14" + +[pypi-dependencies] +pypi_package = { path = "pypi_package" } + +[feature.dev.dependencies] +pixi_build_package_dev = { path = "pixi_build_package_dev" } +python = ">=3.13.2,<3.14" + +[feature.dev.pypi-dependencies] +pypi_package_dev = { path = "pypi_package_dev" } + +[environments] +dev = { features = ["dev"], no-default-feature = true } diff --git a/tests/data/mock-projects/test-rebuild/pixi_build_package/pixi.toml b/tests/data/mock-projects/test-rebuild/pixi_build_package/pixi.toml new file mode 100644 index 0000000000..21583b94f8 --- /dev/null +++ b/tests/data/mock-projects/test-rebuild/pixi_build_package/pixi.toml @@ -0,0 +1,10 @@ +[package] +name = "pixi_build_package" +version = "0.1.0" + +[package.build] +backend = { name = "pixi-build-rattler-build", version = "0.1.*" } +channels = [ + "https://prefix.dev/pixi-build-backends", + "https://prefix.dev/conda-forge", +] diff --git a/tests/data/mock-projects/test-rebuild/pixi_build_package/pyproject.toml b/tests/data/mock-projects/test-rebuild/pixi_build_package/pyproject.toml new file mode 100644 index 0000000000..3250ee0dca --- /dev/null +++ b/tests/data/mock-projects/test-rebuild/pixi_build_package/pyproject.toml @@ -0,0 +1,11 @@ +[project] +authors = [{ name = "Julian Hofer", email = "julianhofer@gnome.org" }] +dependencies = [] +name = "pixi_build_package" +requires-python = ">= 3.11" +scripts = { pixi-build-package-main = "pixi_build_package:main" } +version = "0.1.0" + +[build-system] +build-backend = "hatchling.build" +requires = ["hatchling"] diff --git a/tests/data/mock-projects/test-rebuild/pixi_build_package/recipe.yaml b/tests/data/mock-projects/test-rebuild/pixi_build_package/recipe.yaml new file mode 100644 index 0000000000..3aee311c11 --- /dev/null +++ b/tests/data/mock-projects/test-rebuild/pixi_build_package/recipe.yaml @@ -0,0 +1,18 @@ +package: + name: pixi_build_package + version: 0.1.0 + +source: + path: . + use_gitignore: true + +build: + noarch: python + number: 0 + script: | + pip install . --no-deps -vv +requirements: + host: + - pip + - python + - hatchling diff --git a/tests/data/mock-projects/test-rebuild/pixi_build_package/src/pixi_build_package/__init__.py b/tests/data/mock-projects/test-rebuild/pixi_build_package/src/pixi_build_package/__init__.py new file mode 100644 index 0000000000..7157d8a41a --- /dev/null +++ b/tests/data/mock-projects/test-rebuild/pixi_build_package/src/pixi_build_package/__init__.py @@ -0,0 +1,2 @@ +def main() -> None: + print("Pixi Build is number 1") diff --git a/tests/data/mock-projects/test-rebuild/pixi_build_package_dev/pixi.toml b/tests/data/mock-projects/test-rebuild/pixi_build_package_dev/pixi.toml new file mode 100644 index 0000000000..07f1a034a4 --- /dev/null +++ b/tests/data/mock-projects/test-rebuild/pixi_build_package_dev/pixi.toml @@ -0,0 +1,10 @@ +[package] +name = "pixi_build_package_dev" +version = "0.1.0" + +[package.build] +backend = { name = "pixi-build-rattler-build", version = "0.1.*" } +channels = [ + "https://prefix.dev/pixi-build-backends", + "https://prefix.dev/conda-forge", +] diff --git a/tests/data/mock-projects/test-rebuild/pixi_build_package_dev/pyproject.toml b/tests/data/mock-projects/test-rebuild/pixi_build_package_dev/pyproject.toml new file mode 100644 index 0000000000..f1e58bb856 --- /dev/null +++ b/tests/data/mock-projects/test-rebuild/pixi_build_package_dev/pyproject.toml @@ -0,0 +1,11 @@ +[project] +authors = [{ name = "Julian Hofer", email = "julianhofer@gnome.org" }] +dependencies = [] +name = "pixi_build_package_dev" +requires-python = ">= 3.11" +scripts = { pixi-build-package-dev-main = "pixi_build_package_dev:main" } +version = "0.1.0" + +[build-system] +build-backend = "hatchling.build" +requires = ["hatchling"] diff --git a/tests/data/mock-projects/test-rebuild/pixi_build_package_dev/recipe.yaml b/tests/data/mock-projects/test-rebuild/pixi_build_package_dev/recipe.yaml new file mode 100644 index 0000000000..094da528f7 --- /dev/null +++ b/tests/data/mock-projects/test-rebuild/pixi_build_package_dev/recipe.yaml @@ -0,0 +1,18 @@ +package: + name: pixi_build_package_dev + version: 0.1.0 + +source: + path: . + use_gitignore: true + +build: + noarch: python + number: 0 + script: | + pip install . --no-deps -vv +requirements: + host: + - pip + - python + - hatchling diff --git a/tests/data/mock-projects/test-rebuild/pixi_build_package_dev/src/pixi_build_package_dev/__init__.py b/tests/data/mock-projects/test-rebuild/pixi_build_package_dev/src/pixi_build_package_dev/__init__.py new file mode 100644 index 0000000000..66e8814e70 --- /dev/null +++ b/tests/data/mock-projects/test-rebuild/pixi_build_package_dev/src/pixi_build_package_dev/__init__.py @@ -0,0 +1,2 @@ +def main() -> None: + print("Pixi Build dev is number 1") diff --git a/tests/data/mock-projects/test-rebuild/pypi_package/pyproject.toml b/tests/data/mock-projects/test-rebuild/pypi_package/pyproject.toml new file mode 100644 index 0000000000..ea25b4570c --- /dev/null +++ b/tests/data/mock-projects/test-rebuild/pypi_package/pyproject.toml @@ -0,0 +1,11 @@ +[project] +authors = [{ name = "Julian Hofer", email = "julianhofer@gnome.org" }] +dependencies = [] +name = "pypi_package" +requires-python = ">= 3.11" +scripts = { pypi-package-main = "pypi_package:main" } +version = "0.1.0" + +[build-system] +build-backend = "hatchling.build" +requires = ["hatchling"] diff --git a/tests/data/mock-projects/test-rebuild/pypi_package/src/pypi_package/__init__.py b/tests/data/mock-projects/test-rebuild/pypi_package/src/pypi_package/__init__.py new file mode 100644 index 0000000000..a26a635c7b --- /dev/null +++ b/tests/data/mock-projects/test-rebuild/pypi_package/src/pypi_package/__init__.py @@ -0,0 +1,2 @@ +def main() -> None: + print("PyPI is number 1") diff --git a/tests/data/mock-projects/test-rebuild/pypi_package_dev/pyproject.toml b/tests/data/mock-projects/test-rebuild/pypi_package_dev/pyproject.toml new file mode 100644 index 0000000000..8ebbb4f304 --- /dev/null +++ b/tests/data/mock-projects/test-rebuild/pypi_package_dev/pyproject.toml @@ -0,0 +1,11 @@ +[project] +authors = [{ name = "Julian Hofer", email = "julianhofer@gnome.org" }] +dependencies = [] +name = "pypi_package_dev" +requires-python = ">= 3.11" +scripts = { pypi-package-dev-main = "pypi_package_dev:main" } +version = "0.1.0" + +[build-system] +build-backend = "hatchling.build" +requires = ["hatchling"] diff --git a/tests/data/mock-projects/test-rebuild/pypi_package_dev/src/pypi_package_dev/__init__.py b/tests/data/mock-projects/test-rebuild/pypi_package_dev/src/pypi_package_dev/__init__.py new file mode 100644 index 0000000000..88fb4d118d --- /dev/null +++ b/tests/data/mock-projects/test-rebuild/pypi_package_dev/src/pypi_package_dev/__init__.py @@ -0,0 +1,2 @@ +def main() -> None: + print("PyPI dev is number 1") diff --git a/tests/integration_python/conftest.py b/tests/integration_python/conftest.py index 7420862977..5db4d48ee4 100644 --- a/tests/integration_python/conftest.py +++ b/tests/integration_python/conftest.py @@ -49,6 +49,11 @@ def test_data() -> Path: return Path(__file__).parents[1].joinpath("data").resolve() +@pytest.fixture +def mock_projects(test_data: Path) -> Path: + return test_data.joinpath("mock-projects") + + @pytest.fixture def channels(test_data: Path) -> Path: return test_data.joinpath("channels", "channels") diff --git a/tests/integration_python/test_reinstall.py b/tests/integration_python/test_reinstall.py new file mode 100644 index 0000000000..8abea0c3c9 --- /dev/null +++ b/tests/integration_python/test_reinstall.py @@ -0,0 +1,312 @@ +from .common import verify_cli_command +import pytest +from pathlib import Path +import shutil +import tomllib +import tomli_w + + +@pytest.fixture +def reinstall_workspace(tmp_pixi_workspace: Path, mock_projects: Path) -> Path: + test_rebuild_src = mock_projects / "test-rebuild" + shutil.rmtree(test_rebuild_src.joinpath(".pixi"), ignore_errors=True) + shutil.copytree(test_rebuild_src, tmp_pixi_workspace, dirs_exist_ok=True) + + # Enable debug logging + packages = ["pixi_build_package", "pixi_build_package_dev"] + for package in packages: + package_dir = tmp_pixi_workspace / package + package_manifest = package_dir / "pixi.toml" + manifest_dict = tomllib.loads(package_manifest.read_text()) + manifest_dict["package"]["build"]["configuration"] = {"debug-dir": str(package_dir)} + package_manifest.write_text(tomli_w.dumps(manifest_dict)) + + return tmp_pixi_workspace + + +@pytest.mark.slow +def test_pixi_reinstall_default_env(pixi: Path, reinstall_workspace: Path) -> None: + env = { + "PIXI_CACHE_DIR": str(reinstall_workspace.joinpath("pixi_cache")), + } + manifest = reinstall_workspace.joinpath("pixi.toml") + conda_metadata_params = reinstall_workspace.joinpath( + "pixi_build_package", "conda_metadata_params.json" + ) + conda_build_params = reinstall_workspace.joinpath( + "pixi_build_package", "conda_build_params.json" + ) + + # Check that packages return "number 1" + verify_cli_command( + [pixi, "run", "--manifest-path", manifest, "pypi-package-main"], + stdout_contains="PyPI is number 1", + env=env, + ) + verify_cli_command( + [pixi, "run", "--manifest-path", manifest, "pixi-build-package-main"], + stdout_contains="Pixi Build is number 1", + env=env, + ) + + # In order to build pixi-build-package-main, getMetadata and build has been called + assert conda_metadata_params.is_file() + assert conda_build_params.is_file() + + # Delete the files to get a clean state + conda_metadata_params.unlink() + conda_build_params.unlink() + + # Modify the Python files + for package in ["pypi_package", "pixi_build_package"]: + init_py = reinstall_workspace.joinpath(package, "src", package, "__init__.py") + init_py.write_text(init_py.read_text().replace("1", "2")) + + # That shouldn't trigger a re-install, so running still returns "number 1" + verify_cli_command( + [pixi, "run", "--manifest-path", manifest, "pypi-package-main"], + stdout_contains="PyPI is number 1", + env=env, + ) + verify_cli_command( + [pixi, "run", "--manifest-path", manifest, "pixi-build-package-main"], + stdout_contains="Pixi Build is number 1", + env=env, + ) + + # Everything pixi-build related is cached, no remote procedure was called + assert not conda_metadata_params.is_file() + assert not conda_build_params.is_file() + + # After re-installing pypi-package, it should return "number 2" + # pixi-build-package, should not be rebuild and therefore still return "number 1" + verify_cli_command([pixi, "reinstall", "--manifest-path", manifest, "pypi_package"], env=env) + assert not conda_metadata_params.is_file() + assert not conda_build_params.is_file() + verify_cli_command( + [pixi, "run", "--manifest-path", manifest, "pypi-package-main"], + stdout_contains="PyPI is number 2", + env=env, + ) + verify_cli_command( + [pixi, "run", "--manifest-path", manifest, "pixi-build-package-main"], + stdout_contains="Pixi Build is number 1", + env=env, + ) + + # After re-installing the whole default environment, + # both should return "number 2" + verify_cli_command([pixi, "reinstall", "--manifest-path", manifest], env=env) + assert not conda_metadata_params.is_file() + assert conda_build_params.is_file() + verify_cli_command( + [pixi, "run", "--manifest-path", manifest, "pypi-package-main"], + stdout_contains="PyPI is number 2", + env=env, + ) + verify_cli_command( + [pixi, "run", "--manifest-path", manifest, "pixi-build-package-main"], + stdout_contains="Pixi Build is number 2", + env=env, + ) + + +@pytest.mark.slow +def test_pixi_reinstall_multi_env(pixi: Path, reinstall_workspace: Path) -> None: + env = { + "PIXI_CACHE_DIR": str(reinstall_workspace.joinpath("pixi_cache")), + } + manifest = reinstall_workspace.joinpath("pixi.toml") + conda_metadata_params = reinstall_workspace.joinpath( + "pixi_build_package", "conda_metadata_params.json" + ) + conda_build_params = reinstall_workspace.joinpath( + "pixi_build_package", "conda_build_params.json" + ) + conda_metadata_params_dev = reinstall_workspace.joinpath( + "pixi_build_package_dev", "conda_metadata_params.json" + ) + conda_build_params_dev = reinstall_workspace.joinpath( + "pixi_build_package_dev", "conda_build_params.json" + ) + + # Check that packages return "number 1" in default environment + verify_cli_command( + [pixi, "run", "--manifest-path", manifest, "pypi-package-main"], + stdout_contains="PyPI is number 1", + env=env, + ) + verify_cli_command( + [ + pixi, + "run", + "--manifest-path", + manifest, + "pixi-build-package-main", + ], + stdout_contains="Pixi Build is number 1", + env=env, + ) + + # Check that packages return "number 1" in dev environment + verify_cli_command( + [pixi, "run", "--manifest-path", manifest, "--environment", "dev", "pypi-package-dev-main"], + stdout_contains="PyPI dev is number 1", + env=env, + ) + verify_cli_command( + [ + pixi, + "run", + "--manifest-path", + manifest, + "--environment", + "dev", + "pixi-build-package-dev-main", + ], + stdout_contains="Pixi Build dev is number 1", + env=env, + ) + + # In order to build pixi-build-package-main, getMetadata and build has been called + assert conda_metadata_params_dev.is_file() + assert conda_build_params_dev.is_file() + + # Delete the files to get a clean state + conda_metadata_params_dev.unlink() + conda_build_params_dev.unlink() + + # Modify the Python files + for package in [ + "pypi_package", + "pixi_build_package", + "pypi_package_dev", + "pixi_build_package_dev", + ]: + init_py = reinstall_workspace.joinpath(package, "src", package, "__init__.py") + init_py.write_text(init_py.read_text().replace("1", "2")) + + # That shouldn't trigger a re-install, so running still returns "number 1" + verify_cli_command( + [pixi, "run", "--manifest-path", manifest, "--environment", "dev", "pypi-package-dev-main"], + stdout_contains="PyPI dev is number 1", + env=env, + ) + verify_cli_command( + [ + pixi, + "run", + "--manifest-path", + manifest, + "--environment", + "dev", + "pixi-build-package-dev-main", + ], + stdout_contains="Pixi Build dev is number 1", + env=env, + ) + + # Everything pixi-build related is cached, no remote procedure was called + assert not conda_metadata_params_dev.is_file() + assert not conda_build_params_dev.is_file() + + # After re-building pixi_build_package_dev, it should return "number 2" + # pypi_package_dev, should still return "number 1" + verify_cli_command( + [ + pixi, + "reinstall", + "--manifest-path", + manifest, + "--environment", + "dev", + "pixi_build_package_dev", + ], + env=env, + ) + assert not conda_metadata_params_dev.is_file() + assert conda_build_params_dev.is_file() + verify_cli_command( + [ + pixi, + "run", + "--manifest-path", + manifest, + "--environment", + "dev", + "pixi-build-package-dev-main", + ], + stdout_contains="Pixi Build dev is number 2", + env=env, + ) + verify_cli_command( + [pixi, "run", "--manifest-path", manifest, "--environment", "dev", "pypi-package-dev-main"], + stdout_contains="PyPI dev is number 1", + env=env, + ) + + # After re-installing both packages in the "dev" environment, + # both should return "number 2" + verify_cli_command( + [ + pixi, + "reinstall", + "--manifest-path", + manifest, + "--environment", + "dev", + "pypi_package_dev", + "pixi_build_package_dev", + ], + env=env, + ) + verify_cli_command( + [pixi, "run", "--manifest-path", manifest, "--environment", "dev", "pypi-package-dev-main"], + stdout_contains="PyPI dev is number 2", + env=env, + ) + verify_cli_command( + [ + pixi, + "run", + "--manifest-path", + manifest, + "--environment", + "dev", + "pixi-build-package-dev-main", + ], + stdout_contains="Pixi Build dev is number 2", + env=env, + ) + + # In the default environment, it should still be "number 1" + verify_cli_command( + [pixi, "run", "--manifest-path", manifest, "pypi-package-main"], + stdout_contains="PyPI is number 1", + env=env, + ) + verify_cli_command( + [pixi, "run", "--manifest-path", manifest, "pixi-build-package-main"], + stdout_contains="Pixi Build is number 1", + env=env, + ) + + # Delete the files to get a clean state + conda_metadata_params.unlink() + conda_build_params.unlink() + + # After reinstalling all environments, + # also the default environment should be "number 2" + verify_cli_command([pixi, "reinstall", "--manifest-path", manifest, "--all"], env=env) + assert not conda_metadata_params.is_file() + assert conda_build_params.is_file() + verify_cli_command( + [pixi, "run", "--manifest-path", manifest, "pypi-package-main"], + stdout_contains="PyPI is number 2", + env=env, + ) + verify_cli_command( + [pixi, "run", "--manifest-path", manifest, "pixi-build-package-main"], + stdout_contains="Pixi Build is number 2", + env=env, + ) diff --git a/tests/integration_rust/common/mod.rs b/tests/integration_rust/common/mod.rs index b90e082775..96d37be987 100644 --- a/tests/integration_rust/common/mod.rs +++ b/tests/integration_rust/common/mod.rs @@ -23,7 +23,7 @@ use pixi::{ task::{self, AddArgs, AliasArgs}, update, workspace, LockFileUsageConfig, }, - lock_file::UpdateMode, + lock_file::{ReinstallPackages, UpdateMode}, task::{ get_task_env, ExecutableTask, RunOutput, SearchEnvironments, TaskExecutionError, TaskGraph, TaskGraphError, TaskName, @@ -513,7 +513,11 @@ impl PixiControl { let task_env = match task_env.as_ref() { None => { lock_file - .prefix(&task.run_environment, UpdateMode::Revalidate) + .prefix( + &task.run_environment, + UpdateMode::Revalidate, + ReinstallPackages::default(), + ) .await?; let env = get_task_env(&task.run_environment, args.clean_env, None, false, false) diff --git a/tests/integration_rust/install_tests.rs b/tests/integration_rust/install_tests.rs index d66bbb5edc..1bc681dd67 100644 --- a/tests/integration_rust/install_tests.rs +++ b/tests/integration_rust/install_tests.rs @@ -6,7 +6,7 @@ use crate::common::{ }; use crate::common::{LockFileExt, PixiControl}; use fs_err::tokio as tokio_fs; -use pixi::lock_file::UpdateMode; +use pixi::lock_file::{ReinstallPackages, UpdateMode}; use pixi::{ build::BuildContext, cli::{ @@ -598,6 +598,7 @@ async fn test_old_lock_install() { no_install: false, ..Default::default() }, + ReinstallPackages::default(), ) .await .unwrap(); @@ -969,7 +970,7 @@ async fn test_multiple_prefix_update() { let pixi_records = pixi_records.clone(); // tasks.push(conda_prefix_updater.update(pixi_records)); let updater = conda_prefix_updater.clone(); - sets.spawn(async move { updater.update(pixi_records).await.cloned() }); + sets.spawn(async move { updater.update(pixi_records, None).await.cloned() }); } let mut first_modified = None;