diff --git a/crates/pixi_core/src/environment/mod.rs b/crates/pixi_core/src/environment/mod.rs index 0ff35b7d3b..f7e7071b6a 100644 --- a/crates/pixi_core/src/environment/mod.rs +++ b/crates/pixi_core/src/environment/mod.rs @@ -393,12 +393,9 @@ pub enum LockFileUsage { } impl LockFileUsage { - /// Returns true if the lock-file should be updated if it is out of date. - pub(crate) fn allows_lock_file_updates(self) -> bool { - match self { - LockFileUsage::Update => true, - LockFileUsage::Locked | LockFileUsage::Frozen => false, - } + /// Returns true if the process should error when the lock-file + pub(crate) fn allow_updates(self) -> bool { + !matches!(self, LockFileUsage::Locked) } /// Returns true if the lock-file should be checked if it is out of date. @@ -479,7 +476,8 @@ pub async fn get_update_lock_file_and_prefixes<'env>( no_install, max_concurrent_solves: update_lock_file_options.max_concurrent_solves, }) - .await?; + .await? + .0; // Get the prefix from the lock-file. let lock_file_ref = &lock_file; diff --git a/crates/pixi_core/src/lock_file/update.rs b/crates/pixi_core/src/lock_file/update.rs index 163b6785e4..a8dbc18fca 100644 --- a/crates/pixi_core/src/lock_file/update.rs +++ b/crates/pixi_core/src/lock_file/update.rs @@ -75,7 +75,7 @@ impl Workspace { pub async fn update_lock_file( &self, options: UpdateLockFileOptions, - ) -> miette::Result> { + ) -> miette::Result<(LockFileDerivedData<'_>, bool)> { let lock_file = self.load_lock_file().await?; let glob_hash_cache = GlobHashCache::default(); @@ -97,18 +97,20 @@ impl Workspace { if !options.lock_file_usage.should_check_if_out_of_date() { tracing::info!("skipping check if lock-file is up-to-date"); - return Ok(LockFileDerivedData { - workspace: self, - lock_file, - package_cache, - updated_conda_prefixes: Default::default(), - updated_pypi_prefixes: Default::default(), - uv_context: Default::default(), - io_concurrency_limit: IoConcurrencyLimit::default(), - command_dispatcher, - glob_hash_cache, - was_outdated: false, - }); + return Ok(( + LockFileDerivedData { + workspace: self, + lock_file, + package_cache, + updated_conda_prefixes: Default::default(), + updated_pypi_prefixes: Default::default(), + uv_context: Default::default(), + io_concurrency_limit: IoConcurrencyLimit::default(), + command_dispatcher, + glob_hash_cache, + }, + false, + )); } // Check which environments are out of date. @@ -122,23 +124,25 @@ impl Workspace { tracing::info!("the lock-file is up-to-date"); // If no-environment is outdated we can return early. - return Ok(LockFileDerivedData { - workspace: self, - lock_file, - package_cache, - updated_conda_prefixes: Default::default(), - updated_pypi_prefixes: Default::default(), - uv_context: Default::default(), - io_concurrency_limit: IoConcurrencyLimit::default(), - command_dispatcher, - glob_hash_cache, - was_outdated: false, - }); + return Ok(( + LockFileDerivedData { + workspace: self, + lock_file, + package_cache, + updated_conda_prefixes: Default::default(), + updated_pypi_prefixes: Default::default(), + uv_context: Default::default(), + io_concurrency_limit: IoConcurrencyLimit::default(), + command_dispatcher, + glob_hash_cache, + }, + false, + )); } // If the lock-file is out of date, but we're not allowed to update it, we // should exit. - if !options.lock_file_usage.allows_lock_file_updates() { + if !options.lock_file_usage.allow_updates() { miette::bail!("lock-file not up-to-date with the workspace"); } @@ -158,7 +162,7 @@ impl Workspace { // Write the lock-file to disk lock_file_derived_data.write_to_disk()?; - Ok(lock_file_derived_data) + Ok((lock_file_derived_data, true)) } /// Loads the lockfile for the workspace or returns `Lockfile::default` if @@ -258,9 +262,6 @@ pub struct LockFileDerivedData<'p> { /// An object that caches input hashes pub glob_hash_cache: GlobHashCache, - - /// Whether the lock file was outdated - pub was_outdated: bool, } /// The mode to use when updating a prefix. @@ -1723,7 +1724,6 @@ impl<'p> UpdateContext<'p> { io_concurrency_limit: self.io_concurrency_limit, command_dispatcher: self.command_dispatcher, glob_hash_cache: self.glob_hash_cache, - was_outdated: true, }) } } diff --git a/crates/pixi_core/src/workspace/workspace_mut.rs b/crates/pixi_core/src/workspace/workspace_mut.rs index 5345f01226..d9db4e28d6 100644 --- a/crates/pixi_core/src/workspace/workspace_mut.rs +++ b/crates/pixi_core/src/workspace/workspace_mut.rs @@ -234,7 +234,7 @@ impl WorkspaceMut { match_specs: MatchSpecs, pypi_deps: PypiDeps, source_specs: SourceSpecs, - install_no_updates: bool, + no_install: bool, lock_file_update_config: &LockFileUsage, feature_name: &FeatureName, platforms: &[Platform], @@ -359,13 +359,9 @@ impl WorkspaceMut { command_dispatcher, glob_hash_cache, io_concurrency_limit, - was_outdated: _, } = UpdateContext::builder(self.workspace()) .with_lock_file(unlocked_lock_file) - .with_no_install( - (install_no_updates && !lock_file_update_config.allows_lock_file_updates()) - || dry_run, - ) + .with_no_install(no_install || dry_run) .finish() .await? .update() @@ -412,13 +408,11 @@ impl WorkspaceMut { io_concurrency_limit, command_dispatcher, glob_hash_cache, - was_outdated: true, }; - if lock_file_update_config.allows_lock_file_updates() && !dry_run { + if !dry_run { updated_lock_file.write_to_disk()?; } - if !install_no_updates - && lock_file_update_config.allows_lock_file_updates() + if !no_install && !dry_run && self.workspace().environments().len() == 1 && default_environment_is_affected diff --git a/docs/reference/cli/pixi/add.md b/docs/reference/cli/pixi/add.md index b52b09c30c..a83f3d9009 100644 --- a/docs/reference/cli/pixi/add.md +++ b/docs/reference/cli/pixi/add.md @@ -64,10 +64,6 @@ pixi add [OPTIONS] ... ## Update Options - `--no-install` : Don't modify the environment, only modify the lock-file -- `--revalidate` -: Run the complete environment validation. This will reinstall a broken environment -- `--no-lockfile-update` -: Don't update lockfile, implies the no-install as well - `--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` diff --git a/docs/reference/cli/pixi/import.md b/docs/reference/cli/pixi/import.md index 764598ccea..90a1701017 100644 --- a/docs/reference/cli/pixi/import.md +++ b/docs/reference/cli/pixi/import.md @@ -48,20 +48,6 @@ pixi import [OPTIONS] - `--use-environment-activation-cache` : Use environment activation cache (experimental) -## Update Options -- `--no-install` -: Don't modify the environment, only modify the lock-file -- `--revalidate` -: Run the complete environment validation. This will reinstall a broken environment -- `--no-lockfile-update` -: Don't update lockfile, implies the no-install as well -- `--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` - ## Global Options - `--manifest-path ` : The path to `pixi.toml`, `pyproject.toml`, or the workspace directory diff --git a/docs/reference/cli/pixi/list.md b/docs/reference/cli/pixi/list.md index e814d64688..9aeecae7cb 100644 --- a/docs/reference/cli/pixi/list.md +++ b/docs/reference/cli/pixi/list.md @@ -32,14 +32,14 @@ pixi list [OPTIONS] [REGEX] : Only list packages that are explicitly defined in the workspace ## Update Options -- `--no-lockfile-update` -: Don't update lockfile, implies the no-install as well - `--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` +- `--no-install` +: Don't modify the environment, only modify the lock-file ## Global Options - `--manifest-path ` diff --git a/docs/reference/cli/pixi/lock.md b/docs/reference/cli/pixi/lock.md index 63599d0833..5729602e3b 100644 --- a/docs/reference/cli/pixi/lock.md +++ b/docs/reference/cli/pixi/lock.md @@ -17,6 +17,10 @@ pixi lock [OPTIONS] - `--check` : Check if any changes have been made to the lock file. If yes, exit with a non-zero code +## Update Options +- `--no-install` +: Don't modify the environment, only modify the lock-file + ## Global Options - `--manifest-path ` : The path to `pixi.toml`, `pyproject.toml`, or the workspace directory diff --git a/docs/reference/cli/pixi/remove.md b/docs/reference/cli/pixi/remove.md index f18efb887c..70efe086b9 100644 --- a/docs/reference/cli/pixi/remove.md +++ b/docs/reference/cli/pixi/remove.md @@ -62,10 +62,6 @@ pixi remove [OPTIONS] ... ## Update Options - `--no-install` : Don't modify the environment, only modify the lock-file -- `--revalidate` -: Run the complete environment validation. This will reinstall a broken environment -- `--no-lockfile-update` -: Don't update lockfile, implies the no-install as well - `--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` diff --git a/docs/reference/cli/pixi/run.md b/docs/reference/cli/pixi/run.md index 4e89c155f4..e065a77925 100644 --- a/docs/reference/cli/pixi/run.md +++ b/docs/reference/cli/pixi/run.md @@ -55,10 +55,6 @@ pixi run [OPTIONS] [TASK]... ## Update Options - `--no-install` : Don't modify the environment, only modify the lock-file -- `--revalidate` -: Run the complete environment validation. This will reinstall a broken environment -- `--no-lockfile-update` -: Don't update lockfile, implies the no-install as well - `--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` diff --git a/docs/reference/cli/pixi/shell-hook.md b/docs/reference/cli/pixi/shell-hook.md index d25ae7ba6c..355543b3fb 100644 --- a/docs/reference/cli/pixi/shell-hook.md +++ b/docs/reference/cli/pixi/shell-hook.md @@ -50,10 +50,6 @@ pixi shell-hook [OPTIONS] ## Update Options - `--no-install` : Don't modify the environment, only modify the lock-file -- `--revalidate` -: Run the complete environment validation. This will reinstall a broken environment -- `--no-lockfile-update` -: Don't update lockfile, implies the no-install as well - `--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` diff --git a/docs/reference/cli/pixi/shell.md b/docs/reference/cli/pixi/shell.md index b849aa6b5c..2db4f4e375 100644 --- a/docs/reference/cli/pixi/shell.md +++ b/docs/reference/cli/pixi/shell.md @@ -45,10 +45,6 @@ pixi shell [OPTIONS] ## Update Options - `--no-install` : Don't modify the environment, only modify the lock-file -- `--revalidate` -: Run the complete environment validation. This will reinstall a broken environment -- `--no-lockfile-update` -: Don't update lockfile, implies the no-install as well - `--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` diff --git a/docs/reference/cli/pixi/tree.md b/docs/reference/cli/pixi/tree.md index fad0424641..0cdad2a4e3 100644 --- a/docs/reference/cli/pixi/tree.md +++ b/docs/reference/cli/pixi/tree.md @@ -24,14 +24,14 @@ pixi tree [OPTIONS] [REGEX] : Invert tree and show what depends on given package in the regex argument ## Update Options -- `--no-lockfile-update` -: Don't update lockfile, implies the no-install as well - `--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` +- `--no-install` +: Don't modify the environment, only modify the lock-file ## Global Options - `--manifest-path ` diff --git a/docs/reference/cli/pixi/upgrade.md b/docs/reference/cli/pixi/upgrade.md index bceddbbdb2..a0674414c3 100644 --- a/docs/reference/cli/pixi/upgrade.md +++ b/docs/reference/cli/pixi/upgrade.md @@ -51,10 +51,6 @@ pixi upgrade [OPTIONS] [PACKAGES]... ## Update Options - `--no-install` : Don't modify the environment, only modify the lock-file -- `--revalidate` -: Run the complete environment validation. This will reinstall a broken environment -- `--no-lockfile-update` -: Don't update lockfile, implies the no-install as well - `--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` diff --git a/docs/reference/cli/pixi/workspace/channel/add.md b/docs/reference/cli/pixi/workspace/channel/add.md index 080475e1f0..52735a0a36 100644 --- a/docs/reference/cli/pixi/workspace/channel/add.md +++ b/docs/reference/cli/pixi/workspace/channel/add.md @@ -48,10 +48,6 @@ pixi workspace channel add [OPTIONS] ... ## Update Options - `--no-install` : Don't modify the environment, only modify the lock-file -- `--revalidate` -: Run the complete environment validation. This will reinstall a broken environment -- `--no-lockfile-update` -: Don't update lockfile, implies the no-install as well - `--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` diff --git a/docs/reference/cli/pixi/workspace/channel/remove.md b/docs/reference/cli/pixi/workspace/channel/remove.md index 8cbc1615c4..e7c98f45df 100644 --- a/docs/reference/cli/pixi/workspace/channel/remove.md +++ b/docs/reference/cli/pixi/workspace/channel/remove.md @@ -48,10 +48,6 @@ pixi workspace channel remove [OPTIONS] ... ## Update Options - `--no-install` : Don't modify the environment, only modify the lock-file -- `--revalidate` -: Run the complete environment validation. This will reinstall a broken environment -- `--no-lockfile-update` -: Don't update lockfile, implies the no-install as well - `--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` diff --git a/docs/reference/cli/pixi/workspace/export/conda-explicit-spec.md b/docs/reference/cli/pixi/workspace/export/conda-explicit-spec.md index 1890b09476..b8feba6781 100644 --- a/docs/reference/cli/pixi/workspace/export/conda-explicit-spec.md +++ b/docs/reference/cli/pixi/workspace/export/conda-explicit-spec.md @@ -51,14 +51,14 @@ pixi workspace export conda-explicit-spec [OPTIONS] : Use environment activation cache (experimental) ## Update Options -- `--no-lockfile-update` -: Don't update lockfile, implies the no-install as well - `--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` +- `--no-install` +: Don't modify the environment, only modify the lock-file ## Global Options - `--manifest-path ` diff --git a/docs/workspace/environment.md b/docs/workspace/environment.md index 9606eb3492..08fe446480 100644 --- a/docs/workspace/environment.md +++ b/docs/workspace/environment.md @@ -104,7 +104,7 @@ This file contains the following information: The `environment_lock_file_hash` is used to check if the environment is in sync with the `pixi.lock` file. If the hash of the `pixi.lock` file is different from the hash in the `pixi` file, Pixi will update the environment. -This is used to speedup activation, in order to trigger a full revalidation pass `--revalidate` to the `pixi run` or `pixi shell` command. +This is used to speedup activation, in order to trigger a full revalidation and installation use `pixi install` or `pixi reinstall`. A broken environment would typically not be found with a hash comparison, but a revalidation would reinstall the environment. By default, all lock file modifying commands will always use the revalidation and on `pixi install` it always revalidates. diff --git a/src/cli/add.rs b/src/cli/add.rs index 3eb2411e21..c163697f5d 100644 --- a/src/cli/add.rs +++ b/src/cli/add.rs @@ -8,7 +8,7 @@ use pixi_spec::{GitSpec, SourceLocationSpec, SourceSpec}; use rattler_conda_types::{MatchSpec, PackageName}; use crate::cli::{ - cli_config::{DependencyConfig, LockFileUpdateConfig, PrefixUpdateConfig, WorkspaceConfig}, + cli_config::{DependencyConfig, LockFileUpdateConfig, NoInstallConfig, WorkspaceConfig}, has_specs::HasSpecs, }; @@ -81,7 +81,7 @@ pub struct Args { pub dependency_config: DependencyConfig, #[clap(flatten)] - pub prefix_update_config: PrefixUpdateConfig, + pub no_install_config: NoInstallConfig, #[clap(flatten)] pub lock_file_update_config: LockFileUpdateConfig, @@ -95,9 +95,9 @@ pub struct Args { } pub async fn execute(args: Args) -> miette::Result<()> { - let (dependency_config, prefix_update_config, lock_file_update_config, workspace_config) = ( + let (dependency_config, no_install_config, lock_file_update_config, workspace_config) = ( args.dependency_config, - args.prefix_update_config, + args.no_install_config, args.lock_file_update_config, args.workspace_config, ); @@ -197,8 +197,8 @@ pub async fn execute(args: Args) -> miette::Result<()> { match_specs, pypi_deps, source_specs, - prefix_update_config.no_install, - &lock_file_update_config.lock_file_usage(), + no_install_config.no_install, + &lock_file_update_config.lock_file_usage()?, &dependency_config.feature, &dependency_config.platforms, args.editable, diff --git a/src/cli/cli_config.rs b/src/cli/cli_config.rs index 487763c098..fc4a8e4e9e 100644 --- a/src/cli/cli_config.rs +++ b/src/cli/cli_config.rs @@ -10,7 +10,6 @@ use pixi_consts::consts; use pixi_core::DependencyType; use pixi_core::Workspace; use pixi_core::environment::LockFileUsage; -use pixi_core::lock_file::UpdateMode; use pixi_core::workspace::DiscoveryStart; use pixi_manifest::FeaturesExt; use pixi_manifest::{FeatureName, SpecType}; @@ -102,8 +101,8 @@ impl ChannelsConfig { #[derive(Parser, Debug, Default, Clone)] pub struct LockFileUpdateConfig { - /// Don't update lockfile, implies the no-install as well. - #[clap(long, help_heading = consts::CLAP_UPDATE_OPTIONS)] + /// DEPRECATED: use `--frozen` `--no-install`. Skips lock-file updates + #[clap(hide = true, long, help_heading = consts::CLAP_UPDATE_OPTIONS)] pub no_lockfile_update: bool, /// Lock file usage from the CLI @@ -112,36 +111,40 @@ pub struct LockFileUpdateConfig { } impl LockFileUpdateConfig { - pub fn lock_file_usage(&self) -> LockFileUsage { + pub fn lock_file_usage(&self) -> miette::Result { + // Error on deprecated flag usage + if self.no_lockfile_update { + return Err(miette::miette!( + help = "Use '--frozen' to skip lock-file updates.\nUse '--no-install' to skip installation.", + "The '--no-lockfile-update' flag has been deprecated due to inconsistent behavior across commands. This flag will be removed in a future version." + )); + } + let usage: LockFileUsage = self.lock_file_usage.clone().into(); if self.no_lockfile_update { - LockFileUsage::Frozen + Ok(LockFileUsage::Frozen) } else { - usage + Ok(usage) } } } -/// Configuration for how to update the prefix +/// Configuration for skipping installation #[derive(Parser, Debug, Default, Clone)] -pub struct PrefixUpdateConfig { +pub struct NoInstallConfig { /// Don't modify the environment, only modify the lock-file. #[arg(long, help_heading = consts::CLAP_UPDATE_OPTIONS)] pub no_install: bool, - - /// Run the complete environment validation. This will reinstall a broken environment. - #[arg(long, help_heading = consts::CLAP_UPDATE_OPTIONS)] - pub revalidate: bool, } -impl PrefixUpdateConfig { - /// Which `[UpdateMode]` to use - pub(crate) fn update_mode(&self) -> UpdateMode { - if self.revalidate { - UpdateMode::Revalidate - } else { - UpdateMode::QuickValidate - } +impl NoInstallConfig { + /// Creates a new NoInstallConfig with the specified value + pub fn new(no_install: bool) -> Self { + Self { no_install } + } + + pub fn allow_installs(&self) -> bool { + !self.no_install } } diff --git a/src/cli/completion.rs b/src/cli/completion.rs index 1a37ef8774..665628cba4 100644 --- a/src/cli/completion.rs +++ b/src/cli/completion.rs @@ -297,7 +297,7 @@ _arguments "${_arguments_options[@]}" \ export extern "pixi run" [ ...task: string@"nu-complete pixi run" # The pixi task or a task shell command you want to run in the project's environment, which can be an executable in the environment's PATH --manifest-path: string # The path to `pixi.toml`, `pyproject.toml`, or the project directory - --no-lockfile-update # Don't update lockfile, implies the no-install as well + --no-lockfile-update # Legacy flag, do not use, will be removed in subsequent version --frozen # Install the environment as defined in the lockfile, doesn't update lockfile if it isn't up-to-date with the manifest file --locked # Check if lockfile is up-to-date before installing the environment, aborts when lockfile isn't up-to-date with the manifest file --no-install # Don't modify the environment, only modify the lock-file @@ -306,7 +306,6 @@ _arguments "${_arguments_options[@]}" \ --pypi-keyring-provider: string@"nu-complete pixi run pypi_keyring_provider" # Specifies if we want to use uv keyring provider --concurrent-solves: string # Max concurrent solves, default is the number of CPUs --concurrent-downloads: string # Max concurrent network requests, default is 50 - --revalidate # Run the complete environment validation. This will reinstall a broken environment --force-activate # Do not use the environment activation cache. (default: true except in experimental mode) --environment(-e): string@"nu-complete pixi run environment" # The environment to run the task in --clean-env # Use a clean environment to run the task diff --git a/src/cli/import.rs b/src/cli/import.rs index db37ed9599..509845ba5a 100644 --- a/src/cli/import.rs +++ b/src/cli/import.rs @@ -16,8 +16,7 @@ use uv_requirements_txt::RequirementsTxt; use miette::{Diagnostic, IntoDiagnostic, Result}; use thiserror::Error; -use crate::cli::cli_config::LockFileUpdateConfig; -use crate::cli::cli_config::{PrefixUpdateConfig, WorkspaceConfig}; +use crate::cli::cli_config::WorkspaceConfig; #[derive(Parser, Debug, Clone, PartialEq, ValueEnum)] pub enum ImportFileFormat { @@ -55,12 +54,6 @@ pub struct Args { #[clap(long, short)] pub feature: Option, - #[clap(flatten)] - pub prefix_update_config: PrefixUpdateConfig, - - #[clap(flatten)] - pub lock_file_update_config: LockFileUpdateConfig, - #[clap(flatten)] pub config: ConfigCli, } diff --git a/src/cli/list.rs b/src/cli/list.rs index 6121cb1d15..3c58b6c80e 100644 --- a/src/cli/list.rs +++ b/src/cli/list.rs @@ -26,8 +26,7 @@ use serde::Serialize; use uv_configuration::ConfigSettings; use uv_distribution::RegistryWheelIndex; -use crate::cli::cli_config::LockFileUpdateConfig; -use crate::cli::cli_config::WorkspaceConfig; +use crate::cli::cli_config::{LockFileUpdateConfig, NoInstallConfig, WorkspaceConfig}; // an enum to sort by size or name #[derive(clap::ValueEnum, Clone, Debug, Serialize)] @@ -74,6 +73,9 @@ pub struct Args { #[clap(flatten)] pub lock_file_update_config: LockFileUpdateConfig, + #[clap(flatten)] + pub no_install_config: NoInstallConfig, + /// Only list packages that are explicitly defined in the workspace. #[arg(short = 'x', long)] pub explicit: bool, @@ -184,11 +186,12 @@ pub async fn execute(args: Args) -> miette::Result<()> { let lock_file = workspace .update_lock_file(UpdateLockFileOptions { - lock_file_usage: args.lock_file_update_config.lock_file_usage(), - no_install: false, + lock_file_usage: args.lock_file_update_config.lock_file_usage()?, + no_install: args.no_install_config.no_install, max_concurrent_solves: workspace.config().max_concurrent_solves(), }) .await? + .0 .into_lock_file(); // Load the platform diff --git a/src/cli/lock.rs b/src/cli/lock.rs index e181169566..924eb864c0 100644 --- a/src/cli/lock.rs +++ b/src/cli/lock.rs @@ -1,14 +1,14 @@ use clap::Parser; use miette::{Context, IntoDiagnostic}; - -use pixi_core::lock_file::LockFileDerivedData; use pixi_core::{ WorkspaceLocator, diff::{LockFileDiff, LockFileJsonDiff}, environment::LockFileUsage, - lock_file::UpdateLockFileOptions, + lock_file::{LockFileDerivedData, UpdateLockFileOptions}, }; +use crate::cli::cli_config::NoInstallConfig; + use crate::cli::cli_config::WorkspaceConfig; /// Solve environment and update the lock file without installing the @@ -19,6 +19,9 @@ pub struct Args { #[clap(flatten)] pub workspace_config: WorkspaceConfig, + #[clap(flatten)] + pub no_install_config: NoInstallConfig, + /// Output the changes in JSON format. #[clap(long)] pub json: bool, @@ -37,14 +40,10 @@ pub async fn execute(args: Args) -> miette::Result<()> { // Update the lock-file, and extract it from the derived data to drop additional resources // created for the solve. let original_lock_file = workspace.load_lock_file().await?; - let LockFileDerivedData { - lock_file, - was_outdated, - .. - } = workspace + let (LockFileDerivedData { lock_file, .. }, lock_updated) = workspace .update_lock_file(UpdateLockFileOptions { lock_file_usage: LockFileUsage::Update, - no_install: false, + no_install: args.no_install_config.no_install, max_concurrent_solves: workspace.config().max_concurrent_solves(), }) .await?; @@ -58,7 +57,7 @@ pub async fn execute(args: Args) -> miette::Result<()> { let json_diff = LockFileJsonDiff::new(Some(&workspace), diff); let json = serde_json::to_string_pretty(&json_diff).expect("failed to convert to json"); println!("{}", json); - } else if was_outdated { + } else if lock_updated { eprintln!( "{}Updated lock-file", console::style(console::Emoji("✔ ", "")).green() diff --git a/src/cli/remove.rs b/src/cli/remove.rs index 45c3d8d06e..f5c9a69c2f 100644 --- a/src/cli/remove.rs +++ b/src/cli/remove.rs @@ -8,7 +8,7 @@ use pixi_core::{ }; use pixi_manifest::FeaturesExt; -use crate::cli::cli_config::{DependencyConfig, PrefixUpdateConfig, WorkspaceConfig}; +use crate::cli::cli_config::{DependencyConfig, NoInstallConfig, WorkspaceConfig}; use crate::cli::{cli_config::LockFileUpdateConfig, has_specs::HasSpecs}; /// Removes dependencies from the workspace. @@ -31,8 +31,7 @@ pub struct Args { pub dependency_config: DependencyConfig, #[clap(flatten)] - pub prefix_update_config: PrefixUpdateConfig, - + pub no_install_config: NoInstallConfig, #[clap(flatten)] pub lock_file_update_config: LockFileUpdateConfig, @@ -41,9 +40,9 @@ pub struct Args { } pub async fn execute(args: Args) -> miette::Result<()> { - let (dependency_config, prefix_update_config, lock_file_update_config, workspace_config) = ( + let (dependency_config, no_install_config, lock_file_update_config, workspace_config) = ( args.dependency_config, - args.prefix_update_config, + args.no_install_config, args.lock_file_update_config, args.workspace_config, ); @@ -121,8 +120,8 @@ pub async fn execute(args: Args) -> miette::Result<()> { &workspace.default_environment(), UpdateMode::Revalidate, UpdateLockFileOptions { - lock_file_usage: lock_file_update_config.lock_file_usage(), - no_install: prefix_update_config.no_install, + lock_file_usage: lock_file_update_config.lock_file_usage()?, + no_install: no_install_config.no_install, max_concurrent_solves: workspace.config().max_concurrent_solves(), }, ReinstallPackages::default(), diff --git a/src/cli/run.rs b/src/cli/run.rs index 54ef93c97b..3bb95b3e6d 100644 --- a/src/cli/run.rs +++ b/src/cli/run.rs @@ -21,7 +21,7 @@ use tracing::Level; use pixi_core::{ Workspace, WorkspaceLocator, environment::sanity_check_workspace, - lock_file::{ReinstallPackages, UpdateLockFileOptions}, + lock_file::{ReinstallPackages, UpdateLockFileOptions, UpdateMode}, task::{ AmbiguousTask, CanSkip, ExecutableTask, FailedToParseShellScript, InvalidWorkingDirectory, SearchEnvironments, TaskAndEnvironment, TaskGraph, get_task_env, @@ -29,8 +29,7 @@ use pixi_core::{ workspace::{Environment, errors::UnsupportedPlatformError}, }; -use crate::cli::cli_config::LockFileUpdateConfig; -use crate::cli::cli_config::{PrefixUpdateConfig, WorkspaceConfig}; +use crate::cli::cli_config::{LockFileUpdateConfig, NoInstallConfig, WorkspaceConfig}; /// Runs task in the pixi environment. /// @@ -51,8 +50,7 @@ pub struct Args { pub workspace_config: WorkspaceConfig, #[clap(flatten)] - pub prefix_update_config: PrefixUpdateConfig, - + pub no_install_config: NoInstallConfig, #[clap(flatten)] pub lock_file_update_config: LockFileUpdateConfig, @@ -127,11 +125,12 @@ pub async fn execute(args: Args) -> miette::Result<()> { // Ensure that the lock-file is up-to-date. let lock_file = workspace .update_lock_file(UpdateLockFileOptions { - lock_file_usage: args.lock_file_update_config.lock_file_usage(), + no_install: args.no_install_config.no_install, + lock_file_usage: args.lock_file_update_config.lock_file_usage()?, max_concurrent_solves: workspace.config().max_concurrent_solves(), - ..UpdateLockFileOptions::default() }) - .await?; + .await? + .0; // Spawn a task that listens for ctrl+c and resets the cursor. tokio::spawn(async { @@ -254,15 +253,18 @@ pub async fn execute(args: Args) -> miette::Result<()> { let task_env: &_ = match task_envs.entry(executable_task.run_environment.clone()) { Entry::Occupied(env) => env.into_mut(), Entry::Vacant(entry) => { - // Ensure there is a valid prefix - lock_file - .prefix( - &executable_task.run_environment, - args.prefix_update_config.update_mode(), - &ReinstallPackages::default(), - &[], - ) - .await?; + // Check if we allow installs + if args.no_install_config.allow_installs() { + // Ensure there is a valid prefix + lock_file + .prefix( + &executable_task.run_environment, + UpdateMode::QuickValidate, + &ReinstallPackages::default(), + &[], + ) + .await?; + } // Clear the current progress reports. lock_file.command_dispatcher.clear_reporter().await; diff --git a/src/cli/shell.rs b/src/cli/shell.rs index 0d592c2099..a24cb5c02a 100644 --- a/src/cli/shell.rs +++ b/src/cli/shell.rs @@ -9,13 +9,16 @@ use rattler_shell::{ }; use pixi_config::{ConfigCli, ConfigCliActivation, ConfigCliPrompt}; -use pixi_core::lock_file::ReinstallPackages; -use pixi_core::lock_file::UpdateMode; -use pixi_core::workspace::get_activated_environment_variables; use pixi_core::{ - UpdateLockFileOptions, WorkspaceLocator, activation::CurrentEnvVarBehavior, - environment::get_update_lock_file_and_prefix, prompt, + UpdateLockFileOptions, WorkspaceLocator, + activation::CurrentEnvVarBehavior, + environment::get_update_lock_file_and_prefix, + lock_file::{ReinstallPackages, UpdateMode}, + prompt, + workspace::get_activated_environment_variables, }; + +use crate::cli::cli_config::{NoInstallConfig, WorkspaceConfig}; #[cfg(target_family = "unix")] use pixi_pty::unix::PtySession; @@ -23,7 +26,6 @@ use pixi_pty::unix::PtySession; use pixi_utils::prefix::Prefix; use crate::cli::cli_config::LockFileUpdateConfig; -use crate::cli::cli_config::{PrefixUpdateConfig, WorkspaceConfig}; /// Start a shell in a pixi environment, run `exit` to leave the shell. #[derive(Parser, Debug)] @@ -32,8 +34,7 @@ pub struct Args { workspace_config: WorkspaceConfig, #[clap(flatten)] - pub prefix_update_config: PrefixUpdateConfig, - + pub no_install_config: NoInstallConfig, #[clap(flatten)] pub lock_file_update_config: LockFileUpdateConfig, @@ -280,9 +281,8 @@ pub async fn execute(args: Args) -> miette::Result<()> { &environment, UpdateMode::QuickValidate, UpdateLockFileOptions { - lock_file_usage: args.lock_file_update_config.lock_file_usage(), - no_install: args.prefix_update_config.no_install - && args.lock_file_update_config.no_lockfile_update, + lock_file_usage: args.lock_file_update_config.lock_file_usage()?, + no_install: args.no_install_config.no_install, max_concurrent_solves: workspace.config().max_concurrent_solves(), }, ReinstallPackages::default(), diff --git a/src/cli/shell_hook.rs b/src/cli/shell_hook.rs index 70a2a7a789..8e2efd55ab 100644 --- a/src/cli/shell_hook.rs +++ b/src/cli/shell_hook.rs @@ -15,13 +15,12 @@ use pixi_core::{ UpdateLockFileOptions, Workspace, WorkspaceLocator, activation::{CurrentEnvVarBehavior, get_activator}, environment::get_update_lock_file_and_prefix, - lock_file::ReinstallPackages, + lock_file::{ReinstallPackages, UpdateMode}, prompt, workspace::{Environment, HasWorkspaceRef, get_activated_environment_variables}, }; -use crate::cli::cli_config::LockFileUpdateConfig; -use crate::cli::cli_config::{PrefixUpdateConfig, WorkspaceConfig}; +use crate::cli::cli_config::{LockFileUpdateConfig, NoInstallConfig, WorkspaceConfig}; /// Print the pixi environment activation script. /// @@ -38,8 +37,7 @@ pub struct Args { pub project_config: WorkspaceConfig, #[clap(flatten)] - pub prefix_update_config: PrefixUpdateConfig, - + pub no_install_config: NoInstallConfig, #[clap(flatten)] pub lock_file_update_config: LockFileUpdateConfig, @@ -160,11 +158,10 @@ pub async fn execute(args: Args) -> miette::Result<()> { let (lock_file_data, _prefix) = get_update_lock_file_and_prefix( &environment, - args.prefix_update_config.update_mode(), + UpdateMode::QuickValidate, UpdateLockFileOptions { - lock_file_usage: args.lock_file_update_config.lock_file_usage(), - no_install: args.prefix_update_config.no_install - && args.lock_file_update_config.no_lockfile_update, + lock_file_usage: args.lock_file_update_config.lock_file_usage()?, + no_install: args.no_install_config.no_install, max_concurrent_solves: workspace.config().max_concurrent_solves(), }, ReinstallPackages::default(), diff --git a/src/cli/snapshots/pixi__cli__completion__tests__nushell_completion.snap b/src/cli/snapshots/pixi__cli__completion__tests__nushell_completion.snap index 6325238e1b..f8e3e542ac 100644 --- a/src/cli/snapshots/pixi__cli__completion__tests__nushell_completion.snap +++ b/src/cli/snapshots/pixi__cli__completion__tests__nushell_completion.snap @@ -1,13 +1,12 @@ --- source: src/cli/completion.rs expression: result -snapshot_kind: text --- # Runs task in project export extern "pixi run" [ ...task: string@"nu-complete pixi run" # The pixi task or a task shell command you want to run in the project's environment, which can be an executable in the environment's PATH --manifest-path: string # The path to `pixi.toml`, `pyproject.toml`, or the project directory - --no-lockfile-update # Don't update lockfile, implies the no-install as well + --no-lockfile-update # Legacy flag, do not use, will be removed in subsequent version --frozen # Install the environment as defined in the lockfile, doesn't update lockfile if it isn't up-to-date with the manifest file --locked # Check if lockfile is up-to-date before installing the environment, aborts when lockfile isn't up-to-date with the manifest file --no-install # Don't modify the environment, only modify the lock-file @@ -16,7 +15,6 @@ snapshot_kind: text --pypi-keyring-provider: string@"nu-complete pixi run pypi_keyring_provider" # Specifies if we want to use uv keyring provider --concurrent-solves: string # Max concurrent solves, default is the number of CPUs --concurrent-downloads: string # Max concurrent network requests, default is 50 - --revalidate # Run the complete environment validation. This will reinstall a broken environment --force-activate # Do not use the environment activation cache. (default: true except in experimental mode) --environment(-e): string@"nu-complete pixi run environment" # The environment to run the task in --clean-env # Use a clean environment to run the task diff --git a/src/cli/tree.rs b/src/cli/tree.rs index 686cd183ab..0f0da3a31b 100644 --- a/src/cli/tree.rs +++ b/src/cli/tree.rs @@ -15,8 +15,7 @@ use rattler_conda_types::Platform; use rattler_lock::LockedPackageRef; use regex::Regex; -use crate::cli::cli_config::LockFileUpdateConfig; -use crate::cli::cli_config::WorkspaceConfig; +use crate::cli::cli_config::{LockFileUpdateConfig, NoInstallConfig, WorkspaceConfig}; /// Show a tree of workspace dependencies #[derive(Debug, Parser)] @@ -51,6 +50,9 @@ pub struct Args { #[clap(flatten)] pub lock_file_update_config: LockFileUpdateConfig, + #[clap(flatten)] + pub no_install_config: NoInstallConfig, + /// Invert tree and show what depends on given package in the regex argument #[arg(short, long, requires = "regex")] pub invert: bool, @@ -81,12 +83,13 @@ pub async fn execute(args: Args) -> miette::Result<()> { let lock_file = workspace .update_lock_file(UpdateLockFileOptions { - lock_file_usage: args.lock_file_update_config.lock_file_usage(), - no_install: args.lock_file_update_config.no_lockfile_update, + lock_file_usage: args.lock_file_update_config.lock_file_usage()?, + no_install: args.no_install_config.no_install, max_concurrent_solves: workspace.config().max_concurrent_solves(), }) .await .wrap_err("Failed to update lock file")? + .0 .into_lock_file(); let platform = args.platform.unwrap_or_else(|| environment.best_platform()); diff --git a/src/cli/upgrade.rs b/src/cli/upgrade.rs index eadc2177da..8c5fd1b45f 100644 --- a/src/cli/upgrade.rs +++ b/src/cli/upgrade.rs @@ -4,7 +4,7 @@ use clap::Parser; use fancy_display::FancyDisplay; use indexmap::IndexMap; use itertools::Itertools; -use miette::{Context, IntoDiagnostic, MietteDiagnostic}; +use miette::{IntoDiagnostic, MietteDiagnostic, WrapErr}; use pep508_rs::{MarkerTree, Requirement}; use pixi_config::ConfigCli; use pixi_core::{ @@ -17,8 +17,7 @@ use pixi_pypi_spec::PixiPypiSpec; use pixi_spec::PixiSpec; use rattler_conda_types::{MatchSpec, StringMatcher}; -use crate::cli::cli_config::WorkspaceConfig; -use crate::cli::cli_config::{LockFileUpdateConfig, PrefixUpdateConfig}; +use crate::cli::cli_config::{LockFileUpdateConfig, NoInstallConfig, WorkspaceConfig}; /// Checks if there are newer versions of the dependencies and upgrades them in the lockfile and manifest file. /// @@ -29,8 +28,7 @@ pub struct Args { pub workspace_config: WorkspaceConfig, #[clap(flatten)] - pub prefix_update_config: PrefixUpdateConfig, - + pub no_install_config: NoInstallConfig, #[clap(flatten)] pub lock_file_update_config: LockFileUpdateConfig, @@ -85,15 +83,23 @@ pub async fn execute(args: Args) -> miette::Result<()> { ) }; - let (match_specs, pypi_deps) = parse_specs(feature, &args, &workspace)?; + if !args.no_install_config.allow_installs() + && (args.lock_file_update_config.lock_file_usage.frozen + || args.lock_file_update_config.lock_file_usage.locked) + { + tracing::warn!( + "using `--frozen` or `--locked` will not make any changes and does not display results. You probably meant: `--dry-run`" + ) + } + let (match_specs, pypi_deps) = parse_specs(feature, &args, &workspace)?; let (update_deps, workspace) = match workspace .update_dependencies( match_specs, pypi_deps, IndexMap::default(), - args.prefix_update_config.no_install, - &args.lock_file_update_config.lock_file_usage(), + args.no_install_config.no_install, + &args.lock_file_update_config.lock_file_usage()?, &args.specs.feature, &[], false, diff --git a/src/cli/workspace/channel/add.rs b/src/cli/workspace/channel/add.rs index dc52087e50..37d5cddce0 100644 --- a/src/cli/workspace/channel/add.rs +++ b/src/cli/workspace/channel/add.rs @@ -1,7 +1,7 @@ use miette::IntoDiagnostic; use pixi_core::{ UpdateLockFileOptions, WorkspaceLocator, - environment::{LockFileUsage, get_update_lock_file_and_prefix}, + environment::get_update_lock_file_and_prefix, lock_file::{ReinstallPackages, UpdateMode}, }; @@ -26,9 +26,8 @@ pub async fn execute(args: AddRemoveArgs) -> miette::Result<()> { &workspace.workspace().default_environment(), UpdateMode::Revalidate, UpdateLockFileOptions { - lock_file_usage: LockFileUsage::Update, - no_install: args.prefix_update_config.no_install - && args.lock_file_update_config.no_lockfile_update, + lock_file_usage: args.lock_file_update_config.lock_file_usage()?, + no_install: args.no_install_config.no_install, max_concurrent_solves: workspace.workspace().config().max_concurrent_solves(), }, ReinstallPackages::default(), diff --git a/src/cli/workspace/channel/mod.rs b/src/cli/workspace/channel/mod.rs index b5e7e081d1..87117cbda3 100644 --- a/src/cli/workspace/channel/mod.rs +++ b/src/cli/workspace/channel/mod.rs @@ -8,7 +8,7 @@ use pixi_config::ConfigCli; use pixi_manifest::{FeatureName, PrioritizedChannel}; use rattler_conda_types::{ChannelConfig, NamedChannelOrUrl}; -use crate::cli::cli_config::{LockFileUpdateConfig, PrefixUpdateConfig, WorkspaceConfig}; +use crate::cli::cli_config::{LockFileUpdateConfig, NoInstallConfig, WorkspaceConfig}; /// Commands to manage workspace channels. #[derive(Parser, Debug, Clone)] @@ -36,8 +36,7 @@ pub struct AddRemoveArgs { pub prepend: bool, #[clap(flatten)] - pub prefix_update_config: PrefixUpdateConfig, - + pub no_install_config: NoInstallConfig, #[clap(flatten)] pub lock_file_update_config: LockFileUpdateConfig, diff --git a/src/cli/workspace/channel/remove.rs b/src/cli/workspace/channel/remove.rs index 02bd995e12..79331118fe 100644 --- a/src/cli/workspace/channel/remove.rs +++ b/src/cli/workspace/channel/remove.rs @@ -1,8 +1,8 @@ use miette::IntoDiagnostic; -use pixi_core::lock_file::{ReinstallPackages, UpdateMode}; use pixi_core::{ UpdateLockFileOptions, WorkspaceLocator, - environment::{LockFileUsage, get_update_lock_file_and_prefix}, + environment::get_update_lock_file_and_prefix, + lock_file::{ReinstallPackages, UpdateMode}, }; use super::AddRemoveArgs; @@ -24,9 +24,8 @@ pub async fn execute(args: AddRemoveArgs) -> miette::Result<()> { &workspace.workspace().default_environment(), UpdateMode::Revalidate, UpdateLockFileOptions { - lock_file_usage: LockFileUsage::Update, - no_install: args.prefix_update_config.no_install - && args.lock_file_update_config.no_lockfile_update, + lock_file_usage: args.lock_file_update_config.lock_file_usage()?, + no_install: args.no_install_config.no_install, max_concurrent_solves: workspace.workspace().config().max_concurrent_solves(), }, ReinstallPackages::default(), diff --git a/src/cli/workspace/export/conda_explicit_spec.rs b/src/cli/workspace/export/conda_explicit_spec.rs index 2083a89eb8..47f7b8297f 100644 --- a/src/cli/workspace/export/conda_explicit_spec.rs +++ b/src/cli/workspace/export/conda_explicit_spec.rs @@ -12,7 +12,7 @@ use rattler_conda_types::{ }; use rattler_lock::{CondaPackageData, Environment, LockedPackageRef}; -use crate::cli::cli_config::{LockFileUpdateConfig, WorkspaceConfig}; +use crate::cli::cli_config::{LockFileUpdateConfig, NoInstallConfig, WorkspaceConfig}; #[derive(Debug, Parser)] #[clap(arg_required_else_help = false)] @@ -43,6 +43,9 @@ pub struct Args { #[clap(flatten)] pub lock_file_update_config: LockFileUpdateConfig, + #[clap(flatten)] + pub no_install_config: NoInstallConfig, + #[clap(flatten)] config: ConfigCli, } @@ -168,11 +171,12 @@ pub async fn execute(args: Args) -> miette::Result<()> { let lockfile = workspace .update_lock_file(UpdateLockFileOptions { - lock_file_usage: args.lock_file_update_config.lock_file_usage(), - no_install: args.lock_file_update_config.no_lockfile_update, + lock_file_usage: args.lock_file_update_config.lock_file_usage()?, + no_install: args.no_install_config.no_install, max_concurrent_solves: workspace.config().max_concurrent_solves(), }) .await? + .0 .into_lock_file(); let mut environments = Vec::new(); diff --git a/tests/integration_python/common.py b/tests/integration_python/common.py index 9a80a7068c..eec6bbebf8 100644 --- a/tests/integration_python/common.py +++ b/tests/integration_python/common.py @@ -5,7 +5,7 @@ from enum import IntEnum from pathlib import Path import sys -from typing import Generator, Optional, Sequence, Tuple +from typing import Generator, Optional, Sequence, Set, Tuple from rattler import Platform @@ -199,3 +199,124 @@ def run_and_get_env(pixi: Path, *args: str, env_var: str) -> Tuple[Optional[str] print(f"Error running command: {e}") print(f"Command: {' '.join(cmd)}") raise + + +# Command discovery utilities for testing CLI flag support + + +def discover_pixi_commands() -> set[str]: + """Discover all available pixi commands by walking the docs/reference/cli/pixi directory. + + Returns: + Set[str]: Set of command names in the format "pixi command subcommand ..." + + Examples: + {"pixi add", "pixi workspace channel add", "pixi shell", ...} + """ + docs_path = repo_root() / "docs" / "reference" / "cli" / "pixi" + commands: set[str] = set() + + if not docs_path.exists(): + return commands + + # Walk through all markdown files in the docs directory + for md_file in docs_path.rglob("*.md"): + # Get relative path from the pixi docs directory + relative_path = md_file.relative_to(docs_path) + + # Convert file path to command format + # e.g., "workspace/channel/add.md" -> "pixi workspace channel add" + command_parts = ["pixi"] + list(relative_path.parts[:-1]) + [relative_path.stem] + command = " ".join(command_parts) + commands.add(command) + + return commands + + +def check_command_supports_flags(command_parts: list[str], *flag_names: str) -> tuple[bool, ...]: + """Check if a command supports specific flags by examining its documentation. + + Args: + command_parts: List of command parts (e.g., ["workspace", "channel", "add"]) + *flag_names: Variable number of flag names to check for (e.g., "--frozen", "--no-install") + + Returns: + Tuple[bool, ...]: Tuple of booleans indicating support for each flag in order + + Examples: + check_command_supports_flags(["add"], "--frozen", "--no-install") + # Returns: (True, True) if both flags are supported + + check_command_supports_flags(["shell"], "--frozen", "--locked", "--no-install") + # Returns: (False, True, True) if only --locked and --no-install are supported + """ + # Build the documentation file path + docs_path = repo_root() / "docs" / "reference" / "cli" / "pixi" + doc_file = docs_path / Path(*command_parts).with_suffix(".md") + + if not doc_file.exists(): + return tuple(False for _ in flag_names) + + try: + doc_content = doc_file.read_text() + + # Check each flag + results = [] + for flag_name in flag_names: + results.append(flag_name in doc_content) + + return tuple(results) + + except (OSError, IOError): + return tuple(False for _ in flag_names) + + +def find_commands_supporting_flags(*flag_names: str) -> list[str]: + """Find all pixi commands that support ALL of the specified flags. + + Args: + *flag_names: Variable number of flag names that commands must support + + Returns: + List[str]: List of command names that support all specified flags + + Examples: + find_commands_supporting_flags("--frozen", "--no-install") + # Returns: ["pixi add", "pixi remove", "pixi run", ...] + + find_commands_supporting_flags("--locked", "--no-install") + # Returns: ["pixi shell"] (special case that uses --locked instead of --frozen) + """ + all_commands = discover_pixi_commands() + supported_commands = [] + + for command_str in all_commands: + # Skip the "pixi" prefix to get command parts + command_parts = ( + command_str.split()[1:] if command_str.startswith("pixi ") else command_str.split() + ) + + # Skip empty commands + if not command_parts: + continue + + # Check if the command supports all specified flags + flag_support = check_command_supports_flags(command_parts, *flag_names) + + # Only include if ALL flags are supported + if all(flag_support): + supported_commands.append(command_str) + + return sorted(supported_commands) + + +def find_commands_supporting_frozen_and_no_install() -> Set[str]: + """Convenience function to find commands supporting both --frozen and --no-install flags. + + This also includes commands that use --locked instead of --frozen (like pixi shell). + + Returns: + List[str]: List of command names supporting freeze/lock and no-install functionality + """ + # Find commands that support --frozen and --no-install + return set(find_commands_supporting_flags("--frozen", "--no-install")) diff --git a/tests/integration_python/test_main_cli.py b/tests/integration_python/test_main_cli.py index 1d17e7142a..8fd18f0844 100644 --- a/tests/integration_python/test_main_cli.py +++ b/tests/integration_python/test_main_cli.py @@ -18,6 +18,7 @@ cwd, verify_cli_command, CONDA_FORGE_CHANNEL, + find_commands_supporting_frozen_and_no_install, ) @@ -1583,3 +1584,134 @@ def test_fish_completions(pixi: Path, tmp_pixi_workspace: Path) -> None: f"source {fish_completion_file}", ], ) + + +@pytest.mark.slow +def test_frozen_no_install_invariant(pixi: Path, tmp_pixi_workspace: Path) -> None: + """Test that --frozen --no-install maintains lockfile invariant and keeps conda-meta empty. + This test is made up out of two parts: + + 1. This test verifies that when using --frozen --no-install flags together, the pixi.lock + file does not change and the conda-meta directory stays empty across all commands that + support these flags. + 2. It discovers all documented commands that support both --frozen and --no-install flags + and checks that they are included in the test. If any command is missing, it fails + with a message to add it to the test list. + """ + manifest_path = tmp_pixi_workspace / "pixi.toml" + lock_file_path = tmp_pixi_workspace / "pixi.lock" + conda_meta_path = tmp_pixi_workspace / ".pixi" / "envs" / "default" / "conda-meta" + + # Common flags for frozen no-install operations + frozen_no_install_flags: list[str | Path] = [ + "--manifest-path", + manifest_path, + "--frozen", + "--no-install", + ] + + # Create a new project with bzip2 (lightweight package) + verify_cli_command([pixi, "init", tmp_pixi_workspace], ExitCode.SUCCESS) + # Add bzip2 package to keep installation time low + verify_cli_command([pixi, "add", "--manifest-path", manifest_path, "bzip2"]) + + # Create a simple environment.yml file for import testing + simple_env_yml = tmp_pixi_workspace / "simple_env.yml" + simple_env_yml.write_text("""name: simple-env +channels: + - conda-forge +dependencies: + - sdl2 +""") + + # Store the original lockfile content + original_lock_content = lock_file_path.read_text() + + # Remove conda-meta folder to simulate something that would normally trigger an install + if conda_meta_path.exists(): + shutil.rmtree(conda_meta_path) + + # Helper function to check if the invariants hold after a command execution + def check_invariants(command_name: str) -> None: + # Check that lockfile hasn't changed + current_lock_content = lock_file_path.read_text() + assert current_lock_content == original_lock_content, ( + f"Lockfile changed after {command_name} with --frozen --no-install" + ) + + # Check that conda-meta directory stays empty/non-existent + assert not conda_meta_path.exists() or not any(conda_meta_path.iterdir()), ( + f"conda-meta directory not empty after {command_name} with --frozen --no-install" + ) + + # Test commands that properly respect --frozen --no-install + # Define commands as (command_parts, additional_args, command_name_for_invariants) + commands_to_test: list[tuple[list[str], list[str], str]] = [ + # Let's start with adding a workspace channel because that would trigger a re-solve in most cases + # Don't move this! + ( + ["workspace", "channel", "add"], + ["https://prefix.dev/bioconda"], + "pixi workspace channel add", + ), + ] + commands_to_test += [ + (["list"], [], "pixi list"), + (["tree"], [], "pixi tree"), + (["shell-hook"], [], "pixi shell-hook"), + # Special case: pixi shell uses --locked instead of --frozen and expects failure + (["shell"], [], "pixi shell"), + # Test manifest modifications with --frozen --no-install (these should work) + # Note: These modify manifest but not lockfile due to --frozen + (["add"], ["python"], "pixi add"), + (["remove"], ["python"], "pixi remove"), + (["run"], ["echo", "test"], "pixi run"), + # Export commands - use temporary directory + ( + ["workspace", "export", "conda-explicit-spec"], + [str(tmp_pixi_workspace / "export_test")], + "pixi workspace export conda-explicit-spec", + ), + # Upgrade commands + (["upgrade"], [], "pixi upgrade"), + ] + # This command needs to stay last so we always have something that requires a re-solve + # Dont move this! + commands_to_test.append( + ( + ["workspace", "channel", "remove"], + ["https://prefix.dev/bioconda"], + "pixi workspace channel remove", + ) + ) + + # Execute all commands and check invariants + for command_parts, additional_args, command_name in commands_to_test: + if command_name == "pixi shell": + # Special case: shell uses --locked instead of --frozen and expects failure + verify_cli_command( + [pixi, "shell", "--manifest-path", manifest_path, "--locked", "--no-install"], + expected_exit_code=ExitCode.FAILURE, + ) + else: + verify_cli_command([pixi, *command_parts, *frozen_no_install_flags, *additional_args]) + check_invariants(command_name) + + # Discover all commands that support --frozen and --no-install flags + supported_commands = find_commands_supporting_frozen_and_no_install() + + # Extract commands being tested from the test list + tested_commands = {command_name for _, _, command_name in commands_to_test} + + # Find commands that support the flags but aren't being tested + missing_commands = set(supported_commands) - tested_commands + + if missing_commands: + missing_list = "\n - ".join(sorted(missing_commands)) + pytest.fail( + f"Found {len(missing_commands)} command(s) that support --frozen --no-install " + f"but are not included in the test:\n - {missing_list}\n\n" + f"Please add these commands to the commands_to_test list in test_frozen_no_install_invariant " + f"to ensure comprehensive coverage.\n" + f"If you get here you know all commands that *are* supported correctly listen to --frozen and --no-install flags." + ) diff --git a/tests/integration_python/test_run_cli.py b/tests/integration_python/test_run_cli.py index 4687b77e41..adbf64d202 100644 --- a/tests/integration_python/test_run_cli.py +++ b/tests/integration_python/test_run_cli.py @@ -185,50 +185,6 @@ def test_using_prefix_validation( assert Path(file).exists() -def test_prefix_revalidation(pixi: Path, tmp_pixi_workspace: Path, dummy_channel_1: str) -> None: - manifest = tmp_pixi_workspace.joinpath("pixi.toml") - toml = f""" - [project] - name = "test" - channels = ["{dummy_channel_1}"] - platforms = ["linux-64", "osx-64", "osx-arm64", "win-64"] - - [dependencies] - dummy-a = "*" - """ - manifest.write_text(toml) - - # Run the installation - verify_cli_command( - [pixi, "install", "--manifest-path", manifest], - ) - - # Validate creation of the pixi file with the hash - pixi_file = default_env_path(tmp_pixi_workspace).joinpath("conda-meta").joinpath("pixi") - assert pixi_file.exists() - assert "environment_lock_file_hash" in pixi_file.read_text() - - # Break environment on purpose - dummy_a_meta_files = ( - default_env_path(tmp_pixi_workspace).joinpath("conda-meta").glob("dummy-a*.json") - ) - - for file in dummy_a_meta_files: - path = Path(file) - if path.exists(): - path.unlink() # Removes the file - - # Run with revalidation to force reinstallation - verify_cli_command( - [pixi, "run", "--manifest-path", manifest, "--revalidate", "echo", "hello"], - stdout_contains="hello", - ) - - # Validate that the dummy-a files are reinstalled - for file in dummy_a_meta_files: - assert Path(file).exists() - - def test_run_with_activation(pixi: Path, tmp_pixi_workspace: Path) -> None: manifest = tmp_pixi_workspace.joinpath("pixi.toml") toml = f""" diff --git a/tests/integration_rust/add_tests.rs b/tests/integration_rust/add_tests.rs index 09f4834e64..6bdeb41e56 100644 --- a/tests/integration_rust/add_tests.rs +++ b/tests/integration_rust/add_tests.rs @@ -11,7 +11,7 @@ use url::Url; use crate::common::{ LockFileExt, PixiControl, - builders::{HasDependencyConfig, HasLockFileUpdateConfig, HasPrefixUpdateConfig}, + builders::{HasDependencyConfig, HasLockFileUpdateConfig, HasNoInstallConfig}, package_database::{Package, PackageDatabase}, }; @@ -86,12 +86,14 @@ async fn add_with_channel() { pixi.init().no_fast_prefix_overwrite(true).await.unwrap(); pixi.add("conda-forge::py_rattler") - .without_lockfile_update() + .with_install(false) + .with_frozen(true) .await .unwrap(); pixi.add("https://prefix.dev/conda-forge::_r-mutex") - .without_lockfile_update() + .with_install(false) + .with_frozen(true) .await .unwrap(); @@ -876,7 +878,8 @@ preview = ['pixi-build']"#, // Add a package pixi.add("boost-check") .with_git_url(Url::parse("git+ssh://git@github.com/wolfv/pixi-build-examples.git").unwrap()) - .with_no_lockfile_update(true) + .with_install(false) + .with_frozen(true) .await .unwrap(); @@ -995,7 +998,8 @@ preview = ["pixi-build"] .add("boost-check") .with_git_url(Url::parse("https://github.com/wolfv/pixi-build-examples.git").unwrap()) .with_git_subdir("boost-check".to_string()) - .with_no_lockfile_update(true) + .with_install(false) + .with_frozen(true) .await; assert!(result.is_ok()); diff --git a/tests/integration_rust/common/builders.rs b/tests/integration_rust/common/builders.rs index ee400e8293..397d412f9c 100644 --- a/tests/integration_rust/common/builders.rs +++ b/tests/integration_rust/common/builders.rs @@ -23,10 +23,14 @@ //! } //! ``` -use pixi::cli::cli_config::{ - DependencyConfig, GitRev, LockFileUpdateConfig, PrefixUpdateConfig, WorkspaceConfig, +use pixi::cli::{ + add, + cli_config::{ + DependencyConfig, GitRev, LockFileUpdateConfig, NoInstallConfig, WorkspaceConfig, + }, + init, install, lock, remove, search, task, update, workspace, }; -use pixi::cli::lock; +use pixi_core::DependencyType; use std::{ future::{Future, IntoFuture}, io, @@ -36,8 +40,6 @@ use std::{ }; use futures::FutureExt; -use pixi::cli::{add, init, install, remove, search, task, update, workspace}; -use pixi_core::DependencyType; use pixi_manifest::{EnvironmentName, FeatureName, SpecType, task::Dependency}; use rattler_conda_types::{NamedChannelOrUrl, Platform, RepoDataRecord}; use url::Url; @@ -107,14 +109,13 @@ impl IntoFuture for InitBuilder { .boxed_local() } } -/// A trait used by AddBuilder and RemoveBuilder to set their inner -/// DependencyConfig -pub trait HasPrefixUpdateConfig: Sized { - fn prefix_update_config(&mut self) -> &mut PrefixUpdateConfig; +/// A trait used by builders to access NoInstallConfig +pub trait HasNoInstallConfig: Sized { + fn no_install_config(&mut self) -> &mut NoInstallConfig; /// Set whether to also install the environment. By default, the environment /// is NOT installed to reduce test times. fn with_install(mut self, install: bool) -> Self { - self.prefix_update_config().no_install = !install; + self.no_install_config().no_install = !install; self } } @@ -124,11 +125,9 @@ pub trait HasPrefixUpdateConfig: Sized { pub trait HasLockFileUpdateConfig: Sized { fn lock_file_update_config(&mut self) -> &mut LockFileUpdateConfig; - /// Skip updating lockfile, this will only check if it can add a - /// dependencies. If it can add it will only add it to the manifest. - /// Install will be skipped by default. - fn without_lockfile_update(mut self) -> Self { - self.lock_file_update_config().no_lockfile_update = true; + /// Set the frozen flag to skip lock-file updates + fn with_frozen(mut self, frozen: bool) -> Self { + self.lock_file_update_config().lock_file_usage.frozen = frozen; self } } @@ -231,8 +230,13 @@ impl AddBuilder { self } + /// Deprecated: Use .with_frozen(true).with_install(false) instead pub fn with_no_lockfile_update(mut self, no_lockfile_update: bool) -> Self { - self.args.lock_file_update_config.no_lockfile_update = no_lockfile_update; + if no_lockfile_update { + // Since no_lockfile_update is deprecated, we simulate the behavior by setting frozen=true and no_install=true + self.args.lock_file_update_config.lock_file_usage.frozen = true; + self.args.no_install_config.no_install = true; + } self } } @@ -243,9 +247,9 @@ impl HasDependencyConfig for AddBuilder { } } -impl HasPrefixUpdateConfig for AddBuilder { - fn prefix_update_config(&mut self) -> &mut PrefixUpdateConfig { - &mut self.args.prefix_update_config +impl HasNoInstallConfig for AddBuilder { + fn no_install_config(&mut self) -> &mut NoInstallConfig { + &mut self.args.no_install_config } } @@ -294,9 +298,9 @@ impl HasDependencyConfig for RemoveBuilder { } } -impl HasPrefixUpdateConfig for RemoveBuilder { - fn prefix_update_config(&mut self) -> &mut PrefixUpdateConfig { - &mut self.args.prefix_update_config +impl HasNoInstallConfig for RemoveBuilder { + fn no_install_config(&mut self) -> &mut NoInstallConfig { + &mut self.args.no_install_config } } diff --git a/tests/integration_rust/common/mod.rs b/tests/integration_rust/common/mod.rs index f594deb909..8df58acc41 100644 --- a/tests/integration_rust/common/mod.rs +++ b/tests/integration_rust/common/mod.rs @@ -17,7 +17,7 @@ use indicatif::ProgressDrawTarget; use miette::{Context, Diagnostic, IntoDiagnostic}; use pixi::cli::LockFileUsageConfig; use pixi::cli::cli_config::{ - ChannelsConfig, LockFileUpdateConfig, PrefixUpdateConfig, WorkspaceConfig, + ChannelsConfig, LockFileUpdateConfig, NoInstallConfig, WorkspaceConfig, }; use pixi::cli::{ add, @@ -365,11 +365,7 @@ impl PixiControl { manifest_path: Some(self.manifest_path()), }, dependency_config: AddBuilder::dependency_config_with_specs(specs), - prefix_update_config: PrefixUpdateConfig { - no_install: true, - - revalidate: false, - }, + no_install_config: NoInstallConfig { no_install: true }, lock_file_update_config: LockFileUpdateConfig { no_lockfile_update: false, lock_file_usage: LockFileUsageConfig::default(), @@ -404,10 +400,7 @@ impl PixiControl { manifest_path: Some(self.manifest_path()), }, dependency_config: AddBuilder::dependency_config_with_specs(vec![spec]), - prefix_update_config: PrefixUpdateConfig { - no_install: true, - revalidate: false, - }, + no_install_config: NoInstallConfig { no_install: true }, lock_file_update_config: LockFileUpdateConfig { no_lockfile_update: false, lock_file_usage: LockFileUsageConfig::default(), @@ -425,10 +418,7 @@ impl PixiControl { manifest_path: Some(self.manifest_path()), }, channel: vec![], - prefix_update_config: PrefixUpdateConfig { - no_install: true, - revalidate: false, - }, + no_install_config: NoInstallConfig { no_install: true }, lock_file_update_config: LockFileUpdateConfig { no_lockfile_update: false, lock_file_usage: LockFileUsageConfig::default(), @@ -450,10 +440,7 @@ impl PixiControl { manifest_path: Some(self.manifest_path()), }, channel: vec![], - prefix_update_config: PrefixUpdateConfig { - no_install: true, - revalidate: false, - }, + no_install_config: NoInstallConfig { no_install: true }, lock_file_update_config: LockFileUpdateConfig { no_lockfile_update: false, lock_file_usage: LockFileUsageConfig::default(), @@ -504,10 +491,11 @@ impl PixiControl { // Ensure the lock-file is up-to-date let lock_file = project .update_lock_file(UpdateLockFileOptions { - lock_file_usage: args.lock_file_update_config.lock_file_usage(), + lock_file_usage: args.lock_file_update_config.lock_file_usage().unwrap(), ..UpdateLockFileOptions::default() }) - .await?; + .await? + .0; // Create a task graph from the command line arguments. let search_env = SearchEnvironments::from_opt_env( @@ -616,6 +604,7 @@ impl PixiControl { Ok(project .update_lock_file(UpdateLockFileOptions::default()) .await? + .0 .into_lock_file()) } @@ -627,6 +616,7 @@ impl PixiControl { workspace_config: WorkspaceConfig { manifest_path: Some(self.manifest_path()), }, + no_install_config: NoInstallConfig { no_install: false }, check: false, json: false, }, diff --git a/tests/integration_rust/install_tests.rs b/tests/integration_rust/install_tests.rs index 28fb4c937e..147c38c56c 100644 --- a/tests/integration_rust/install_tests.rs +++ b/tests/integration_rust/install_tests.rs @@ -34,7 +34,7 @@ use uv_python::PythonEnvironment; use crate::common::{ LockFileExt, PixiControl, builders::{ - HasDependencyConfig, HasLockFileUpdateConfig, HasPrefixUpdateConfig, string_from_iter, + HasDependencyConfig, HasLockFileUpdateConfig, HasNoInstallConfig, string_from_iter, }, logging::try_init_test_subscriber, package_database::{Package, PackageDatabase}, @@ -204,7 +204,8 @@ async fn install_locked_with_config() { // Add new version of python only to the manifest pixi.add("python==3.9.0") - .without_lockfile_update() + .with_frozen(true) + .with_install(false) .await .unwrap(); @@ -278,7 +279,8 @@ async fn install_frozen() { // Add new version of python only to the manifest pixi.add("python==3.10.1") - .without_lockfile_update() + .with_frozen(true) + .with_install(false) .await .unwrap(); @@ -841,13 +843,13 @@ dependencies = [] [dependencies] python = "3.12.*" setuptools = ">=72,<73" - + [pypi-dependencies.package-b] path = "./package-b" [pypi-dependencies.package-tdjager] path = "./package-tdjager" - + "#, platform = current_platform, ); diff --git a/tests/integration_rust/solve_group_tests.rs b/tests/integration_rust/solve_group_tests.rs index 88a9c46e05..62d65aeb64 100644 --- a/tests/integration_rust/solve_group_tests.rs +++ b/tests/integration_rust/solve_group_tests.rs @@ -14,7 +14,7 @@ use url::Url; use crate::common::{ LockFileExt, PixiControl, - builders::{HasDependencyConfig, HasPrefixUpdateConfig}, + builders::{HasDependencyConfig, HasNoInstallConfig}, client::OfflineMiddleware, package_database::{Package, PackageDatabase}, }; diff --git a/tests/integration_rust/upgrade_tests.rs b/tests/integration_rust/upgrade_tests.rs index a44a0b1f43..df5d516e2d 100644 --- a/tests/integration_rust/upgrade_tests.rs +++ b/tests/integration_rust/upgrade_tests.rs @@ -44,8 +44,8 @@ async fn pypi_dependency_index_preserved_on_upgrade() { match_specs, pypi_deps, IndexMap::default(), - args.prefix_update_config.no_install, - &args.lock_file_update_config.lock_file_usage(), + args.no_install_config.no_install, + &args.lock_file_update_config.lock_file_usage().unwrap(), &args.specs.feature, &[], true,