Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: pixi exec --list #3311

Merged
merged 7 commits into from
Mar 11, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions docs/reference/cli/pixi/exec.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ pixi exec [OPTIONS] [COMMAND]...
: Max concurrent solves, default is the number of CPUs
- <a id="arg---force-reinstall" href="#arg---force-reinstall">`--force-reinstall`</a>
: If specified a new environment is always created even if one already exists
- <a id="arg---list" href="#arg---list">`--list <LIST>`</a>
: Before executing the command, list packages in the environment Specify `--list=some_regex` to filter the shown packages
- <a id="arg---platform" href="#arg---platform">`--platform (-p) <PLATFORM>`</a>
: The platform to create the environment for
<br>**default**: `current_platform`
Expand Down
731 changes: 362 additions & 369 deletions pixi.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions pixi.toml
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ git = ">=2.46.0,<3"
openssl = "3.*"
pkg-config = "0.29.*"
rust = "==1.84.0"
rust-src = ">=1.84.0,<2"

[feature.build.target.linux-64.dependencies]
clang = ">=18.1.8,<19.0"
Expand Down
70 changes: 67 additions & 3 deletions src/cli/exec.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::{path::Path, str::FromStr, sync::LazyLock};

use clap::{Parser, ValueHint};
use itertools::Itertools;
use miette::{Context, IntoDiagnostic};
use pixi_config::{self, Config, ConfigCli};
use pixi_progress::{await_in_progress, global_multi_progress, wrap_in_progress};
Expand All @@ -16,7 +17,10 @@ use reqwest_middleware::ClientWithMiddleware;
use uv_configuration::RAYON_INITIALIZE;

use super::cli_config::ChannelsConfig;
use crate::prefix::Prefix;
use crate::{
environment::list::{print_package_table, PackageToOutput},
prefix::Prefix,
};

/// Run a command and install it in a temporary environment.
///
Expand Down Expand Up @@ -45,6 +49,11 @@ pub struct Args {
#[clap(long)]
pub force_reinstall: bool,

/// Before executing the command, list packages in the environment
/// Specify `--list=some_regex` to filter the shown packages
#[clap(long = "list", num_args = 0..=1, default_missing_value = "", require_equals = true)]
pub list: Option<String>,

#[clap(flatten)]
pub config: ConfigCli,
}
Expand Down Expand Up @@ -180,9 +189,10 @@ pub async fn create_exec_prefix(
.unwrap_or(prefix.root())
.display()
);
let specs_clone = specs.clone();
let solved_records = wrap_in_progress("solving environment", move || {
Solver.solve(SolverTask {
specs,
specs: specs_clone,
virtual_packages,
..SolverTask::from_iter(&repodata)
})
Expand All @@ -207,15 +217,69 @@ pub async fn create_exec_prefix(
.with_package_cache(PackageCache::new(
cache_dir.join(pixi_consts::consts::CONDA_PACKAGE_CACHE_DIR),
))
.install(prefix.root(), solved_records.records)
.install(prefix.root(), solved_records.records.clone())
.await
.into_diagnostic()
.context("failed to create environment")?;

write_guard.finish().await.into_diagnostic()?;

if let Some(ref regex) = args.list {
list_exec_environment(specs, solved_records, regex.clone())?;
}

Ok(prefix)
}

fn list_exec_environment(
specs: Vec<MatchSpec>,
solved_records: rattler_conda_types::SolverResult,
regex: String,
) -> Result<(), miette::Error> {
let regex = {
if regex.is_empty() {
None
} else {
Some(regex)
}
};
let mut packages_to_output = solved_records
.records
.iter()
.map(|record| {
PackageToOutput::new(
&record.package_record,
specs
.clone()
.into_iter()
.filter_map(|spec| spec.name) // Extract the name if it exists
.collect_vec()
.contains(&record.package_record.name),
)
})
.collect_vec();
if let Some(ref regex) = regex {
let regex = regex::Regex::new(regex).into_diagnostic()?;
packages_to_output.retain(|package| regex.is_match(package.name.as_normalized()));
}
let output_message = if let Some(ref regex) = regex {
format!(
"The environment has {} packages filtered by regex `{}`:",
console::style(packages_to_output.len()).bold(),
regex
)
} else {
format!(
"The environment has {} packages:",
console::style(packages_to_output.len()).bold()
)
};
packages_to_output.sort_by(|a, b| a.name.cmp(&b.name));
println!("{}", output_message);
print_package_table(packages_to_output).into_diagnostic()?;
Ok(())
}

/// This function is used to guess the package name from the command.
fn guess_package_spec(command: &str) -> MatchSpec {
// Replace any illegal character with a dash.
Expand Down
4 changes: 2 additions & 2 deletions src/cli/global/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use crate::{
global::{
self,
common::{contains_menuinst_document, NotChangedReason},
list::list_global_environments,
list::list_all_global_environments,
project::ExposedType,
EnvChanges, EnvState, EnvironmentName, Mapping, Project, StateChange, StateChanges,
},
Expand Down Expand Up @@ -149,7 +149,7 @@ pub async fn execute(args: Args) -> miette::Result<()> {
}

// After installing, we always want to list the changed environments
list_global_environments(
list_all_global_environments(
&last_updated_project,
Some(env_names),
Some(&env_changes),
Expand Down
8 changes: 5 additions & 3 deletions src/cli/global/list.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use crate::global::list::{list_environment, list_global_environments, GlobalSortBy};
use crate::global::list::{
list_all_global_environments, list_specific_global_environment, GlobalSortBy,
};
use crate::global::{EnvironmentName, Project};
use clap::Parser;
use fancy_display::FancyDisplay;
Expand Down Expand Up @@ -50,13 +52,13 @@ pub async fn execute(args: Args) -> miette::Result<()> {
tracing::warn!("The environment {} is not in sync with the manifest, to sync run\n\tpixi global sync", env_name.fancy_display());
}

list_environment(&project, &env_name, args.sort_by, args.regex).await?;
list_specific_global_environment(&project, &env_name, args.sort_by, args.regex).await?;
} else {
// Verify that the environments are in sync with the manifest and report to the user otherwise
if !project.environments_in_sync().await? {
tracing::warn!("The environments are not in sync with the manifest, to sync run\n\tpixi global sync");
}
list_global_environments(&project, None, None, args.regex).await?;
list_all_global_environments(&project, None, None, args.regex).await?;
}

Ok(())
Expand Down
70 changes: 70 additions & 0 deletions src/environment/list.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
use std::io::Write;

use human_bytes::human_bytes;
use rattler_conda_types::{PackageName, PackageRecord, Version};
use serde::Serialize;

#[derive(Serialize, Hash, Eq, PartialEq)]
pub struct PackageToOutput {
pub name: PackageName,
version: Version,
build: Option<String>,
pub size_bytes: Option<u64>,
is_explicit: bool,
}

impl PackageToOutput {
pub fn new(record: &PackageRecord, is_explicit: bool) -> Self {
Self {
name: record.name.clone(),
version: record.version.version().clone(),
build: Some(record.build.clone()),
size_bytes: record.size,
is_explicit,
}
}
}

/// Create a human-readable representation of a list of packages.
/// Using a tabwriter to align the columns.
pub fn print_package_table(packages: Vec<PackageToOutput>) -> Result<(), std::io::Error> {
let mut writer = tabwriter::TabWriter::new(std::io::stdout());
let header_style = console::Style::new().bold().cyan();
let header = format!(
"{}\t{}\t{}\t{}",
header_style.apply_to("Package"),
header_style.apply_to("Version"),
header_style.apply_to("Build"),
header_style.apply_to("Size"),
);
writeln!(writer, "{}", &header)?;

for package in packages {
// Convert size to human-readable format
let size_human = package
.size_bytes
.map(|size| human_bytes(size as f64))
.unwrap_or_default();

let package_info = format!(
"{}\t{}\t{}\t{}",
package.name.as_normalized(),
&package.version,
package.build.as_deref().unwrap_or(""),
size_human
);

writeln!(
writer,
"{}",
if package.is_explicit {
console::style(package_info).green().to_string()
} else {
package_info
}
)?;
}

writeln!(writer, "{}\n", header)?;
writer.flush()
}
1 change: 1 addition & 0 deletions src/environment/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
pub(crate) mod conda_metadata;
mod conda_prefix;
pub mod list;
mod pypi_prefix;
mod python_status;
mod reporters;
Expand Down
88 changes: 10 additions & 78 deletions src/global/list.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
use std::io::stdout;

use fancy_display::FancyDisplay;
use human_bytes::human_bytes;
use indexmap::{IndexMap, IndexSet};
use itertools::Itertools;
use pixi_consts::consts;
use pixi_spec::PixiSpec;
use rattler_conda_types::{PackageName, PackageRecord, PrefixRecord, Version};
use rattler_conda_types::{PackageName, PrefixRecord, Version};
use serde::Serialize;
use std::io::Write;

use miette::{miette, IntoDiagnostic};

use crate::global::common::find_package_records;
use crate::{
environment::list::{print_package_table, PackageToOutput},
global::common::find_package_records,
};

use super::{project::ParsedEnvironment, EnvChanges, EnvState, EnvironmentName, Mapping, Project};

Expand Down Expand Up @@ -91,53 +90,8 @@ fn print_meta_info(environment: &ParsedEnvironment) {
}
}

/// Create a human-readable representation of the global environment.
/// Using a tabwriter to align the columns.
fn print_package_table(packages: Vec<PackageToOutput>) -> Result<(), std::io::Error> {
let mut writer = tabwriter::TabWriter::new(stdout());
let header_style = console::Style::new().bold().cyan();
let header = format!(
"{}\t{}\t{}\t{}",
header_style.apply_to("Package"),
header_style.apply_to("Version"),
header_style.apply_to("Build"),
header_style.apply_to("Size"),
);
writeln!(writer, "{}", &header)?;

for package in packages {
// Convert size to human-readable format
let size_human = package
.size_bytes
.map(|size| human_bytes(size as f64))
.unwrap_or_default();

let package_info = format!(
"{}\t{}\t{}\t{}",
package.name.as_normalized(),
&package.version,
package.build.as_deref().unwrap_or(""),
size_human
);

writeln!(
writer,
"{}",
if package.is_explicit {
console::style(package_info).green().to_string()
} else {
package_info
}
)?;
}

writeln!(writer, "{}", header)?;

writer.flush()
}

/// List package and binaries in environment
pub async fn list_environment(
/// List package and binaries in global environment
pub async fn list_specific_global_environment(
project: &Project,
environment_name: &EnvironmentName,
sort_by: GlobalSortBy,
Expand All @@ -157,7 +111,7 @@ pub async fn list_environment(
)
.await?;

let mut packages_to_output: Vec<PackageToOutput> = records
let mut packages_to_output = records
.iter()
.map(|record| {
PackageToOutput::new(
Expand All @@ -167,7 +121,7 @@ pub async fn list_environment(
.contains_key(&record.repodata_record.package_record.name),
)
})
.collect();
.collect_vec();

// Filter according to the regex
if let Some(ref regex) = regex {
Expand Down Expand Up @@ -202,14 +156,13 @@ pub async fn list_environment(
}
println!("{}", output_message);
print_package_table(packages_to_output).into_diagnostic()?;
println!();
print_meta_info(env);

Ok(())
}

/// List all environments in the global environment
pub async fn list_global_environments(
pub async fn list_all_global_environments(
project: &Project,
envs: Option<Vec<EnvironmentName>>,
envs_changes: Option<&EnvChanges>,
Expand Down Expand Up @@ -368,24 +321,3 @@ fn format_dependencies(
None
}
}

#[derive(Serialize, Hash, Eq, PartialEq)]
struct PackageToOutput {
name: PackageName,
version: Version,
build: Option<String>,
size_bytes: Option<u64>,
is_explicit: bool,
}

impl PackageToOutput {
fn new(record: &PackageRecord, is_explicit: bool) -> Self {
Self {
name: record.name.clone(),
version: record.version.version().clone(),
build: Some(record.build.clone()),
size_bytes: record.size,
is_explicit,
}
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading
Loading