#![allow(dead_code)] use std::{env, fmt::Display, path::Path, path::PathBuf, process::Command}; use serde::Deserialize; #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct SwiftTarget { triple: String, unversioned_triple: String, module_triple: String, //pub swift_runtime_compatibility_version: String, #[serde(rename = "librariesRequireRPath")] libraries_require_rpath: bool, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct SwiftPaths { runtime_library_paths: Vec, runtime_library_import_paths: Vec, runtime_resource_path: String, } #[derive(Deserialize)] struct SwiftEnv { target: SwiftTarget, paths: SwiftPaths, } impl SwiftEnv { fn new(minimum_macos_version: &str, minimum_ios_version: Option<&str>) -> Self { let rust_target = RustTarget::from_env(); let target = rust_target.swift_target_triple(minimum_macos_version, minimum_ios_version); let swift_target_info_str = Command::new("swift") .args(["-target", &target, "-print-target-info"]) .output() .unwrap() .stdout; serde_json::from_slice(&swift_target_info_str).unwrap() } } #[allow(clippy::upper_case_acronyms)] enum RustTargetOS { MacOS, IOS, } impl RustTargetOS { fn from_env() -> Self { match env::var("CARGO_CFG_TARGET_OS").unwrap().as_str() { "macos" => RustTargetOS::MacOS, "ios" => RustTargetOS::IOS, _ => panic!("unexpected target operating system"), } } fn to_swift(&self) -> &'static str { match self { Self::MacOS => "macosx", Self::IOS => "ios", } } } impl Display for RustTargetOS { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::MacOS => write!(f, "macos"), Self::IOS => write!(f, "ios"), } } } #[allow(clippy::upper_case_acronyms)] enum SwiftSDK { MacOS, IOS, IOSSimulator, } impl SwiftSDK { fn from_os(os: &RustTargetOS) -> Self { let target = env::var("TARGET").unwrap(); let simulator = target.ends_with("ios-sim") || (target.starts_with("x86_64") && target.ends_with("ios")); match os { RustTargetOS::MacOS => Self::MacOS, RustTargetOS::IOS if simulator => Self::IOSSimulator, RustTargetOS::IOS => Self::IOS, } } fn clang_lib_extension(&self) -> &'static str { match self { Self::MacOS => "osx", Self::IOS => "ios", Self::IOSSimulator => "iossim", } } } impl Display for SwiftSDK { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::MacOS => write!(f, "macosx"), Self::IOSSimulator => write!(f, "iphonesimulator"), Self::IOS => write!(f, "iphoneos"), } } } struct RustTarget { arch: String, os: RustTargetOS, sdk: SwiftSDK, } impl RustTarget { fn from_env() -> Self { let arch = env::var("CARGO_CFG_TARGET_ARCH").unwrap(); let os = RustTargetOS::from_env(); let sdk = SwiftSDK::from_os(&os); Self { arch, os, sdk } } fn swift_target_triple( &self, minimum_macos_version: &str, minimum_ios_version: Option<&str>, ) -> String { let unversioned = self.unversioned_swift_target_triple(); format!( "{unversioned}{}{}", match (&self.os, minimum_ios_version) { (RustTargetOS::MacOS, _) => minimum_macos_version, (RustTargetOS::IOS, Some(version)) => version, _ => "", }, // simulator suffix matches!(self.sdk, SwiftSDK::IOSSimulator) .then(|| "-simulator".to_string()) .unwrap_or_default() ) } fn unversioned_swift_target_triple(&self) -> String { format!( "{}-apple-{}", match self.arch.as_str() { "aarch64" => "arm64", a => a, }, self.os.to_swift(), ) } } struct SwiftPackage { name: String, path: PathBuf, } /// Builder for linking the Swift runtime and custom packages. #[cfg(feature = "build")] pub struct SwiftLinker { packages: Vec, macos_min_version: String, ios_min_version: Option, } impl SwiftLinker { /// Creates a new [`SwiftLinker`] with a minimum macOS verison. /// /// Minimum macOS version must be at least 10.13. pub fn new(macos_min_version: &str) -> Self { Self { packages: vec![], macos_min_version: macos_min_version.to_string(), ios_min_version: None, } } /// Instructs the [`SwiftLinker`] to also compile for iOS /// using the specified minimum iOS version. /// /// Minimum iOS version must be at least 11. pub fn with_ios(mut self, min_version: &str) -> Self { self.ios_min_version = Some(min_version.to_string()); self } /// Adds a package to be linked against. /// `name` should match the `name` field in your `Package.swift`, /// and `path` should point to the root of your Swift package relative /// to your crate's root. pub fn with_package(mut self, name: &str, path: impl AsRef) -> Self { self.packages.extend([SwiftPackage { name: name.to_string(), path: path.as_ref().into(), }]); self } /// Links the Swift runtime, then builds and links the provided packages. /// This does not (yet) automatically rebuild your Swift files when they are modified, /// you'll need to modify/save your `build.rs` file for that. pub fn link(self) { let swift_env = SwiftEnv::new(&self.macos_min_version, self.ios_min_version.as_deref()); #[allow(clippy::uninlined_format_args)] for path in swift_env.paths.runtime_library_paths { println!("cargo:rustc-link-search=native={path}"); } let debug = env::var("DEBUG").unwrap() == "true"; let configuration = if debug { "debug" } else { "release" }; let rust_target = RustTarget::from_env(); link_clang_rt(&rust_target); for package in self.packages { let package_path = Path::new(&env::var("CARGO_MANIFEST_DIR").unwrap()).join(&package.path); let out_path = Path::new(&env::var("OUT_DIR").unwrap()) .join("swift-rs") .join(&package.name); let sdk_path_output = Command::new("xcrun") .args(["--sdk", &rust_target.sdk.to_string(), "--show-sdk-path"]) .output() .unwrap(); if !sdk_path_output.status.success() { panic!( "Failed to get SDK path with `xcrun --sdk {} --show-sdk-path`", rust_target.sdk ); } let sdk_path = String::from_utf8_lossy(&sdk_path_output.stdout); let mut command = Command::new("swift"); command.current_dir(&package.path); let arch = match std::env::consts::ARCH { "aarch64" => "arm64", arch => arch, }; command // Build the package (duh) .args(["build"]) // SDK path for regular compilation (idk) .args(["--sdk", sdk_path.trim()]) // Release/Debug configuration .args(["-c", configuration]) .args(["--arch", arch]) // Where the artifacts will be generated to .args(["--build-path", &out_path.display().to_string()]) // Override SDK path for each swiftc instance. // Necessary for iOS compilation. .args(["-Xswiftc", "-sdk"]) .args(["-Xswiftc", sdk_path.trim()]) // Override target triple for each swiftc instance. // Necessary for iOS compilation. .args(["-Xswiftc", "-target"]) .args([ "-Xswiftc", &rust_target.swift_target_triple( &self.macos_min_version, self.ios_min_version.as_deref(), ), ]); if !command.status().unwrap().success() { panic!("Failed to compile swift package {}", package.name); } let search_path = out_path // swift build uses this output folder no matter what is the target .join(format!( "{}-apple-macosx", arch )) .join(configuration); println!("cargo:rerun-if-changed={}", package_path.display()); println!("cargo:rustc-link-search=native={}", search_path.display()); println!("cargo:rustc-link-lib=static={}", package.name); } } } fn link_clang_rt(rust_target: &RustTarget) { println!( "cargo:rustc-link-lib=clang_rt.{}", rust_target.sdk.clang_lib_extension() ); println!("cargo:rustc-link-search={}", clang_link_search_path()); } fn clang_link_search_path() -> String { let output = std::process::Command::new( std::env::var("SWIFT_RS_CLANG").unwrap_or_else(|_| "/usr/bin/clang".to_string()), ) .arg("--print-search-dirs") .output() .unwrap(); if !output.status.success() { panic!("Can't get search paths from clang"); } let stdout = String::from_utf8_lossy(&output.stdout); for line in stdout.lines() { if line.contains("libraries: =") { let path = line.split('=').nth(1).unwrap(); return format!("{}/lib/darwin", path); } } panic!("clang is missing search paths"); }