From 12c0e09b8a3793e073a5ba190503ae83566dbac5 Mon Sep 17 00:00:00 2001 From: hiyouwhatsapp <89997383+hiyouwhatsapp@users.noreply.github.com> Date: Tue, 3 Nov 2026 23:14:46 +0200 Subject: [PATCH 1/9] fix(macos): share validated deployment and SDK versions with LLVM linker Resolve independent minimum and SDK versions, infer SDK versions from named bundles, and reject invalid or incompatible platform values before link command execution Signed-off-by: hiyouwhatsapp <89997383+hiyouwhatsapp@users.noreply.github.com> --- llvm/src/backend.rs | 26 ++++++++-- llvm/src/lib.rs | 1 + llvm/src/macos.rs | 121 ++++++++++++++++++++++++++++++++++++++++++++ src/flags.rs | 2 + src/runner.rs | 2 + 5 files changed, 147 insertions(+), 5 deletions(-) create mode 100644 llvm/src/macos.rs diff --git a/llvm/src/backend.rs b/llvm/src/backend.rs index 6c6946e1..971dc022 100644 --- a/llvm/src/backend.rs +++ b/llvm/src/backend.rs @@ -32,6 +32,8 @@ pub struct BackendOptions { pub code_model: Option, pub relocation_model: Option, pub sysroot: Option, + pub macos_deployment_target: Option, + pub macos_sdk_version: Option, pub linker: Option, pub link_args: Vec, pub no_default_libs: bool, @@ -246,7 +248,7 @@ pub fn link_objects( .arg("--export-if-defined=main") .arg("--export-memory"); } else if backend.linker.is_none() { - append_lld_target_args(&mut cmd, target, backend); + append_lld_target_args(&mut cmd, target, backend)?; } for obj in objects { @@ -300,10 +302,23 @@ fn default_lld_for_target(target: &str) -> String { } } -fn append_lld_target_args(cmd: &mut Command, target: &str, backend: &BackendOptions) { +fn append_lld_target_args( + cmd: &mut Command, + target: &str, + backend: &BackendOptions, +) -> Result<(), CodegenError> { if is_darwin_target(target) { let spec = target_spec_for_triple(target) .expect("Darwin linker configuration requires a registered target"); + let versions = crate::macos::resolve_versions( + backend.macos_deployment_target.as_deref(), + None, + backend.macos_sdk_version.as_deref(), + None, + backend.sysroot.as_deref(), + None, + ) + .map_err(|message| CodegenError::new(CodegenPhase::Link, "macOS platform version", message))?; cmd.arg("-arch") .arg(if spec.architecture.name() == "aarch64" { "arm64" @@ -312,13 +327,13 @@ fn append_lld_target_args(cmd: &mut Command, target: &str, backend: &BackendOpti }) .arg("-platform_version") .arg("macos") - .arg("11.0") - .arg("11.0"); + .arg(&versions.deployment) + .arg(&versions.sdk); if let Some(sysroot) = &backend.sysroot { cmd.arg("-syslibroot").arg(sysroot); } - return; + return Ok(()); } if let Some(emulation) = elf_lld_emulation(target) { @@ -327,6 +342,7 @@ fn append_lld_target_args(cmd: &mut Command, target: &str, backend: &BackendOpti if let Some(sysroot) = &backend.sysroot { cmd.arg(format!("--sysroot={}", sysroot)); } + Ok(()) } fn expand_lld_link_args(link_args: &[String]) -> Vec { diff --git a/llvm/src/lib.rs b/llvm/src/lib.rs index 345e38cf..c8a1ee15 100644 --- a/llvm/src/lib.rs +++ b/llvm/src/lib.rs @@ -40,6 +40,7 @@ pub mod backend; pub mod codegen; pub mod expression; pub mod importgen; +pub mod macos; pub mod msvc; pub mod statement; pub mod toolchain; diff --git a/llvm/src/macos.rs b/llvm/src/macos.rs new file mode 100644 index 00000000..fbd10299 --- /dev/null +++ b/llvm/src/macos.rs @@ -0,0 +1,121 @@ +// This file is part of the Wave language project. +// Copyright (c) 2024–2026 Wave Foundation +// Copyright (c) 2024–2026 LunaStev and contributors +// +// This Source Code Form is subject to the terms of the +// Mozilla Public License, v. 2.0. +// If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. +// +// SPDX-License-Identifier: MPL-2.0 +// AI TRAINING NOTICE: Prohibited without prior written permission. No use for machine learning or generative AI training, fine-tuning, distillation, embedding, or dataset creation. + +//! Validated macOS linker platform versions, shared by the driver and backend. +//! +//! The deployment minimum and SDK version are independent settings. An SDK +//! directory such as MacOSX15.4.sdk can supply its version without requiring +//! a compiler-specific SDK metadata parser. + +pub const DEFAULT_DEPLOYMENT_TARGET: &str = "11.0"; +pub const DEFAULT_SDK_VERSION: &str = "15.0"; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PlatformVersions { + pub deployment: String, + pub sdk: String, +} + +fn parsed_version(text: &str, label: &str) -> Result<[u32; 3], String> { + let parts = text.split('.').collect::>(); + if !(2..=3).contains(&parts.len()) + || parts.iter().any(|part| part.is_empty() || !part.bytes().all(|b| b.is_ascii_digit())) + { + return Err(format!("invalid {label} version {text:?}; expected major.minor[.patch]")); + } + let mut version = [0; 3]; + for (index, part) in parts.iter().enumerate() { + version[index] = part + .parse::() + .map_err(|_| format!("invalid {label} version {text:?}"))?; + } + if !(10..=999).contains(&version[0]) { + return Err(format!("invalid {label} version {text:?}; major must be at least 10")); + } + Ok(version) +} + +/// Extract a version only from a versioned macOS SDK bundle name. +pub fn sdk_version_from_path(sysroot: &str) -> Option { + let file = std::path::Path::new(sysroot).file_name()?.to_str()?; + let value = file.strip_prefix("MacOSX")?.strip_suffix(".sdk")?; + parsed_version(value, "macOS SDK").ok().map(|_| value.to_string()) +} + +/// Resolve and validate both linker versions before constructing a link command. +/// +/// Explicit Wave flags take precedence over environment variables. An SDK +/// version from the selected SDK directory takes precedence over the host SDK +/// discovery result. Defaults are independent, and never use the host macOS +/// release as a proxy for the SDK or deployment policy. +pub fn resolve_versions( + deployment_override: Option<&str>, + deployment_env: Option<&str>, + sdk_override: Option<&str>, + sdk_env: Option<&str>, + sysroot: Option<&str>, + discovered_sdk: Option<&str>, +) -> Result { + let deployment = deployment_override + .or(deployment_env) + .unwrap_or(DEFAULT_DEPLOYMENT_TARGET); + let path_sdk = sysroot.and_then(sdk_version_from_path); + let sdk = sdk_override + .or(sdk_env) + .or(path_sdk.as_deref()) + .or(discovered_sdk) + .unwrap_or(DEFAULT_SDK_VERSION); + let minimum = parsed_version(deployment, "macOS deployment target")?; + let selected = parsed_version(sdk, "macOS SDK")?; + if selected < minimum { + return Err(format!( + "macOS SDK version {sdk} is older than deployment target {deployment}" + )); + } + Ok(PlatformVersions { deployment: deployment.into(), sdk: sdk.into() }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn explicit_overrides_env_and_sdk_path() { + let versions = resolve_versions( + Some("12.3"), Some("11.2"), Some("16.1"), Some("15.2"), + Some("/SDKs/MacOSX14.4.sdk"), Some("13.3"), + ).unwrap(); + assert_eq!(versions.deployment, "12.3"); + assert_eq!(versions.sdk, "16.1"); + let versions = resolve_versions( + None, Some("11.2"), None, None, + Some("/SDKs/MacOSX14.4.sdk"), Some("13.3"), + ).unwrap(); + assert_eq!(versions.deployment, "11.2"); + assert_eq!(versions.sdk, "14.4"); + let versions = resolve_versions(None, None, None, Some("15.2"), None, Some("14.0")).unwrap(); + assert_eq!(versions.sdk, "15.2"); + } + + #[test] + fn bad_versions_and_incompatible_sdks_are_rejected() { + for invalid in ["", "11", "11.", ".1", "11.0.0.1", "11.a", "11.0 -arch", "0.0", "999999999999999999.1"] { + assert!(resolve_versions(Some(invalid), None, None, None, None, None).is_err()); + assert!(resolve_versions(None, None, Some(invalid), None, None, None).is_err()); + } + assert!(resolve_versions(Some("15.1"), None, Some("15.0"), None, None, None).is_err()); + assert_eq!(sdk_version_from_path("/SDKs/MacOSX15.5.sdk"), Some("15.5".into())); + assert_eq!(sdk_version_from_path("/SDKs/MacOSX.sdk"), None); + assert_eq!(sdk_version_from_path("/SDKs/iPhoneOS18.0.sdk"), None); + assert_eq!(sdk_version_from_path("/SDKs/MacOSX15.4_bad.sdk"), None); + } +} diff --git a/src/flags.rs b/src/flags.rs index 3dcce685..88a26731 100644 --- a/src/flags.rs +++ b/src/flags.rs @@ -88,6 +88,8 @@ pub struct LlvmFlags { pub relocation_model: Option, pub sysroot: Option, pub sysroot_source: Option, + pub macos_deployment_target: Option, + pub macos_sdk_version: Option, pub linker: Option, pub link_args: Vec, pub no_default_libs: bool, diff --git a/src/runner.rs b/src/runner.rs index a28a82f2..e1d4f532 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -551,6 +551,8 @@ fn build_backend_options(llvm: &LlvmFlags) -> BackendOptions { code_model: llvm.code_model.clone(), relocation_model: llvm.relocation_model.clone(), sysroot: llvm.sysroot.clone(), + macos_deployment_target: llvm.macos_deployment_target.clone(), + macos_sdk_version: llvm.macos_sdk_version.clone(), linker: llvm.linker.clone(), link_args: llvm.link_args.clone(), no_default_libs: llvm.no_default_libs, From 4a0d8c40093e89094089f180552d048bce80a1c7 Mon Sep 17 00:00:00 2001 From: hiyouwhatsapp <89997383+hiyouwhatsapp@users.noreply.github.com> Date: Tue, 3 Nov 2026 23:16:10 +0200 Subject: [PATCH 2/9] feat(cli): expose macOS deployment and SDK version controls Support explicit overrides, environment fallbacks and versioned SDK roots while rendering both values in link plans and target-spec JSON Validate incompatible versions and reject macOS-only flags on other targets before emitting artifacts Signed-off-by: hiyouwhatsapp <89997383+hiyouwhatsapp@users.noreply.github.com> --- src/cli.rs | 138 +++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 119 insertions(+), 19 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 54002149..385c871f 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -474,6 +474,7 @@ fn dispatch_build(global: &Global, build: &BuildRequest) -> Result<(), CliError> let mut effective_global = effective_global_for_build(global, &build); resolve_target_configuration(&mut effective_global.llvm)?; resolve_build_sysroot(&mut effective_global.llvm, &build); + resolve_macos_versions(&mut effective_global.llvm)?; if effective_global.dep.resolved_std_root.is_none() { effective_global.dep.resolved_std_root = Some(parser::import::ResolvedStdRoot::resolve(None)); @@ -1131,6 +1132,32 @@ fn parse_llvm_backend_option( return Ok(true); } + + for (flag, field) in [ + ("--macos-deployment-target", &mut llvm.macos_deployment_target), + ("--macos-sdk-version", &mut llvm.macos_sdk_version), + ] { + if let Some(value) = a.strip_prefix(flag).and_then(|tail| tail.strip_prefix('=')) { + if value.trim().is_empty() { + return Err(CliError::usage(format!("missing value: {flag}="))); + } + *field = Some(value.to_string()); + *i += 1; + return Ok(true); + } + if a == flag { + let value = args.get(*i + 1).ok_or_else(|| { + CliError::usage(format!("missing value: {flag} ")) + })?; + if value.trim().is_empty() { + return Err(CliError::usage(format!("missing value: {flag} "))); + } + *field = Some(value.to_string()); + *i += 2; + return Ok(true); + } + } + if let Some(v) = a.strip_prefix("--sysroot=") { if v.trim().is_empty() { return Err(CliError::usage("missing value: --sysroot=")); @@ -2952,11 +2979,12 @@ fn build_darwin_lld_args( args.push("-arch".to_string()); args.push(darwin_arch(target).to_string()); - let macos_version = macos_deployment_version(); + let versions = macos_versions_for(&global.llvm) + .expect("Darwin linking requires validated macOS platform versions"); args.push("-platform_version".to_string()); args.push("macos".to_string()); - args.push(macos_version.clone()); - args.push(macos_version); + args.push(versions.deployment); + args.push(versions.sdk); let detected_sysroot = detect_macos_sysroot_owned(); if let Some(sysroot) = global.llvm.sysroot.as_deref().or(detected_sysroot.as_deref()) { @@ -3459,22 +3487,48 @@ fn sysroot_path(sysroot: &str, suffix: &str) -> PathBuf { } } -fn macos_deployment_version() -> String { - if let Ok(value) = env::var("MACOSX_DEPLOYMENT_TARGET") { - let value = value.trim(); - if !value.is_empty() { - return value.to_string(); - } - } +fn macos_versions_for(llvm: &LlvmFlags) -> Result { + let deployment_env = env::var("MACOSX_DEPLOYMENT_TARGET").ok(); + let sdk_env = env::var("MACOSX_SDK_VERSION").ok(); + let sysroot = llvm.sysroot.clone().or_else(detect_macos_sysroot_owned); + let from_path = sysroot.as_deref().and_then(llvm::macos::sdk_version_from_path); + let discovered_sdk = if llvm.macos_sdk_version.is_none() + && sdk_env.is_none() + && from_path.is_none() + { + ProcessCommand::new("xcrun") + .args(["--sdk", "macosx", "--show-sdk-version"]) + .output() + .ok() + .filter(|output| output.status.success()) + .map(|output| String::from_utf8_lossy(&output.stdout).trim().to_string()) + .filter(|version| !version.is_empty()) + } else { + None + }; + llvm::macos::resolve_versions( + llvm.macos_deployment_target.as_deref(), + deployment_env.as_deref(), + llvm.macos_sdk_version.as_deref(), + sdk_env.as_deref(), + sysroot.as_deref(), + discovered_sdk.as_deref(), + ) + .map_err(CliError::usage) +} - ProcessCommand::new("sw_vers") - .arg("-productVersion") - .output() - .ok() - .filter(|out| out.status.success()) - .map(|out| String::from_utf8_lossy(&out.stdout).trim().to_string()) - .filter(|value| !value.is_empty()) - .unwrap_or_else(|| "11.0".to_string()) +fn resolve_macos_versions(llvm: &mut LlvmFlags) -> Result<(), CliError> { + let target = llvm.target.as_deref().unwrap_or(""); + if is_darwin_target(target) { + let versions = macos_versions_for(llvm)?; + llvm.macos_deployment_target = Some(versions.deployment); + llvm.macos_sdk_version = Some(versions.sdk); + } else if llvm.macos_deployment_target.is_some() || llvm.macos_sdk_version.is_some() { + return Err(CliError::usage( + "--macos-deployment-target and --macos-sdk-version require a macOS target", + )); + } + Ok(()) } fn detect_macos_sysroot_owned() -> Option { @@ -3726,6 +3780,12 @@ fn print_dry_run_human( println!(" isa: {}", target_options.isa.as_deref().unwrap_or("")); println!(" sysroot: {}", global.llvm.sysroot.as_deref().unwrap_or("")); println!(" sysroot-source: {}", global.llvm.sysroot_source.as_deref().unwrap_or("")); + if is_darwin_target(&target) { + let versions = macos_versions_for(&global.llvm) + .expect("dry-run requires validated macOS versions"); + println!(" macos-deployment-target: {}", versions.deployment); + println!(" macos-sdk-version: {}", versions.sdk); + } println!(" emit: {}", render_emit_spec(&build.emit)); println!(" link-only: {}", build.link_only); println!(" run: {}", build.run); @@ -3867,6 +3927,11 @@ fn print_dry_run_json( EmitSpec::Check => Value::strings(["check"]), EmitSpec::Set(set) => Value::strings(set.iter().map(|kind| emit_kind_name(*kind))), }; + let macos_versions = if is_darwin_target(&target) { + Some(macos_versions_for(&global.llvm).expect("validated macOS versions")) + } else { + None + }; let value = Value::object([ ("schema_version", Value::integer(1)), ( @@ -3910,6 +3975,14 @@ fn print_dry_run_json( ("isa", Value::optional_string(target_options.isa.as_deref())), ("sysroot", Value::optional_string(global.llvm.sysroot.as_deref())), ("sysroot_source", Value::optional_string(global.llvm.sysroot_source.as_deref())), + ( + "macos_deployment_target", + Value::optional_string(macos_versions.as_ref().map(|v| v.deployment.as_str())), + ), + ( + "macos_sdk_version", + Value::optional_string(macos_versions.as_ref().map(|v| v.sdk.as_str())), + ), ("emit", Value::string(render_emit_spec(&build.emit))), ("emit_kinds", emit_kinds), ("control_mode", Value::optional_string(build.emit.is_check().then_some("check"))), @@ -4147,6 +4220,12 @@ fn print_target_spec_human(global: &Global, target: &str) { println!("freestanding: {}", !spec.hosted); println!("supported: {}", spec.supported); println!("default-linker: {}", default_linker_name(&target_global)); + if is_darwin_target(target) { + let versions = macos_versions_for(&global.llvm) + .expect("target spec requires validated macOS versions"); + println!("macos-deployment-target: {}", versions.deployment); + println!("macos-sdk-version: {}", versions.sdk); + } println!("sysroot: {}", sysroot.as_ref().map(|value| value.path.as_str()).unwrap_or("")); println!( "sysroot-source: {}", @@ -4159,6 +4238,11 @@ fn target_spec_json(global: &Global, target: &str) -> String { let target_global = global_with_target(global, target); let sysroot = effective_sysroot_selection(global, target) .expect("target spec rendering requires validated target options"); + let macos_versions = if is_darwin_target(target) { + Some(macos_versions_for(&global.llvm).expect("validated macOS versions")) + } else { + None + }; schema_json(&Value::object([ ("triple", Value::string(&spec.triple)), ("arch", Value::string(&spec.arch)), @@ -4174,6 +4258,14 @@ fn target_spec_json(global: &Global, target: &str) -> String { ("freestanding", Value::Bool(!spec.hosted)), ("supported", Value::Bool(spec.supported)), ("default_linker", Value::string(default_linker_name(&target_global))), + ( + "macos_deployment_target", + Value::optional_string(macos_versions.as_ref().map(|v| v.deployment.as_str())), + ), + ( + "macos_sdk_version", + Value::optional_string(macos_versions.as_ref().map(|v| v.sdk.as_str())), + ), ("sysroot", Value::optional_string(sysroot.as_ref().map(|v| v.path.as_str()))), ("sysroot_source", Value::optional_string(sysroot.as_ref().map(|v| v.source.as_str()))), ])) @@ -4246,7 +4338,15 @@ fn resolve_build_sysroot(llvm: &mut LlvmFlags, build: &BuildRequest) { } fn validate_target_options_for(target: &str, llvm: &LlvmFlags) -> Result<(), CliError> { - target_options_for(target, llvm).map(|_| ()) + target_options_for(target, llvm)?; + if is_darwin_target(target) { + macos_versions_for(llvm)?; + } else if llvm.macos_deployment_target.is_some() || llvm.macos_sdk_version.is_some() { + return Err(CliError::usage( + "--macos-deployment-target and --macos-sdk-version require a macOS target", + )); + } + Ok(()) } fn effective_sysroot_selection( From db2e8cd2181c336c7bf03829de37f0fdc29c66a7 Mon Sep 17 00:00:00 2001 From: hiyouwhatsapp <89997383+hiyouwhatsapp@users.noreply.github.com> Date: Tue, 3 Nov 2026 23:18:45 +0200 Subject: [PATCH 3/9] test(macos): cover Darwin linker arguments and platform option precedence Check independent platform-version arguments for x86_64 and arm64, validate overrides and failures in CLI dry-run and target specs, and document cross-link selection Signed-off-by: hiyouwhatsapp <89997383+hiyouwhatsapp@users.noreply.github.com> --- docs/macos-linking.md | 35 +++++++++ llvm/src/backend.rs | 47 ++++++++++++ tests/macos_link_versions.rs | 134 +++++++++++++++++++++++++++++++++++ 3 files changed, 216 insertions(+) create mode 100644 docs/macos-linking.md create mode 100644 tests/macos_link_versions.rs diff --git a/docs/macos-linking.md b/docs/macos-linking.md new file mode 100644 index 00000000..a62dc34a --- /dev/null +++ b/docs/macos-linking.md @@ -0,0 +1,35 @@ +# macOS platform version selection + +The two Darwin link values are independent: the deployment target is the +minimum supported macOS release, while the SDK version describes the SDK +used to link the binary. Both values appear in the `ld64.lld` command as +`-platform_version macos `. + +Wave accepts `--macos-deployment-target=` and +`--macos-sdk-version=` with both `=` and separate-value +forms. These flags are accepted with Darwin targets only. + +Deployment version precedence: + +1. `--macos-deployment-target` +2. `MACOSX_DEPLOYMENT_TARGET` +3. Compiler minimum, `11.0` + +SDK version precedence: + +1. `--macos-sdk-version` +2. `MACOSX_SDK_VERSION` +3. Versioned SDK bundle selected by `--sysroot`, `SDKROOT` or `xcrun` + (for example, `MacOSX15.4.sdk` resolves to SDK `15.4`) +4. The host's `xcrun --sdk macosx --show-sdk-version`, when available +5. Compiler fallback `15.0` when no SDK metadata is available + +The fallback is a link-plan assumption, **not** a claim that an SDK is +installed or that the host runs that macOS version. For deterministic +cross-linking, set `--sysroot` and `--macos-sdk-version` explicitly. A +selected SDK must not be older than the deployment target. Malformed +versions are rejected with usage diagnostics before any link process. + +Both values appear in `wavec print target-spec --format=json` and +`wavec build ... --dry-run --error-format=json`, as well as the human +renderings. x86_64 and AArch64 Darwin use the same contract. diff --git a/llvm/src/backend.rs b/llvm/src/backend.rs index 971dc022..d5262932 100644 --- a/llvm/src/backend.rs +++ b/llvm/src/backend.rs @@ -482,3 +482,50 @@ fn llvm_tool_search_dirs() -> Vec { dirs } + +#[cfg(test)] +mod macos_platform_tests { + use super::*; + + #[test] + fn both_darwin_linkers_receive_independent_platform_versions() { + for (triple, arch) in [ + ("x86_64-apple-darwin", "x86_64"), + ("aarch64-apple-darwin", "arm64"), + ] { + if target_spec_for_triple(triple).is_none() { + continue; + } + let backend = BackendOptions { + target: Some(triple.to_string()), + sysroot: Some("/SDKs/MacOSX15.4.sdk".into()), + macos_deployment_target: Some("12.1".into()), + macos_sdk_version: Some("15.4".into()), + ..Default::default() + }; + let mut command = Command::new("ld64.lld"); + append_lld_target_args(&mut command, triple, &backend).unwrap(); + let args: Vec<_> = + command.get_args().map(|arg| arg.to_string_lossy().to_string()).collect(); + assert_eq!(&args[..6], ["-arch", arch, "-platform_version", "macos", "12.1", "15.4"]); + assert_eq!(&args[6..], ["-syslibroot", "/SDKs/MacOSX15.4.sdk"]); + } + } + + #[test] + fn invalid_darwin_backend_versions_fail_before_running_linker() { + for triple in ["x86_64-apple-darwin", "aarch64-apple-darwin"] { + if target_spec_for_triple(triple).is_none() { + continue; + } + let backend = BackendOptions { + macos_deployment_target: Some("15.1".into()), + macos_sdk_version: Some("14.0".into()), + ..Default::default() + }; + let error = append_lld_target_args(&mut Command::new("ld64.lld"), triple, &backend) + .unwrap_err(); + assert_eq!(error.phase, CodegenPhase::Link); + } + } +} diff --git a/tests/macos_link_versions.rs b/tests/macos_link_versions.rs new file mode 100644 index 00000000..e528328e --- /dev/null +++ b/tests/macos_link_versions.rs @@ -0,0 +1,134 @@ +// SPDX-License-Identifier: MPL-2.0 +//! Both Darwin architectures use the same validated minimum and SDK linker contract. +use std::fs; +use std::path::PathBuf; +use std::process::{Command, Output}; +use std::sync::atomic::{AtomicU64, Ordering}; + +static NEXT: AtomicU64 = AtomicU64::new(0); + +fn wavec_bin() -> PathBuf { + option_env!("CARGO_BIN_EXE_wavec") + .map(PathBuf::from) + .unwrap_or_else(|| PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("target/debug/wavec")) +} + +fn case_dir() -> PathBuf { + let root = std::env::temp_dir().join(format!( + "wave-macos-platform-{}-{}", + std::process::id(), + NEXT.fetch_add(1, Ordering::Relaxed) + )); + fs::create_dir_all(&root).unwrap(); + root +} + +fn run(args: &[&str], environment: &[(&str, &str)]) -> Output { + let mut cmd = Command::new(wavec_bin()); + cmd.args(args) + .env("NO_COLOR", "1") + .env_remove("MACOSX_DEPLOYMENT_TARGET") + .env_remove("MACOSX_SDK_VERSION") + .env_remove("SDKROOT"); + for (key, value) in environment { + cmd.env(key, value); + } + cmd.output().unwrap() +} + +fn output_text(output: Output) -> (bool, String) { + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + (output.status.success(), format!("{stdout}\n{stderr}")) +} + +#[test] +fn darwin_dry_run_uses_distinct_sdk_versions_for_both_architectures() { + let root = case_dir(); + let source = root.join("main.wave"); + fs::write(&source, "fun main() -> i32 { return 0; }\n").unwrap(); + let sdk = root.join("MacOSX15.4.sdk"); + fs::create_dir_all(&sdk).unwrap(); + for (triple, arch) in [ + ("x86_64-apple-darwin", "x86_64"), + ("aarch64-apple-darwin", "arm64"), + ] { + if llvm::codegen::target::target_spec_for_triple(triple).is_none() { + continue; + } + let (ok, out) = output_text(run( + &[ + "build", + source.to_str().unwrap(), + &format!("--target={triple}"), + "--emit=bin", + "--dry-run", + "--error-format=json", + &format!("--sysroot={}", sdk.display()), + "--macos-deployment-target=12.1", + ], + &[], + )); + assert!(ok, "{triple}: {out}"); + assert!(out.contains(r#""macos_deployment_target":"12.1""#), "{out}"); + assert!(out.contains(r#""macos_sdk_version":"15.4""#), "{out}"); + assert!(out.contains(&format!( + r#""-arch","{arch}","-platform_version","macos","12.1","15.4""# + )), "{out}"); + assert!(out.contains("MacOSX15.4.sdk"), "{out}"); + } +} + +#[test] +fn explicit_flags_override_environment_and_sdk_directory() { + let triple = "x86_64-apple-darwin"; + if llvm::codegen::target::target_spec_for_triple(triple).is_none() { + return; + } + let (ok, out) = output_text(run( + &[ + "print", "target-spec", "--format=json", + "--target=x86_64-apple-darwin", + "--macos-deployment-target", "12.1", + "--macos-sdk-version", "16.1", + "--sysroot=/SDKs/MacOSX15.4.sdk", + ], + &[("MACOSX_DEPLOYMENT_TARGET", "13.0"), ("MACOSX_SDK_VERSION", "15.0")], + )); + assert!(ok, "{out}"); + assert!(out.contains(r#""macos_deployment_target":"12.1""#), "{out}"); + assert!(out.contains(r#""macos_sdk_version":"16.1""#), "{out}"); + + let (ok, out) = output_text(run( + &[ + "print", "target-spec", "--format=json", + "--target=x86_64-apple-darwin", + "--sysroot=/SDKs/MacOSX15.4.sdk", + ], + &[("MACOSX_DEPLOYMENT_TARGET", "11.2"), ("MACOSX_SDK_VERSION", "15.0")], + )); + assert!(ok, "{out}"); + assert!(out.contains(r#""macos_deployment_target":"11.2""#), "{out}"); + assert!(out.contains(r#""macos_sdk_version":"15.0""#), "{out}"); +} + +#[test] +fn invalid_macos_settings_fail_as_usage_errors() { + let triple = "x86_64-apple-darwin"; + if llvm::codegen::target::target_spec_for_triple(triple).is_none() { + return; + } + for args in [ + vec!["print", "target-spec", "--target=x86_64-apple-darwin", "--macos-deployment-target=bad"], + vec!["print", "target-spec", "--target=x86_64-apple-darwin", "--macos-sdk-version=0.0"], + vec![ + "print", "target-spec", "--target=x86_64-apple-darwin", + "--macos-deployment-target=15.1", "--macos-sdk-version=15.0", + ], + vec!["print", "target-spec", "--target=x86_64-unknown-linux-gnu", "--macos-sdk-version=15.4"], + ] { + let (ok, out) = output_text(run(&args, &[])); + assert!(!ok, "unexpectedly accepted {args:?}: {out}"); + assert!(out.contains("macOS") || out.contains("--macos"), "{out}"); + } +} From 6545b172fa093c79ad1f05f4a57c5f5cfd4ca47e Mon Sep 17 00:00:00 2001 From: hiyouwhatsapp <89997383+hiyouwhatsapp@users.noreply.github.com> Date: Tue, 3 Nov 2026 23:21:12 +0200 Subject: [PATCH 4/9] docs(macos): describe version flags in compiler help Keep the platform-version resolver and backend error handling consistently formatted Signed-off-by: hiyouwhatsapp <89997383+hiyouwhatsapp@users.noreply.github.com> --- llvm/src/backend.rs | 4 +++- llvm/src/macos.rs | 12 +++++++++++- src/cli.rs | 10 ++++++++++ 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/llvm/src/backend.rs b/llvm/src/backend.rs index d5262932..34bc3755 100644 --- a/llvm/src/backend.rs +++ b/llvm/src/backend.rs @@ -318,7 +318,9 @@ fn append_lld_target_args( backend.sysroot.as_deref(), None, ) - .map_err(|message| CodegenError::new(CodegenPhase::Link, "macOS platform version", message))?; + .map_err(|message| { + CodegenError::new(CodegenPhase::Link, "macOS platform version", message) + })?; cmd.arg("-arch") .arg(if spec.architecture.name() == "aarch64" { "arm64" diff --git a/llvm/src/macos.rs b/llvm/src/macos.rs index fbd10299..34279e66 100644 --- a/llvm/src/macos.rs +++ b/llvm/src/macos.rs @@ -108,7 +108,17 @@ mod tests { #[test] fn bad_versions_and_incompatible_sdks_are_rejected() { - for invalid in ["", "11", "11.", ".1", "11.0.0.1", "11.a", "11.0 -arch", "0.0", "999999999999999999.1"] { + for invalid in [ + "", + "11", + "11.", + ".1", + "11.0.0.1", + "11.a", + "11.0 -arch", + "0.0", + "999999999999999999.1", + ] { assert!(resolve_versions(Some(invalid), None, None, None, None, None).is_err()); assert!(resolve_versions(None, None, Some(invalid), None, None, None).is_err()); } diff --git a/src/cli.rs b/src/cli.rs index 385c871f..2a6b91af 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -4765,6 +4765,16 @@ pub fn print_help() { "Intermediate/default artifact root" ); println!(" {:<24} {}", "--dry-run".color("38,139,235"), "Plan only, no compile/link/exec"); + println!( + " {:<24} {}", + "--macos-deployment-target".color("38,139,235"), + "macOS minimum (flag > MACOSX_DEPLOYMENT_TARGET > 11.0)" + ); + println!( + " {:<24} {}", + "--macos-sdk-version".color("38,139,235"), + "macOS SDK version (flag > env > SDK root > discovered SDK)" + ); println!(" {:<24} {}", "--error-format=...".color("38,139,235"), "human, json"); println!("\nLink mode options:"); From 238d80c2207d6b1e2d1929d0dde981b090028caf Mon Sep 17 00:00:00 2001 From: hiyouwhatsapp <89997383+hiyouwhatsapp@users.noreply.github.com> Date: Wed, 4 Nov 2026 13:39:56 +0200 Subject: [PATCH 5/9] fix(macos): determine SDK version from selected root aliases and metadata --- llvm/src/macos.rs | 61 +++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 57 insertions(+), 4 deletions(-) diff --git a/llvm/src/macos.rs b/llvm/src/macos.rs index 34279e66..36a9e075 100644 --- a/llvm/src/macos.rs +++ b/llvm/src/macos.rs @@ -44,11 +44,54 @@ fn parsed_version(text: &str, label: &str) -> Result<[u32; 3], String> { Ok(version) } -/// Extract a version only from a versioned macOS SDK bundle name. -pub fn sdk_version_from_path(sysroot: &str) -> Option { - let file = std::path::Path::new(sysroot).file_name()?.to_str()?; +fn sdk_bundle_version(path: &std::path::Path) -> Option { + let file = path.file_name()?.to_str()?; let value = file.strip_prefix("MacOSX")?.strip_suffix(".sdk")?; - parsed_version(value, "macOS SDK").ok().map(|_| value.to_string()) + parsed_version(value, "macOS SDK") + .ok() + .map(|_| value.to_string()) +} + +fn sdk_metadata_version(path: &std::path::Path) -> Option { + if let Ok(data) = std::fs::read_to_string(path.join("SDKSettings.json")) { + if let Some((_, rest)) = data.split_once("\"Version\"") { + if let Some((_, value)) = rest.split_once(':') { + if let Some(value) = value.trim_start().strip_prefix('"') { + if let Some((version, _)) = value.split_once('"') { + if parsed_version(version, "macOS SDK").is_ok() { + return Some(version.to_string()); + } + } + } + } + } + } + if let Ok(data) = std::fs::read_to_string(path.join("SDKSettings.plist")) { + if let Some((_, rest)) = data.split_once("Version") { + if let Some((_, value)) = rest.split_once("") { + if let Some((version, _)) = value.split_once("") { + let version = version.trim(); + if parsed_version(version, "macOS SDK").is_ok() { + return Some(version.to_string()); + } + } + } + } + } + None +} + +/// Extract the selected SDK's version, following aliases and SDK metadata. +/// A selected SDK is never identified by an unrelated host-default SDK. +pub fn sdk_version_from_path(sysroot: &str) -> Option { + let path = std::path::Path::new(sysroot); + let resolved = path.canonicalize().ok(); + resolved + .as_deref() + .and_then(sdk_bundle_version) + .or_else(|| resolved.as_deref().and_then(sdk_metadata_version)) + .or_else(|| sdk_bundle_version(path)) + .or_else(|| sdk_metadata_version(path)) } /// Resolve and validate both linker versions before constructing a link command. @@ -69,6 +112,16 @@ pub fn resolve_versions( .or(deployment_env) .unwrap_or(DEFAULT_DEPLOYMENT_TARGET); let path_sdk = sysroot.and_then(sdk_version_from_path); + if sysroot.is_some() + && path_sdk.is_none() + && sdk_override.is_none() + && sdk_env.is_none() + { + return Err( + "cannot determine macOS SDK version from the selected SDK; supply --macos-sdk-version" + .to_string(), + ); + } let sdk = sdk_override .or(sdk_env) .or(path_sdk.as_deref()) From a7e2d39e2453e473cf2e2f24880f42c5c521d06f Mon Sep 17 00:00:00 2001 From: hiyouwhatsapp <89997383+hiyouwhatsapp@users.noreply.github.com> Date: Wed, 4 Nov 2026 13:40:26 +0200 Subject: [PATCH 6/9] fix(macos): never infer selected SDK from host default --- src/cli.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/cli.rs b/src/cli.rs index 2a6b91af..601d2058 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -3495,6 +3495,7 @@ fn macos_versions_for(llvm: &LlvmFlags) -> Result Date: Wed, 4 Nov 2026 13:40:31 +0200 Subject: [PATCH 7/9] test(macos): cover SDK aliases and unversioned metadata roots --- tests/macos_link_versions.rs | 57 ++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/tests/macos_link_versions.rs b/tests/macos_link_versions.rs index e528328e..2da21e9e 100644 --- a/tests/macos_link_versions.rs +++ b/tests/macos_link_versions.rs @@ -132,3 +132,60 @@ fn invalid_macos_settings_fail_as_usage_errors() { assert!(out.contains("macOS") || out.contains("--macos"), "{out}"); } } + +#[cfg(unix)] +#[test] +fn selected_sdk_alias_and_metadata_override_host_discovery_for_both_architectures() { + use std::os::unix::fs::symlink; + + let root = case_dir(); + let source = root.join("main.wave"); + fs::write(&source, "fun main() -> i32 { return 0; }\n").unwrap(); + + let named = root.join("MacOSX14.4.sdk"); + fs::create_dir_all(&named).unwrap(); + let alias = root.join("Pinned.sdk"); + symlink(&named, &alias).unwrap(); + + let unversioned = root.join("Unversioned.sdk"); + fs::create_dir_all(&unversioned).unwrap(); + fs::write(unversioned.join("SDKSettings.json"), "{\"Version\":\"14.4\"}\n").unwrap(); + + for triple in ["x86_64-apple-darwin", "aarch64-apple-darwin"] { + if llvm::codegen::target::target_spec_for_triple(triple).is_none() { + continue; + } + for selected in [&alias, &unversioned] { + let (ok, output) = output_text(run( + &[ + "print", + "target-spec", + "--format=json", + &format!("--target={triple}"), + &format!("--sysroot={}", selected.display()), + "--macos-deployment-target=15.0", + ], + &[], + )); + assert!(!ok, "{triple}, {}: {output}", selected.display()); + assert!(output.contains("older than deployment target"), "{output}"); + + let (ok, output) = output_text(run( + &[ + "build", + source.to_str().unwrap(), + &format!("--target={triple}"), + "--emit=bin", + "--dry-run", + "--error-format=json", + &format!("--sysroot={}", selected.display()), + "--macos-deployment-target=12.0", + ], + &[], + )); + assert!(ok, "{triple}, {}: {output}", selected.display()); + assert!(output.contains(r#""macos_sdk_version":"14.4""#), "{output}"); + assert!(output.contains(r#""-platform_version","macos","12.0","14.4""#), "{output}"); + } + } +} From d1f8bfad1ce18102d4600feeedc02a1134d77c3c Mon Sep 17 00:00:00 2001 From: hiyouwhatsapp <89997383+hiyouwhatsapp@users.noreply.github.com> Date: Wed, 4 Nov 2026 13:40:33 +0200 Subject: [PATCH 8/9] docs: keep Wave root documentation local-only --- docs/macos-linking.md | 35 ----------------------------------- 1 file changed, 35 deletions(-) delete mode 100644 docs/macos-linking.md diff --git a/docs/macos-linking.md b/docs/macos-linking.md deleted file mode 100644 index a62dc34a..00000000 --- a/docs/macos-linking.md +++ /dev/null @@ -1,35 +0,0 @@ -# macOS platform version selection - -The two Darwin link values are independent: the deployment target is the -minimum supported macOS release, while the SDK version describes the SDK -used to link the binary. Both values appear in the `ld64.lld` command as -`-platform_version macos `. - -Wave accepts `--macos-deployment-target=` and -`--macos-sdk-version=` with both `=` and separate-value -forms. These flags are accepted with Darwin targets only. - -Deployment version precedence: - -1. `--macos-deployment-target` -2. `MACOSX_DEPLOYMENT_TARGET` -3. Compiler minimum, `11.0` - -SDK version precedence: - -1. `--macos-sdk-version` -2. `MACOSX_SDK_VERSION` -3. Versioned SDK bundle selected by `--sysroot`, `SDKROOT` or `xcrun` - (for example, `MacOSX15.4.sdk` resolves to SDK `15.4`) -4. The host's `xcrun --sdk macosx --show-sdk-version`, when available -5. Compiler fallback `15.0` when no SDK metadata is available - -The fallback is a link-plan assumption, **not** a claim that an SDK is -installed or that the host runs that macOS version. For deterministic -cross-linking, set `--sysroot` and `--macos-sdk-version` explicitly. A -selected SDK must not be older than the deployment target. Malformed -versions are rejected with usage diagnostics before any link process. - -Both values appear in `wavec print target-spec --format=json` and -`wavec build ... --dry-run --error-format=json`, as well as the human -renderings. x86_64 and AArch64 Darwin use the same contract. From c44d107e79690b3275b18976948f2e33265aaf28 Mon Sep 17 00:00:00 2001 From: hiyouwhatsapp <89997383+hiyouwhatsapp@users.noreply.github.com> Date: Thu, 5 Nov 2026 17:50:12 +0200 Subject: [PATCH 9/9] fix(macos): parse selected SDK metadata structurally --- Cargo.lock | 24 +++++++ llvm/Cargo.toml | 2 + llvm/src/backend.rs | 6 +- llvm/src/macos.rs | 125 +++++++++++++++++++++-------------- src/cli.rs | 11 ++- tests/macos_link_versions.rs | 124 +++++++++++++++++++++++++++++----- 6 files changed, 219 insertions(+), 73 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9f11d2d8..ca8432ce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -309,6 +309,8 @@ dependencies = [ "lexer", "llvm-sys", "parser", + "plist", + "utils", ] [[package]] @@ -383,6 +385,19 @@ dependencies = [ "utils", ] +[[package]] +name = "plist" +version = "1.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3af6b589e163c5a788fab00ce0c0366f6efbb9959c2f9874b224936af7fce7e1" +dependencies = [ + "base64", + "indexmap", + "quick-xml", + "serde", + "time", +] + [[package]] name = "powerfmt" version = "0.2.1" @@ -398,6 +413,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "quick-xml" +version = "0.38.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" +dependencies = [ + "memchr", +] + [[package]] name = "quote" version = "1.0.45" diff --git a/llvm/Cargo.toml b/llvm/Cargo.toml index 234a6d31..50435c9a 100644 --- a/llvm/Cargo.toml +++ b/llvm/Cargo.toml @@ -8,6 +8,8 @@ parser = { path = "../front/parser" } hir = { path = "../front/hir" } lexer = { path = "../front/lexer" } error = { path = "../front/error" } +utils = { path = "../utils" } +plist = "=1.7.4" # Wave does not use Inkwell's libffi execution engine. LLVM's own dependencies # are supplied by llvm-config, including libffi when the selected SDK needs it. inkwell = { version = "0.8.0", default-features = false, features = ["llvm21-1", "no-libffi-linking"] } diff --git a/llvm/src/backend.rs b/llvm/src/backend.rs index 34bc3755..07843e71 100644 --- a/llvm/src/backend.rs +++ b/llvm/src/backend.rs @@ -491,10 +491,8 @@ mod macos_platform_tests { #[test] fn both_darwin_linkers_receive_independent_platform_versions() { - for (triple, arch) in [ - ("x86_64-apple-darwin", "x86_64"), - ("aarch64-apple-darwin", "arm64"), - ] { + for (triple, arch) in [("x86_64-apple-darwin", "x86_64"), ("aarch64-apple-darwin", "arm64")] + { if target_spec_for_triple(triple).is_none() { continue; } diff --git a/llvm/src/macos.rs b/llvm/src/macos.rs index 36a9e075..fd3d5b6b 100644 --- a/llvm/src/macos.rs +++ b/llvm/src/macos.rs @@ -34,9 +34,8 @@ fn parsed_version(text: &str, label: &str) -> Result<[u32; 3], String> { } let mut version = [0; 3]; for (index, part) in parts.iter().enumerate() { - version[index] = part - .parse::() - .map_err(|_| format!("invalid {label} version {text:?}"))?; + version[index] = + part.parse::().map_err(|_| format!("invalid {label} version {text:?}"))?; } if !(10..=999).contains(&version[0]) { return Err(format!("invalid {label} version {text:?}; major must be at least 10")); @@ -47,33 +46,24 @@ fn parsed_version(text: &str, label: &str) -> Result<[u32; 3], String> { fn sdk_bundle_version(path: &std::path::Path) -> Option { let file = path.file_name()?.to_str()?; let value = file.strip_prefix("MacOSX")?.strip_suffix(".sdk")?; - parsed_version(value, "macOS SDK") - .ok() - .map(|_| value.to_string()) + parsed_version(value, "macOS SDK").ok().map(|_| value.to_string()) } fn sdk_metadata_version(path: &std::path::Path) -> Option { if let Ok(data) = std::fs::read_to_string(path.join("SDKSettings.json")) { - if let Some((_, rest)) = data.split_once("\"Version\"") { - if let Some((_, value)) = rest.split_once(':') { - if let Some(value) = value.trim_start().strip_prefix('"') { - if let Some((version, _)) = value.split_once('"') { - if parsed_version(version, "macOS SDK").is_ok() { - return Some(version.to_string()); - } - } + if let Ok(document) = utils::wson::parse_json(&data) { + if let Some(version) = document.get_str("Version") { + if parsed_version(version, "macOS SDK").is_ok() { + return Some(version.to_string()); } } } } - if let Ok(data) = std::fs::read_to_string(path.join("SDKSettings.plist")) { - if let Some((_, rest)) = data.split_once("Version") { - if let Some((_, value)) = rest.split_once("") { - if let Some((version, _)) = value.split_once("") { - let version = version.trim(); - if parsed_version(version, "macOS SDK").is_ok() { - return Some(version.to_string()); - } + if let Ok(document) = plist::Value::from_file(path.join("SDKSettings.plist")) { + if let Some(version) = document.as_dictionary().and_then(|dict| dict.get("Version")) { + if let Some(version) = version.as_string().map(str::trim) { + if parsed_version(version, "macOS SDK").is_ok() { + return Some(version.to_string()); } } } @@ -108,15 +98,9 @@ pub fn resolve_versions( sysroot: Option<&str>, discovered_sdk: Option<&str>, ) -> Result { - let deployment = deployment_override - .or(deployment_env) - .unwrap_or(DEFAULT_DEPLOYMENT_TARGET); + let deployment = deployment_override.or(deployment_env).unwrap_or(DEFAULT_DEPLOYMENT_TARGET); let path_sdk = sysroot.and_then(sdk_version_from_path); - if sysroot.is_some() - && path_sdk.is_none() - && sdk_override.is_none() - && sdk_env.is_none() - { + if sysroot.is_some() && path_sdk.is_none() && sdk_override.is_none() && sdk_env.is_none() { return Err( "cannot determine macOS SDK version from the selected SDK; supply --macos-sdk-version" .to_string(), @@ -141,37 +125,82 @@ pub fn resolve_versions( mod tests { use super::*; + #[test] + fn sdk_json_version_must_be_a_valid_root_string() { + let root = std::env::temp_dir().join(format!("wave-sdk-json-{}", std::process::id())); + std::fs::create_dir_all(&root).unwrap(); + let metadata = root.join("SDKSettings.json"); + for (json, expected) in [ + (r#"{"Version":"14.4"}"#, Some("14.4")), + (r#"{"Nested":{"Version":"15.0"},"Version":"14.4"}"#, Some("14.4")), + (r#"{"Description":"Version","Other":"15.0","Version":"14.4"}"#, Some("14.4")), + (r#"{"Nested":{"Version":"15.0"}}"#, None), + (r#"{"Version":"15.0""#, None), + (r#"{"Version":15.0,"Other":"15.0"}"#, None), + (r#"{"Version":"14.4","Version":"15.0"}"#, None), + ] { + std::fs::write(&metadata, json).unwrap(); + assert_eq!(sdk_metadata_version(&root).as_deref(), expected, "{json}"); + let selected = root.to_str().unwrap(); + assert!(resolve_versions(Some("15.0"), None, None, None, Some(selected), None).is_err()); + } + std::fs::remove_dir_all(root).unwrap(); + } + + #[test] + fn sdk_plist_version_must_belong_to_the_root_key() { + let root = std::env::temp_dir().join(format!("wave-sdk-plist-{}", std::process::id())); + std::fs::create_dir_all(&root).unwrap(); + let metadata = root.join("SDKSettings.plist"); + for (fields, expected) in [ + ("Version14.4", Some("14.4")), + ("NestedVersion15.0Version14.4", Some("14.4")), + ("Version15Other15.0", None), + ("NestedVersion15.0", None), + ] { + let plist = format!("{fields}"); + std::fs::write(&metadata, plist).unwrap(); + assert_eq!(sdk_metadata_version(&root).as_deref(), expected, "{fields}"); + } + std::fs::write(&metadata, "Version15.0").unwrap(); + assert_eq!(sdk_metadata_version(&root), None); + std::fs::remove_dir_all(root).unwrap(); + } + #[test] fn explicit_overrides_env_and_sdk_path() { let versions = resolve_versions( - Some("12.3"), Some("11.2"), Some("16.1"), Some("15.2"), - Some("/SDKs/MacOSX14.4.sdk"), Some("13.3"), - ).unwrap(); + Some("12.3"), + Some("11.2"), + Some("16.1"), + Some("15.2"), + Some("/SDKs/MacOSX14.4.sdk"), + Some("13.3"), + ) + .unwrap(); assert_eq!(versions.deployment, "12.3"); assert_eq!(versions.sdk, "16.1"); let versions = resolve_versions( - None, Some("11.2"), None, None, - Some("/SDKs/MacOSX14.4.sdk"), Some("13.3"), - ).unwrap(); + None, + Some("11.2"), + None, + None, + Some("/SDKs/MacOSX14.4.sdk"), + Some("13.3"), + ) + .unwrap(); assert_eq!(versions.deployment, "11.2"); assert_eq!(versions.sdk, "14.4"); - let versions = resolve_versions(None, None, None, Some("15.2"), None, Some("14.0")).unwrap(); + let versions = + resolve_versions(None, None, None, Some("15.2"), None, Some("14.0")).unwrap(); assert_eq!(versions.sdk, "15.2"); } #[test] fn bad_versions_and_incompatible_sdks_are_rejected() { - for invalid in [ - "", - "11", - "11.", - ".1", - "11.0.0.1", - "11.a", - "11.0 -arch", - "0.0", - "999999999999999999.1", - ] { + for invalid in + ["", "11", "11.", ".1", "11.0.0.1", "11.a", "11.0 -arch", "0.0", "999999999999999999.1"] + { assert!(resolve_versions(Some(invalid), None, None, None, None, None).is_err()); assert!(resolve_versions(None, None, Some(invalid), None, None, None).is_err()); } diff --git a/src/cli.rs b/src/cli.rs index 601d2058..81ff9c6b 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1132,7 +1132,6 @@ fn parse_llvm_backend_option( return Ok(true); } - for (flag, field) in [ ("--macos-deployment-target", &mut llvm.macos_deployment_target), ("--macos-sdk-version", &mut llvm.macos_sdk_version), @@ -1146,9 +1145,9 @@ fn parse_llvm_backend_option( return Ok(true); } if a == flag { - let value = args.get(*i + 1).ok_or_else(|| { - CliError::usage(format!("missing value: {flag} ")) - })?; + let value = args + .get(*i + 1) + .ok_or_else(|| CliError::usage(format!("missing value: {flag} ")))?; if value.trim().is_empty() { return Err(CliError::usage(format!("missing value: {flag} "))); } @@ -3782,8 +3781,8 @@ fn print_dry_run_human( println!(" sysroot: {}", global.llvm.sysroot.as_deref().unwrap_or("")); println!(" sysroot-source: {}", global.llvm.sysroot_source.as_deref().unwrap_or("")); if is_darwin_target(&target) { - let versions = macos_versions_for(&global.llvm) - .expect("dry-run requires validated macOS versions"); + let versions = + macos_versions_for(&global.llvm).expect("dry-run requires validated macOS versions"); println!(" macos-deployment-target: {}", versions.deployment); println!(" macos-sdk-version: {}", versions.sdk); } diff --git a/tests/macos_link_versions.rs b/tests/macos_link_versions.rs index 2da21e9e..0cbb07b0 100644 --- a/tests/macos_link_versions.rs +++ b/tests/macos_link_versions.rs @@ -49,10 +49,7 @@ fn darwin_dry_run_uses_distinct_sdk_versions_for_both_architectures() { fs::write(&source, "fun main() -> i32 { return 0; }\n").unwrap(); let sdk = root.join("MacOSX15.4.sdk"); fs::create_dir_all(&sdk).unwrap(); - for (triple, arch) in [ - ("x86_64-apple-darwin", "x86_64"), - ("aarch64-apple-darwin", "arm64"), - ] { + for (triple, arch) in [("x86_64-apple-darwin", "x86_64"), ("aarch64-apple-darwin", "arm64")] { if llvm::codegen::target::target_spec_for_triple(triple).is_none() { continue; } @@ -72,9 +69,10 @@ fn darwin_dry_run_uses_distinct_sdk_versions_for_both_architectures() { assert!(ok, "{triple}: {out}"); assert!(out.contains(r#""macos_deployment_target":"12.1""#), "{out}"); assert!(out.contains(r#""macos_sdk_version":"15.4""#), "{out}"); - assert!(out.contains(&format!( - r#""-arch","{arch}","-platform_version","macos","12.1","15.4""# - )), "{out}"); + assert!( + out.contains(&format!(r#""-arch","{arch}","-platform_version","macos","12.1","15.4""#)), + "{out}" + ); assert!(out.contains("MacOSX15.4.sdk"), "{out}"); } } @@ -87,10 +85,14 @@ fn explicit_flags_override_environment_and_sdk_directory() { } let (ok, out) = output_text(run( &[ - "print", "target-spec", "--format=json", + "print", + "target-spec", + "--format=json", "--target=x86_64-apple-darwin", - "--macos-deployment-target", "12.1", - "--macos-sdk-version", "16.1", + "--macos-deployment-target", + "12.1", + "--macos-sdk-version", + "16.1", "--sysroot=/SDKs/MacOSX15.4.sdk", ], &[("MACOSX_DEPLOYMENT_TARGET", "13.0"), ("MACOSX_SDK_VERSION", "15.0")], @@ -101,7 +103,9 @@ fn explicit_flags_override_environment_and_sdk_directory() { let (ok, out) = output_text(run( &[ - "print", "target-spec", "--format=json", + "print", + "target-spec", + "--format=json", "--target=x86_64-apple-darwin", "--sysroot=/SDKs/MacOSX15.4.sdk", ], @@ -119,13 +123,26 @@ fn invalid_macos_settings_fail_as_usage_errors() { return; } for args in [ - vec!["print", "target-spec", "--target=x86_64-apple-darwin", "--macos-deployment-target=bad"], + vec![ + "print", + "target-spec", + "--target=x86_64-apple-darwin", + "--macos-deployment-target=bad", + ], vec!["print", "target-spec", "--target=x86_64-apple-darwin", "--macos-sdk-version=0.0"], vec![ - "print", "target-spec", "--target=x86_64-apple-darwin", - "--macos-deployment-target=15.1", "--macos-sdk-version=15.0", + "print", + "target-spec", + "--target=x86_64-apple-darwin", + "--macos-deployment-target=15.1", + "--macos-sdk-version=15.0", + ], + vec![ + "print", + "target-spec", + "--target=x86_64-unknown-linux-gnu", + "--macos-sdk-version=15.4", ], - vec!["print", "target-spec", "--target=x86_64-unknown-linux-gnu", "--macos-sdk-version=15.4"], ] { let (ok, out) = output_text(run(&args, &[])); assert!(!ok, "unexpectedly accepted {args:?}: {out}"); @@ -133,6 +150,83 @@ fn invalid_macos_settings_fail_as_usage_errors() { } } +#[test] +fn selected_sdk_metadata_uses_the_root_version_for_diagnostics_and_linking() { + let root = case_dir(); + let source = root.join("main.wave"); + fs::write(&source, "fun main() -> i32 { return 0; }\n").unwrap(); + let sdk = root.join("Selected.sdk"); + fs::create_dir_all(&sdk).unwrap(); + for triple in ["x86_64-apple-darwin", "aarch64-apple-darwin"] { + if llvm::codegen::target::target_spec_for_triple(triple).is_none() { + continue; + } + for (json, root_version) in [ + (r#"{"Version":"14.4"}"#, true), + (r#"{"Nested":{"Version":"15.0"},"Version":"14.4"}"#, true), + (r#"{"Description":"Version","Other":"15.0","Version":"14.4"}"#, true), + (r#"{"Nested":{"Version":"15.0"}}"#, false), + (r#"{"Version":"15.0""#, false), + (r#"{"Version":15.0,"Other":"15.0"}"#, false), + ] { + fs::write(sdk.join("SDKSettings.json"), json).unwrap(); + let (ok, output) = output_text(run( + &[ + "print", + "target-spec", + "--format=json", + "--error-format=json", + &format!("--target={triple}"), + &format!("--sysroot={}", sdk.display()), + "--macos-deployment-target=15.0", + ], + &[], + )); + assert!(!ok, "{triple}: {json}: {output}"); + let diagnostic = if root_version { + "older than deployment target" + } else { + "cannot determine macOS SDK version" + }; + assert!(output.contains(diagnostic), "{triple}: {json}: {output}"); + if root_version { + let (ok, output) = output_text(run( + &[ + "build", + source.to_str().unwrap(), + &format!("--target={triple}"), + "--emit=bin", + "--dry-run", + "--error-format=json", + &format!("--sysroot={}", sdk.display()), + "--macos-deployment-target=12.0", + ], + &[], + )); + assert!(ok, "{triple}: {json}: {output}"); + assert!(output.contains(r#""macos_sdk_version":"14.4""#), "{output}"); + assert!( + output.contains(r#""-platform_version","macos","12.0","14.4""#), + "{output}" + ); + } + } + let (ok, output) = output_text(run( + &[ + "print", + "target-spec", + "--format=json", + &format!("--target={triple}"), + &format!("--sysroot={}", sdk.display()), + "--macos-sdk-version=16.0", + ], + &[], + )); + assert!(ok, "explicit override: {output}"); + assert!(output.contains(r#""macos_sdk_version":"16.0""#), "{output}"); + } +} + #[cfg(unix)] #[test] fn selected_sdk_alias_and_metadata_override_host_discovery_for_both_architectures() {