From 07056f6a2f2bbec188953be7f22f4a186b6f5ab9 Mon Sep 17 00:00:00 2001 From: fecet Date: Tue, 26 Aug 2025 16:49:13 +0800 Subject: [PATCH] feat: add --to-prefix option for custom environment installation - Add PrefixOverrideGuard for thread-local prefix management - Support platform overrides with --platform flag - Enable installing environments to custom directories - Add validation for platform compatibility - Update CLI documentation and integration tests --- crates/pixi_cli/src/install.rs | 83 ++++++++++++++++++- crates/pixi_core/src/lib.rs | 3 + crates/pixi_core/src/prefix.rs | 1 + crates/pixi_core/src/prefix_override.rs | 75 +++++++++++++++++ crates/pixi_core/src/workspace/environment.rs | 18 ++++ docs/reference/cli/pixi/install.md | 6 ++ tests/integration_rust/common/mod.rs | 2 + 7 files changed, 184 insertions(+), 4 deletions(-) create mode 100644 crates/pixi_core/src/prefix.rs create mode 100644 crates/pixi_core/src/prefix_override.rs diff --git a/crates/pixi_cli/src/install.rs b/crates/pixi_cli/src/install.rs index e1226bbb01..75e4ff850b 100644 --- a/crates/pixi_cli/src/install.rs +++ b/crates/pixi_cli/src/install.rs @@ -1,3 +1,5 @@ +use std::path::PathBuf; + use clap::Parser; use fancy_display::FancyDisplay; use itertools::Itertools; @@ -6,7 +8,10 @@ use pixi_core::{ UpdateLockFileOptions, WorkspaceLocator, environment::get_update_lock_file_and_prefixes, lock_file::{ReinstallPackages, UpdateMode}, + prefix_override::PrefixOverrideGuard, }; +use pixi_manifest::FeaturesExt; +use rattler_conda_types::Platform; use std::fmt::Write; use crate::cli_config::WorkspaceConfig; @@ -30,6 +35,9 @@ use crate::cli_config::WorkspaceConfig; /// /// You can use `pixi reinstall` to reinstall all environments, one environment /// or just some packages of an environment. +/// +/// Use the `--to-prefix` flag to install packages to a custom directory instead +/// of the default environment location. #[derive(Parser, Debug)] pub struct Args { #[clap(flatten)] @@ -53,20 +61,48 @@ pub struct Args { /// This can be useful for instance in a Dockerfile to skip local source dependencies when installing dependencies. #[arg(long, requires = "frozen")] pub skip: Option>, + + /// Install to a custom prefix directory instead of the default environment location + #[arg(long, value_name = "PREFIX", conflicts_with = "all")] + pub to_prefix: Option, + + /// The platform to install packages for (only used with --to-prefix) + #[arg(long, short = 'p', requires = "to_prefix")] + pub platform: Option, } pub async fn execute(args: Args) -> miette::Result<()> { + use miette::{Context, IntoDiagnostic}; + let workspace = WorkspaceLocator::for_cli() .with_search_start(args.project_config.workspace_locator_start()) .locate()? .with_cli_config(args.config); + // Setup custom prefix if specified + if let Some(prefix_path) = &args.to_prefix { + tokio::fs::create_dir_all(prefix_path) + .await + .into_diagnostic() + .with_context(|| { + format!( + "Failed to create prefix directory: {}", + prefix_path.display() + ) + })?; + } + // Install either: - // // 1. specific environments // 2. all environments // 3. default environment (if no environments are specified) - let envs = if let Some(envs) = args.environment { + let envs = if args.to_prefix.is_some() { + vec![ + args.environment + .and_then(|envs| envs.into_iter().next()) + .unwrap_or_else(|| "default".to_string()), + ] + } else if let Some(envs) = args.environment { envs } else if args.all { workspace @@ -84,6 +120,37 @@ pub async fn execute(args: Args) -> miette::Result<()> { .map(|env| workspace.environment_from_name_or_env_var(Some(env))) .collect::, _>>()?; + // Validate platform support if platform is specified + if let Some(platform) = args.platform { + for env in &environments { + if !env.platforms().contains(&platform) { + return Err(miette::miette!( + "Platform '{}' is not supported by environment '{}'. Supported platforms: {}", + platform, + env.name(), + env.platforms() + .iter() + .map(|p| p.to_string()) + .collect::>() + .join(", ") + )); + } + } + } + + // Use prefix override guard if installing to custom prefix + let _guard = args.to_prefix.as_ref().map(|prefix_path| { + if let Some(platform) = args.platform { + PrefixOverrideGuard::new_with_platform( + environments[0].name().to_string(), + prefix_path.clone(), + platform, + ) + } else { + PrefixOverrideGuard::new(environments[0].name().to_string(), prefix_path.clone()) + } + }); + // Update the prefixes by installing all packages let (lock_file, _) = get_update_lock_file_and_prefixes( &environments, @@ -122,13 +189,21 @@ pub async fn execute(args: Args) -> miette::Result<()> { .unwrap(); } - if let Ok(Some(path)) = workspace.config().detached_environments().path() { + // Add location information to the message + if let Some(prefix_path) = &args.to_prefix { + write!( + &mut message, + " to '{}'", + console::style(prefix_path.display()).bold() + ) + .unwrap(); + } else if let Ok(Some(path)) = workspace.config().detached_environments().path() { write!( &mut message, " in '{}'", console::style(path.display()).bold() ) - .unwrap() + .unwrap(); } if let Some(skip) = &args.skip { diff --git a/crates/pixi_core/src/lib.rs b/crates/pixi_core/src/lib.rs index fae2657107..efaf90a72a 100644 --- a/crates/pixi_core/src/lib.rs +++ b/crates/pixi_core/src/lib.rs @@ -5,8 +5,11 @@ pub mod diff; pub mod environment; mod install_pypi; pub mod lock_file; +mod prefix; +pub mod prefix_override; pub mod prompt; pub mod repodata; + pub mod workspace; pub mod signals; diff --git a/crates/pixi_core/src/prefix.rs b/crates/pixi_core/src/prefix.rs new file mode 100644 index 0000000000..7025aa4df5 --- /dev/null +++ b/crates/pixi_core/src/prefix.rs @@ -0,0 +1 @@ +// Empty module - placeholder for future prefix utilities diff --git a/crates/pixi_core/src/prefix_override.rs b/crates/pixi_core/src/prefix_override.rs new file mode 100644 index 0000000000..c62e9792bd --- /dev/null +++ b/crates/pixi_core/src/prefix_override.rs @@ -0,0 +1,75 @@ +use rattler_conda_types::Platform; +use std::cell::RefCell; +use std::collections::HashMap; +use std::path::PathBuf; + +thread_local! { + static PREFIX_OVERRIDES: RefCell> = RefCell::new(HashMap::new()); + static PLATFORM_OVERRIDES: RefCell> = RefCell::new(HashMap::new()); +} + +/// A RAII guard that manages thread-local prefix overrides +pub struct PrefixOverrideGuard { + env_names: Vec, +} + +impl PrefixOverrideGuard { + /// Create a new prefix override guard for a single environment + pub fn new(env_name: String, custom_prefix: PathBuf) -> Self { + PREFIX_OVERRIDES.with(|overrides| { + overrides + .borrow_mut() + .insert(env_name.clone(), custom_prefix); + }); + + Self { + env_names: vec![env_name], + } + } + + /// Create a new prefix override guard with platform override for a single environment + pub fn new_with_platform(env_name: String, custom_prefix: PathBuf, platform: Platform) -> Self { + PREFIX_OVERRIDES.with(|overrides| { + overrides + .borrow_mut() + .insert(env_name.clone(), custom_prefix); + }); + + PLATFORM_OVERRIDES.with(|overrides| { + overrides.borrow_mut().insert(env_name.clone(), platform); + }); + + Self { + env_names: vec![env_name], + } + } +} + +impl Drop for PrefixOverrideGuard { + fn drop(&mut self) { + // Remove all overrides for the environments this guard was managing + PREFIX_OVERRIDES.with(|overrides| { + let mut map = overrides.borrow_mut(); + for env_name in &self.env_names { + map.remove(env_name); + } + }); + + PLATFORM_OVERRIDES.with(|overrides| { + let mut map = overrides.borrow_mut(); + for env_name in &self.env_names { + map.remove(env_name); + } + }); + } +} + +/// Get the prefix override for a specific environment, if any +pub fn get_prefix_override(env_name: &str) -> Option { + PREFIX_OVERRIDES.with(|overrides| overrides.borrow().get(env_name).cloned()) +} + +/// Get the platform override for a specific environment, if any +pub fn get_platform_override(env_name: &str) -> Option { + PLATFORM_OVERRIDES.with(|overrides| overrides.borrow().get(env_name).cloned()) +} diff --git a/crates/pixi_core/src/workspace/environment.rs b/crates/pixi_core/src/workspace/environment.rs index 6e068f7e27..69a9632c71 100644 --- a/crates/pixi_core/src/workspace/environment.rs +++ b/crates/pixi_core/src/workspace/environment.rs @@ -96,7 +96,18 @@ impl<'p> Environment<'p> { } /// Returns the directory where this environment is stored. + /// + /// This method first checks for any thread-local prefix overrides before + /// falling back to the default environment directory. pub fn dir(&self) -> std::path::PathBuf { + // Check for thread-local prefix override first + if let Some(override_path) = + crate::prefix_override::get_prefix_override(self.name().as_str()) + { + return override_path; + } + + // Fall back to default behavior self.workspace .environments_dir() .join(self.environment.name.as_str()) @@ -118,6 +129,13 @@ impl<'p> Environment<'p> { /// Returns the best platform for the current platform & environment. pub fn best_platform(&self) -> Platform { + // Check for thread-local platform override first + if let Some(override_platform) = + crate::prefix_override::get_platform_override(self.name().as_str()) + { + return override_platform; + } + let current = Platform::current(); // If the current platform is supported, return it. diff --git a/docs/reference/cli/pixi/install.md b/docs/reference/cli/pixi/install.md index c23aacdf75..d8c3becf2f 100644 --- a/docs/reference/cli/pixi/install.md +++ b/docs/reference/cli/pixi/install.md @@ -20,6 +20,10 @@ pixi install [OPTIONS] - `--skip ` : Skip installation of specific packages present in the lockfile. Requires --frozen. This can be useful for instance in a Dockerfile to skip local source dependencies when installing dependencies
May be provided more than once. +- `--to-prefix ` +: Install to a custom prefix directory instead of the default environment location +- `--platform (-p) ` +: The platform to install packages for (only used with --to-prefix) ## Config Options - `--auth-file ` @@ -66,5 +70,7 @@ Running `pixi install` is not required before running other commands like `pixi You can use `pixi reinstall` to reinstall all environments, one environment or just some packages of an environment. +Use the `--to-prefix` flag to install packages to a custom directory instead of the default environment location. + --8<-- "docs/reference/cli/pixi/install_extender:example" diff --git a/tests/integration_rust/common/mod.rs b/tests/integration_rust/common/mod.rs index 113c9c77c0..1e8d519f13 100644 --- a/tests/integration_rust/common/mod.rs +++ b/tests/integration_rust/common/mod.rs @@ -567,6 +567,8 @@ impl PixiControl { config: Default::default(), all: false, skip: None, + to_prefix: None, + platform: None, }, } }