Repository: mun-lang/mun (language: rust)

Issue:
Support for iOS
I would like to build a `munlib` for iOS. I have tinkered with the code in `mun_target` and added an ios target, and call mun with `--target x86_64-apple-ios`. Still, the generated lib file is for macOS. Any help with the parts that I need to change?


Discussion / hints:
Nice! I havent tried this yet! Can you show me what you custom target looks like?
Sure, here's the code for it:
```rust
// apple_base.rs

pub fn ios_llvm_target(arch: &str) -> String {
    let (major, minor) = ios_deployment_target();
    format!("{}-apple-ios{}.{}", arch, major, minor)
}

fn ios_deployment_target() -> (u32, u32) {
    let deployment_target = env::var("IPHONEOS_DEPLOYMENT_TARGET").ok();
    let version = deployment_target
        .as_ref()
        .and_then(|s| s.split_once('.'))
        .and_then(|(a, b)| {
            a.parse::<u32>()
                .and_then(|a| b.parse::<u32>().map(|b| (a, b)))
                .ok()
        });

    version.unwrap_or((15, 5))
}
```
```rust
// crates/mun_target/src/spec/x86_64_apple_ios.rs

use crate::spec::{LinkerFlavor, Target, TargetResult};

pub fn target() -> TargetResult {
    let base = super::apple_base::opts();
    // base.cpu = "core2".to_string();

    // Clang automatically chooses a more specific target based on
    // MACOSX_DEPLOYMENT_TARGET.  To enable cross-language LTO to work
    // correctly, we do too.
    let arch = "x86_64";
    let llvm_target = super::apple_base::ios_llvm_target(arch);

    Ok(Target {
        llvm_target,
        target_os: "ios".to_string(),
        target_endian: "little".to_string(),
        target_pointer_width: "64".to_string(),
        target_c_int_width: "32".to_string(),
        target_env: String::new(),
        target_vendor: "apple".to_string(),
        arch: arch.to_string(),
        data_layout: "e-m:o-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128"
            .to_string(),
        linker_flavor: LinkerFlavor::Ld64,
        options: base,
    })
}
```
One thing I noticed is that in `mun_codegen` the `get_apple_sdk_root` function gets the macOS sdk root by default, so I set `IPHONEOS_DEPLOYMENT_TARGET=15.5 SDKROOT=/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk`
Im booting ye old mac, Ill see if I can get something working. 

Fix patch:
diff --git a/crates/mun/src/ops/build.rs b/crates/mun/src/ops/build.rs
index 1f3ae07d0..7fc6f9378 100644
--- a/crates/mun/src/ops/build.rs
+++ b/crates/mun/src/ops/build.rs
@@ -38,10 +38,15 @@ pub struct Args {
     watch: bool,
 
     /// Target for machine code
-    #[clap(long, parse(try_from_str=Target::search))]
+    #[clap(long, parse(try_from_str=parse_target_triple))]
     target: Option<Target>,
 }
 
+fn parse_target_triple(target_triple: &str) -> Result<Target, String> {
+    Target::search(target_triple)
+        .ok_or_else(|| format!("could not find target for '{}'", target_triple))
+}
+
 /// This method is invoked when the executable is run with the `build` argument indicating that a
 /// user requested us to build a project in the current directory or one of its parent directories.
 pub fn build(args: Args) -> Result<ExitStatus, anyhow::Error> {
diff --git a/crates/mun_codegen/src/apple.rs b/crates/mun_codegen/src/apple.rs
index 498d462c2..ef95250ac 100644
--- a/crates/mun_codegen/src/apple.rs
+++ b/crates/mun_codegen/src/apple.rs
@@ -1,4 +1,6 @@
-use once_cell::sync::OnceCell;
+use once_cell::sync::Lazy;
+use parking_lot::Mutex;
+use std::collections::HashMap;
 use std::{
     env, io,
     path::{Path, PathBuf},
@@ -8,32 +10,76 @@ use std::{
 /// Finds the Apple SDK root directory by checking the `SDKROOT` environment variable or running
 /// `xcrun --show-sdk-path`. The result is cached so multiple calls to this function should be
 /// fast.
-pub fn get_apple_sdk_root() -> Result<&'static Path, String> {
-    static SDK_PATH: OnceCell<PathBuf> = OnceCell::new();
+pub fn get_apple_sdk_root(sdk_name: &str) -> Result<PathBuf, String> {
+    static SDK_PATH: Lazy<Mutex<HashMap<String, PathBuf>>> = Lazy::new(Default::default);
 
-    SDK_PATH
-        .get_or_try_init(|| {
-            if let Ok(sdkroot) = env::var("SDKROOT") {
-                return Ok(PathBuf::from(sdkroot));
-            }
+    let mut lock = SDK_PATH.lock();
+    if let Some(path) = lock.get(sdk_name) {
+        return Ok(path.clone());
+    }
+
+    match find_apple_sdk_root(sdk_name) {
+        Ok(sdk_root) => Ok(lock.entry(sdk_name.into()).or_insert(sdk_root).clone()),
+        Err(e) => Err(e),
+    }
+}
 
-            let res = Command::new("xcrun")
-                .arg("--show-sdk-path")
-                .output()
-                .and_then(|output| {
-                    if output.status.success() {
-                        Ok(String::from_utf8(output.stdout).unwrap())
-                    } else {
-                        let error = String::from_utf8(output.stderr);
-                        let error = format!("process exit with error: {}", error.unwrap());
-                        Err(io::Error::new(io::ErrorKind::Other, &error[..]))
-                    }
-                });
+fn find_apple_sdk_root(sdk_name: &str) -> Result<PathBuf, String> {
+    // Following what clang (and rustc) does
+    // (https://github.com/llvm/llvm-project/blob/
+    // 296a80102a9b72c3eda80558fb78a3ed8849b341/clang/lib/Driver/ToolChains/Darwin.cpp#L1661-L1678
+    // and https://github.dev/rust-lang/rust/blob/33eb3c05c54b306afea341dd233671a9f039156f/compiler/
+    // rustc_codegen_ssa/src/back/link.rs#L2659-L2681)
+    // to allow the SDK path to be set. (For clang, xcrun sets
+    // SDKROOT; for rustc, the user or build system can set it, or we
+    // can fall back to checking for xcrun on PATH.)
 
-            match res {
-                Ok(output) => Ok(PathBuf::from(output.trim())),
-                Err(e) => Err(format!("failed to get SDK path: {}", e)),
+    if let Ok(sdkroot) = env::var("SDKROOT") {
+        let p = PathBuf::from(&sdkroot);
+        match sdk_name {
+            // Ignore `SDKROOT` if it's clearly set for the wrong platform.
+            "appletvos"
+                if sdkroot.contains("TVSimulator.platform")
+                    || sdkroot.contains("MacOSX.platform") => {}
+            "appletvsimulator"
+                if sdkroot.contains("TVOS.platform") || sdkroot.contains("MacOSX.platform") => {}
+            "iphoneos"
+                if sdkroot.contains("iPhoneSimulator.platform")
+                    || sdkroot.contains("MacOSX.platform") => {}
+            "iphonesimulator"
+                if sdkroot.contains("iPhoneOS.platform") || sdkroot.contains("MacOSX.platform") => {
             }
-        })
-        .map(AsRef::as_ref)
+            "macosx10.15"
+                if sdkroot.contains("iPhoneOS.platform")
+                    || sdkroot.contains("iPhoneSimulator.platform") => {}
+            "watchos"
+                if sdkroot.contains("WatchSimulator.platform")
+                    || sdkroot.contains("MacOSX.platform") => {}
+            "watchsimulator"
+                if sdkroot.contains("WatchOS.platform") || sdkroot.contains("MacOSX.platform") => {}
+            // Ignore `SDKROOT` if it's not a valid path.
+            _ if !p.is_absolute() || p == Path::new("/") || !p.exists() => {}
+            _ => return Ok(p),
+        }
+    }
+
+    let res = Command::new("xcrun")
+        .arg("--show-sdk-path")
+        .arg("-sdk")
+        .arg(sdk_name)
+        .output()
+        .and_then(|output| {
+            if output.status.success() {
+                Ok(String::from_utf8(output.stdout).unwrap())
+            } else {
+                let error = String::from_utf8(output.stderr);
+                let error = format!("process exit with error: {}", error.unwrap());
+                Err(io::Error::new(io::ErrorKind::Other, &error[..]))
+            }
+        });
+
+    match res {
+        Ok(output) => Ok(PathBuf::from(output.trim())),
+        Err(e) => Err(format!("failed to get SDK path: {}", e)),
+    }
 }
diff --git a/crates/mun_codegen/src/db.rs b/crates/mun_codegen/src/db.rs
index dc205a7a0..82ebfaabc 100644
--- a/crates/mun_codegen/src/db.rs
+++ b/crates/mun_codegen/src/db.rs
@@ -40,6 +40,7 @@ fn target_machine(db: &dyn CodeGenDatabase) -> ByAddress<Arc<inkwell::targets::T
 
     // Initialize the x86 target
     Target::initialize_x86(&InitializationConfig::default());
+    Target::initialize_aarch64(&InitializationConfig::default());
 
     // Retrieve the LLVM target using the specified target.
     let target_triple = TargetTriple::create(&db.target().llvm_target);
diff --git a/crates/mun_codegen/src/linker.rs b/crates/mun_codegen/src/linker.rs
index fff576d9f..af173c5dd 100644
--- a/crates/mun_codegen/src/linker.rs
+++ b/crates/mun_codegen/src/linker.rs
@@ -34,7 +34,7 @@ impl fmt::Display for LinkerError {
 }
 
 pub fn create_with_target(target: &spec::Target) -> Box<dyn Linker> {
-    match target.linker_flavor {
+    match target.options.linker_flavor {
         LinkerFlavor::Ld => Box::new(LdLinker::new(target)),
         LinkerFlavor::Ld64 => Box::new(Ld64Linker::new(target)),
         LinkerFlavor::Msvc => Box::new(MsvcLinker::new(target)),
@@ -52,9 +52,9 @@ struct LdLinker {
 }
 
 impl LdLinker {
-    fn new(_target: &spec::Target) -> Self {
+    fn new(target: &spec::Target) -> Self {
         LdLinker {
-            args: Vec::default(),
+            args: target.options.pre_link_args.clone(),
         }
     }
 }
@@ -93,14 +93,82 @@ impl Linker for LdLinker {
 
 struct Ld64Linker {
     args: Vec<String>,
+    target: spec::Target,
 }
 
 impl Ld64Linker {
     fn new(target: &spec::Target) -> Self {
+        let arch_name = target
+            .llvm_target
+            .split('-')
+            .next()
+            .expect("LLVM target must have a hyphen");
+
+        let mut args = target.options.pre_link_args.clone();
+        args.push(format!("-arch {}", arch_name));
+
         Ld64Linker {
-            args: vec![format!("-arch {}", &target.arch)],
+            args,
+            target: target.clone(),
         }
     }
+
+    fn add_apple_sdk(&mut self) -> Result<(), LinkerError> {
+        let arch = &self.target.arch;
+        let os = &self.target.options.os;
+        let llvm_target = &self.target.llvm_target;
+
+        let sdk_name = match (arch.as_ref(), os.as_ref()) {
+            ("aarch64", "tvos") => "appletvos",
+            ("x86_64", "tvos") => "appletvsimulator",
+            ("arm", "ios") => "iphoneos",
+            ("aarch64", "ios") if llvm_target.contains("macabi") => "macosx",
+            ("aarch64", "ios") if llvm_target.ends_with("-simulator") => "iphonesimulator",
+            ("aarch64", "ios") => "iphoneos",
+            ("x86", "ios") => "iphonesimulator",
+            ("x86_64", "ios") if llvm_target.contains("macabi") => "macosx",
+            ("x86_64", "ios") => "iphonesimulator",
+            ("x86_64", "watchos") => "watchsimulator",
+            ("arm64_32", "watchos") => "watchos",
+            ("aarch64", "watchos") if llvm_target.ends_with("-simulator") => "watchsimulator",
+            ("aarch64", "watchos") => "watchos",
+            ("arm", "watchos") => "watchos",
+            (_, "macos") => "macosx",
+            _ => {
+                return Err(LinkerError::PlatformSdkMissing(format!(
+                    "unsupported arch `{}` for os `{}`",
+                    arch, os
+                )));
+            }
+        };
+
+        let sdk_root = get_apple_sdk_root(sdk_name).map_err(LinkerError::PlatformSdkMissing)?;
+        self.args.push(String::from("-syslibroot"));
+        self.args.push(format!("{}", sdk_root.display()));
+        Ok(())
+    }
+
+    fn add_load_command(&mut self) {
+        let (major, minor, patch) = match self.target.options.min_os_version {
+            None => return,
+            Some(min) => min,
+        };
+
+        let arch = &self.target.arch;
+        let os = &self.target.options.os;
+
+        let load_command = match os.as_ref() {
+            "macos" => "-macosx_version_min",
+            "ios" | "watchos" | "tvos" => match arch.as_ref() {
+                "x86_64" => "-ios_simulator_version_min",
+                _ => "-iphoneos_version_min",
+            },
+            _ => unreachable!(),
+        };
+
+        self.args.push(load_command.to_string());
+        self.args.push(format!("{}.{}.{}", major, minor, patch));
+    }
 }
 
 impl Linker for Ld64Linker {
@@ -127,9 +195,8 @@ impl Linker for Ld64Linker {
         // Link as dynamic library
         self.args.push("-dylib".to_owned());
 
-        let sdk_root = get_apple_sdk_root().map_err(LinkerError::PlatformSdkMissing)?;
-        self.args.push(format!("-L{}/usr/lib", sdk_root.display()));
-        self.args.push("-lsystem".to_owned());
+        self.add_apple_sdk()?;
+        self.args.push("-lSystem".to_owned());
 
         // Specify output path
         self.args.push("-o".to_owned());
@@ -140,6 +207,8 @@ impl Linker for Ld64Linker {
         self.args.push("-install_name".to_owned());
         self.args.push(filename_str.to_owned());
 
+        self.add_load_command();
+
         Ok(())
     }
 
@@ -155,9 +224,9 @@ struct MsvcLinker {
 }
 
 impl MsvcLinker {
-    fn new(_target: &spec::Target) -> Self {
+    fn new(target: &spec::Target) -> Self {
         MsvcLinker {
-            args: Vec::default(),
+            args: target.options.pre_link_args.clone(),
         }
     }
 }
diff --git a/crates/mun_target/src/abi/mod.rs b/crates/mun_target/src/abi/mod.rs
index 7f337463c..f5cc56372 100644
--- a/crates/mun_target/src/abi/mod.rs
+++ b/crates/mun_target/src/abi/mod.rs
@@ -5,6 +5,8 @@ mod integer;
 mod size;
 
 use crate::spec::Target;
+use std::fmt::{Display, Formatter};
+use std::str::FromStr;
 
 pub use align::Align;
 pub use integer::Integer;
@@ -176,24 +178,21 @@ impl TargetDataLayout {
         }
 
         // Perform consistency checks against the Target information.
-        let endian_str = match dl.endian {
-            Endian::Little => "little",
-            Endian::Big => "big",
-        };
-        if endian_str != target.target_endian {
+        if dl.endian != target.options.endian {
             return Err(format!(
                 "inconsistent target specification: \"data-layout\" claims \
                                 architecture is {}-endian, while \"target-endian\" is `{}`",
-                endian_str, target.target_endian
+                dl.endian, target.options.endian
             ));
         }
 
-        if dl.pointer_size.bits().to_string() != target.target_pointer_width {
+        let target_pointer_width: u64 = target.pointer_width.into();
+        if dl.pointer_size.bits() != target_pointer_width {
             return Err(format!(
                 "inconsistent target specification: \"data-layout\" claims \
                                 pointers are {}-bit, while \"target-pointer-width\" is `{}`",
                 dl.pointer_size.bits(),
-                target.target_pointer_width
+                target.pointer_width
             ));
         }
 
@@ -259,6 +258,33 @@ pub enum Endian {
     Big,
 }
 
+impl Endian {
+    pub fn as_str(&self) -> &'static str {
+        match self {
+            Self::Little => "little",
+            Self::Big => "big",
+        }
+    }
+}
+
+impl Display for Endian {
+    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+        write!(f, "{}", self.as_str())
+    }
+}
+
+impl FromStr for Endian {
+    type Err = String;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        match s {
+            "little" => Ok(Self::Little),
+            "big" => Ok(Self::Big),
+            _ => Err(format!(r#"unknown endian: "{}""#, s)),
+        }
+    }
+}
+
 /// A pair of alignments, ABI-mandated and preferred.
 #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
 pub struct AbiAndPrefAlign {
diff --git a/crates/mun_target/src/spec.rs b/crates/mun_target/src/spec.rs
index 091ef42af..e25127554 100644
--- a/crates/mun_target/src/spec.rs
+++ b/crates/mun_target/src/spec.rs
@@ -1,8 +1,10 @@
 mod apple_base;
+mod apple_sdk_base;
 mod linux_base;
 mod windows_msvc_base;
+
+use crate::abi::Endian;
 use crate::host_triple;
-use thiserror::Error;
 
 #[derive(Debug, Clone, Copy, Eq, Ord, PartialOrd, PartialEq, Hash)]
 pub enum LinkerFlavor {
@@ -18,33 +20,15 @@ pub struct Target {
     /// Target triple to pass to LLVM
     pub llvm_target: String,
 
-    /// String to use as the `target_endian` `cfg` variable.
-    pub target_endian: String,
-
     /// String to use as the `target_pointer_width` `cfg` variable.
-    pub target_pointer_width: String,
-
-    /// Width of c_int type
-    pub target_c_int_width: String,
-
-    /// The name of the OS
-    pub target_os: String,
-
-    /// The name of the environment
-    pub target_env: String,
+    pub pointer_width: u32,
 
-    /// The name of the vendor
-    pub target_vendor: String,
-
-    /// The name of the architecture. For example "x86" or "x86_64"
+    /// The name of the architecture. For example "x86" or "x86_64", "arm", "aarch64"
     pub arch: String,
 
     /// [Data layout](http://llvm.org/docs/LangRef.html#data-layout) to pass to LLVM.
     pub data_layout: String,
 
-    /// Linker flavor
-    pub linker_flavor: LinkerFlavor,
-
     /// Optional settings
     pub options: TargetOptions,
 }
@@ -55,6 +39,34 @@ pub struct TargetOptions {
     /// True if this is a built-in target
     pub is_builtin: bool,
 
+    /// Used as the `target_endian` `cfg` variable. Defaults to little endian.
+    pub endian: Endian,
+
+    /// Width of c_int type
+    pub c_int_width: String,
+
+    /// The name of the OS
+    pub os: String,
+
+    /// Minimum version of the OS to target
+    pub min_os_version: Option<(u32, u32, u32)>,
+
+    /// The name of the environment
+    pub env: String,
+
+    /// ABI name to distinguish multiple ABIs on the same OS and architecture. For instance, `"eabi"`
+    /// or `"eabihf"`. Defaults to "".
+    pub abi: String,
+
+    /// The name of the vendor
+    pub vendor: String,
+
+    /// Linker flavor
+    pub linker_flavor: LinkerFlavor,
+
+    /// Linker arguments that are passed *before* any user-defined libraries.
+    pub pre_link_args: Vec<String>,
+
     /// Default CPU to pass to LLVM. Corresponds to `llc -mcpu=$cpu`. Defaults to "generic".
     pub cpu: String,
 
@@ -68,30 +80,34 @@ pub struct TargetOptions {
     /// Whether the target toolchain is like Windows
     pub is_like_windows: bool,
     pub is_like_msvc: bool,
+
+    /// Whether the target toolchain is like macOS's. Only useful for compiling against iOS/macOS,
+    /// in particular running dsymutil and some other stuff like `-dead_strip`. Defaults to false.
+    pub is_like_osx: bool,
 }
 
 impl Default for TargetOptions {
     fn default() -> Self {
         TargetOptions {
             is_builtin: false,
+            endian: Endian::Little,
+            c_int_width: "32".into(),
+            os: "none".into(),
+            min_os_version: None,
+            env: "".into(),
+            abi: "".into(),
+            vendor: "unknown".into(),
+            linker_flavor: LinkerFlavor::Ld,
+            pre_link_args: vec![],
             cpu: "generic".to_string(),
             features: "".to_string(),
             dll_prefix: "lib".to_string(),
             is_like_windows: false,
             is_like_msvc: false,
+            is_like_osx: false,
         }
     }
 }
-#[derive(Error, Debug)]
-pub enum LoadTargetError {
-    #[error("target not found: {0}")]
-    BuiltinTargetNotFound(String),
-
-    #[error("{0}")]
-    Other(String),
-}
-
-pub type TargetResult = Result<Target, String>;
 
 macro_rules! supported_targets {
     ( $(($( $triple:literal, )+ $module:ident ),)+ ) => {
@@ -100,45 +116,37 @@ macro_rules! supported_targets {
         /// List of supported targets
         const TARGETS: &[&str] = &[$($($triple),+),+];
 
-        fn load_specific(target: &str) -> Result<Target, LoadTargetError> {
-            match target {
-                $(
-                    $($triple)|+ => {
-                        let mut t = $module::target()
-                            .map_err(LoadTargetError::Other)?;
-                        t.options.is_builtin = true;
-
-                        log::debug!("got builtin target: {:?}", t);
-                        Ok(t)
-                    },
-                )+
-                    _ => Err(LoadTargetError::BuiltinTargetNotFound(
-                        format!("Unable to find target: {}", target)))
-            }
+        fn load_specific(target: &str) -> Option<Target> {
+            let mut t = match target {
+                $( $($triple)|+ => $module::target(), )+
+                _ => return None,
+            };
+            t.options.is_builtin = true;
+            log::debug!("got builtin target: {:?}", t);
+            Some(t)
         }
 
-        pub fn get_targets() -> impl Iterator<Item = String> {
-            TARGETS.iter().filter_map(|t| -> Option<String> {
-                load_specific(t)
-                    .and(Ok(t.to_string()))
-                    .ok()
-            })
+        pub fn get_targets() -> impl Iterator<Item = &'static str> {
+            TARGETS.iter().copied()
         }
     }
 }
 
 supported_targets!(
     ("x86_64-apple-darwin", x86_64_apple_darwin),
+    ("x86_64-apple-ios", x86_64_apple_ios),
     ("x86_64-pc-windows-msvc", x86_64_pc_windows_msvc),
     ("x86_64-unknown-linux-gnu", x86_64_unknown_linux_gnu),
+    ("aarch64-apple-ios", aarch64_apple_ios),
+    ("aarch64-apple-ios-sim", aarch64_apple_ios_sim),
 );
 
 impl Target {
-    pub fn search(target_triple: &str) -> Result<Target, LoadTargetError> {
+    pub fn search(target_triple: &str) -> Option<Target> {
         load_specific(target_triple)
     }
 
-    pub fn host_target() -> Result<Target, LoadTargetError> {
+    pub fn host_target() -> Option<Target> {
         Self::search(host_triple())
     }
 }
diff --git a/crates/mun_target/src/spec/aarch64_apple_ios.rs b/crates/mun_target/src/spec/aarch64_apple_ios.rs
new file mode 100644
index 000000000..1469e9f4b
--- /dev/null
+++ b/crates/mun_target/src/spec/aarch64_apple_ios.rs
@@ -0,0 +1,24 @@
+use crate::spec::{Target, TargetOptions};
+use super::apple_sdk_base::{opts, Arch};
+
+pub fn target() -> Target {
+    // Clang automatically chooses a more specific target based on
+    // IPHONEOS_DEPLOYMENT_TARGET.
+    // This is required for the target to pick the right
+    // MACH-O commands, so we do too.
+    let arch = "arm64";
+    let llvm_target = super::apple_base::ios_llvm_target(arch);
+    let (major, minor) = super::apple_base::ios_deployment_target();
+
+    Target {
+        llvm_target,
+        pointer_width: 64,
+        arch: "aarch64".into(),
+        data_layout: "e-m:o-i64:64-i128:128-n32:64-S128".into(),
+        options: TargetOptions {
+            features: "+neon,+fp-armv8,+apple-a7".into(),
+            min_os_version: Some((major, minor, 0)),
+            ..opts("ios", Arch::Arm64)
+        }
+    }
+}
diff --git a/crates/mun_target/src/spec/aarch64_apple_ios_sim.rs b/crates/mun_target/src/spec/aarch64_apple_ios_sim.rs
new file mode 100644
index 000000000..18dbd98e4
--- /dev/null
+++ b/crates/mun_target/src/spec/aarch64_apple_ios_sim.rs
@@ -0,0 +1,24 @@
+use crate::spec::{Target, TargetOptions};
+use super::apple_sdk_base::{opts, Arch};
+
+pub fn target() -> Target {
+    // Clang automatically chooses a more specific target based on
+    // IPHONEOS_DEPLOYMENT_TARGET.
+    // This is required for the target to pick the right
+    // MACH-O commands, so we do too.
+    let arch = "arm64";
+    let llvm_target = super::apple_base::ios_sim_llvm_target(arch);
+    let (major, minor) = super::apple_base::ios_deployment_target();
+
+    Target {
+        llvm_target,
+        pointer_width: 64,
+        arch: "aarch64".into(),
+        data_layout: "e-m:o-i64:64-i128:128-n32:64-S128".into(),
+        options: TargetOptions {
+            features: "+neon,+fp-armv8,+apple-a7".into(),
+            min_os_version: Some((major, minor, 0)),
+            ..opts("ios", Arch::Arm64_sim)
+        }
+    }
+}
diff --git a/crates/mun_target/src/spec/apple_base.rs b/crates/mun_target/src/spec/apple_base.rs
index 13a69f643..6e9f95f2f 100644
--- a/crates/mun_target/src/spec/apple_base.rs
+++ b/crates/mun_target/src/spec/apple_base.rs
@@ -1,28 +1,62 @@
-use crate::spec::TargetOptions;
+use crate::spec::{LinkerFlavor, TargetOptions};
 use std::env;
 
-pub fn opts() -> TargetOptions {
+pub fn opts(os: &'static str) -> TargetOptions {
     TargetOptions {
+        os: os.into(),
+        vendor: "apple".into(),
+        linker_flavor: LinkerFlavor::Ld64,
         dll_prefix: "lib".to_string(),
         ..Default::default()
     }
 }
 
-fn macos_deployment_target() -> (u32, u32) {
-    let deployment_target = env::var("MACOSX_DEPLOYMENT_TARGET").ok();
-    let version = deployment_target
+fn deployment_target(var_name: &str) -> Option<(u32, u32)> {
+    let deployment_target = env::var(var_name).ok();
+    deployment_target
         .as_ref()
         .and_then(|s| s.split_once('.'))
         .and_then(|(a, b)| {
             a.parse::<u32>()
                 .and_then(|a| b.parse::<u32>().map(|b| (a, b)))
                 .ok()
-        });
+        })
+}
 
-    version.unwrap_or((10, 7))
+fn macos_default_deployment_target(arch: &str) -> (u32, u32) {
+    if arch == "arm64" {
+        (11, 0)
+    } else {
+        (10, 7)
+    }
+}
+
+pub fn macos_deployment_target(arch: &str) -> (u32, u32) {
+    deployment_target("MACOSX_DEPLOYMENT_TARGET")
+        .unwrap_or_else(|| macos_default_deployment_target(arch))
 }
 
 pub fn macos_llvm_target(arch: &str) -> String {
-    let (major, minor) = macos_deployment_target();
+    let (major, minor) = macos_deployment_target(arch);
     format!("{}-apple-macosx{}.{}.0", arch, major, minor)
 }
+
+pub fn ios_deployment_target() -> (u32, u32) {
+    deployment_target("IPHONEOS_DEPLOYMENT_TARGET").unwrap_or((7, 0))
+}
+
+pub fn ios_llvm_target(arch: &str) -> String {
+    // Modern iOS tooling extracts information about deployment target
+    // from LC_BUILD_VERSION. This load command will only be emitted when
+    // we build with a version specific `llvm_target`, with the version
+    // set high enough. Luckily one LC_BUILD_VERSION is enough, for Xcode
+    // to pick it up (since std and core are still built with the fallback
+    // of version 7.0 and hence emit the old LC_IPHONE_MIN_VERSION).
+    let (major, minor) = ios_deployment_target();
+    format!("{}-apple-ios{}.{}.0", arch, major, minor)
+}
+
+pub fn ios_sim_llvm_target(arch: &str) -> String {
+    let (major, minor) = ios_deployment_target();
+    format!("{}-apple-ios{}.{}.0-simulator", arch, major, minor)
+}
diff --git a/crates/mun_target/src/spec/apple_sdk_base.rs b/crates/mun_target/src/spec/apple_sdk_base.rs
new file mode 100644
index 000000000..b681b414e
--- /dev/null
+++ b/crates/mun_target/src/spec/apple_sdk_base.rs
@@ -0,0 +1,49 @@
+use crate::spec::TargetOptions;
+
+use Arch::*;
+
+#[allow(non_camel_case_types, dead_code)]
+#[derive(Copy, Clone)]
+pub enum Arch {
+    Armv7,
+    Armv7k,
+    Armv7s,
+    Arm64,
+    Arm64_32,
+    I386,
+    X86_64,
+    X86_64_macabi,
+    Arm64_macabi,
+    Arm64_sim,
+}
+
+fn target_abi(arch: Arch) -> &'static str {
+    match arch {
+        Armv7 | Armv7k | Armv7s | Arm64 | Arm64_32 | I386 | X86_64 => "",
+        X86_64_macabi | Arm64_macabi => "macabi",
+        Arm64_sim => "sim",
+    }
+}
+
+fn target_cpu(arch: Arch) -> &'static str {
+    match arch {
+        Armv7 => "cortex-a8", // iOS7 is supported on iPhone 4 and higher
+        Armv7k => "cortex-a8",
+        Armv7s => "cortex-a9",
+        Arm64 => "apple-a7",
+        Arm64_32 => "apple-s4",
+        I386 => "yonah",
+        X86_64 => "core2",
+        X86_64_macabi => "core2",
+        Arm64_macabi => "apple-a12",
+        Arm64_sim => "apple-a12",
+    }
+}
+
+pub fn opts(os: &'static str, arch: Arch) -> TargetOptions {
+    TargetOptions {
+        abi: target_abi(arch).into(),
+        cpu: target_cpu(arch).into(),
+        ..super::apple_base::opts(os)
+    }
+}
diff --git a/crates/mun_target/src/spec/linux_base.rs b/crates/mun_target/src/spec/linux_base.rs
index 9e8485359..25383b3b0 100644
--- a/crates/mun_target/src/spec/linux_base.rs
+++ b/crates/mun_target/src/spec/linux_base.rs
@@ -1,7 +1,11 @@
-use crate::spec::TargetOptions;
+use crate::spec::{LinkerFlavor, TargetOptions};
 
 pub fn opts() -> TargetOptions {
     TargetOptions {
+        os: "linux".to_string(),
+        env: "gnu".to_string(),
+        vendor: "unknown".to_string(),
+        linker_flavor: LinkerFlavor::Ld,
         ..Default::default()
     }
 }
diff --git a/crates/mun_target/src/spec/windows_msvc_base.rs b/crates/mun_target/src/spec/windows_msvc_base.rs
index 6be332aa5..2a91d9479 100644
--- a/crates/mun_target/src/spec/windows_msvc_base.rs
+++ b/crates/mun_target/src/spec/windows_msvc_base.rs
@@ -1,7 +1,11 @@
-use crate::spec::TargetOptions;
+use crate::spec::{LinkerFlavor, TargetOptions};
 
 pub fn opts() -> TargetOptions {
     TargetOptions {
+        os: "windows".into(),
+        env: "msvc".into(),
+        vendor: "pc".into(),
+        linker_flavor: LinkerFlavor::Msvc,
         dll_prefix: "".to_string(),
         is_like_windows: true,
         is_like_msvc: true,
diff --git a/crates/mun_target/src/spec/x86_64_apple_darwin.rs b/crates/mun_target/src/spec/x86_64_apple_darwin.rs
index 82882bc12..6b31318c3 100644
--- a/crates/mun_target/src/spec/x86_64_apple_darwin.rs
+++ b/crates/mun_target/src/spec/x86_64_apple_darwin.rs
@@ -1,27 +1,23 @@
-use crate::spec::{LinkerFlavor, Target, TargetResult};
-
-pub fn target() -> TargetResult {
-    let mut base = super::apple_base::opts();
-    base.cpu = "core2".to_string();
+use crate::spec::{Target, TargetOptions};
 
+pub fn target() -> Target {
     // Clang automatically chooses a more specific target based on
     // MACOSX_DEPLOYMENT_TARGET.  To enable cross-language LTO to work
     // correctly, we do too.
     let arch = "x86_64";
     let llvm_target = super::apple_base::macos_llvm_target(arch);
+    let (major, minor) = super::apple_base::macos_deployment_target(arch);
 
-    Ok(Target {
+    Target {
         llvm_target,
-        target_os: "macos".to_string(),
-        target_endian: "little".to_string(),
-        target_pointer_width: "64".to_string(),
-        target_c_int_width: "32".to_string(),
-        target_env: String::new(),
-        target_vendor: "apple".to_string(),
+        pointer_width: 64,
         arch: arch.to_string(),
         data_layout: "e-m:o-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128"
             .to_string(),
-        linker_flavor: LinkerFlavor::Ld64,
-        options: base,
-    })
+        options: TargetOptions {
+            cpu: "core2".into(),
+            min_os_version: Some((major, minor, 0)),
+            .. super::apple_base::opts("macos")
+        },
+    }
 }
diff --git a/crates/mun_target/src/spec/x86_64_apple_ios.rs b/crates/mun_target/src/spec/x86_64_apple_ios.rs
new file mode 100644
index 000000000..65375410d
--- /dev/null
+++ b/crates/mun_target/src/spec/x86_64_apple_ios.rs
@@ -0,0 +1,18 @@
+use crate::spec::{Target, TargetOptions};
+use super::apple_sdk_base::{opts, Arch};
+
+pub fn target() -> Target {
+    let llvm_target = super::apple_base::ios_sim_llvm_target("x86_64");
+    let (major, minor) = super::apple_base::ios_deployment_target();
+
+    Target {
+        llvm_target,
+        pointer_width: 64,
+        arch: "x86_64".into(),
+        data_layout: "e-m:o-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128".into(),
+        options: TargetOptions {
+            min_os_version: Some((major, minor, 0)),
+            ..opts("ios", Arch::X86_64)
+        }
+    }
+}
diff --git a/crates/mun_target/src/spec/x86_64_pc_windows_msvc.rs b/crates/mun_target/src/spec/x86_64_pc_windows_msvc.rs
index 014f2332c..82d6e7883 100644
--- a/crates/mun_target/src/spec/x86_64_pc_windows_msvc.rs
+++ b/crates/mun_target/src/spec/x86_64_pc_windows_msvc.rs
@@ -1,21 +1,15 @@
-use crate::spec::{LinkerFlavor, Target, TargetResult};
+use crate::spec::{Target, TargetOptions};
 
-pub fn target() -> TargetResult {
-    let mut base = super::windows_msvc_base::opts();
-    base.cpu = "x86-64".to_string();
-
-    Ok(Target {
+pub fn target() -> Target {
+    Target {
         llvm_target: "x86_64-pc-windows-msvc".to_string(),
-        target_endian: "little".to_string(),
-        target_pointer_width: "64".to_string(),
-        target_c_int_width: "32".to_string(),
-        target_os: "windows".to_string(),
-        target_env: "msvc".to_string(),
-        target_vendor: "pc".to_string(),
+        pointer_width: 64,
         arch: "x86_64".to_string(),
         data_layout: "e-m:w-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128"
             .to_string(),
-        linker_flavor: LinkerFlavor::Msvc,
-        options: base,
-    })
+        options: TargetOptions {
+            cpu: "x86-64".to_string(),
+            ..super::windows_msvc_base::opts()
+        },
+    }
 }
diff --git a/crates/mun_target/src/spec/x86_64_unknown_linux_gnu.rs b/crates/mun_target/src/spec/x86_64_unknown_linux_gnu.rs
index dcb550825..8907ca09a 100644
--- a/crates/mun_target/src/spec/x86_64_unknown_linux_gnu.rs
+++ b/crates/mun_target/src/spec/x86_64_unknown_linux_gnu.rs
@@ -1,21 +1,15 @@
-use crate::spec::{LinkerFlavor, Target, TargetResult};
+use crate::spec::{Target, TargetOptions};
 
-pub fn target() -> TargetResult {
-    let mut base = super::linux_base::opts();
-    base.cpu = "x86-64".to_string();
-
-    Ok(Target {
+pub fn target() -> Target {
+    Target {
         llvm_target: "x86_64-unknown-linux-gnu".to_string(),
-        target_endian: "little".to_string(),
-        target_pointer_width: "64".to_string(),
-        target_c_int_width: "32".to_string(),
-        target_os: "linux".to_string(),
-        target_env: "gnu".to_string(),
-        target_vendor: "unknown".to_string(),
+        pointer_width: 64,
         arch: "x86_64".to_string(),
         data_layout: "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128"
             .to_string(),
-        linker_flavor: LinkerFlavor::Ld,
-        options: base,
-    })
+        options: TargetOptions {
+            cpu: "x86-64".into(),
+            .. super::linux_base::opts()
+        },
+    }
 }


Test patch: