diff --git a/docs/reference/cli/pixi/install.md b/docs/reference/cli/pixi/install.md
index c897f0a641..c23aacdf75 100644
--- a/docs/reference/cli/pixi/install.md
+++ b/docs/reference/cli/pixi/install.md
@@ -17,6 +17,9 @@ pixi install [OPTIONS]
May be provided more than once.
- `--all (-a)`
: Install all environments
+- `--skip `
+: Skip installation of specific packages present in the lockfile. Requires --frozen. This can be useful for instance in a Dockerfile to skip local source dependencies when installing dependencies
+
May be provided more than once.
## Config Options
- `--auth-file `
diff --git a/src/cli/install.rs b/src/cli/install.rs
index fa0906171e..6141c98299 100644
--- a/src/cli/install.rs
+++ b/src/cli/install.rs
@@ -2,6 +2,7 @@ use clap::Parser;
use fancy_display::FancyDisplay;
use itertools::Itertools;
use pixi_config::ConfigCli;
+use std::fmt::Write;
use crate::{
UpdateLockFileOptions, WorkspaceLocator,
@@ -47,6 +48,11 @@ pub struct Args {
/// Install all environments
#[arg(long, short, conflicts_with = "environment")]
pub all: bool,
+
+ /// Skip installation of specific packages present in the lockfile. Requires --frozen.
+ /// This can be useful for instance in a Dockerfile to skip local source dependencies when installing dependencies.
+ #[arg(long, requires = "frozen")]
+ pub skip: Option>,
}
pub async fn execute(args: Args) -> miette::Result<()> {
@@ -79,7 +85,7 @@ pub async fn execute(args: Args) -> miette::Result<()> {
.collect::, _>>()?;
// Update the prefixes by installing all packages
- get_update_lock_file_and_prefixes(
+ let (lock_file, _) = get_update_lock_file_and_prefixes(
&environments,
UpdateMode::Revalidate,
UpdateLockFileOptions {
@@ -88,37 +94,68 @@ pub async fn execute(args: Args) -> miette::Result<()> {
max_concurrent_solves: workspace.config().max_concurrent_solves(),
},
ReinstallPackages::default(),
+ &args.skip.clone().unwrap_or_default(),
)
.await?;
let installed_envs = environments
- .into_iter()
+ .iter()
.map(|env| env.name())
.collect::>();
// 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()
- };
+ let mut message = console::style(console::Emoji("✔ ", "")).green().to_string();
if installed_envs.len() == 1 {
- eprintln!(
- "{}The {} environment has been installed{}.",
- console::style(console::Emoji("✔ ", "")).green(),
+ write!(
+ &mut message,
+ "The {} environment has been installed",
installed_envs[0].fancy_display(),
- detached_envs_message
- );
+ )
+ .unwrap();
} else {
- eprintln!(
- "{}The following environments have been installed: {}\t{}",
- console::style(console::Emoji("✔ ", "")).green(),
+ write!(
+ &mut message,
+ "The following environments have been installed: {}",
installed_envs.iter().map(|n| n.fancy_display()).join(", "),
- detached_envs_message
- );
+ )
+ .unwrap();
+ }
+
+ if let Ok(Some(path)) = workspace.config().detached_environments().path() {
+ write!(
+ &mut message,
+ " in '{}'",
+ console::style(path.display()).bold()
+ )
+ .unwrap()
}
+ if let Some(skip) = &args.skip {
+ let mut all_skipped_packages = std::collections::HashSet::new();
+ for env in &environments {
+ let skipped_packages = lock_file.get_skipped_package_names(env, skip)?;
+ all_skipped_packages.extend(skipped_packages);
+ }
+
+ if !all_skipped_packages.is_empty() {
+ let mut skipped_packages_vec: Vec<_> = all_skipped_packages.into_iter().collect();
+ skipped_packages_vec.sort();
+ write!(
+ &mut message,
+ " excluding '{}'",
+ skipped_packages_vec.join("', '")
+ )
+ .unwrap();
+ } else {
+ tracing::warn!(
+ "No packages were skipped. '{}' did not match any packages in the lockfile.",
+ skip.join("', '")
+ );
+ }
+ }
+
+ eprintln!("{}.", message);
+
Ok(())
}
diff --git a/src/cli/reinstall.rs b/src/cli/reinstall.rs
index 5a2e2e2d61..84a031ccaa 100644
--- a/src/cli/reinstall.rs
+++ b/src/cli/reinstall.rs
@@ -86,6 +86,7 @@ pub async fn execute(args: Args) -> miette::Result<()> {
max_concurrent_solves: workspace.config().max_concurrent_solves(),
},
reinstall_packages.clone(),
+ &[],
)
.await?;
diff --git a/src/cli/remove.rs b/src/cli/remove.rs
index 7b64e309d9..752c7602fb 100644
--- a/src/cli/remove.rs
+++ b/src/cli/remove.rs
@@ -125,6 +125,7 @@ pub async fn execute(args: Args) -> miette::Result<()> {
max_concurrent_solves: workspace.config().max_concurrent_solves(),
},
ReinstallPackages::default(),
+ &[],
)
.await?;
}
diff --git a/src/cli/run.rs b/src/cli/run.rs
index bf907d1216..dbfc8eb585 100644
--- a/src/cli/run.rs
+++ b/src/cli/run.rs
@@ -259,6 +259,7 @@ pub async fn execute(args: Args) -> miette::Result<()> {
&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 86ea937484..0549885421 100644
--- a/src/cli/shell.rs
+++ b/src/cli/shell.rs
@@ -288,6 +288,7 @@ pub async fn execute(args: Args) -> miette::Result<()> {
max_concurrent_solves: workspace.config().max_concurrent_solves(),
},
ReinstallPackages::default(),
+ &[],
)
.await?;
let lock_file = lock_file_data.into_lock_file();
diff --git a/src/cli/shell_hook.rs b/src/cli/shell_hook.rs
index 171cf428f9..640db5c0d0 100644
--- a/src/cli/shell_hook.rs
+++ b/src/cli/shell_hook.rs
@@ -168,6 +168,7 @@ pub async fn execute(args: Args) -> miette::Result<()> {
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 bb08b0f7ec..d5ee7550f9 100644
--- a/src/cli/workspace/channel/add.rs
+++ b/src/cli/workspace/channel/add.rs
@@ -32,6 +32,7 @@ pub async fn execute(args: AddRemoveArgs) -> miette::Result<()> {
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 8b12116fbf..8eba053ebc 100644
--- a/src/cli/workspace/channel/remove.rs
+++ b/src/cli/workspace/channel/remove.rs
@@ -30,6 +30,7 @@ pub async fn execute(args: AddRemoveArgs) -> miette::Result<()> {
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 427a8018d0..88aae34fec 100644
--- a/src/cli/workspace/platform/add.rs
+++ b/src/cli/workspace/platform/add.rs
@@ -57,6 +57,7 @@ pub async fn execute(workspace: Workspace, args: Args) -> miette::Result<()> {
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 78e1266b24..a4e9520d6a 100644
--- a/src/cli/workspace/platform/remove.rs
+++ b/src/cli/workspace/platform/remove.rs
@@ -46,6 +46,7 @@ pub async fn execute(workspace: Workspace, args: Args) -> miette::Result<()> {
max_concurrent_solves: workspace.workspace().config().max_concurrent_solves(),
},
ReinstallPackages::default(),
+ &[],
)
.await?;
workspace.save().await.into_diagnostic()?;
diff --git a/src/environment/mod.rs b/src/environment/mod.rs
index 8b8979fc8c..a6899b892c 100644
--- a/src/environment/mod.rs
+++ b/src/environment/mod.rs
@@ -111,28 +111,29 @@ impl LockedEnvironmentHash {
pub(crate) fn from_environment(
environment: rattler_lock::Environment,
platform: Platform,
+ skipped: &[String],
) -> Self {
let mut hasher = Xxh3::new();
- if let Some(packages) = environment.packages(platform) {
- for package in packages {
- // Always has the url or path
- package.location().to_owned().to_string().hash(&mut hasher);
-
- match package {
- // A select set of fields are used to hash the package
- LockedPackageRef::Conda(pack) => {
- if let Some(sha) = pack.record().sha256 {
- sha.hash(&mut hasher);
- } else if let Some(md5) = pack.record().md5 {
- md5.hash(&mut hasher);
- }
- }
- LockedPackageRef::Pypi(pack, env) => {
- pack.editable.hash(&mut hasher);
- env.extras.hash(&mut hasher);
+ for package in
+ LockFileDerivedData::filter_skipped_packages(environment.packages(platform), skipped)
+ {
+ // Always has the url or path
+ package.location().to_owned().to_string().hash(&mut hasher);
+
+ match package {
+ // A select set of fields are used to hash the package
+ LockedPackageRef::Conda(pack) => {
+ if let Some(sha) = pack.record().sha256 {
+ sha.hash(&mut hasher);
+ } else if let Some(md5) = pack.record().md5 {
+ md5.hash(&mut hasher);
}
}
+ LockedPackageRef::Pypi(pack, env) => {
+ pack.editable.hash(&mut hasher);
+ env.extras.hash(&mut hasher);
+ }
}
}
@@ -418,12 +419,14 @@ pub async fn get_update_lock_file_and_prefix<'env>(
update_mode: UpdateMode,
update_lock_file_options: UpdateLockFileOptions,
reinstall_packages: ReinstallPackages,
+ skipped: &[String],
) -> miette::Result<(LockFileDerivedData<'env>, Prefix)> {
let (lock_file, prefixes) = get_update_lock_file_and_prefixes(
&[environment.clone()],
update_mode,
update_lock_file_options,
reinstall_packages,
+ skipped,
)
.await?;
Ok((
@@ -442,6 +445,7 @@ pub async fn get_update_lock_file_and_prefixes<'env>(
update_mode: UpdateMode,
update_lock_file_options: UpdateLockFileOptions,
reinstall_packages: ReinstallPackages,
+ skipped: &[String],
) -> miette::Result<(LockFileDerivedData<'env>, Vec)> {
if environments.is_empty() {
return Err(miette::miette!("No environments provided to install."));
@@ -487,7 +491,7 @@ pub async fn get_update_lock_file_and_prefixes<'env>(
std::future::ready(Ok(Prefix::new(env.dir()))).left_future()
} else {
lock_file_ref
- .prefix(env, update_mode, reinstall_packages)
+ .prefix(env, update_mode, reinstall_packages, skipped)
.right_future()
}
})
diff --git a/src/lib.rs b/src/lib.rs
index cad84fc472..c5a28aad16 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -7,7 +7,7 @@ pub mod environment;
mod global;
mod install_pypi;
pub mod lock_file;
-mod prefix;
+pub mod prefix;
mod prompt;
pub(crate) mod repodata;
pub mod task;
diff --git a/src/lock_file/update.rs b/src/lock_file/update.rs
index 72511c5269..210a83551d 100644
--- a/src/lock_file/update.rs
+++ b/src/lock_file/update.rs
@@ -43,9 +43,7 @@ use pypi_mapping::{self, MappingClient};
use pypi_modifiers::pypi_marker_env::determine_marker_environment;
use rattler::package_cache::PackageCache;
use rattler_conda_types::{Arch, GenericVirtualPackage, PackageName, Platform};
-use rattler_lock::{
- LockFile, ParseCondaLockError, PypiIndexes, PypiPackageData, PypiPackageEnvironmentData,
-};
+use rattler_lock::{LockFile, LockedPackageRef, ParseCondaLockError};
use std::{
cmp::PartialEq,
collections::{HashMap, HashSet},
@@ -305,6 +303,7 @@ impl<'p> LockFileDerivedData<'p> {
fn locked_environment_hash(
&self,
environment: &Environment<'p>,
+ skipped: &[String],
) -> miette::Result {
let locked_environment = self
.lock_file
@@ -313,6 +312,7 @@ impl<'p> LockFileDerivedData<'p> {
Ok(LockedEnvironmentHash::from_environment(
locked_environment,
environment.best_platform(),
+ skipped,
))
}
@@ -322,10 +322,11 @@ impl<'p> LockFileDerivedData<'p> {
environment: &Environment<'p>,
update_mode: UpdateMode,
reinstall_packages: &ReinstallPackages,
+ skipped: &[String],
) -> miette::Result {
// Check if the prefix is already up-to-date by validating the hash with the
// environment file
- let hash = self.locked_environment_hash(environment)?;
+ let hash = self.locked_environment_hash(environment, skipped)?;
if update_mode == UpdateMode::QuickValidate {
if let Some(prefix) = self.cached_prefix(environment, &hash) {
return prefix;
@@ -333,7 +334,9 @@ impl<'p> LockFileDerivedData<'p> {
}
// Get the up-to-date prefix
- let prefix = self.update_prefix(environment, reinstall_packages).await?;
+ let prefix = self
+ .update_prefix(environment, reinstall_packages, skipped)
+ .await?;
// Save an environment file to the environment directory after the update.
// Avoiding writing the cache away before the update is done.
@@ -403,6 +406,7 @@ impl<'p> LockFileDerivedData<'p> {
&self,
environment: &Environment<'p>,
reinstall_packages: &ReinstallPackages,
+ skipped: &[String],
) -> miette::Result {
let prefix_once_cell = self
.updated_pypi_prefixes
@@ -426,9 +430,22 @@ impl<'p> LockFileDerivedData<'p> {
))?;
let platform = environment.best_platform();
- let pixi_records = self
- .pixi_records(environment, platform)?
- .unwrap_or_default();
+ let locked_env = self.locked_env(environment)?;
+ let packages =
+ Self::filter_skipped_packages(locked_env.packages(platform), skipped);
+
+ // Separate the packages into conda and pypi packages
+ let (conda_packages, pypi_packages) = packages
+ .into_iter()
+ .partition::, _>(|p| p.as_conda().is_some());
+
+ let pixi_records = locked_packages_to_pixi_records(conda_packages)?;
+
+ let pypi_records = pypi_packages
+ .into_iter()
+ .filter_map(LockedPackageRef::as_pypi)
+ .map(|(data, env_data)| (data.clone(), env_data.clone()))
+ .collect::>();
let conda_reinstall_packages = match reinstall_packages {
ReinstallPackages::None => None,
@@ -445,13 +462,9 @@ impl<'p> LockFileDerivedData<'p> {
// Get the prefix with the conda packages installed.
let (prefix, python_status) = self
- .conda_prefix(environment, conda_reinstall_packages)
+ .conda_prefix(environment, conda_reinstall_packages, skipped)
.await?;
- let pypi_records = self
- .pypi_records(environment, platform)?
- .unwrap_or_default();
-
// No `uv` support for WASM right now
if platform.arch() == Some(Arch::Wasm32) {
return Ok(prefix);
@@ -508,7 +521,7 @@ impl<'p> LockFileDerivedData<'p> {
// Update the prefix with Pypi records
{
- let pypi_indexes = self.pypi_indexes(environment)?;
+ let pypi_indexes = self.locked_env(environment)?.pypi_indexes().cloned();
let config = PyPIUpdateConfig {
environment_name: environment.name(),
@@ -553,58 +566,36 @@ impl<'p> LockFileDerivedData<'p> {
.cloned()
}
- fn pypi_records(
+ fn locked_env(
&self,
environment: &Environment<'p>,
- platform: Platform,
- ) -> Result