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
14 changes: 13 additions & 1 deletion crates/pixi_core/src/lock_file/resolve/pypi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,14 @@ pub enum SolveError {
#[error("failed to resolve pypi dependencies")]
Other(#[from] ResolveError),
#[error("build dispatch initialization failed: {message}")]
BuildDispatchPanic { message: String },
BuildDispatchPanic {
message: String,
/// Help carried over from the underlying diagnostic (e.g. the
/// `CONDA_OVERRIDE_*` hints for an unsupported platform), kept separate
/// so miette renders it as its own help section.
#[help]
help: Option<String>,
},
#[error("unexpected panic during PyPI resolution: {message}")]
GeneralPanic { message: String },

Expand Down Expand Up @@ -812,8 +819,13 @@ pub async fn resolve_pypi(
Err(panic_payload) => {
// Try to get the stored initialization error from the last_error holder
if let Some(stored_error) = last_error.get() {
// The panic is re-wrapped as a plain message, so carry the inner
// diagnostic's help (e.g. the `CONDA_OVERRIDE_*` hints for an
// unsupported platform) across as a separate field rather than
// losing it or mashing it into the message.
return Err(SolveError::BuildDispatchPanic {
message: format!("{stored_error}"),
help: miette::Diagnostic::help(stored_error).map(|help| help.to_string()),
}
.into());
} else {
Expand Down
52 changes: 27 additions & 25 deletions crates/pixi_core/src/lock_file/update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ use pixi_uv_conversions::{
use pypi_mapping::{self, PurlDerivationClient};
use pypi_modifiers::pypi_marker_env::determine_marker_environment;
use rattler::package_cache::PackageCache;
use rattler_conda_types::{Arch, GenericVirtualPackage, PackageName, ParseChannelError, Platform};
use rattler_conda_types::{Arch, GenericVirtualPackage, PackageName, ParseChannelError};
use rattler_lock::{LockFile, LockedPackage, ParseCondaLockError};
use serde::{Deserialize, Serialize};
use thiserror::Error;
Expand Down Expand Up @@ -2693,19 +2693,31 @@ impl<'p> UpdateContext<'p> {
}
}

/// Constructs an error that indicates that the current platform cannot solve
/// pypi dependencies because there is no python interpreter available for the
/// current platform.
/// Constructs the error shown when pypi dependencies cannot be solved for want
/// of a usable Python interpreter, disambiguating the two distinct causes:
///
/// - The environment declares no platform this machine can run (e.g. a
/// `__cuda`-requiring platform on a host without CUDA). This is a
/// virtual-package mismatch, not a missing interpreter, so it is surfaced as
/// an [`UnsupportedPlatformError`], which names the unsatisfied requirements
/// and suggests the matching `CONDA_OVERRIDE_*` mocks.
/// - A runnable platform exists but Python is not among its dependencies.
fn make_unsupported_pypi_platform_error(
environment: &Environment<'_>,
top_level_error: bool,
) -> Report {
// No host-runnable platform: the real cause is unsatisfied host virtual
// packages, not a missing interpreter. Report which requirements are unmet
// instead of the misleading `no compatible Python interpreter for '<subdir>'`.
let Some(best_platform) = environment.best_declared_platform() else {
return Report::new(environment.unsupported_platform_error());
};

// A runnable platform exists, so Python is simply missing from its
// dependencies. `best_declared_platform` only returns platforms the
// environment declares, so this platform is always in its `platforms` list.
let grouped_environment = GroupedEnvironment::from(environment.clone());
let current_platform_name = environment
.best_declared_platform()
.map(|p| p.name().clone())
.unwrap_or_else(|| Platform::current().into());
let platforms = environment.platforms();
let platform_name = best_platform.name();

let mut diag = if top_level_error {
MietteDiagnostic::new(format!(
Expand All @@ -2715,29 +2727,19 @@ fn make_unsupported_pypi_platform_error(
GroupedEnvironment::Group(_) => "solve group",
GroupedEnvironment::Environment(_) => "environment",
},
consts::PLATFORM_STYLE.apply_to(&current_platform_name),
consts::PLATFORM_STYLE.apply_to(platform_name),
))
} else {
MietteDiagnostic::new(format!(
"there is no compatible Python interpreter for '{}'",
consts::PLATFORM_STYLE.apply_to(&current_platform_name),
consts::PLATFORM_STYLE.apply_to(platform_name),
))
};

let help_message = if !platforms.contains(&current_platform_name) {
// State 1: The current platform is not in the `platforms` list
format!(
"Try: {}",
consts::TASK_STYLE.apply_to(format!(
"pixi workspace platform add {current_platform_name}"
)),
)
} else {
// State 2: Python is not in the dependencies.
format!("Try: {}", consts::TASK_STYLE.apply_to("pixi add python"))
};

diag.help = Some(help_message);
diag.help = Some(format!(
"Try: {}",
consts::TASK_STYLE.apply_to("pixi add python")
));

Report::new(diag)
}
Expand Down
58 changes: 48 additions & 10 deletions crates/pixi_core/src/workspace/environment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use std::{
};

use indexmap::IndexMap;
use itertools::Either;
use itertools::{Either, Itertools};
use pixi_consts::consts;
use pixi_manifest::{
self as manifest, EnvironmentName, Feature, FeatureName, FeaturesExt, HasFeaturesIter,
Expand Down Expand Up @@ -192,11 +192,43 @@ impl<'p> Environment<'p> {
.declared_virtual_packages()
.to_vec();
let env_platforms = self.platforms();
self.workspace_manifest()

// The candidates are the workspace platforms whose subdir matches this
// host and whose declared virtual packages the host satisfies; the
// selection is the first that the environment itself declares.
let candidates = self
.workspace_manifest()
.workspace
.possible_pixi_platforms(current, &system_virtual_packages)
.into_iter()
.find(|p| env_platforms.contains(p.name()))
.possible_pixi_platforms(current, &system_virtual_packages);
let selected = candidates
.iter()
.copied()
.find(|p| env_platforms.contains(p.name()));

if tracing::enabled!(tracing::Level::DEBUG) {
let mut declared: Vec<&str> = env_platforms.iter().map(|p| p.as_str()).collect();
declared.sort_unstable();
tracing::debug!(
"selecting best platform for environment '{}' on host subdir '{}' \
(host virtual packages: [{}]); environment declares [{}]; \
host-runnable candidates [{}]; selected {}",
self.name(),
current,
system_virtual_packages
.iter()
.map(|vp| format!("{}={}", vp.name.as_normalized(), vp.version))
.format(", "),
declared.iter().format(", "),
candidates.iter().map(|p| p.name().as_str()).format(", "),
match selected {
Some(p) => format!("'{}'", p.name().as_str()),
None => "<none>: no host-runnable candidate is declared by this environment"
.to_string(),
},
);
}

selected
}

/// Picks the workspace platform install/solve should target, with
Expand Down Expand Up @@ -240,23 +272,28 @@ impl<'p> Environment<'p> {
.declared_virtual_packages()
.to_vec();
let env_platforms = self.platforms();
let unsatisfied_requirements = self
.workspace_manifest()
.workspace
.unsatisfied_platform_requirements(current, &system_virtual_packages, &env_platforms);
let workspace = &self.workspace_manifest().workspace;
let unsatisfied_requirements = workspace.unsatisfied_platform_requirements(
current,
&system_virtual_packages,
&env_platforms,
);
let platform_diagnostics =
workspace.platform_match_diagnostics(current, &system_virtual_packages, &env_platforms);
UnsupportedPlatformError {
environments_platforms: env_platforms.into_iter().collect(),
environment: self.name().clone(),
platform: current,
unsatisfied_requirements,
platform_diagnostics,
}
}

/// Emits a one-time warning if this environment requires platform emulation
/// (e.g. Rosetta on Apple Silicon Macs).
///
/// This should only be called when the environment is actually being
/// installed or activated not during lock file solving, which is
/// installed or activated -- not during lock file solving, which is
/// cross-platform and does not use emulation.
pub fn emit_emulation_warning(&self) {
if std::env::var(consts::PIXI_OVERRIDE_PLATFORM).is_ok() {
Expand Down Expand Up @@ -432,6 +469,7 @@ impl<'p> Environment<'p> {
environment: self.name().clone(),
platform: platform.subdir(),
unsatisfied_requirements: Vec::new(),
platform_diagnostics: Vec::new(),
});
}

Expand Down
Loading
Loading