1use std::process::Command;
2use std::io::Write;
3use std::fs;
4use std::path::Path;
5use rustbasic_core::colored::*;
6
7
8pub fn build_native_project(run_android: bool, run_desktop: bool) {
9 println!("\n{}", "š RustBasic Native Build Manager".magenta().bold());
10 println!("{}", "---------------------------------".magenta());
11
12 if Path::new("package.json").exists() {
14 println!("\n{}", "š¦ Memulai kompilasi aset frontend (npm run build)...".blue());
15 let npm_cmd = if cfg!(target_os = "windows") { "npm.cmd" } else { "npm" };
16 let status = Command::new(npm_cmd)
17 .args(["run", "build"])
18 .status();
19
20 match status {
21 Ok(s) if s.success() => {
22 println!("{}", "ā
Kompilasi frontend berhasil!".green().bold());
23 }
24 Ok(s) => {
25 println!("{} {}", "ā Error: npm run build keluar dengan kode:".red().bold(), s);
26 println!("{}", "ā ļø Proses build dihentikan karena kompilasi frontend gagal.".yellow());
27 return;
28 }
29 Err(e) => {
30 println!("{} {}", "ā Error: Gagal mengeksekusi 'npm'. Pastikan npm terinstal.".red().bold(), e);
31 return;
32 }
33 }
34 }
35
36 if run_desktop {
37 println!("\n{}", "š ļø Menyiapkan build Desktop Wrapper...".blue());
38 if !Path::new("native/desktop/src/main.rs").exists() {
39 println!("{}", "ā Error: File native/desktop/src/main.rs tidak ditemukan. Jalankan 'rustbasic-native install' terlebih dahulu.".red().bold());
40 return;
41 }
42
43 let mut cmd = Command::new("cargo");
44 cmd.args(["build", "--bin", "rustbasic-desktop", "--release"]);
45 println!("{} {:?}", "š Menjalankan:".blue().bold(), cmd);
46
47 let status = cmd.status();
48 match status {
49 Ok(s) if s.success() => {
50 let bin_name = if cfg!(target_os = "windows") {
51 "rustbasic-desktop.exe"
52 } else {
53 "rustbasic-desktop"
54 };
55 let bin_path = Path::new("target/release").join(bin_name);
56 println!("\nš {}", "Build Desktop Wrapper berhasil!".green().bold());
57 println!("š Hasil executable berada di: {}", bin_path.display().to_string().cyan().bold());
58 }
59 _ => {
60 println!("\nā {}", "Build Desktop Wrapper gagal.".red().bold());
61 }
62 }
63 }
64 if run_android {
65 println!("\n{}", "š ļø Menyiapkan build Android Wrapper...".blue());
66 if !Path::new("native/android/build.gradle").exists() {
67 println!("{}", "ā Error: Folder native/android tidak ditemukan. Jalankan 'rustbasic-native install' terlebih dahulu.".red().bold());
68 return;
69 }
70
71 println!(" JNI shared libraries...");
73 if !compile_jni_libraries() {
74 println!("{}", "ā Error: Gagal mengompilasi JNI libraries.".red().bold());
75 return;
76 }
77
78 let has_java_home = std::env::var("JAVA_HOME").is_ok();
80 let mut custom_java_home = None;
81 if !has_java_home {
82 if cfg!(target_os = "macos") {
84 let mac_studio_jdk = "/Applications/Android Studio.app/Contents/jbr/Contents/Home";
85 if Path::new(mac_studio_jdk).exists() {
86 custom_java_home = Some(mac_studio_jdk.to_string());
87 }
88 } else if cfg!(target_os = "windows") {
89 let win_paths = [
90 "C:\\Program Files\\Android\\Android Studio\\jbr",
91 "C:\\Program Files\\Android\\Android Studio\\jre",
92 ];
93 for path in &win_paths {
94 if Path::new(path).exists() {
95 custom_java_home = Some(path.to_string());
96 break;
97 }
98 }
99 } else {
100 let unix_paths = [
102 "/opt/android-studio/jbr",
103 "/opt/android-studio/jre",
104 "/snap/android-studio/current/jbr",
105 "/snap/android-studio/current/jre",
106 "/usr/local/android-studio/jbr",
107 "/usr/local/android-studio/jre",
108 "/usr/lib/jvm/default-java",
109 ];
110 for path in &unix_paths {
111 if Path::new(path).exists() {
112 custom_java_home = Some(path.to_string());
113 break;
114 }
115 }
116 }
117 }
118
119 let keystore_path = Path::new("native/android/app/release.keystore");
121 if !keystore_path.exists() {
122 println!("š Menghasilkan developer release keystore baru...");
123 let keytool_bin = if let Some(jh) = custom_java_home.as_ref() {
124 let jh_bin = Path::new(jh).join("bin/keytool");
125 if jh_bin.exists() {
126 jh_bin.display().to_string()
127 } else {
128 "keytool".to_string()
129 }
130 } else {
131 "keytool".to_string()
132 };
133
134 let mut keytool_cmd = Command::new(keytool_bin);
135 keytool_cmd.args([
136 "-genkeypair",
137 "-v",
138 "-keystore",
139 "native/android/app/release.keystore",
140 "-alias",
141 "rustbasic",
142 "-keyalg",
143 "RSA",
144 "-keysize",
145 "2048",
146 "-validity",
147 "10000",
148 "-storepass",
149 "rustbasic",
150 "-keypass",
151 "rustbasic",
152 "-dname",
153 "CN=RustBasic Developer, O=RustBasic, C=ID"
154 ]);
155 let _ = keytool_cmd.status();
156 }
157
158 let gradle_path = Path::new("native/android/app/build.gradle");
160 if gradle_path.exists()
161 && let Ok(content) = fs::read_to_string(gradle_path)
162 && !content.contains("signingConfigs") {
163 println!("š Menyematkan konfigurasi tanda tangan (signingConfigs) ke build.gradle...");
164 let updated_content = content
165 .replace(
166 "buildTypes {",
167 "signingConfigs {\n release {\n storeFile file(\"release.keystore\")\n storePassword \"rustbasic\"\n keyAlias \"rustbasic\"\n keyPassword \"rustbasic\"\n }\n }\n\n buildTypes {"
168 )
169 .replace(
170 "buildTypes {\n release {\n minifyEnabled",
171 "buildTypes {\n release {\n signingConfig signingConfigs.release\n minifyEnabled"
172 )
173 .replace(
174 "buildTypes {\r\n release {\r\n minifyEnabled",
175 "buildTypes {\r\n release {\r\n signingConfig signingConfigs.release\r\n minifyEnabled"
176 );
177 let _ = fs::write(gradle_path, updated_content);
178 }
179
180 println!("šØ Memulai kompilasi APK & AAB menggunakan Gradle...");
181 let gradlew_bin = if cfg!(target_os = "windows") { "gradlew.bat" } else { "./gradlew" };
182 let mut gradle_cmd = Command::new(gradlew_bin);
183 gradle_cmd.args(["assembleRelease", "bundleRelease"]);
184 gradle_cmd.current_dir("native/android");
185
186 if let Some(jh) = custom_java_home.as_ref() {
187 gradle_cmd.env("JAVA_HOME", jh);
188 }
189
190 let status = gradle_cmd.status();
191 match status {
192 Ok(s) if s.success() => {
193 println!("\nš {}", "Build Android Wrapper berhasil!".green().bold());
194 println!("š¦ Hasil output:");
195 let apk_signed = "native/android/app/build/outputs/apk/release/app-release.apk";
196 let final_apk = if Path::new(apk_signed).exists() {
197 apk_signed
198 } else {
199 "native/android/app/build/outputs/apk/release/app-release-unsigned.apk"
200 };
201 println!(" - APK: {}", final_apk.cyan().bold());
202 println!(" - AAB: {}", "native/android/app/build/outputs/bundle/release/app-release.aab".cyan().bold());
203 }
204 _ => {
205 println!("\nā {}", "Build Android Wrapper gagal.".red().bold());
206 }
207 }
208 }
209}
210
211
212
213pub fn build_docker(custom_tag: &str) {
215 let docker_check = Command::new("docker")
217 .arg("version")
218 .stdout(std::process::Stdio::null())
219 .stderr(std::process::Stdio::null())
220 .status();
221
222 match docker_check {
223 Ok(status) if status.success() => {}
224 _ => {
225 println!("ā Docker tidak ditemukan! Pastikan Docker sudah terinstall dan berjalan.");
226 println!(" Install Docker: https://docs.docker.com/get-docker/");
227 return;
228 }
229 }
230
231 let dockerfile_path = Path::new("Dockerfile");
233 if !dockerfile_path.exists() {
234 println!("š Membuat Dockerfile...");
235 let dockerfile_content = r#"# ============================================================
236# RustBasic Docker Build ā Multi-stage
237# ============================================================
238
239# Stage 1: Builder
240FROM rust:1-slim-bookworm AS builder
241
242RUN apt-get update && apt-get install -y \
243 pkg-config libssl-dev \
244 && rm -rf /var/lib/apt/lists/*
245
246WORKDIR /build
247
248# Copy rustbasic-core (dari konteks workspace root)
249COPY rustbasic-core /build/rustbasic-core
250
251# Copy proyek utama rustbasic
252COPY rustbasic /build/rustbasic
253
254WORKDIR /build/rustbasic
255
256# Build release binary
257RUN cargo build --release --bin rustbasic
258
259# Stage 2: Runtime
260FROM debian:bookworm-slim
261
262RUN apt-get update && apt-get install -y \
263 ca-certificates libssl3 \
264 && rm -rf /var/lib/apt/lists/*
265
266WORKDIR /app
267
268# Copy binary dari builder stage
269COPY --from=builder /build/rustbasic/target/release/rustbasic .
270
271# Copy assets yang diperlukan dari builder stage (lebih aman dan bersih)
272COPY --from=builder /build/rustbasic/src/resources/views/ src/resources/views/
273COPY --from=builder /build/rustbasic/src/dist/ src/dist/
274COPY --from=builder /build/rustbasic/public/ public/
275COPY --from=builder /build/rustbasic/database/migrations/ database/migrations/
276COPY --from=builder /build/rustbasic/database/seeders/ database/seeders/
277COPY --from=builder /build/rustbasic/.env.example .env
278
279# Expose port aplikasi
280EXPOSE 4000
281
282CMD ["./rustbasic"]
283"#;
284 if let Err(e) = fs::write(dockerfile_path, dockerfile_content) {
285 println!("ā Gagal membuat Dockerfile: {}", e);
286 return;
287 }
288 println!("ā
Dockerfile berhasil dibuat.");
289 }
290
291 let app_name = std::env::var("BUILD_NAME")
293 .or_else(|_| std::env::var("APP_NAME"))
294 .unwrap_or_else(|_| "rustbasic".to_string())
295 .to_lowercase();
296
297 let image_tag = if custom_tag.is_empty() {
298 format!("{}:latest", app_name)
299 } else {
300 custom_tag.to_string()
301 };
302
303 let core_context = if std::path::Path::new("../rustbasic-core").exists() {
304 "core=../rustbasic-core"
305 } else {
306 "core=."
307 };
308
309 println!("\nš³ Memulai Docker build...");
311 println!(" Image tag: {}", image_tag);
312 println!(" Running: docker build --build-context {} -t {} .", core_context, image_tag);
313
314 let mut cmd = Command::new("docker")
315 .args(["build", "--build-context", core_context, "-t", &image_tag, "."])
316 .stdin(std::process::Stdio::inherit())
317 .stdout(std::process::Stdio::inherit())
318 .stderr(std::process::Stdio::inherit())
319 .spawn()
320 .expect("Gagal menjalankan docker build");
321
322 let status = cmd.wait().expect("Gagal menunggu docker build");
323
324 if status.success() {
325 println!("\nā
Docker build selesai dengan sukses!");
326 println!("š¦ Image: {}", image_tag);
327 println!("\n Jalankan container (Development/Lokal):");
328 println!(" docker run -p 4000:4000 --env-file .env {}", image_tag);
329 println!("\n Jalankan container (Produksi/Server - Auto Restart):");
330 println!(" docker run -d -p 80:4000 --restart unless-stopped --env-file .env {}", image_tag);
331 } else {
332 println!("\nā Docker build gagal.");
333 }
334}
335
336pub fn build_interactive(args: &[String]) {
338 let mut build_docker_flag = args.iter().any(|arg| arg == "--docker");
339 let mut build_desktop = args.iter().any(|arg| arg == "--desktop");
340 let mut build_android = args.iter().any(|arg| arg == "--android");
341 let mut release_mode = args.iter().any(|arg| arg == "--release" || arg == "-r");
342 let mut target_type = String::new(); let mut docker_tag = String::new();
344
345 for i in 0..args.len() {
347 if args[i] == "--type" && i + 1 < args.len() {
348 target_type = args[i+1].to_lowercase();
349 }
350 if args[i] == "--tag" && i + 1 < args.len() {
351 docker_tag = args[i+1].clone();
352 }
353 }
354
355 if !build_docker_flag && !build_desktop && !build_android {
356 let is_native_installed = crate::packages::read_manifest()
357 .packages
358 .iter()
359 .any(|pkg| pkg.name == "rustbasic-native");
360
361 println!("š ļø RustBasic Build CLI");
362 println!("Pilih platform target untuk di-build:");
363 println!(" [1] Docker (Container Image)");
364
365 let max_choice = if is_native_installed {
366 println!(" [2] Desktop Wrapper (Windows, macOS, Linux)");
367 println!(" [3] Android Wrapper (APK, AAB)");
368 3
369 } else {
370 1
371 };
372
373 let prompt_str = format!("š Pilih nomor platform (1-{}): ", max_choice);
374 let choice = crate::utils::prompt_choice(&prompt_str, 1, max_choice);
375 match choice {
376 1 => build_docker_flag = true,
377 2 => build_desktop = true,
378 3 => build_android = true,
379 _ => {}
380 }
381 }
382
383 if build_docker_flag {
384 build_docker(&docker_tag);
385 } else if build_desktop {
386 let mut target_os = String::new();
387 for i in 0..args.len() {
388 if args[i] == "--os" && i + 1 < args.len() {
389 target_os = args[i+1].clone();
390 }
391 }
392
393 let mut target_triple = "";
394 if target_os.is_empty() {
395 println!("\nPilih OS Target Desktop:");
396 println!(" [1] Current OS (Sistem saat ini)");
397 println!(" [2] macOS Intel (x86_64)");
398 println!(" [3] macOS Apple Silicon (aarch64)");
399 println!(" [4] Windows (x86_64)");
400 println!(" [5] Linux (x86_64)");
401 let choice = crate::utils::prompt_choice("š Pilih nomor target OS (1-5): ", 1, 5);
402 match choice {
403 2 => target_triple = "x86_64-apple-darwin",
404 3 => target_triple = "aarch64-apple-darwin",
405 4 => target_triple = "x86_64-pc-windows-msvc",
406 5 => target_triple = "x86_64-unknown-linux-gnu",
407 _ => {}
408 }
409 } else {
410 match target_os.as_str() {
411 "macos-intel" | "macos_intel" => target_triple = "x86_64-apple-darwin",
412 "macos-silicon" | "macos_silicon" => target_triple = "aarch64-apple-darwin",
413 "macos" => {
414 #[cfg(target_arch = "aarch64")]
415 { target_triple = "aarch64-apple-darwin"; }
416 #[cfg(not(target_arch = "aarch64"))]
417 { target_triple = "x86_64-apple-darwin"; }
418 }
419 "windows" => target_triple = "x86_64-pc-windows-msvc",
420 "linux" => target_triple = "x86_64-unknown-linux-gnu",
421 _ => {
422 println!("ā ļø Warning: Target OS '{}' tidak dikenal, menggunakan default OS saat ini.", target_os);
423 }
424 }
425 }
426
427 if !args.iter().any(|arg| arg == "--release" || arg == "-r" || arg == "--debug" || arg == "-d") {
428 println!("\nPilih Mode Build:");
429 println!(" [1] Debug (Cepat compile)");
430 println!(" [2] Release (Optimasi penuh)");
431 let choice = crate::utils::prompt_choice("š Pilih nomor mode (1-2): ", 1, 2);
432 if choice == 2 {
433 release_mode = true;
434 }
435 }
436
437 println!("\nš„ļø Memulai proses build Desktop...");
438 let mut build_args = vec!["build", "--bin", "rustbasic-desktop"];
439 if release_mode {
440 build_args.push("--release");
441 }
442 if !target_triple.is_empty() {
443 build_args.push("--target");
444 build_args.push(target_triple);
445 let _ = Command::new("rustup")
446 .args(["target", "add", target_triple])
447 .status();
448 }
449
450 println!(" Running: cargo {}", build_args.join(" "));
451 let mut cmd = Command::new("cargo")
452 .args(&build_args)
453 .stdin(std::process::Stdio::inherit())
454 .stdout(std::process::Stdio::inherit())
455 .stderr(std::process::Stdio::inherit())
456 .spawn()
457 .expect("Gagal menjalankan cargo build");
458
459 let status = cmd.wait().expect("Gagal menunggu proses cargo build");
460 if status.success() {
461 println!("\nā
Build Desktop selesai dengan sukses!");
462 let mode_str = if release_mode { "release" } else { "debug" };
463 let path_str = if target_triple.is_empty() {
464 format!("target/{}/rustbasic-desktop", mode_str)
465 } else {
466 format!("target/{}/{}/rustbasic-desktop", target_triple, mode_str)
467 };
468 println!("š Output biner: {}", path_str);
469 } else {
470 println!("\nā Build Desktop gagal.");
471 }
472
473 } else if build_android {
474 let mut is_aab = false;
475 if target_type.is_empty() {
476 println!("\nPilih Format Output Android:");
477 println!(" [1] APK (Android Package - Siap install)");
478 println!(" [2] AAB (Android App Bundle - Siap Google Play)");
479 let choice = crate::utils::prompt_choice("š Pilih format (1-2): ", 1, 2);
480 if choice == 2 {
481 is_aab = true;
482 }
483 } else {
484 is_aab = target_type == "aab";
485 }
486
487 if !args.iter().any(|arg| arg == "--release" || arg == "-r" || arg == "--debug" || arg == "-d") {
488 println!("\nPilih Mode Build:");
489 println!(" [1] Debug");
490 println!(" [2] Release (Produksi)");
491 let choice = crate::utils::prompt_choice("š Pilih nomor mode (1-2): ", 1, 2);
492 if choice == 2 {
493 release_mode = true;
494 }
495 }
496
497 println!("\nšØ Membangun JNI library untuk Android...");
498 if !compile_jni_libraries() {
499 println!("ā Gagal membangun JNI libraries.");
500 return;
501 }
502
503 let os = std::env::consts::OS;
505 let home = std::env::var("HOME").unwrap_or_default();
506 let android_home = if let Ok(val) = std::env::var("ANDROID_HOME") {
507 val
508 } else {
509 if os == "macos" {
510 format!("{}/Library/Android/sdk", home)
511 } else {
512 format!("{}/Android/Sdk", home)
513 }
514 };
515 unsafe {
516 std::env::set_var("ANDROID_HOME", &android_home);
517 }
518
519 if std::env::var("JAVA_HOME").is_err() {
520 let studio_jbr = if os == "macos" {
521 let mut jbr = "/Applications/Android Studio.app/Contents/jbr/Contents/Home".to_string();
522 if !Path::new(&jbr).exists() {
523 jbr = "/Applications/Android Studio.app/Contents/jre/Contents/Home".to_string();
524 }
525 jbr
526 } else {
527 let mut jbr = "/opt/android-studio/jbr".to_string();
528 if !Path::new(&jbr).exists() {
529 jbr = "/usr/local/android-studio/jbr".to_string();
530 }
531 jbr
532 };
533 if Path::new(&studio_jbr).exists() {
534 unsafe {
535 std::env::set_var("JAVA_HOME", &studio_jbr);
536 }
537 }
538 }
539
540 let local_props = Path::new("native/android/local.properties");
541 if !local_props.exists()
542 && let Ok(mut file) = fs::File::create(local_props) {
543 let _ = writeln!(file, "sdk.dir={}", android_home);
544 }
545
546 let gradle_task = match (is_aab, release_mode) {
547 (false, false) => "assembleDebug",
548 (false, true) => "assembleRelease",
549 (true, false) => "bundleDebug",
550 (true, true) => "bundleRelease",
551 };
552
553 println!("\nšØ Membangun target Android via Gradle (task: {})...", gradle_task);
554
555 let mut build_cmd = if Path::new("native/android/gradlew").exists() {
556 let mut cmd = Command::new("./gradlew");
557 cmd.arg(gradle_task);
558 cmd.current_dir("native/android");
559 cmd
560 } else {
561 let mut cmd = Command::new("gradle");
562 cmd.arg(gradle_task);
563 cmd.current_dir("native/android");
564 cmd
565 };
566
567 let spawn_res = build_cmd
568 .stdin(std::process::Stdio::inherit())
569 .stdout(std::process::Stdio::inherit())
570 .stderr(std::process::Stdio::inherit())
571 .spawn();
572
573 if let Ok(mut child) = spawn_res {
574 let status = child.wait().expect("Gagal menunggu Gradle build");
575 if status.success() {
576 println!("\nā
Build Android selesai dengan sukses!");
577 let output_dir = if is_aab {
578 let mode_folder = if release_mode { "release" } else { "debug" };
579 format!("native/android/app/build/outputs/bundle/{}", mode_folder)
580 } else {
581 let mode_folder = if release_mode { "release" } else { "debug" };
582 format!("native/android/app/build/outputs/apk/{}", mode_folder)
583 };
584 println!("š Folder output: {}", output_dir);
585 } else {
586 println!("\nā Gradle build gagal.");
587 }
588 } else {
589 println!("ā Gagal mengeksekusi Gradle wrapper. Pastikan Java dan Gradle wrapper terkonfigurasi dengan benar.");
590 }
591 }
592}
593
594pub fn run_native(run_android: bool, run_desktop: bool) {
596 if run_android {
597 println!("š Memulai RustBasic Android Wrapper...");
598
599 let os = std::env::consts::OS;
600 let home = std::env::var("HOME").unwrap_or_default();
601 let android_home = if let Ok(val) = std::env::var("ANDROID_HOME") {
602 val
603 } else {
604 if os == "macos" {
605 format!("{}/Library/Android/sdk", home)
606 } else {
607 format!("{}/Android/Sdk", home)
608 }
609 };
610
611 let mut custom_java_home = None;
612 if cfg!(target_os = "macos") {
614 let mac_studio_jdk = "/Applications/Android Studio.app/Contents/jbr/Contents/Home";
615 if Path::new(mac_studio_jdk).exists() {
616 custom_java_home = Some(mac_studio_jdk.to_string());
617 }
618 } else if cfg!(target_os = "windows") {
619 let win_paths = [
620 "C:\\Program Files\\Android\\Android Studio\\jbr",
621 "C:\\Program Files\\Android\\Android Studio\\jre",
622 ];
623 for path in &win_paths {
624 if Path::new(path).exists() {
625 custom_java_home = Some(path.to_string());
626 break;
627 }
628 }
629 } else {
630 let unix_paths = [
632 "/opt/android-studio/jbr",
633 "/opt/android-studio/jre",
634 "/snap/android-studio/current/jbr",
635 "/snap/android-studio/current/jre",
636 "/usr/local/android-studio/jbr",
637 "/usr/local/android-studio/jre",
638 "/usr/lib/jvm/default-java",
639 ];
640 for path in &unix_paths {
641 if Path::new(path).exists() {
642 custom_java_home = Some(path.to_string());
643 break;
644 }
645 }
646 }
647
648 let mut devices = get_adb_devices();
649 if devices.is_empty() {
650 println!("š± Perangkat Android atau emulator tidak terdeteksi aktif.");
651 let emulator_bin = format!("{}/emulator/emulator", android_home);
652 if Path::new(&emulator_bin).exists() {
653 let avd_output = Command::new(&emulator_bin).arg("-list-avds").output();
654 if let Ok(avd_out) = avd_output {
655 let avds_str = String::from_utf8_lossy(&avd_out.stdout);
656 if let Some(avd_name) = avds_str.lines().next() {
657 println!("š Menyalakan emulator AVD: {}...", avd_name);
658 let _ = Command::new(&emulator_bin)
659 .args(["-avd", avd_name])
660 .stdout(std::process::Stdio::null())
661 .stderr(std::process::Stdio::null())
662 .spawn();
663
664 println!("ā³ Menunggu emulator menyala dan terdeteksi adb...");
665 let _ = Command::new("adb").arg("wait-for-device").status();
666 println!("ā
Emulator berhasil aktif!");
667 std::thread::sleep(std::time::Duration::from_secs(3));
668 devices = get_adb_devices();
669 }
670 }
671 }
672 }
673
674 let (device_id, device_name) = if devices.len() == 1 {
675 let d = devices[0].clone();
676 println!("š± Menggunakan perangkat tunggal: {} ({})", d.1, d.0);
677 d
678 } else if devices.len() > 1 {
679 println!("š± Terdeteksi beberapa perangkat Android. Silakan pilih target:");
680 for (idx, d) in devices.iter().enumerate() {
681 println!(" [{}] {} ({})", idx + 1, d.1, d.0);
682 }
683 let choice = crate::utils::prompt_choice("š Pilih nomor perangkat: ", 1, devices.len());
684 devices[choice - 1].clone()
685 } else {
686 println!("ā Error: Tidak ada perangkat Android terhubung.");
687 return;
688 };
689
690 if !compile_jni_libraries() {
692 return;
693 }
694
695 let local_props = Path::new("native/android/local.properties");
697 if !local_props.exists() {
698 if let Ok(mut file) = fs::File::create(local_props) {
699 let _ = writeln!(file, "sdk.dir={}", android_home);
700 }
701 }
702
703 println!("šØ Membangun debug APK menggunakan Gradle...");
705 let gradlew_bin = if cfg!(target_os = "windows") { "gradlew.bat" } else { "./gradlew" };
706 let mut gradle_cmd = Command::new(gradlew_bin);
707 gradle_cmd.arg("assembleDebug");
708 gradle_cmd.current_dir("native/android");
709
710 if let Some(jh) = &custom_java_home {
711 gradle_cmd.env("JAVA_HOME", jh);
712 }
713
714 let gradle_status = gradle_cmd.status();
715 if gradle_status.is_err() || !gradle_status.unwrap().success() {
716 println!("ā Gradle build assembleDebug gagal.");
717 return;
718 }
719
720 println!("šØ Memasang APK ke perangkat {} ({})...", device_name, device_id);
722 let install_status = Command::new("adb")
723 .args(["-s", &device_id, "install", "-r", "native/android/app/build/outputs/apk/debug/app-debug.apk"])
724 .status();
725
726 if install_status.is_err() || !install_status.unwrap().success() {
727 println!("ā Gagal memasang APK ke device.");
728 return;
729 }
730
731 let vite_port = "5173"; let reverse_status = Command::new("adb")
734 .args(["-s", &device_id, "reverse", &format!("tcp:{}", vite_port), &format!("tcp:{}", vite_port)])
735 .status();
736 if reverse_status.is_err() {
737 println!("ā ļø Warning: Gagal melakukan adb reverse port {}", vite_port);
738 }
739
740 println!("š Membuka aplikasi di perangkat {}...", device_name);
742 let _ = Command::new("adb")
743 .args(["-s", &device_id, "logcat", "-c"])
744 .status();
745
746 let _ = Command::new("adb")
747 .args(["-s", &device_id, "shell", "am", "start", "-n", "com.rustbasic.mobile/com.rustbasic.mobile.MainActivity"])
748 .status();
749
750 println!("š Menampilkan log realtime dari perangkat {} (Tekan Ctrl+C untuk keluar)...", device_name);
751 let mut logcat_cmd = Command::new("adb");
752 logcat_cmd.args(["-s", &device_id, "logcat", "-s", "RustBasicServer"]);
753 let mut child = logcat_cmd.spawn().expect("Gagal menjalankan adb logcat");
754 let _ = child.wait();
755 } else if run_desktop {
756 println!("š Memulai RustBasic Desktop Wrapper...");
757 let mut cmd = Command::new("cargo");
758 cmd.args(["run", "--bin", "rustbasic-desktop"]);
759 let status = cmd.status();
760 match status {
761 Ok(s) if s.success() => {}
762 _ => {
763 println!("ā Gagal menjalankan Desktop Wrapper.");
764 }
765 }
766 }
767}
768
769fn prompt_string(prompt: &str, default: &str) -> String {
770 print!("{}", prompt);
771 let _ = std::io::stdout().flush();
772 let mut input = String::new();
773 if std::io::stdin().read_line(&mut input).is_ok() {
774 let trimmed = input.trim();
775 if trimmed.is_empty() {
776 default.to_string()
777 } else {
778 trimmed.to_string()
779 }
780 } else {
781 default.to_string()
782 }
783}
784
785pub fn deploy_interactive() {
786 println!("\n{}", "š RustBasic Docker Deploy CLI".magenta().bold());
787 println!("{}", "------------------------------".magenta());
788
789 let image_name = prompt_string("š Masukkan Nama/Tag Docker Image (default: rustbasic:latest): ", "rustbasic:latest");
791
792 let container_name = std::env::var("DOCKER_CONTAINER_NAME").unwrap_or_else(|_| {
794 let base_name = image_name
795 .split(':')
796 .next()
797 .unwrap_or("rustbasic")
798 .split('/')
799 .last()
800 .unwrap_or("rustbasic")
801 .to_lowercase();
802 format!("{}-app", base_name)
803 });
804
805 let inspect = Command::new("docker")
807 .args(["image", "inspect", &image_name])
808 .stdout(std::process::Stdio::null())
809 .stderr(std::process::Stdio::null())
810 .status();
811
812 match inspect {
813 Ok(status) if status.success() => {}
814 _ => {
815 println!("{}", format!("ā ļø Peringatan: Image '{}' tidak ditemukan lokal.", image_name).yellow());
816 let proceed = prompt_string("š Tetap lanjutkan proses ekspor? (y/n) [default: n]: ", "n");
817 if proceed.to_lowercase() != "y" {
818 println!("ā Proses dihentikan.");
819 return;
820 }
821 }
822 }
823
824 println!("š¦ Mengekspor Docker image '{}' ke 'rustbasic.tar'...", image_name);
826 let save_status = Command::new("docker")
827 .args(["save", "-o", "rustbasic.tar", &image_name])
828 .status();
829
830 match save_status {
831 Ok(status) if status.success() => {
832 println!("{}", "ā
Image berhasil diekspor ke rustbasic.tar.".green());
833 }
834 _ => {
835 println!("{}", "ā Gagal mengekspor Docker image.".red().bold());
836 return;
837 }
838 }
839
840 println!("\n{}", "š Konfigurasi Pengiriman ke Server".cyan().bold());
842 println!("{}", "-----------------------------------".cyan());
843 let ssh_user = prompt_string("š Masukkan SSH Username Server (contoh: root) [default: root]: ", "root");
844 let ssh_ip = prompt_string("š Masukkan IP Address Server: ", "");
845 if ssh_ip.is_empty() {
846 println!("{}", "ā IP Address server tidak boleh kosong.".red().bold());
847 let _ = fs::remove_file("rustbasic.tar");
848 return;
849 }
850 let ssh_port = prompt_string("š Masukkan SSH Port Server (default: 22): ", "22");
851 let dest_dir = prompt_string("š Masukkan Folder Tujuan di Server (default: ~/app): ", "~/app");
852 let server_port = prompt_string("š Masukkan Port Server Mapping (contoh: 80:4000) [default: 80:4000]: ", "80:4000");
853 let env_file = prompt_string("š Masukkan File Env yang akan dikirim (default: .env): ", ".env");
854
855 println!("\nš Menyiapkan folder tujuan di server...");
856 let mkdir_status = Command::new("ssh")
857 .args([
858 "-p", &ssh_port,
859 &format!("{}@{}", ssh_user, ssh_ip),
860 &format!("mkdir -p {}", dest_dir)
861 ])
862 .status();
863
864 match mkdir_status {
865 Ok(status) if status.success() => {}
866 _ => {
867 println!("{}", "ā Gagal terhubung ke server menggunakan SSH.".red().bold());
868 let _ = fs::remove_file("rustbasic.tar");
869 return;
870 }
871 }
872
873 println!("š Mengirimkan berkas rustbasic.tar & {} ke server...", env_file);
874 let scp_status = Command::new("scp")
875 .args([
876 "-P", &ssh_port,
877 "rustbasic.tar", &env_file,
878 &format!("{}@{}:{}", ssh_user, ssh_ip, dest_dir)
879 ])
880 .status();
881
882 let _ = fs::remove_file("rustbasic.tar");
884
885 if let Ok(status) = scp_status {
886 if !status.success() {
887 println!("{}", "ā Gagal mengirimkan berkas via SCP.".red().bold());
888 return;
889 }
890 } else {
891 println!("{}", "ā Gagal menjalankan SCP.".red().bold());
892 return;
893 }
894
895 println!("{}", "ā
Pengiriman berkas berhasil!".green());
896
897 let auto_run = prompt_string("\nš Apakah Anda ingin langsung menjalankan container di server secara otomatis? (y/n) [default: y]: ", "y");
899 if auto_run.to_lowercase() == "y" || auto_run.is_empty() {
900 println!("\nš Memuat image di server (docker load)...");
901 let load_status = Command::new("ssh")
902 .args([
903 "-p", &ssh_port,
904 &format!("{}@{}", ssh_user, ssh_ip),
905 &format!("docker load -i {}/rustbasic.tar", dest_dir)
906 ])
907 .status();
908
909 match load_status {
910 Ok(status) if status.success() => {
911 println!("{}", "ā
Image berhasil dimuat di server.".green());
912 }
913 _ => {
914 println!("{}", "ā Gagal memuat image di server.".red().bold());
915 return;
916 }
917 }
918
919 println!("š Menghentikan & menghapus container lama '{}' jika ada...", container_name);
920 let stop_status = Command::new("ssh")
921 .args([
922 "-p", &ssh_port,
923 &format!("{}@{}", ssh_user, ssh_ip),
924 &format!("docker stop {} || true && docker rm {} || true", container_name, container_name)
925 ])
926 .status();
927
928 if let Err(e) = stop_status {
929 println!("ā ļø Peringatan saat membersihkan container lama: {}", e);
930 }
931
932 println!("š Menjalankan container baru '{}'...", container_name);
933 let run_cmd = format!(
934 "docker run -d --name {} -p {} --restart unless-stopped --env-file {}/.env {}",
935 container_name, server_port, dest_dir, image_name
936 );
937 let run_status = Command::new("ssh")
938 .args([
939 "-p", &ssh_port,
940 &format!("{}@{}", ssh_user, ssh_ip),
941 &run_cmd
942 ])
943 .status();
944
945 match run_status {
946 Ok(status) if status.success() => {
947 println!("{}", format!("š Container '{}' berhasil dijalankan di server!", container_name).green().bold());
948 }
949 _ => {
950 println!("{}", "ā Gagal menjalankan container di server.".red().bold());
951 return;
952 }
953 }
954
955 println!("š Membersihkan file tar di server...");
956 let rm_status = Command::new("ssh")
957 .args([
958 "-p", &ssh_port,
959 &format!("{}@{}", ssh_user, ssh_ip),
960 &format!("rm {}/rustbasic.tar", dest_dir)
961 ])
962 .status();
963
964 if let Err(e) = rm_status {
965 println!("ā ļø Peringatan saat membersihkan file tar di server: {}", e);
966 }
967
968 println!("\n{}", "š Deployment selesai!".green().bold());
969 println!("{}", "--------------------------------------------------------".green());
970 println!("Untuk melihat log aplikasi di server, jalankan:");
971 println!("ssh -p {} {}@{} \"docker logs -f {}\"", ssh_port, ssh_user, ssh_ip, container_name);
972 println!("{}", "--------------------------------------------------------".green());
973 } else {
974 println!("\n{}", "š„ļø Langkah Selanjutnya di Server Anda:".cyan().bold());
975 println!("{}", "--------------------------------------------------------".green());
976 println!("1. Hubungkan ke server via SSH:");
977 println!(" ssh -p {} {}@{}", ssh_port, ssh_user, ssh_ip);
978 println!("");
979 println!("2. Masuk ke folder tujuan:");
980 println!(" cd {}", dest_dir);
981 println!("");
982 println!("3. Muat (load) image Docker dari berkas tar:");
983 println!(" docker load -i rustbasic.tar");
984 println!("");
985 println!("4. Jalankan container dengan fitur auto-restart:");
986 println!(" docker run -d --name {} -p {} --restart unless-stopped --env-file .env {}", container_name, server_port, image_name);
987 println!("");
988 println!("5. Hapus file tar di server untuk menghemat disk:");
989 println!(" rm rustbasic.tar");
990 println!("{}", "--------------------------------------------------------".green());
991 }
992}
993
994fn get_adb_devices() -> Vec<(String, String)> {
995 let output = Command::new("adb").arg("devices").output();
996 let mut devices = Vec::new();
997 if let Ok(out) = output {
998 let stdout = String::from_utf8_lossy(&out.stdout);
999 for line in stdout.lines() {
1000 let line = line.trim();
1001 if line.is_empty() || line.starts_with("List of devices") {
1002 continue;
1003 }
1004 let parts: Vec<&str> = line.split_whitespace().collect();
1005 if parts.len() >= 2 && parts[1] == "device" {
1006 let device_id = parts[0].to_string();
1007 let model_out = Command::new("adb")
1008 .args(["-s", &device_id, "shell", "getprop", "ro.product.model"])
1009 .output();
1010 let model = if let Ok(m_out) = model_out {
1011 String::from_utf8_lossy(&m_out.stdout).trim().to_string()
1012 } else {
1013 "Unknown Device".to_string()
1014 };
1015 devices.push((device_id, model));
1016 }
1017 }
1018 }
1019 devices
1020}
1021
1022fn compile_jni_libraries() -> bool {
1023 println!("š Building Rust library for Android (Native Rust implementation)...");
1024
1025 let _ = Command::new("rustup")
1026 .args(["target", "add", "aarch64-linux-android", "armv7-linux-androideabi", "x86_64-linux-android"])
1027 .status();
1028
1029 let home = std::env::var("HOME").unwrap_or_default();
1030 let android_ndk_home = if let Ok(val) = std::env::var("ANDROID_NDK_HOME") {
1031 val
1032 } else {
1033 let mac_ndk = format!("{}/Library/Android/sdk/ndk", home);
1034 if Path::new(&mac_ndk).exists() {
1035 if let Ok(entries) = fs::read_dir(&mac_ndk) {
1036 let mut paths: Vec<_> = entries.flatten().map(|e| e.path()).collect();
1037 paths.sort();
1038 if let Some(highest) = paths.last() {
1039 highest.display().to_string()
1040 } else {
1041 "".to_string()
1042 }
1043 } else {
1044 "".to_string()
1045 }
1046 } else {
1047 "".to_string()
1048 }
1049 };
1050
1051 if android_ndk_home.is_empty() {
1052 println!("ā Error: ANDROID_NDK_HOME is not set. Please set ANDROID_NDK_HOME.");
1053 return false;
1054 }
1055
1056 println!("Using NDK: {}", android_ndk_home);
1057
1058 let os = std::env::consts::OS;
1059 let toolchain_sub = if os == "macos" { "darwin-x86_64" } else { "linux-x86_64" };
1060 let toolchain_bin_path = Path::new(&android_ndk_home)
1061 .join("toolchains/llvm/prebuilt")
1062 .join(toolchain_sub)
1063 .join("bin");
1064
1065 if !toolchain_bin_path.exists() {
1066 println!("ā Error: Toolchain bin path not found: {}", toolchain_bin_path.display());
1067 return false;
1068 }
1069
1070 let sqlite_version = "3450100";
1071 let sqlite_dir = format!("target/sqlite-amalgamation-{}", sqlite_version);
1072 if !Path::new(&sqlite_dir).exists() {
1073 println!("š„ Downloading SQLite source amalgamation...");
1074 fs::create_dir_all("target").ok();
1075
1076 let zip_path = "target/sqlite.zip";
1077 let sqlite_url = format!("https://www.sqlite.org/2024/sqlite-amalgamation-{}.zip", sqlite_version);
1078
1079 let curl_status = Command::new("curl")
1080 .args(["-sSLo", zip_path, &sqlite_url])
1081 .status();
1082
1083 if curl_status.is_err() || !curl_status.unwrap().success() {
1084 println!("ā Gagal men-download SQLite source.");
1085 return false;
1086 }
1087
1088 let unzip_status = Command::new("unzip")
1089 .args(["-q", zip_path, "-d", "target/"])
1090 .status();
1091
1092 let _ = fs::remove_file(zip_path);
1093
1094 if unzip_status.is_err() || !unzip_status.unwrap().success() {
1095 println!("ā Gagal mengekstrak SQLite source.");
1096 return false;
1097 }
1098 }
1099
1100 let targets = vec![
1101 ("aarch64-linux-android", "arm64-v8a", "aarch64-linux-android21-clang"),
1102 ("armv7-linux-androideabi", "armeabi-v7a", "armv7a-linux-androideabi21-clang"),
1103 ("x86_64-linux-android", "x86_64", "x86_64-linux-android21-clang"),
1104 ];
1105
1106 let jnilibs_dir = "native/android/app/src/main/jniLibs";
1107
1108 for (target, arch, clang_name) in targets {
1109 println!("šØ Preparing SQLite static library for {}...", target);
1110
1111 let clang_path = toolchain_bin_path.join(clang_name);
1112 let ar_path = toolchain_bin_path.join("llvm-ar");
1113
1114 if !clang_path.exists() {
1115 println!("ā Error: Compiler not found: {}", clang_path.display());
1116 return false;
1117 }
1118
1119 let sqlite_out = format!("target/{}/sqlite", target);
1120 fs::create_dir_all(&sqlite_out).ok();
1121
1122 let libsqlite3_a = format!("{}/libsqlite3.a", sqlite_out);
1123 if !Path::new(&libsqlite3_a).exists() {
1124 println!(" Compiling SQLite static lib for {}...", target);
1125 let sqlite3_o = format!("{}/sqlite3.o", sqlite_out);
1126 let sqlite3_c = format!("{}/sqlite3.c", sqlite_dir);
1127
1128 let compile_status = Command::new(&clang_path)
1129 .args(["-O2", "-c", &sqlite3_c, "-o", &sqlite3_o])
1130 .status();
1131
1132 if compile_status.is_err() || !compile_status.unwrap().success() {
1133 println!("ā Gagal mengompilasi sqlite3.o");
1134 return false;
1135 }
1136
1137 let archive_status = Command::new(&ar_path)
1138 .args(["rcs", &libsqlite3_a, &sqlite3_o])
1139 .status();
1140
1141 if archive_status.is_err() || !archive_status.unwrap().success() {
1142 println!("ā Gagal mengarsip libsqlite3.a");
1143 return false;
1144 }
1145 }
1146
1147 println!("šØ Compiling Rust library for {}...", target);
1148 let mut cargo_cmd = Command::new("cargo");
1149 cargo_cmd.args(["build", "--target", target, "--release"]);
1150
1151 let clang_path_str = clang_path.display().to_string();
1152 let ar_path_str = ar_path.display().to_string();
1153
1154 let target_upper = target.replace("-", "_").to_uppercase();
1155 let linker_env = format!("CARGO_TARGET_{}_LINKER", target_upper);
1156 let cc_env = format!("CC_{}", target.replace("-", "_"));
1157 let ar_env = format!("AR_{}", target.replace("-", "_"));
1158
1159 cargo_cmd.env(&linker_env, &clang_path_str);
1160 cargo_cmd.env(&cc_env, &clang_path_str);
1161 cargo_cmd.env(&ar_env, &ar_path_str);
1162
1163 let cargo_status = cargo_cmd.status();
1164 if cargo_status.is_err() || !cargo_status.unwrap().success() {
1165 println!("ā Gagal mengompilasi library Rust untuk target {}", target);
1166 return false;
1167 }
1168
1169 let dest_dir = format!("{}/{}", jnilibs_dir, arch);
1170 fs::create_dir_all(&dest_dir).ok();
1171
1172 let src_so = format!("target/{}/release/librustbasic.so", target);
1173 let dest_so = format!("{}/librustbasic_mobile.so", dest_dir);
1174
1175 if let Err(e) = fs::copy(&src_so, &dest_so) {
1176 println!("ā Gagal menyalin {}: {}", src_so, e);
1177 return false;
1178 }
1179 }
1180
1181 println!("ā
Android JNI libraries built successfully!");
1182 true
1183}