diff --git a/crates/pixi_cli/src/run.rs b/crates/pixi_cli/src/run.rs index b56f76ae7c..2b88720fcb 100644 --- a/crates/pixi_cli/src/run.rs +++ b/crates/pixi_cli/src/run.rs @@ -18,10 +18,10 @@ use miette::{Diagnostic, IntoDiagnostic}; use pixi_config::{ConfigCli, ConfigCliActivation}; use pixi_core::{ Workspace, WorkspaceLocator, - environment::{PlatformData, sanity_check_workspace}, + environment::sanity_check_workspace, lock_file::{ReinstallPackages, UpdateLockFileOptions, UpdateMode}, workspace::{ - Environment, HasWorkspaceRef, PlatformOverrides, PlatformSource, + Environment, errors::UnsupportedPlatformError, virtual_packages::{ EnvironmentRunnability, classify_environment_runnability, @@ -166,55 +166,11 @@ pub async fn execute(args: Args) -> miette::Result<()> { // Sanity check of prefix location sanity_check_workspace(&workspace).await?; - // `--platform` pins which declared platform the environment is installed - // and activated for. Without it we auto-upgrade to the platform the - // environment was last installed for (so users need not repeat - // `--platform`), falling back to the host-aware best match when the - // environment isn't installed yet. + // `--platform` pins which declared platform environments are installed + // and activated for. Without it, each task's environment resolves its own + // platform in the loop below, sticky to the platform it was last + // installed for (see `LockFileDerivedData::install_platform`). let user_platform = resolve_install_platform(&workspace, args.platform.as_ref())?; - let installed_platform = environment.installed_resolved_platform_name(); - let run_platform = user_platform.clone().or_else(|| installed_platform.clone()); - let best_declared_platform = environment.named_or_best_declared_platform(run_platform.as_ref()); - - let (resolved_marker, minimum_marker) = environment.installed_platforms(); - let format_marker = |marker: &Option| { - marker - .as_ref() - .map_or_else(|| "".to_string(), ToString::to_string) - }; - tracing::debug!( - "Run-platform decision for environment '{}': --platform={:?}, auto-detected={}, installed resolved platform={:?}, marker resolved={}, marker minimum={}, chosen run platform={:?}", - environment.name(), - user_platform.as_ref().map(|p| p.as_str()), - PlatformData::from(&environment.workspace().host_platform( - PlatformSource::Defaults, - PlatformOverrides::EnvironmentVariableOverrides - )), - installed_platform.as_ref().map(|p| p.as_str()), - format_marker(&resolved_marker), - format_marker(&minimum_marker), - run_platform.as_ref().map(|p| p.as_str()), - ); - if let Some(platform) = best_declared_platform { - tracing::info!( - "Running tasks in environment '{}' assuming platform '{}'", - environment.name().fancy_display(), - platform.name(), - ); - } - - // A `--platform` the environment doesn't list is a membership error. With - // no platform requested, defer to the install path's minimum fallback. - if args.lock_and_install_config.allow_installs() - && best_declared_platform.is_none() - && let Some(name) = user_platform.as_ref() - { - return Err(miette::miette!( - "platform '{}' is not part of environment '{}'", - name, - environment.name(), - )); - } if args.lock_and_install_config.allow_installs() { environment.emit_emulation_warning(); @@ -237,9 +193,10 @@ pub async fn execute(args: Args) -> miette::Result<()> { .await? .0; - // Only an explicit `--platform` pins the global target; the implicit - // auto-upgrade is resolved per-environment in the loop below, since a - // global pin broke sibling environments with a different platform. + // Only an explicit `--platform` pins the global target; without it the + // install path resolves a platform per environment (sticky to what each + // environment was last installed for), so sibling environments with + // different platforms don't affect each other. lock_file.target_platform = user_platform.clone(); // Spawn a task that listens for ctrl+c and resets the cursor. @@ -310,27 +267,41 @@ pub async fn execute(args: Args) -> miette::Result<()> { continue; } - // Fail before announcing a task whose environment can't run here at - // all; by-accident environments proceed and `--platform` overrides. - if args.lock_and_install_config.allow_installs() - && user_platform.is_none() - && classify_environment_runnability( + // Fail before announcing a task whose environment can't run here. + if args.lock_and_install_config.allow_installs() { + if let Some(name) = user_platform.as_ref() { + // A `--platform` the task's environment doesn't list is a + // membership error; a listed one overrides the host checks. + if executable_task + .run_environment + .named_or_best_declared_platform(Some(name)) + .is_none() + { + return Err(miette::miette!( + "platform '{}' is not part of environment '{}'", + name, + executable_task.run_environment.name(), + )); + } + } else if classify_environment_runnability( &executable_task.run_environment, Some(lock_file.as_lock_file()), ) == EnvironmentRunnability::Unsupported - { - return Err( - match verify_current_platform_can_run_environment( - &executable_task.run_environment, - Some(lock_file.as_lock_file()), - ) { - Err(err) => err.into(), - Ok(()) => executable_task - .run_environment - .unsupported_platform_error() - .into(), - }, - ); + { + // By-accident environments proceed past this check. + return Err( + match verify_current_platform_can_run_environment( + &executable_task.run_environment, + Some(lock_file.as_lock_file()), + ) { + Err(err) => err.into(), + Ok(()) => executable_task + .run_environment + .unsupported_platform_error() + .into(), + }, + ); + } } // Showing which command is being run if the level and type allows it. @@ -408,12 +379,14 @@ pub async fn execute(args: Args) -> miette::Result<()> { Entry::Vacant(entry) => { // Check if we allow installs if args.lock_and_install_config.allow_installs() { - // No `--platform`: pin to the platform this environment was - // last installed for, not a sibling's bare subdir. - if user_platform.is_none() { - lock_file.target_platform = executable_task - .run_environment - .installed_resolved_platform_name(); + if let Some(platform) = + lock_file.install_platform(&executable_task.run_environment) + { + tracing::info!( + "Running tasks in environment '{}' assuming platform '{}'", + executable_task.run_environment.name().fancy_display(), + platform.name(), + ); } // Ensure there is a valid prefix diff --git a/crates/pixi_core/src/lock_file/update.rs b/crates/pixi_core/src/lock_file/update.rs index 57c3e293db..e8e55c7a6e 100644 --- a/crates/pixi_core/src/lock_file/update.rs +++ b/crates/pixi_core/src/lock_file/update.rs @@ -565,9 +565,10 @@ pub struct LockFileDerivedData<'p> { /// Optional workspace-platform override applied to every install-side /// "which platform does this environment target" lookup. Set by the - /// `pixi install --platform ` (and `pixi reinstall --platform - /// `) flows so cross-target installs skip the host-VP - /// satisfaction check that would otherwise reject them. + /// explicit `--platform ` flag of `pixi install`, `pixi reinstall` + /// and `pixi run` so cross-target installs skip the host-VP + /// satisfaction check that would otherwise reject them. Without it, + /// [`Self::install_platform`] resolves a platform per environment. pub target_platform: Option, /// The lock file @@ -714,16 +715,24 @@ impl<'p> LockFileDerivedData<'p> { .ok_or_else(|| UpdateError::LockFileMissingEnv(environment.name().clone()))?; Ok(LockedEnvironmentHash::from_environment( locked_environment, - environment.named_or_best_declared_platform(self.target_platform.as_ref()), + self.install_platform(environment), )) } /// The declared platform install targets for `environment`: the explicit - /// `--platform` override or the best declared platform; when neither - /// matches this machine, a declared platform whose lock-resolved minimum - /// requirements the machine meets (running "by accident"). - fn install_platform(&self, environment: &Environment<'p>) -> Option<&'p PixiPlatform> { + /// `--platform` override; else, sticky, the platform the environment was + /// last installed for (so an implicit install doesn't flip a prefix that + /// was deliberately installed for another declared platform); else the + /// best declared platform for this machine; when none of those match, + /// a declared platform whose lock-resolved minimum requirements the + /// machine meets (running "by accident"). + pub fn install_platform(&self, environment: &Environment<'p>) -> Option<&'p PixiPlatform> { let target_override = self.target_platform.as_ref(); + if target_override.is_none() + && let Some(installed) = environment.installed_declared_platform() + { + return Some(installed); + } environment .named_or_best_declared_platform(target_override) .or_else(|| { diff --git a/crates/pixi_core/src/workspace/environment.rs b/crates/pixi_core/src/workspace/environment.rs index 9d4a7236b6..62d84fa49c 100644 --- a/crates/pixi_core/src/workspace/environment.rs +++ b/crates/pixi_core/src/workspace/environment.rs @@ -120,19 +120,21 @@ impl<'p> Environment<'p> { } } - /// The name of the workspace platform this environment was last installed - /// for, recovered by matching the resolved platform in `conda-meta/pixi` - /// (subdir + virtual packages) against the declared platforms. Lets `pixi - /// run` default to what the user installed for without a repeated - /// `--platform`. `None` when the environment isn't installed or its - /// resolved platform is no longer declared. - pub fn installed_resolved_platform_name(&self) -> Option { - self.installed_resolved_platform() - .map(|platform| platform.name().clone()) + /// [`Self::installed_resolved_platform`], but only when this environment + /// itself declares that platform, so callers can treat the marker as this + /// environment's install target. `None` when the marker platform is + /// declared by the workspace but not listed by this environment. + pub fn installed_declared_platform(&self) -> Option<&'p PixiPlatform> { + let platform = self.installed_resolved_platform()?; + let env_platforms = self.platforms(); + (env_platforms.is_empty() || env_platforms.contains(platform.name())).then_some(platform) } - /// The declared platform [`Self::installed_resolved_platform_name`] refers - /// to, returned by reference to avoid a second name-based lookup. + /// The workspace platform this environment was last installed for, + /// recovered by matching the resolved platform in `conda-meta/pixi` + /// (subdir + virtual packages) against the declared platforms. `None` + /// when the environment isn't installed or its resolved platform is no + /// longer declared. pub fn installed_resolved_platform(&self) -> Option<&'p PixiPlatform> { let (resolved, _) = self.installed_platforms(); let resolved = resolved?; diff --git a/crates/pixi_task/src/task_environment.rs b/crates/pixi_task/src/task_environment.rs index ff1b6cfc33..16103eb285 100644 --- a/crates/pixi_task/src/task_environment.rs +++ b/crates/pixi_task/src/task_environment.rs @@ -130,22 +130,16 @@ pub(crate) fn environments_defining_task( /// machine-incompatible environments are still found -- the first declared /// platform. fn default_search_platform<'p>(env: &Environment<'p>) -> Option<&'p PixiPlatform> { - if let Some(platform) = env.installed_resolved_platform() { - // Mirror `named_or_best_declared_platform`'s guard: only use the - // installed platform when the environment actually declares it. - let env_platforms = env.platforms(); - if env_platforms.is_empty() || env_platforms.contains(platform.name()) { - return Some(platform); - } - } - env.best_declared_platform().or_else(|| { - let env_platform_names = env.platforms(); - env.workspace_manifest() - .workspace - .platforms - .iter() - .find(|platform| env_platform_names.contains(platform.name())) - }) + env.installed_declared_platform() + .or_else(|| env.best_declared_platform()) + .or_else(|| { + let env_platform_names = env.platforms(); + env.workspace_manifest() + .workspace + .platforms + .iter() + .find(|platform| env_platform_names.contains(platform.name())) + }) } impl<'p, D: TaskDisambiguation<'p>> SearchEnvironments<'p, D> {