Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions llvm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand Down
73 changes: 68 additions & 5 deletions llvm/src/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ pub struct BackendOptions {
pub code_model: Option<String>,
pub relocation_model: Option<String>,
pub sysroot: Option<String>,
pub macos_deployment_target: Option<String>,
pub macos_sdk_version: Option<String>,
pub linker: Option<String>,
pub link_args: Vec<String>,
pub no_default_libs: bool,
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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"
Expand All @@ -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) {
Expand All @@ -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<String> {
Expand Down Expand Up @@ -466,3 +484,48 @@ fn llvm_tool_search_dirs() -> Vec<PathBuf> {

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);
}
}
}
1 change: 1 addition & 0 deletions llvm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
213 changes: 213 additions & 0 deletions llvm/src/macos.rs
Original file line number Diff line number Diff line change
@@ -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::<Vec<_>>();
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::<u32>().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<String> {
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<String> {
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<String> {
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<PlatformVersions, String> {
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 [
("<key>Version</key><string>14.4</string>", Some("14.4")),
("<key>Nested</key><dict><key>Version</key><string>15.0</string></dict><key>Version</key><string>14.4</string>", Some("14.4")),
("<key>Version</key><integer>15</integer><key>Other</key><string>15.0</string>", None),
("<key>Nested</key><dict><key>Version</key><string>15.0</string></dict>", None),
] {
let plist = format!("<?xml version=\"1.0\"?><plist version=\"1.0\"><dict>{fields}</dict></plist>");
std::fs::write(&metadata, plist).unwrap();
assert_eq!(sdk_metadata_version(&root).as_deref(), expected, "{fields}");
}
std::fs::write(&metadata, "<plist><dict><key>Version</key><string>15.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);
}
}
Loading
Loading