Skip to content
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
149 changes: 145 additions & 4 deletions src/cli/global/global_specs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,16 @@ impl GlobalSpecs {
} else if let Some(path) = &self.path {
// Handle path dependencies
PixiSpec::Path(pixi_spec::PathSpec { path: path.clone() })
} else if spec_str.ends_with(".conda")
|| spec_str.ends_with(".tar.bz2")
|| spec_str.starts_with("./")
|| spec_str.starts_with("../")
|| spec_str.starts_with('/')
{
// Auto-detect .conda/.tar.bz2 files or path-like specs and handle as path dependencies
PixiSpec::Path(pixi_spec::PathSpec {
path: Utf8TypedPathBuf::from(spec_str.to_string()),
})
} else {
// Handle regular conda/version dependencies - use the new try_from_str method
let global_spec =
Expand All @@ -91,11 +101,45 @@ impl GlobalSpecs {
};

// For git/path dependencies, we need to parse the spec to get the name
let match_spec = MatchSpec::from_str(spec_str, ParseStrictness::Lenient)?;
if let Some(name) = match_spec.name {
result.push(GlobalSpec::named(name, pixi_spec));
// For auto-detected paths, extract the name from the filename if it's a .conda or .tar.bz2 file
if matches!(pixi_spec, PixiSpec::Path(_))
&& (spec_str.ends_with(".conda")
|| spec_str.ends_with(".tar.bz2")
|| spec_str.starts_with("./")
|| spec_str.starts_with("../")
|| spec_str.starts_with('/'))
{
// For .conda or .tar.bz2 files, extract package name from filename
if spec_str.ends_with(".conda") || spec_str.ends_with(".tar.bz2") {
let filename = std::path::Path::new(spec_str)
.file_name()
.and_then(|name| name.to_str())
.unwrap_or(spec_str);

// Extract package name from conda filename (e.g., "curl-8.14.1-h332b0f4_0.conda" or "curl-8.14.1-h332b0f4_0.tar.bz2" -> "curl")
if let Some(package_name_part) = filename.split('-').next() {
if let Ok(package_name) = rattler_conda_types::PackageName::try_from(
package_name_part.to_string(),
) {
result.push(GlobalSpec::named(package_name, pixi_spec));
} else {
result.push(GlobalSpec::nameless(pixi_spec));
}
} else {
result.push(GlobalSpec::nameless(pixi_spec));
}
} else {
// For other paths, create nameless spec
result.push(GlobalSpec::nameless(pixi_spec));
}
} else {
result.push(GlobalSpec::nameless(pixi_spec));
// For explicit git/path dependencies, parse the spec to get the name
let match_spec = MatchSpec::from_str(spec_str, ParseStrictness::Lenient)?;
if let Some(name) = match_spec.name {
result.push(GlobalSpec::named(name, pixi_spec));
} else {
result.push(GlobalSpec::nameless(pixi_spec));
}
}
}

Expand Down Expand Up @@ -188,4 +232,101 @@ mod tests {
&PixiSpec::Path(..)
))
}

#[test]
fn test_to_global_specs_auto_detect_conda_file() {
let specs = GlobalSpecs {
specs: vec!["./curl-8.14.1-h332b0f4_0.conda".to_string()],
path: None,
git: None,
rev: None,
subdir: None,
};

let channel_config = ChannelConfig::default_with_root_dir(PathBuf::from("."));
let global_specs = specs.to_global_specs(&channel_config).unwrap();

assert_eq!(global_specs.len(), 1);
let global_spec = global_specs.first().unwrap();
assert!(matches!(global_spec.spec(), &PixiSpec::Path(..)));
// Should extract "curl" as the package name from "curl-8.14.1-h332b0f4_0.conda"
match global_spec {
GlobalSpec::Named(named_spec) => {
assert_eq!(named_spec.name().as_source(), "curl");
}
GlobalSpec::Nameless(_) => panic!("Expected named spec for .conda file"),
}
}

#[test]
fn test_to_global_specs_auto_detect_relative_path() {
let specs = GlobalSpecs {
specs: vec!["../some-package".to_string()],
path: None,
git: None,
rev: None,
subdir: None,
};

let channel_config = ChannelConfig::default_with_root_dir(PathBuf::from("."));
let global_specs = specs.to_global_specs(&channel_config).unwrap();

assert_eq!(global_specs.len(), 1);
let global_spec = global_specs.first().unwrap();
assert!(matches!(global_spec.spec(), &PixiSpec::Path(..)));
// Should be nameless for non-.conda paths
assert!(matches!(global_spec, GlobalSpec::Nameless(_)));
}

#[test]
fn test_to_global_specs_auto_detect_tar_bz2_file() {
let specs = GlobalSpecs {
specs: vec!["./python-3.8.5-h7579374_1.tar.bz2".to_string()],
path: None,
git: None,
rev: None,
subdir: None,
};

let channel_config = ChannelConfig::default_with_root_dir(PathBuf::from("."));
let global_specs = specs.to_global_specs(&channel_config).unwrap();

assert_eq!(global_specs.len(), 1);
let global_spec = global_specs.first().unwrap();
assert!(matches!(global_spec.spec(), &PixiSpec::Path(..)));
// Should extract "python" as the package name from "python-3.8.5-h7579374_1.tar.bz2"
match global_spec {
GlobalSpec::Named(named_spec) => {
assert_eq!(named_spec.name().as_source(), "python");
}
GlobalSpec::Nameless(_) => panic!("Expected named spec for .tar.bz2 file"),
}
}

#[test]
fn test_to_global_specs_auto_detect_absolute_path_tar_bz2() {
let specs = GlobalSpecs {
specs: vec!["/tmp/numpy-1.21.0-py38h9894fe3_0.tar.bz2".to_string()],
path: None,
git: None,
rev: None,
subdir: None,
};

let channel_config = ChannelConfig::default_with_root_dir(PathBuf::from("."));
let global_specs = specs.to_global_specs(&channel_config).unwrap();

assert_eq!(global_specs.len(), 1);
let global_spec = global_specs.first().unwrap();
assert!(matches!(global_spec.spec(), &PixiSpec::Path(..)));
// Should extract "numpy" as the package name
match global_spec {
GlobalSpec::Named(named_spec) => {
assert_eq!(named_spec.name().as_source(), "numpy");
}
GlobalSpec::Nameless(_) => {
panic!("Expected named spec for absolute path .tar.bz2 file")
}
}
}
}
13 changes: 11 additions & 2 deletions src/cli/global/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use indexmap::IndexMap;
use clap::Parser;
use fancy_display::FancyDisplay;
use itertools::Itertools;
use miette::Context;
use miette::{Context, IntoDiagnostic};
use rattler_conda_types::{MatchSpec, NamedChannelOrUrl, Platform};

use crate::{
Expand Down Expand Up @@ -84,9 +84,18 @@ pub async fn execute(args: Args) -> miette::Result<()> {
.await?
.with_cli_config(config.clone());

// Capture the current working directory for proper relative path resolution
let current_dir = std::env::current_dir()
.into_diagnostic()
.wrap_err("Could not retrieve the current directory")?;
let channel_config = rattler_conda_types::ChannelConfig {
root_dir: current_dir,
..project_original.global_channel_config().clone()
};

let (specs, source): (Vec<_>, Vec<_>) = args
.packages
.to_global_specs(project_original.global_channel_config())?
.to_global_specs(&channel_config)?
.into_iter()
// TODO: will allow nameless specs later
.filter_map(|s| s.into_named())
Expand Down
Loading