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}