swift_rs/
build.rs

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    //pub swift_runtime_compatibility_version: String,
13    #[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            // simulator suffix
146            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/// Builder for linking the Swift runtime and custom packages.
170#[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    /// Creates a new [`SwiftLinker`] with a minimum macOS verison.
179    ///
180    /// Minimum macOS version must be at least 10.13.
181    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    /// Instructs the [`SwiftLinker`] to also compile for iOS
190    /// using the specified minimum iOS version.
191    ///
192    /// Minimum iOS version must be at least 11.
193    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    /// Adds a package to be linked against.
199    /// `name` should match the `name` field in your `Package.swift`,
200    /// and `path` should point to the root of your Swift package relative
201    /// to your crate's root.
202    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    /// Links the Swift runtime, then builds and links the provided packages.
212    /// This does not (yet) automatically rebuild your Swift files when they are modified,
213    /// you'll need to modify/save your `build.rs` file for that.
214    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                // Build the package (duh)
261                .arg("build")
262                // SDK path for regular compilation (idk)
263                .args(["--sdk", sdk_path.trim()])
264                // Release/Debug configuration
265                .args(["-c", configuration])
266                .args(["--arch", arch])
267                // Where the artifacts will be generated to
268                .args(["--build-path", &out_path.display().to_string()])
269                // Override SDK path for each swiftc instance.
270                // Necessary for iOS compilation.
271                .args(["-Xswiftc", "-sdk"])
272                .args(["-Xswiftc", sdk_path.trim()])
273                // Override target triple for each swiftc instance.
274                // Necessary for iOS compilation.
275                .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                // swift build uses this output folder no matter what is the target
286                .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}