nimbus_fml/backends/swift/
mod.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2* License, v. 2.0. If a copy of the MPL was not distributed with this
3* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5use crate::error::{FMLError, Result};
6use crate::frontend::AboutBlock;
7use askama::Template;
8
9use crate::command_line::commands::GenerateStructCmd;
10use crate::intermediate_representation::FeatureManifest;
11
12mod gen_structs;
13
14impl AboutBlock {
15    fn nimbus_object_name_swift(&self) -> String {
16        let swift_about = self.swift_about.as_ref().unwrap();
17        swift_about.class.clone()
18    }
19
20    fn nimbus_module_name(&self) -> String {
21        let swift_about = self.swift_about.as_ref().unwrap();
22        swift_about.module.clone()
23    }
24}
25
26pub(crate) fn generate_struct(manifest: &FeatureManifest, cmd: &GenerateStructCmd) -> Result<()> {
27    if manifest.about.swift_about.is_none() {
28        return Err(FMLError::ValidationError(
29            "about".to_string(),
30            format!(
31                "The `about` block is missing a valid `ios` or `swift` entry: {}",
32                &cmd.manifest
33            ),
34        ));
35    }
36
37    let path = &cmd.output;
38    let path = if path.is_dir() {
39        path.join(format!(
40            "{}.swift",
41            manifest.about.nimbus_object_name_swift()
42        ))
43    } else {
44        path.clone()
45    };
46
47    let fm = gen_structs::FeatureManifestDeclaration::new(manifest);
48
49    let contents = fm.render()?;
50
51    std::fs::write(path, contents)?;
52
53    Ok(())
54}
55
56#[cfg(all(
57    test,
58    feature = "swift-tests",
59    not(feature = "all-features-workaround")
60))]
61pub mod test {
62    use crate::util::{join, pkg_dir, sdk_dir};
63    use anyhow::{bail, Context, Result};
64    use std::{
65        ffi::OsString,
66        path::{Path, PathBuf},
67        process::Command,
68    };
69
70    // The root of the Android kotlin package structure
71    fn sdk_ios_dir() -> String {
72        join(sdk_dir(), "ios/Nimbus")
73    }
74
75    fn mock_nimbus_error_swift() -> String {
76        join(pkg_dir(), "fixtures/ios/runtime/NimbusError.swift")
77    }
78
79    fn mock_uiimage_swift() -> String {
80        join(pkg_dir(), "fixtures/ios/runtime/UIImage.swift")
81    }
82
83    // The file with the swift implementation of FeatureVariables
84    fn variables_swift() -> String {
85        join(sdk_ios_dir(), "FeatureVariables.swift")
86    }
87
88    // The file with the swift implementation of FeatureVariables
89    fn features_swift() -> String {
90        join(sdk_ios_dir(), "FeatureInterface.swift")
91    }
92
93    // The file with the swift implementation of FeatureVariables
94    fn collections_swift() -> String {
95        join(sdk_ios_dir(), "Collections+.swift")
96    }
97
98    // The file with the swift implementation of FeatureVariables
99    fn dictionaries_swift() -> String {
100        join(sdk_ios_dir(), "Dictionary+.swift")
101    }
102
103    // The file with the swift implementation of Bundle extensions
104    fn bundle_swift() -> String {
105        join(sdk_ios_dir(), "Bundle+.swift")
106    }
107
108    // The file with the swift implementation of FeatureHolder
109    fn feature_holder() -> String {
110        join(sdk_ios_dir(), "FeatureHolder.swift")
111    }
112
113    fn hardcoded_nimbus_features() -> String {
114        join(sdk_ios_dir(), "HardcodedNimbusFeatures.swift")
115    }
116
117    // The file with the swift implementation of Feature Manifest protocol file
118    fn generated_feature_manifest() -> String {
119        join(sdk_ios_dir(), "FeatureManifestInterface.swift")
120    }
121
122    fn detect_swiftc() -> Result<bool> {
123        let output = Command::new("which").arg("swiftc").output()?;
124
125        Ok(output.status.success())
126    }
127
128    pub fn compile_manifest_swift(manifest_files: &[String], out_dir: &Path) -> Result<()> {
129        let out_path = PathBuf::from(out_dir);
130        let manifest_files = manifest_files.iter().map(PathBuf::from);
131        let mut dylib_file = out_path.clone();
132        dylib_file.push(format!("lib{}.dylib", "FeatureManifest"));
133
134        // `-emit-library -o <path>` generates a `.dylib`, so that we can use the
135        // Swift module from the REPL. Otherwise, we'll get "Couldn't lookup
136        // symbols" when we try to import the module.
137        // See https://bugs.swift.org/browse/SR-1191.
138
139        let status = Command::new("swiftc")
140            .arg("-module-name")
141            .arg("FeatureManifest")
142            .arg("-emit-library")
143            .arg("-o")
144            .arg(&dylib_file)
145            .arg("-emit-module")
146            .arg("-emit-module-path")
147            .arg(&out_path)
148            .arg("-parse-as-library")
149            .arg("-L")
150            .arg(&out_path)
151            .arg(collections_swift())
152            .arg(dictionaries_swift())
153            .arg(mock_uiimage_swift())
154            .arg(variables_swift())
155            .arg(features_swift())
156            .arg(feature_holder())
157            .arg(hardcoded_nimbus_features())
158            .arg(bundle_swift())
159            .arg(generated_feature_manifest())
160            .arg(mock_nimbus_error_swift())
161            .args(manifest_files)
162            .spawn()
163            .context("Failed to spawn `swiftc` when compiling bindings")?
164            .wait()
165            .context("Failed to wait for `swiftc` when compiling bindings")?;
166        if !status.success() {
167            bail!("running `swiftc` failed")
168        }
169        Ok(())
170    }
171
172    pub fn run_script(out_dir: &Path, script_file: &Path) -> Result<()> {
173        let mut cmd = Command::new("swift");
174
175        // Find any module maps and/or dylibs in the target directory, and tell swift to use them.
176        // Listing the directory like this is a little bit hacky - it would be nicer if we could tell
177        // Swift to load only the module(s) for the component under test, but the way we're calling
178        // this test function doesn't allow us to pass that name in to the call.
179
180        cmd.arg("-I").arg(out_dir).arg("-L").arg(out_dir);
181        for entry in PathBuf::from(out_dir)
182            .read_dir()
183            .context("Failed to list target directory when running script")?
184        {
185            let entry = entry.context("Failed to list target directory when running script")?;
186            if let Some(ext) = entry.path().extension() {
187                if ext == "dylib" || ext == "so" {
188                    let mut option = OsString::from("-l");
189                    option.push(entry.path());
190                    cmd.arg(option);
191                }
192            }
193        }
194        cmd.arg(script_file);
195
196        let status = cmd
197            .spawn()
198            .context("Failed to spawn `swift` when running script")?
199            .wait()
200            .context("Failed to wait for `swift` when running script")?;
201        if !status.success() {
202            bail!("running `swift` failed")
203        }
204        Ok(())
205    }
206
207    pub fn run_script_with_generated_code(manifest_files: &[String], script: &Path) -> Result<()> {
208        if !detect_swiftc()? {
209            eprintln!("SDK-446 Install swift or add it the PATH to run tests");
210            return Ok(());
211        }
212        let temp = tempfile::tempdir()?;
213        let build_dir = temp.path();
214        compile_manifest_swift(manifest_files, build_dir)?;
215        run_script(build_dir, script)
216    }
217}