Skip to content

feat: ability to install package subsets#4404

Merged
ruben-arts merged 49 commits intoprefix-dev:mainfrom
tdejager:feat/install-build-partial-prefix
Aug 29, 2025
Merged

feat: ability to install package subsets#4404
ruben-arts merged 49 commits intoprefix-dev:mainfrom
tdejager:feat/install-build-partial-prefix

Conversation

@tdejager
Copy link
Contributor

@tdejager tdejager commented Aug 20, 2025

Overview

This Introduces install filtering to control what from the lockfile is installed. Adds new CLI options to skip packages, skip with full dependency subtree, or target a single package.

Why?

In some cases it might be desirable to skip part of the installation, e.g. for docker builds. Or you could be working on a pixi build project and only test part of the build changes. This unifies the behavior that was introduced with: #3092 by @olivier-lacroix

@ruben-arts noticed, that especially when also developing build backends it can be really annoying if you are only interested in a certain packages that it still tries to build others.

This kind of ties in with a new philosophy that we are trying out where we allow the user to play with the environment a bit more as long as you are able to recover.

When running these commands without --frozen, it can be the case that the lock file no longer reflects the environment changes.

CLI Changes

  • New pixi install options:
    • --skip <NAME>: soft skip; omit package but still install its deps.
    • --skip-with-deps <NAME>: hard skip; omit package and its dependency subtree unless reachable via another root.
    • --package : install/build only the target package and its deps.

Some examples:

  • pixi install --skip foo
  • pixi install --skip-with-deps torch
  • pixi install --package numpy
  • Combine: pixi install --package app --skip-with-deps dev-docs --skip linters

How was this tested?

  • New Rust integration tests: tests/integration_rust/install_filter_tests.rs covering:
    • soft skip, hard skip, target-only, combinations.

What was skipped.

  • PyPI filtering is unused until we implement ignore for the PyPI installer as well.

Notes for the reviewer

  • Core filtering:
    • New InstallSubset in crates/pixi_core/src/lock_file/install_subset.rs builds a reachability graph over lockfile packages and applies:
    • stop set: skip_with_deps
    • passthrough set: skip (direct skip, traverse deps)
    • optional target root: --only
  • Public API:
    • InstallFilter in pixi_core::environment threads skip/target options through install/update paths.
    • get_skipped_package_names to surface which packages were excluded.
  • Prefix updates:
    • update_prefix filters lockfile packages before installing.
    • Conda: passes ignored names to prefix updater to avoid installing skipped packages.

@tdejager tdejager changed the title wip: working on creating package filtering feat: working on creating package filtering Aug 26, 2025
@tdejager
Copy link
Contributor Author

Okay did a final user test myself on the ros_worskpace and fixed some annoyances that I encountered. Mainly with the messaging.

Things I added in the messaging:

  • --only filter
    • “Installing foo in env bar, including 23 packages”
  • Few skips (<=5)
    • “Installing … excluding 'a', 'b', 'c'”
  • Many skips (>5), with matched args
    • “Installing … excluding 'a', 'b' and 17 other packages”
  • Many skips (>5), but none of the args are directly listed
    • “Installing … excluding 19 other packages”
  • Unmatched skip args
    • Warning: “The skipped arg(s) 'foo, bar' did not match any packages in the lock file”
  • No packages skipped
    • “Installing … no packages were skipped (check if cli args were correct)”

@tdejager tdejager requested a review from ruben-arts August 28, 2025 10:26
pub only: Option<String>,
}

const SKIP_CUTOFF: usize = 5;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This just for the display method.

Comment on lines +56 to +62
/// Skip a package and its entire dependency subtree. This performs a hard exclusion: the package and its dependencies are not installed unless reachable from another non-skipped root.
#[arg(long)]
pub skip_with_deps: Option<Vec<String>>,

/// Install and build only this package and its dependencies
#[arg(long)]
pub only: Option<String>,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added these CLI args.

Comment on lines +97 to +100
let filter = InstallFilter::new()
.skip_direct(args.skip.clone().unwrap_or_default())
.skip_with_deps(args.skip_with_deps.clone().unwrap_or_default())
.target_package(args.only.clone());
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here you see an example of setting an install filter.

);
}

if !all_skipped_packages.is_empty() {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This includes all the messaging, if we ever add the flag somewhere else we should consider reusing this.

.with_download_client(command_dispatcher.download_client().clone())
.with_package_cache(command_dispatcher.package_cache().clone())
.with_reinstall_packages(self.force_reinstall)
.with_ignored_packages(self.ignore_packages.unwrap_or_default())
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the new rattler feature that ignores installed packages.

@@ -0,0 +1,458 @@
use itertools::Itertools;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This the file that contains the graph reachability stuff and is kind of the meat of this feature.

}
}

/// Filters packages using two skip modes and optional target selection.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Read this here carefully.


#[cfg(test)]
mod tests {
use super::*;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added simple unit tests to verify this behavior.

.partition::<Vec<_>, _>(|p| p.as_conda().is_some());

// TODO: use the same ignore logic for PyPI
let (ignored_conda, _ignored_pypi) = ignored
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've kept the behavior as before for PyPI. So that if you do:

pixi install --frozen --skip foobar`

It works like before, but we need to improve that, because without --frozen first this was required it might work unintuitively.

@@ -0,0 +1,205 @@
use pixi_core::{InstallFilter, UpdateLockFileOptions};
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This contains some integration tests, with an e2e test to check if all works as expected.

@tdejager tdejager changed the title feat: working on creating package filtering feat: ability to install package subsets Aug 28, 2025
Copy link
Contributor Author

@tdejager tdejager left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My own comments

@ruben-arts
Copy link
Contributor

Found an easter egg:

> cd examples/pypi
> pixi i --only plot-antenna

✔ The default environment has been installed, including 70 packages excluding 18446744073709551591 other packages.

@ruben-arts
Copy link
Contributor

I see only can't be defined multiple times, is there a technical reason for that?

.partition::<Vec<_>, _>(|p| p.as_conda().is_some());

// TODO: use the same ignore logic for PyPI
let (ignored_conda, _ignored_pypi) = ignored
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently it also skips python if I do pixi install --only pypi-dependency since python is not a dependency. We need to always install python when we install only a pypi-dependency.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just made all of this work.

@tdejager
Copy link
Contributor Author

I see only can't be defined multiple times, is there a technical reason for that?

There is, lets pick it up in a subsequent PR.

Copy link
Contributor

@ruben-arts ruben-arts left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really cool! Thanks Tim

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants