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, ServerCommand, 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::Server { command } => match command {
110 ServerCommand::Build {
111 project_dir,
112 release,
113 } => fission_command_server::build(&project_dir, release),
114 ServerCommand::Check {
115 project_dir,
116 release,
117 } => fission_command_server::check(&project_dir, release),
118 ServerCommand::Serve {
119 project_dir,
120 host,
121 port,
122 release,
123 } => fission_command_server::serve(&project_dir, release, host, port),
124 ServerCommand::Routes { project_dir } => fission_command_server::routes(&project_dir),
125 ServerCommand::Artifacts {
126 project_dir,
127 release,
128 no_compile,
129 } => fission_command_server::artifacts(&project_dir, release, !no_compile),
130 },
131 Command::Package {
132 target,
133 format,
134 project_dir,
135 release,
136 json,
137 } => fission_command_package::package(fission_command_package::PackageOptions {
138 project_dir,
139 target,
140 format,
141 release,
142 json,
143 }),
144 Command::Distribute {
145 action,
146 provider,
147 artifact,
148 site,
149 deploy,
150 track,
151 dry_run,
152 yes,
153 project_dir,
154 json,
155 } => fission_command_package::distribute(fission_command_package::DistributeOptions {
156 project_dir,
157 provider,
158 action: action.unwrap_or(fission_command_package::DistributeAction::Publish),
159 artifact,
160 site,
161 deploy,
162 track,
163 dry_run,
164 yes,
165 json,
166 }),
167 Command::Publish {
168 provider,
169 artifact,
170 site,
171 deploy,
172 track,
173 dry_run,
174 yes,
175 project_dir,
176 json,
177 } => fission_command_package::distribute(fission_command_package::DistributeOptions {
178 project_dir,
179 provider,
180 action: fission_command_package::DistributeAction::Publish,
181 artifact,
182 site,
183 deploy,
184 track,
185 dry_run,
186 yes,
187 json,
188 }),
189 Command::Readiness {
190 kind,
191 target,
192 format,
193 provider,
194 artifact,
195 site,
196 track,
197 project_dir,
198 json,
199 } => fission_command_package::readiness(fission_command_package::ReadinessOptions {
200 project_dir,
201 kind,
202 target,
203 format,
204 provider,
205 artifact,
206 site,
207 track,
208 json,
209 }),
210 Command::ReleaseConfig { command } => fission_command_release::release_config(command),
211 Command::ReleaseContent { command } => fission_command_release::release_content(command),
212 Command::Beta { command } => fission_command_release::beta(command),
213 Command::Signing { command } => fission_command_release::signing(command),
214 Command::Reviews { command } => fission_command_release::reviews(command),
215 Command::ReleaseWorkflow { command } => fission_command_release::release_workflow(command),
216 Command::Auth { command } => fission_command_release::auth(command),
217 Command::Logs {
218 target,
219 device,
220 project_dir,
221 follow,
222 } => fission_command_run::attach_logs(fission_command_run::LogOptions {
223 project_dir,
224 target,
225 device,
226 follow,
227 }),
228 Command::Ui {
229 project_dir,
230 screenshot,
231 exit_after_render,
232 width,
233 height,
234 } => fission_command_ui::run_ui(fission_command_ui::UiOptions {
235 project_dir,
236 screenshot,
237 exit_after_render,
238 width,
239 height,
240 }),
241 Command::ServeWeb {
242 project_dir,
243 host,
244 port,
245 open,
246 } => fission_command_run::serve_web(fission_command_run::ServeWebOptions {
247 project_dir,
248 host,
249 port,
250 open,
251 }),
252 }
253}
254
255#[cfg(test)]
256mod tests {
257 use super::*;
258 use std::{fs, path::PathBuf, process::Command};
259
260 fn unique_dir(name: &str) -> PathBuf {
261 let dir =
262 std::env::temp_dir().join(format!("cargo-fission-{}-{}", name, std::process::id()));
263 let _ = fs::remove_dir_all(&dir);
264 dir
265 }
266
267 #[cfg(unix)]
268 fn write_executable(path: impl AsRef<std::path::Path>, contents: &str) {
269 use std::os::unix::fs::PermissionsExt;
270
271 let path = path.as_ref();
272 if let Some(parent) = path.parent() {
273 fs::create_dir_all(parent).unwrap();
274 }
275 fs::write(path, contents).unwrap();
276 fs::set_permissions(path, fs::Permissions::from_mode(0o755)).unwrap();
277 }
278
279 #[cfg(unix)]
280 fn path_with_fake_bin(fake_bin: &std::path::Path) -> String {
281 let existing = std::env::var("PATH").unwrap_or_default();
282 format!("{}:{existing}", fake_bin.display())
283 }
284
285 #[cfg(unix)]
286 fn write_fake_cargo(fake_bin: &std::path::Path) {
287 write_executable(
288 fake_bin.join("cargo"),
289 r#"#!/usr/bin/env bash
290set -euo pipefail
291if [[ "${1:-}" == "metadata" ]]; then
292 printf '%s\n' "$FAKE_TARGET_DIR"
293 exit 0
294fi
295if [[ "${1:-}" == "build" ]]; then
296 target=""
297 package=""
298 profile="debug"
299 args=("$@")
300 for ((i = 0; i < ${#args[@]}; i++)); do
301 case "${args[$i]}" in
302 --target) target="${args[$((i + 1))]}" ;;
303 --package) package="${args[$((i + 1))]}" ;;
304 --release) profile="release" ;;
305 esac
306 done
307 if [[ -z "$target" || -z "$package" ]]; then
308 printf 'fake cargo expected --target and --package\n' >&2
309 exit 2
310 fi
311 artifact_dir="$FAKE_TARGET_DIR/$target/$profile"
312 mkdir -p "$artifact_dir"
313 lib_name="${package//-/_}"
314 printf 'fake native library\n' > "$artifact_dir/lib${lib_name}.so"
315 printf '#!/usr/bin/env sh\nexit 0\n' > "$artifact_dir/$package"
316 chmod +x "$artifact_dir/$package"
317 exit 0
318fi
319printf 'unsupported fake cargo invocation: %s\n' "$*" >&2
320exit 2
321"#,
322 );
323 }
324
325 #[cfg(unix)]
326 fn write_fake_python3(fake_bin: &std::path::Path) {
327 write_executable(
328 fake_bin.join("python3"),
329 r#"#!/usr/bin/env bash
330set -euo pipefail
331script=$(cat)
332if [[ "$script" == *"import plistlib"* ]]; then
333 printf 'plistlib must not be used by generated mobile package scripts\n' >&2
334 exit 44
335fi
336if [[ "$script" == *"cargo\", \"metadata\""* || "$script" == *"metadata = json.loads"* ]]; then
337 printf '%s\n' "$FAKE_TARGET_DIR"
338 exit 0
339fi
340if [[ "$script" == *"android:minSdkVersion"* ]]; then
341 if [[ "${1:-}" == "-" ]]; then
342 shift
343 fi
344 source="$1"
345 dest="$2"
346 min_api="$3"
347 target_api="$4"
348 has_code=false
349 if [[ -f "$(dirname "$dest")/apk-root/classes.dex" ]]; then
350 has_code=true
351 fi
352 sed -E \
353 -e "s/android:minSdkVersion=\"[0-9]+\"/android:minSdkVersion=\"$min_api\"/" \
354 -e "s/android:targetSdkVersion=\"[0-9]+\"/android:targetSdkVersion=\"$target_api\"/" \
355 -e "s/android:hasCode=\"(true|false)\"/android:hasCode=\"$has_code\"/" \
356 "$source" > "$dest"
357 exit 0
358fi
359printf 'unsupported fake python3 script\n%s\n' "$script" >&2
360exit 2
361"#,
362 );
363 }
364
365 #[cfg(unix)]
366 fn write_fake_ios_tools(fake_bin: &std::path::Path) {
367 write_executable(
368 fake_bin.join("xcrun"),
369 r#"#!/usr/bin/env bash
370set -euo pipefail
371if [[ "${1:-}" == "--find" && "${2:-}" == "plutil" ]]; then
372 printf '%s/plutil\n' "$FISSION_FAKE_BIN"
373 exit 0
374fi
375if [[ "${1:-}" == "--find" && "${2:-}" == "ibtool" ]]; then
376 printf '%s/ibtool\n' "$FISSION_FAKE_BIN"
377 exit 0
378fi
379printf 'unsupported fake xcrun invocation: %s\n' "$*" >&2
380exit 2
381"#,
382 );
383 write_executable(
384 fake_bin.join("plutil"),
385 r#"#!/usr/bin/env bash
386set -euo pipefail
387if [[ "${1:-}" != "-replace" || "${3:-}" != "-string" ]]; then
388 printf 'unsupported fake plutil invocation: %s\n' "$*" >&2
389 exit 2
390fi
391key="$2"
392value="$4"
393file="$5"
394tmp="$(mktemp)"
395awk -v key="$key" -v value="$value" '
396 replace_next {
397 print " <string>" value "</string>"
398 replace_next = 0
399 next
400 }
401 {
402 print
403 if ($0 ~ "<key>" key "</key>") {
404 replace_next = 1
405 }
406 }
407' "$file" > "$tmp"
408mv "$tmp" "$file"
409"#,
410 );
411 write_executable(
412 fake_bin.join("ibtool"),
413 r#"#!/usr/bin/env bash
414set -euo pipefail
415args=("$@")
416for ((i = 0; i < ${#args[@]}; i++)); do
417 if [[ "${args[$i]}" == "--compile" ]]; then
418 mkdir -p "${args[$((i + 1))]}"
419 exit 0
420 fi
421done
422printf 'fake ibtool missing --compile\n' >&2
423exit 2
424"#,
425 );
426 }
427
428 #[cfg(unix)]
429 fn write_fake_android_tools(android_home: &std::path::Path, fake_bin: &std::path::Path) {
430 let build_tools = android_home.join("build-tools/35.0.0");
431 let ndk_bin = android_home.join("ndk/27.0.0/toolchains/llvm/prebuilt/linux-x86_64/bin");
432 fs::create_dir_all(android_home.join("platforms/android-35")).unwrap();
433 fs::write(android_home.join("platforms/android-35/android.jar"), "").unwrap();
434 fs::create_dir_all(&build_tools).unwrap();
435 fs::create_dir_all(&ndk_bin).unwrap();
436 fs::write(ndk_bin.join("aarch64-linux-android24-clang"), "").unwrap();
437 fs::write(ndk_bin.join("llvm-ar"), "").unwrap();
438
439 write_executable(
440 build_tools.join("aapt"),
441 r#"#!/usr/bin/env bash
442set -euo pipefail
443out=""
444manifest=""
445args=("$@")
446for ((i = 0; i < ${#args[@]}; i++)); do
447 case "${args[$i]}" in
448 -F) out="${args[$((i + 1))]}" ;;
449 -M) manifest="${args[$((i + 1))]}" ;;
450 esac
451done
452if [[ -z "$out" || -z "$manifest" ]]; then
453 printf 'fake aapt missing -F or -M\n' >&2
454 exit 2
455fi
456mkdir -p "$(dirname "$out")"
457{
458 printf 'AAPT_MANIFEST_BEGIN\n'
459 cat "$manifest"
460 printf '\nAAPT_MANIFEST_END\n'
461} > "$out"
462"#,
463 );
464 write_executable(
465 build_tools.join("zipalign"),
466 r#"#!/usr/bin/env bash
467set -euo pipefail
468args=("$@")
469count=${#args[@]}
470input="${args[$((count - 2))]}"
471output="${args[$((count - 1))]}"
472cp "$input" "$output"
473"#,
474 );
475 write_executable(
476 build_tools.join("apksigner"),
477 r#"#!/usr/bin/env bash
478set -euo pipefail
479out=""
480input=""
481args=("$@")
482for ((i = 0; i < ${#args[@]}; i++)); do
483 if [[ "${args[$i]}" == "--out" ]]; then
484 out="${args[$((i + 1))]}"
485 fi
486done
487input="${args[$((${#args[@]} - 1))]}"
488if [[ -z "$out" ]]; then
489 printf 'fake apksigner missing --out\n' >&2
490 exit 2
491fi
492cp "$input" "$out"
493"#,
494 );
495 write_executable(
496 fake_bin.join("zip"),
497 r#"#!/usr/bin/env bash
498set -euo pipefail
499archive=""
500entries=()
501for arg in "$@"; do
502 if [[ "$arg" == -* ]]; then
503 continue
504 fi
505 if [[ -z "$archive" ]]; then
506 archive="$arg"
507 else
508 entries+=("$arg")
509 fi
510done
511if [[ -z "$archive" ]]; then
512 printf 'fake zip missing archive\n' >&2
513 exit 2
514fi
515for entry in "${entries[@]}"; do
516 if [[ -d "$entry" ]]; then
517 while IFS= read -r file; do
518 printf 'APK_ENTRY=%s\n' "$file" >> "$archive"
519 done < <(find "$entry" -type f | sort)
520 elif [[ -f "$entry" ]]; then
521 printf 'APK_ENTRY=%s\n' "$entry" >> "$archive"
522 fi
523done
524 "#,
525 );
526 write_executable(
527 fake_bin.join("gradle"),
528 r#"#!/usr/bin/env bash
529set -euo pipefail
530project=""
531args=("$@")
532for ((i = 0; i < ${#args[@]}; i++)); do
533 if [[ "${args[$i]}" == "-p" ]]; then
534 project="${args[$((i + 1))]}"
535 fi
536done
537if [[ -z "$project" ]]; then
538 printf 'fake gradle missing -p project\n' >&2
539 exit 2
540fi
541variant="debug"
542for arg in "$@"; do
543 if [[ "$arg" == *"assembleRelease"* ]]; then
544 variant="release"
545 fi
546done
547apk="$project/app/build/outputs/apk/$variant/app-$variant.apk"
548mkdir -p "$(dirname "$apk")"
549{
550 printf 'GRADLE_MANIFEST_BEGIN\n'
551 cat "$project/AndroidManifest.xml"
552 printf '\nGRADLE_MANIFEST_END\n'
553 if [[ -d "$project/app/src/main/jniLibs" ]]; then
554 (cd "$project/app/src/main/jniLibs" && find . -type f | sort | sed 's#^\./#APK_ENTRY=lib/#')
555 fi
556} > "$apk"
557"#,
558 );
559 }
560
561 #[test]
562 fn init_creates_project_files() {
563 let dir = unique_dir("init");
564 run([
565 "fission",
566 "init",
567 dir.to_str().unwrap(),
568 "--name",
569 "hello-fission",
570 ])
571 .unwrap();
572
573 assert!(dir.join("Cargo.toml").exists());
574 assert!(dir.join("src/main.rs").exists());
575 assert!(dir.join("src/lib.rs").exists());
576 assert!(dir.join("src/app.rs").exists());
577 assert!(dir.join("assets/app-icon.png").exists());
578 assert!(dir.join("fission.toml").exists());
579 assert!(dir.join("platforms/windows/README.md").exists());
580 assert!(dir.join("platforms/macos/README.md").exists());
581 assert!(dir.join("platforms/linux/README.md").exists());
582 let readme = std::fs::read_to_string(dir.join("README.md")).unwrap();
583 assert!(readme.contains("fission devices --project-dir ."));
584 assert!(readme.contains("fission run --project-dir ."));
585 assert!(readme.contains("fission logs --target <target>"));
586 assert!(readme.contains("fission build --target <target>"));
587 assert!(readme.contains("fission test --target <target>"));
588 let manifest = std::fs::read_to_string(dir.join("Cargo.toml")).unwrap();
589 assert!(manifest.contains("default-features = false"));
590 assert!(manifest.contains("features = [\"desktop\"]"));
591 }
592
593 #[test]
594 fn add_target_updates_manifest_and_scaffold() {
595 let dir = unique_dir("targets");
596 run(["fission", "init", dir.to_str().unwrap()]).unwrap();
597 run([
598 "fission",
599 "add-target",
600 "web",
601 "ios",
602 "android",
603 "--project-dir",
604 dir.to_str().unwrap(),
605 ])
606 .unwrap();
607
608 let project = read_project_config(&dir).unwrap();
609 assert!(project.targets.contains(&Target::Web));
610 assert!(project.targets.contains(&Target::Ios));
611 assert!(project.targets.contains(&Target::Android));
612 let manifest = std::fs::read_to_string(dir.join("Cargo.toml")).unwrap();
613 assert!(manifest.contains("default-features = false"));
614 assert!(manifest.contains("features = [\"desktop\", \"web\", \"android\", \"ios\"]"));
615 assert!(dir.join("platforms/web/README.md").exists());
616 assert!(dir.join("platforms/web/index.html").exists());
617 assert!(dir.join("platforms/web/bootstrap.mjs").exists());
618 assert!(dir.join("platforms/web/build-wasm.sh").exists());
619 assert!(dir.join("platforms/web/run-browser.sh").exists());
620 assert!(dir.join("platforms/web/test-browser.sh").exists());
621 assert!(dir.join("platforms/ios/README.md").exists());
622 assert!(dir.join("platforms/ios/Info.plist").exists());
623 assert!(dir.join("platforms/ios/LaunchScreen.storyboard").exists());
624 assert!(dir.join("platforms/ios/package-sim.sh").exists());
625 assert!(dir.join("platforms/ios/run-sim.sh").exists());
626 assert!(dir.join("platforms/ios/test-sim.sh").exists());
627 assert!(dir.join("platforms/ios/Package.swift").exists());
628 assert!(dir
629 .join("platforms/ios/Sources/FissionHost/FissionNativeCapabilities.swift")
630 .exists());
631 assert!(dir
632 .join(
633 "platforms/ios/NativeModules/Sources/FissionNativeModules/FissionNativeCapabilities.swift"
634 )
635 .exists());
636 assert!(dir.join("platforms/android/README.md").exists());
637 assert!(dir.join("platforms/android/AndroidManifest.xml").exists());
638 assert!(dir.join("platforms/android/settings.gradle.kts").exists());
639 assert!(dir.join("platforms/android/build.gradle.kts").exists());
640 assert!(dir.join("platforms/android/gradle.properties").exists());
641 assert!(dir.join("platforms/android/app/build.gradle.kts").exists());
642 assert!(dir.join("platforms/android/native-modules.gradle").exists());
643 assert!(dir
644 .join("platforms/android/java/rs/fission/runtime/FissionActivity.java")
645 .exists());
646 assert!(dir
647 .join("platforms/android/native-modules/README.md")
648 .exists());
649 assert!(dir.join("platforms/android/res/values/colors.xml").exists());
650 assert!(dir.join("platforms/android/res/values/styles.xml").exists());
651 assert!(dir
652 .join("platforms/android/res/drawable/fission_splash_background.xml")
653 .exists());
654 assert!(dir.join("platforms/android/package-apk.sh").exists());
655 assert!(dir.join("platforms/android/run-emulator.sh").exists());
656 assert!(dir.join("platforms/android/test-emulator.sh").exists());
657 let android_manifest =
658 std::fs::read_to_string(dir.join("platforms/android/AndroidManifest.xml")).unwrap();
659 assert!(android_manifest.contains("android:icon=\"@drawable/app_icon\""));
660 assert!(android_manifest.contains("android:hasCode=\"true\""));
661 assert!(android_manifest.contains("android:targetSdkVersion=\"35\""));
662 assert!(android_manifest.contains("android:theme=\"@style/FissionLaunchTheme\""));
663 assert!(android_manifest.contains("rs.fission.runtime.FissionActivity"));
664 let android_styles =
665 std::fs::read_to_string(dir.join("platforms/android/res/values/styles.xml")).unwrap();
666 assert!(android_styles.contains("android:windowBackground"));
667 assert!(android_styles.contains("android:windowSplashScreenAnimatedIcon"));
668 let android_package_script =
669 std::fs::read_to_string(dir.join("platforms/android/package-apk.sh")).unwrap();
670 assert!(android_package_script.contains("detect_android_toolchain"));
671 assert!(android_package_script
672 .contains("darwin-aarch64 darwin-x86_64 linux-x86_64 windows-x86_64"));
673 assert!(android_package_script.contains(
674 "ANDROID_MIN_API_LEVEL=\"${ANDROID_MIN_API_LEVEL:-${ANDROID_API_LEVEL:-24}}\""
675 ));
676 assert!(android_package_script.contains("ANDROID_TARGET_API_LEVEL="));
677 assert!(
678 android_package_script.contains("aarch64-linux-android${ANDROID_MIN_API_LEVEL}-clang")
679 );
680 assert!(android_package_script.contains("GRADLE_CMD"));
681 assert!(android_package_script.contains(":app:assemble"));
682 assert!(android_package_script.contains("app/src/main/jniLibs/arm64-v8a"));
683 assert!(android_package_script.contains("FISSION_GRADLE"));
684 assert!(android_package_script.contains("Gradle is required"));
685 let android_run_script =
686 std::fs::read_to_string(dir.join("platforms/android/run-emulator.sh")).unwrap();
687 assert!(android_run_script.contains("ANDROID_EMULATOR_API_LEVEL"));
688 assert!(android_run_script.contains("fission doctor android"));
689 assert!(android_run_script.contains("wait_for_android_boot()"));
690 assert!(android_run_script.contains("cmd package list packages"));
691 assert!(android_run_script.contains("ADB_INSTALL_FLAGS:---no-streaming -r"));
692 assert!(android_run_script.contains("rs.fission.runtime.FissionActivity"));
693 assert!(!android_run_script.contains("wait_for_android_boot() {\n wait_for_android_boot"));
694 assert!(!android_run_script.contains(" wait_for_android_boot\n wait_for_android_boot"));
695 assert!(
696 std::fs::read_to_string(dir.join("platforms/android/README.md"))
697 .unwrap()
698 .contains("fission run --target android")
699 );
700 let android_test_script =
701 std::fs::read_to_string(dir.join("platforms/android/test-emulator.sh")).unwrap();
702 assert!(android_test_script.contains("/health"));
703 let ios_package_script =
704 std::fs::read_to_string(dir.join("platforms/ios/package-sim.sh")).unwrap();
705 assert!(ios_package_script.contains("TARGET=\"${IOS_SIM_TARGET:-aarch64-apple-ios-sim}\""));
706 assert!(ios_package_script.contains("PROFILE=\"${IOS_SIM_PROFILE:-debug}\""));
707 assert!(ios_package_script.contains("BUNDLE_ID=\"${IOS_BUNDLE_ID:-com.example."));
708 assert!(ios_package_script.contains("DISPLAY_NAME=\"${IOS_DISPLAY_NAME:-"));
709 assert!(ios_package_script.contains("EXECUTABLE_NAME=\"${IOS_EXECUTABLE_NAME:-"));
710 assert!(ios_package_script.contains("xcrun --find plutil"));
711 assert!(ios_package_script.contains("-replace CFBundleIdentifier -string"));
712 assert!(!ios_package_script.contains("import plistlib"));
713 assert!(ios_package_script.contains("PkgInfo"));
714 assert!(ios_package_script.contains("PLATFORM_APP_ICONS"));
715 assert!(ios_package_script.contains("AppIcon.png"));
716 assert!(ios_package_script.contains("LaunchScreen.storyboard"));
717 assert!(ios_package_script.contains("ibtool"));
718 assert!(ios_package_script.contains("LaunchScreen.storyboardc"));
719 assert!(ios_package_script.contains("SplashImage.png"));
720 let ios_plist = std::fs::read_to_string(dir.join("platforms/ios/Info.plist")).unwrap();
721 assert!(ios_plist.contains("UILaunchStoryboardName"));
722 let ios_run_script = std::fs::read_to_string(dir.join("platforms/ios/run-sim.sh")).unwrap();
723 assert!(ios_run_script.contains("BUNDLE_ID=\"${IOS_BUNDLE_ID:-com.example."));
724 assert!(ios_run_script.contains("IOS_SIM_UNINSTALL_BEFORE_INSTALL"));
725 assert!(ios_run_script.contains(
726 "xcrun simctl launch --terminate-running-process \"$DEVICE_ID\" \"$BUNDLE_ID\""
727 ));
728 assert!(std::fs::read_to_string(dir.join("platforms/ios/README.md"))
729 .unwrap()
730 .contains("fission run --target ios"));
731 assert!(
732 std::fs::read_to_string(dir.join("platforms/ios/test-sim.sh"))
733 .unwrap()
734 .contains("/health")
735 );
736 assert!(
737 std::fs::read_to_string(dir.join("platforms/web/index.html"))
738 .unwrap()
739 .contains("../../assets/app-icon.png")
740 );
741 let web_index = std::fs::read_to_string(dir.join("platforms/web/index.html")).unwrap();
742 assert!(web_index.contains("id=\"fission-web-mount\""));
743 assert!(web_index.contains("height: 100vh"));
744 assert!(web_index.contains("outline: none"));
745 assert!(web_index.contains("touch-action: none"));
746 assert!(!web_index.contains("Generated by"));
747 let web_test_script =
748 std::fs::read_to_string(dir.join("platforms/web/test-browser.sh")).unwrap();
749 assert!(web_test_script.contains("--remote-debugging-port=\"$CDP_PORT\""));
750 assert!(web_test_script.contains("/json/list"));
751 assert!(std::fs::read_to_string(dir.join("platforms/web/README.md"))
752 .unwrap()
753 .contains("fission run --target web"));
754 }
755
756 #[test]
757 fn init_hardens_existing_android_native_only_scaffold() {
758 let dir = unique_dir("android-hardening");
759 run(["fission", "init", dir.to_str().unwrap()]).unwrap();
760 run([
761 "fission",
762 "add-target",
763 "android",
764 "--project-dir",
765 dir.to_str().unwrap(),
766 ])
767 .unwrap();
768
769 let manifest_path = dir.join("platforms/android/AndroidManifest.xml");
770 let package_path = dir.join("platforms/android/package-apk.sh");
771 fs::write(
772 &manifest_path,
773 fs::read_to_string(&manifest_path).unwrap().replace(
774 "rs.fission.runtime.FissionActivity",
775 "android.app.NativeActivity",
776 ),
777 )
778 .unwrap();
779 fs::write(
780 &package_path,
781 fs::read_to_string(&package_path)
782 .unwrap()
783 .replace("import pathlib\n", "")
784 .replace(
785 r#"has_code = "true" if pathlib.Path(dest).with_name("apk-root").joinpath("classes.dex").exists() else "false"
786manifest = re.sub(r'android:hasCode="(?:true|false)"', f'android:hasCode="{has_code}"', manifest)
787"#,
788 "",
789 ),
790 )
791 .unwrap();
792
793 run(["fission", "init", dir.to_str().unwrap()]).unwrap();
794
795 let manifest = fs::read_to_string(manifest_path).unwrap();
796 assert!(manifest.contains("android:hasCode=\"false\""));
797 assert!(manifest.contains("android.app.NativeActivity"));
798 let package_script = fs::read_to_string(package_path).unwrap();
799 assert!(package_script.contains(":app:assemble"));
800 assert!(package_script.contains("app/src/main/jniLibs/arm64-v8a"));
801 assert!(!package_script.contains("import pathlib"));
802 }
803
804 #[test]
805 fn init_hardens_existing_android_raw_package_script_resources() {
806 let dir = unique_dir("android-raw-resources");
807 run(["fission", "init", dir.to_str().unwrap()]).unwrap();
808 run([
809 "fission",
810 "add-target",
811 "android",
812 "--project-dir",
813 dir.to_str().unwrap(),
814 ])
815 .unwrap();
816
817 let package_path = dir.join("platforms/android/package-apk.sh");
818 fs::write(
819 &package_path,
820 r#"#!/usr/bin/env bash
821set -euo pipefail
822SCRIPT_DIR=$(cd -- "$(dirname "${BASH_SOURCE[0]}")" && pwd)
823ICON_SOURCE="${FISSION_APP_ICON:-$SCRIPT_DIR/icon.png}"
824APK_ROOT="$SCRIPT_DIR/build/debug/apk-root"
825SO_PATH="$SCRIPT_DIR/libexample.so"
826LIB_NAME="example"
827mkdir -p "$APK_ROOT/lib/arm64-v8a" "$APK_ROOT/res/drawable-nodpi"
828cp "$SO_PATH" "$APK_ROOT/lib/arm64-v8a/lib$LIB_NAME.so"
829cp "$ICON_SOURCE" "$APK_ROOT/res/drawable-nodpi/app_icon.png"
830"#,
831 )
832 .unwrap();
833
834 run(["fission", "init", dir.to_str().unwrap()]).unwrap();
835
836 let package_script = fs::read_to_string(package_path).unwrap();
837 assert!(package_script.contains("SPLASH_IMAGES"));
838 assert!(package_script.contains(
839 "cp \"$ICON_SOURCE\" \"$APK_ROOT/res/drawable-nodpi/fission_splash_image.png\""
840 ));
841 assert!(package_script.contains("cp -R \"$SCRIPT_DIR/res/.\" \"$APK_ROOT/res/\""));
842 }
843
844 #[test]
845 fn init_hardens_existing_ios_package_script_without_replacing_user_files() {
846 let dir = unique_dir("ios-package-hardening");
847 run(["fission", "init", dir.to_str().unwrap()]).unwrap();
848 run([
849 "fission",
850 "add-target",
851 "ios",
852 "--project-dir",
853 dir.to_str().unwrap(),
854 ])
855 .unwrap();
856 let script_path = dir.join("platforms/ios/package-sim.sh");
857 let mut script = fs::read_to_string(&script_path).unwrap();
858 script = script.replace(
859 r#"cp "$SCRIPT_DIR/Info.plist" "$BUNDLE_DIR/Info.plist"
860PLUTIL=$(xcrun --find plutil 2>/dev/null || command -v plutil || true)
861if [[ -z "$PLUTIL" ]]; then
862 printf 'plutil not found. Install Xcode command line tools to package the iOS simulator app.\n' >&2
863 exit 1
864fi
865"$PLUTIL" -replace CFBundleIdentifier -string "$BUNDLE_ID" "$BUNDLE_DIR/Info.plist"
866"$PLUTIL" -replace CFBundleDisplayName -string "$DISPLAY_NAME" "$BUNDLE_DIR/Info.plist"
867"$PLUTIL" -replace CFBundleName -string "$DISPLAY_NAME" "$BUNDLE_DIR/Info.plist"
868"$PLUTIL" -replace CFBundleExecutable -string "$EXECUTABLE_NAME" "$BUNDLE_DIR/Info.plist"
869"#,
870 r#"python3 - <<'PY' "$SCRIPT_DIR/Info.plist" "$BUNDLE_DIR/Info.plist" "$BUNDLE_ID" "$DISPLAY_NAME" "$EXECUTABLE_NAME"
871import plistlib
872import sys
873
874source, dest, bundle_id, display_name, executable_name = sys.argv[1:]
875with open(source, "rb") as handle:
876 plist = plistlib.load(handle)
877plist["CFBundleIdentifier"] = bundle_id
878plist["CFBundleDisplayName"] = display_name
879plist["CFBundleName"] = display_name
880plist["CFBundleExecutable"] = executable_name
881with open(dest, "wb") as handle:
882 plistlib.dump(plist, handle, sort_keys=False)
883PY
884"#,
885 );
886 fs::write(&script_path, script).unwrap();
887
888 run(["fission", "init", dir.to_str().unwrap()]).unwrap();
889
890 let hardened = fs::read_to_string(script_path).unwrap();
891 assert!(hardened.contains("xcrun --find plutil"));
892 assert!(hardened.contains("-replace CFBundleExecutable -string"));
893 assert!(!hardened.contains("import plistlib"));
894 }
895
896 #[cfg(unix)]
897 #[test]
898 fn ios_package_script_executes_without_python_plistlib() {
899 let dir = unique_dir("ios-package-e2e");
900 run([
901 "fission",
902 "init",
903 dir.to_str().unwrap(),
904 "--name",
905 "ios-package-e2e",
906 "--app-id",
907 "com.example.iospackagee2e",
908 ])
909 .unwrap();
910 run([
911 "fission",
912 "add-target",
913 "ios",
914 "--project-dir",
915 dir.to_str().unwrap(),
916 ])
917 .unwrap();
918
919 let fake_bin = dir.join("fake-bin");
920 let fake_target = dir.join("fake-target");
921 fs::create_dir_all(&fake_bin).unwrap();
922 fs::create_dir_all(&fake_target).unwrap();
923 write_fake_cargo(&fake_bin);
924 write_fake_python3(&fake_bin);
925 write_fake_ios_tools(&fake_bin);
926
927 let output = Command::new("bash")
928 .arg("platforms/ios/package-sim.sh")
929 .current_dir(&dir)
930 .env("PATH", path_with_fake_bin(&fake_bin))
931 .env("FAKE_TARGET_DIR", &fake_target)
932 .env("FISSION_FAKE_BIN", &fake_bin)
933 .env("IOS_BUNDLE_ID", "com.example.overridden")
934 .env("IOS_DISPLAY_NAME", "Overridden iOS")
935 .env("IOS_EXECUTABLE_NAME", "OverriddenExecutable")
936 .output()
937 .unwrap();
938 assert!(
939 output.status.success(),
940 "package-sim.sh failed\nstdout:\n{}\nstderr:\n{}",
941 String::from_utf8_lossy(&output.stdout),
942 String::from_utf8_lossy(&output.stderr)
943 );
944
945 let bundle_dir = String::from_utf8(output.stdout).unwrap();
946 let bundle_dir = PathBuf::from(bundle_dir.trim());
947 assert!(bundle_dir.join("OverriddenExecutable").exists());
948 assert!(bundle_dir.join("LaunchScreen.storyboardc").exists());
949 let plist = fs::read_to_string(bundle_dir.join("Info.plist")).unwrap();
950 assert!(plist.contains("<string>com.example.overridden</string>"));
951 assert!(plist.contains("<string>Overridden iOS</string>"));
952 assert!(plist.contains("<string>OverriddenExecutable</string>"));
953 }
954
955 #[cfg(unix)]
956 #[test]
957 fn android_package_script_builds_native_only_apk_metadata() {
958 let dir = unique_dir("android-package-e2e");
959 run([
960 "fission",
961 "init",
962 dir.to_str().unwrap(),
963 "--name",
964 "android-package-e2e",
965 "--app-id",
966 "com.example.androidpackagee2e",
967 ])
968 .unwrap();
969 run([
970 "fission",
971 "add-target",
972 "android",
973 "--project-dir",
974 dir.to_str().unwrap(),
975 ])
976 .unwrap();
977
978 let fake_bin = dir.join("fake-bin");
979 let fake_target = dir.join("fake-target");
980 let android_home = dir.join("fake-android-sdk");
981 fs::create_dir_all(&fake_bin).unwrap();
982 fs::create_dir_all(&fake_target).unwrap();
983 write_fake_cargo(&fake_bin);
984 write_fake_python3(&fake_bin);
985 write_fake_android_tools(&android_home, &fake_bin);
986 let keystore = dir.join("debug.keystore");
987 fs::write(&keystore, "fake debug keystore").unwrap();
988
989 let output = Command::new("bash")
990 .arg("platforms/android/package-apk.sh")
991 .current_dir(&dir)
992 .env("PATH", path_with_fake_bin(&fake_bin))
993 .env("FAKE_TARGET_DIR", &fake_target)
994 .env("ANDROID_HOME", &android_home)
995 .env("ANDROID_DEBUG_KEYSTORE", &keystore)
996 .output()
997 .unwrap();
998 assert!(
999 output.status.success(),
1000 "package-apk.sh failed\nstdout:\n{}\nstderr:\n{}",
1001 String::from_utf8_lossy(&output.stdout),
1002 String::from_utf8_lossy(&output.stderr)
1003 );
1004
1005 let apk = String::from_utf8(output.stdout).unwrap();
1006 let apk = PathBuf::from(apk.trim());
1007 let apk_payload = fs::read_to_string(apk).unwrap();
1008 assert!(apk_payload.contains("android:hasCode=\"true\""));
1009 assert!(apk_payload.contains("android:minSdkVersion=\"24\""));
1010 assert!(apk_payload.contains("android:targetSdkVersion=\"35\""));
1011 assert!(apk_payload.contains("APK_ENTRY=lib/arm64-v8a/libandroid_package_e2e.so"));
1012 assert!(!apk_payload.contains("APK_ENTRY=classes.dex"));
1013 }
1014
1015 #[test]
1016 fn init_existing_project_enables_features_for_detected_mobile_targets() {
1017 let dir = unique_dir("init-mobile-features");
1018 fs::create_dir_all(dir.join("src")).unwrap();
1019 fs::create_dir_all(dir.join("platforms/android")).unwrap();
1020 fs::create_dir_all(dir.join("platforms/ios")).unwrap();
1021 fs::write(dir.join("src/main.rs"), "fn main() {}\n").unwrap();
1022 fs::write(
1023 dir.join("Cargo.toml"),
1024 r#"[package]
1025name = "existing-mobile"
1026version = "0.1.0"
1027edition = "2021"
1028
1029[dependencies]
1030fission = { version = "0.6.1", default-features = false, features = ["desktop"] }
1031"#,
1032 )
1033 .unwrap();
1034
1035 run(["fission", "init", dir.to_str().unwrap()]).unwrap();
1036
1037 let manifest = fs::read_to_string(dir.join("Cargo.toml")).unwrap();
1038 assert!(manifest.contains("default-features = false"));
1039 assert!(manifest.contains(r#"features = ["desktop", "android", "ios"]"#));
1040 }
1041
1042 #[test]
1043 fn add_target_updates_multiline_fission_dependency_features() {
1044 let dir = unique_dir("multiline-features");
1045 run(["fission", "init", dir.to_str().unwrap()]).unwrap();
1046 fs::write(
1047 dir.join("Cargo.toml"),
1048 r#"[package]
1049name = "multiline-features"
1050version = "0.1.0"
1051edition = "2021"
1052
1053[dependencies]
1054anyhow = "1"
1055
1056[dependencies.fission]
1057version = "0.6.1"
1058default-features = true
1059features = ["desktop"]
1060"#,
1061 )
1062 .unwrap();
1063
1064 run([
1065 "fission",
1066 "add-target",
1067 "android",
1068 "ios",
1069 "--project-dir",
1070 dir.to_str().unwrap(),
1071 ])
1072 .unwrap();
1073
1074 let manifest = fs::read_to_string(dir.join("Cargo.toml")).unwrap();
1075 assert!(manifest.contains("[dependencies.fission]"));
1076 assert!(manifest.contains("default-features = false"));
1077 assert!(manifest.contains(r#"features = ["desktop", "android", "ios"]"#));
1078 }
1079
1080 #[test]
1081 fn custom_icon_config_is_preserved_and_applied_to_mobile_scaffolds() {
1082 let dir = unique_dir("custom-icons");
1083 run(["fission", "init", dir.to_str().unwrap()]).unwrap();
1084 fs::copy(
1085 dir.join("assets/app-icon.png"),
1086 dir.join("assets/shared-icon.png"),
1087 )
1088 .unwrap();
1089 fs::copy(
1090 dir.join("assets/app-icon.png"),
1091 dir.join("assets/android-icon.png"),
1092 )
1093 .unwrap();
1094 fs::copy(
1095 dir.join("assets/app-icon.png"),
1096 dir.join("assets/ios-icon.png"),
1097 )
1098 .unwrap();
1099 let mut manifest = fs::read_to_string(dir.join("fission.toml")).unwrap();
1100 manifest.push_str(
1101 r##"
1102
1103[package.icons]
1104mode = "mixed"
1105source = "assets/shared-icon.png"
1106safe_zone = 0.72
1107allow_upscale = false
1108
1109[package.icons.android]
1110source = "assets/android-icon.png"
1111
1112[package.icons.ios]
1113source = "assets/ios-icon.png"
1114"##,
1115 );
1116 fs::write(dir.join("fission.toml"), manifest).unwrap();
1117
1118 run([
1119 "fission",
1120 "add-target",
1121 "ios",
1122 "android",
1123 "--project-dir",
1124 dir.to_str().unwrap(),
1125 ])
1126 .unwrap();
1127
1128 let manifest = fs::read_to_string(dir.join("fission.toml")).unwrap();
1129 assert!(manifest.contains("[package.icons]"));
1130 assert!(manifest.contains("source = \"assets/shared-icon.png\""));
1131 assert!(manifest.contains("[package.icons.android]"));
1132 assert!(manifest.contains("[package.icons.ios]"));
1133 assert!(dir
1134 .join("platforms/android/res/drawable-nodpi/app_icon.png")
1135 .exists());
1136 assert!(dir.join("platforms/ios/AppIcon.png").exists());
1137 let android_manifest =
1138 fs::read_to_string(dir.join("platforms/android/AndroidManifest.xml")).unwrap();
1139 assert!(android_manifest.contains("android:icon=\"@drawable/app_icon\""));
1140 let android_package =
1141 fs::read_to_string(dir.join("platforms/android/package-apk.sh")).unwrap();
1142 assert!(android_package.contains("APP_ICONS"));
1143 let ios_package = fs::read_to_string(dir.join("platforms/ios/package-sim.sh")).unwrap();
1144 assert!(ios_package.contains("PLATFORM_APP_ICONS"));
1145 }
1146
1147 #[test]
1148 fn custom_splash_config_updates_mobile_platform_files() {
1149 let dir = unique_dir("custom-splash");
1150 run(["fission", "init", dir.to_str().unwrap()]).unwrap();
1151 fs::copy(
1152 dir.join("assets/app-icon.png"),
1153 dir.join("assets/custom-splash.png"),
1154 )
1155 .unwrap();
1156 let mut manifest = fs::read_to_string(dir.join("fission.toml")).unwrap();
1157 manifest.push_str(
1158 r##"
1159
1160[app.splash]
1161background_color = "#123456"
1162image = "assets/custom-splash.png"
1163resize_mode = "cover"
1164android_animation_duration_ms = 650
1165"##,
1166 );
1167 fs::write(dir.join("fission.toml"), manifest).unwrap();
1168
1169 run([
1170 "fission",
1171 "add-target",
1172 "ios",
1173 "android",
1174 "--project-dir",
1175 dir.to_str().unwrap(),
1176 ])
1177 .unwrap();
1178
1179 let android_colors =
1180 fs::read_to_string(dir.join("platforms/android/res/values/colors.xml")).unwrap();
1181 assert!(android_colors.contains("#123456"));
1182 let android_styles =
1183 fs::read_to_string(dir.join("platforms/android/res/values/styles.xml")).unwrap();
1184 assert!(android_styles.contains("650"));
1185 assert!(dir
1186 .join("platforms/android/res/drawable-nodpi/fission_splash_image.png")
1187 .exists());
1188 let storyboard =
1189 fs::read_to_string(dir.join("platforms/ios/LaunchScreen.storyboard")).unwrap();
1190 assert!(storyboard.contains("scaleAspectFill"));
1191 assert!(storyboard.contains("red=\"0.070588\""));
1192 assert!(dir.join("platforms/ios/SplashImage.png").exists());
1193 }
1194
1195 #[test]
1196 fn add_capability_updates_project_and_platform_config() {
1197 let dir = unique_dir("capability");
1198 run(["fission", "init", dir.to_str().unwrap()]).unwrap();
1199 run([
1200 "fission",
1201 "add-target",
1202 "ios",
1203 "android",
1204 "--project-dir",
1205 dir.to_str().unwrap(),
1206 ])
1207 .unwrap();
1208 run([
1209 "fission",
1210 "add-capability",
1211 "nfc",
1212 "biometric",
1213 "bluetooth",
1214 "barcode-scanner",
1215 "camera",
1216 "geolocation",
1217 "haptics",
1218 "microphone",
1219 "volume-control",
1220 "wifi",
1221 "--project-dir",
1222 dir.to_str().unwrap(),
1223 ])
1224 .unwrap();
1225
1226 let project = read_project_config(&dir).unwrap();
1227 assert!(project
1228 .capabilities
1229 .contains(&fission_command_core::PlatformCapability::Nfc));
1230 assert!(project
1231 .capabilities
1232 .contains(&fission_command_core::PlatformCapability::Biometric));
1233 assert!(project
1234 .capabilities
1235 .contains(&fission_command_core::PlatformCapability::Bluetooth));
1236 assert!(project
1237 .capabilities
1238 .contains(&fission_command_core::PlatformCapability::BarcodeScanner));
1239 assert!(project
1240 .capabilities
1241 .contains(&fission_command_core::PlatformCapability::Camera));
1242 assert!(project
1243 .capabilities
1244 .contains(&fission_command_core::PlatformCapability::Geolocation));
1245 assert!(project
1246 .capabilities
1247 .contains(&fission_command_core::PlatformCapability::Haptics));
1248 assert!(project
1249 .capabilities
1250 .contains(&fission_command_core::PlatformCapability::Microphone));
1251 assert!(project
1252 .capabilities
1253 .contains(&fission_command_core::PlatformCapability::VolumeControl));
1254 assert!(project
1255 .capabilities
1256 .contains(&fission_command_core::PlatformCapability::Wifi));
1257
1258 let android_manifest =
1259 std::fs::read_to_string(dir.join("platforms/android/AndroidManifest.xml")).unwrap();
1260 assert!(android_manifest.contains("android.permission.NFC"));
1261 assert!(android_manifest.contains("android.hardware.nfc"));
1262 assert!(android_manifest.contains("android.permission.USE_BIOMETRIC"));
1263 assert!(android_manifest.contains("android.permission.BLUETOOTH_SCAN"));
1264 assert!(android_manifest.contains("android.permission.BLUETOOTH_CONNECT"));
1265 assert!(android_manifest.contains("android.hardware.bluetooth_le"));
1266 assert!(android_manifest.contains("android.permission.CAMERA"));
1267 assert!(android_manifest.contains("android.hardware.camera.flash"));
1268 assert!(android_manifest.contains("android.permission.ACCESS_FINE_LOCATION"));
1269 assert!(android_manifest.contains("android.permission.VIBRATE"));
1270 assert!(android_manifest.contains("android.permission.RECORD_AUDIO"));
1271 assert!(android_manifest.contains("android.permission.MODIFY_AUDIO_SETTINGS"));
1272 assert!(android_manifest.contains("android.permission.NEARBY_WIFI_DEVICES"));
1273 assert!(android_manifest.contains("android.permission.ACCESS_WIFI_STATE"));
1274 assert!(dir
1275 .join("platforms/android/java/rs/fission/runtime/FissionAndroidCapabilities.java")
1276 .exists());
1277
1278 let ios_info = std::fs::read_to_string(dir.join("platforms/ios/Info.plist")).unwrap();
1279 assert!(ios_info.contains("NFCReaderUsageDescription"));
1280 assert!(ios_info.contains("NSFaceIDUsageDescription"));
1281 assert!(ios_info.contains("NSBluetoothAlwaysUsageDescription"));
1282 assert!(ios_info.contains("NSCameraUsageDescription"));
1283 assert!(ios_info.contains("NSLocationWhenInUseUsageDescription"));
1284 assert!(ios_info.contains("NSMicrophoneUsageDescription"));
1285 let ios_entitlements =
1286 std::fs::read_to_string(dir.join("platforms/ios/Entitlements.plist")).unwrap();
1287 assert!(ios_entitlements.contains("com.apple.developer.nfc.readersession.formats"));
1288 assert!(ios_entitlements.contains("com.apple.developer.networking.wifi-info"));
1289 }
1290
1291 #[test]
1292 fn init_existing_project_preserves_user_files_and_detects_targets() {
1293 let dir = unique_dir("existing");
1294 fs::create_dir_all(dir.join("src")).unwrap();
1295 fs::create_dir_all(dir.join("platforms/web")).unwrap();
1296 fs::write(
1297 dir.join("Cargo.toml"),
1298 "[package]\nname = \"existing-web\"\nversion = \"0.1.0\"\nedition = \"2021\"\n",
1299 )
1300 .unwrap();
1301 fs::write(dir.join("src/main.rs"), "fn main() {}\n").unwrap();
1302 fs::write(dir.join("src/lib.rs"), "pub fn existing() {}\n").unwrap();
1303 fs::write(dir.join("README.md"), "# keep me\n").unwrap();
1304 fs::write(
1305 dir.join("platforms/web/index.html"),
1306 "<!doctype html><title>keep me</title>\n",
1307 )
1308 .unwrap();
1309
1310 run(["fission", "init", dir.to_str().unwrap()]).unwrap();
1311
1312 assert_eq!(
1313 fs::read_to_string(dir.join("README.md")).unwrap(),
1314 "# keep me\n"
1315 );
1316 assert_eq!(
1317 fs::read_to_string(dir.join("src/main.rs")).unwrap(),
1318 "fn main() {}\n"
1319 );
1320 assert_eq!(
1321 fs::read_to_string(dir.join("src/lib.rs")).unwrap(),
1322 "pub fn existing() {}\n"
1323 );
1324 assert_eq!(
1325 fs::read_to_string(dir.join("platforms/web/index.html")).unwrap(),
1326 "<!doctype html><title>keep me</title>\n"
1327 );
1328
1329 let project = read_project_config(&dir).unwrap();
1330 assert_eq!(project.app.name, "existing-web");
1331 assert!(project.targets.contains(&Target::Web));
1332 assert!(project.targets.contains(&Target::Macos));
1333 assert!(project.targets.contains(&Target::Linux));
1334 assert!(project.targets.contains(&Target::Windows));
1335 assert!(dir.join("platforms/web/README.md").exists());
1336 assert!(dir.join("platforms/web/bootstrap.mjs").exists());
1337 assert!(dir.join("assets/app-icon.png").exists());
1338 }
1339
1340 #[test]
1341 fn init_existing_project_is_idempotent() {
1342 let dir = unique_dir("idempotent");
1343 run(["fission", "init", dir.to_str().unwrap(), "--name", "idem"]).unwrap();
1344 let manifest = fs::read_to_string(dir.join("fission.toml")).unwrap();
1345 let main = fs::read_to_string(dir.join("src/main.rs")).unwrap();
1346
1347 run(["fission", "init", dir.to_str().unwrap()]).unwrap();
1348
1349 assert_eq!(
1350 fs::read_to_string(dir.join("fission.toml")).unwrap(),
1351 manifest
1352 );
1353 assert_eq!(fs::read_to_string(dir.join("src/main.rs")).unwrap(), main);
1354 }
1355
1356 #[test]
1357 fn add_target_preserves_existing_target_files() {
1358 let dir = unique_dir("preserve-target");
1359 run([
1360 "fission",
1361 "init",
1362 dir.to_str().unwrap(),
1363 "--name",
1364 "preserve-target",
1365 ])
1366 .unwrap();
1367 fs::create_dir_all(dir.join("platforms/web")).unwrap();
1368 fs::write(
1369 dir.join("platforms/web/index.html"),
1370 "<!doctype html><title>custom</title>\n",
1371 )
1372 .unwrap();
1373 fs::write(dir.join("README.md"), "# custom readme\n").unwrap();
1374
1375 run([
1376 "fission",
1377 "add-target",
1378 "web",
1379 "--project-dir",
1380 dir.to_str().unwrap(),
1381 ])
1382 .unwrap();
1383
1384 assert_eq!(
1385 fs::read_to_string(dir.join("platforms/web/index.html")).unwrap(),
1386 "<!doctype html><title>custom</title>\n"
1387 );
1388 assert_eq!(
1389 fs::read_to_string(dir.join("README.md")).unwrap(),
1390 "# custom readme\n"
1391 );
1392 assert!(dir.join("platforms/web/README.md").exists());
1393 assert!(dir.join("platforms/web/bootstrap.mjs").exists());
1394 let project = read_project_config(&dir).unwrap();
1395 assert!(project.targets.contains(&Target::Web));
1396 }
1397
1398 #[test]
1399 fn cargo_fission_alias_accepts_prefixed_subcommand() {
1400 let dir = unique_dir("cargo-fission");
1401 run([
1402 "cargo-fission",
1403 "fission",
1404 "init",
1405 dir.to_str().unwrap(),
1406 "--name",
1407 "cargo-fission-demo",
1408 ])
1409 .unwrap();
1410
1411 assert!(dir.join("Cargo.toml").exists());
1412 assert!(dir.join("fission.toml").exists());
1413 }
1414
1415 #[test]
1416 fn doctor_command_runs_in_non_strict_mode() {
1417 let dir = unique_dir("doctor");
1418 run(["fission", "init", dir.to_str().unwrap()]).unwrap();
1419 run([
1420 "fission",
1421 "doctor",
1422 "web",
1423 "--project-dir",
1424 dir.to_str().unwrap(),
1425 ])
1426 .unwrap();
1427 }
1428}