diff --git a/docs/reference/cli/pixi/search_extender b/docs/reference/cli/pixi/search_extender index b88e56398f..1d49789327 100644 --- a/docs/reference/cli/pixi/search_extender +++ b/docs/reference/cli/pixi/search_extender @@ -7,6 +7,9 @@ pixi search pixi pixi search --limit 30 "py*" # search in a different channel and for a specific platform pixi search -c robostack --platform linux-64 "plotjuggler*" +# search for a specific version of a package +pixi search "rattler-build<=0.35.4" +pixi search "rattler-build[build=ha8cf89e_0]" ``` --8<-- [end:example] diff --git a/src/cli/search.rs b/src/cli/search.rs index fbf8998a2c..9940e8993c 100644 --- a/src/cli/search.rs +++ b/src/cli/search.rs @@ -13,7 +13,8 @@ use miette::{IntoDiagnostic, Report}; use pixi_config::{default_channel_config, Config}; use pixi_progress::await_in_progress; use pixi_utils::reqwest::build_reqwest_clients; -use rattler_conda_types::{MatchSpec, PackageName, Platform, RepoDataRecord}; +use rattler_conda_types::{MatchSpec, PackageName, ParseStrictness, Platform, RepoDataRecord}; +use rattler_lock::Matches; use rattler_repodata_gateway::{GatewayError, RepoData}; use regex::Regex; use strsim::jaro; @@ -188,8 +189,9 @@ pub async fn execute_impl( // If package name filter doesn't contain * (wildcard), it will search and display specific // package info (if any package is found) else { - let package_name = PackageName::try_from(package_name_filter).into_diagnostic()?; - search_exact_package(package_name, all_names, repodata_query_func, out).await? + let package_spec = MatchSpec::from_str(&package_name_filter, ParseStrictness::Lenient) + .into_diagnostic()?; + search_exact_package(package_spec, all_names, repodata_query_func, out).await? }; Ok(packages) @@ -202,7 +204,7 @@ pub async fn execute(args: Args) -> miette::Result<()> { } async fn search_exact_package( - package_name: PackageName, + package_spec: MatchSpec, all_repodata_names: Vec, repodata_query_func: QF, out: &mut W, @@ -211,7 +213,10 @@ where QF: Fn(Vec) -> FR, FR: Future, GatewayError>>, { - let package_name_search = package_name.clone(); + let package_name_search = package_spec.name.clone().ok_or_else(|| { + miette::miette!("could not find package name in MatchSpec {}", package_spec) + })?; + let packages = search_package_by_filter( &package_name_search, all_repodata_names, @@ -220,9 +225,16 @@ where false, ) .await?; + + if packages.is_empty() { + let normalized_package_name = package_name_search.as_normalized(); + return Err(miette::miette!("Package {normalized_package_name} not found, please use a wildcard '*' in the search name for a broader result.")); + } + // Sort packages by version, build number and build string let packages = packages .iter() + .filter(|&p| package_spec.matches(p)) .sorted_by(|a, b| { Ord::cmp( &( @@ -241,8 +253,9 @@ where .collect::>(); if packages.is_empty() { - let normalized_package_name = package_name.as_normalized(); - return Err(miette::miette!("Package {normalized_package_name} not found, please use a wildcard '*' in the search name for a broader result.")); + return Err(miette::miette!( + "Package found, but MatchSpec {package_spec} does not match any record." + )); } let newest_package = packages.last(); diff --git a/tests/integration_rust/search_tests.rs b/tests/integration_rust/search_tests.rs index dac19c9ad4..2857a3a49f 100644 --- a/tests/integration_rust/search_tests.rs +++ b/tests/integration_rust/search_tests.rs @@ -68,6 +68,76 @@ async fn search_return_latest_across_everything() { assert_eq!(found_package.package_record.version.as_str(), "4"); } +#[tokio::test] +async fn search_using_match_spec() { + let mut package_database = PackageDatabase::default(); + + // Add a package `foo` with different versions and different builds + package_database.add_package( + Package::build("foo", "0.1.0") + .with_build("h60d57d3_0") + .finish(), + ); + package_database.add_package( + Package::build("foo", "0.1.0") + .with_build("h60d57d3_1") + .finish(), + ); + package_database.add_package( + Package::build("foo", "0.2.0") + .with_build("h60d57d3_0") + .finish(), + ); + package_database.add_package( + Package::build("foo", "0.2.0") + .with_build("h60d57d3_1") + .finish(), + ); + + // Write the repodata to disk + let temp_dir = TempDir::new().unwrap(); + let channel_dir = temp_dir.path().join("channel"); + package_database.write_repodata(&channel_dir).await.unwrap(); + let channel = Url::from_file_path(channel_dir).unwrap(); + let platform = Platform::current(); + let pixi = PixiControl::from_manifest(&format!( + r#" + [project] + name = "test-search-using-match-spec" + channels = ["{channel}"] + platforms = ["{platform}"] + + "# + )) + .unwrap(); + + // Without match spec the latest version is returned + let binding = pixi.search("foo".to_string()).await.unwrap().unwrap(); + let found_package = binding.last().unwrap(); + assert_eq!(found_package.package_record.version.as_str(), "0.2.0"); + assert_eq!(found_package.package_record.build.as_str(), "h60d57d3_1"); + + // Search for a specific version + let binding = pixi + .search("foo<=0.1.0".to_string()) + .await + .unwrap() + .unwrap(); + let found_package = binding.last().unwrap(); + assert_eq!(found_package.package_record.version.as_str(), "0.1.0"); + assert_eq!(found_package.package_record.build.as_str(), "h60d57d3_1"); + + // Search for a specific build + let binding = pixi + .search("foo[build=h60d57d3_0]".to_string()) + .await + .unwrap() + .unwrap(); + let found_package = binding.last().unwrap(); + assert_eq!(found_package.package_record.version.as_str(), "0.2.0"); + assert_eq!(found_package.package_record.build.as_str(), "h60d57d3_0"); +} + #[tokio::test] async fn test_search_multiple_versions() { let mut package_database = PackageDatabase::default();