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
7fn setup_java_home() {
8    if std::env::var("JAVA_HOME").is_err() {
9        let mut custom_java_home: Option<String> = None;
10        if cfg!(target_os = "macos") {
11            let paths = vec![
12                "/Applications/Android Studio.app/Contents/jbr/Contents/Home",
13                "/Applications/Android Studio.app/Contents/jre/Contents/Home",
14                "/Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home",
15            ];
16            for path in &paths {
17                if std::path::Path::new(path).exists() {
18                    custom_java_home = Some(path.to_string());
19                    break;
20                }
21            }
22        } else if cfg!(target_os = "windows") {
23            let win_paths = [
24                "C:\\Program Files\\Android\\Android Studio\\jbr",
25                "C:\\Program Files\\Android\\Android Studio\\jre",
26            ];
27            for path in &win_paths {
28                if std::path::Path::new(path).exists() {
29                    custom_java_home = Some(path.to_string());
30                    break;
31                }
32            }
33        } else {
34            // Linux & other Unix-like OS
35            let unix_paths = [
36                "/opt/android-studio/jbr",
37                "/opt/android-studio/jre",
38                "/snap/android-studio/current/jbr",
39                "/snap/android-studio/current/jre",
40                "/usr/local/android-studio/jbr",
41                "/usr/local/android-studio/jre",
42                "/usr/lib/jvm/default-java",
43            ];
44            for path in &unix_paths {
45                if std::path::Path::new(path).exists() {
46                    custom_java_home = Some(path.to_string());
47                    break;
48                }
49            }
50        }
51        if let Some(jh) = custom_java_home {
52            unsafe {
53                std::env::set_var("JAVA_HOME", &jh);
54            }
55        }
56    }
57}
58
59fn get_cargo_package_name() -> String {
60    if let Ok(content) = fs::read_to_string("Cargo.toml") {
61        for line in content.lines() {
62            let line = line.trim();
63            if line.starts_with("name") {
64                let parts: Vec<&str> = line.split('=').collect();
65                if parts.len() >= 2 {
66                    return parts[1].trim().trim_matches('"').trim_matches('\'').to_string();
67                }
68            }
69        }
70    }
71    "rustbasic".to_string()
72}
73
74pub fn build_native_project(run_android: bool, run_desktop: bool) {
75    println!("\n{}", "šŸš€ RustBasic Native Build Manager".magenta().bold());
76    println!("{}", "---------------------------------".magenta());
77
78    // 1. Jalankan npm run build untuk Frontend
79    if Path::new("package.json").exists() {
80        println!("\n{}", "šŸ“¦ Memulai kompilasi aset frontend (npm run build)...".blue());
81        let npm_cmd = if cfg!(target_os = "windows") { "npm.cmd" } else { "npm" };
82        let status = Command::new(npm_cmd)
83            .args(["run", "build"])
84            .status();
85
86        match status {
87            Ok(s) if s.success() => {
88                println!("{}", "āœ… Kompilasi frontend berhasil!".green().bold());
89            }
90            Ok(s) => {
91                println!("{} {}", "āŒ Error: npm run build keluar dengan kode:".red().bold(), s);
92                println!("{}", "āš ļø  Proses build dihentikan karena kompilasi frontend gagal.".yellow());
93                return;
94            }
95            Err(e) => {
96                println!("{} {}", "āŒ Error: Gagal mengeksekusi 'npm'. Pastikan npm terinstal.".red().bold(), e);
97                return;
98            }
99        }
100    }
101
102    if run_desktop {
103        println!("\n{}", "šŸ› ļø  Menyiapkan build Desktop Wrapper...".blue());
104        if !Path::new("native/desktop/src/main.rs").exists() {
105            println!("{}", "āŒ Error: File native/desktop/src/main.rs tidak ditemukan. Jalankan 'rustbasic-native install' terlebih dahulu.".red().bold());
106            return;
107        }
108
109        let mut cmd = Command::new("cargo");
110        cmd.args(["build", "--bin", "rustbasic-desktop", "--release", "--features", "desktop"]);
111        println!("{} {:?}", "šŸš€ Menjalankan:".blue().bold(), cmd);
112        
113        let status = cmd.status();
114        match status {
115            Ok(s) if s.success() => {
116                let bin_name = if cfg!(target_os = "windows") {
117                    "rustbasic-desktop.exe"
118                } else {
119                    "rustbasic-desktop"
120                };
121                let bin_path = Path::new("target/release").join(bin_name);
122                println!("\nšŸŽ‰ {}", "Build Desktop Wrapper berhasil!".green().bold());
123                println!("šŸš€ Hasil executable berada di: {}", bin_path.display().to_string().cyan().bold());
124            }
125            _ => {
126                println!("\nāŒ {}", "Build Desktop Wrapper gagal.".red().bold());
127            }
128        }
129    }
130    if run_android {
131        println!("\n{}", "šŸ› ļø  Menyiapkan build Android Wrapper...".blue());
132        if !Path::new("native/android/build.gradle").exists() {
133            println!("{}", "āŒ Error: Folder native/android tidak ditemukan. Jalankan 'rustbasic-native install' terlebih dahulu.".red().bold());
134            return;
135        }
136
137        // Jalankan JNI compilation
138        println!(" JNI shared libraries...");
139        if !compile_jni_libraries() {
140            println!("{}", "āŒ Error: Gagal mengompilasi JNI libraries.".red().bold());
141            return;
142        }
143
144        setup_java_home();
145        let jh_val = std::env::var("JAVA_HOME").ok();
146
147        // 1. Generate Keystore if not exists
148        let keystore_path = Path::new("native/android/app/release.keystore");
149        if !keystore_path.exists() {
150            println!("šŸ”‘ Menghasilkan developer release keystore baru...");
151            let keytool_bin = if let Some(jh) = jh_val.as_ref() {
152                let jh_bin = Path::new(jh).join("bin/keytool");
153                if jh_bin.exists() {
154                    jh_bin.display().to_string()
155                } else {
156                    "keytool".to_string()
157                }
158            } else {
159                "keytool".to_string()
160            };
161
162            let mut keytool_cmd = Command::new(keytool_bin);
163            keytool_cmd.args([
164                "-genkeypair",
165                "-v",
166                "-keystore",
167                "native/android/app/release.keystore",
168                "-alias",
169                "rustbasic",
170                "-keyalg",
171                "RSA",
172                "-keysize",
173                "2048",
174                "-validity",
175                "10000",
176                "-storepass",
177                "rustbasic",
178                "-keypass",
179                "rustbasic",
180                "-dname",
181                "CN=RustBasic Developer, O=RustBasic, C=ID"
182            ]);
183            let _ = keytool_cmd.status();
184        }
185
186        // 2. Inject signingConfigs into build.gradle if not already present
187        let gradle_path = Path::new("native/android/app/build.gradle");
188        if gradle_path.exists()
189            && let Ok(content) = fs::read_to_string(gradle_path)
190                && !content.contains("signingConfigs") {
191                    println!("šŸ“ Menyematkan konfigurasi tanda tangan (signingConfigs) ke build.gradle...");
192                    let updated_content = content
193                        .replace(
194                            "buildTypes {",
195                            "signingConfigs {\n        release {\n            storeFile file(\"release.keystore\")\n            storePassword \"rustbasic\"\n            keyAlias \"rustbasic\"\n            keyPassword \"rustbasic\"\n        }\n    }\n\n    buildTypes {"
196                        )
197                        .replace(
198                            "buildTypes {\n        release {\n            minifyEnabled",
199                            "buildTypes {\n        release {\n            signingConfig signingConfigs.release\n            minifyEnabled"
200                        )
201                        .replace(
202                            "buildTypes {\r\n        release {\r\n            minifyEnabled",
203                            "buildTypes {\r\n        release {\r\n            signingConfig signingConfigs.release\r\n            minifyEnabled"
204                        );
205                    let _ = fs::write(gradle_path, updated_content);
206                }
207
208        println!("šŸ”Ø Memulai kompilasi APK & AAB menggunakan Gradle...");
209        let gradlew_bin = if cfg!(target_os = "windows") { "gradlew.bat" } else { "./gradlew" };
210        let mut gradle_cmd = Command::new(gradlew_bin);
211        gradle_cmd.args(["assembleRelease", "bundleRelease"]);
212        gradle_cmd.current_dir("native/android");
213
214        if let Some(jh) = jh_val.as_ref() {
215            gradle_cmd.env("JAVA_HOME", jh);
216        }
217
218        let status = gradle_cmd.status();
219        match status {
220            Ok(s) if s.success() => {
221                println!("\nšŸŽ‰ {}", "Build Android Wrapper berhasil!".green().bold());
222                println!("šŸ“¦ Hasil output:");
223                let apk_signed = "native/android/app/build/outputs/apk/release/app-release.apk";
224                let final_apk = if Path::new(apk_signed).exists() {
225                    apk_signed
226                } else {
227                    "native/android/app/build/outputs/apk/release/app-release-unsigned.apk"
228                };
229                println!("   - APK: {}", final_apk.cyan().bold());
230                println!("   - AAB: {}", "native/android/app/build/outputs/bundle/release/app-release.aab".cyan().bold());
231            }
232            _ => {
233                println!("\nāŒ {}", "Build Android Wrapper gagal.".red().bold());
234            }
235        }
236    }
237}
238
239/// Build Docker image — auto-generate Dockerfile jika belum ada
240pub fn build_docker(custom_tag: &str, extract_binary: bool) {
241    // 1. Cek apakah docker tersedia
242    let docker_check = Command::new("docker")
243        .arg("version")
244        .stdout(std::process::Stdio::null())
245        .stderr(std::process::Stdio::null())
246        .status();
247
248    match docker_check {
249        Ok(status) if status.success() => {}
250        _ => {
251            println!("āŒ Docker tidak ditemukan! Pastikan Docker sudah terinstall dan berjalan.");
252            println!("   Install Docker: https://docs.docker.com/get-docker/");
253            return;
254        }
255    }
256
257    // 2. Generate Dockerfile jika belum ada
258    let dockerfile_path = Path::new("Dockerfile");
259    if !dockerfile_path.exists() {
260        println!("šŸ“ Membuat Dockerfile...");
261        let is_monorepo = Path::new("../rustbasic-core").exists() || Path::new("rustbasic-core").exists();
262        let binary_name = get_cargo_package_name();
263
264        let dockerfile_content = if is_monorepo {
265            r#"# ============================================================
266# RustBasic Docker Build — Standalone (Cached)
267# ============================================================
268
269# Stage 1: Builder
270FROM rust:1-slim-bookworm AS builder
271
272RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
273    --mount=type=cache,target=/var/lib/apt,sharing=locked \
274    apt-get update && apt-get install -y \
275    pkg-config libssl-dev
276
277# Copy rustbasic-core (dari konteks workspace root)
278WORKDIR /rustbasic-core
279COPY --from=core . .
280
281# Copy proyek utama rustbasic
282WORKDIR /build
283COPY . .
284
285# Build release binary using Cargo registry, git cache, and target cache
286ENV CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse
287RUN --mount=type=cache,target=/usr/local/cargo/registry \
288    --mount=type=cache,target=/usr/local/cargo/git \
289    --mount=type=cache,target=/build/target \
290    cargo build --release --bin rustbasic && \
291    cp target/release/rustbasic /build/rustbasic-bin
292
293# Stage 2: Runtime
294FROM debian:bookworm-slim
295
296RUN apt-get update && apt-get install -y \
297    ca-certificates libssl3 \
298    && rm -rf /var/lib/apt/lists/*
299
300WORKDIR /app
301
302# Copy binary dari builder stage
303COPY --from=builder /build/rustbasic-bin ./rustbasic
304
305# Copy assets yang diperlukan dari builder stage (lebih aman dan bersih)
306COPY --from=builder /build/src/resources/views/ src/resources/views/
307COPY --from=builder /build/src/dist/ src/dist/
308COPY --from=builder /build/public/ public/
309COPY --from=builder /build/database/ database/
310COPY --from=builder /build/.env.example .env
311
312# Expose port aplikasi
313EXPOSE 4000
314
315CMD ["./rustbasic"]
316"#.to_string()
317        } else {
318            format!(r#"# ============================================================
319# RustBasic Docker Build — Standalone (Cached)
320# ============================================================
321
322# Stage 1: Builder
323FROM rust:1-slim-bookworm AS builder
324
325RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
326    --mount=type=cache,target=/var/lib/apt,sharing=locked \
327    apt-get update && apt-get install -y \
328    pkg-config libssl-dev
329
330WORKDIR /build
331
332COPY . .
333
334# Build release binary using Cargo registry, git cache, and target cache
335ENV CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse
336RUN --mount=type=cache,target=/usr/local/cargo/registry \
337    --mount=type=cache,target=/usr/local/cargo/git \
338    --mount=type=cache,target=/build/target \
339    cargo build --release --bin {bin_name} && \
340    cp target/release/{bin_name} /build/{bin_name}-bin
341
342# Stage 2: Runtime
343FROM debian:bookworm-slim
344
345RUN apt-get update && apt-get install -y \
346    ca-certificates libssl3 \
347    && rm -rf /var/lib/apt/lists/*
348
349WORKDIR /app
350
351# Copy binary dari builder stage
352COPY --from=builder /build/{bin_name}-bin ./{bin_name}
353
354# Copy assets yang diperlukan dari builder stage
355COPY --from=builder /build/src/resources/views/ src/resources/views/
356COPY --from=builder /build/src/dist/ src/dist/
357COPY --from=builder /build/public/ public/
358COPY --from=builder /build/database/ database/
359COPY --from=builder /build/.env.example .env
360
361# Expose port aplikasi
362EXPOSE 4000
363
364CMD ["./{bin_name}"]
365"#, bin_name = binary_name)
366        };
367
368        if let Err(e) = fs::write(dockerfile_path, dockerfile_content) {
369            println!("āŒ Gagal membuat Dockerfile: {}", e);
370            return;
371        }
372        println!("āœ… Dockerfile berhasil dibuat.");
373    }
374
375    // 3. Tentukan image tag
376    let app_name = std::env::var("BUILD_NAME")
377        .or_else(|_| std::env::var("APP_NAME"))
378        .unwrap_or_else(|_| get_cargo_package_name())
379        .to_lowercase();
380    
381    let image_tag = if custom_tag.is_empty() {
382        format!("{}:latest", app_name)
383    } else {
384        custom_tag.to_string()
385    };
386
387    let core_context = if std::path::Path::new("../rustbasic-core").exists() {
388        "core=../rustbasic-core"
389    } else {
390        "core=."
391    };
392
393    // Prompt Platform Target CPU Docker
394    let mut docker_platform = String::new();
395    println!("\nPilih Platform Target CPU Docker:");
396    println!("  [1] Current Host Platform (Sesuai OS komputer Anda)");
397    println!("  [2] Linux AMD64 / x86_64 (Standard VPS Intel/AMD - Umum/Rekomendasi)");
398    println!("  [3] Linux ARM64 / aarch64 (Server berbasis ARM / AWS Graviton)");
399    match crate::utils::prompt_choice("šŸ‘‰ Pilih (1-3): ", 1, 3) {
400        2 => docker_platform = "linux/amd64".to_string(),
401        3 => docker_platform = "linux/arm64".to_string(),
402        _ => {}
403    }
404
405    // Peringatan Arsitektur CPU Mismatch
406    let host_arch = get_host_arch();
407    let is_mismatch = (host_arch == "aarch64" && docker_platform == "linux/amd64")
408        || (host_arch == "x86_64" && docker_platform == "linux/arm64");
409
410    if is_mismatch {
411        println!("\nāš ļø  {}", "PERINGATAN: Arsitektur CPU Mismatch (Sangat Lambat)".yellow().bold());
412        println!("   Anda berada di host dengan CPU '{}' tetapi memilih target Docker '{}'.", host_arch, docker_platform);
413        println!("   Docker akan menggunakan emulasi CPU (QEMU) yang membuat proses kompilasi");
414        println!("   Rust berjalan {} (bisa memakan waktu 10-30 menit).", "10x-20x LEBIH LAMBAT".red().bold());
415        println!("   ");
416        println!("   šŸ’” {} Kami telah mengaktifkan target caching untuk mempercepat build.", "TIPS:".cyan().bold());
417        println!("      Build pertama tetap lambat, namun build berikutnya akan sangat cepat (1-2 menit)");
418        println!("      karena target directory dan dependency cache disimpan oleh Docker.");
419        println!("   ");
420        let proceed = prompt_string("šŸ‘‰ Apakah Anda ingin melanjutkan proses build? (y/n) [default: y]: ", "y");
421        if !proceed.to_lowercase().starts_with('y') {
422            println!("āŒ Build dibatalkan oleh pengguna.");
423            return;
424        }
425    }
426
427    // 4. Build Docker image dengan named context 'core' untuk rustbasic-core
428    println!("\n🐳 Memulai Docker build...");
429    println!("   Image tag: {}", image_tag);
430
431    let mut build_args = vec![
432        "build".to_string(),
433        "--build-context".to_string(),
434        core_context.to_string(),
435        "--progress=plain".to_string(),
436    ];
437    
438    if !docker_platform.is_empty() {
439        build_args.push("--platform".to_string());
440        build_args.push(docker_platform.clone());
441        println!("   Platform: {}", docker_platform);
442    }
443    
444    build_args.push("-t".to_string());
445    build_args.push(image_tag.clone());
446    build_args.push(".".to_string());
447
448    println!("   Running: docker {}", build_args.join(" "));
449
450    let mut cmd = Command::new("docker")
451        .args(&build_args)
452        .stdin(std::process::Stdio::inherit())
453        .stdout(std::process::Stdio::inherit())
454        .stderr(std::process::Stdio::inherit())
455        .spawn()
456        .expect("Gagal menjalankan docker build");
457
458    let status = cmd.wait().expect("Gagal menunggu docker build");
459
460    if status.success() {
461        println!("\nāœ… Docker build selesai dengan sukses!");
462        println!("šŸ“¦ Image: {}", image_tag);
463
464        if extract_binary {
465            println!("šŸ“¦ Mengekstrak biner Linux dari image Docker...");
466            let container_name = format!("temp-extract-{}", std::time::SystemTime::now()
467                .duration_since(std::time::UNIX_EPOCH)
468                .unwrap_or_default()
469                .as_secs());
470            
471            // docker create
472            let create_res = Command::new("docker")
473                .args(["create", "--name", &container_name, &image_tag])
474                .status();
475
476            if create_res.is_ok() && create_res.unwrap().success() {
477                // create build-output directory
478                let _ = std::fs::create_dir_all("build-output");
479                
480                // Get cargo package name (which is the binary name inside container)
481                let binary_name = get_cargo_package_name();
482
483                // docker cp
484                let cp_status = Command::new("docker")
485                    .args(["cp", &format!("{}:/app/{}", container_name, binary_name), &format!("build-output/{}", binary_name)])
486                    .status();
487                    
488                // docker rm
489                let _ = Command::new("docker")
490                    .args(["rm", &container_name])
491                    .status();
492                    
493                if cp_status.is_ok() && cp_status.unwrap().success() {
494                    println!("āœ… Biner berhasil diekstrak ke: {}", format!("build-output/{}", binary_name).cyan().bold());
495                } else {
496                    println!("āŒ Gagal mengekstrak biner dari container.");
497                }
498            } else {
499                println!("āŒ Gagal membuat temporary container untuk ekstraksi.");
500            }
501        }
502
503        println!("\n   Jalankan container (Development/Lokal):");
504        println!("   docker run -p 4000:4000 --env-file .env {}", image_tag);
505        println!("\n   Jalankan container (Produksi/Server - Auto Restart):");
506        println!("   docker run -d -p 80:4000 --restart unless-stopped --env-file .env {}", image_tag);
507    } else {
508        println!("\nāŒ Docker build gagal.");
509    }
510}
511
512/// Unified interactive build entry point — menampilkan menu untuk memilih target build
513pub fn build_interactive(args: &[String]) {
514    let mut build_docker_flag = args.iter().any(|arg| arg == "--docker");
515    let mut build_desktop = args.iter().any(|arg| arg == "--desktop");
516    let mut build_android = args.iter().any(|arg| arg == "--android");
517    let mut release_mode = args.iter().any(|arg| arg == "--release" || arg == "-r");
518    let mut target_type = String::new(); // apk / aab
519    let mut docker_tag = String::new();
520    let extract_binary = args.iter().any(|arg| arg == "--extract" || arg == "-e");
521
522    // Parse arguments
523    for i in 0..args.len() {
524        if args[i] == "--type" && i + 1 < args.len() {
525            target_type = args[i+1].to_lowercase();
526        }
527        if args[i] == "--tag" && i + 1 < args.len() {
528            docker_tag = args[i+1].clone();
529        }
530    }
531
532    if !build_docker_flag && !build_desktop && !build_android {
533        let is_native_installed = crate::packages::read_manifest()
534            .packages
535            .iter()
536            .any(|pkg| pkg.name == "rustbasic-native");
537
538        println!("šŸ› ļø  RustBasic Build CLI");
539        println!("Pilih platform target untuk di-build:");
540        println!("  [1] Docker (Container Image)");
541        
542        let max_choice = if is_native_installed {
543            println!("  [2] Desktop Wrapper (Windows, macOS, Linux)");
544            println!("  [3] Android Wrapper (APK, AAB)");
545            3
546        } else {
547            1
548        };
549
550        let prompt_str = format!("šŸ‘‰ Pilih nomor platform (1-{}): ", max_choice);
551        let choice = crate::utils::prompt_choice(&prompt_str, 1, max_choice);
552        match choice {
553            1 => build_docker_flag = true,
554            2 => build_desktop = true,
555            3 => build_android = true,
556            _ => {}
557        }
558    }
559
560    if build_docker_flag {
561        let mut extract = extract_binary;
562        if !extract && !args.iter().any(|arg| arg == "--docker") {
563            let extract_choice = prompt_string("šŸ‘‰ Apakah Anda ingin mengekstrak biner Linux hasil build ke folder './build-output'? (y/n) [default: n]: ", "n");
564            extract = extract_choice.to_lowercase().starts_with('y');
565        }
566        build_docker(&docker_tag, extract);
567    } else if build_desktop {
568        let mut target_os = String::new();
569        for i in 0..args.len() {
570            if args[i] == "--os" && i + 1 < args.len() {
571                target_os = args[i+1].clone();
572            }
573        }
574
575        let mut target_triple = "";
576        if target_os.is_empty() {
577            println!("\nPilih OS Target Desktop:");
578            println!("  [1] Current OS (Sistem saat ini)");
579            println!("  [2] macOS Intel (x86_64)");
580            println!("  [3] macOS Apple Silicon (aarch64)");
581            println!("  [4] Windows MSVC (x86_64-pc-windows-msvc)");
582            println!("  [5] Windows GNU (x86_64-pc-windows-gnu - Rekomendasi Cross-compile dari macOS/Linux)");
583            println!("  [6] Linux (x86_64-unknown-linux-gnu)");
584            let choice = crate::utils::prompt_choice("šŸ‘‰ Pilih nomor target OS (1-6): ", 1, 6);
585            match choice {
586                2 => target_triple = "x86_64-apple-darwin",
587                3 => target_triple = "aarch64-apple-darwin",
588                4 => target_triple = "x86_64-pc-windows-msvc",
589                5 => target_triple = "x86_64-pc-windows-gnu",
590                6 => target_triple = "x86_64-unknown-linux-gnu",
591                _ => {}
592            }
593        } else {
594            match target_os.as_str() {
595                "macos-intel" | "macos_intel" => target_triple = "x86_64-apple-darwin",
596                "macos-silicon" | "macos_silicon" => target_triple = "aarch64-apple-darwin",
597                "macos" => {
598                    #[cfg(target_arch = "aarch64")]
599                    { target_triple = "aarch64-apple-darwin"; }
600                    #[cfg(not(target_arch = "aarch64"))]
601                    { target_triple = "x86_64-apple-darwin"; }
602                }
603                "windows" => target_triple = "x86_64-pc-windows-msvc",
604                "windows-gnu" => target_triple = "x86_64-pc-windows-gnu",
605                "linux" => target_triple = "x86_64-unknown-linux-gnu",
606                _ => {
607                    println!("āš ļø Warning: Target OS '{}' tidak dikenal, menggunakan default OS saat ini.", target_os);
608                }
609            }
610        }
611
612        if !args.iter().any(|arg| arg == "--release" || arg == "-r" || arg == "--debug" || arg == "-d") {
613            println!("\nPilih Mode Build:");
614            println!("  [1] Debug (Cepat compile)");
615            println!("  [2] Release (Optimasi penuh)");
616            let choice = crate::utils::prompt_choice("šŸ‘‰ Pilih nomor mode (1-2): ", 1, 2);
617            if choice == 2 {
618                release_mode = true;
619            }
620        }
621
622        println!("\nšŸ–„ļø  Memulai proses build Desktop...");
623        let mut build_args = vec!["build", "--bin", "rustbasic-desktop", "--features", "desktop"];
624        if release_mode {
625            build_args.push("--release");
626        }
627        if !target_triple.is_empty() {
628            build_args.push("--target");
629            build_args.push(target_triple);
630            let _ = Command::new("rustup")
631                .args(["target", "add", target_triple])
632                .status();
633        }
634
635        println!("   Running: cargo {}", build_args.join(" "));
636        let mut cmd = Command::new("cargo")
637            .args(&build_args)
638            .stdin(std::process::Stdio::inherit())
639            .stdout(std::process::Stdio::inherit())
640            .stderr(std::process::Stdio::inherit())
641            .spawn()
642            .expect("Gagal menjalankan cargo build");
643        
644        let status = cmd.wait().expect("Gagal menunggu proses cargo build");
645        if status.success() {
646            println!("\nāœ… Build Desktop selesai dengan sukses!");
647            let mode_str = if release_mode { "release" } else { "debug" };
648            let path_str = if target_triple.is_empty() {
649                format!("target/{}/rustbasic-desktop", mode_str)
650            } else {
651                format!("target/{}/{}/rustbasic-desktop", target_triple, mode_str)
652            };
653            println!("šŸ“‚ Output biner: {}", path_str);
654        } else {
655            println!("\nāŒ Build Desktop gagal.");
656        }
657
658    } else if build_android {
659        let mut is_aab = false;
660        if target_type.is_empty() {
661            println!("\nPilih Format Output Android:");
662            println!("  [1] APK (Android Package - Siap install)");
663            println!("  [2] AAB (Android App Bundle - Siap Google Play)");
664            let choice = crate::utils::prompt_choice("šŸ‘‰ Pilih format (1-2): ", 1, 2);
665            if choice == 2 {
666                is_aab = true;
667            }
668        } else {
669            is_aab = target_type == "aab";
670        }
671
672        if !args.iter().any(|arg| arg == "--release" || arg == "-r" || arg == "--debug" || arg == "-d") {
673            println!("\nPilih Mode Build:");
674            println!("  [1] Debug");
675            println!("  [2] Release (Produksi)");
676            let choice = crate::utils::prompt_choice("šŸ‘‰ Pilih nomor mode (1-2): ", 1, 2);
677            if choice == 2 {
678                release_mode = true;
679            }
680        }
681
682        println!("\nšŸ”Ø Membangun JNI library untuk Android...");
683        if !compile_jni_libraries() {
684            println!("āŒ Gagal membangun JNI libraries.");
685            return;
686        }
687
688        // Setup Android home and SDK environments
689        let os = std::env::consts::OS;
690        let home = std::env::var("HOME").unwrap_or_default();
691        let android_home = if let Ok(val) = std::env::var("ANDROID_HOME") {
692            val
693        } else {
694            if os == "macos" {
695                format!("{}/Library/Android/sdk", home)
696            } else {
697                format!("{}/Android/Sdk", home)
698            }
699        };
700        unsafe {
701            std::env::set_var("ANDROID_HOME", &android_home);
702        }
703        
704        if std::env::var("JAVA_HOME").is_err() {
705            let studio_jbr = if os == "macos" {
706                let mut jbr = "/Applications/Android Studio.app/Contents/jbr/Contents/Home".to_string();
707                if !Path::new(&jbr).exists() {
708                    jbr = "/Applications/Android Studio.app/Contents/jre/Contents/Home".to_string();
709                }
710                jbr
711            } else {
712                let mut jbr = "/opt/android-studio/jbr".to_string();
713                if !Path::new(&jbr).exists() {
714                    jbr = "/usr/local/android-studio/jbr".to_string();
715                }
716                jbr
717            };
718            if Path::new(&studio_jbr).exists() {
719                unsafe {
720                    std::env::set_var("JAVA_HOME", &studio_jbr);
721                }
722            }
723        }
724
725        let local_props = Path::new("native/android/local.properties");
726        if !local_props.exists()
727            && let Ok(mut file) = fs::File::create(local_props) {
728                let _ = writeln!(file, "sdk.dir={}", android_home);
729            }
730
731        let gradle_task = match (is_aab, release_mode) {
732            (false, false) => "assembleDebug",
733            (false, true) => "assembleRelease",
734            (true, false) => "bundleDebug",
735            (true, true) => "bundleRelease",
736        };
737
738        println!("\nšŸ”Ø Membangun target Android via Gradle (task: {})...", gradle_task);
739        
740        let mut build_cmd = if Path::new("native/android/gradlew").exists() {
741            let mut cmd = Command::new("./gradlew");
742            cmd.arg(gradle_task);
743            cmd.current_dir("native/android");
744            cmd
745        } else {
746            let mut cmd = Command::new("gradle");
747            cmd.arg(gradle_task);
748            cmd.current_dir("native/android");
749            cmd
750        };
751
752        let spawn_res = build_cmd
753            .stdin(std::process::Stdio::inherit())
754            .stdout(std::process::Stdio::inherit())
755            .stderr(std::process::Stdio::inherit())
756            .spawn();
757
758        if let Ok(mut child) = spawn_res {
759            let status = child.wait().expect("Gagal menunggu Gradle build");
760            if status.success() {
761                println!("\nāœ… Build Android selesai dengan sukses!");
762                let output_dir = if is_aab {
763                    let mode_folder = if release_mode { "release" } else { "debug" };
764                    format!("native/android/app/build/outputs/bundle/{}", mode_folder)
765                } else {
766                    let mode_folder = if release_mode { "release" } else { "debug" };
767                    format!("native/android/app/build/outputs/apk/{}", mode_folder)
768                };
769                println!("šŸ“‚ Folder output: {}", output_dir);
770            } else {
771                println!("\nāŒ Gradle build gagal.");
772            }
773        } else {
774            println!("āŒ Gagal mengeksekusi Gradle wrapper. Pastikan Java dan Gradle wrapper terkonfigurasi dengan benar.");
775        }
776    }
777}
778
779/// Jalankan native runner script (Android / Desktop)
780pub fn run_native(run_android: bool, run_desktop: bool) {
781    if run_android {
782        println!("šŸš€ Memulai RustBasic Android Wrapper...");
783        
784        let os = std::env::consts::OS;
785        let home = std::env::var("HOME").unwrap_or_default();
786        let android_home = if let Ok(val) = std::env::var("ANDROID_HOME") {
787            val
788        } else {
789            if os == "macos" {
790                format!("{}/Library/Android/sdk", home)
791            } else {
792                format!("{}/Android/Sdk", home)
793            }
794        };
795
796        let mut custom_java_home = None;
797        // Coba deteksi Android Studio JDK di berbagai platform
798        if cfg!(target_os = "macos") {
799            let mac_studio_jdk = "/Applications/Android Studio.app/Contents/jbr/Contents/Home";
800            if Path::new(mac_studio_jdk).exists() {
801                custom_java_home = Some(mac_studio_jdk.to_string());
802            }
803        } else if cfg!(target_os = "windows") {
804            let win_paths = [
805                "C:\\Program Files\\Android\\Android Studio\\jbr",
806                "C:\\Program Files\\Android\\Android Studio\\jre",
807            ];
808            for path in &win_paths {
809                if Path::new(path).exists() {
810                    custom_java_home = Some(path.to_string());
811                    break;
812                }
813            }
814        } else {
815            // Linux & other Unix-like OS
816            let unix_paths = [
817                "/opt/android-studio/jbr",
818                "/opt/android-studio/jre",
819                "/snap/android-studio/current/jbr",
820                "/snap/android-studio/current/jre",
821                "/usr/local/android-studio/jbr",
822                "/usr/local/android-studio/jre",
823                "/usr/lib/jvm/default-java",
824            ];
825            for path in &unix_paths {
826                if Path::new(path).exists() {
827                    custom_java_home = Some(path.to_string());
828                    break;
829                }
830            }
831        }
832
833        let adb = get_adb_bin();
834        let mut devices = get_adb_devices();
835        if devices.is_empty() {
836            println!("šŸ“± Perangkat Android atau emulator tidak terdeteksi aktif.");
837            let mut emulator_bin = format!("{}/emulator/emulator", android_home);
838            if !Path::new(&emulator_bin).exists() {
839                let fallback = format!("{}/tools/emulator", android_home);
840                if Path::new(&fallback).exists() {
841                    emulator_bin = fallback;
842                } else {
843                    emulator_bin = "emulator".to_string();
844                }
845            }
846
847            let avd_output = Command::new(&emulator_bin).arg("-list-avds").output();
848            if let Ok(avd_out) = avd_output {
849                let avds_str = String::from_utf8_lossy(&avd_out.stdout);
850                if let Some(avd_name) = avds_str.lines().next() {
851                    println!("šŸš€ Menyalakan emulator AVD: {}...", avd_name);
852                    let _ = Command::new(&emulator_bin)
853                        .args(["-avd", avd_name])
854                        .stdout(std::process::Stdio::null())
855                        .stderr(std::process::Stdio::null())
856                        .spawn();
857                    
858                    println!("ā³ Menunggu emulator menyala dan terdeteksi adb...");
859                    let _ = Command::new(&adb).arg("wait-for-device").status();
860                    println!("āœ… Emulator berhasil aktif!");
861                    std::thread::sleep(std::time::Duration::from_secs(3));
862                    devices = get_adb_devices();
863                }
864            }
865        }
866
867        let (device_id, device_name) = if devices.len() == 1 {
868            let d = devices[0].clone();
869            println!("šŸ“± Menggunakan perangkat tunggal: {} ({})", d.1, d.0);
870            d
871        } else if devices.len() > 1 {
872            println!("šŸ“± Terdeteksi beberapa perangkat Android. Silakan pilih target:");
873            for (idx, d) in devices.iter().enumerate() {
874                println!("  [{}] {} ({})", idx + 1, d.1, d.0);
875            }
876            let choice = crate::utils::prompt_choice("šŸ‘‰ Pilih nomor perangkat: ", 1, devices.len());
877            devices[choice - 1].clone()
878        } else {
879            println!("āŒ Error: Tidak ada perangkat Android terhubung.");
880            return;
881        };
882
883        // 3. build JNI
884        if !compile_jni_libraries() {
885            return;
886        }
887
888        // 4. local.properties
889        let local_props = Path::new("native/android/local.properties");
890        if !local_props.exists() {
891            if let Ok(mut file) = fs::File::create(local_props) {
892                let _ = writeln!(file, "sdk.dir={}", android_home);
893            }
894        }
895
896        // 5. gradlew assembleDebug
897        println!("šŸ”Ø Membangun debug APK menggunakan Gradle...");
898        let gradlew_bin = if cfg!(target_os = "windows") { "gradlew.bat" } else { "./gradlew" };
899        let mut gradle_cmd = Command::new(gradlew_bin);
900        gradle_cmd.arg("assembleDebug");
901        gradle_cmd.current_dir("native/android");
902
903        if let Some(jh) = &custom_java_home {
904            gradle_cmd.env("JAVA_HOME", jh);
905        }
906
907        let gradle_status = gradle_cmd.status();
908        if gradle_status.is_err() || !gradle_status.unwrap().success() {
909            println!("āŒ Gradle build assembleDebug gagal.");
910            return;
911        }
912
913        // 6. adb install
914        println!("šŸ”Ø Memasang APK ke perangkat {} ({})...", device_name, device_id);
915        let install_status = Command::new(&adb)
916            .args(["-s", &device_id, "install", "-r", "native/android/app/build/outputs/apk/debug/app-debug.apk"])
917            .status();
918
919        if install_status.is_err() || !install_status.unwrap().success() {
920            println!("āŒ Gagal memasang APK ke device.");
921            return;
922        }
923
924        // 7. adb reverse
925        let vite_port = "5173"; // default
926        let reverse_status = Command::new(&adb)
927            .args(["-s", &device_id, "reverse", &format!("tcp:{}", vite_port), &format!("tcp:{}", vite_port)])
928            .status();
929        if reverse_status.is_err() {
930            println!("āš ļø Warning: Gagal melakukan adb reverse port {}", vite_port);
931        }
932
933        // 8. adb shell am start
934        println!("šŸš€ Membuka aplikasi di perangkat {}...", device_name);
935        let _ = Command::new(&adb)
936            .args(["-s", &device_id, "logcat", "-c"])
937            .status();
938        
939        let _ = Command::new(&adb)
940            .args(["-s", &device_id, "shell", "am", "start", "-n", "com.rustbasic.mobile/com.rustbasic.mobile.MainActivity"])
941            .status();
942
943        println!("šŸ“‹ Menampilkan log realtime dari perangkat {} (Tekan Ctrl+C untuk keluar)...", device_name);
944        let mut logcat_cmd = Command::new(&adb);
945        logcat_cmd.args(["-s", &device_id, "logcat", "-s", "RustBasicServer"]);
946        let mut child = logcat_cmd.spawn().expect("Gagal menjalankan adb logcat");
947        let _ = child.wait();
948    } else if run_desktop {
949        println!("šŸš€ Memulai RustBasic Desktop Wrapper...");
950        let mut cmd = Command::new("cargo");
951        cmd.args(["run", "--bin", "rustbasic-desktop", "--features", "desktop"]);
952        let status = cmd.status();
953        match status {
954            Ok(s) if s.success() => {}
955            _ => {
956                println!("āŒ Gagal menjalankan Desktop Wrapper.");
957            }
958        }
959    }
960}
961
962fn prompt_string(prompt: &str, default: &str) -> String {
963    print!("{}", prompt);
964    let _ = std::io::stdout().flush();
965    let mut input = String::new();
966    if std::io::stdin().read_line(&mut input).is_ok() {
967        let trimmed = input.trim();
968        if trimmed.is_empty() {
969            default.to_string()
970        } else {
971            trimmed.to_string()
972        }
973    } else {
974        default.to_string()
975    }
976}
977
978pub fn deploy_interactive() {
979    println!("\n{}", "šŸš€ RustBasic Docker Deploy CLI".magenta().bold());
980    println!("{}", "------------------------------".magenta());
981
982    // 1. Konfigurasi Image & Pengiriman
983    let image_name = prompt_string("šŸ‘‰ Masukkan Nama/Tag Docker Image (default: rustbasic:latest): ", "rustbasic:latest");
984    
985    // Tentukan nama container dari env DOCKER_CONTAINER_NAME atau fallback dari nama image
986    let container_name = std::env::var("DOCKER_CONTAINER_NAME").unwrap_or_else(|_| {
987        let base_name = image_name
988            .split(':')
989            .next()
990            .unwrap_or("rustbasic")
991            .split('/')
992            .last()
993            .unwrap_or("rustbasic")
994            .to_lowercase();
995        format!("{}-app", base_name)
996    });
997
998    // Cek apakah Docker image sudah ada
999    let inspect = Command::new("docker")
1000        .args(["image", "inspect", &image_name])
1001        .stdout(std::process::Stdio::null())
1002        .stderr(std::process::Stdio::null())
1003        .status();
1004
1005    match inspect {
1006        Ok(status) if status.success() => {}
1007        _ => {
1008            println!("{}", format!("āš ļø  Peringatan: Image '{}' tidak ditemukan lokal.", image_name).yellow());
1009            let proceed = prompt_string("šŸ‘‰ Tetap lanjutkan proses ekspor? (y/n) [default: n]: ", "n");
1010            if proceed.to_lowercase() != "y" {
1011                println!("āŒ Proses dihentikan.");
1012                return;
1013            }
1014        }
1015    }
1016
1017    // 2. Ekspor ke tar
1018    println!("šŸ“¦ Mengekspor Docker image '{}' ke 'rustbasic.tar'...", image_name);
1019    let save_status = Command::new("docker")
1020        .args(["save", "-o", "rustbasic.tar", &image_name])
1021        .status();
1022
1023    match save_status {
1024        Ok(status) if status.success() => {
1025            println!("{}", "āœ… Image berhasil diekspor ke rustbasic.tar.".green());
1026        }
1027        _ => {
1028            println!("{}", "āŒ Gagal mengekspor Docker image.".red().bold());
1029            return;
1030        }
1031    }
1032
1033    // 3. Konfigurasi Pengiriman
1034    println!("\n{}", "🌐 Konfigurasi Pengiriman ke Server".cyan().bold());
1035    println!("{}", "-----------------------------------".cyan());
1036    let ssh_user = prompt_string("šŸ‘‰ Masukkan SSH Username Server (contoh: root) [default: root]: ", "root");
1037    let ssh_ip = prompt_string("šŸ‘‰ Masukkan IP Address Server: ", "");
1038    if ssh_ip.is_empty() {
1039        println!("{}", "āŒ IP Address server tidak boleh kosong.".red().bold());
1040        let _ = fs::remove_file("rustbasic.tar");
1041        return;
1042    }
1043    let ssh_port = prompt_string("šŸ‘‰ Masukkan SSH Port Server (default: 22): ", "22");
1044    let dest_dir = prompt_string("šŸ‘‰ Masukkan Folder Tujuan di Server (default: ~/app): ", "~/app");
1045    let server_port = prompt_string("šŸ‘‰ Masukkan Port Server Mapping (contoh: 80:4000) [default: 80:4000]: ", "80:4000");
1046    let env_file = prompt_string("šŸ‘‰ Masukkan File Env yang akan dikirim (default: .env): ", ".env");
1047
1048    println!("\nšŸš€ Menyiapkan folder tujuan di server...");
1049    let mkdir_status = Command::new("ssh")
1050        .args([
1051            "-p", &ssh_port,
1052            &format!("{}@{}", ssh_user, ssh_ip),
1053            &format!("mkdir -p {}", dest_dir)
1054        ])
1055        .status();
1056
1057    match mkdir_status {
1058        Ok(status) if status.success() => {}
1059        _ => {
1060            println!("{}", "āŒ Gagal terhubung ke server menggunakan SSH.".red().bold());
1061            let _ = fs::remove_file("rustbasic.tar");
1062            return;
1063        }
1064    }
1065
1066    println!("šŸš€ Mengirimkan berkas rustbasic.tar & {} ke server...", env_file);
1067    let scp_status = Command::new("scp")
1068        .args([
1069            "-P", &ssh_port,
1070            "rustbasic.tar", &env_file,
1071            &format!("{}@{}:{}", ssh_user, ssh_ip, dest_dir)
1072        ])
1073        .status();
1074
1075    // Hapus file tar lokal
1076    let _ = fs::remove_file("rustbasic.tar");
1077
1078    if let Ok(status) = scp_status {
1079        if !status.success() {
1080            println!("{}", "āŒ Gagal mengirimkan berkas via SCP.".red().bold());
1081            return;
1082        }
1083    } else {
1084        println!("{}", "āŒ Gagal menjalankan SCP.".red().bold());
1085        return;
1086    }
1087
1088    println!("{}", "āœ… Pengiriman berkas berhasil!".green());
1089
1090    // 4. Eksekusi SSH otomatis di server jika disetujui
1091    let auto_run = prompt_string("\nšŸ‘‰ Apakah Anda ingin langsung menjalankan container di server secara otomatis? (y/n) [default: y]: ", "y");
1092    if auto_run.to_lowercase() == "y" || auto_run.is_empty() {
1093        println!("\nšŸš€ Memuat image di server (docker load)...");
1094        let load_status = Command::new("ssh")
1095            .args([
1096                "-p", &ssh_port,
1097                &format!("{}@{}", ssh_user, ssh_ip),
1098                &format!("docker load -i {}/rustbasic.tar", dest_dir)
1099            ])
1100            .status();
1101
1102        match load_status {
1103            Ok(status) if status.success() => {
1104                println!("{}", "āœ… Image berhasil dimuat di server.".green());
1105            }
1106            _ => {
1107                println!("{}", "āŒ Gagal memuat image di server.".red().bold());
1108                return;
1109            }
1110        }
1111
1112        println!("šŸš€ Menghentikan & menghapus container lama '{}' jika ada...", container_name);
1113        let stop_status = Command::new("ssh")
1114            .args([
1115                "-p", &ssh_port,
1116                &format!("{}@{}", ssh_user, ssh_ip),
1117                &format!("docker stop {} || true && docker rm {} || true", container_name, container_name)
1118            ])
1119            .status();
1120
1121        if let Err(e) = stop_status {
1122            println!("āš ļø Peringatan saat membersihkan container lama: {}", e);
1123        }
1124
1125        println!("šŸš€ Menjalankan container baru '{}'...", container_name);
1126        let run_cmd = format!(
1127            "docker run -d --name {} -p {} --restart unless-stopped --env-file {}/.env {}",
1128            container_name, server_port, dest_dir, image_name
1129        );
1130        let run_status = Command::new("ssh")
1131            .args([
1132                "-p", &ssh_port,
1133                &format!("{}@{}", ssh_user, ssh_ip),
1134                &run_cmd
1135            ])
1136            .status();
1137
1138        match run_status {
1139            Ok(status) if status.success() => {
1140                println!("{}", format!("šŸŽ‰ Container '{}' berhasil dijalankan di server!", container_name).green().bold());
1141            }
1142            _ => {
1143                println!("{}", "āŒ Gagal menjalankan container di server.".red().bold());
1144                return;
1145            }
1146        }
1147
1148        println!("šŸš€ Membersihkan file tar di server...");
1149        let rm_status = Command::new("ssh")
1150            .args([
1151                "-p", &ssh_port,
1152                &format!("{}@{}", ssh_user, ssh_ip),
1153                &format!("rm {}/rustbasic.tar", dest_dir)
1154            ])
1155            .status();
1156
1157        if let Err(e) = rm_status {
1158            println!("āš ļø Peringatan saat membersihkan file tar di server: {}", e);
1159        }
1160
1161        println!("\n{}", "šŸŽ‰ Deployment selesai!".green().bold());
1162        println!("{}", "--------------------------------------------------------".green());
1163        println!("Untuk melihat log aplikasi di server, jalankan:");
1164        println!("ssh -p {} {}@{} \"docker logs -f {}\"", ssh_port, ssh_user, ssh_ip, container_name);
1165        println!("{}", "--------------------------------------------------------".green());
1166    } else {
1167        println!("\n{}", "šŸ–„ļø  Langkah Selanjutnya di Server Anda:".cyan().bold());
1168        println!("{}", "--------------------------------------------------------".green());
1169        println!("1. Hubungkan ke server via SSH:");
1170        println!("   ssh -p {} {}@{}", ssh_port, ssh_user, ssh_ip);
1171        println!("");
1172        println!("2. Masuk ke folder tujuan:");
1173        println!("   cd {}", dest_dir);
1174        println!("");
1175        println!("3. Muat (load) image Docker dari berkas tar:");
1176        println!("   docker load -i rustbasic.tar");
1177        println!("");
1178        println!("4. Jalankan container dengan fitur auto-restart:");
1179        println!("   docker run -d --name {} -p {} --restart unless-stopped --env-file .env {}", container_name, server_port, image_name);
1180        println!("");
1181        println!("5. Hapus file tar di server untuk menghemat disk:");
1182        println!("   rm rustbasic.tar");
1183        println!("{}", "--------------------------------------------------------".green());
1184    }
1185}
1186
1187fn get_adb_bin() -> String {
1188    // 1. Cek apakah adb terdaftar di PATH dan bisa dijalankan
1189    if let Ok(output) = Command::new("adb").arg("devices").output() {
1190        if output.status.success() {
1191            return "adb".to_string();
1192        }
1193    }
1194
1195    // 2. Jika tidak ada di PATH, cari di folder SDK default
1196    let os = std::env::consts::OS;
1197    let home = std::env::var("HOME").unwrap_or_default();
1198    let android_home = if let Ok(val) = std::env::var("ANDROID_HOME") {
1199        val
1200    } else {
1201        if os == "macos" {
1202            format!("{}/Library/Android/sdk", home)
1203        } else if os == "windows" {
1204            let local_app_data = std::env::var("LOCALAPPDATA").unwrap_or_default();
1205            format!("{}\\Android\\Sdk", local_app_data)
1206        } else {
1207            format!("{}/Android/Sdk", home)
1208        }
1209    };
1210
1211    let adb_name = if os == "windows" { "adb.exe" } else { "adb" };
1212    let path = Path::new(&android_home).join("platform-tools").join(adb_name);
1213    if path.exists() {
1214        path.display().to_string()
1215    } else {
1216        "adb".to_string() // fallback terakhir jika semua gagal
1217    }
1218}
1219
1220fn get_adb_devices() -> Vec<(String, String)> {
1221    let adb = get_adb_bin();
1222    let output = Command::new(&adb).arg("devices").output();
1223    let mut devices = Vec::new();
1224    if let Ok(out) = output {
1225        let stdout = String::from_utf8_lossy(&out.stdout);
1226        for line in stdout.lines() {
1227            let line = line.trim();
1228            if line.is_empty() || line.starts_with("List of devices") {
1229                continue;
1230            }
1231            let parts: Vec<&str> = line.split_whitespace().collect();
1232            if parts.len() >= 2 && parts[1] == "device" {
1233                let device_id = parts[0].to_string();
1234                let model_out = Command::new(&adb)
1235                    .args(["-s", &device_id, "shell", "getprop", "ro.product.model"])
1236                    .output();
1237                let model = if let Ok(m_out) = model_out {
1238                    String::from_utf8_lossy(&m_out.stdout).trim().to_string()
1239                } else {
1240                    "Unknown Device".to_string()
1241                };
1242                devices.push((device_id, model));
1243            }
1244        }
1245    }
1246    devices
1247}
1248
1249fn compile_jni_libraries() -> bool {
1250    println!("šŸš€ Building Rust library for Android (Native Rust implementation)...");
1251
1252    let _ = Command::new("rustup")
1253        .args(["target", "add", "aarch64-linux-android", "armv7-linux-androideabi", "x86_64-linux-android"])
1254        .status();
1255
1256    let home = std::env::var("HOME").unwrap_or_default();
1257    let android_ndk_home = if let Ok(val) = std::env::var("ANDROID_NDK_HOME") {
1258        val
1259    } else {
1260        let mac_ndk = format!("{}/Library/Android/sdk/ndk", home);
1261        if Path::new(&mac_ndk).exists() {
1262            if let Ok(entries) = fs::read_dir(&mac_ndk) {
1263                let mut paths: Vec<_> = entries.flatten().map(|e| e.path()).collect();
1264                paths.sort();
1265                if let Some(highest) = paths.last() {
1266                    highest.display().to_string()
1267                } else {
1268                    "".to_string()
1269                }
1270            } else {
1271                "".to_string()
1272            }
1273        } else {
1274            "".to_string()
1275        }
1276    };
1277
1278    if android_ndk_home.is_empty() {
1279        println!("āŒ Error: ANDROID_NDK_HOME is not set. Please set ANDROID_NDK_HOME.");
1280        return false;
1281    }
1282
1283    println!("Using NDK: {}", android_ndk_home);
1284
1285    let os = std::env::consts::OS;
1286    let toolchain_sub = if os == "macos" { "darwin-x86_64" } else { "linux-x86_64" };
1287    let toolchain_bin_path = Path::new(&android_ndk_home)
1288        .join("toolchains/llvm/prebuilt")
1289        .join(toolchain_sub)
1290        .join("bin");
1291
1292    if !toolchain_bin_path.exists() {
1293        println!("āŒ Error: Toolchain bin path not found: {}", toolchain_bin_path.display());
1294        return false;
1295    }
1296
1297    let sqlite_version = "3450100";
1298    let sqlite_dir = format!("target/sqlite-amalgamation-{}", sqlite_version);
1299    if !Path::new(&sqlite_dir).exists() {
1300        println!("šŸ“„ Downloading SQLite source amalgamation...");
1301        fs::create_dir_all("target").ok();
1302        
1303        let zip_path = "target/sqlite.zip";
1304        let sqlite_url = format!("https://www.sqlite.org/2024/sqlite-amalgamation-{}.zip", sqlite_version);
1305        
1306        let curl_status = Command::new("curl")
1307            .args(["-sSLo", zip_path, &sqlite_url])
1308            .status();
1309        
1310        if curl_status.is_err() || !curl_status.unwrap().success() {
1311            println!("āŒ Gagal men-download SQLite source.");
1312            return false;
1313        }
1314
1315        let unzip_status = Command::new("unzip")
1316            .args(["-q", zip_path, "-d", "target/"])
1317            .status();
1318
1319        let _ = fs::remove_file(zip_path);
1320
1321        if unzip_status.is_err() || !unzip_status.unwrap().success() {
1322            println!("āŒ Gagal mengekstrak SQLite source.");
1323            return false;
1324        }
1325    }
1326
1327    let targets = vec![
1328        ("aarch64-linux-android", "arm64-v8a", "aarch64-linux-android21-clang"),
1329        ("armv7-linux-androideabi", "armeabi-v7a", "armv7a-linux-androideabi21-clang"),
1330        ("x86_64-linux-android", "x86_64", "x86_64-linux-android21-clang"),
1331    ];
1332
1333    let jnilibs_dir = "native/android/app/src/main/jniLibs";
1334
1335    for (target, arch, clang_name) in targets {
1336        println!("šŸ”Ø Preparing SQLite static library for {}...", target);
1337        
1338        let clang_path = toolchain_bin_path.join(clang_name);
1339        let ar_path = toolchain_bin_path.join("llvm-ar");
1340
1341        if !clang_path.exists() {
1342            println!("āŒ Error: Compiler not found: {}", clang_path.display());
1343            return false;
1344        }
1345
1346        let sqlite_out = format!("target/{}/sqlite", target);
1347        fs::create_dir_all(&sqlite_out).ok();
1348
1349        let libsqlite3_a = format!("{}/libsqlite3.a", sqlite_out);
1350        if !Path::new(&libsqlite3_a).exists() {
1351            println!("   Compiling SQLite static lib for {}...", target);
1352            let sqlite3_o = format!("{}/sqlite3.o", sqlite_out);
1353            let sqlite3_c = format!("{}/sqlite3.c", sqlite_dir);
1354            
1355            let compile_status = Command::new(&clang_path)
1356                .args(["-O2", "-c", &sqlite3_c, "-o", &sqlite3_o])
1357                .status();
1358
1359            if compile_status.is_err() || !compile_status.unwrap().success() {
1360                println!("āŒ Gagal mengompilasi sqlite3.o");
1361                return false;
1362            }
1363
1364            let archive_status = Command::new(&ar_path)
1365                .args(["rcs", &libsqlite3_a, &sqlite3_o])
1366                .status();
1367
1368            if archive_status.is_err() || !archive_status.unwrap().success() {
1369                println!("āŒ Gagal mengarsip libsqlite3.a");
1370                return false;
1371            }
1372        }
1373
1374        println!("šŸ”Ø Compiling Rust library for {}...", target);
1375        let mut cargo_cmd = Command::new("cargo");
1376        cargo_cmd.args(["build", "--target", target, "--release"]);
1377
1378        let clang_path_str = clang_path.display().to_string();
1379        let ar_path_str = ar_path.display().to_string();
1380
1381        let target_upper = target.replace("-", "_").to_uppercase();
1382        let linker_env = format!("CARGO_TARGET_{}_LINKER", target_upper);
1383        let cc_env = format!("CC_{}", target.replace("-", "_"));
1384        let ar_env = format!("AR_{}", target.replace("-", "_"));
1385
1386        cargo_cmd.env(&linker_env, &clang_path_str);
1387        cargo_cmd.env(&cc_env, &clang_path_str);
1388        cargo_cmd.env(&ar_env, &ar_path_str);
1389
1390        let cargo_status = cargo_cmd.status();
1391        if cargo_status.is_err() || !cargo_status.unwrap().success() {
1392            println!("āŒ Gagal mengompilasi library Rust untuk target {}", target);
1393            return false;
1394        }
1395
1396        let dest_dir = format!("{}/{}", jnilibs_dir, arch);
1397        fs::create_dir_all(&dest_dir).ok();
1398
1399        let src_so = format!("target/{}/release/librustbasic.so", target);
1400        let dest_so = format!("{}/librustbasic_mobile.so", dest_dir);
1401
1402        if let Err(e) = fs::copy(&src_so, &dest_so) {
1403            println!("āŒ Gagal menyalin {}: {}", src_so, e);
1404            return false;
1405        }
1406    }
1407
1408    println!("āœ… Android JNI libraries built successfully!");
1409    true
1410}
1411
1412fn get_host_arch() -> String {
1413    // 1. Coba deteksi via command 'uname -m' (macOS / Linux)
1414    if cfg!(any(target_os = "macos", target_os = "linux")) {
1415        if let Ok(output) = std::process::Command::new("uname").arg("-m").output() {
1416            let arch = String::from_utf8_lossy(&output.stdout).trim().to_lowercase();
1417            if !arch.is_empty() {
1418                if arch.contains("aarch64") || arch.contains("arm64") {
1419                    return "aarch64".to_string();
1420                } else if arch.contains("x86_64") || arch.contains("amd64") {
1421                    return "x86_64".to_string();
1422                }
1423                return arch;
1424            }
1425        }
1426    }
1427
1428    // 2. Coba deteksi via environment variable (Windows)
1429    if cfg!(target_os = "windows") {
1430        if let Ok(arch) = std::env::var("PROCESSOR_ARCHITECTURE") {
1431            let arch_lower = arch.to_lowercase();
1432            if arch_lower.contains("amd64") || arch_lower.contains("x64") {
1433                return "x86_64".to_string();
1434            } else if arch_lower.contains("arm64") {
1435                return "aarch64".to_string();
1436            }
1437        }
1438    }
1439
1440    // 3. Fallback ke compile-time constant
1441    let const_arch = std::env::consts::ARCH;
1442    if const_arch == "x86_64" {
1443        "x86_64".to_string()
1444    } else if const_arch == "aarch64" {
1445        "aarch64".to_string()
1446    } else {
1447        const_arch.to_string()
1448    }
1449}