1#![allow(dead_code)]
2use std::{env, fmt::Display, path::Path, path::PathBuf, process::Command};
3
4use serde::Deserialize;
5
6#[derive(Debug, Deserialize)]
7#[serde(rename_all = "camelCase")]
8struct SwiftTarget {
9 triple: String,
10 unversioned_triple: String,
11 module_triple: String,
12 #[serde(rename = "librariesRequireRPath")]
14 libraries_require_rpath: bool,
15}
16
17#[derive(Debug, Deserialize)]
18#[serde(rename_all = "camelCase")]
19struct SwiftPaths {
20 runtime_library_paths: Vec<String>,
21 runtime_library_import_paths: Vec<String>,
22 runtime_resource_path: String,
23}
24
25#[derive(Deserialize)]
26struct SwiftEnv {
27 target: SwiftTarget,
28 paths: SwiftPaths,
29}
30
31impl SwiftEnv {
32 fn new(minimum_macos_version: &str, minimum_ios_version: Option<&str>) -> Self {
33 let rust_target = RustTarget::from_env();
34 let target = rust_target.swift_target_triple(minimum_macos_version, minimum_ios_version);
35
36 let swift_target_info_str = Command::new("swift")
37 .args(["-target", &target, "-print-target-info"])
38 .output()
39 .unwrap()
40 .stdout;
41
42 serde_json::from_slice(&swift_target_info_str).unwrap()
43 }
44}
45
46#[allow(clippy::upper_case_acronyms)]
47enum RustTargetOS {
48 MacOS,
49 IOS,
50}
51
52impl RustTargetOS {
53 fn from_env() -> Self {
54 match env::var("CARGO_CFG_TARGET_OS").unwrap().as_str() {
55 "macos" => RustTargetOS::MacOS,
56 "ios" => RustTargetOS::IOS,
57 _ => panic!("unexpected target operating system"),
58 }
59 }
60
61 fn to_swift(&self) -> &'static str {
62 match self {
63 Self::MacOS => "macosx",
64 Self::IOS => "ios",
65 }
66 }
67}
68
69impl Display for RustTargetOS {
70 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71 match self {
72 Self::MacOS => write!(f, "macos"),
73 Self::IOS => write!(f, "ios"),
74 }
75 }
76}
77
78#[allow(clippy::upper_case_acronyms)]
79enum SwiftSDK {
80 MacOS,
81 IOS,
82 IOSSimulator,
83}
84
85impl SwiftSDK {
86 fn from_os(os: &RustTargetOS) -> Self {
87 let target = env::var("TARGET").unwrap();
88 let simulator = target.ends_with("ios-sim")
89 || (target.starts_with("x86_64") && target.ends_with("ios"));
90
91 match os {
92 RustTargetOS::MacOS => Self::MacOS,
93 RustTargetOS::IOS if simulator => Self::IOSSimulator,
94 RustTargetOS::IOS => Self::IOS,
95 }
96 }
97
98 fn clang_lib_extension(&self) -> &'static str {
99 match self {
100 Self::MacOS => "osx",
101 Self::IOS => "ios",
102 Self::IOSSimulator => "iossim",
103 }
104 }
105}
106
107impl Display for SwiftSDK {
108 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
109 match self {
110 Self::MacOS => write!(f, "macosx"),
111 Self::IOSSimulator => write!(f, "iphonesimulator"),
112 Self::IOS => write!(f, "iphoneos"),
113 }
114 }
115}
116
117struct RustTarget {
118 arch: String,
119 os: RustTargetOS,
120 sdk: SwiftSDK,
121}
122
123impl RustTarget {
124 fn from_env() -> Self {
125 let arch = env::var("CARGO_CFG_TARGET_ARCH").unwrap();
126 let os = RustTargetOS::from_env();
127 let sdk = SwiftSDK::from_os(&os);
128
129 Self { arch, os, sdk }
130 }
131
132 fn swift_target_triple(
133 &self,
134 minimum_macos_version: &str,
135 minimum_ios_version: Option<&str>,
136 ) -> String {
137 let unversioned = self.unversioned_swift_target_triple();
138 format!(
139 "{unversioned}{}{}",
140 match (&self.os, minimum_ios_version) {
141 (RustTargetOS::MacOS, _) => minimum_macos_version,
142 (RustTargetOS::IOS, Some(version)) => version,
143 _ => "",
144 },
145 matches!(self.sdk, SwiftSDK::IOSSimulator)
147 .then(|| "-simulator".to_string())
148 .unwrap_or_default()
149 )
150 }
151
152 fn unversioned_swift_target_triple(&self) -> String {
153 format!(
154 "{}-apple-{}",
155 match self.arch.as_str() {
156 "aarch64" => "arm64",
157 a => a,
158 },
159 self.os.to_swift(),
160 )
161 }
162}
163
164struct SwiftPackage {
165 name: String,
166 path: PathBuf,
167}
168
169#[cfg(feature = "build")]
171pub struct SwiftLinker {
172 packages: Vec<SwiftPackage>,
173 macos_min_version: String,
174 ios_min_version: Option<String>,
175}
176
177impl SwiftLinker {
178 pub fn new(macos_min_version: &str) -> Self {
182 Self {
183 packages: vec![],
184 macos_min_version: macos_min_version.to_string(),
185 ios_min_version: None,
186 }
187 }
188
189 pub fn with_ios(mut self, min_version: &str) -> Self {
194 self.ios_min_version = Some(min_version.to_string());
195 self
196 }
197
198 pub fn with_package(mut self, name: &str, path: impl AsRef<Path>) -> Self {
203 self.packages.extend([SwiftPackage {
204 name: name.to_string(),
205 path: path.as_ref().into(),
206 }]);
207
208 self
209 }
210
211 pub fn link(self) {
215 let swift_env = SwiftEnv::new(&self.macos_min_version, self.ios_min_version.as_deref());
216
217 #[allow(clippy::uninlined_format_args)]
218 for path in swift_env.paths.runtime_library_paths {
219 println!("cargo:rustc-link-search=native={path}");
220 }
221
222 let debug = env::var("DEBUG").unwrap() == "true";
223 let configuration = if debug { "debug" } else { "release" };
224 let rust_target = RustTarget::from_env();
225
226 link_clang_rt(&rust_target);
227
228 for package in self.packages {
229 let package_path =
230 Path::new(&env::var("CARGO_MANIFEST_DIR").unwrap()).join(&package.path);
231 let out_path = Path::new(&env::var("OUT_DIR").unwrap())
232 .join("swift-rs")
233 .join(&package.name);
234
235 let sdk_path_output = Command::new("xcrun")
236 .args(["--sdk", &rust_target.sdk.to_string(), "--show-sdk-path"])
237 .output()
238 .unwrap();
239 if !sdk_path_output.status.success() {
240 panic!(
241 "Failed to get SDK path with `xcrun --sdk {} --show-sdk-path`",
242 rust_target.sdk
243 );
244 }
245
246 let sdk_path = String::from_utf8_lossy(&sdk_path_output.stdout);
247
248 let mut command = Command::new("swift");
249 command.current_dir(&package.path);
250
251 let arch = match std::env::consts::ARCH {
252 "aarch64" => "arm64",
253 arch => arch,
254 };
255
256 let swift_target_triple = rust_target
257 .swift_target_triple(&self.macos_min_version, self.ios_min_version.as_deref());
258
259 command
260 .arg("build")
262 .args(["--sdk", sdk_path.trim()])
264 .args(["-c", configuration])
266 .args(["--arch", arch])
267 .args(["--build-path", &out_path.display().to_string()])
269 .args(["-Xswiftc", "-sdk"])
272 .args(["-Xswiftc", sdk_path.trim()])
273 .args(["-Xswiftc", "-target"])
276 .args(["-Xswiftc", &swift_target_triple])
277 .args(["-Xcc", &format!("--target={swift_target_triple}")])
278 .args(["-Xcxx", &format!("--target={swift_target_triple}")]);
279
280 if !command.status().unwrap().success() {
281 panic!("Failed to compile swift package {}", package.name);
282 }
283
284 let search_path = out_path
285 .join(format!("{}-apple-macosx", arch))
287 .join(configuration);
288
289 println!("cargo:rerun-if-changed={}", package_path.display());
290 println!("cargo:rustc-link-search=native={}", search_path.display());
291 println!("cargo:rustc-link-lib=static={}", package.name);
292 }
293 }
294}
295
296fn link_clang_rt(rust_target: &RustTarget) {
297 println!(
298 "cargo:rustc-link-lib=clang_rt.{}",
299 rust_target.sdk.clang_lib_extension()
300 );
301 println!("cargo:rustc-link-search={}", clang_link_search_path());
302}
303
304fn clang_link_search_path() -> String {
305 let output = std::process::Command::new(
306 std::env::var("SWIFT_RS_CLANG").unwrap_or_else(|_| "/usr/bin/clang".to_string()),
307 )
308 .arg("--print-search-dirs")
309 .output()
310 .unwrap();
311 if !output.status.success() {
312 panic!("Can't get search paths from clang");
313 }
314 let stdout = String::from_utf8_lossy(&output.stdout);
315 for line in stdout.lines() {
316 if line.contains("libraries: =") {
317 let path = line.split('=').nth(1).unwrap();
318 return format!("{}/lib/darwin", path);
319 }
320 }
321 panic!("clang is missing search paths");
322}