Skip to content
Open
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
127 changes: 50 additions & 77 deletions crates/pixi_cli/src/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<PlatformData>| {
marker
.as_ref()
.map_or_else(|| "<none>".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();
Expand All @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
25 changes: 17 additions & 8 deletions crates/pixi_core/src/lock_file/update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 <name>` (and `pixi reinstall --platform
/// <name>`) flows so cross-target installs skip the host-VP
/// satisfaction check that would otherwise reject them.
/// explicit `--platform <name>` 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<PixiPlatformName>,

/// The lock file
Expand Down Expand Up @@ -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(|| {
Expand Down
24 changes: 13 additions & 11 deletions crates/pixi_core/src/workspace/environment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<PixiPlatformName> {
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?;
Expand Down
26 changes: 10 additions & 16 deletions crates/pixi_task/src/task_environment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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> {
Expand Down
Loading