Skip to content

Commit

Permalink
feat: Add pixi global install --with
Browse files Browse the repository at this point in the history
Still need to add more docs and tests
  • Loading branch information
Hofer-Julian committed Oct 21, 2024
1 parent 91c3668 commit d11587a
Show file tree
Hide file tree
Showing 10 changed files with 129 additions and 50 deletions.
12 changes: 8 additions & 4 deletions docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -978,12 +978,13 @@ Allowing you to access it anywhere on your system without activating the environ
- `--platform <PLATFORM> (-p)`: specify a platform that you want to install the package for. (default: current platform)
- `--environment <ENVIRONMENT> (-e)`: The environment to install the package into. (default: name of the tool)
- `--expose <EXPOSE>`: A mapping from name to the binary to expose to the system. (default: name of the tool)
- `--with <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
Expand All @@ -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.*"
Expand Down
2 changes: 1 addition & 1 deletion src/cli/global/add.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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?;

Expand Down
44 changes: 33 additions & 11 deletions src/cli/global/install.rs
Original file line number Diff line number Diff line change
@@ -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},
Expand Down Expand Up @@ -50,6 +49,11 @@ pub struct Args {
#[arg(long)]
expose: Vec<Mapping>,

/// Add additional dependencies to the environment.
/// Their executables will not be exposed.
#[arg(long)]
with: Vec<MatchSpec>,

#[clap(flatten)]
config: ConfigCli,

Expand Down Expand Up @@ -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();
Expand All @@ -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()))
{
Expand Down Expand Up @@ -142,7 +155,7 @@ pub async fn execute(args: Args) -> miette::Result<()> {
async fn setup_environment(
env_name: &EnvironmentName,
args: &Args,
specs: IndexMap<PackageName, MatchSpec>,
specs: &[MatchSpec],
project: &mut Project,
) -> miette::Result<StateChanges> {
let mut state_changes = StateChanges::new_with_env(env_name.clone());
Expand All @@ -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,
Expand All @@ -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::<miette::Result<Vec<_>>>()?;

// 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
Expand Down
4 changes: 3 additions & 1 deletion src/cli/global/remove.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 7 additions & 2 deletions src/global/common.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use crate::prefix::Executable;

use super::{extract_executable_from_script, EnvironmentName, ExposedName, Mapping};
use ahash::HashSet;
use console::StyledObject;
Expand Down Expand Up @@ -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<PackageName, Vec<(String, PathBuf)>>,
env_binaries: &IndexMap<PackageName, Vec<Executable>>,
exposed_mapping_binaries: &IndexSet<Mapping>,
) -> 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()
Expand Down
16 changes: 8 additions & 8 deletions src/global/install.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down Expand Up @@ -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<Item = &'a (String, PathBuf)>,
mut executables: impl Iterator<Item = &'a Executable>,
bin_dir: &BinDir,
env_dir: &EnvDir,
) -> miette::Result<ScriptExecMapping> {
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()
)
})
}
Expand Down Expand Up @@ -352,15 +352,15 @@ pub(crate) fn local_environment_matches_spec(
pub async fn find_binary_by_name(
prefix: &Prefix,
package_name: &PackageName,
) -> miette::Result<Option<(String, PathBuf)>> {
) -> miette::Result<Option<Executable>> {
let installed_packages = prefix.find_installed_packages(None).await?;
for package in &installed_packages {
let executables = prefix.find_executables(&[package.clone()]);

// 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()));
}
Expand Down
20 changes: 13 additions & 7 deletions src/global/project/manifest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -472,19 +472,25 @@ impl FromStr for Mapping {
pub enum ExposedType {
#[default]
All,
Subset(Vec<Mapping>),
Filter(Vec<PackageName>),
Mappings(Vec<Mapping>),
}

impl ExposedType {
pub fn from_mappings(mappings: Vec<Mapping>) -> Self {
match mappings.is_empty() {
true => Self::All,
false => Self::Subset(mappings),
pub fn new(mappings: Vec<Mapping>, filter: Vec<PackageName>) -> 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())
}
}

Expand Down
51 changes: 38 additions & 13 deletions src/global/project/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -610,7 +611,7 @@ impl Project {
pub async fn executables(
&self,
env_name: &EnvironmentName,
) -> miette::Result<IndexMap<PackageName, Vec<(String, PathBuf)>>> {
) -> miette::Result<IndexMap<PackageName, Vec<Executable>>> {
let parsed_env = self
.environment(env_name)
.ok_or_else(|| miette::miette!("Environment {} not found", env_name.fancy_display()))?;
Expand All @@ -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);
Expand Down Expand Up @@ -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;
}
Expand All @@ -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)?;
Expand Down Expand Up @@ -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();

Expand Down
Loading

0 comments on commit d11587a

Please sign in to comment.