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 6c6946e1..07843e71 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,25 @@ 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 +329,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 +344,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 { @@ -466,3 +484,48 @@ 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/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..fd3d5b6b --- /dev/null +++ b/llvm/src/macos.rs @@ -0,0 +1,213 @@ +// 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) +} + +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()) +} + +fn sdk_metadata_version(path: &std::path::Path) -> Option { + if let Ok(data) = std::fs::read_to_string(path.join("SDKSettings.json")) { + 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(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()); + } + } + } + } + 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. +/// +/// 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); + 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()) + .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 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(); + 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/cli.rs b/src/cli.rs index 54002149..81ff9c6b 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,31 @@ 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 +2978,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 +3486,49 @@ 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() + && sysroot.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( @@ -4665,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:"); 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, diff --git a/tests/macos_link_versions.rs b/tests/macos_link_versions.rs new file mode 100644 index 00000000..0cbb07b0 --- /dev/null +++ b/tests/macos_link_versions.rs @@ -0,0 +1,285 @@ +// 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}"); + } +} + +#[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() { + 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}"); + } + } +}