1use crate::app::Config;
2use crate::schema::MigratorTrait;
3use crate::seeder::SeederTrait;
4use crate::colored::Colorize;
5
6pub async fn handle<
11 M: MigratorTrait + Send + Sync + 'static,
12 S: SeederTrait + Send + Sync + 'static,
13>(
14 args: &[String],
15 cfg: &Config,
16 seeder: Option<S>,
17) -> bool {
18 if args.len() < 2 {
19 return false;
20 }
21
22 let command = args[1].as_str();
23
24 let is_migration_cmd = command.starts_with("migrate") || command == "db:seed";
25 let is_storage_cmd = command == "storage:link";
26 let is_server_cmd = command == "server" || command == "serve";
27 let is_build_cmd = command == "build";
28 let is_make_cmd = command.starts_with("make:");
29 let is_route_cmd = command == "route:list";
30 let is_key_cmd = command == "key:generate";
31 let is_deploy_cmd = command == "deploy";
32 let is_publish_cmd = command == "publish";
33
34 if is_server_cmd {
36 let run_android = args.iter().any(|arg| arg == "--android");
37 let run_desktop = args.iter().any(|arg| arg == "--desktop");
38 if run_android || run_desktop {
39 run_native(run_android, run_desktop);
40 return true;
41 }
42 return false; }
44
45 if !is_migration_cmd && !is_storage_cmd && !is_build_cmd
46 && !is_make_cmd && !is_route_cmd && !is_key_cmd && !is_deploy_cmd
47 && !is_publish_cmd {
48 return false;
49 }
50
51 println!("š ļø RustBasic CLI - Command: {}", command);
52
53 if is_build_cmd {
54 handle_build(args).await;
55 return true;
56 }
57
58 if is_deploy_cmd {
59 handle_deploy().await;
60 return true;
61 }
62
63 if is_storage_cmd {
64 handle_storage_link(cfg);
65 return true;
66 }
67
68 if is_make_cmd {
69 handle_make(args);
70 return true;
71 }
72
73 if is_route_cmd {
74 handle_route_list();
75 return true;
76 }
77
78 if is_key_cmd {
79 handle_key_generate();
80 return true;
81 }
82
83 if is_publish_cmd {
84 let target = args.get(2).map(|s| s.as_str()).unwrap_or("");
85 handle_publish(target);
86 return true;
87 }
88
89 let pool = crate::database::connect(cfg).await;
91
92 match command {
93 "migrate" => {
94 println!("š Menjalankan migrasi database...");
95 if let Err(e) = M::up(&pool, None).await {
96 println!("ā Gagal menjalankan migrasi: {}", e);
97 } else {
98 println!("ā
Migrasi selesai!");
99 }
100 }
101 "migrate:refresh" => {
102 println!("š Mereset dan menjalankan ulang migrasi...");
103 if let Err(e) = M::fresh(&pool).await {
104 println!("ā Gagal refresh migrasi: {}", e);
105 } else {
106 println!("ā
Database berhasil di-refresh!");
107 }
108 }
109 "migrate:back" | "migrate:rollback" => {
110 println!("ā¬
ļø Rollback migrasi terakhir...");
111 if let Err(e) = M::down(&pool, None).await {
112 println!("ā Gagal rollback: {}", e);
113 } else {
114 println!("ā
Rollback berhasil!");
115 }
116 }
117 "db:seed" => {
118 println!("š± Menjalankan database seeder...");
119 match seeder {
120 Some(s) => {
121 if let Err(e) = s.run(&pool).await {
122 println!("ā Seeder gagal: {}", e);
123 } else {
124 println!("ā
Seeder selesai!");
125 }
126 }
127 None => {
128 println!("ā ļø Tidak ada seeder yang terdaftar.");
129 }
130 }
131 }
132 _ => return false,
133 }
134
135 true
136}
137
138fn handle_key_generate() {
142 let key = crate::rand::random_alphanumeric(32);
143 let encoded = crate::base64::encode(key.as_bytes());
144
145 let env_path = std::path::Path::new(".env");
147 if env_path.exists() {
148 let content = std::fs::read_to_string(env_path).unwrap_or_default();
149 let new_content = if content.contains("APP_KEY=") {
150 let mut lines: Vec<String> = content
151 .lines()
152 .map(|line| {
153 if line.starts_with("APP_KEY=") {
154 format!("APP_KEY={}", encoded)
155 } else {
156 line.to_string()
157 }
158 })
159 .collect();
160 if !lines.last().map(|l| l.is_empty()).unwrap_or(true) {
162 lines.push(String::new());
163 }
164 lines.join("\n")
165 } else {
166 format!("{}\nAPP_KEY={}\n", content.trim_end(), encoded)
167 };
168 if let Err(e) = std::fs::write(env_path, new_content) {
169 println!("ā Gagal menulis ke .env: {}", e);
170 return;
171 }
172 println!("ā
APP_KEY berhasil dibuat dan disimpan ke .env");
173 println!(" Key: {}", encoded);
174 } else {
175 println!("ā ļø File .env tidak ditemukan. Key yang dibuat:");
176 println!(" APP_KEY={}", encoded);
177 }
178}
179
180fn handle_route_list() {
184 let route_files = ["src/routes/web.rs", "src/routes/api.rs"];
186 let mut routes: Vec<(String, String, String)> = Vec::new(); for file_path in &route_files {
189 if let Ok(content) = std::fs::read_to_string(file_path) {
190 for line in content.lines() {
191 let line = line.trim();
192 if line.starts_with(".route(") || line.contains(".route(\"") {
195 parse_route_line(line, &mut routes);
196 }
197 }
198 }
199 }
200
201 if routes.is_empty() {
202 println!("ā¹ļø Tidak ada rute ditemukan atau format rute tidak dikenal.");
203 println!(" Cek file src/routes/web.rs dan src/routes/api.rs");
204 return;
205 }
206
207 println!("\n{}", "=".repeat(72));
209 println!(" {:<8} {:<35} {}", "METHOD", "PATH", "HANDLER");
210 println!("{}", "=".repeat(72));
211 for (method, path, handler) in &routes {
212 println!(" {:<8} {:<35} {}", method, path, handler);
213 }
214 println!("{}\n", "=".repeat(72));
215 println!(" Total: {} rute terdaftar", routes.len());
216}
217
218fn parse_route_line(line: &str, routes: &mut Vec<(String, String, String)>) {
219 let methods = ["get", "post", "put", "patch", "delete"];
221
222 let path = if let Some(start) = line.find(".route(\"") {
224 let after = &line[start + 8..];
225 if let Some(end) = after.find('"') {
226 after[..end].to_string()
227 } else { return; }
228 } else { return; };
229
230 for method in &methods {
232 let pattern = format!("{}(", method);
233 if let Some(pos) = line.find(&pattern) {
234 let after = &line[pos + method.len() + 1..];
235 let handler = if let Some(end) = after.find(')') {
236 after[..end].to_string()
237 } else {
238 after.to_string()
239 };
240 routes.push((method.to_uppercase(), path.clone(), handler));
241 break;
242 }
243 }
244}
245
246fn handle_make(args: &[String]) {
250 let subcommand = args[1].as_str(); let name = args.get(2).map(|s| s.as_str()).unwrap_or("");
252
253 if name.is_empty() {
254 println!("ā Nama diperlukan. Contoh: rustbasic {} MyName", subcommand);
255 return;
256 }
257
258 match subcommand {
259 "make:controller" => make_controller(name),
260 "make:model" => {
261 let with_migration = args.iter().any(|a| a == "-m" || a == "--migration");
262 make_model(name, with_migration);
263 }
264 "make:middleware" => make_middleware(name),
265 "make:observer" => {
266 let model = args.iter().find(|a| a.starts_with("--model="))
267 .and_then(|a| a.strip_prefix("--model="))
268 .unwrap_or("Model");
269 make_observer(name, model);
270 }
271 "make:service" => make_service(name),
272 "make:seeder" => make_seeder(name),
273 "make:migration" => make_migration(name),
274 _ => {
275 println!("ā Subcommand tidak dikenal: {}", subcommand);
276 }
277 }
278}
279
280fn to_snake_case(s: &str) -> String {
282 let mut out = String::new();
283 for (i, c) in s.chars().enumerate() {
284 if c.is_uppercase() && i > 0 {
285 out.push('_');
286 }
287 out.push(c.to_ascii_lowercase());
288 }
289 out
290}
291
292fn write_file(path: &str, content: &str) {
294 let p = std::path::Path::new(path);
295 if let Some(parent) = p.parent() {
296 if let Err(e) = std::fs::create_dir_all(parent) {
297 println!("ā Gagal membuat direktori {}: {}", parent.display(), e);
298 return;
299 }
300 }
301 if p.exists() {
302 println!("ā ļø File sudah ada, dilewati: {}", path);
303 return;
304 }
305 if let Err(e) = std::fs::write(p, content) {
306 println!("ā Gagal membuat file {}: {}", path, e);
307 } else {
308 println!("ā
Dibuat: {}", path);
309 }
310}
311
312fn register_mod(mod_file: &str, module_name: &str) {
314 let line = format!("pub mod {};\n", module_name);
315 let content = std::fs::read_to_string(mod_file).unwrap_or_default();
316 if content.contains(&line) { return; }
317
318 let label = "// š LABEL: MODULE";
319 let new_content = if let Some(pos) = content.find(label) {
320 let nl = content[pos..].find('\n').map(|n| pos + n + 1).unwrap_or(content.len());
322 format!("{}{}{}", &content[..nl], &line, &content[nl..])
323 } else {
324 format!("{}{}", content, &line)
325 };
326
327 if let Err(e) = std::fs::write(mod_file, new_content) {
328 println!("ā ļø Gagal mendaftarkan modul di {}: {}", mod_file, e);
329 } else {
330 println!(" š Daftar di: {}", mod_file);
331 }
332}
333
334fn make_controller(name: &str) {
335 let snake = to_snake_case(name);
336 let file_name = if snake.ends_with("_controller") {
337 snake.clone()
338 } else {
339 format!("{}_controller", snake)
340 };
341 let struct_name = name.to_string();
342 let path = format!("src/app/http/controllers/{}.rs", file_name);
343
344 let content = format!(r#"use rustbasic_core::requests::Request;
345use rustbasic_core::IntoResponse;
346
347pub async fn index(_req: Request) -> impl IntoResponse {{
348 "Hello from {struct_name}Controller!"
349}}
350
351pub async fn show(_req: Request) -> impl IntoResponse {{
352 "Show action"
353}}
354
355pub async fn store(_req: Request) -> impl IntoResponse {{
356 "Store action"
357}}
358
359pub async fn update(_req: Request) -> impl IntoResponse {{
360 "Update action"
361}}
362
363pub async fn destroy(_req: Request) -> impl IntoResponse {{
364 "Destroy action"
365}}
366"#);
367
368 write_file(&path, &content);
369 register_mod("src/app/http/controllers/mod.rs", &file_name);
370}
371
372fn make_model(name: &str, with_migration: bool) {
373 let snake = to_snake_case(name);
374 let path = format!("src/app/models/{}.rs", snake);
375
376 let content = format!(r#"use rustbasic_core::model;
377
378model! {{
379 table: "{snake}s",
380 fillable: [],
381 Model {{
382 pub id: i32,
383 pub created_at: Option<String>,
384 pub updated_at: Option<String>,
385 }}
386}}
387
388impl Model {{
389 pub fn to_resource(&self) -> rustbasic_core::serde_json::Value {{
390 rustbasic_core::serde_json::json!({{
391 "id": self.id,
392 }})
393 }}
394}}
395"#);
396
397 write_file(&path, &content);
398 register_mod("src/app/models/mod.rs", &snake);
399
400 if with_migration {
401 make_migration(&format!("create_{}_table", snake));
402 }
403}
404
405fn make_middleware(name: &str) {
406 let snake = to_snake_case(name);
407 let path = format!("src/app/http/middleware/{}.rs", snake);
408
409 let content = format!(r#"use rustbasic_core::requests::Request;
410use rustbasic_core::router::{{Response, Next}};
411
412pub async fn {snake}_middleware(req: Request, next: Next) -> Response {{
413 // Logika middleware sebelum handler
414 let res = next.run(req).await;
415 // Logika middleware setelah handler
416 res
417}}
418"#);
419
420 write_file(&path, &content);
421 register_mod("src/app/http/middleware/mod.rs", &snake);
422}
423
424fn make_observer(name: &str, model: &str) {
425 let snake = to_snake_case(name);
426 let obs_name = if snake.ends_with("_observer") {
427 snake.clone()
428 } else {
429 format!("{}_observer", snake)
430 };
431 let path = format!("src/app/observers/{}.rs", obs_name);
432
433 let content = format!(r#"use crate::app::models::{snake}::Model as {model};
434
435pub struct {name}Observer;
436
437impl {name}Observer {{
438 pub async fn created(&self, model: &{model}) {{
439 // Dipanggil setelah model dibuat
440 let _ = model;
441 }}
442
443 pub async fn updated(&self, model: &{model}) {{
444 // Dipanggil setelah model diupdate
445 let _ = model;
446 }}
447
448 pub async fn deleted(&self, model: &{model}) {{
449 // Dipanggil setelah model dihapus
450 let _ = model;
451 }}
452}}
453"#);
454
455 write_file(&path, &content);
456}
457
458fn make_service(name: &str) {
459 let snake = to_snake_case(name);
460 let svc_name = if snake.ends_with("_service") {
461 snake.clone()
462 } else {
463 format!("{}_service", snake)
464 };
465 let path = format!("src/app/services/{}.rs", svc_name);
466
467 let content = format!(r#"use rustbasic_core::sql::AnyPool;
468
469pub struct {name}Service<'a> {{
470 pub db: &'a AnyPool,
471}}
472
473impl<'a> {name}Service<'a> {{
474 pub fn new(db: &'a AnyPool) -> Self {{
475 Self {{ db }}
476 }}
477
478 pub async fn execute(&self) -> Result<(), String> {{
479 // Implementasi logika bisnis di sini
480 Ok(())
481 }}
482}}
483"#);
484
485 write_file(&path, &content);
486}
487
488fn make_seeder(name: &str) {
489 let snake = to_snake_case(name);
490 let seed_name = if snake.ends_with("_seeder") {
491 snake.clone()
492 } else {
493 format!("{}_seeder", snake)
494 };
495 let path = format!("database/seeders/{}.rs", seed_name);
496
497 let content = format!(r#"use rustbasic_core::sql::AnyPool;
498
499pub async fn run(db: &AnyPool) {{
500 println!("š± Menjalankan {}...");
501 // Contoh:
502 // rustbasic_core::database::execute(db, "INSERT INTO ...", ()).await;
503}}
504"#, seed_name);
505
506 write_file(&path, &content);
507}
508
509fn make_migration(name: &str) {
510 use std::time::{SystemTime, UNIX_EPOCH};
511
512 let ts = SystemTime::now()
513 .duration_since(UNIX_EPOCH)
514 .map(|d| d.as_secs())
515 .unwrap_or(0);
516
517 let migration_name = format!("m{}_{}", ts, to_snake_case(name));
519 let path = format!("database/migrations/{}.rs", migration_name);
520
521 let content = format!(r#"use rustbasic_core::schema::{{Schema, MigrationTrait, DbErr}};
522use rustbasic_core::sql::AnyPool;
523use rustbasic_core::async_trait;
524
525pub struct {migration_name};
526
527#[async_trait]
528impl MigrationTrait for {migration_name} {{
529 async fn up(&self, db: &AnyPool) -> Result<(), DbErr> {{
530 Schema::create("{}", db, |t| {{
531 t.id();
532 t.timestamps();
533 }}).await
534 }}
535
536 async fn down(&self, db: &AnyPool) -> Result<(), DbErr> {{
537 Schema::drop_if_exists("{}", db).await
538 }}
539}}
540"#, to_snake_case(name), to_snake_case(name));
541
542 write_file(&path, &content);
543}
544
545fn handle_storage_link(cfg: &Config) {
549 let target = "public/storage";
550 let source = "storage/app/public";
551
552 if let Err(e) = std::fs::create_dir_all(source) {
553 println!("ā Gagal membuat direktori storage: {}", e);
554 return;
555 }
556
557 let path = std::path::Path::new(target);
558 if path.exists() || path.is_symlink() {
559 println!("ā¹ļø Link 'public/storage' sudah ada.");
560 return;
561 }
562
563 println!("š Membuat symbolic link...");
564
565 #[cfg(unix)]
566 {
567 use std::os::unix::fs::symlink;
568 if let Err(e) = symlink("../storage/app/public", target) {
569 println!("ā Gagal membuat symlink: {}", e);
570 } else {
571 println!("ā
Link storage berhasil! [public/storage -> storage/app/public]");
572 }
573 }
574
575 #[cfg(windows)]
576 {
577 use std::os::windows::fs::symlink_dir;
578 if let Err(e) = symlink_dir("../storage/app/public", target) {
579 println!("ā Gagal membuat symlink: {}", e);
580 } else {
581 println!("ā
Link storage berhasil! [public/storage -> storage/app/public]");
582 }
583 }
584
585 let _ = cfg;
586}
587
588fn setup_java_home() {
592 if std::env::var("JAVA_HOME").is_err() {
593 let mut custom_java_home: Option<String> = None;
594 let os = std::env::consts::OS;
595 if os == "macos" {
596 let paths = vec![
597 "/Applications/Android Studio.app/Contents/jbr/Contents/Home",
598 "/Applications/Android Studio.app/Contents/jre/Contents/Home",
599 "/Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home",
600 ];
601 for path in &paths {
602 if std::path::Path::new(path).exists() {
603 custom_java_home = Some(path.to_string());
604 break;
605 }
606 }
607 } else if os == "windows" {
608 let win_paths = [
609 "C:\\Program Files\\Android\\Android Studio\\jbr",
610 "C:\\Program Files\\Android\\Android Studio\\jre",
611 ];
612 for path in &win_paths {
613 if std::path::Path::new(path).exists() {
614 custom_java_home = Some(path.to_string());
615 break;
616 }
617 }
618 } else {
619 let unix_paths = [
621 "/opt/android-studio/jbr",
622 "/opt/android-studio/jre",
623 "/snap/android-studio/current/jbr",
624 "/snap/android-studio/current/jre",
625 "/usr/local/android-studio/jbr",
626 "/usr/local/android-studio/jre",
627 "/usr/lib/jvm/default-java",
628 ];
629 for path in &unix_paths {
630 if std::path::Path::new(path).exists() {
631 custom_java_home = Some(path.to_string());
632 break;
633 }
634 }
635 }
636 if let Some(jh) = custom_java_home {
637 unsafe {
638 std::env::set_var("JAVA_HOME", &jh);
639 }
640 }
641 }
642}
643
644fn run_native(run_android: bool, run_desktop: bool) {
645 if run_android {
646 println!("š Memulai RustBasic Android Wrapper (Native implementation)...");
647
648 let home = std::env::var("HOME").unwrap_or_default();
650 let android_home = std::env::var("ANDROID_HOME")
651 .unwrap_or_else(|_| format!("{}/Library/Android/sdk", home));
652 unsafe { std::env::set_var("ANDROID_HOME", &android_home); }
653
654 setup_java_home();
655
656 let mut devices = get_adb_devices();
657 if devices.is_empty() {
658 println!("š± Perangkat Android atau emulator tidak terdeteksi aktif.");
659 let emulator_bin = format!("{}/emulator/emulator", android_home);
660 if std::path::Path::new(&emulator_bin).exists() {
661 let avd_output = std::process::Command::new(&emulator_bin).arg("-list-avds").output();
662 if let Ok(avd_out) = avd_output {
663 let avds_str = String::from_utf8_lossy(&avd_out.stdout);
664 if let Some(avd_name) = avds_str.lines().next() {
665 println!("š Menyalakan emulator AVD: {}...", avd_name);
666 let _ = std::process::Command::new(&emulator_bin)
667 .args(["-avd", avd_name])
668 .stdout(std::process::Stdio::null())
669 .stderr(std::process::Stdio::null())
670 .spawn();
671
672 println!("ā³ Menunggu emulator menyala dan terdeteksi adb...");
673 let _ = std::process::Command::new("adb").arg("wait-for-device").status();
674 println!("ā
Emulator berhasil aktif!");
675 std::thread::sleep(std::time::Duration::from_secs(3));
676 devices = get_adb_devices();
677 }
678 }
679 }
680 }
681
682 let (device_id, device_name) = if devices.len() == 1 {
683 let d = devices[0].clone();
684 println!("š± Menggunakan perangkat tunggal: {} ({})", d.1, d.0);
685 d
686 } else if devices.len() > 1 {
687 println!("š± Terdeteksi beberapa perangkat Android. Silakan pilih target:");
688 for (idx, d) in devices.iter().enumerate() {
689 println!(" [{}] {} ({})", idx + 1, d.1, d.0);
690 }
691 let choice = prompt_choice("š Pilih nomor perangkat: ", 1, devices.len());
692 devices[choice - 1].clone()
693 } else {
694 println!("ā Error: Tidak ada perangkat Android terhubung.");
695 return;
696 };
697
698 if !compile_jni_libraries() {
700 return;
701 }
702
703 let local_props = std::path::Path::new("native/android/local.properties");
705 if !local_props.exists() {
706 if let Ok(mut file) = std::fs::File::create(local_props) {
707 use std::io::Write;
708 let _ = writeln!(file, "sdk.dir={}", android_home);
709 }
710 }
711
712 println!("šØ Membangun debug APK menggunakan Gradle...");
714 let gradlew_bin = if cfg!(target_os = "windows") { "gradlew.bat" } else { "./gradlew" };
715 let mut gradle_cmd = std::process::Command::new(gradlew_bin);
716 gradle_cmd.arg("assembleDebug");
717 gradle_cmd.current_dir("native/android");
718
719 if let Ok(jh) = std::env::var("JAVA_HOME") {
720 gradle_cmd.env("JAVA_HOME", jh);
721 }
722
723 let gradle_status = gradle_cmd.status();
724 if gradle_status.is_err() || !gradle_status.unwrap().success() {
725 println!("ā Gradle build assembleDebug gagal.");
726 return;
727 }
728
729 println!("šØ Memasang APK ke perangkat {} ({})...", device_name, device_id);
731 let install_status = std::process::Command::new("adb")
732 .args(["-s", &device_id, "install", "-r", "native/android/app/build/outputs/apk/debug/app-debug.apk"])
733 .status();
734
735 if install_status.is_err() || !install_status.unwrap().success() {
736 println!("ā Gagal memasang APK ke device.");
737 return;
738 }
739
740 let vite_port = "5173"; let reverse_status = std::process::Command::new("adb")
743 .args(["-s", &device_id, "reverse", &format!("tcp:{}", vite_port), &format!("tcp:{}", vite_port)])
744 .status();
745 if reverse_status.is_err() {
746 println!("ā ļø Warning: Gagal melakukan adb reverse port {}", vite_port);
747 }
748
749 println!("š Membuka aplikasi di perangkat {}...", device_name);
751 let _ = std::process::Command::new("adb")
752 .args(["-s", &device_id, "logcat", "-c"])
753 .status();
754
755 let _ = std::process::Command::new("adb")
756 .args(["-s", &device_id, "shell", "am", "start", "-n", "com.rustbasic.mobile/com.rustbasic.mobile.MainActivity"])
757 .status();
758
759 println!("š Menampilkan log realtime dari perangkat {} (Tekan Ctrl+C untuk keluar)...", device_name);
760 let mut logcat_cmd = std::process::Command::new("adb");
761 logcat_cmd.args(["-s", &device_id, "logcat", "-s", "RustBasicServer"]);
762 let mut child = logcat_cmd.spawn().expect("Gagal menjalankan adb logcat");
763 let _ = child.wait();
764 } else if run_desktop {
765 println!("š Memulai RustBasic Desktop Wrapper...");
766 let mut cmd = std::process::Command::new("cargo");
767 cmd.args(["run", "--bin", "rustbasic-desktop", "--features", "desktop"]);
768 let status = cmd.status();
769 match status {
770 Ok(s) if s.success() => {}
771 _ => {
772 println!("ā Gagal menjalankan Desktop Wrapper.");
773 }
774 }
775 }
776}
777
778fn get_adb_devices() -> Vec<(String, String)> {
779 let output = std::process::Command::new("adb").arg("devices").output();
780 let mut devices = Vec::new();
781 if let Ok(out) = output {
782 let stdout = String::from_utf8_lossy(&out.stdout);
783 for line in stdout.lines() {
784 if line.contains("device") && !line.contains("List of devices") {
785 let parts: Vec<&str> = line.split_whitespace().collect();
786 if parts.is_empty() { continue; }
787 let device_id = parts[0].to_string();
788 let model_out = std::process::Command::new("adb")
789 .args(["-s", &device_id, "shell", "getprop", "ro.product.model"])
790 .output();
791 let model = if let Ok(m_out) = model_out {
792 String::from_utf8_lossy(&m_out.stdout).trim().to_string()
793 } else {
794 "Unknown Device".to_string()
795 };
796 devices.push((device_id, model));
797 }
798 }
799 }
800 devices
801}
802
803fn compile_jni_libraries() -> bool {
804 println!("š Building Rust library for Android (Native Rust implementation)...");
805
806 let _ = std::process::Command::new("rustup")
807 .args(["target", "add", "aarch64-linux-android", "armv7-linux-androideabi", "x86_64-linux-android"])
808 .status();
809
810 let home = std::env::var("HOME").unwrap_or_default();
811 let android_ndk_home = if let Ok(val) = std::env::var("ANDROID_NDK_HOME") {
812 val
813 } else {
814 let mac_ndk = format!("{}/Library/Android/sdk/ndk", home);
815 if std::path::Path::new(&mac_ndk).exists() {
816 if let Ok(entries) = std::fs::read_dir(&mac_ndk) {
817 let mut paths: Vec<_> = entries.flatten().map(|e| e.path()).collect();
818 paths.sort();
819 if let Some(highest) = paths.last() {
820 highest.display().to_string()
821 } else {
822 "".to_string()
823 }
824 } else {
825 "".to_string()
826 }
827 } else {
828 "".to_string()
829 }
830 };
831
832 if android_ndk_home.is_empty() {
833 println!("ā Error: ANDROID_NDK_HOME is not set. Please set ANDROID_NDK_HOME.");
834 return false;
835 }
836
837 println!("Using NDK: {}", android_ndk_home);
838
839 let os = std::env::consts::OS;
840 let toolchain_sub = if os == "macos" { "darwin-x86_64" } else { "linux-x86_64" };
841 let toolchain_bin_path = std::path::Path::new(&android_ndk_home)
842 .join("toolchains/llvm/prebuilt")
843 .join(toolchain_sub)
844 .join("bin");
845
846 if !toolchain_bin_path.exists() {
847 println!("ā Error: Toolchain bin path not found: {}", toolchain_bin_path.display());
848 return false;
849 }
850
851 let sqlite_version = "3450100";
852 let sqlite_dir = format!("target/sqlite-amalgamation-{}", sqlite_version);
853 if !std::path::Path::new(&sqlite_dir).exists() {
854 println!("š„ Downloading SQLite source amalgamation...");
855 std::fs::create_dir_all("target").ok();
856
857 let zip_path = "target/sqlite.zip";
858 let sqlite_url = format!("https://www.sqlite.org/2024/sqlite-amalgamation-{}.zip", sqlite_version);
859
860 let curl_status = std::process::Command::new("curl")
861 .args(["-sSLo", zip_path, &sqlite_url])
862 .status();
863
864 if curl_status.is_err() || !curl_status.unwrap().success() {
865 println!("ā Gagal men-download SQLite source.");
866 return false;
867 }
868
869 let unzip_status = std::process::Command::new("unzip")
870 .args(["-q", zip_path, "-d", "target/"])
871 .status();
872
873 let _ = std::fs::remove_file(zip_path);
874
875 if unzip_status.is_err() || !unzip_status.unwrap().success() {
876 println!("ā Gagal mengekstrak SQLite source.");
877 return false;
878 }
879 }
880
881 let targets = vec![
882 ("aarch64-linux-android", "arm64-v8a", "aarch64-linux-android21-clang"),
883 ("armv7-linux-androideabi", "armeabi-v7a", "armv7a-linux-androideabi21-clang"),
884 ("x86_64-linux-android", "x86_64", "x86_64-linux-android21-clang"),
885 ];
886
887 let jnilibs_dir = "native/android/app/src/main/jniLibs";
888
889 for (target, arch, clang_name) in targets {
890 println!("šØ Preparing SQLite static library for {}...", target);
891
892 let clang_path = toolchain_bin_path.join(clang_name);
893 let ar_path = toolchain_bin_path.join("llvm-ar");
894
895 if !clang_path.exists() {
896 println!("ā Error: Compiler not found: {}", clang_path.display());
897 return false;
898 }
899
900 let sqlite_out = format!("target/{}/sqlite", target);
901 std::fs::create_dir_all(&sqlite_out).ok();
902
903 let libsqlite3_a = format!("{}/libsqlite3.a", sqlite_out);
904 if !std::path::Path::new(&libsqlite3_a).exists() {
905 println!(" Compiling SQLite static lib for {}...", target);
906 let sqlite3_o = format!("{}/sqlite3.o", sqlite_out);
907 let sqlite3_c = format!("{}/sqlite3.c", sqlite_dir);
908
909 let compile_status = std::process::Command::new(&clang_path)
910 .args(["-O2", "-c", &sqlite3_c, "-o", &sqlite3_o])
911 .status();
912
913 if compile_status.is_err() || !compile_status.unwrap().success() {
914 println!("ā Gagal mengompilasi sqlite3.o");
915 return false;
916 }
917
918 let archive_status = std::process::Command::new(&ar_path)
919 .args(["rcs", &libsqlite3_a, &sqlite3_o])
920 .status();
921
922 if archive_status.is_err() || !archive_status.unwrap().success() {
923 println!("ā Gagal mengarsip libsqlite3.a");
924 return false;
925 }
926 }
927
928 println!("šØ Compiling Rust library for {}...", target);
929 let mut cargo_cmd = std::process::Command::new("cargo");
930 cargo_cmd.args(["build", "--target", target, "--release"]);
931
932 let clang_path_str = clang_path.display().to_string();
933 let ar_path_str = ar_path.display().to_string();
934
935 let target_upper = target.replace("-", "_").to_uppercase();
936 let linker_env = format!("CARGO_TARGET_{}_LINKER", target_upper);
937 let cc_env = format!("CC_{}", target.replace("-", "_"));
938 let ar_env = format!("AR_{}", target.replace("-", "_"));
939
940 cargo_cmd.env(&linker_env, &clang_path_str);
941 cargo_cmd.env(&cc_env, &clang_path_str);
942 cargo_cmd.env(&ar_env, &ar_path_str);
943
944 let cargo_status = cargo_cmd.status();
945 if cargo_status.is_err() || !cargo_status.unwrap().success() {
946 println!("ā Gagal mengompilasi library Rust untuk target {}", target);
947 return false;
948 }
949
950 let dest_dir = format!("{}/{}", jnilibs_dir, arch);
951 std::fs::create_dir_all(&dest_dir).ok();
952
953 let src_so = format!("target/{}/release/librustbasic.so", target);
954 let dest_so = format!("{}/librustbasic_mobile.so", dest_dir);
955
956 if let Err(e) = std::fs::copy(&src_so, &dest_so) {
957 println!("ā Gagal menyalin {}: {}", src_so, e);
958 return false;
959 }
960 }
961
962 println!("ā
Android JNI libraries built successfully!");
963 true
964}
965
966fn prompt_choice(prompt: &str, min: usize, max: usize) -> usize {
970 use std::io::{self, Write};
971 loop {
972 print!("{}", prompt);
973 let _ = io::stdout().flush();
974 let mut input = String::new();
975 if io::stdin().read_line(&mut input).is_ok() {
976 if let Ok(choice) = input.trim().parse::<usize>() {
977 if choice >= min && choice <= max {
978 return choice;
979 }
980 }
981 }
982 println!("ā ļø Pilihan tidak valid, silakan coba lagi.");
983 }
984}
985
986pub async fn handle_build(args: &[String]) {
987 let mut build_docker = args.iter().any(|a| a == "--docker");
988 let mut build_desktop = args.iter().any(|a| a == "--desktop");
989 let mut build_android = args.iter().any(|a| a == "--android");
990 let release_mode = args.iter().any(|a| a == "--release" || a == "-r");
991 let mut target_type = String::new();
992 let mut docker_tag = String::new();
993 let mut docker_platform = String::new();
994
995 for i in 0..args.len() {
996 if args[i] == "--type" && i + 1 < args.len() { target_type = args[i+1].to_lowercase(); }
997 if args[i] == "--tag" && i + 1 < args.len() { docker_tag = args[i+1].clone(); }
998 if args[i] == "--platform" && i + 1 < args.len() { docker_platform = args[i+1].clone(); }
999 }
1000
1001 if !build_docker && !build_desktop && !build_android {
1002 let is_native_installed = if let Ok(content) = std::fs::read_to_string(".rustbasic_packages.json") {
1003 content.contains("\"rustbasic-native\"")
1004 } else {
1005 false
1006 };
1007
1008 println!("š ļø RustBasic Build CLI");
1009 println!("Pilih platform target untuk di-build:");
1010 println!(" [1] Docker (Container Image)");
1011
1012 let max_choice = if is_native_installed {
1013 println!(" [2] Desktop Wrapper (Windows, macOS, Linux)");
1014 println!(" [3] Android Wrapper (APK, AAB)");
1015 3
1016 } else {
1017 1
1018 };
1019
1020 let prompt_str = format!("š Pilih nomor platform (1-{}): ", max_choice);
1021 match prompt_choice(&prompt_str, 1, max_choice) {
1022 1 => build_docker = true,
1023 2 => build_desktop = true,
1024 3 => build_android = true,
1025 _ => {}
1026 }
1027 }
1028
1029 if build_docker {
1030 let mut extract = args.iter().any(|a| a == "--extract" || a == "-e");
1031 if docker_platform.is_empty() {
1032 println!("\nPilih Platform Target CPU Docker:");
1033 println!(" [1] Current Host Platform (Sesuai OS komputer Anda)");
1034 println!(" [2] Linux AMD64 / x86_64 (Standard VPS Intel/AMD - Umum/Rekomendasi)");
1035 println!(" [3] Linux ARM64 / aarch64 (Server berbasis ARM / AWS Graviton)");
1036 match prompt_choice("š Pilih (1-3): ", 1, 3) {
1037 2 => docker_platform = "linux/amd64".to_string(),
1038 3 => docker_platform = "linux/arm64".to_string(),
1039 _ => {}
1040 }
1041 }
1042
1043 let host_arch = std::env::consts::ARCH;
1045 let is_mismatch = (host_arch == "aarch64" && docker_platform == "linux/amd64")
1046 || (host_arch == "x86_64" && docker_platform == "linux/arm64");
1047
1048 if is_mismatch {
1049 println!("\nā ļø {}", "PERINGATAN: Arsitektur CPU Mismatch (Sangat Lambat)".yellow().bold());
1050 println!(" Anda berada di host dengan CPU '{}' tetapi memilih target Docker '{}'.", host_arch, docker_platform);
1051 println!(" Docker akan menggunakan emulasi CPU (QEMU) yang membuat proses kompilasi");
1052 println!(" Rust berjalan {} (bisa memakan waktu 10-30 menit).", "10x-20x LEBIH LAMBAT".red().bold());
1053 println!(" ");
1054 println!(" š” {} Kami telah mengaktifkan target caching untuk mempercepat build.", "TIPS:".cyan().bold());
1055 println!(" Build pertama tetap lambat, namun build berikutnya akan sangat cepat (1-2 menit)");
1056 println!(" karena target directory dan dependency cache disimpan oleh Docker.");
1057 println!(" ");
1058 let proceed = prompt_string("š Apakah Anda ingin melanjutkan proses build? (y/n) [default: y]: ", "y");
1059 if !proceed.to_lowercase().starts_with('y') {
1060 println!("ā Build dibatalkan oleh pengguna.");
1061 return;
1062 }
1063 }
1064
1065 if !extract && !args.iter().any(|a| a == "--docker") {
1066 let extract_choice = prompt_string("\nš Apakah Anda ingin mengekstrak biner Linux hasil build ke folder './build-output'? (y/n) [default: n]: ", "n");
1067 extract = extract_choice.to_lowercase().starts_with('y');
1068 }
1069
1070 build_docker_image(&docker_tag, &docker_platform, extract).await;
1071 } else if build_desktop {
1072 build_desktop_binary(args, release_mode).await;
1073 } else if build_android {
1074 build_android_apk(args, &target_type, release_mode).await;
1075 }
1076}
1077
1078async fn build_docker_image(custom_tag: &str, platform: &str, extract_binary: bool) {
1079 if !std::process::Command::new("docker").arg("version")
1081 .stdout(std::process::Stdio::null()).stderr(std::process::Stdio::null())
1082 .status().map(|s| s.success()).unwrap_or(false)
1083 {
1084 println!("ā Docker tidak ditemukan. Install: https://docs.docker.com/get-docker/");
1085 return;
1086 }
1087
1088 let dockerfile_path = std::path::Path::new("Dockerfile");
1089 if !dockerfile_path.exists() {
1090 println!("š Membuat Dockerfile...");
1091 let is_monorepo = std::path::Path::new("../rustbasic-core").exists() || std::path::Path::new("rustbasic-core").exists();
1092 let binary_name = get_cargo_package_name();
1093
1094 let dockerfile_content = if is_monorepo {
1095 r#"# ============================================================
1096# RustBasic Docker Build ā Standalone (Cached)
1097# ============================================================
1098
1099# Stage 1: Builder
1100FROM rust:1-slim-bookworm AS builder
1101
1102RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
1103 --mount=type=cache,target=/var/lib/apt,sharing=locked \
1104 apt-get update && apt-get install -y \
1105 pkg-config libssl-dev
1106
1107# Copy rustbasic-core (dari konteks workspace root)
1108WORKDIR /rustbasic-core
1109COPY --from=core . .
1110
1111# Copy proyek utama rustbasic
1112WORKDIR /build
1113COPY . .
1114
1115# Build release binary using Cargo registry, git cache, and target cache
1116ENV CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse
1117RUN --mount=type=cache,target=/usr/local/cargo/registry \
1118 --mount=type=cache,target=/usr/local/cargo/git \
1119 --mount=type=cache,target=/build/target \
1120 cargo build --release --bin rustbasic && \
1121 cp target/release/rustbasic /build/rustbasic-bin
1122
1123# Stage 2: Runtime
1124FROM debian:bookworm-slim
1125
1126RUN apt-get update && apt-get install -y \
1127 ca-certificates libssl3 \
1128 && rm -rf /var/lib/apt/lists/*
1129
1130WORKDIR /app
1131
1132# Copy binary dari builder stage
1133COPY --from=builder /build/rustbasic-bin ./rustbasic
1134
1135# Copy assets yang diperlukan dari builder stage (lebih aman dan bersih)
1136COPY --from=builder /build/src/resources/views/ src/resources/views/
1137COPY --from=builder /build/src/dist/ src/dist/
1138COPY --from=builder /build/public/ public/
1139COPY --from=builder /build/database/ database/
1140COPY --from=builder /build/.env.example .env
1141
1142# Expose port aplikasi
1143EXPOSE 4000
1144
1145CMD ["./rustbasic"]
1146"#.to_string()
1147 } else {
1148 format!(r#"# ============================================================
1149# RustBasic Docker Build ā Standalone (Cached)
1150# ============================================================
1151
1152# Stage 1: Builder
1153FROM rust:1-slim-bookworm AS builder
1154
1155RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
1156 --mount=type=cache,target=/var/lib/apt,sharing=locked \
1157 apt-get update && apt-get install -y \
1158 pkg-config libssl-dev
1159
1160WORKDIR /build
1161
1162COPY . .
1163
1164# Build release binary using Cargo registry, git cache, and target cache
1165ENV CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse
1166RUN --mount=type=cache,target=/usr/local/cargo/registry \
1167 --mount=type=cache,target=/usr/local/cargo/git \
1168 --mount=type=cache,target=/build/target \
1169 cargo build --release --bin {bin_name} && \
1170 cp target/release/{bin_name} /build/{bin_name}-bin
1171
1172# Stage 2: Runtime
1173FROM debian:bookworm-slim
1174
1175RUN apt-get update && apt-get install -y \
1176 ca-certificates libssl3 \
1177 && rm -rf /var/lib/apt/lists/*
1178
1179WORKDIR /app
1180
1181# Copy binary dari builder stage
1182COPY --from=builder /build/{bin_name}-bin ./{bin_name}
1183
1184# Copy assets yang diperlukan dari builder stage
1185COPY --from=builder /build/src/resources/views/ src/resources/views/
1186COPY --from=builder /build/src/dist/ src/dist/
1187COPY --from=builder /build/public/ public/
1188COPY --from=builder /build/database/ database/
1189COPY --from=builder /build/.env.example .env
1190
1191# Expose port aplikasi
1192EXPOSE 4000
1193
1194CMD ["./{bin_name}"]
1195"#, bin_name = binary_name)
1196 };
1197
1198 if let Err(e) = std::fs::write(dockerfile_path, dockerfile_content) {
1199 println!("ā Gagal membuat Dockerfile: {}", e);
1200 return;
1201 }
1202 println!("ā
Dockerfile berhasil dibuat.");
1203 }
1204
1205 let app_name = std::env::var("APP_NAME").unwrap_or_else(|_| get_cargo_package_name()).to_lowercase();
1206 let image_tag = if custom_tag.is_empty() { format!("{}:latest", app_name) } else { custom_tag.to_string() };
1207
1208 if std::path::Path::new("package.json").exists() {
1209 println!("š¦ Mengompilasi frontend assets (npm run build)...");
1210 let npm_cmd = if cfg!(windows) { "npm.cmd" } else { "npm" };
1211 let success = match std::process::Command::new(npm_cmd)
1212 .args(&["run", "build"])
1213 .status()
1214 {
1215 Ok(status) => status.success(),
1216 Err(_) => {
1217 println!("ā Gagal menjalankan npm. Pastikan Node.js & npm terinstal di sistem Anda.");
1218 return;
1219 }
1220 };
1221
1222 if !success {
1223 println!("ā Gagal mengompilasi frontend assets. Silakan periksa error di atas.");
1224 return;
1225 }
1226 }
1227
1228 println!("\nš³ Docker build dimulai...");
1229 println!(" Image: {}", image_tag);
1230
1231 let core_context = if std::path::Path::new("../rustbasic-core").exists() {
1232 "core=../rustbasic-core".to_string()
1233 } else {
1234 "core=.".to_string()
1235 };
1236
1237 let mut build_args = vec![
1238 "build".to_string(),
1239 "--build-context".to_string(),
1240 core_context,
1241 ];
1242
1243 if !platform.is_empty() {
1244 build_args.push("--platform".to_string());
1245 build_args.push(platform.to_string());
1246 println!(" Platform: {}", platform);
1247 }
1248
1249 build_args.push("-t".to_string());
1250 build_args.push(image_tag.clone());
1251 build_args.push(".".to_string());
1252
1253 println!(" Running: docker {}", build_args.join(" "));
1254
1255 let mut cmd = std::process::Command::new("docker")
1256 .args(&build_args)
1257 .stdin(std::process::Stdio::inherit())
1258 .stdout(std::process::Stdio::inherit())
1259 .stderr(std::process::Stdio::inherit())
1260 .spawn().expect("Gagal menjalankan docker build");
1261
1262 let success = cmd.wait().map(|s| s.success()).unwrap_or(false);
1263
1264 if success {
1265 println!("\nā
Docker build selesai! Image: {}", image_tag);
1266
1267 if extract_binary {
1268 println!("š¦ Mengekstrak biner Linux dari image Docker...");
1269 let container_name = format!("temp-extract-{}", std::time::SystemTime::now()
1270 .duration_since(std::time::UNIX_EPOCH)
1271 .unwrap_or_default()
1272 .as_secs());
1273
1274 let create_res = std::process::Command::new("docker")
1276 .args(["create", "--name", &container_name, &image_tag])
1277 .status();
1278
1279 if create_res.is_ok() && create_res.unwrap().success() {
1280 let _ = std::fs::create_dir_all("build-output");
1282
1283 let binary_name = get_cargo_package_name();
1285
1286 let cp_status = std::process::Command::new("docker")
1288 .args(["cp", &format!("{}:/app/{}", container_name, binary_name), &format!("build-output/{}", binary_name)])
1289 .status();
1290
1291 let _ = std::process::Command::new("docker")
1293 .args(["rm", &container_name])
1294 .status();
1295
1296 if cp_status.is_ok() && cp_status.unwrap().success() {
1297 println!("ā
Biner berhasil diekstrak ke: {}", format!("build-output/{}", binary_name).cyan().bold());
1298 } else {
1299 println!("ā Gagal mengekstrak biner dari container.");
1300 }
1301 } else {
1302 println!("ā Gagal membuat temporary container untuk ekstraksi.");
1303 }
1304 }
1305
1306 println!(" Jalankan container (Lokal/Development):");
1307 if !platform.is_empty() {
1308 println!(" docker run --platform {} -p 4000:4000 --env-file .env {}", platform, image_tag);
1309 } else {
1310 println!(" docker run -p 4000:4000 --env-file .env {}", image_tag);
1311 }
1312 println!(" Jalankan container (Produksi/Server - Auto Restart):");
1313 if !platform.is_empty() {
1314 println!(" docker run --platform {} -d -p 80:4000 --restart unless-stopped --env-file .env {}", platform, image_tag);
1315 } else {
1316 println!(" docker run -d -p 80:4000 --restart unless-stopped --env-file .env {}", image_tag);
1317 }
1318 } else {
1319 println!("\nā Docker build gagal.");
1320 }
1321}
1322
1323async fn build_desktop_binary(args: &[String], mut release_mode: bool) {
1324 let mut target_triple = "";
1325
1326 for i in 0..args.len() {
1327 if args[i] == "--os" && i + 1 < args.len() {
1328 target_triple = match args[i+1].as_str() {
1329 "macos-intel" => "x86_64-apple-darwin",
1330 "macos-silicon" => "aarch64-apple-darwin",
1331 "windows" => "x86_64-pc-windows-msvc",
1332 "windows-gnu" => "x86_64-pc-windows-gnu",
1333 "linux" => "x86_64-unknown-linux-gnu",
1334 _ => "",
1335 };
1336 }
1337 }
1338
1339 if target_triple.is_empty() && !args.iter().any(|a| a.starts_with("--os")) {
1340 println!("\nPilih OS Target Desktop:");
1341 println!(" [1] Current OS");
1342 println!(" [2] macOS Intel (x86_64-apple-darwin)");
1343 println!(" [3] macOS Apple Silicon (aarch64-apple-darwin)");
1344 println!(" [4] Windows MSVC (x86_64-pc-windows-msvc)");
1345 println!(" [5] Windows GNU (x86_64-pc-windows-gnu - Rekomendasi Cross-compile dari macOS/Linux)");
1346 println!(" [6] Linux (x86_64-unknown-linux-gnu)");
1347 match prompt_choice("š Pilih (1-6): ", 1, 6) {
1348 2 => target_triple = "x86_64-apple-darwin",
1349 3 => target_triple = "aarch64-apple-darwin",
1350 4 => target_triple = "x86_64-pc-windows-msvc",
1351 5 => target_triple = "x86_64-pc-windows-gnu",
1352 6 => target_triple = "x86_64-unknown-linux-gnu",
1353 _ => {}
1354 }
1355 }
1356
1357 if !args.iter().any(|a| a == "--release" || a == "-r" || a == "--debug" || a == "-d") {
1358 println!("\n [1] Debug\n [2] Release");
1359 if prompt_choice("š Mode (1-2): ", 1, 2) == 2 { release_mode = true; }
1360 }
1361
1362 let mut build_args = vec!["build", "--bin", "rustbasic-desktop", "--features", "desktop"];
1363 if release_mode { build_args.push("--release"); }
1364 if !target_triple.is_empty() {
1365 build_args.push("--target");
1366 build_args.push(target_triple);
1367 let _ = std::process::Command::new("rustup").args(["target", "add", target_triple]).status();
1368 }
1369
1370 println!("\nš„ļø Desktop build: cargo {}", build_args.join(" "));
1371 let mut cmd = std::process::Command::new("cargo")
1372 .args(&build_args)
1373 .stdin(std::process::Stdio::inherit())
1374 .stdout(std::process::Stdio::inherit())
1375 .stderr(std::process::Stdio::inherit())
1376 .spawn().expect("Gagal menjalankan cargo build");
1377
1378 if cmd.wait().map(|s| s.success()).unwrap_or(false) {
1379 println!("\nā
Desktop build selesai!");
1380 } else {
1381 println!("\nā Desktop build gagal.");
1382 }
1383}
1384
1385async fn build_android_apk(args: &[String], target_type: &str, mut release_mode: bool) {
1386 let is_aab = if target_type.is_empty() {
1387 println!("\n [1] APK\n [2] AAB (Google Play)");
1388 prompt_choice("š Format (1-2): ", 1, 2) == 2
1389 } else {
1390 target_type == "aab"
1391 };
1392
1393 if !args.iter().any(|a| a == "--release" || a == "-r" || a == "--debug" || a == "-d") {
1394 println!("\n [1] Debug\n [2] Release");
1395 if prompt_choice("š Mode (1-2): ", 1, 2) == 2 { release_mode = true; }
1396 }
1397
1398 println!("\nšØ Membangun JNI library (Native implementation)...");
1400 if !compile_jni_libraries() {
1401 println!("ā JNI build gagal.");
1402 return;
1403 }
1404
1405 let home = std::env::var("HOME").unwrap_or_default();
1407 let android_home = std::env::var("ANDROID_HOME")
1408 .unwrap_or_else(|_| format!("{}/Library/Android/sdk", home));
1409 unsafe { std::env::set_var("ANDROID_HOME", &android_home); }
1410
1411 setup_java_home();
1412
1413 let gradle_task = match (is_aab, release_mode) {
1414 (false, false) => "assembleDebug",
1415 (false, true) => "assembleRelease",
1416 (true, false) => "bundleDebug",
1417 (true, true) => "bundleRelease",
1418 };
1419
1420 println!("\nšØ Gradle task: {}", gradle_task);
1421 let gradlew = if std::path::Path::new("native/android/gradlew").exists() { "./gradlew" } else { "gradle" };
1422 let mut cmd = std::process::Command::new(gradlew);
1423 cmd.arg(gradle_task)
1424 .current_dir("native/android");
1425
1426 if let Ok(jh) = std::env::var("JAVA_HOME") {
1427 cmd.env("JAVA_HOME", jh);
1428 }
1429
1430 cmd.stdin(std::process::Stdio::inherit()).stdout(std::process::Stdio::inherit()).stderr(std::process::Stdio::inherit());
1431 let mut child = cmd.spawn().expect("Gagal menjalankan Gradle");
1432
1433 if child.wait().map(|s| s.success()).unwrap_or(false) {
1434 println!("\nā
Android build selesai!");
1435 } else {
1436 println!("\nā Android build gagal.");
1437 }
1438}
1439
1440fn prompt_string(prompt: &str, default: &str) -> String {
1441 use std::io::Write;
1442 print!("{}", prompt);
1443 let _ = std::io::stdout().flush();
1444 let mut input = String::new();
1445 if std::io::stdin().read_line(&mut input).is_ok() {
1446 let trimmed = input.trim();
1447 if trimmed.is_empty() {
1448 default.to_string()
1449 } else {
1450 trimmed.to_string()
1451 }
1452 } else {
1453 default.to_string()
1454 }
1455}
1456
1457pub async fn handle_deploy() {
1458 println!("\n{}", "š RustBasic Docker Deploy CLI".magenta().bold());
1459 println!("{}", "------------------------------".magenta());
1460
1461 let image_name = prompt_string("š Masukkan Nama/Tag Docker Image (default: rustbasic:latest): ", "rustbasic:latest");
1463
1464 let inspect = std::process::Command::new("docker")
1466 .args(["image", "inspect", &image_name])
1467 .stdout(std::process::Stdio::null())
1468 .stderr(std::process::Stdio::null())
1469 .status();
1470
1471 match inspect {
1472 Ok(status) if status.success() => {}
1473 _ => {
1474 println!("{}", format!("ā ļø Peringatan: Image '{}' tidak ditemukan lokal.", image_name).yellow());
1475 let proceed = prompt_string("š Tetap lanjutkan proses ekspor? (y/n) [default: n]: ", "n");
1476 if proceed.to_lowercase() != "y" {
1477 println!("ā Proses dihentikan.");
1478 return;
1479 }
1480 }
1481 }
1482
1483 println!("š¦ Mengekspor Docker image '{}' ke 'rustbasic.tar'...", image_name);
1485 let save_status = std::process::Command::new("docker")
1486 .args(["save", "-o", "rustbasic.tar", &image_name])
1487 .status();
1488
1489 match save_status {
1490 Ok(status) if status.success() => {
1491 println!("{}", "ā
Image berhasil diekspor ke rustbasic.tar.".green());
1492 }
1493 _ => {
1494 println!("{}", "ā Gagal mengekspor Docker image.".red().bold());
1495 return;
1496 }
1497 }
1498
1499 println!("\n{}", "š Konfigurasi Pengiriman ke Server".cyan().bold());
1501 println!("{}", "-----------------------------------".cyan());
1502 let ssh_user = prompt_string("š Masukkan SSH Username Server (contoh: root) [default: root]: ", "root");
1503 let ssh_ip = prompt_string("š Masukkan IP Address Server: ", "");
1504 if ssh_ip.is_empty() {
1505 println!("{}", "ā IP Address server tidak boleh kosong.".red().bold());
1506 let _ = std::fs::remove_file("rustbasic.tar");
1507 return;
1508 }
1509 let ssh_port = prompt_string("š Masukkan SSH Port Server (default: 22): ", "22");
1510 let dest_dir = prompt_string("š Masukkan Folder Tujuan di Server (default: ~/app): ", "~/app");
1511 let server_port = prompt_string("š Masukkan Port Server Mapping (contoh: 80:4000) [default: 80:4000]: ", "80:4000");
1512 let env_file = prompt_string("š Masukkan File Env yang akan dikirim (default: .env): ", ".env");
1513
1514 println!("\nš Menyiapkan folder tujuan di server...");
1515 let mkdir_status = std::process::Command::new("ssh")
1516 .args([
1517 "-p", &ssh_port,
1518 &format!("{}@{}", ssh_user, ssh_ip),
1519 &format!("mkdir -p {}", dest_dir)
1520 ])
1521 .status();
1522
1523 match mkdir_status {
1524 Ok(status) if status.success() => {}
1525 _ => {
1526 println!("{}", "ā Gagal terhubung ke server menggunakan SSH.".red().bold());
1527 let _ = std::fs::remove_file("rustbasic.tar");
1528 return;
1529 }
1530 }
1531
1532 println!("š Mengirimkan berkas rustbasic.tar & {} ke server...", env_file);
1533 let scp_status = std::process::Command::new("scp")
1534 .args([
1535 "-P", &ssh_port,
1536 "rustbasic.tar", &env_file,
1537 &format!("{}@{}:{}", ssh_user, ssh_ip, dest_dir)
1538 ])
1539 .status();
1540
1541 let _ = std::fs::remove_file("rustbasic.tar");
1543
1544 if let Ok(status) = scp_status {
1545 if !status.success() {
1546 println!("{}", "ā Gagal mengirimkan berkas via SCP.".red().bold());
1547 return;
1548 }
1549 } else {
1550 println!("{}", "ā Gagal menjalankan SCP.".red().bold());
1551 return;
1552 }
1553
1554 println!("{}", "ā
Pengiriman berkas berhasil!".green());
1555
1556 let auto_run = prompt_string("\nš Apakah Anda ingin langsung menjalankan container di server secara otomatis? (y/n) [default: y]: ", "y");
1558 if auto_run.to_lowercase() == "y" || auto_run.is_empty() {
1559 println!("\nš Memuat image di server (docker load)...");
1560 let load_status = std::process::Command::new("ssh")
1561 .args([
1562 "-p", &ssh_port,
1563 &format!("{}@{}", ssh_user, ssh_ip),
1564 &format!("docker load -i {}/rustbasic.tar", dest_dir)
1565 ])
1566 .status();
1567
1568 match load_status {
1569 Ok(status) if status.success() => {
1570 println!("{}", "ā
Image berhasil dimuat di server.".green());
1571 }
1572 _ => {
1573 println!("{}", "ā Gagal memuat image di server.".red().bold());
1574 return;
1575 }
1576 }
1577
1578 println!("š Menghentikan & menghapus container lama 'rustbasic-app' jika ada...");
1579 let stop_status = std::process::Command::new("ssh")
1580 .args([
1581 "-p", &ssh_port,
1582 &format!("{}@{}", ssh_user, ssh_ip),
1583 "docker stop rustbasic-app || true && docker rm rustbasic-app || true"
1584 ])
1585 .status();
1586
1587 if let Err(e) = stop_status {
1588 println!("ā ļø Peringatan saat membersihkan container lama: {}", e);
1589 }
1590
1591 println!("š Menjalankan container baru 'rustbasic-app'...");
1592 let run_cmd = format!(
1593 "docker run -d --name rustbasic-app -p {} --restart unless-stopped --env-file {}/.env {}",
1594 server_port, dest_dir, image_name
1595 );
1596 let run_status = std::process::Command::new("ssh")
1597 .args([
1598 "-p", &ssh_port,
1599 &format!("{}@{}", ssh_user, ssh_ip),
1600 &run_cmd
1601 ])
1602 .status();
1603
1604 match run_status {
1605 Ok(status) if status.success() => {
1606 println!("{}", "š Container 'rustbasic-app' berhasil dijalankan di server!".green().bold());
1607 }
1608 _ => {
1609 println!("{}", "ā Gagal menjalankan container di server.".red().bold());
1610 return;
1611 }
1612 }
1613
1614 println!("š Membersihkan file tar di server...");
1615 let rm_status = std::process::Command::new("ssh")
1616 .args([
1617 "-p", &ssh_port,
1618 &format!("{}@{}", ssh_user, ssh_ip),
1619 &format!("rm {}/rustbasic.tar", dest_dir)
1620 ])
1621 .status();
1622
1623 if let Err(e) = rm_status {
1624 println!("ā ļø Peringatan saat membersihkan file tar di server: {}", e);
1625 }
1626
1627 println!("\n{}", "š Deployment selesai!".green().bold());
1628 println!("{}", "--------------------------------------------------------".green());
1629 println!("Untuk melihat log aplikasi di server, jalankan:");
1630 println!("ssh -p {} {}@{} \"docker logs -f rustbasic-app\"", ssh_port, ssh_user, ssh_ip);
1631 println!("{}", "--------------------------------------------------------".green());
1632 } else {
1633 println!("\n{}", "š„ļø Langkah Selanjutnya di Server Anda:".cyan().bold());
1634 println!("{}", "--------------------------------------------------------".green());
1635 println!("1. Hubungkan ke server via SSH:");
1636 println!(" ssh -p {} {}@{}", ssh_port, ssh_user, ssh_ip);
1637 println!("");
1638 println!("2. Masuk ke folder tujuan:");
1639 println!(" cd {}", dest_dir);
1640 println!("");
1641 println!("3. Muat (load) image Docker dari berkas tar:");
1642 println!(" docker load -i rustbasic.tar");
1643 println!("");
1644 println!("4. Jalankan container dengan fitur auto-restart:");
1645 println!(" docker run -d --name rustbasic-app -p {} --restart unless-stopped --env-file .env {}", server_port, image_name);
1646 println!("");
1647 println!("5. Hapus file tar di server untuk menghemat disk:");
1648 println!(" rm rustbasic.tar");
1649 println!("{}", "--------------------------------------------------------".green());
1650 }
1651}
1652
1653fn handle_publish(target: &str) {
1657 let mut selected_target = target.to_string();
1658 if selected_target.is_empty() {
1659 println!("š ļø RustBasic Configuration Publisher");
1660 println!("Pilih konfigurasi yang ingin dipublikasikan ke proyek Anda:");
1661 println!(" [1] CORS (Cross-Origin Resource Sharing)");
1662 println!(" [2] CSRF (Cross-Site Request Forgery)");
1663 println!(" [3] APP (Application settings & Storage path overrides)");
1664 let choice = prompt_choice("š Pilih nomor konfigurasi (1-3): ", 1, 3);
1665 match choice {
1666 1 => selected_target = "cors".to_string(),
1667 2 => selected_target = "csrf".to_string(),
1668 3 => selected_target = "app".to_string(),
1669 _ => return,
1670 }
1671 }
1672
1673 match selected_target.as_str() {
1674 "cors" => {
1675 let path = std::path::Path::new("src/config/cors.rs");
1676 if path.exists() {
1677 println!("ā¹ļø File cors.rs sudah ada di src/config/cors.rs");
1678 return;
1679 }
1680 if let Some(parent) = path.parent() {
1681 let _ = std::fs::create_dir_all(parent);
1682 }
1683 let content = r#"/* ---------------------------------------------------------
1684 * š LABEL: CORS CONFIGURATION (src/config/cors.rs)
1685 * Berkas konfigurasi tambahan untuk kustomisasi CORS.
1686 * --------------------------------------------------------- */
1687
1688pub struct CorsConfig {
1689 pub allowed_origins: Vec<&'static str>,
1690 pub allowed_methods: Vec<&'static str>,
1691 pub allowed_headers: Vec<&'static str>,
1692}
1693
1694impl Default for CorsConfig {
1695 fn default() -> Self {
1696 Self {
1697 allowed_origins: vec!["*"],
1698 allowed_methods: vec!["GET", "POST", "PUT", "DELETE", "OPTIONS"],
1699 allowed_headers: vec!["*"],
1700 }
1701 }
1702}
1703"#;
1704 if std::fs::write(path, content).is_ok() {
1705 println!("{} {}", "ā
Berhasil mempublikasikan konfigurasi CORS ke:".green().bold(), path.display().to_string().cyan());
1706 println!("š” File ini sekarang dapat diimpor untuk menyesuaikan aturan CORS lokal.");
1707 } else {
1708 println!("ā Gagal menulis file CORS config.");
1709 }
1710 }
1711 "csrf" => {
1712 let path = std::path::Path::new("src/config/csrf.rs");
1713 if path.exists() {
1714 println!("ā¹ļø File csrf.rs sudah ada di src/config/csrf.rs");
1715 return;
1716 }
1717 if let Some(parent) = path.parent() {
1718 let _ = std::fs::create_dir_all(parent);
1719 }
1720 let content = r#"/* ---------------------------------------------------------
1721 * š LABEL: CSRF CONFIGURATION (src/config/csrf.rs)
1722 * Berkas konfigurasi tambahan untuk perlindungan CSRF.
1723 * --------------------------------------------------------- */
1724
1725pub struct CsrfConfig {
1726 pub except_paths: Vec<&'static str>,
1727}
1728
1729impl Default for CsrfConfig {
1730 fn default() -> Self {
1731 Self {
1732 except_paths: vec![], // Masukkan rute yang dikecualikan dari CSRF di sini
1733 }
1734 }
1735}
1736"#;
1737 if std::fs::write(path, content).is_ok() {
1738 println!("{} {}", "ā
Berhasil mempublikasikan konfigurasi CSRF ke:".green().bold(), path.display().to_string().cyan());
1739 println!("š” File ini sekarang dapat diimpor untuk mengecualikan rute tertentu dari CSRF.");
1740 } else {
1741 println!("ā Gagal menulis file CSRF config.");
1742 }
1743 }
1744 "app" => {
1745 let path = std::path::Path::new("src/config/app.rs");
1746 if path.exists() {
1747 println!("ā¹ļø File app.rs sudah ada di src/config/app.rs");
1748 return;
1749 }
1750 if let Some(parent) = path.parent() {
1751 let _ = std::fs::create_dir_all(parent);
1752 }
1753 let content = r#"/* ---------------------------------------------------------
1754 * š LABEL: APP CONFIGURATION (src/config/app.rs)
1755 * Berkas konfigurasi tambahan untuk kustomisasi lokasi storage lokal.
1756 * --------------------------------------------------------- */
1757
1758pub const STORAGE_TARGET: &str = "public/storage";
1759pub const STORAGE_SOURCE: &str = "storage/app/public";
1760"#;
1761 if std::fs::write(path, content).is_ok() {
1762 println!("{} {}", "ā
Berhasil mempublikasikan konfigurasi APP ke:".green().bold(), path.display().to_string().cyan());
1763 } else {
1764 println!("ā Gagal menulis file APP config.");
1765 }
1766 }
1767 _ => {
1768 println!("ā Target '{}' tidak dikenal untuk di-publish.", selected_target);
1769 println!("š” Target yang didukung: cors, csrf, app");
1770 }
1771 }
1772}
1773
1774fn get_cargo_package_name() -> String {
1775 if let Ok(content) = std::fs::read_to_string("Cargo.toml") {
1776 for line in content.lines() {
1777 let trimmed = line.trim();
1778 if trimmed.starts_with("name =") || trimmed.starts_with("name=") {
1779 let parts: Vec<&str> = trimmed.split('=').collect();
1780 if parts.len() > 1 {
1781 let name = parts[1].trim().trim_matches('"').trim_matches('\'');
1782 return name.to_string();
1783 }
1784 }
1785 }
1786 }
1787 "rustbasic".to_string()
1788}
1789