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
12 changes: 11 additions & 1 deletion crates/pixi_cli/src/global/global_specs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ pub struct GlobalSpecs {
#[clap(long, requires = "git", help_heading = consts::CLAP_GIT_OPTIONS)]
pub subdir: Option<String>,

/// The path to the local directory
/// The path to the local package
#[clap(long, conflicts_with = "git")]
pub path: Option<Utf8NativePathBuf>,
}
Expand Down Expand Up @@ -59,6 +59,8 @@ pub enum GlobalSpecsConversionError {
#[error("failed to infer package name")]
#[diagnostic(transparent)]
PackageNameInference(#[from] pixi_global::project::InferPackageNameError),
#[error("Input {0} looks like a path: please pass `--path`.")]
MissingPathArg(String),
}

impl GlobalSpecs {
Expand Down Expand Up @@ -92,6 +94,14 @@ impl GlobalSpecs {
.to_typed_path_buf(),
}))
} else {
fn pathlike(s: &str) -> bool {
s.contains(".conda") || s.contains('/') || s.contains('\\')
}
if let Some(pathlike_input) = self.specs.iter().find(|s| pathlike(s)) {
return Err(GlobalSpecsConversionError::MissingPathArg(
pathlike_input.clone(),
));
}
None
};
if let Some(pixi_spec) = git_or_path_spec {
Expand Down
15 changes: 5 additions & 10 deletions crates/pixi_cli/src/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, IntoDiagnostic, Report};
use miette::Report;
use rattler_conda_types::{MatchSpec, NamedChannelOrUrl, Platform};

use crate::global::{global_specs::GlobalSpecs, revert_environment_after_error};
Expand Down Expand Up @@ -78,18 +78,13 @@ pub struct Args {

pub async fn execute(args: Args) -> miette::Result<()> {
let config = Config::with_cli_config(&args.config);

// Load the global config and ensure
// that the root_dir is relative to the manifest directory
let project_original = pixi_global::Project::discover_or_create()
.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 channel_config = project_original.global_channel_config().clone();

let specs = args
.packages
Expand Down
9 changes: 6 additions & 3 deletions crates/pixi_config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -654,7 +654,7 @@ pub struct Config {
pub loaded_from: Vec<PathBuf>,

#[serde(skip, default = "default_channel_config")]
channel_config: ChannelConfig,
pub channel_config: ChannelConfig,

/// Configuration for repodata fetching.
#[serde(alias = "repodata_config")] // BREAK: remove to stop supporting snake_case alias
Expand Down Expand Up @@ -1346,8 +1346,11 @@ impl Config {
// Extended self.mirrors with other.mirrors
mirrors: self.mirrors,
loaded_from: other.loaded_from,
// currently this is always the default so just use the other value
channel_config: other.channel_config,
channel_config: if other.channel_config == default_channel_config() {
self.channel_config
} else {
other.channel_config
},
repodata_config: self.repodata_config.merge(other.repodata_config),
pypi_config: self.pypi_config.merge(other.pypi_config),
s3_options: {
Expand Down
57 changes: 37 additions & 20 deletions crates/pixi_global/src/project/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ use pixi_core::repodata::Repodata;
use pixi_manifest::PrioritizedChannel;
use pixi_progress::global_multi_progress;
use pixi_reporters::TopLevelProgress;
use pixi_spec::{BinarySpec, PathBinarySpec};
use pixi_spec_containers::DependencyMap;
use pixi_utils::{
executable_from_path,
Expand All @@ -40,7 +41,7 @@ use pixi_utils::{
};
use rattler_conda_types::{
ChannelConfig, GenericVirtualPackage, MatchSpec, PackageName, Platform, PrefixRecord,
menuinst::MenuMode,
menuinst::MenuMode, package::ArchiveIdentifier,
};
use rattler_repodata_gateway::Gateway;
// Removed unused rattler_solve imports
Expand Down Expand Up @@ -120,7 +121,7 @@ pub struct Project {
/// The manifest for the project
pub manifest: Manifest,
/// The global configuration as loaded from the config file(s)
config: Config,
pub config: Config,
/// Root directory of the global environments
pub(crate) env_root: EnvRoot,
/// Binary directory
Expand Down Expand Up @@ -303,7 +304,10 @@ impl Project {
.expect("manifest path should always have a parent")
.to_owned();

let config = Config::load(&root);
// Load the global config and ensure
// that the root_dir is relative to the manifest directory
let mut config = Config::load_global();
config.channel_config.root_dir = root.clone();

let client = OnceCell::new();
let repodata_gateway = OnceCell::new();
Expand Down Expand Up @@ -1361,26 +1365,18 @@ impl Project {
})
}

/// Infer the package name from a PixiSpec (path or git) by examining build
/// outputs
pub async fn infer_package_name_from_spec(
/// Infer the package name from a SourceSpec by examining build outputs
async fn infer_package_name_from_source_spec(
&self,
pixi_spec: &pixi_spec::PixiSpec,
source_spec: pixi_spec::SourceSpec,
) -> Result<PackageName, InferPackageNameError> {
let pinned_source_spec = match pixi_spec.clone().into_source_or_binary() {
Either::Left(source_spec) => {
let command_dispatcher = self.command_dispatcher()?;
let checkout = command_dispatcher
.pin_and_checkout(source_spec)
.await
.map_err(|e| InferPackageNameError::BuildBackendMetadata(Box::new(e)))?;
let command_dispatcher = self.command_dispatcher()?;
let checkout = command_dispatcher
.pin_and_checkout(source_spec)
.await
.map_err(|e| InferPackageNameError::BuildBackendMetadata(Box::new(e)))?;

checkout.pinned
}
Either::Right(_) => {
return Err(InferPackageNameError::UnsupportedSpecType);
}
};
let pinned_source_spec = checkout.pinned;

// Create the metadata spec
let metadata_spec = BuildBackendMetadataSpec {
Expand Down Expand Up @@ -1422,6 +1418,27 @@ impl Project {
}
}
}

/// Infer the package name from a PixiSpec (path or git) by examining build
/// outputs
pub async fn infer_package_name_from_spec(
&self,
pixi_spec: &pixi_spec::PixiSpec,
) -> Result<PackageName, InferPackageNameError> {
match pixi_spec.clone().into_source_or_binary() {
Either::Left(source_spec) => {
self.infer_package_name_from_source_spec(source_spec).await
}
Either::Right(binary_spec) => match binary_spec {
BinarySpec::Path(PathBinarySpec { path }) => path
.file_name()
.and_then(ArchiveIdentifier::try_from_filename)
.and_then(|iden| PackageName::from_str(&iden.name).ok())
.ok_or(InferPackageNameError::UnsupportedSpecType),
_ => Err(InferPackageNameError::UnsupportedSpecType),
},
}
}
}

impl Repodata for Project {
Expand Down
2 changes: 1 addition & 1 deletion docs/reference/cli/pixi/global/add.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ pixi global add [OPTIONS] --environment <ENVIRONMENT> [PACKAGE]...

## Options
- <a id="arg---path" href="#arg---path">`--path <PATH>`</a>
: The path to the local directory
: The path to the local package
- <a id="arg---environment" href="#arg---environment">`--environment (-e) <ENVIRONMENT>`</a>
: Specifies the environment that the dependencies need to be added to
<br>**required**: `true`
Expand Down
2 changes: 1 addition & 1 deletion docs/reference/cli/pixi/global/install.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ pixi global install [OPTIONS] [PACKAGE]...

## Options
- <a id="arg---path" href="#arg---path">`--path <PATH>`</a>
: The path to the local directory
: The path to the local package
- <a id="arg---channel" href="#arg---channel">`--channel (-c) <CHANNEL>`</a>
: The channels to consider as a name or a url. Multiple channels can be specified by using this field multiple times
<br>May be provided more than once.
Expand Down
5 changes: 5 additions & 0 deletions pixi.toml
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,11 @@ test-specific-test = { cmd = "pytest -k '{{ test_substring }}'", args = [
], depends-on = [
"build-release",
] }
test-specific-test-debug = { cmd = "pytest --pixi-build=debug -k '{{ test_substring }}'", args = [
"test_substring",
], depends-on = [
"build-debug",
] }
# Update one test channel by passing on value of `mappings.toml`
# e.g. "multiple_versions_channel_1"
update-test-channel = { cmd = "python update-channels.py {{ channel }}", args = [
Expand Down
154 changes: 154 additions & 0 deletions tests/integration_python/pixi_global/test_global.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import os
import platform
import shutil
import tomllib
Expand Down Expand Up @@ -2211,3 +2212,156 @@ def test_tree_invert(pixi: Path, tmp_path: Path, dummy_channel_1: str) -> None:
env=env,
stdout_contains=["dummy-c", "dummy-a 0.1.0"],
)


class TestCondaFile:
@pytest.mark.parametrize("path_arg", [True, False])
def test_install_conda_file(
self, pixi: Path, tmp_path: Path, shortcuts_channel_1: str, path_arg: bool
) -> None:
"""Test directly installing a `.conda` file with `pixi global`"""
env = {"PIXI_HOME": str(tmp_path), "PIXI_CACHE_DIR": str(tmp_path / "foo")}
cwd = tmp_path

conda_file = tmp_path / "pixi-editor-1.0.0-h4616a5c_0.conda"
shutil.copyfile(
Path.from_uri(shortcuts_channel_1) / "noarch" / "pixi-editor-1.0.0-h4616a5c_0.conda",
conda_file,
)

def check_install(conda_file_path: Path, cwd: Path):
if path_arg:
verify_cli_command(
[pixi, "global", "install", "--path", conda_file_path], env=env, cwd=cwd
)
else:
verify_cli_command(
[pixi, "global", "install", conda_file_path],
env=env,
expected_exit_code=ExitCode.FAILURE,
stderr_contains="please pass `--path`",
cwd=cwd,
)

# check absolute path
check_install(conda_file, cwd)

# check relative path in same dir
cwd = conda_file.parent
relative_conda_file = conda_file.relative_to(cwd, walk_up=True)
check_install(relative_conda_file, cwd)

# check relative path in subdir
cwd = conda_file.parent.parent
relative_conda_file = conda_file.relative_to(cwd, walk_up=True)
check_install(relative_conda_file, cwd)

# check relative path in a 'cousin' relative directory
cwd = tmp_path
relative_conda_file = conda_file.relative_to(cwd, walk_up=True)
check_install(relative_conda_file, cwd)

def test_update_sync_conda_file(
self, pixi: Path, tmp_path: Path, shortcuts_channel_1: str
) -> None:
"""Test that `pixi global {update, sync}` work and use the existing file."""
env = {"PIXI_HOME": str(tmp_path), "PIXI_CACHE_DIR": str(tmp_path / "foo")}
cwd = tmp_path

package_name = "pixi-editor"
conda_file = tmp_path / "pixi-editor-1.0.0-h4616a5c_0.conda"
shutil.copyfile(
Path.from_uri(shortcuts_channel_1) / "noarch" / "pixi-editor-1.0.0-h4616a5c_0.conda",
conda_file,
)

verify_cli_command(
[
pixi,
"global",
"install",
"--path",
conda_file,
],
env=env,
cwd=cwd,
)

# update with file still there
verify_cli_command(
[
pixi,
"global",
"update",
"pixi-editor",
],
env=env,
cwd=cwd,
stderr_contains="Environment pixi-editor was already up-to-date.",
)

# sync with file still there
verify_cli_command(
[
pixi,
"global",
"sync",
],
env=env,
cwd=cwd,
stderr_contains="Nothing to do",
)

os.remove(conda_file)

# update with file gone
verify_cli_command(
[
pixi,
"global",
"update",
"pixi-editor",
],
env=env,
cwd=cwd,
stderr_contains="Environment pixi-editor was already up-to-date.",
)

# sync with file gone
verify_cli_command(
[
pixi,
"global",
"sync",
],
env=env,
cwd=cwd,
stderr_contains="Nothing to do",
)

# remove the environment
# XXX: should this fail instead?
shutil.rmtree(tmp_path / "envs" / package_name)

# update with environment removed
verify_cli_command(
[
pixi,
"global",
"update",
"pixi-editor",
],
env=env,
cwd=cwd,
)

# sync with environment removed
verify_cli_command(
[
pixi,
"global",
"sync",
],
env=env,
cwd=cwd,
)
Loading