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