diff --git a/crates/pixi_build_backend/src/specs_conversion.rs b/crates/pixi_build_backend/src/specs_conversion.rs index 6f5ba73987..cef119f96d 100644 --- a/crates/pixi_build_backend/src/specs_conversion.rs +++ b/crates/pixi_build_backend/src/specs_conversion.rs @@ -78,6 +78,8 @@ pub fn convert_variant_to_pixi_build_types( pub fn to_rattler_build_selector(selector: &TargetSelector, platform_kind: PlatformKind) -> String { match selector { TargetSelector::Platform(p) => format!("{platform_kind}_platform == '{p}'"), + // Expression selectors are passed through directly to rattler-build. + TargetSelector::Expression(expr) => expr.clone(), _ => selector.to_string(), } } @@ -494,6 +496,39 @@ mod test { assert_eq!(match_spec.to_string(), "python"); } + #[test] + fn test_to_rattler_build_selector_platform() { + let selector = TargetSelector::Platform("linux-64".to_string()); + assert_eq!( + to_rattler_build_selector(&selector, PlatformKind::Host), + "host_platform == 'linux-64'" + ); + } + + #[test] + fn test_to_rattler_build_selector_expression_passthrough() { + let selector = TargetSelector::Expression("host_platform == build_platform".to_string()); + assert_eq!( + to_rattler_build_selector(&selector, PlatformKind::Host), + "host_platform == build_platform" + ); + + let selector2 = TargetSelector::Expression("target_platform != 'linux-64'".to_string()); + assert_eq!( + to_rattler_build_selector(&selector2, PlatformKind::Build), + "target_platform != 'linux-64'" + ); + } + + #[test] + fn test_to_rattler_build_selector_family() { + let selector = TargetSelector::Unix; + assert_eq!( + to_rattler_build_selector(&selector, PlatformKind::Host), + "unix" + ); + } + #[test] fn test_binary_package_conversion_preserves_condition() { use rattler_conda_types::{MatchSpecCondition, ParseMatchSpecOptions, RepodataRevision}; diff --git a/crates/pixi_build_backend/src/traits/targets.rs b/crates/pixi_build_backend/src/traits/targets.rs index 1132d5cd98..1f0ca1b809 100644 --- a/crates/pixi_build_backend/src/traits/targets.rs +++ b/crates/pixi_build_backend/src/traits/targets.rs @@ -155,6 +155,7 @@ impl TargetSelector for pbt::TargetSelector { pbt::TargetSelector::Unix => platform.is_unix(), pbt::TargetSelector::Win => platform.is_windows(), pbt::TargetSelector::MacOs => platform.is_osx(), + pbt::TargetSelector::Expression(_) => false, } } } diff --git a/crates/pixi_build_backend_passthrough/src/lib.rs b/crates/pixi_build_backend_passthrough/src/lib.rs index 6aedd049dc..db8d1ea96a 100644 --- a/crates/pixi_build_backend_passthrough/src/lib.rs +++ b/crates/pixi_build_backend_passthrough/src/lib.rs @@ -789,6 +789,7 @@ fn matches_target_selector(selector: &TargetSelector, platform: Platform) -> boo TargetSelector::Win => platform.is_windows(), TargetSelector::MacOs => platform.is_osx(), TargetSelector::Platform(target_platform) => target_platform == platform.as_str(), + TargetSelector::Expression(_) => false, } } diff --git a/crates/pixi_build_type_conversions/src/project_model.rs b/crates/pixi_build_type_conversions/src/project_model.rs index 7cc48f1420..c455b872dd 100644 --- a/crates/pixi_build_type_conversions/src/project_model.rs +++ b/crates/pixi_build_type_conversions/src/project_model.rs @@ -188,6 +188,7 @@ pub fn to_target_selector_v1(selector: &TargetSelector) -> pbt::TargetSelector { TargetSelector::Linux => pbt::TargetSelector::Linux, TargetSelector::Win => pbt::TargetSelector::Win, TargetSelector::MacOs => pbt::TargetSelector::MacOs, + TargetSelector::Expression(expr) => pbt::TargetSelector::Expression(expr.clone()), } } diff --git a/crates/pixi_build_types/src/project_model.rs b/crates/pixi_build_types/src/project_model.rs index 9c38df0310..f4e8dd55ba 100644 --- a/crates/pixi_build_types/src/project_model.rs +++ b/crates/pixi_build_types/src/project_model.rs @@ -85,8 +85,13 @@ impl IsDefault for ProjectModel { } } -/// Represents a target selector. Currently, we only support explicit platform -/// selection. +/// Represents a target selector. +/// +/// In addition to explicit platform selection, the `Expression` variant allows +/// arbitrary selector expressions wrapped in `if(...)` (e.g. +/// `"if(host_platform == build_platform)"`) that are passed through directly to +/// rattler-build. The stored string is the bare inner expression without the +/// `if(...)` wrapper. #[derive(Debug, Clone, DeserializeFromStr, SerializeDisplay, Eq, PartialEq)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub enum TargetSelector { @@ -96,7 +101,10 @@ pub enum TargetSelector { Win, MacOs, Platform(String), - // TODO: Add minijinja coolness here. + /// A free-form selector expression passed through to rattler-build. + /// Written by users as `if()`; the stored value is the bare + /// inner expression. + Expression(String), } impl Display for TargetSelector { @@ -107,6 +115,7 @@ impl Display for TargetSelector { TargetSelector::Win => write!(f, "win"), TargetSelector::MacOs => write!(f, "macos"), TargetSelector::Platform(p) => write!(f, "{p}"), + TargetSelector::Expression(expr) => write!(f, "if({expr})"), } } } @@ -114,16 +123,27 @@ impl FromStr for TargetSelector { type Err = Infallible; fn from_str(s: &str) -> Result { - match s { - "unix" => Ok(TargetSelector::Unix), - "linux" => Ok(TargetSelector::Linux), - "win" => Ok(TargetSelector::Win), - "macos" => Ok(TargetSelector::MacOs), - _ => Ok(TargetSelector::Platform(s.to_string())), + // Expression selectors are wrapped in `if(...)`, e.g. + // `if(host_platform == build_platform)`. + if let Some(inner) = strip_if_wrapper(s) { + return Ok(TargetSelector::Expression(inner.to_string())); } + Ok(match s { + "unix" => TargetSelector::Unix, + "linux" => TargetSelector::Linux, + "win" => TargetSelector::Win, + "macos" => TargetSelector::MacOs, + _ => TargetSelector::Platform(s.to_string()), + }) } } +/// If `s` is wrapped as `if()`, return the trimmed inner expression. +fn strip_if_wrapper(s: &str) -> Option<&str> { + let inner = s.strip_prefix("if(")?.strip_suffix(')')?; + Some(inner.trim()) +} + /// A collect of targets including a default target. #[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] @@ -601,6 +621,10 @@ impl Hash for TargetSelector { 4u8.hash(state); p.hash(state); } + TargetSelector::Expression(expr) => { + 5u8.hash(state); + expr.hash(state); + } } } } diff --git a/crates/pixi_manifest/src/lib.rs b/crates/pixi_manifest/src/lib.rs index c124ecbe67..414691cb90 100644 --- a/crates/pixi_manifest/src/lib.rs +++ b/crates/pixi_manifest/src/lib.rs @@ -54,7 +54,9 @@ pub use spec_type::SpecType; pub use system_requirements::{ GLIBC_FAMILY, LibCFamilyAndVersion, LibCSystemRequirement, MUSL_FAMILY, SystemRequirements, }; -pub use target::{PackageTarget, TargetSelector, Targets, WorkspaceTarget}; +pub use target::{ + PackageTarget, ParseTargetSelectorError, TargetSelector, Targets, WorkspaceTarget, +}; pub use task::{Task, TaskName}; use thiserror::Error; pub use warning::{Warning, WarningWithSource, WithWarnings}; diff --git a/crates/pixi_manifest/src/target.rs b/crates/pixi_manifest/src/target.rs index 1bfd1064c0..1139911e92 100644 --- a/crates/pixi_manifest/src/target.rs +++ b/crates/pixi_manifest/src/target.rs @@ -442,8 +442,12 @@ impl PackageTarget { } } -/// Represents a target selector. Currently we only support explicit platform -/// selection. +/// Represents a target selector. +/// +/// In addition to explicit platform selection, the `Expression` variant allows +/// arbitrary selector expressions (e.g. `"host_platform == build_platform"`) +/// that are passed through directly to rattler-build. Expression selectors are +/// only valid in the `[package]` section of the manifest. #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub enum TargetSelector { // Platform specific configuration @@ -452,11 +456,16 @@ pub enum TargetSelector { Linux, Win, MacOs, - // TODO: Add minijinja coolness here. + /// A free-form selector expression that is passed through to rattler-build. + /// Only valid in the `[package]` section. + Expression(String), } impl TargetSelector { /// Returns true if this selector matches the given platform. + /// + /// Expression selectors never match a specific platform since they are + /// evaluated by rattler-build, not by pixi. pub fn matches(&self, platform: Platform) -> bool { match self { TargetSelector::Platform(p) => p == &platform, @@ -464,8 +473,14 @@ impl TargetSelector { TargetSelector::Unix => platform.is_unix(), TargetSelector::Win => platform.is_windows(), TargetSelector::MacOs => platform.is_osx(), + TargetSelector::Expression(_) => false, } } + + /// Returns `true` if this is a free-form expression selector. + pub fn is_expression(&self) -> bool { + matches!(self, TargetSelector::Expression(_)) + } } impl std::fmt::Display for TargetSelector { @@ -476,6 +491,7 @@ impl std::fmt::Display for TargetSelector { TargetSelector::Unix => write!(f, "unix"), TargetSelector::Win => write!(f, "win"), TargetSelector::MacOs => write!(f, "osx"), + TargetSelector::Expression(expr) => write!(f, "if({expr})"), } } } @@ -486,8 +502,53 @@ impl From for TargetSelector { } } +/// Error returned when a target selector key cannot be parsed. +/// +/// Wraps [`ParsePlatformError`] and adds a hint when the input looks like a +/// rattler-build selector expression (contains characters outside +/// `[a-z0-9-]`), nudging users toward the required `if(...)` wrapper. +#[derive(Debug, thiserror::Error)] +pub enum ParseTargetSelectorError { + LooksLikeExpression { + source: ParsePlatformError, + input: String, + }, + + #[error(transparent)] + Platform(#[from] ParsePlatformError), +} + +impl std::fmt::Display for ParseTargetSelectorError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Platform(source) => write!(f, "{source}"), + Self::LooksLikeExpression { source, input } => { + // Encode the help text so `TomlDiagnostic` can render it as a + // separate `help:` section in miette output. + let help = format!( + "'{input}' looks like a selector expression. Wrap it in `if(...)` to pass it through to rattler-build, e.g. `if({input})`." + ); + write!( + f, + "{}", + pixi_toml::custom_error_message_with_help(&source.to_string(), &help) + ) + } + } + } +} + +/// Returns true if `s` cannot be a platform or family name because it contains +/// characters outside `[a-z0-9-]`. Such strings most likely intend to be +/// rattler-build selector expressions and should be wrapped with `if(...)`. +fn looks_like_expression(s: &str) -> bool { + !s.is_empty() + && s.chars() + .any(|c| !(c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')) +} + impl FromStr for TargetSelector { - type Err = ParsePlatformError; + type Err = ParseTargetSelectorError; fn from_str(s: &str) -> Result { match s { @@ -495,8 +556,35 @@ impl FromStr for TargetSelector { "unix" => Ok(TargetSelector::Unix), "win" => Ok(TargetSelector::Win), "osx" => Ok(TargetSelector::MacOs), - _ => Platform::from_str(s).map(TargetSelector::Platform), + _ => Platform::from_str(s) + .map(TargetSelector::Platform) + .map_err(|e| { + if looks_like_expression(s) { + ParseTargetSelectorError::LooksLikeExpression { + source: e, + input: s.to_string(), + } + } else { + ParseTargetSelectorError::Platform(e) + } + }), + } + } +} + +impl TargetSelector { + /// Parse a target selector that accepts either a platform name (or + /// `unix`/`linux`/`win`/`osx` family) or an `if()` wrapper for + /// arbitrary rattler-build selector expressions. This is used in the + /// `[package]` section where expression selectors are allowed. + /// + /// The stored expression is the bare inner string without the `if(...)` + /// wrapper. + pub fn from_str_or_expression(s: &str) -> Result { + if let Some(inner) = s.strip_prefix("if(").and_then(|r| r.strip_suffix(')')) { + return Ok(TargetSelector::Expression(inner.trim().to_string())); } + s.parse() } } @@ -988,4 +1076,130 @@ mod tests { "Expected foo=1.0 on osx-arm64" ); } + + #[test] + fn test_target_selector_parsing() { + use crate::TargetSelector; + + // Platform family selectors + assert_eq!( + TargetSelector::from_str("linux").unwrap(), + TargetSelector::Linux + ); + assert_eq!( + TargetSelector::from_str("unix").unwrap(), + TargetSelector::Unix + ); + assert_eq!( + TargetSelector::from_str("win").unwrap(), + TargetSelector::Win + ); + assert_eq!( + TargetSelector::from_str("osx").unwrap(), + TargetSelector::MacOs + ); + + // Platform-specific selectors + assert!(matches!( + TargetSelector::from_str("linux-64").unwrap(), + TargetSelector::Platform(_) + )); + + // Unknown strings should fail with from_str + assert!(TargetSelector::from_str("host_platform == build_platform").is_err()); + assert!(TargetSelector::from_str("foobar").is_err()); + } + + #[test] + fn test_target_selector_expression_hint_on_unknown_platform() { + use crate::{TargetSelector, target::ParseTargetSelectorError}; + + // Strings containing characters outside `[a-z0-9-]` look like + // selector expressions and produce a hint pointing to `if(...)`. + let err = TargetSelector::from_str("host_platform == build_platform").unwrap_err(); + assert!( + matches!(err, ParseTargetSelectorError::LooksLikeExpression { .. }), + "expected LooksLikeExpression hint, got: {err:?}" + ); + let message = err.to_string(); + assert!( + message.contains("looks like a selector expression"), + "hint missing from error message: {message}" + ); + assert!( + message.contains("if(host_platform == build_platform)"), + "if(...) suggestion missing from error message: {message}" + ); + + // Plain unknown strings (matching `[a-z0-9-]`) just report the + // platform parse error without the expression hint. + let err = TargetSelector::from_str("foobar").unwrap_err(); + assert!( + matches!(err, ParseTargetSelectorError::Platform(_)), + "expected plain platform error, got: {err:?}" + ); + } + + #[test] + fn test_target_selector_expression_hint_triggers() { + use crate::TargetSelector; + // Anything outside `[a-z0-9-]` should trigger the expression hint. + let cases = [ + "host_platform == build_platform", + "host_platform != 'linux-64'", + "matches(python, '>=3.10')", + "linux 64", // contains a space + ]; + for case in cases { + let err = TargetSelector::from_str(case).unwrap_err(); + assert!( + matches!( + err, + super::ParseTargetSelectorError::LooksLikeExpression { .. } + ), + "expected expression hint for '{case}', got: {err:?}" + ); + } + } + + #[test] + fn test_target_selector_from_str_or_expression() { + use crate::TargetSelector; + + // Known platforms still parse as platform selectors + assert_eq!( + TargetSelector::from_str_or_expression("linux").unwrap(), + TargetSelector::Linux + ); + assert!(matches!( + TargetSelector::from_str_or_expression("linux-64").unwrap(), + TargetSelector::Platform(_) + )); + + // `if(...)` strings become expression selectors, storing the bare + // inner expression. + let expr = + TargetSelector::from_str_or_expression("if(host_platform == build_platform)").unwrap(); + assert!( + matches!(expr, TargetSelector::Expression(ref s) if s == "host_platform == build_platform") + ); + assert!(expr.is_expression()); + // Display re-wraps with `if(...)` for round-tripping. + assert_eq!(expr.to_string(), "if(host_platform == build_platform)"); + + // Expression selectors don't match any platform + assert!(!expr.matches(rattler_conda_types::Platform::Linux64)); + assert!(!expr.matches(rattler_conda_types::Platform::OsxArm64)); + + // Another expression — inner whitespace is trimmed. + let expr2 = + TargetSelector::from_str_or_expression("if( host_platform != 'linux-64' )").unwrap(); + assert!(expr2.is_expression()); + if let TargetSelector::Expression(inner) = &expr2 { + assert_eq!(inner, "host_platform != 'linux-64'"); + } + + // Bare expression strings without `if(...)` are rejected. + assert!(TargetSelector::from_str_or_expression("host_platform == build_platform").is_err()); + } } diff --git a/crates/pixi_manifest/src/toml/build_backend.rs b/crates/pixi_manifest/src/toml/build_backend.rs index ced90c7093..e9b872983c 100644 --- a/crates/pixi_manifest/src/toml/build_backend.rs +++ b/crates/pixi_manifest/src/toml/build_backend.rs @@ -12,10 +12,10 @@ use std::borrow::Cow; use toml_span::{DeserError, Error, Spanned, Value, de_helpers::TableHelper, value::ValueInner}; use crate::{ - PackageBuild, TargetSelector, TomlError, WithWarnings, + PackageBuild, TomlError, WithWarnings, build_system::BuildBackend, error::GenericError, - toml::build_target::TomlPackageBuildTarget, + toml::{build_target::TomlPackageBuildTarget, package::PackageTargetKey}, utils::{PixiSpanned, package_map::UniquePackageMap}, warning::Deprecation, }; @@ -27,7 +27,7 @@ pub struct TomlPackageBuild { pub additional_dependencies: UniquePackageMap, pub source: Option, pub configuration: Option, - pub target: IndexMap, TomlPackageBuildTarget>, + pub target: IndexMap, TomlPackageBuildTarget>, pub warnings: Vec, pub build_string_prefix: Option, @@ -114,7 +114,9 @@ impl TomlPackageBuild { .target .into_iter() .flat_map(|(selector, target)| { - target.config.map(|config| (selector.into_inner(), config)) + target + .config + .map(|config| (selector.into_inner().0, config)) }) .collect::>(); diff --git a/crates/pixi_manifest/src/toml/manifest.rs b/crates/pixi_manifest/src/toml/manifest.rs index 175a9a945d..d6bf5fccc5 100644 --- a/crates/pixi_manifest/src/toml/manifest.rs +++ b/crates/pixi_manifest/src/toml/manifest.rs @@ -1162,6 +1162,36 @@ mod test { )); } + #[test] + fn test_expression_selector_rejected_in_workspace_target() { + // Expression selectors should be rejected in the workspace target section + // because TargetSelector::from_str doesn't accept arbitrary expressions. + // Only the [package.target] section accepts expression selectors. The + // error must include a hint pointing users at the `if(...)` wrapper. + assert_snapshot!(expect_parse_failure( + r#" + [workspace] + name = "test" + channels = [] + platforms = ['linux-64'] + + [target."host_platform == build_platform".dependencies] + foo = "*" + "#, + ), @r###" + × 'host_platform == build_platform' is not a known platform. Valid platforms are 'noarch', 'unknown', 'linux-32', 'linux-64', 'linux-aarch64', 'linux-armv6l', 'linux-armv7l', 'linux-loongarch64', + │ 'linux-ppc64le', 'linux-ppc64', 'linux-ppc', 'linux-s390x', 'linux-riscv32', 'linux-riscv64', 'freebsd-32', 'freebsd-64', 'freebsd-arm64', 'osx-64', 'osx-arm64', 'win-32', 'win-64', 'win-arm64', + │ 'emscripten-wasm32', 'wasi-wasm32', 'zos-z' + ╭─[pixi.toml:7:18] + 6 │ + 7 │ [target."host_platform == build_platform".dependencies] + · ─────────────────────────────── + 8 │ foo = "*" + ╰──── + help: 'host_platform == build_platform' looks like a selector expression. Wrap it in `if(...)` to pass it through to rattler-build, e.g. `if(host_platform == build_platform)`. + "###); + } + #[test] fn test_unknown_feature() { assert_snapshot!(expect_parse_failure( diff --git a/crates/pixi_manifest/src/toml/package.rs b/crates/pixi_manifest/src/toml/package.rs index 84debe74a6..be710104b7 100644 --- a/crates/pixi_manifest/src/toml/package.rs +++ b/crates/pixi_manifest/src/toml/package.rs @@ -3,7 +3,7 @@ use std::path::{Path, PathBuf}; use indexmap::IndexMap; use pixi_spec::TomlSpec; pub use pixi_toml::TomlFromStr; -use pixi_toml::{DeserializeAs, Same, TomlIndexMap, TomlWith}; +use pixi_toml::{DeserializeAs, FromKey, Same, TomlIndexMap, TomlWith}; use rattler_conda_types::{PackageName, Version}; use thiserror::Error; use toml_span::{DeserError, Span, Spanned, Value, de_helpers::TableHelper}; @@ -19,6 +19,21 @@ use crate::{ utils::{PixiSpanned, inheritable_package_map::InheritablePackageMap}, }; +/// A wrapper around [`TargetSelector`] for use as a TOML key in the +/// `[package.target]` section. Unlike `TargetSelector::from_str`, this +/// additionally accepts `if()` wrappers for selector expressions +/// passed through to rattler-build. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct PackageTargetKey(pub TargetSelector); + +impl<'de> FromKey<'de> for PackageTargetKey { + type Err = crate::target::ParseTargetSelectorError; + + fn from_key(key: toml_span::value::Key<'de>) -> Result { + TargetSelector::from_str_or_expression(&key.name).map(PackageTargetKey) + } +} + /// Represents a field that can either have a direct value or inherit from /// workspace #[derive(Debug, Clone)] @@ -137,7 +152,7 @@ pub struct TomlPackage { pub build_dependencies: Option>, pub run_dependencies: Option>, pub run_constraints: Option>, - pub target: IndexMap, TomlPackageTarget>, + pub target: IndexMap, TomlPackageTarget>, pub span: Span, } @@ -366,6 +381,10 @@ impl TomlPackage { .into_iter() .map(|(selector, target)| { let target = target.into_package_target(preview, &workspace_dependencies)?; + let selector = PixiSpanned { + value: selector.value.0, + span: selector.span, + }; Ok::<_, TomlError>((selector, target)) }) .collect::>()?; @@ -1351,6 +1370,82 @@ mod test { assert!(manifest.package.readme.is_some()); } + #[test] + fn test_expression_selector_in_package_target() { + let input = r#" + name = "package-name" + version = "1.0.0" + + [build] + backend = { name = "bla", version = "1.0" } + + [target."if(host_platform == build_platform)".build-dependencies] + cmake = "*" + "#; + let package = TomlPackage::from_toml_str(input).unwrap(); + let workspace = WorkspacePackageProperties::default(); + + let parsed = package + .into_manifest( + workspace, + PackageDefaults::default(), + &Preview::default(), + Path::new(""), + ) + .unwrap(); + + // Verify the expression selector target was parsed correctly + let targets = &parsed.value.targets; + let expr_target = targets + .iter() + .find_map(|(target, selector)| { + selector.and_then(|s| { + if s.is_expression() { + Some((s, target)) + } else { + None + } + }) + }) + .expect("expected an expression selector target"); + // Display re-wraps the expression in `if(...)` for round-tripping. + assert_eq!( + expr_target.0.to_string(), + "if(host_platform == build_platform)" + ); + assert!(expr_target.1.build_dependencies().is_some()); + } + + #[test] + fn test_bare_expression_in_package_target_is_rejected() { + // Without the `if(...)` wrapper, an expression-like key must fail to + // parse as a platform. The error must include a hint pointing users + // at the `if(...)` wrapper. + assert_snapshot!(expect_parse_failure( + r#" + name = "package-name" + version = "1.0.0" + + [build] + backend = { name = "bla", version = "1.0" } + + [target."host_platform == build_platform".build-dependencies] + cmake = "*" + "#, + ), @r###" + × 'host_platform == build_platform' is not a known platform. Valid platforms are 'noarch', 'unknown', 'linux-32', 'linux-64', 'linux-aarch64', 'linux-armv6l', 'linux-armv7l', 'linux-loongarch64', + │ 'linux-ppc64le', 'linux-ppc64', 'linux-ppc', 'linux-s390x', 'linux-riscv32', 'linux-riscv64', 'freebsd-32', 'freebsd-64', 'freebsd-arm64', 'osx-64', 'osx-arm64', 'win-32', 'win-64', 'win-arm64', + │ 'emscripten-wasm32', 'wasi-wasm32', 'zos-z' + ╭─[pixi.toml:8:18] + 7 │ + 8 │ [target."host_platform == build_platform".build-dependencies] + · ─────────────────────────────── + 9 │ cmake = "*" + ╰──── + help: 'host_platform == build_platform' looks like a selector expression. Wrap it in `if(...)` to pass it through to rattler-build, e.g. `if(host_platform == build_platform)`. + "###); + } + #[test] fn test_rebase_workspace_path_specs_relativizes_to_member() { use indexmap::IndexMap;