Skip to content
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
8ca2c1e
Add the ability to skip install of path dependencies
olivier-lacroix Feb 9, 2025
f3b1c15
Improve argument documentation
olivier-lacroix Feb 9, 2025
43476d9
Add integration test and doc
olivier-lacroix Feb 15, 2025
0f4dd2b
Merge branch 'main' into feat/no-path-dep
olivier-lacroix Feb 15, 2025
9a7c9df
fixes
olivier-lacroix Feb 15, 2025
5965f12
Test if pixi record is source
olivier-lacroix Feb 18, 2025
98bc642
Limit to pypi source
olivier-lacroix Feb 21, 2025
d5a45fa
Switch to --skip-local-sources
olivier-lacroix Feb 21, 2025
65c2ced
Update strings / doc
olivier-lacroix Feb 21, 2025
63e1628
Merge branch 'main' into feat/no-path-dep
olivier-lacroix Jul 25, 2025
61c19c7
fixes
olivier-lacroix Jul 25, 2025
d538e4e
Skip explicit packages
olivier-lacroix Jul 26, 2025
3a6c6cd
clid docs
olivier-lacroix Jul 26, 2025
8b8fe1e
Merge branch 'main' into feat/no-path-dep
olivier-lacroix Jul 26, 2025
193ac3e
Merge branch 'main' into feat/no-path-dep
olivier-lacroix Jul 30, 2025
f4722e5
Re-add conda test
olivier-lacroix Jul 30, 2025
28743e0
Remove --skip for reinstall
olivier-lacroix Jul 30, 2025
efb4558
Merge branch 'main' into feat/no-path-dep
tdejager Jul 31, 2025
440c736
Add function to get skipped packages
olivier-lacroix Aug 4, 2025
c3ae3a1
Merge branch 'main' into feat/no-path-dep
olivier-lacroix Aug 4, 2025
6569e6d
report
olivier-lacroix Aug 4, 2025
e563416
Add test on non-existent package being skipped
olivier-lacroix Aug 4, 2025
0f1d7fc
Filter at LockedPackageRef level
olivier-lacroix Aug 4, 2025
13db8a4
improve warning
olivier-lacroix Aug 4, 2025
4ddef55
improve message
olivier-lacroix Aug 4, 2025
00c2c21
Merge branch 'main' into feat/no-path-dep
olivier-lacroix Aug 4, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/reference/cli/pixi/install.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ pixi install [OPTIONS]
<br>May be provided more than once.
- <a id="arg---all" href="#arg---all">`--all (-a)`</a>
: Install all environments
- <a id="arg---skip" href="#arg---skip">`--skip <SKIP>`</a>
: 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
<br>May be provided more than once.

## Config Options
- <a id="arg---auth-file" href="#arg---auth-file">`--auth-file <AUTH_FILE>`</a>
Expand Down
51 changes: 35 additions & 16 deletions src/cli/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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")]
Comment thread
ruben-arts marked this conversation as resolved.
pub skip: Option<Vec<String>>,
}

pub async fn execute(args: Args) -> miette::Result<()> {
Expand Down Expand Up @@ -88,6 +94,7 @@ 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?;

Expand All @@ -97,28 +104,40 @@ pub async fn execute(args: Args) -> miette::Result<()> {
.collect::<Vec<_>>();

// 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 {
if !skip.is_empty() {
write!(&mut message, " excluding {}", skip.join(", ")).unwrap()
}
}

eprintln!("{}.", message);

Ok(())
}
1 change: 1 addition & 0 deletions src/cli/reinstall.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ pub async fn execute(args: Args) -> miette::Result<()> {
max_concurrent_solves: workspace.config().max_concurrent_solves(),
},
reinstall_packages.clone(),
&[],
)
.await?;

Expand Down
1 change: 1 addition & 0 deletions src/cli/remove.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ pub async fn execute(args: Args) -> miette::Result<()> {
max_concurrent_solves: workspace.config().max_concurrent_solves(),
},
ReinstallPackages::default(),
&[],
)
.await?;
}
Expand Down
1 change: 1 addition & 0 deletions src/cli/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,7 @@ pub async fn execute(args: Args) -> miette::Result<()> {
&executable_task.run_environment,
args.prefix_update_config.update_mode(),
&ReinstallPackages::default(),
&[],
)
.await?;

Expand Down
1 change: 1 addition & 0 deletions src/cli/shell.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
1 change: 1 addition & 0 deletions src/cli/shell_hook.rs
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ pub async fn execute(args: Args) -> miette::Result<()> {
max_concurrent_solves: workspace.config().max_concurrent_solves(),
},
ReinstallPackages::default(),
&[],
)
.await?;

Expand Down
1 change: 1 addition & 0 deletions src/cli/workspace/channel/add.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ pub async fn execute(args: AddRemoveArgs) -> miette::Result<()> {
max_concurrent_solves: workspace.workspace().config().max_concurrent_solves(),
},
ReinstallPackages::default(),
&[],
)
.await?;

Expand Down
1 change: 1 addition & 0 deletions src/cli/workspace/channel/remove.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()?;
Expand Down
1 change: 1 addition & 0 deletions src/cli/workspace/platform/add.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()?;
Expand Down
1 change: 1 addition & 0 deletions src/cli/workspace/platform/remove.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()?;
Expand Down
11 changes: 9 additions & 2 deletions src/environment/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,11 +111,15 @@ 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 {
for package in packages
// exclude skipped packages
.filter(|p| !skipped.iter().any(|s| s == p.name()))
{
// Always has the url or path
package.location().to_owned().to_string().hash(&mut hasher);

Expand Down Expand Up @@ -418,12 +422,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((
Expand All @@ -442,6 +448,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<Prefix>)> {
if environments.is_empty() {
return Err(miette::miette!("No environments provided to install."));
Expand Down Expand Up @@ -487,7 +494,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()
}
})
Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ pub mod environment;
mod global;
mod install_pypi;
pub mod lock_file;
mod prefix;
pub mod prefix;
Comment thread
tdejager marked this conversation as resolved.
mod prompt;
pub(crate) mod repodata;
pub mod task;
Expand Down
81 changes: 48 additions & 33 deletions src/lock_file/update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,7 @@ impl<'p> LockFileDerivedData<'p> {
fn locked_environment_hash(
&self,
environment: &Environment<'p>,
skipped: &[String],
) -> miette::Result<LockedEnvironmentHash> {
let locked_environment = self
.lock_file
Expand All @@ -313,6 +314,7 @@ impl<'p> LockFileDerivedData<'p> {
Ok(LockedEnvironmentHash::from_environment(
locked_environment,
environment.best_platform(),
skipped,
))
}

Expand All @@ -322,18 +324,21 @@ impl<'p> LockFileDerivedData<'p> {
environment: &Environment<'p>,
update_mode: UpdateMode,
reinstall_packages: &ReinstallPackages,
skipped: &[String],
) -> miette::Result<Prefix> {
// 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;
}
}

// 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.
Expand Down Expand Up @@ -403,6 +408,7 @@ impl<'p> LockFileDerivedData<'p> {
&self,
environment: &Environment<'p>,
reinstall_packages: &ReinstallPackages,
skipped: &[String],
) -> miette::Result<Prefix> {
let prefix_once_cell = self
.updated_pypi_prefixes
Expand All @@ -427,7 +433,7 @@ impl<'p> LockFileDerivedData<'p> {

let platform = environment.best_platform();
let pixi_records = self
.pixi_records(environment, platform)?
.pixi_records(environment, platform, skipped)?
.unwrap_or_default();

let conda_reinstall_packages = match reinstall_packages {
Expand All @@ -445,11 +451,11 @@ 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)?
.pypi_records(environment, platform, skipped)?
.unwrap_or_default();

// No `uv` support for WASM right now
Expand Down Expand Up @@ -553,58 +559,67 @@ impl<'p> LockFileDerivedData<'p> {
.cloned()
}

fn locked_env(
&self,
environment: &Environment<'p>,
) -> Result<rattler_lock::Environment<'_>, UpdateError> {
self.lock_file
.environment(environment.name().as_str())
.ok_or_else(|| UpdateError::LockFileMissingEnv(environment.name().clone()))
}

fn pypi_records(
&self,
environment: &Environment<'p>,
platform: Platform,
skipped: &[String],
) -> Result<Option<Vec<(PypiPackageData, PypiPackageEnvironmentData)>>, UpdateError> {
let locked_env = self
.lock_file
.environment(environment.name().as_str())
.ok_or_else(|| UpdateError::LockFileMissingEnv(environment.name().clone()))?;

let packages = locked_env.pypi_packages(platform);
Ok(packages.map(|iter| {
iter.map(|(data, env_data)| (data.clone(), env_data.clone()))
.collect()
}))
match self.locked_env(environment)?.pypi_packages(platform) {
Some(packages) => {
let records = packages
// exclude skipped packages
.filter(|(p, _)| !skipped.iter().any(|s| s == p.name.as_ref()))
.map(|(data, env_data)| (data.clone(), env_data.clone()))
.collect_vec();
Ok(Some(records))
}
None => Ok(None),
}
}

fn pypi_indexes(
&self,
environment: &Environment<'p>,
) -> Result<Option<PypiIndexes>, UpdateError> {
let locked_env = self
.lock_file
.environment(environment.name().as_str())
.ok_or_else(|| UpdateError::LockFileMissingEnv(environment.name().clone()))?;
Ok(locked_env.pypi_indexes().cloned())
Ok(self.locked_env(environment)?.pypi_indexes().cloned())
}

fn pixi_records(
&self,
environment: &Environment<'p>,
platform: Platform,
skipped: &[String],
) -> Result<Option<Vec<PixiRecord>>, UpdateError> {
let locked_env = self
.lock_file
.environment(environment.name().as_str())
.ok_or_else(|| UpdateError::LockFileMissingEnv(environment.name().clone()))?;

Ok(locked_env
.conda_packages(platform)
.map(|iter| {
iter.cloned()
match self.locked_env(environment)?.conda_packages(platform) {
Some(packages) => {
let records = packages
.cloned()
.map(PixiRecord::try_from)
.collect::<Result<Vec<_>, _>>()
})
.transpose()?)
.collect::<Result<Vec<_>, _>>()?
.into_iter()
.filter(|p| !skipped.iter().any(|s| s == p.name().as_normalized()))
.collect_vec();
Ok(Some(records))
}
None => Ok(None),
}
}

async fn conda_prefix(
&self,
environment: &Environment<'p>,
reinstall_packages: Option<HashSet<PackageName>>,
skipped: &[String],
) -> miette::Result<(Prefix, PythonStatus)> {
// If we previously updated this environment, early out.
let prefix_once_cell = self
Expand Down Expand Up @@ -632,7 +647,7 @@ impl<'p> LockFileDerivedData<'p> {

// Get the locked environment from the lock-file.
let records = self
.pixi_records(environment, platform)?
.pixi_records(environment, platform, skipped)?
.unwrap_or_default();
// Update the conda prefix
let CondaPrefixUpdated {
Expand Down
Loading
Loading