Skip to main content

rustbasic_cli/
builder.rs

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    // 1. Jalankan npm run build untuk Frontend
13    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        // Jalankan JNI compilation
72        println!(" JNI shared libraries...");
73        if !compile_jni_libraries() {
74            println!("{}", "āŒ Error: Gagal mengompilasi JNI libraries.".red().bold());
75            return;
76        }
77
78        // Tentukan JAVA_HOME jika belum diatur
79        let has_java_home = std::env::var("JAVA_HOME").is_ok();
80        let mut custom_java_home = None;
81        if !has_java_home {
82            // Coba deteksi Android Studio JDK di berbagai platform
83            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                // Linux & other Unix-like OS
101                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        // 1. Generate Keystore if not exists
120        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        // 2. Inject signingConfigs into build.gradle if not already present
159        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
213/// Build Docker image — auto-generate Dockerfile jika belum ada
214pub fn build_docker(custom_tag: &str) {
215    // 1. Cek apakah docker tersedia
216    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    // 2. Generate Dockerfile jika belum ada
232    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    // 3. Tentukan image tag
292    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    // 4. Build Docker image dengan named context 'core' untuk rustbasic-core
310    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
336/// Unified interactive build entry point — menampilkan menu untuk memilih target build
337pub 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(); // apk / aab
343    let mut docker_tag = String::new();
344
345    // Parse arguments
346    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        // Setup Android home and SDK environments
504        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
594/// Jalankan native runner script (Android / Desktop)
595pub 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        // Coba deteksi Android Studio JDK di berbagai platform
613        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            // Linux & other Unix-like OS
631            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        // 3. build JNI
691        if !compile_jni_libraries() {
692            return;
693        }
694
695        // 4. local.properties
696        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        // 5. gradlew assembleDebug
704        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        // 6. adb install
721        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        // 7. adb reverse
732        let vite_port = "5173"; // default
733        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        // 8. adb shell am start
741        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    // 1. Konfigurasi Image & Pengiriman
790    let image_name = prompt_string("šŸ‘‰ Masukkan Nama/Tag Docker Image (default: rustbasic:latest): ", "rustbasic:latest");
791    
792    // Tentukan nama container dari env DOCKER_CONTAINER_NAME atau fallback dari nama image
793    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    // Cek apakah Docker image sudah ada
806    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    // 2. Ekspor ke tar
825    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    // 3. Konfigurasi Pengiriman
841    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    // Hapus file tar lokal
883    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    // 4. Eksekusi SSH otomatis di server jika disetujui
898    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}