Skip to main content

fission_cli/
lib.rs

1use anyhow::Result;
2use clap::Parser;
3use std::path::Path;
4
5mod cli;
6
7#[cfg(test)]
8use fission_command_core::{read_project_config, Target};
9
10use cli::{Cli, Command, SiteCommand};
11
12pub fn run<I, S>(args: I) -> Result<()>
13where
14    I: IntoIterator<Item = S>,
15    S: Into<std::ffi::OsString> + Clone,
16{
17    let mut argv: Vec<std::ffi::OsString> = args.into_iter().map(Into::into).collect();
18    if let Some(bin) = argv.first() {
19        if let Some(name) = Path::new(bin).file_name().and_then(|value| value.to_str()) {
20            if name == "cargo-fission" {
21                argv[0] = std::ffi::OsString::from("fission");
22                if argv.get(1).and_then(|value| value.to_str()) == Some("fission") {
23                    argv.remove(1);
24                }
25            }
26        }
27    }
28    let cli = Cli::parse_from(argv);
29    match cli.command {
30        Command::Init {
31            path,
32            name,
33            app_id,
34            local_path,
35        } => fission_command_core::init_project(&path, name, app_id, local_path),
36        Command::AddTarget {
37            targets,
38            project_dir,
39        } => fission_command_core::add_targets(&project_dir, &targets),
40        Command::AddCapability {
41            capabilities,
42            project_dir,
43        } => fission_command_core::add_capabilities(&project_dir, &capabilities),
44        Command::Doctor {
45            targets,
46            project_dir,
47            strict,
48        } => fission_command_run::doctor::run_doctor(&project_dir, &targets, strict),
49        Command::Devices { project_dir, json } => {
50            fission_command_run::list_devices(&project_dir, json)
51        }
52        Command::Run {
53            target,
54            device,
55            project_dir,
56            detach,
57            release,
58            host,
59            port,
60            no_open,
61            headless,
62        } => fission_command_run::run_app(fission_command_run::RunOptions {
63            project_dir,
64            target,
65            device,
66            detach,
67            release,
68            host,
69            port,
70            no_open,
71            headless,
72        }),
73        Command::Build {
74            target,
75            project_dir,
76            release,
77        } => fission_command_run::build_app(fission_command_run::BuildOptions {
78            project_dir,
79            target,
80            release,
81        }),
82        Command::Test {
83            target,
84            project_dir,
85            headless,
86        } => fission_command_run::test_app(fission_command_run::TestOptions {
87            project_dir,
88            target,
89            headless,
90        }),
91        Command::Site { command } => match command {
92            SiteCommand::Build {
93                project_dir,
94                release,
95            } => fission_command_site::build(&project_dir, release),
96            SiteCommand::Check {
97                project_dir,
98                release,
99            } => fission_command_site::check(&project_dir, release),
100            SiteCommand::Serve {
101                project_dir,
102                host,
103                port,
104                release,
105                no_open,
106            } => fission_command_site::serve(&project_dir, release, host, port, !no_open),
107            SiteCommand::Routes { project_dir } => fission_command_site::routes(&project_dir),
108        },
109        Command::Package {
110            target,
111            format,
112            project_dir,
113            release,
114            json,
115        } => fission_command_package::package(fission_command_package::PackageOptions {
116            project_dir,
117            target,
118            format,
119            release,
120            json,
121        }),
122        Command::Distribute {
123            action,
124            provider,
125            artifact,
126            site,
127            deploy,
128            track,
129            dry_run,
130            yes,
131            project_dir,
132            json,
133        } => fission_command_package::distribute(fission_command_package::DistributeOptions {
134            project_dir,
135            provider,
136            action: action.unwrap_or(fission_command_package::DistributeAction::Publish),
137            artifact,
138            site,
139            deploy,
140            track,
141            dry_run,
142            yes,
143            json,
144        }),
145        Command::Readiness {
146            kind,
147            target,
148            format,
149            provider,
150            artifact,
151            site,
152            track,
153            project_dir,
154            json,
155        } => fission_command_package::readiness(fission_command_package::ReadinessOptions {
156            project_dir,
157            kind,
158            target,
159            format,
160            provider,
161            artifact,
162            site,
163            track,
164            json,
165        }),
166        Command::ReleaseConfig { command } => fission_command_release::release_config(command),
167        Command::ReleaseContent { command } => fission_command_release::release_content(command),
168        Command::Beta { command } => fission_command_release::beta(command),
169        Command::Signing { command } => fission_command_release::signing(command),
170        Command::Reviews { command } => fission_command_release::reviews(command),
171        Command::ReleaseWorkflow { command } => fission_command_release::release_workflow(command),
172        Command::Auth { command } => fission_command_release::auth(command),
173        Command::Logs {
174            target,
175            device,
176            project_dir,
177            follow,
178        } => fission_command_run::attach_logs(fission_command_run::LogOptions {
179            project_dir,
180            target,
181            device,
182            follow,
183        }),
184        Command::Ui {
185            project_dir,
186            screenshot,
187            exit_after_render,
188            width,
189            height,
190        } => fission_command_ui::run_ui(fission_command_ui::UiOptions {
191            project_dir,
192            screenshot,
193            exit_after_render,
194            width,
195            height,
196        }),
197        Command::ServeWeb {
198            project_dir,
199            host,
200            port,
201            open,
202        } => fission_command_run::serve_web(fission_command_run::ServeWebOptions {
203            project_dir,
204            host,
205            port,
206            open,
207        }),
208    }
209}
210
211#[cfg(test)]
212mod tests {
213    use super::*;
214    use std::{fs, path::PathBuf};
215
216    fn unique_dir(name: &str) -> PathBuf {
217        let dir =
218            std::env::temp_dir().join(format!("cargo-fission-{}-{}", name, std::process::id()));
219        let _ = fs::remove_dir_all(&dir);
220        dir
221    }
222
223    #[test]
224    fn init_creates_project_files() {
225        let dir = unique_dir("init");
226        run([
227            "fission",
228            "init",
229            dir.to_str().unwrap(),
230            "--name",
231            "hello-fission",
232        ])
233        .unwrap();
234
235        assert!(dir.join("Cargo.toml").exists());
236        assert!(dir.join("src/main.rs").exists());
237        assert!(dir.join("src/lib.rs").exists());
238        assert!(dir.join("src/app.rs").exists());
239        assert!(dir.join("assets/app-icon.png").exists());
240        assert!(dir.join("fission.toml").exists());
241        assert!(dir.join("platforms/windows/README.md").exists());
242        assert!(dir.join("platforms/macos/README.md").exists());
243        assert!(dir.join("platforms/linux/README.md").exists());
244        let readme = std::fs::read_to_string(dir.join("README.md")).unwrap();
245        assert!(readme.contains("fission devices --project-dir ."));
246        assert!(readme.contains("fission run --project-dir ."));
247        assert!(readme.contains("fission logs --target <target>"));
248        assert!(readme.contains("fission build --target <target>"));
249        assert!(readme.contains("fission test --target <target>"));
250        let manifest = std::fs::read_to_string(dir.join("Cargo.toml")).unwrap();
251        assert!(manifest.contains("default-features = false"));
252        assert!(manifest.contains("features = [\"desktop\"]"));
253    }
254
255    #[test]
256    fn add_target_updates_manifest_and_scaffold() {
257        let dir = unique_dir("targets");
258        run(["fission", "init", dir.to_str().unwrap()]).unwrap();
259        run([
260            "fission",
261            "add-target",
262            "web",
263            "ios",
264            "android",
265            "--project-dir",
266            dir.to_str().unwrap(),
267        ])
268        .unwrap();
269
270        let project = read_project_config(&dir).unwrap();
271        assert!(project.targets.contains(&Target::Web));
272        assert!(project.targets.contains(&Target::Ios));
273        assert!(project.targets.contains(&Target::Android));
274        let manifest = std::fs::read_to_string(dir.join("Cargo.toml")).unwrap();
275        assert!(manifest.contains("default-features = false"));
276        assert!(manifest.contains("features = [\"desktop\", \"web\", \"android\", \"ios\"]"));
277        assert!(dir.join("platforms/web/README.md").exists());
278        assert!(dir.join("platforms/web/index.html").exists());
279        assert!(dir.join("platforms/web/bootstrap.mjs").exists());
280        assert!(dir.join("platforms/web/build-wasm.sh").exists());
281        assert!(dir.join("platforms/web/run-browser.sh").exists());
282        assert!(dir.join("platforms/web/test-browser.sh").exists());
283        assert!(dir.join("platforms/ios/README.md").exists());
284        assert!(dir.join("platforms/ios/Info.plist").exists());
285        assert!(dir.join("platforms/ios/package-sim.sh").exists());
286        assert!(dir.join("platforms/ios/run-sim.sh").exists());
287        assert!(dir.join("platforms/ios/test-sim.sh").exists());
288        assert!(dir.join("platforms/android/README.md").exists());
289        assert!(dir.join("platforms/android/AndroidManifest.xml").exists());
290        assert!(dir.join("platforms/android/package-apk.sh").exists());
291        assert!(dir.join("platforms/android/run-emulator.sh").exists());
292        assert!(dir.join("platforms/android/test-emulator.sh").exists());
293        let android_manifest =
294            std::fs::read_to_string(dir.join("platforms/android/AndroidManifest.xml")).unwrap();
295        assert!(android_manifest.contains("android:icon=\"@drawable/app_icon\""));
296        assert!(android_manifest.contains("android:targetSdkVersion=\"35\""));
297        let android_package_script =
298            std::fs::read_to_string(dir.join("platforms/android/package-apk.sh")).unwrap();
299        assert!(android_package_script.contains("detect_android_toolchain"));
300        assert!(android_package_script
301            .contains("darwin-aarch64 darwin-x86_64 linux-x86_64 windows-x86_64"));
302        assert!(android_package_script.contains(
303            "ANDROID_MIN_API_LEVEL=\"${ANDROID_MIN_API_LEVEL:-${ANDROID_API_LEVEL:-24}}\""
304        ));
305        assert!(android_package_script.contains("ANDROID_TARGET_API_LEVEL="));
306        assert!(
307            android_package_script.contains("aarch64-linux-android${ANDROID_MIN_API_LEVEL}-clang")
308        );
309        assert!(android_package_script.contains("BUILD_MANIFEST"));
310        assert!(android_package_script.contains("android:targetSdkVersion=\"{target_api}\""));
311        let android_run_script =
312            std::fs::read_to_string(dir.join("platforms/android/run-emulator.sh")).unwrap();
313        assert!(android_run_script.contains("ANDROID_EMULATOR_API_LEVEL"));
314        assert!(android_run_script.contains("fission doctor android"));
315        assert!(
316            std::fs::read_to_string(dir.join("platforms/android/README.md"))
317                .unwrap()
318                .contains("fission run --target android")
319        );
320        let android_test_script =
321            std::fs::read_to_string(dir.join("platforms/android/test-emulator.sh")).unwrap();
322        assert!(android_test_script.contains("/health"));
323        let ios_package_script =
324            std::fs::read_to_string(dir.join("platforms/ios/package-sim.sh")).unwrap();
325        assert!(ios_package_script.contains("TARGET=\"${IOS_SIM_TARGET:-aarch64-apple-ios-sim}\""));
326        assert!(ios_package_script.contains("PROFILE=\"${IOS_SIM_PROFILE:-debug}\""));
327        assert!(ios_package_script.contains("BUNDLE_ID=\"${IOS_BUNDLE_ID:-com.example."));
328        assert!(ios_package_script.contains("DISPLAY_NAME=\"${IOS_DISPLAY_NAME:-"));
329        assert!(ios_package_script.contains("EXECUTABLE_NAME=\"${IOS_EXECUTABLE_NAME:-"));
330        assert!(ios_package_script.contains("plistlib.load"));
331        assert!(ios_package_script.contains("PkgInfo"));
332        assert!(ios_package_script.contains("AppIcon.png"));
333        let ios_run_script = std::fs::read_to_string(dir.join("platforms/ios/run-sim.sh")).unwrap();
334        assert!(ios_run_script.contains("BUNDLE_ID=\"${IOS_BUNDLE_ID:-com.example."));
335        assert!(ios_run_script.contains(
336            "xcrun simctl launch --terminate-running-process \"$DEVICE_ID\" \"$BUNDLE_ID\""
337        ));
338        assert!(std::fs::read_to_string(dir.join("platforms/ios/README.md"))
339            .unwrap()
340            .contains("fission run --target ios"));
341        assert!(
342            std::fs::read_to_string(dir.join("platforms/ios/test-sim.sh"))
343                .unwrap()
344                .contains("/health")
345        );
346        assert!(
347            std::fs::read_to_string(dir.join("platforms/web/index.html"))
348                .unwrap()
349                .contains("../../assets/app-icon.png")
350        );
351        let web_index = std::fs::read_to_string(dir.join("platforms/web/index.html")).unwrap();
352        assert!(web_index.contains("id=\"fission-web-mount\""));
353        assert!(web_index.contains("height: 100vh"));
354        assert!(web_index.contains("outline: none"));
355        assert!(web_index.contains("touch-action: none"));
356        assert!(!web_index.contains("Generated by"));
357        let web_test_script =
358            std::fs::read_to_string(dir.join("platforms/web/test-browser.sh")).unwrap();
359        assert!(web_test_script.contains("--remote-debugging-port=\"$CDP_PORT\""));
360        assert!(web_test_script.contains("/json/list"));
361        assert!(std::fs::read_to_string(dir.join("platforms/web/README.md"))
362            .unwrap()
363            .contains("fission run --target web"));
364    }
365
366    #[test]
367    fn add_capability_updates_project_and_platform_config() {
368        let dir = unique_dir("capability");
369        run(["fission", "init", dir.to_str().unwrap()]).unwrap();
370        run([
371            "fission",
372            "add-target",
373            "ios",
374            "android",
375            "--project-dir",
376            dir.to_str().unwrap(),
377        ])
378        .unwrap();
379        run([
380            "fission",
381            "add-capability",
382            "nfc",
383            "biometric",
384            "bluetooth",
385            "barcode-scanner",
386            "camera",
387            "geolocation",
388            "haptics",
389            "microphone",
390            "volume-control",
391            "wifi",
392            "--project-dir",
393            dir.to_str().unwrap(),
394        ])
395        .unwrap();
396
397        let project = read_project_config(&dir).unwrap();
398        assert!(project
399            .capabilities
400            .contains(&fission_command_core::PlatformCapability::Nfc));
401        assert!(project
402            .capabilities
403            .contains(&fission_command_core::PlatformCapability::Biometric));
404        assert!(project
405            .capabilities
406            .contains(&fission_command_core::PlatformCapability::Bluetooth));
407        assert!(project
408            .capabilities
409            .contains(&fission_command_core::PlatformCapability::BarcodeScanner));
410        assert!(project
411            .capabilities
412            .contains(&fission_command_core::PlatformCapability::Camera));
413        assert!(project
414            .capabilities
415            .contains(&fission_command_core::PlatformCapability::Geolocation));
416        assert!(project
417            .capabilities
418            .contains(&fission_command_core::PlatformCapability::Haptics));
419        assert!(project
420            .capabilities
421            .contains(&fission_command_core::PlatformCapability::Microphone));
422        assert!(project
423            .capabilities
424            .contains(&fission_command_core::PlatformCapability::VolumeControl));
425        assert!(project
426            .capabilities
427            .contains(&fission_command_core::PlatformCapability::Wifi));
428
429        let android_manifest =
430            std::fs::read_to_string(dir.join("platforms/android/AndroidManifest.xml")).unwrap();
431        assert!(android_manifest.contains("android.permission.NFC"));
432        assert!(android_manifest.contains("android.hardware.nfc"));
433        assert!(android_manifest.contains("android.permission.USE_BIOMETRIC"));
434        assert!(android_manifest.contains("android.permission.BLUETOOTH_SCAN"));
435        assert!(android_manifest.contains("android.permission.BLUETOOTH_CONNECT"));
436        assert!(android_manifest.contains("android.hardware.bluetooth_le"));
437        assert!(android_manifest.contains("android.permission.CAMERA"));
438        assert!(android_manifest.contains("android.hardware.camera.flash"));
439        assert!(android_manifest.contains("android.permission.ACCESS_FINE_LOCATION"));
440        assert!(android_manifest.contains("android.permission.VIBRATE"));
441        assert!(android_manifest.contains("android.permission.RECORD_AUDIO"));
442        assert!(android_manifest.contains("android.permission.MODIFY_AUDIO_SETTINGS"));
443        assert!(android_manifest.contains("android.permission.NEARBY_WIFI_DEVICES"));
444        assert!(android_manifest.contains("android.permission.ACCESS_WIFI_STATE"));
445
446        let ios_info = std::fs::read_to_string(dir.join("platforms/ios/Info.plist")).unwrap();
447        assert!(ios_info.contains("NFCReaderUsageDescription"));
448        assert!(ios_info.contains("NSFaceIDUsageDescription"));
449        assert!(ios_info.contains("NSBluetoothAlwaysUsageDescription"));
450        assert!(ios_info.contains("NSCameraUsageDescription"));
451        assert!(ios_info.contains("NSLocationWhenInUseUsageDescription"));
452        assert!(ios_info.contains("NSMicrophoneUsageDescription"));
453        let ios_entitlements =
454            std::fs::read_to_string(dir.join("platforms/ios/Entitlements.plist")).unwrap();
455        assert!(ios_entitlements.contains("com.apple.developer.nfc.readersession.formats"));
456        assert!(ios_entitlements.contains("com.apple.developer.networking.wifi-info"));
457    }
458
459    #[test]
460    fn init_existing_project_preserves_user_files_and_detects_targets() {
461        let dir = unique_dir("existing");
462        fs::create_dir_all(dir.join("src")).unwrap();
463        fs::create_dir_all(dir.join("platforms/web")).unwrap();
464        fs::write(
465            dir.join("Cargo.toml"),
466            "[package]\nname = \"existing-web\"\nversion = \"0.1.0\"\nedition = \"2021\"\n",
467        )
468        .unwrap();
469        fs::write(dir.join("src/main.rs"), "fn main() {}\n").unwrap();
470        fs::write(dir.join("src/lib.rs"), "pub fn existing() {}\n").unwrap();
471        fs::write(dir.join("README.md"), "# keep me\n").unwrap();
472        fs::write(
473            dir.join("platforms/web/index.html"),
474            "<!doctype html><title>keep me</title>\n",
475        )
476        .unwrap();
477
478        run(["fission", "init", dir.to_str().unwrap()]).unwrap();
479
480        assert_eq!(
481            fs::read_to_string(dir.join("README.md")).unwrap(),
482            "# keep me\n"
483        );
484        assert_eq!(
485            fs::read_to_string(dir.join("src/main.rs")).unwrap(),
486            "fn main() {}\n"
487        );
488        assert_eq!(
489            fs::read_to_string(dir.join("src/lib.rs")).unwrap(),
490            "pub fn existing() {}\n"
491        );
492        assert_eq!(
493            fs::read_to_string(dir.join("platforms/web/index.html")).unwrap(),
494            "<!doctype html><title>keep me</title>\n"
495        );
496
497        let project = read_project_config(&dir).unwrap();
498        assert_eq!(project.app.name, "existing-web");
499        assert!(project.targets.contains(&Target::Web));
500        assert!(project.targets.contains(&Target::Macos));
501        assert!(project.targets.contains(&Target::Linux));
502        assert!(project.targets.contains(&Target::Windows));
503        assert!(dir.join("platforms/web/README.md").exists());
504        assert!(dir.join("platforms/web/bootstrap.mjs").exists());
505        assert!(dir.join("assets/app-icon.png").exists());
506    }
507
508    #[test]
509    fn init_existing_project_is_idempotent() {
510        let dir = unique_dir("idempotent");
511        run(["fission", "init", dir.to_str().unwrap(), "--name", "idem"]).unwrap();
512        let manifest = fs::read_to_string(dir.join("fission.toml")).unwrap();
513        let main = fs::read_to_string(dir.join("src/main.rs")).unwrap();
514
515        run(["fission", "init", dir.to_str().unwrap()]).unwrap();
516
517        assert_eq!(
518            fs::read_to_string(dir.join("fission.toml")).unwrap(),
519            manifest
520        );
521        assert_eq!(fs::read_to_string(dir.join("src/main.rs")).unwrap(), main);
522    }
523
524    #[test]
525    fn add_target_preserves_existing_target_files() {
526        let dir = unique_dir("preserve-target");
527        run([
528            "fission",
529            "init",
530            dir.to_str().unwrap(),
531            "--name",
532            "preserve-target",
533        ])
534        .unwrap();
535        fs::create_dir_all(dir.join("platforms/web")).unwrap();
536        fs::write(
537            dir.join("platforms/web/index.html"),
538            "<!doctype html><title>custom</title>\n",
539        )
540        .unwrap();
541        fs::write(dir.join("README.md"), "# custom readme\n").unwrap();
542
543        run([
544            "fission",
545            "add-target",
546            "web",
547            "--project-dir",
548            dir.to_str().unwrap(),
549        ])
550        .unwrap();
551
552        assert_eq!(
553            fs::read_to_string(dir.join("platforms/web/index.html")).unwrap(),
554            "<!doctype html><title>custom</title>\n"
555        );
556        assert_eq!(
557            fs::read_to_string(dir.join("README.md")).unwrap(),
558            "# custom readme\n"
559        );
560        assert!(dir.join("platforms/web/README.md").exists());
561        assert!(dir.join("platforms/web/bootstrap.mjs").exists());
562        let project = read_project_config(&dir).unwrap();
563        assert!(project.targets.contains(&Target::Web));
564    }
565
566    #[test]
567    fn cargo_fission_alias_accepts_prefixed_subcommand() {
568        let dir = unique_dir("cargo-fission");
569        run([
570            "cargo-fission",
571            "fission",
572            "init",
573            dir.to_str().unwrap(),
574            "--name",
575            "cargo-fission-demo",
576        ])
577        .unwrap();
578
579        assert!(dir.join("Cargo.toml").exists());
580        assert!(dir.join("fission.toml").exists());
581    }
582
583    #[test]
584    fn doctor_command_runs_in_non_strict_mode() {
585        let dir = unique_dir("doctor");
586        run(["fission", "init", dir.to_str().unwrap()]).unwrap();
587        run([
588            "fission",
589            "doctor",
590            "web",
591            "--project-dir",
592            dir.to_str().unwrap(),
593        ])
594        .unwrap();
595    }
596}