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 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 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 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 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 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
239pub fn build_docker(custom_tag: &str) {
241 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 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 ā Multi-stage
267# ============================================================
268
269# Stage 1: Builder
270FROM rust:1-slim-bookworm AS builder
271
272RUN apt-get update && apt-get install -y \
273 pkg-config libssl-dev \
274 && rm -rf /var/lib/apt/lists/*
275
276WORKDIR /build
277
278# Copy rustbasic-core (dari konteks workspace root)
279COPY rustbasic-core /build/rustbasic-core
280
281# Copy proyek utama rustbasic
282COPY rustbasic /build/rustbasic
283
284WORKDIR /build/rustbasic
285
286# Build release binary
287RUN cargo build --release --bin rustbasic
288
289# Stage 2: Runtime
290FROM debian:bookworm-slim
291
292RUN apt-get update && apt-get install -y \
293 ca-certificates libssl3 \
294 && rm -rf /var/lib/apt/lists/*
295
296WORKDIR /app
297
298# Copy binary dari builder stage
299COPY --from=builder /build/rustbasic/target/release/rustbasic .
300
301# Copy assets yang diperlukan dari builder stage (lebih aman dan bersih)
302COPY --from=builder /build/rustbasic/src/resources/views/ src/resources/views/
303COPY --from=builder /build/rustbasic/src/dist/ src/dist/
304COPY --from=builder /build/rustbasic/public/ public/
305COPY --from=builder /build/rustbasic/database/migrations/ database/migrations/
306COPY --from=builder /build/rustbasic/database/seeders/ database/seeders/
307COPY --from=builder /build/rustbasic/.env.example .env
308
309# Expose port aplikasi
310EXPOSE 4000
311
312CMD ["./rustbasic"]
313"#.to_string()
314 } else {
315 format!(r#"# ============================================================
316# RustBasic Docker Build ā Multi-stage
317# ============================================================
318
319# Stage 1: Builder
320FROM rust:1-slim-bookworm AS builder
321
322RUN apt-get update && apt-get install -y \
323 pkg-config libssl-dev \
324 && rm -rf /var/lib/apt/lists/*
325
326WORKDIR /build
327
328# Copy proyek utama
329COPY . .
330
331# Build release binary
332RUN cargo build --release --bin {bin_name}
333
334# Stage 2: Runtime
335FROM debian:bookworm-slim
336
337RUN apt-get update && apt-get install -y \
338 ca-certificates libssl3 \
339 && rm -rf /var/lib/apt/lists/*
340
341WORKDIR /app
342
343# Copy binary dari builder stage
344COPY --from=builder /build/target/release/{bin_name} .
345
346# Copy assets yang diperlukan dari builder stage
347COPY --from=builder /build/src/resources/views/ src/resources/views/
348COPY --from=builder /build/src/dist/ src/dist/
349COPY --from=builder /build/public/ public/
350COPY --from=builder /build/database/migrations/ database/migrations/
351COPY --from=builder /build/database/seeders/ database/seeders/
352COPY --from=builder /build/.env.example .env
353
354# Expose port aplikasi
355EXPOSE 4000
356
357CMD ["./{bin_name}"]
358"#, bin_name = binary_name)
359 };
360
361 if let Err(e) = fs::write(dockerfile_path, dockerfile_content) {
362 println!("ā Gagal membuat Dockerfile: {}", e);
363 return;
364 }
365 println!("ā
Dockerfile berhasil dibuat.");
366 }
367
368 let app_name = std::env::var("BUILD_NAME")
370 .or_else(|_| std::env::var("APP_NAME"))
371 .unwrap_or_else(|_| get_cargo_package_name())
372 .to_lowercase();
373
374 let image_tag = if custom_tag.is_empty() {
375 format!("{}:latest", app_name)
376 } else {
377 custom_tag.to_string()
378 };
379
380 let core_context = if std::path::Path::new("../rustbasic-core").exists() {
381 "core=../rustbasic-core"
382 } else {
383 "core=."
384 };
385
386 println!("\nš³ Memulai Docker build...");
388 println!(" Image tag: {}", image_tag);
389 println!(" Running: docker build --build-context {} -t {} .", core_context, image_tag);
390
391 let mut cmd = Command::new("docker")
392 .args(["build", "--build-context", core_context, "-t", &image_tag, "."])
393 .stdin(std::process::Stdio::inherit())
394 .stdout(std::process::Stdio::inherit())
395 .stderr(std::process::Stdio::inherit())
396 .spawn()
397 .expect("Gagal menjalankan docker build");
398
399 let status = cmd.wait().expect("Gagal menunggu docker build");
400
401 if status.success() {
402 println!("\nā
Docker build selesai dengan sukses!");
403 println!("š¦ Image: {}", image_tag);
404 println!("\n Jalankan container (Development/Lokal):");
405 println!(" docker run -p 4000:4000 --env-file .env {}", image_tag);
406 println!("\n Jalankan container (Produksi/Server - Auto Restart):");
407 println!(" docker run -d -p 80:4000 --restart unless-stopped --env-file .env {}", image_tag);
408 } else {
409 println!("\nā Docker build gagal.");
410 }
411}
412
413pub fn build_interactive(args: &[String]) {
415 let mut build_docker_flag = args.iter().any(|arg| arg == "--docker");
416 let mut build_desktop = args.iter().any(|arg| arg == "--desktop");
417 let mut build_android = args.iter().any(|arg| arg == "--android");
418 let mut release_mode = args.iter().any(|arg| arg == "--release" || arg == "-r");
419 let mut target_type = String::new(); let mut docker_tag = String::new();
421
422 for i in 0..args.len() {
424 if args[i] == "--type" && i + 1 < args.len() {
425 target_type = args[i+1].to_lowercase();
426 }
427 if args[i] == "--tag" && i + 1 < args.len() {
428 docker_tag = args[i+1].clone();
429 }
430 }
431
432 if !build_docker_flag && !build_desktop && !build_android {
433 let is_native_installed = crate::packages::read_manifest()
434 .packages
435 .iter()
436 .any(|pkg| pkg.name == "rustbasic-native");
437
438 println!("š ļø RustBasic Build CLI");
439 println!("Pilih platform target untuk di-build:");
440 println!(" [1] Docker (Container Image)");
441
442 let max_choice = if is_native_installed {
443 println!(" [2] Desktop Wrapper (Windows, macOS, Linux)");
444 println!(" [3] Android Wrapper (APK, AAB)");
445 3
446 } else {
447 1
448 };
449
450 let prompt_str = format!("š Pilih nomor platform (1-{}): ", max_choice);
451 let choice = crate::utils::prompt_choice(&prompt_str, 1, max_choice);
452 match choice {
453 1 => build_docker_flag = true,
454 2 => build_desktop = true,
455 3 => build_android = true,
456 _ => {}
457 }
458 }
459
460 if build_docker_flag {
461 build_docker(&docker_tag);
462 } else if build_desktop {
463 let mut target_os = String::new();
464 for i in 0..args.len() {
465 if args[i] == "--os" && i + 1 < args.len() {
466 target_os = args[i+1].clone();
467 }
468 }
469
470 let mut target_triple = "";
471 if target_os.is_empty() {
472 println!("\nPilih OS Target Desktop:");
473 println!(" [1] Current OS (Sistem saat ini)");
474 println!(" [2] macOS Intel (x86_64)");
475 println!(" [3] macOS Apple Silicon (aarch64)");
476 println!(" [4] Windows MSVC (x86_64-pc-windows-msvc)");
477 println!(" [5] Windows GNU (x86_64-pc-windows-gnu - Rekomendasi Cross-compile dari macOS/Linux)");
478 println!(" [6] Linux (x86_64-unknown-linux-gnu)");
479 let choice = crate::utils::prompt_choice("š Pilih nomor target OS (1-6): ", 1, 6);
480 match choice {
481 2 => target_triple = "x86_64-apple-darwin",
482 3 => target_triple = "aarch64-apple-darwin",
483 4 => target_triple = "x86_64-pc-windows-msvc",
484 5 => target_triple = "x86_64-pc-windows-gnu",
485 6 => target_triple = "x86_64-unknown-linux-gnu",
486 _ => {}
487 }
488 } else {
489 match target_os.as_str() {
490 "macos-intel" | "macos_intel" => target_triple = "x86_64-apple-darwin",
491 "macos-silicon" | "macos_silicon" => target_triple = "aarch64-apple-darwin",
492 "macos" => {
493 #[cfg(target_arch = "aarch64")]
494 { target_triple = "aarch64-apple-darwin"; }
495 #[cfg(not(target_arch = "aarch64"))]
496 { target_triple = "x86_64-apple-darwin"; }
497 }
498 "windows" => target_triple = "x86_64-pc-windows-msvc",
499 "windows-gnu" => target_triple = "x86_64-pc-windows-gnu",
500 "linux" => target_triple = "x86_64-unknown-linux-gnu",
501 _ => {
502 println!("ā ļø Warning: Target OS '{}' tidak dikenal, menggunakan default OS saat ini.", target_os);
503 }
504 }
505 }
506
507 if !args.iter().any(|arg| arg == "--release" || arg == "-r" || arg == "--debug" || arg == "-d") {
508 println!("\nPilih Mode Build:");
509 println!(" [1] Debug (Cepat compile)");
510 println!(" [2] Release (Optimasi penuh)");
511 let choice = crate::utils::prompt_choice("š Pilih nomor mode (1-2): ", 1, 2);
512 if choice == 2 {
513 release_mode = true;
514 }
515 }
516
517 println!("\nš„ļø Memulai proses build Desktop...");
518 let mut build_args = vec!["build", "--bin", "rustbasic-desktop", "--features", "desktop"];
519 if release_mode {
520 build_args.push("--release");
521 }
522 if !target_triple.is_empty() {
523 build_args.push("--target");
524 build_args.push(target_triple);
525 let _ = Command::new("rustup")
526 .args(["target", "add", target_triple])
527 .status();
528 }
529
530 println!(" Running: cargo {}", build_args.join(" "));
531 let mut cmd = Command::new("cargo")
532 .args(&build_args)
533 .stdin(std::process::Stdio::inherit())
534 .stdout(std::process::Stdio::inherit())
535 .stderr(std::process::Stdio::inherit())
536 .spawn()
537 .expect("Gagal menjalankan cargo build");
538
539 let status = cmd.wait().expect("Gagal menunggu proses cargo build");
540 if status.success() {
541 println!("\nā
Build Desktop selesai dengan sukses!");
542 let mode_str = if release_mode { "release" } else { "debug" };
543 let path_str = if target_triple.is_empty() {
544 format!("target/{}/rustbasic-desktop", mode_str)
545 } else {
546 format!("target/{}/{}/rustbasic-desktop", target_triple, mode_str)
547 };
548 println!("š Output biner: {}", path_str);
549 } else {
550 println!("\nā Build Desktop gagal.");
551 }
552
553 } else if build_android {
554 let mut is_aab = false;
555 if target_type.is_empty() {
556 println!("\nPilih Format Output Android:");
557 println!(" [1] APK (Android Package - Siap install)");
558 println!(" [2] AAB (Android App Bundle - Siap Google Play)");
559 let choice = crate::utils::prompt_choice("š Pilih format (1-2): ", 1, 2);
560 if choice == 2 {
561 is_aab = true;
562 }
563 } else {
564 is_aab = target_type == "aab";
565 }
566
567 if !args.iter().any(|arg| arg == "--release" || arg == "-r" || arg == "--debug" || arg == "-d") {
568 println!("\nPilih Mode Build:");
569 println!(" [1] Debug");
570 println!(" [2] Release (Produksi)");
571 let choice = crate::utils::prompt_choice("š Pilih nomor mode (1-2): ", 1, 2);
572 if choice == 2 {
573 release_mode = true;
574 }
575 }
576
577 println!("\nšØ Membangun JNI library untuk Android...");
578 if !compile_jni_libraries() {
579 println!("ā Gagal membangun JNI libraries.");
580 return;
581 }
582
583 let os = std::env::consts::OS;
585 let home = std::env::var("HOME").unwrap_or_default();
586 let android_home = if let Ok(val) = std::env::var("ANDROID_HOME") {
587 val
588 } else {
589 if os == "macos" {
590 format!("{}/Library/Android/sdk", home)
591 } else {
592 format!("{}/Android/Sdk", home)
593 }
594 };
595 unsafe {
596 std::env::set_var("ANDROID_HOME", &android_home);
597 }
598
599 if std::env::var("JAVA_HOME").is_err() {
600 let studio_jbr = if os == "macos" {
601 let mut jbr = "/Applications/Android Studio.app/Contents/jbr/Contents/Home".to_string();
602 if !Path::new(&jbr).exists() {
603 jbr = "/Applications/Android Studio.app/Contents/jre/Contents/Home".to_string();
604 }
605 jbr
606 } else {
607 let mut jbr = "/opt/android-studio/jbr".to_string();
608 if !Path::new(&jbr).exists() {
609 jbr = "/usr/local/android-studio/jbr".to_string();
610 }
611 jbr
612 };
613 if Path::new(&studio_jbr).exists() {
614 unsafe {
615 std::env::set_var("JAVA_HOME", &studio_jbr);
616 }
617 }
618 }
619
620 let local_props = Path::new("native/android/local.properties");
621 if !local_props.exists()
622 && let Ok(mut file) = fs::File::create(local_props) {
623 let _ = writeln!(file, "sdk.dir={}", android_home);
624 }
625
626 let gradle_task = match (is_aab, release_mode) {
627 (false, false) => "assembleDebug",
628 (false, true) => "assembleRelease",
629 (true, false) => "bundleDebug",
630 (true, true) => "bundleRelease",
631 };
632
633 println!("\nšØ Membangun target Android via Gradle (task: {})...", gradle_task);
634
635 let mut build_cmd = if Path::new("native/android/gradlew").exists() {
636 let mut cmd = Command::new("./gradlew");
637 cmd.arg(gradle_task);
638 cmd.current_dir("native/android");
639 cmd
640 } else {
641 let mut cmd = Command::new("gradle");
642 cmd.arg(gradle_task);
643 cmd.current_dir("native/android");
644 cmd
645 };
646
647 let spawn_res = build_cmd
648 .stdin(std::process::Stdio::inherit())
649 .stdout(std::process::Stdio::inherit())
650 .stderr(std::process::Stdio::inherit())
651 .spawn();
652
653 if let Ok(mut child) = spawn_res {
654 let status = child.wait().expect("Gagal menunggu Gradle build");
655 if status.success() {
656 println!("\nā
Build Android selesai dengan sukses!");
657 let output_dir = if is_aab {
658 let mode_folder = if release_mode { "release" } else { "debug" };
659 format!("native/android/app/build/outputs/bundle/{}", mode_folder)
660 } else {
661 let mode_folder = if release_mode { "release" } else { "debug" };
662 format!("native/android/app/build/outputs/apk/{}", mode_folder)
663 };
664 println!("š Folder output: {}", output_dir);
665 } else {
666 println!("\nā Gradle build gagal.");
667 }
668 } else {
669 println!("ā Gagal mengeksekusi Gradle wrapper. Pastikan Java dan Gradle wrapper terkonfigurasi dengan benar.");
670 }
671 }
672}
673
674pub fn run_native(run_android: bool, run_desktop: bool) {
676 if run_android {
677 println!("š Memulai RustBasic Android Wrapper...");
678
679 let os = std::env::consts::OS;
680 let home = std::env::var("HOME").unwrap_or_default();
681 let android_home = if let Ok(val) = std::env::var("ANDROID_HOME") {
682 val
683 } else {
684 if os == "macos" {
685 format!("{}/Library/Android/sdk", home)
686 } else {
687 format!("{}/Android/Sdk", home)
688 }
689 };
690
691 let mut custom_java_home = None;
692 if cfg!(target_os = "macos") {
694 let mac_studio_jdk = "/Applications/Android Studio.app/Contents/jbr/Contents/Home";
695 if Path::new(mac_studio_jdk).exists() {
696 custom_java_home = Some(mac_studio_jdk.to_string());
697 }
698 } else if cfg!(target_os = "windows") {
699 let win_paths = [
700 "C:\\Program Files\\Android\\Android Studio\\jbr",
701 "C:\\Program Files\\Android\\Android Studio\\jre",
702 ];
703 for path in &win_paths {
704 if Path::new(path).exists() {
705 custom_java_home = Some(path.to_string());
706 break;
707 }
708 }
709 } else {
710 let unix_paths = [
712 "/opt/android-studio/jbr",
713 "/opt/android-studio/jre",
714 "/snap/android-studio/current/jbr",
715 "/snap/android-studio/current/jre",
716 "/usr/local/android-studio/jbr",
717 "/usr/local/android-studio/jre",
718 "/usr/lib/jvm/default-java",
719 ];
720 for path in &unix_paths {
721 if Path::new(path).exists() {
722 custom_java_home = Some(path.to_string());
723 break;
724 }
725 }
726 }
727
728 let adb = get_adb_bin();
729 let mut devices = get_adb_devices();
730 if devices.is_empty() {
731 println!("š± Perangkat Android atau emulator tidak terdeteksi aktif.");
732 let mut emulator_bin = format!("{}/emulator/emulator", android_home);
733 if !Path::new(&emulator_bin).exists() {
734 let fallback = format!("{}/tools/emulator", android_home);
735 if Path::new(&fallback).exists() {
736 emulator_bin = fallback;
737 } else {
738 emulator_bin = "emulator".to_string();
739 }
740 }
741
742 let avd_output = Command::new(&emulator_bin).arg("-list-avds").output();
743 if let Ok(avd_out) = avd_output {
744 let avds_str = String::from_utf8_lossy(&avd_out.stdout);
745 if let Some(avd_name) = avds_str.lines().next() {
746 println!("š Menyalakan emulator AVD: {}...", avd_name);
747 let _ = Command::new(&emulator_bin)
748 .args(["-avd", avd_name])
749 .stdout(std::process::Stdio::null())
750 .stderr(std::process::Stdio::null())
751 .spawn();
752
753 println!("ā³ Menunggu emulator menyala dan terdeteksi adb...");
754 let _ = Command::new(&adb).arg("wait-for-device").status();
755 println!("ā
Emulator berhasil aktif!");
756 std::thread::sleep(std::time::Duration::from_secs(3));
757 devices = get_adb_devices();
758 }
759 }
760 }
761
762 let (device_id, device_name) = if devices.len() == 1 {
763 let d = devices[0].clone();
764 println!("š± Menggunakan perangkat tunggal: {} ({})", d.1, d.0);
765 d
766 } else if devices.len() > 1 {
767 println!("š± Terdeteksi beberapa perangkat Android. Silakan pilih target:");
768 for (idx, d) in devices.iter().enumerate() {
769 println!(" [{}] {} ({})", idx + 1, d.1, d.0);
770 }
771 let choice = crate::utils::prompt_choice("š Pilih nomor perangkat: ", 1, devices.len());
772 devices[choice - 1].clone()
773 } else {
774 println!("ā Error: Tidak ada perangkat Android terhubung.");
775 return;
776 };
777
778 if !compile_jni_libraries() {
780 return;
781 }
782
783 let local_props = Path::new("native/android/local.properties");
785 if !local_props.exists() {
786 if let Ok(mut file) = fs::File::create(local_props) {
787 let _ = writeln!(file, "sdk.dir={}", android_home);
788 }
789 }
790
791 println!("šØ Membangun debug APK menggunakan Gradle...");
793 let gradlew_bin = if cfg!(target_os = "windows") { "gradlew.bat" } else { "./gradlew" };
794 let mut gradle_cmd = Command::new(gradlew_bin);
795 gradle_cmd.arg("assembleDebug");
796 gradle_cmd.current_dir("native/android");
797
798 if let Some(jh) = &custom_java_home {
799 gradle_cmd.env("JAVA_HOME", jh);
800 }
801
802 let gradle_status = gradle_cmd.status();
803 if gradle_status.is_err() || !gradle_status.unwrap().success() {
804 println!("ā Gradle build assembleDebug gagal.");
805 return;
806 }
807
808 println!("šØ Memasang APK ke perangkat {} ({})...", device_name, device_id);
810 let install_status = Command::new(&adb)
811 .args(["-s", &device_id, "install", "-r", "native/android/app/build/outputs/apk/debug/app-debug.apk"])
812 .status();
813
814 if install_status.is_err() || !install_status.unwrap().success() {
815 println!("ā Gagal memasang APK ke device.");
816 return;
817 }
818
819 let vite_port = "5173"; let reverse_status = Command::new(&adb)
822 .args(["-s", &device_id, "reverse", &format!("tcp:{}", vite_port), &format!("tcp:{}", vite_port)])
823 .status();
824 if reverse_status.is_err() {
825 println!("ā ļø Warning: Gagal melakukan adb reverse port {}", vite_port);
826 }
827
828 println!("š Membuka aplikasi di perangkat {}...", device_name);
830 let _ = Command::new(&adb)
831 .args(["-s", &device_id, "logcat", "-c"])
832 .status();
833
834 let _ = Command::new(&adb)
835 .args(["-s", &device_id, "shell", "am", "start", "-n", "com.rustbasic.mobile/com.rustbasic.mobile.MainActivity"])
836 .status();
837
838 println!("š Menampilkan log realtime dari perangkat {} (Tekan Ctrl+C untuk keluar)...", device_name);
839 let mut logcat_cmd = Command::new(&adb);
840 logcat_cmd.args(["-s", &device_id, "logcat", "-s", "RustBasicServer"]);
841 let mut child = logcat_cmd.spawn().expect("Gagal menjalankan adb logcat");
842 let _ = child.wait();
843 } else if run_desktop {
844 println!("š Memulai RustBasic Desktop Wrapper...");
845 let mut cmd = Command::new("cargo");
846 cmd.args(["run", "--bin", "rustbasic-desktop", "--features", "desktop"]);
847 let status = cmd.status();
848 match status {
849 Ok(s) if s.success() => {}
850 _ => {
851 println!("ā Gagal menjalankan Desktop Wrapper.");
852 }
853 }
854 }
855}
856
857fn prompt_string(prompt: &str, default: &str) -> String {
858 print!("{}", prompt);
859 let _ = std::io::stdout().flush();
860 let mut input = String::new();
861 if std::io::stdin().read_line(&mut input).is_ok() {
862 let trimmed = input.trim();
863 if trimmed.is_empty() {
864 default.to_string()
865 } else {
866 trimmed.to_string()
867 }
868 } else {
869 default.to_string()
870 }
871}
872
873pub fn deploy_interactive() {
874 println!("\n{}", "š RustBasic Docker Deploy CLI".magenta().bold());
875 println!("{}", "------------------------------".magenta());
876
877 let image_name = prompt_string("š Masukkan Nama/Tag Docker Image (default: rustbasic:latest): ", "rustbasic:latest");
879
880 let container_name = std::env::var("DOCKER_CONTAINER_NAME").unwrap_or_else(|_| {
882 let base_name = image_name
883 .split(':')
884 .next()
885 .unwrap_or("rustbasic")
886 .split('/')
887 .last()
888 .unwrap_or("rustbasic")
889 .to_lowercase();
890 format!("{}-app", base_name)
891 });
892
893 let inspect = Command::new("docker")
895 .args(["image", "inspect", &image_name])
896 .stdout(std::process::Stdio::null())
897 .stderr(std::process::Stdio::null())
898 .status();
899
900 match inspect {
901 Ok(status) if status.success() => {}
902 _ => {
903 println!("{}", format!("ā ļø Peringatan: Image '{}' tidak ditemukan lokal.", image_name).yellow());
904 let proceed = prompt_string("š Tetap lanjutkan proses ekspor? (y/n) [default: n]: ", "n");
905 if proceed.to_lowercase() != "y" {
906 println!("ā Proses dihentikan.");
907 return;
908 }
909 }
910 }
911
912 println!("š¦ Mengekspor Docker image '{}' ke 'rustbasic.tar'...", image_name);
914 let save_status = Command::new("docker")
915 .args(["save", "-o", "rustbasic.tar", &image_name])
916 .status();
917
918 match save_status {
919 Ok(status) if status.success() => {
920 println!("{}", "ā
Image berhasil diekspor ke rustbasic.tar.".green());
921 }
922 _ => {
923 println!("{}", "ā Gagal mengekspor Docker image.".red().bold());
924 return;
925 }
926 }
927
928 println!("\n{}", "š Konfigurasi Pengiriman ke Server".cyan().bold());
930 println!("{}", "-----------------------------------".cyan());
931 let ssh_user = prompt_string("š Masukkan SSH Username Server (contoh: root) [default: root]: ", "root");
932 let ssh_ip = prompt_string("š Masukkan IP Address Server: ", "");
933 if ssh_ip.is_empty() {
934 println!("{}", "ā IP Address server tidak boleh kosong.".red().bold());
935 let _ = fs::remove_file("rustbasic.tar");
936 return;
937 }
938 let ssh_port = prompt_string("š Masukkan SSH Port Server (default: 22): ", "22");
939 let dest_dir = prompt_string("š Masukkan Folder Tujuan di Server (default: ~/app): ", "~/app");
940 let server_port = prompt_string("š Masukkan Port Server Mapping (contoh: 80:4000) [default: 80:4000]: ", "80:4000");
941 let env_file = prompt_string("š Masukkan File Env yang akan dikirim (default: .env): ", ".env");
942
943 println!("\nš Menyiapkan folder tujuan di server...");
944 let mkdir_status = Command::new("ssh")
945 .args([
946 "-p", &ssh_port,
947 &format!("{}@{}", ssh_user, ssh_ip),
948 &format!("mkdir -p {}", dest_dir)
949 ])
950 .status();
951
952 match mkdir_status {
953 Ok(status) if status.success() => {}
954 _ => {
955 println!("{}", "ā Gagal terhubung ke server menggunakan SSH.".red().bold());
956 let _ = fs::remove_file("rustbasic.tar");
957 return;
958 }
959 }
960
961 println!("š Mengirimkan berkas rustbasic.tar & {} ke server...", env_file);
962 let scp_status = Command::new("scp")
963 .args([
964 "-P", &ssh_port,
965 "rustbasic.tar", &env_file,
966 &format!("{}@{}:{}", ssh_user, ssh_ip, dest_dir)
967 ])
968 .status();
969
970 let _ = fs::remove_file("rustbasic.tar");
972
973 if let Ok(status) = scp_status {
974 if !status.success() {
975 println!("{}", "ā Gagal mengirimkan berkas via SCP.".red().bold());
976 return;
977 }
978 } else {
979 println!("{}", "ā Gagal menjalankan SCP.".red().bold());
980 return;
981 }
982
983 println!("{}", "ā
Pengiriman berkas berhasil!".green());
984
985 let auto_run = prompt_string("\nš Apakah Anda ingin langsung menjalankan container di server secara otomatis? (y/n) [default: y]: ", "y");
987 if auto_run.to_lowercase() == "y" || auto_run.is_empty() {
988 println!("\nš Memuat image di server (docker load)...");
989 let load_status = Command::new("ssh")
990 .args([
991 "-p", &ssh_port,
992 &format!("{}@{}", ssh_user, ssh_ip),
993 &format!("docker load -i {}/rustbasic.tar", dest_dir)
994 ])
995 .status();
996
997 match load_status {
998 Ok(status) if status.success() => {
999 println!("{}", "ā
Image berhasil dimuat di server.".green());
1000 }
1001 _ => {
1002 println!("{}", "ā Gagal memuat image di server.".red().bold());
1003 return;
1004 }
1005 }
1006
1007 println!("š Menghentikan & menghapus container lama '{}' jika ada...", container_name);
1008 let stop_status = Command::new("ssh")
1009 .args([
1010 "-p", &ssh_port,
1011 &format!("{}@{}", ssh_user, ssh_ip),
1012 &format!("docker stop {} || true && docker rm {} || true", container_name, container_name)
1013 ])
1014 .status();
1015
1016 if let Err(e) = stop_status {
1017 println!("ā ļø Peringatan saat membersihkan container lama: {}", e);
1018 }
1019
1020 println!("š Menjalankan container baru '{}'...", container_name);
1021 let run_cmd = format!(
1022 "docker run -d --name {} -p {} --restart unless-stopped --env-file {}/.env {}",
1023 container_name, server_port, dest_dir, image_name
1024 );
1025 let run_status = Command::new("ssh")
1026 .args([
1027 "-p", &ssh_port,
1028 &format!("{}@{}", ssh_user, ssh_ip),
1029 &run_cmd
1030 ])
1031 .status();
1032
1033 match run_status {
1034 Ok(status) if status.success() => {
1035 println!("{}", format!("š Container '{}' berhasil dijalankan di server!", container_name).green().bold());
1036 }
1037 _ => {
1038 println!("{}", "ā Gagal menjalankan container di server.".red().bold());
1039 return;
1040 }
1041 }
1042
1043 println!("š Membersihkan file tar di server...");
1044 let rm_status = Command::new("ssh")
1045 .args([
1046 "-p", &ssh_port,
1047 &format!("{}@{}", ssh_user, ssh_ip),
1048 &format!("rm {}/rustbasic.tar", dest_dir)
1049 ])
1050 .status();
1051
1052 if let Err(e) = rm_status {
1053 println!("ā ļø Peringatan saat membersihkan file tar di server: {}", e);
1054 }
1055
1056 println!("\n{}", "š Deployment selesai!".green().bold());
1057 println!("{}", "--------------------------------------------------------".green());
1058 println!("Untuk melihat log aplikasi di server, jalankan:");
1059 println!("ssh -p {} {}@{} \"docker logs -f {}\"", ssh_port, ssh_user, ssh_ip, container_name);
1060 println!("{}", "--------------------------------------------------------".green());
1061 } else {
1062 println!("\n{}", "š„ļø Langkah Selanjutnya di Server Anda:".cyan().bold());
1063 println!("{}", "--------------------------------------------------------".green());
1064 println!("1. Hubungkan ke server via SSH:");
1065 println!(" ssh -p {} {}@{}", ssh_port, ssh_user, ssh_ip);
1066 println!("");
1067 println!("2. Masuk ke folder tujuan:");
1068 println!(" cd {}", dest_dir);
1069 println!("");
1070 println!("3. Muat (load) image Docker dari berkas tar:");
1071 println!(" docker load -i rustbasic.tar");
1072 println!("");
1073 println!("4. Jalankan container dengan fitur auto-restart:");
1074 println!(" docker run -d --name {} -p {} --restart unless-stopped --env-file .env {}", container_name, server_port, image_name);
1075 println!("");
1076 println!("5. Hapus file tar di server untuk menghemat disk:");
1077 println!(" rm rustbasic.tar");
1078 println!("{}", "--------------------------------------------------------".green());
1079 }
1080}
1081
1082fn get_adb_bin() -> String {
1083 if let Ok(output) = Command::new("adb").arg("devices").output() {
1085 if output.status.success() {
1086 return "adb".to_string();
1087 }
1088 }
1089
1090 let os = std::env::consts::OS;
1092 let home = std::env::var("HOME").unwrap_or_default();
1093 let android_home = if let Ok(val) = std::env::var("ANDROID_HOME") {
1094 val
1095 } else {
1096 if os == "macos" {
1097 format!("{}/Library/Android/sdk", home)
1098 } else if os == "windows" {
1099 let local_app_data = std::env::var("LOCALAPPDATA").unwrap_or_default();
1100 format!("{}\\Android\\Sdk", local_app_data)
1101 } else {
1102 format!("{}/Android/Sdk", home)
1103 }
1104 };
1105
1106 let adb_name = if os == "windows" { "adb.exe" } else { "adb" };
1107 let path = Path::new(&android_home).join("platform-tools").join(adb_name);
1108 if path.exists() {
1109 path.display().to_string()
1110 } else {
1111 "adb".to_string() }
1113}
1114
1115fn get_adb_devices() -> Vec<(String, String)> {
1116 let adb = get_adb_bin();
1117 let output = Command::new(&adb).arg("devices").output();
1118 let mut devices = Vec::new();
1119 if let Ok(out) = output {
1120 let stdout = String::from_utf8_lossy(&out.stdout);
1121 for line in stdout.lines() {
1122 let line = line.trim();
1123 if line.is_empty() || line.starts_with("List of devices") {
1124 continue;
1125 }
1126 let parts: Vec<&str> = line.split_whitespace().collect();
1127 if parts.len() >= 2 && parts[1] == "device" {
1128 let device_id = parts[0].to_string();
1129 let model_out = Command::new(&adb)
1130 .args(["-s", &device_id, "shell", "getprop", "ro.product.model"])
1131 .output();
1132 let model = if let Ok(m_out) = model_out {
1133 String::from_utf8_lossy(&m_out.stdout).trim().to_string()
1134 } else {
1135 "Unknown Device".to_string()
1136 };
1137 devices.push((device_id, model));
1138 }
1139 }
1140 }
1141 devices
1142}
1143
1144fn compile_jni_libraries() -> bool {
1145 println!("š Building Rust library for Android (Native Rust implementation)...");
1146
1147 let _ = Command::new("rustup")
1148 .args(["target", "add", "aarch64-linux-android", "armv7-linux-androideabi", "x86_64-linux-android"])
1149 .status();
1150
1151 let home = std::env::var("HOME").unwrap_or_default();
1152 let android_ndk_home = if let Ok(val) = std::env::var("ANDROID_NDK_HOME") {
1153 val
1154 } else {
1155 let mac_ndk = format!("{}/Library/Android/sdk/ndk", home);
1156 if Path::new(&mac_ndk).exists() {
1157 if let Ok(entries) = fs::read_dir(&mac_ndk) {
1158 let mut paths: Vec<_> = entries.flatten().map(|e| e.path()).collect();
1159 paths.sort();
1160 if let Some(highest) = paths.last() {
1161 highest.display().to_string()
1162 } else {
1163 "".to_string()
1164 }
1165 } else {
1166 "".to_string()
1167 }
1168 } else {
1169 "".to_string()
1170 }
1171 };
1172
1173 if android_ndk_home.is_empty() {
1174 println!("ā Error: ANDROID_NDK_HOME is not set. Please set ANDROID_NDK_HOME.");
1175 return false;
1176 }
1177
1178 println!("Using NDK: {}", android_ndk_home);
1179
1180 let os = std::env::consts::OS;
1181 let toolchain_sub = if os == "macos" { "darwin-x86_64" } else { "linux-x86_64" };
1182 let toolchain_bin_path = Path::new(&android_ndk_home)
1183 .join("toolchains/llvm/prebuilt")
1184 .join(toolchain_sub)
1185 .join("bin");
1186
1187 if !toolchain_bin_path.exists() {
1188 println!("ā Error: Toolchain bin path not found: {}", toolchain_bin_path.display());
1189 return false;
1190 }
1191
1192 let sqlite_version = "3450100";
1193 let sqlite_dir = format!("target/sqlite-amalgamation-{}", sqlite_version);
1194 if !Path::new(&sqlite_dir).exists() {
1195 println!("š„ Downloading SQLite source amalgamation...");
1196 fs::create_dir_all("target").ok();
1197
1198 let zip_path = "target/sqlite.zip";
1199 let sqlite_url = format!("https://www.sqlite.org/2024/sqlite-amalgamation-{}.zip", sqlite_version);
1200
1201 let curl_status = Command::new("curl")
1202 .args(["-sSLo", zip_path, &sqlite_url])
1203 .status();
1204
1205 if curl_status.is_err() || !curl_status.unwrap().success() {
1206 println!("ā Gagal men-download SQLite source.");
1207 return false;
1208 }
1209
1210 let unzip_status = Command::new("unzip")
1211 .args(["-q", zip_path, "-d", "target/"])
1212 .status();
1213
1214 let _ = fs::remove_file(zip_path);
1215
1216 if unzip_status.is_err() || !unzip_status.unwrap().success() {
1217 println!("ā Gagal mengekstrak SQLite source.");
1218 return false;
1219 }
1220 }
1221
1222 let targets = vec![
1223 ("aarch64-linux-android", "arm64-v8a", "aarch64-linux-android21-clang"),
1224 ("armv7-linux-androideabi", "armeabi-v7a", "armv7a-linux-androideabi21-clang"),
1225 ("x86_64-linux-android", "x86_64", "x86_64-linux-android21-clang"),
1226 ];
1227
1228 let jnilibs_dir = "native/android/app/src/main/jniLibs";
1229
1230 for (target, arch, clang_name) in targets {
1231 println!("šØ Preparing SQLite static library for {}...", target);
1232
1233 let clang_path = toolchain_bin_path.join(clang_name);
1234 let ar_path = toolchain_bin_path.join("llvm-ar");
1235
1236 if !clang_path.exists() {
1237 println!("ā Error: Compiler not found: {}", clang_path.display());
1238 return false;
1239 }
1240
1241 let sqlite_out = format!("target/{}/sqlite", target);
1242 fs::create_dir_all(&sqlite_out).ok();
1243
1244 let libsqlite3_a = format!("{}/libsqlite3.a", sqlite_out);
1245 if !Path::new(&libsqlite3_a).exists() {
1246 println!(" Compiling SQLite static lib for {}...", target);
1247 let sqlite3_o = format!("{}/sqlite3.o", sqlite_out);
1248 let sqlite3_c = format!("{}/sqlite3.c", sqlite_dir);
1249
1250 let compile_status = Command::new(&clang_path)
1251 .args(["-O2", "-c", &sqlite3_c, "-o", &sqlite3_o])
1252 .status();
1253
1254 if compile_status.is_err() || !compile_status.unwrap().success() {
1255 println!("ā Gagal mengompilasi sqlite3.o");
1256 return false;
1257 }
1258
1259 let archive_status = Command::new(&ar_path)
1260 .args(["rcs", &libsqlite3_a, &sqlite3_o])
1261 .status();
1262
1263 if archive_status.is_err() || !archive_status.unwrap().success() {
1264 println!("ā Gagal mengarsip libsqlite3.a");
1265 return false;
1266 }
1267 }
1268
1269 println!("šØ Compiling Rust library for {}...", target);
1270 let mut cargo_cmd = Command::new("cargo");
1271 cargo_cmd.args(["build", "--target", target, "--release"]);
1272
1273 let clang_path_str = clang_path.display().to_string();
1274 let ar_path_str = ar_path.display().to_string();
1275
1276 let target_upper = target.replace("-", "_").to_uppercase();
1277 let linker_env = format!("CARGO_TARGET_{}_LINKER", target_upper);
1278 let cc_env = format!("CC_{}", target.replace("-", "_"));
1279 let ar_env = format!("AR_{}", target.replace("-", "_"));
1280
1281 cargo_cmd.env(&linker_env, &clang_path_str);
1282 cargo_cmd.env(&cc_env, &clang_path_str);
1283 cargo_cmd.env(&ar_env, &ar_path_str);
1284
1285 let cargo_status = cargo_cmd.status();
1286 if cargo_status.is_err() || !cargo_status.unwrap().success() {
1287 println!("ā Gagal mengompilasi library Rust untuk target {}", target);
1288 return false;
1289 }
1290
1291 let dest_dir = format!("{}/{}", jnilibs_dir, arch);
1292 fs::create_dir_all(&dest_dir).ok();
1293
1294 let src_so = format!("target/{}/release/librustbasic.so", target);
1295 let dest_so = format!("{}/librustbasic_mobile.so", dest_dir);
1296
1297 if let Err(e) = fs::copy(&src_so, &dest_so) {
1298 println!("ā Gagal menyalin {}: {}", src_so, e);
1299 return false;
1300 }
1301 }
1302
1303 println!("ā
Android JNI libraries built successfully!");
1304 true
1305}