diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 6f3b8906d..ff073bd7f 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -978,12 +978,13 @@ Allowing you to access it anywhere on your system without activating the environ - `--platform (-p)`: specify a platform that you want to install the package for. (default: current platform) - `--environment (-e)`: The environment to install the package into. (default: name of the tool) - `--expose `: A mapping from name to the binary to expose to the system. (default: name of the tool) +- `--with `: Add additional dependencies to the environment. Their executables will not be exposed. ```shell pixi global install ruff -# multiple packages can be installed at once +# Multiple packages can be installed at once pixi global install starship rattler-build -# specify the channel(s) +# Specify the channel(s) pixi global install --channel conda-forge --channel bioconda trackplot # Or in a more concise form pixi global install -c conda-forge -c bioconda trackplot @@ -997,8 +998,11 @@ pixi global install python=3.11.0=h10a6764_1_cpython # Install for a specific platform, only useful on osx-arm64 pixi global install --platform osx-64 ruff -# Install into a specific environment name -pixi global install --environment data-science python numpy matplotlib ipython +# Install a package with all its executables exposed, together with additional packages that don't expose anything +pixi global install ipython --with numpy --with scipy + +# Install into a specific environment name and expose all executables +pixi global install --environment data-science ipython jupyterlab numpy matplotlib # Expose the binary under a different name pixi global install --expose "py39=python3.9" "python=3.9.*" diff --git a/src/cli/global/add.rs b/src/cli/global/add.rs index d3be12807..b0494c6ed 100644 --- a/src/cli/global/add.rs +++ b/src/cli/global/add.rs @@ -75,7 +75,7 @@ pub async fn execute(args: Args) -> miette::Result<()> { state_changes |= project.sync_environment(env_name).await?; // Figure out added packages and their corresponding versions - state_changes |= project.added_packages(specs, env_name).await?; + state_changes |= project.added_packages(specs.as_ref(), env_name).await?; project.manifest.save().await?; diff --git a/src/cli/global/install.rs b/src/cli/global/install.rs index 6f5e59f22..9afee27a4 100644 --- a/src/cli/global/install.rs +++ b/src/cli/global/install.rs @@ -1,9 +1,8 @@ use clap::Parser; use fancy_display::FancyDisplay; -use indexmap::IndexMap; use itertools::Itertools; use miette::{Context, IntoDiagnostic}; -use rattler_conda_types::{MatchSpec, NamedChannelOrUrl, PackageName, Platform}; +use rattler_conda_types::{MatchSpec, NamedChannelOrUrl, Platform}; use crate::{ cli::{global::revert_environment_after_error, has_specs::HasSpecs}, @@ -50,6 +49,11 @@ pub struct Args { #[arg(long)] expose: Vec, + /// Add additional dependencies to the environment. + /// Their executables will not be exposed. + #[arg(long)] + with: Vec, + #[clap(flatten)] config: ConfigCli, @@ -82,7 +86,11 @@ pub async fn execute(args: Args) -> miette::Result<()> { let multiple_envs = env_names.len() > 1; if !args.expose.is_empty() && env_names.len() != 1 { - miette::bail!("Can't add exposed mappings for more than one environment"); + miette::bail!("Can't add exposed mappings with `--exposed` for more than one environment"); + } + + if !args.with.is_empty() && env_names.len() != 1 { + miette::bail!("Can't add packages with `--with` for more than one environment"); } let mut state_changes = StateChanges::default(); @@ -95,12 +103,17 @@ pub async fn execute(args: Args) -> miette::Result<()> { .clone() .into_iter() .filter(|(package_name, _)| env_name.as_str() == package_name.as_source()) - .collect() + .map(|(_, spec)| spec) + .collect_vec() } else { - specs.clone() + specs + .clone() + .into_iter() + .map(|(_, spec)| spec) + .collect_vec() }; let mut project = last_updated_project.clone(); - match setup_environment(env_name, &args, specs, &mut project) + match setup_environment(env_name, &args, &specs, &mut project) .await .wrap_err_with(|| format!("Couldn't install {}", env_name.fancy_display())) { @@ -142,7 +155,7 @@ pub async fn execute(args: Args) -> miette::Result<()> { async fn setup_environment( env_name: &EnvironmentName, args: &Args, - specs: IndexMap, + specs: &[MatchSpec], project: &mut Project, ) -> miette::Result { let mut state_changes = StateChanges::new_with_env(env_name.clone()); @@ -164,7 +177,7 @@ async fn setup_environment( } // Add the dependencies to the environment - for (_package_name, spec) in &specs { + for spec in specs { project.manifest.add_dependency( env_name, spec, @@ -187,14 +200,23 @@ async fn setup_environment( // Installing the environment to be able to find the bin paths later project.install_environment(env_name).await?; + let with_package_names = args + .with + .iter() + .map(|spec| { + spec.name + .clone() + .ok_or_else(|| miette::miette!("could not find package name in MatchSpec {}", spec)) + }) + .collect::>>()?; + // Sync exposed binaries - let expose_type = ExposedType::from_mappings(args.expose.clone()); + let expose_type = ExposedType::new(args.expose.clone(), with_package_names); project.sync_exposed_names(env_name, expose_type).await?; // Figure out added packages and their corresponding versions - let specs = specs.values().cloned().collect_vec(); - state_changes |= project.added_packages(specs.as_slice(), env_name).await?; + state_changes |= project.added_packages(specs, env_name).await?; // Expose executables of the new environment state_changes |= project diff --git a/src/cli/global/remove.rs b/src/cli/global/remove.rs index 0e33f625e..0fb6f4069 100644 --- a/src/cli/global/remove.rs +++ b/src/cli/global/remove.rs @@ -68,7 +68,9 @@ pub async fn execute(args: Args) -> miette::Result<()> { prefix .find_executables(&[record]) .into_iter() - .filter_map(|(name, _path)| ExposedName::from_str(name.as_str()).ok()) + .filter_map(|executable| { + ExposedName::from_str(executable.name.as_str()).ok() + }) .for_each(|exposed_name| { project .manifest diff --git a/src/global/common.rs b/src/global/common.rs index 87efb9c54..6b766f6d6 100644 --- a/src/global/common.rs +++ b/src/global/common.rs @@ -1,3 +1,5 @@ +use crate::prefix::Executable; + use super::{extract_executable_from_script, EnvironmentName, ExposedName, Mapping}; use ahash::HashSet; use console::StyledObject; @@ -539,10 +541,13 @@ pub(crate) async fn get_expose_scripts_sync_status( /// Check if all binaries were exposed, or if the user selected a subset of them. pub fn check_all_exposed( - env_binaries: &IndexMap>, + env_binaries: &IndexMap>, exposed_mapping_binaries: &IndexSet, ) -> bool { - let mut env_binaries_names_iter = env_binaries.values().flatten().map(|(name, _)| name); + let mut env_binaries_names_iter = env_binaries + .values() + .flatten() + .map(|executable| executable.name.clone()); let exposed_binaries_names: HashSet<&str> = exposed_mapping_binaries .iter() diff --git a/src/global/install.rs b/src/global/install.rs index f209a7c23..d3825adc8 100644 --- a/src/global/install.rs +++ b/src/global/install.rs @@ -1,7 +1,7 @@ use super::{EnvDir, EnvironmentName, ExposedName, StateChanges}; use crate::{ global::{BinDir, StateChange}, - prefix::Prefix, + prefix::{Executable, Prefix}, }; use fs_err::tokio as tokio_fs; use indexmap::{IndexMap, IndexSet}; @@ -36,21 +36,21 @@ use std::{collections::HashMap, path::PathBuf, str::FromStr}; pub(crate) fn script_exec_mapping<'a>( exposed_name: &ExposedName, entry_point: &str, - mut executables: impl Iterator, + mut executables: impl Iterator, bin_dir: &BinDir, env_dir: &EnvDir, ) -> miette::Result { executables - .find(|(executable_name, _)| *executable_name == entry_point) - .map(|(_, executable_path)| ScriptExecMapping { + .find(|executable| executable.name == entry_point) + .map(|executable| ScriptExecMapping { global_script_path: bin_dir.executable_script_path(exposed_name), - original_executable: executable_path.clone(), + original_executable: executable.path.clone(), }) .ok_or_else(|| { miette::miette!( "Couldn't find executable {entry_point} in {}, found these executables: {:?}", env_dir.path().display(), - executables.map(|(name, _)| name).collect_vec() + executables.map(|exec| exec.name.clone()).collect_vec() ) }) } @@ -352,7 +352,7 @@ pub(crate) fn local_environment_matches_spec( pub async fn find_binary_by_name( prefix: &Prefix, package_name: &PackageName, -) -> miette::Result> { +) -> miette::Result> { let installed_packages = prefix.find_installed_packages(None).await?; for package in &installed_packages { let executables = prefix.find_executables(&[package.clone()]); @@ -360,7 +360,7 @@ pub async fn find_binary_by_name( // Check if any of the executables match the package name if let Some(executable) = executables .iter() - .find(|(name, _)| name.as_str() == package_name.as_normalized()) + .find(|executable| executable.name.as_str() == package_name.as_normalized()) { return Ok(Some(executable.clone())); } diff --git a/src/global/project/manifest.rs b/src/global/project/manifest.rs index 15964723e..c0c1e01ef 100644 --- a/src/global/project/manifest.rs +++ b/src/global/project/manifest.rs @@ -12,7 +12,7 @@ use crate::global::project::ParsedEnvironment; use pixi_config::Config; use pixi_manifest::{PrioritizedChannel, TomlManifest}; use pixi_spec::PixiSpec; -use rattler_conda_types::{ChannelConfig, MatchSpec, NamedChannelOrUrl, Platform}; +use rattler_conda_types::{ChannelConfig, MatchSpec, NamedChannelOrUrl, PackageName, Platform}; use serde::{Deserialize, Serialize}; use toml_edit::{DocumentMut, Item}; @@ -472,19 +472,25 @@ impl FromStr for Mapping { pub enum ExposedType { #[default] All, - Subset(Vec), + Filter(Vec), + Mappings(Vec), } impl ExposedType { - pub fn from_mappings(mappings: Vec) -> Self { - match mappings.is_empty() { - true => Self::All, - false => Self::Subset(mappings), + pub fn new(mappings: Vec, filter: Vec) -> Self { + if !mappings.is_empty() { + return Self::Mappings(mappings); + } + + if filter.is_empty() { + Self::All + } else { + Self::Filter(filter) } } pub fn subset() -> Self { - Self::Subset(Default::default()) + Self::Mappings(Default::default()) } } diff --git a/src/global/project/mod.rs b/src/global/project/mod.rs index 585358a4f..63ced88cb 100644 --- a/src/global/project/mod.rs +++ b/src/global/project/mod.rs @@ -7,6 +7,7 @@ use crate::global::install::{ create_activation_script, create_executable_scripts, script_exec_mapping, }; use crate::global::project::environment::environment_specs_in_sync; +use crate::prefix::Executable; use crate::repodata::Repodata; use crate::rlimit::try_increase_rlimit_to_sensible; use crate::{ @@ -610,7 +611,7 @@ impl Project { pub async fn executables( &self, env_name: &EnvironmentName, - ) -> miette::Result>> { + ) -> miette::Result>> { let parsed_env = self .environment(env_name) .ok_or_else(|| miette::miette!("Environment {} not found", env_name.fancy_display()))?; @@ -628,7 +629,7 @@ impl Project { // We need to search for it in different packages. if !package_executables .iter() - .any(|(exec_name, _)| exec_name.as_str() == package_name.as_normalized()) + .any(|executable| executable.name.as_str() == package_name.as_normalized()) { if let Some(exec) = find_binary_by_name(&prefix, package_name).await? { package_executables.push(exec); @@ -661,17 +662,15 @@ impl Project { .environment(env_name) .ok_or_else(|| miette::miette!("Environment {} not found", env_name.fancy_display()))?; - // Find the exposed names that are no longer and remove them + // Find the exposed names that are no longer there and remove them let to_remove = environment .exposed .iter() .filter_map(|mapping| { // If the executable is still requested, do not remove the mapping - if env_executables - .values() - .flatten() - .any(|(_, path)| executable_from_path(path) == mapping.executable_name()) - { + if env_executables.values().flatten().any(|executable| { + executable_from_path(&executable.path) == mapping.executable_name() + }) { tracing::debug!("Not removing mapping to: {}", mapping.executable_name()); return None; } @@ -688,16 +687,42 @@ impl Project { // auto-expose the executables if necessary match expose_type { ExposedType::All => { - // Add new binaries that are not exposed - for (executable_name, _) in env_executables.values().flatten() { + // Add new binaries that are not yet exposed + let executable_names = env_executables + .into_iter() + .flat_map(|(_, executables)| executables) + .map(|executable| executable.name); + for executable_name in executable_names { + let mapping = Mapping::new( + ExposedName::from_str(&executable_name)?, + executable_name.to_string(), + ); + self.manifest.add_exposed_mapping(env_name, &mapping)?; + } + } + ExposedType::Filter(filter) => { + // Add new binaries that are not yet exposed and that come from one of the packages we filter on + let executable_names = env_executables + .into_iter() + .filter_map(|(package_name, executable)| { + if filter.contains(&package_name) { + Some(executable) + } else { + None + } + }) + .flatten() + .map(|executable| executable.name); + + for executable_name in executable_names { let mapping = Mapping::new( - ExposedName::from_str(executable_name)?, + ExposedName::from_str(&executable_name)?, executable_name.to_string(), ); self.manifest.add_exposed_mapping(env_name, &mapping)?; } } - ExposedType::Subset(mapping) => { + ExposedType::Mappings(mapping) => { // Expose only the requested binaries for mapping in mapping { self.manifest.add_exposed_mapping(env_name, &mapping)?; @@ -818,7 +843,7 @@ impl Project { let exposed_executables: Vec<_> = all_executables .iter() - .filter(|(name, _)| exposed.contains(name.as_str())) + .filter(|executable| exposed.contains(executable.name.as_str())) .cloned() .collect(); diff --git a/src/prefix.rs b/src/prefix.rs index 392d1c8c2..e6a4863e0 100644 --- a/src/prefix.rs +++ b/src/prefix.rs @@ -111,7 +111,7 @@ impl Prefix { /// Processes prefix records (that you can get by using `find_installed_packages`) /// to filter and collect executable files. - pub fn find_executables(&self, prefix_packages: &[PrefixRecord]) -> Vec<(String, PathBuf)> { + pub fn find_executables(&self, prefix_packages: &[PrefixRecord]) -> Vec { let executables = prefix_packages .iter() .flat_map(|record| { @@ -121,7 +121,10 @@ impl Prefix { .filter(|relative_path| self.is_executable(relative_path)) .filter_map(|path| { path.iter().last().and_then(OsStr::to_str).map(|name| { - (strip_executable_extension(name.to_string()), path.clone()) + Executable::new( + strip_executable_extension(name.to_string()), + path.clone(), + ) }) }) }) @@ -186,3 +189,15 @@ impl Prefix { .ok_or_else(|| miette::miette!("could not find {} in prefix", package_name.as_source())) } } + +#[derive(Debug, Clone)] +pub struct Executable { + pub name: String, + pub path: PathBuf, +} + +impl Executable { + fn new(name: String, path: PathBuf) -> Self { + Self { name, path } + } +} diff --git a/tests/integration/test_global.py b/tests/integration/test_global.py index 6189ab29f..e0ca077eb 100644 --- a/tests/integration/test_global.py +++ b/tests/integration/test_global.py @@ -807,7 +807,7 @@ def test_install_expose_multiple_packages(pixi: Path, tmp_path: Path, dummy_chan ], ExitCode.FAILURE, env=env, - stderr_contains="Can't add exposed mappings for more than one environment", + stderr_contains="Can't add exposed mappings with `--exposed` for more than one environment", ) # But it does work with multiple packages and a single environment