atagen

matrix-rust-sdk

public
Star0
Watch0
ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk.git
xtask/src/swift.rsRust586 lines20.2 KB
1
use std::{
2
    collections::HashMap,
3
    fs::{copy, create_dir_all, remove_dir_all, remove_file, rename},
4
};
5
6
use camino::{Utf8Path, Utf8PathBuf};
7
use clap::{Args, Subcommand};
8
use uniffi_bindgen::bindings::{GenerateOptions, TargetLanguage, generate};
9
use xshell::cmd;
10
11
use crate::{Result, sh, workspace};
12
13
/// Builds the SDK for Swift as a Static Library or XCFramework.
14
#[derive(Args)]
15
pub struct SwiftArgs {
16
    #[clap(subcommand)]
17
    cmd: SwiftCommand,
18
}
19
20
#[derive(Subcommand)]
21
enum SwiftCommand {
22
    /// Builds the SDK for Swift as a static lib.
23
    BuildLibrary,
24
25
    /// Builds the SDK for Swift as an XCFramework.
26
    BuildFramework {
27
        /// Build with the release profile
28
        #[clap(long)]
29
        release: bool,
30
31
        /// Build with a custom profile, takes precedence over `--release`
32
        #[clap(long)]
33
        profile: Option<String>,
34
35
        /// Build the given target. This option can be specified multiple times
36
        /// to build more than one. Omitting this option will build all
37
        /// supported targets.
38
        #[clap(long)]
39
        target: Option<Vec<String>>,
40
41
        /// Includes the Tier 3 targets (such as watchOS) when building all
42
        /// supported targets. Requires a nightly toolchain with the `rust-src`
43
        /// component installed.
44
        #[clap(long)]
45
        tier3_targets: bool,
46
47
        /// Move the generated xcframework and swift sources into the given
48
        /// components-folder
49
        #[clap(long)]
50
        components_path: Option<Utf8PathBuf>,
51
52
        /// The iOS deployment target to use when building the framework.
53
        ///
54
        /// Defaults to not being set, which implies that the build will use the
55
        /// default values provided by the Rust and Xcode toolchains.
56
        #[clap(long)]
57
        ios_deployment_target: Option<String>,
58
59
        /// The wachOS deployment target to use when building the framework.
60
        ///
61
        /// Defaults to not being set, which implies that the build will use the
62
        /// default values provided by the Rust and Xcode toolchains.
63
        #[clap(long)]
64
        watchos_deployment_target: Option<String>,
65
66
        /// Build the targets one by one instead of passing all of them
67
        /// to cargo in one go, which makes it hang on lesser devices like plain
68
        /// Apple Silicon M1s
69
        #[clap(long)]
70
        sequentially: bool,
71
    },
72
}
73
74
impl SwiftArgs {
75
    pub fn run(self) -> Result<()> {
76
        let sh = sh();
77
        let _p = sh.push_dir(workspace::root_path()?);
78
79
        match self.cmd {
80
            SwiftCommand::BuildLibrary => build_library(),
81
            SwiftCommand::BuildFramework {
82
                release,
83
                profile,
84
                target: targets,
85
                tier3_targets,
86
                components_path,
87
                ios_deployment_target,
88
                watchos_deployment_target,
89
                sequentially,
90
            } => {
91
                // The dev profile seems to cause crashes on some platforms so we default to
92
                // reldbg (https://github.com/matrix-org/matrix-rust-sdk/issues/4009)
93
                let profile =
94
                    profile.as_deref().unwrap_or(if release { "release" } else { "reldbg" });
95
                build_xcframework(
96
                    profile,
97
                    targets,
98
                    tier3_targets,
99
                    components_path,
100
                    sequentially,
101
                    ios_deployment_target.as_deref(),
102
                    watchos_deployment_target.as_deref(),
103
                )
104
            }
105
        }
106
    }
107
}
108
109
/// A specific build target supported by the SDK.
110
struct Target {
111
    triple: &'static str,
112
    platform: Platform,
113
    status: TargetStatus,
114
    description: &'static str,
115
}
116
117
#[derive(Hash, PartialEq, Eq, Clone)]
118
enum TargetStatus {
119
    /// A tier 1 or 2 target that can be built with stable Rust.
120
    TopTier,
121
    /// A tier 3 target that requires nightly Rust and `-Zbuild-std`.
122
    Tier3,
123
}
124
125
/// The platform for which a particular target can run on.
126
#[derive(Hash, PartialEq, Eq, Clone)]
127
enum Platform {
128
    Macos,
129
    Ios,
130
    IosSimulator,
131
    Watchos,
132
    WatchosSimulator,
133
}
134
135
impl Platform {
136
    /// The human-readable name of the platform.
137
    fn as_str(&self) -> &str {
138
        match self {
139
            Platform::Macos => "macOS",
140
            Platform::Ios => "iOS",
141
            Platform::IosSimulator => "iOS Simulator",
142
            Platform::Watchos => "watchOS",
143
            Platform::WatchosSimulator => "watchOS Simulator",
144
        }
145
    }
146
147
    /// The name of the subfolder in which to place the library for the platform
148
    /// once all architectures are lipo'd together.
149
    fn lib_folder_name(&self) -> &str {
150
        match self {
151
            Platform::Macos => "macos",
152
            Platform::Ios => "ios",
153
            Platform::IosSimulator => "ios-simulator",
154
            Platform::Watchos => "watchos",
155
            Platform::WatchosSimulator => "watchos-simulator",
156
        }
157
    }
158
}
159
/// The base name of the FFI library.
160
const FFI_LIBRARY_NAME: &str = "libmatrix_sdk_ffi.a";
161
162
/// The features enabled for the FFI library.
163
const FFI_FEATURES: &str = "sentry";
164
165
/// The list of targets supported by the SDK.
166
const TARGETS: &[Target] = &[
167
    Target {
168
        triple: "aarch64-apple-ios",
169
        platform: Platform::Ios,
170
        status: TargetStatus::TopTier,
171
        description: "iOS",
172
    },
173
    Target {
174
        triple: "aarch64-apple-darwin",
175
        platform: Platform::Macos,
176
        status: TargetStatus::TopTier,
177
        description: "macOS (Apple Silicon)",
178
    },
179
    Target {
180
        triple: "x86_64-apple-darwin",
181
        platform: Platform::Macos,
182
        status: TargetStatus::TopTier,
183
        description: "macOS (Intel)",
184
    },
185
    Target {
186
        triple: "aarch64-apple-ios-sim",
187
        platform: Platform::IosSimulator,
188
        status: TargetStatus::TopTier,
189
        description: "iOS Simulator (Apple Silicon)",
190
    },
191
    Target {
192
        triple: "x86_64-apple-ios",
193
        platform: Platform::IosSimulator,
194
        status: TargetStatus::TopTier,
195
        description: "iOS Simulator (Intel) ",
196
    },
197
    Target {
198
        triple: "aarch64-apple-watchos",
199
        platform: Platform::Watchos,
200
        status: TargetStatus::Tier3,
201
        description: "watchOS (ARM64)",
202
    },
203
    Target {
204
        triple: "arm64_32-apple-watchos",
205
        platform: Platform::Watchos,
206
        status: TargetStatus::Tier3,
207
        description: "watchOS (ARM64_32)",
208
    },
209
    Target {
210
        triple: "aarch64-apple-watchos-sim",
211
        platform: Platform::WatchosSimulator,
212
        status: TargetStatus::Tier3,
213
        description: "watchOS Simulator (ARM64)",
214
    },
215
    Target {
216
        triple: "x86_64-apple-watchos-sim",
217
        platform: Platform::WatchosSimulator,
218
        status: TargetStatus::Tier3,
219
        description: "watchOS Simulator (Intel)",
220
    },
221
];
222
223
fn build_library() -> Result<()> {
224
    println!("Running debug library build.");
225
226
    let root_directory = workspace::root_path()?;
227
    let target_directory = workspace::target_path()?;
228
    let ffi_directory = root_directory.join("bindings/apple/generated/matrix_sdk_ffi");
229
    let lib_output_dir = target_directory.join("debug");
230
231
    create_dir_all(ffi_directory.as_path())?;
232
233
    let sh = sh();
234
    cmd!(sh, "rustup run stable cargo build -p matrix-sdk-ffi --features {FFI_FEATURES}").run()?;
235
236
    rename(lib_output_dir.join(FFI_LIBRARY_NAME), ffi_directory.join(FFI_LIBRARY_NAME))?;
237
    let swift_directory = root_directory.join("bindings/apple/generated/swift");
238
    create_dir_all(swift_directory.as_path())?;
239
240
    generate_uniffi(&ffi_directory.join(FFI_LIBRARY_NAME), &ffi_directory)?;
241
242
    let module_map_file = ffi_directory.join("module.modulemap");
243
    if module_map_file.exists() {
244
        remove_file(module_map_file.as_path())?;
245
    }
246
247
    consolidate_modulemap_files(&ffi_directory, &ffi_directory)?;
248
    move_files("swift", &ffi_directory, &swift_directory)?;
249
    update_swift_module_imports(&swift_directory)?;
250
    Ok(())
251
}
252
253
fn generate_uniffi(library_path: &Utf8Path, ffi_directory: &Utf8Path) -> Result<()> {
254
    generate(GenerateOptions {
255
        languages: vec![TargetLanguage::Swift],
256
        source: library_path.to_path_buf(),
257
        out_dir: ffi_directory.to_path_buf(),
258
        ..GenerateOptions::default()
259
    })?;
260
    Ok(())
261
}
262
263
fn build_xcframework(
264
    profile: &str,
265
    targets: Option<Vec<String>>,
266
    tier3_targets: bool,
267
    components_path: Option<Utf8PathBuf>,
268
    sequentially: bool,
269
    ios_deployment_target: Option<&str>,
270
    watchos_deployment_target: Option<&str>,
271
) -> Result<()> {
272
    let root_dir = workspace::root_path()?;
273
    let apple_dir = root_dir.join("bindings/apple");
274
    let generated_dir = apple_dir.join("generated");
275
276
    // Cleanup destination folder
277
    let _ = remove_dir_all(&generated_dir);
278
    create_dir_all(&generated_dir)?;
279
280
    let headers_dir = generated_dir.join("headers");
281
    // Use a subdirectory to fix conflicts with other UniFFI libraries.
282
    let headers_module_dir = headers_dir.join("MatrixSDKFFI");
283
    let swift_dir = generated_dir.join("swift");
284
    create_dir_all(headers_module_dir.clone())?;
285
    create_dir_all(swift_dir.clone())?;
286
287
    let targets = if let Some(triples) = targets {
288
        triples
289
            .iter()
290
            .map(|t| {
291
                TARGETS.iter().find(|target| target.triple == *t).expect("Invalid target specified")
292
            })
293
            .collect()
294
    } else if tier3_targets {
295
        TARGETS.iter().collect()
296
    } else {
297
        TARGETS.iter().filter(|target| target.status == TargetStatus::TopTier).collect()
298
    };
299
300
    let platform_build_paths = build_targets(
301
        targets,
302
        profile,
303
        sequentially,
304
        ios_deployment_target,
305
        watchos_deployment_target,
306
    )?;
307
    let libs = lipo_platform_libraries(&platform_build_paths, &generated_dir)?;
308
309
    println!("-- Generating uniffi files");
310
    let uniffi_lib_path = platform_build_paths.values().next().unwrap().first().unwrap().clone();
311
    generate_uniffi(&uniffi_lib_path, &generated_dir)?;
312
313
    move_files("h", &generated_dir, &headers_module_dir)?;
314
    consolidate_modulemap_files(&generated_dir, &headers_module_dir)?;
315
316
    move_files("swift", &generated_dir, &swift_dir)?;
317
    update_swift_module_imports(&swift_dir)?;
318
319
    println!("-- Generating MatrixSDKFFI.xcframework framework");
320
    let xcframework_path = generated_dir.join("MatrixSDKFFI.xcframework");
321
    if xcframework_path.exists() {
322
        remove_dir_all(&xcframework_path)?;
323
    }
324
    let sh = sh();
325
    let mut cmd = cmd!(sh, "xcodebuild -create-xcframework");
326
    for p in libs {
327
        cmd = cmd.arg("-library").arg(p).arg("-headers").arg(&headers_dir)
328
    }
329
    cmd.arg("-output").arg(&xcframework_path).run()?;
330
331
    // Copy the Swift package manifest to the SDK root for local development.
332
    copy(apple_dir.join("Debug-Package.swift"), root_dir.join("Package.swift"))?;
333
334
    // Copy an empty package to folders we want ignored
335
    let ignored_package_folders = ["target"];
336
    for path in ignored_package_folders {
337
        copy(
338
            apple_dir.join("Debug-Empty-Package.swift"),
339
            root_dir.join(path).join("Package.swift"),
340
        )?;
341
    }
342
343
    // cleaning up the intermediate data
344
    remove_dir_all(headers_dir.as_path())?;
345
346
    if let Some(path) = components_path {
347
        println!("-- Copying MatrixSDKFFI.xcframework to {path}");
348
        let framework_target = path.join("MatrixSDKFFI.xcframework");
349
        let swift_target = path.join("Sources/MatrixRustSDK");
350
        if framework_target.exists() {
351
            remove_dir_all(&framework_target)?;
352
        }
353
        if swift_target.exists() {
354
            remove_dir_all(&swift_target)?;
355
        }
356
        create_dir_all(&framework_target)?;
357
        create_dir_all(&swift_target)?;
358
359
        let copy_options = fs_extra::dir::CopyOptions { content_only: true, ..Default::default() };
360
361
        fs_extra::dir::copy(&xcframework_path, &framework_target, &copy_options)?;
362
        fs_extra::dir::copy(&swift_dir, &swift_target, &copy_options)?;
363
    }
364
365
    println!("-- All done and hunky dory. Enjoy!");
366
367
    Ok(())
368
}
369
370
/// Builds the SDK for the specified targets and profile.
371
fn build_targets(
372
    targets: Vec<&Target>,
373
    profile: &str,
374
    sequentially: bool,
375
    ios_deployment_target: Option<&str>,
376
    watchos_deployment_target: Option<&str>,
377
) -> Result<HashMap<Platform, Vec<Utf8PathBuf>>> {
378
    let sh = sh();
379
380
    // Note: `push_env` stores environment variables and returns a RAII guard that
381
    // will restore the environment variable to its previous value when dropped.
382
    let _env_guard1 =
383
        sh.push_env("CARGO_TARGET_AARCH64_APPLE_IOS_RUSTFLAGS", "-Clinker=/usr/bin/clang");
384
    let _env_guard2 = sh.push_env("AARCH64_APPLE_IOS_CC", "/usr/bin/clang");
385
    let _env_guard3 =
386
        ios_deployment_target.map(|target| sh.push_env("IPHONEOS_DEPLOYMENT_TARGET", target));
387
    let _env_guard4 =
388
        watchos_deployment_target.map(|target| sh.push_env("WATCHOS_DEPLOYMENT_TARGET", target));
389
390
    if sequentially {
391
        for target in &targets {
392
            let triple = target.triple;
393
394
            println!("-- Building for {}", target.description);
395
            if target.status == TargetStatus::TopTier {
396
                cmd!(sh, "rustup run stable cargo build -p matrix-sdk-ffi --target {triple} --profile {profile} --features {FFI_FEATURES}")
397
                    .run()?;
398
            } else {
399
                cmd!(sh, "rustup run nightly cargo build -p matrix-sdk-ffi -Zbuild-std --target {triple} --profile {profile} --features {FFI_FEATURES}")
400
                    .run()?;
401
            }
402
        }
403
    } else {
404
        let (stable_targets, tier3_targets): (Vec<&Target>, Vec<&Target>) =
405
            targets.iter().partition(|t| t.status == TargetStatus::TopTier);
406
407
        if !stable_targets.is_empty() {
408
            let triples = stable_targets.iter().map(|target| target.triple).collect::<Vec<_>>();
409
            let mut cmd = cmd!(sh, "rustup run stable cargo build -p matrix-sdk-ffi");
410
            for triple in &triples {
411
                cmd = cmd.arg("--target").arg(triple);
412
            }
413
            cmd = cmd.arg("--profile").arg(profile).arg("--features").arg(FFI_FEATURES);
414
415
            println!("-- Building for {} targets", triples.len());
416
            cmd.run()?;
417
        }
418
419
        if !tier3_targets.is_empty() {
420
            let triples = tier3_targets.iter().map(|target| target.triple).collect::<Vec<_>>();
421
            let mut cmd = cmd!(sh, "rustup run nightly cargo build -p matrix-sdk-ffi -Zbuild-std");
422
            for triple in &triples {
423
                cmd = cmd.arg("--target").arg(triple);
424
            }
425
            cmd = cmd.arg("--profile").arg(profile).arg("--features").arg(FFI_FEATURES);
426
427
            println!("-- Building for {} targets with nightly -Zbuild-std", triples.len());
428
            cmd.run()?;
429
        }
430
    }
431
432
    // a hashmap of platform to array, where each array contains all the paths for
433
    // that platform.
434
    let mut platform_build_paths = HashMap::new();
435
    for target in targets {
436
        let path = build_path_for_target(target, profile)?;
437
        let paths = platform_build_paths.entry(target.platform.clone()).or_insert_with(Vec::new);
438
        paths.push(path);
439
    }
440
441
    Ok(platform_build_paths)
442
}
443
444
/// The path of the built library for a specific target and profile.
445
fn build_path_for_target(target: &Target, profile: &str) -> Result<Utf8PathBuf> {
446
    // The builtin dev profile has its files stored under target/debug, all
447
    // other targets have matching directory names
448
    let profile_dir_name = if profile == "dev" { "debug" } else { profile };
449
    Ok(workspace::target_path()?.join(target.triple).join(profile_dir_name).join(FFI_LIBRARY_NAME))
450
}
451
452
/// Lipo's together the libraries for each platform into a single library.
453
fn lipo_platform_libraries(
454
    platform_build_paths: &HashMap<Platform, Vec<Utf8PathBuf>>,
455
    generated_dir: &Utf8Path,
456
) -> Result<Vec<Utf8PathBuf>> {
457
    let mut libs = Vec::new();
458
    let sh = sh();
459
    for platform in platform_build_paths.keys() {
460
        let paths = platform_build_paths.get(platform).unwrap();
461
462
        if paths.len() == 1 {
463
            libs.push(paths[0].clone());
464
            continue;
465
        }
466
467
        let output_folder = generated_dir.join("lipo").join(platform.lib_folder_name());
468
        create_dir_all(&output_folder)?;
469
470
        let output_path = output_folder.join(FFI_LIBRARY_NAME);
471
        let mut cmd = cmd!(sh, "lipo -create");
472
        for path in paths {
473
            cmd = cmd.arg(path);
474
        }
475
        cmd = cmd.arg("-output").arg(&output_path);
476
477
        println!("-- Running Lipo for {}", platform.as_str());
478
        cmd.run()?;
479
480
        libs.push(output_path);
481
    }
482
    Ok(libs)
483
}
484
485
/// Moves all files of the specified file extension from one directory into
486
/// another.
487
fn move_files(extension: &str, source: &Utf8Path, destination: &Utf8Path) -> Result<()> {
488
    for entry in source.read_dir_utf8()? {
489
        let entry = entry?;
490
491
        if entry.file_type()?.is_file() {
492
            let path = entry.path();
493
            if path.extension() == Some(extension) {
494
                let file_name = path.file_name().expect("Failed to get file name");
495
                rename(path, destination.join(file_name)).expect("Failed to move swift file");
496
            }
497
        }
498
    }
499
    Ok(())
500
}
501
502
/// Updates all the swift files in the given directory to import the same module
503
/// that gets defined by the `consolidate_modulemap_files` function.
504
fn update_swift_module_imports(directory: &Utf8Path) -> Result<()> {
505
    let regex = regex::Regex::new(r"#if canImport\(\w+FFI\)\nimport \w+FFI\n#endif")?;
506
507
    for entry in directory.read_dir_utf8()? {
508
        let entry = entry?;
509
        if entry.file_type()?.is_file() {
510
            let path = entry.path();
511
            if path.extension() == Some("swift") {
512
                let contents = std::fs::read_to_string(path)?;
513
                let new_contents = regex.replace_all(
514
                    &contents,
515
                    "#if canImport(MatrixSDKFFI)\nimport MatrixSDKFFI\n#endif",
516
                );
517
                if new_contents != contents {
518
                    std::fs::write(path, new_contents.as_ref())?;
519
                }
520
            }
521
        }
522
    }
523
524
    Ok(())
525
}
526
527
/// Consolidates the modulemap files found in the source directory into a single
528
/// `module.modulemap` file in the destination directory.
529
///
530
/// The first modulemap file found is used as the base and all `header`
531
/// directives from the remaining modulemap files are spliced in after the
532
/// base's own header line. All other content in the base (e.g. `export *`,
533
/// `use` directives, and any future additions) is preserved verbatim.
534
fn consolidate_modulemap_files(source: &Utf8Path, destination: &Utf8Path) -> Result<()> {
535
    let mut base_contents: Option<String> = None;
536
    let mut extra_headers: Vec<String> = Vec::new();
537
538
    for entry in source.read_dir_utf8()? {
539
        let entry = entry?;
540
541
        if entry.file_type()?.is_file() {
542
            let path = entry.path();
543
            if path.extension() == Some("modulemap") {
544
                let contents = std::fs::read_to_string(path)?;
545
                if base_contents.is_none() {
546
                    base_contents = Some(contents);
547
                } else {
548
                    for line in contents.lines() {
549
                        if line.trim().starts_with("header ")
550
                            && !extra_headers.contains(&line.to_string())
551
                        {
552
                            extra_headers.push(line.to_string());
553
                        }
554
                    }
555
                }
556
                remove_file(path)?;
557
            }
558
        }
559
    }
560
561
    let base = base_contents.expect("No modulemap files found");
562
563
    // Rebuild the base line-by-line, renaming the module to MatrixSDKFFI and
564
    // inserting the extra headers immediately after the base's own header line.
565
    let mut lines: Vec<String> = Vec::new();
566
    let mut last_header_position: Option<usize> = None;
567
568
    for line in base.lines() {
569
        if line.starts_with("module ") {
570
            lines.push("module MatrixSDKFFI {".to_string());
571
        } else {
572
            if line.trim().starts_with("header ") {
573
                last_header_position = Some(lines.len());
574
            }
575
            lines.push(line.to_string());
576
        }
577
    }
578
579
    if let Some(last_header_position) = last_header_position {
580
        lines.splice(last_header_position + 1..last_header_position + 1, extra_headers);
581
    }
582
583
    let modulemap = lines.join("\n") + "\n";
584
    std::fs::write(destination.join("module.modulemap"), modulemap)?;
585
    Ok(())
586
}