Skip to main content

rustbasic_cli/
builder.rs

1use std::process::Command;
2use std::io::{self, Write};
3use std::fs;
4use std::path::{Path, PathBuf};
5use rustbasic_core::colored::*;
6
7/// Fungsi rekursif untuk menyalin seluruh isi folder
8fn copy_dir_all(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> io::Result<()> {
9    fs::create_dir_all(&dst)?;
10    for entry in fs::read_dir(src)? {
11        let entry = entry?;
12        let ty = entry.file_type()?;
13        if ty.is_dir() {
14            copy_dir_all(entry.path(), dst.as_ref().join(entry.file_name()))?;
15        } else {
16            fs::copy(entry.path(), dst.as_ref().join(entry.file_name()))?;
17        }
18    }
19    Ok(())
20}
21
22/// Fungsi untuk memperbarui variabel APP_DEBUG di file .env
23fn update_env_app_debug(is_release: bool) {
24    let env_path = Path::new(".env");
25    if !env_path.exists() {
26        return;
27    }
28    if let Ok(content) = fs::read_to_string(env_path) {
29        let target_value = if is_release { "false" } else { "true" };
30        let mut lines: Vec<String> = content.lines().map(|s| s.to_string()).collect();
31        let mut found = false;
32        for line in &mut lines {
33            if line.trim_start().starts_with("APP_DEBUG=") {
34                *line = format!("APP_DEBUG={}", target_value);
35                found = true;
36                break;
37            }
38        }
39        if !found {
40            lines.push(format!("APP_DEBUG={}", target_value));
41        }
42        let new_content = lines.join("\n") + "\n";
43        if let Err(e) = fs::write(env_path, new_content) {
44            println!("{} {}", "โš ๏ธ  Gagal memperbarui file .env:".yellow(), e);
45        } else {
46            println!("{} {}{}", "๐Ÿ“".green(), "APP_DEBUG diatur ke ".dimmed(), target_value.cyan());
47        }
48    }
49}
50
51/// Fungsi untuk mendapatkan port aplikasi dari file .env
52fn get_app_port() -> u16 {
53    let env_path = Path::new(".env");
54    if env_path.exists() {
55        if let Ok(content) = fs::read_to_string(env_path) {
56            for line in content.lines() {
57                let trimmed = line.trim();
58                if trimmed.starts_with("APP_PORT=") {
59                    if let Some(port_str) = trimmed.split('=').nth(1) {
60                        if let Ok(port) = port_str.trim().parse::<u16>() {
61                            return port;
62                        }
63                    }
64                }
65            }
66        }
67    }
68    4000
69}
70
71pub fn build_project() {
72    println!("\n{}", "๐Ÿš€ RustBasic Build Manager".magenta().bold());
73    println!("{}", "--------------------------".magenta());
74    
75    // 1. Pilih Target
76    println!("{}", "--- Pilih Target OS ---".cyan().bold());
77    println!("1) Native (Sesuai OS Anda)");
78    println!("2) Windows x86_64 (x86_64-pc-windows-msvc)");
79    println!("3) Linux x86_64 GNU (x86_64-unknown-linux-gnu)");
80    println!("4) Linux x86_64 MUSL (x86_64-unknown-linux-musl)");
81    println!("5) Linux ARM64 GNU (aarch64-unknown-linux-gnu)");
82    println!("6) Linux ARM64 MUSL (aarch64-unknown-linux-musl)");
83    println!("7) macOS ARM64 (aarch64-apple-darwin)");
84    println!("8) macOS Intel (x86_64-apple-darwin)");
85    println!("9) Batal");
86    print!("\n{}", "Masukkan pilihan target (1-9): ".bold());
87    io::stdout().flush().unwrap();
88
89    let mut target_choice = String::new();
90    io::stdin().read_line(&mut target_choice).ok();
91    let target_choice = target_choice.trim();
92
93    if target_choice == "9" {
94        println!("{}", "๐Ÿ‘‹ Build dibatalkan.".yellow());
95        return;
96    }
97
98    let target = match target_choice {
99        "2" => Some("x86_64-pc-windows-msvc"),
100        "3" => Some("x86_64-unknown-linux-gnu"),
101        "4" => Some("x86_64-unknown-linux-musl"),
102        "5" => Some("aarch64-unknown-linux-gnu"),
103        "6" => Some("aarch64-unknown-linux-musl"),
104        "7" => Some("aarch64-apple-darwin"),
105        "8" => Some("x86_64-apple-darwin"),
106        _ => None, // Native
107    };
108
109    // 2. Pilih Mode
110    println!("\n{}", "--- Pilih Mode Build ---".cyan().bold());
111    println!("1) Development");
112    println!("2) Production (Release)");
113    print!("\n{}", "Masukkan pilihan mode (1-2): ".bold());
114    io::stdout().flush().unwrap();
115
116    let mut mode_choice = String::new();
117    io::stdin().read_line(&mut mode_choice).ok();
118    let is_release = mode_choice.trim() == "2";
119
120    // 3. Update File .env (APP_DEBUG)
121    println!("\n{}", "๐Ÿ”ง Menyiapkan konfigurasi .env...".blue());
122    update_env_app_debug(is_release);
123
124    // 4. Jalankan npm run build untuk Frontend
125    if Path::new("package.json").exists() {
126        println!("\n{}", "๐Ÿ“ฆ Memulai kompilasi aset frontend (npm run build)...".blue());
127        let npm_cmd = if cfg!(target_os = "windows") { "npm.cmd" } else { "npm" };
128        let status = Command::new(npm_cmd)
129            .args(["run", "build"])
130            .status();
131
132        match status {
133            Ok(s) if s.success() => {
134                println!("{}", "โœ… Kompilasi frontend berhasil!".green().bold());
135            }
136            Ok(s) => {
137                println!("{} {}", "โŒ Error: npm run build keluar dengan kode:".red().bold(), s);
138                println!("{}", "โš ๏ธ  Proses build dihentikan karena kompilasi frontend gagal.".yellow());
139                return;
140            }
141            Err(e) => {
142                println!("{} {}", "โŒ Error: Gagal mengeksekusi 'npm'. Pastikan npm terinstal.".red().bold(), e);
143                return;
144            }
145        }
146    }
147
148    // 5. Eksekusi Build Rust
149    println!("\n{}", "๐Ÿ› ๏ธ  Menyiapkan build Rust...".blue());
150
151    let has_zigbuild = Command::new("cargo")
152        .arg("zigbuild")
153        .arg("--version")
154        .output()
155        .is_ok();
156
157    let mut use_zigbuild = false;
158    if has_zigbuild && target.is_some() {
159        println!("\n{}", "--- Pilih Compiler untuk Kompilasi Silang ---".cyan().bold());
160        println!("1) Cargo Build Standard (Membutuhkan target toolchain terpasang)");
161        println!("2) Cargo Zigbuild (Lebih mudah untuk kompilasi silang)");
162        print!("\n{}", "Masukkan pilihan compiler (1-2, default 2): ".bold());
163        io::stdout().flush().unwrap();
164
165        let mut compiler_choice = String::new();
166        io::stdin().read_line(&mut compiler_choice).ok();
167        let choice = compiler_choice.trim();
168        if choice == "1" {
169            use_zigbuild = false;
170        } else {
171            use_zigbuild = true;
172        }
173    }
174
175    let mut cmd = if use_zigbuild {
176        println!("{}", "โœจ Menggunakan cargo-zigbuild untuk kompilasi silang...".green().italic());
177        let mut c = Command::new("cargo");
178        c.arg("zigbuild");
179        c
180    } else {
181        if let Some(t) = target {
182            println!("{} {} {}", "๐Ÿ“ฆ Menambahkan target".blue(), t.yellow(), "via rustup...".blue());
183            Command::new("rustup")
184                .args(["target", "add", t])
185                .status()
186                .ok();
187        }
188        let mut c = Command::new("cargo");
189        c.arg("build");
190        c
191    };
192
193    if is_release {
194        cmd.arg("--release");
195    }
196
197    if let Some(t) = target {
198        cmd.arg("--target").arg(t);
199    }
200
201    println!("{} {:?}", "๐Ÿš€ Menjalankan:".blue().bold(), cmd);
202    let status = crate::utils::run_cargo_with_progress(cmd).expect("Gagal menjalankan perintah build");
203
204    if status.success() {
205        println!("\n{}", "โœ… Build Rust berhasil!".green().bold());
206        
207        // 6. Siapkan folder deploy dan salin aset
208        println!("\n{}", "๐Ÿ“‚ Menyiapkan folder deploy...".cyan().bold());
209        
210        let deploy_dir = Path::new("deploy");
211        if deploy_dir.exists() {
212            println!("{}", "๐Ÿงน Membersihkan folder deploy lama...".dimmed());
213            let _ = crate::utils::remove_dir_all_recursive(deploy_dir);
214        }
215        
216        if let Err(e) = fs::create_dir_all(deploy_dir) {
217            println!("{} {}", "โŒ Gagal membuat folder deploy:".red().bold(), e);
218            return;
219        }
220
221        // Salin folder database jika ada
222        if Path::new("database").exists() {
223            print!("   {} Menyalin folder database... ", "๐Ÿ“ฆ".blue());
224            io::stdout().flush().unwrap();
225            if copy_dir_all("database", "deploy/database").is_ok() {
226                println!("{}", "selesai".green());
227            } else {
228                println!("{}", "gagal".red());
229            }
230        }
231
232        // Salin folder dist jika ada
233        let mut dist_copied = false;
234        if Path::new("src/dist").exists() {
235            print!("   {} Menyalin folder src/dist... ", "๐Ÿ“ฆ".blue());
236            io::stdout().flush().unwrap();
237            if copy_dir_all("src/dist", "deploy/dist").is_ok() {
238                println!("{}", "selesai".green());
239                dist_copied = true;
240            } else {
241                println!("{}", "gagal".red());
242            }
243        }
244
245        if !dist_copied && Path::new("dist").exists() {
246            print!("   {} Menyalin folder dist... ", "๐Ÿ“ฆ".blue());
247            io::stdout().flush().unwrap();
248            if copy_dir_all("dist", "deploy/dist").is_ok() {
249                println!("{}", "selesai".green());
250            } else {
251                println!("{}", "gagal".red());
252            }
253        }
254
255        // Salin folder storage jika ada
256        if Path::new("storage").exists() {
257            print!("   {} Menyalin folder storage... ", "๐Ÿ“ฆ".blue());
258            io::stdout().flush().unwrap();
259            if copy_dir_all("storage", "deploy/storage").is_ok() {
260                println!("{}", "selesai".green());
261            } else {
262                println!("{}", "gagal".red());
263            }
264        }
265
266        // Salin file .env jika ada
267        if Path::new(".env").exists() {
268            print!("   {} Menyalin file .env... ", "๐Ÿ“„".blue());
269            io::stdout().flush().unwrap();
270            if fs::copy(".env", "deploy/.env").is_ok() {
271                println!("{}", "selesai".green());
272            } else {
273                println!("{}", "gagal".red());
274            }
275        }
276
277        // Generate konfigurasi server deployment (.htaccess & nginx.conf)
278        let app_port = get_app_port();
279
280        // 1. Generate .htaccess untuk Apache / Shared Hosting
281        let htaccess_content = format!(
282            r#"<IfModule mod_rewrite.c>
283    RewriteEngine On
284
285    # 1. Jika meminta file statis yang ada di folder dist, sajikan langsung dari dist/
286    RewriteCond %{{DOCUMENT_ROOT}}/dist/$1 -f
287    RewriteRule ^(.*)$ dist/$1 [L]
288
289    # 2. Jika bukan file statis nyata, teruskan ke binary RustBasic yang berjalan di port {}
290    RewriteCond %{{REQUEST_FILENAME}} !-f
291    RewriteCond %{{REQUEST_FILENAME}} !-d
292    RewriteRule ^(.*)$ http://127.0.0.1:{}/$1 [P,L]
293    
294    RewriteRule ^$ http://127.0.0.1:{}/ [P,L]
295</IfModule>
296"#,
297            app_port, app_port, app_port
298        );
299        if fs::write("deploy/.htaccess", htaccess_content).is_ok() {
300            println!("   {} Menghasilkan file konfigurasi deploy/.htaccess... selesai", "๐Ÿ“„".blue());
301        }
302
303        // 2. Generate nginx.conf untuk VPS / Nginx Reverse Proxy
304        let nginx_content = format!(
305            r#"server {{
306    listen 80;
307    server_name domainanda.com;
308
309    # Root diarahkan ke folder deploy
310    root /path/ke/folder/deploy;
311
312    # Coba sajikan file statis dari folder dist jika ada
313    location / {{
314        try_files /dist$uri @rust_backend;
315    }}
316
317    # Teruskan request dinamis ke binary RustBasic yang berjalan di port {}
318    location @rust_backend {{
319        proxy_pass http://127.0.0.1:{};
320        proxy_set_header Host $host;
321        proxy_set_header X-Real-IP $remote_addr;
322        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
323        proxy_set_header X-Forwarded-Proto $scheme;
324    }}
325}}
326"#,
327            app_port, app_port
328        );
329        if fs::write("deploy/nginx.conf", nginx_content).is_ok() {
330            println!("   {} Menghasilkan file konfigurasi deploy/nginx.conf... selesai", "๐Ÿ“„".blue());
331        }
332
333        // Salin file binary hasil kompilasi
334        let source_binary_filename = if let Some(t) = target {
335            if t.contains("windows") {
336                "rustbasic.exe"
337            } else {
338                "rustbasic"
339            }
340        } else if cfg!(target_os = "windows") {
341            "rustbasic.exe"
342        } else {
343            "rustbasic"
344        };
345
346        // Membaca nama kustom untuk hasil build dari .env
347        let build_name = std::env::var("BUILD_NAME")
348            .unwrap_or_else(|_| "rustbasic".to_string());
349
350        let dest_binary_filename = if let Some(t) = target {
351            if t.contains("windows") {
352                format!("{}.exe", build_name)
353            } else {
354                build_name.clone()
355            }
356        } else if cfg!(target_os = "windows") {
357            format!("{}.exe", build_name)
358        } else {
359            build_name.clone()
360        };
361
362        let mode_dir = if is_release { "release" } else { "debug" };
363        let binary_path = if let Some(t) = target {
364            PathBuf::from("target")
365                .join(t)
366                .join(mode_dir)
367                .join(source_binary_filename)
368        } else {
369            PathBuf::from("target")
370                .join(mode_dir)
371                .join(source_binary_filename)
372        };
373
374        if binary_path.exists() {
375            let dest_binary_path = deploy_dir.join(&dest_binary_filename);
376            print!("   {} Menyalin binary ke deploy/{}... ", "๐Ÿš€".blue(), dest_binary_filename.cyan());
377            io::stdout().flush().unwrap();
378            if fs::copy(&binary_path, &dest_binary_path).is_ok() {
379                println!("{}", "selesai".green());
380                println!("\n๐ŸŽ‰ {}", "Proses deployment berhasil disiapkan di folder 'deploy'!".green().bold());
381            } else {
382                println!("{}", "gagal".red());
383            }
384        } else {
385            println!("\n{}", format!("โŒ File binary tidak ditemukan di: {}", binary_path.display()).red().bold());
386        }
387    } else {
388        println!("\n{}", "โŒ Build Rust gagal.".red().bold());
389        println!("{}", "๐Ÿ’ก Penyebab: Linker untuk target tersebut tidak ditemukan di sistem Anda.".yellow());
390        
391        if target_choice == "2" {
392            println!("\n{}", "๐Ÿ”ง Cara memperbaiki untuk Windows:".cyan());
393            println!("   Jalankan: {}", "brew install mingw-w64".white().on_black());
394        } else if target_choice == "3" {
395            println!("\n{}", "๐Ÿ”ง Cara memperbaiki untuk Linux:".cyan());
396            println!("   Jalankan: {}", "brew install messense/macos-cross-toolchains/x86_64-unknown-linux-gnu".white().on_black());
397        }
398        
399        println!("\n{}", "Atau gunakan 'cargo-zigbuild' untuk kompilasi silang yang lebih mudah:".cyan());
400        println!("1. brew install zig");
401        println!("2. cargo install cargo-zigbuild");
402        println!("3. Gunakan '{}'", "cargo zigbuild --target <target>".white().on_black());
403    }
404}
405pub fn build_native_project(run_android: bool, run_desktop: bool) {
406    println!("\n{}", "๐Ÿš€ RustBasic Native Build Manager".magenta().bold());
407    println!("{}", "---------------------------------".magenta());
408
409    // 1. Jalankan npm run build untuk Frontend
410    if Path::new("package.json").exists() {
411        println!("\n{}", "๐Ÿ“ฆ Memulai kompilasi aset frontend (npm run build)...".blue());
412        let npm_cmd = if cfg!(target_os = "windows") { "npm.cmd" } else { "npm" };
413        let status = Command::new(npm_cmd)
414            .args(["run", "build"])
415            .status();
416
417        match status {
418            Ok(s) if s.success() => {
419                println!("{}", "โœ… Kompilasi frontend berhasil!".green().bold());
420            }
421            Ok(s) => {
422                println!("{} {}", "โŒ Error: npm run build keluar dengan kode:".red().bold(), s);
423                println!("{}", "โš ๏ธ  Proses build dihentikan karena kompilasi frontend gagal.".yellow());
424                return;
425            }
426            Err(e) => {
427                println!("{} {}", "โŒ Error: Gagal mengeksekusi 'npm'. Pastikan npm terinstal.".red().bold(), e);
428                return;
429            }
430        }
431    }
432
433    if run_desktop {
434        println!("\n{}", "๐Ÿ› ๏ธ  Menyiapkan build Desktop Wrapper...".blue());
435        if !Path::new("native/desktop/Cargo.toml").exists() {
436            println!("{}", "โŒ Error: Folder native/desktop tidak ditemukan. Jalankan 'rustbasic-native install' terlebih dahulu.".red().bold());
437            return;
438        }
439
440        let mut cmd = Command::new("cargo");
441        cmd.args(["build", "--manifest-path", "native/desktop/Cargo.toml", "--release"]);
442        println!("{} {:?}", "๐Ÿš€ Menjalankan:".blue().bold(), cmd);
443        
444        let status = cmd.status();
445        match status {
446            Ok(s) if s.success() => {
447                let bin_name = if cfg!(target_os = "windows") {
448                    "rustbasic-native-desktop.exe"
449                } else {
450                    "rustbasic-native-desktop"
451                };
452                let bin_path = Path::new("native/desktop/target/release").join(bin_name);
453                println!("\n๐ŸŽ‰ {}", "Build Desktop Wrapper berhasil!".green().bold());
454                println!("๐Ÿš€ Hasil executable berada di: {}", bin_path.display().to_string().cyan().bold());
455            }
456            _ => {
457                println!("\nโŒ {}", "Build Desktop Wrapper gagal.".red().bold());
458            }
459        }
460    }
461    if run_android {
462        println!("\n{}", "๐Ÿ› ๏ธ  Menyiapkan build Android Wrapper...".blue());
463        if !Path::new("native/android/build.gradle").exists() {
464            println!("{}", "โŒ Error: Folder native/android tidak ditemukan. Jalankan 'rustbasic-native install' terlebih dahulu.".red().bold());
465            return;
466        }
467
468        // Jalankan build-android.sh untuk compile JNI release
469        println!(" JNI shared libraries...");
470        let sh_cmd = if cfg!(target_os = "windows") { "sh" } else { "bash" };
471        let status = Command::new(sh_cmd)
472            .arg("./native/build-android.sh")
473            .status();
474
475        match status {
476            Ok(s) if s.success() => {
477                println!("{}", "โœ… Kompilasi JNI shared libraries berhasil!".green().bold());
478            }
479            _ => {
480                println!("{}", "โŒ Error: Gagal mengompilasi JNI libraries.".red().bold());
481                return;
482            }
483        }
484
485        // Tentukan JAVA_HOME jika belum diatur
486        let has_java_home = std::env::var("JAVA_HOME").is_ok();
487        let mut custom_java_home = None;
488        if !has_java_home {
489            // Coba deteksi Android Studio JDK di berbagai platform
490            if cfg!(target_os = "macos") {
491                let mac_studio_jdk = "/Applications/Android Studio.app/Contents/jbr/Contents/Home";
492                if Path::new(mac_studio_jdk).exists() {
493                    custom_java_home = Some(mac_studio_jdk.to_string());
494                }
495            } else if cfg!(target_os = "windows") {
496                let win_paths = [
497                    "C:\\Program Files\\Android\\Android Studio\\jbr",
498                    "C:\\Program Files\\Android\\Android Studio\\jre",
499                ];
500                for path in &win_paths {
501                    if Path::new(path).exists() {
502                        custom_java_home = Some(path.to_string());
503                        break;
504                    }
505                }
506            } else {
507                // Linux & other Unix-like OS
508                let unix_paths = [
509                    "/opt/android-studio/jbr",
510                    "/opt/android-studio/jre",
511                    "/snap/android-studio/current/jbr",
512                    "/snap/android-studio/current/jre",
513                    "/usr/local/android-studio/jbr",
514                    "/usr/local/android-studio/jre",
515                    "/usr/lib/jvm/default-java",
516                ];
517                for path in &unix_paths {
518                    if Path::new(path).exists() {
519                        custom_java_home = Some(path.to_string());
520                        break;
521                    }
522                }
523            }
524        }
525
526        // 1. Generate Keystore if not exists
527        let keystore_path = Path::new("native/android/app/release.keystore");
528        if !keystore_path.exists() {
529            println!("๐Ÿ”‘ Menghasilkan developer release keystore baru...");
530            let keytool_bin = if let Some(jh) = custom_java_home.as_ref() {
531                let jh_bin = Path::new(jh).join("bin/keytool");
532                if jh_bin.exists() {
533                    jh_bin.display().to_string()
534                } else {
535                    "keytool".to_string()
536                }
537            } else {
538                "keytool".to_string()
539            };
540
541            let mut keytool_cmd = Command::new(keytool_bin);
542            keytool_cmd.args([
543                "-genkeypair",
544                "-v",
545                "-keystore",
546                "native/android/app/release.keystore",
547                "-alias",
548                "rustbasic",
549                "-keyalg",
550                "RSA",
551                "-keysize",
552                "2048",
553                "-validity",
554                "10000",
555                "-storepass",
556                "rustbasic",
557                "-keypass",
558                "rustbasic",
559                "-dname",
560                "CN=RustBasic Developer, O=RustBasic, C=ID"
561            ]);
562            let _ = keytool_cmd.status();
563        }
564
565        // 2. Inject signingConfigs into build.gradle if not already present
566        let gradle_path = Path::new("native/android/app/build.gradle");
567        if gradle_path.exists() {
568            if let Ok(content) = fs::read_to_string(gradle_path) {
569                if !content.contains("signingConfigs") {
570                    println!("๐Ÿ“ Menyematkan konfigurasi tanda tangan (signingConfigs) ke build.gradle...");
571                    let updated_content = content
572                        .replace(
573                            "buildTypes {",
574                            "signingConfigs {\n        release {\n            storeFile file(\"release.keystore\")\n            storePassword \"rustbasic\"\n            keyAlias \"rustbasic\"\n            keyPassword \"rustbasic\"\n        }\n    }\n\n    buildTypes {"
575                        )
576                        .replace(
577                            "buildTypes {\n        release {\n            minifyEnabled",
578                            "buildTypes {\n        release {\n            signingConfig signingConfigs.release\n            minifyEnabled"
579                        )
580                        .replace(
581                            "buildTypes {\r\n        release {\r\n            minifyEnabled",
582                            "buildTypes {\r\n        release {\r\n            signingConfig signingConfigs.release\r\n            minifyEnabled"
583                        );
584                    let _ = fs::write(gradle_path, updated_content);
585                }
586            }
587        }
588
589        println!("๐Ÿ”จ Memulai kompilasi APK & AAB menggunakan Gradle...");
590        let gradlew_bin = if cfg!(target_os = "windows") { "gradlew.bat" } else { "./gradlew" };
591        let mut gradle_cmd = Command::new(gradlew_bin);
592        gradle_cmd.args(["assembleRelease", "bundleRelease"]);
593        gradle_cmd.current_dir("native/android");
594
595        if let Some(jh) = custom_java_home.as_ref() {
596            gradle_cmd.env("JAVA_HOME", jh);
597        }
598
599        let status = gradle_cmd.status();
600        match status {
601            Ok(s) if s.success() => {
602                println!("\n๐ŸŽ‰ {}", "Build Android Wrapper berhasil!".green().bold());
603                println!("๐Ÿ“ฆ Hasil output:");
604                let apk_signed = "native/android/app/build/outputs/apk/release/app-release.apk";
605                let final_apk = if Path::new(apk_signed).exists() {
606                    apk_signed
607                } else {
608                    "native/android/app/build/outputs/apk/release/app-release-unsigned.apk"
609                };
610                println!("   - APK: {}", final_apk.cyan().bold());
611                println!("   - AAB: {}", "native/android/app/build/outputs/bundle/release/app-release.aab".cyan().bold());
612            }
613            _ => {
614                println!("\nโŒ {}", "Build Android Wrapper gagal.".red().bold());
615            }
616        }
617    }
618}
619