Skip to main content

rootcx_platform/
bundle.rs

1use std::fs;
2use std::io::{BufRead, BufReader, Write};
3use std::path::{Path, PathBuf};
4use std::process::{Command, Stdio};
5
6pub fn run(app_dir: PathBuf, log: &(dyn Fn(&str) + Sync)) -> Result<PathBuf, String> {
7    let app_dir = fs::canonicalize(&app_dir).map_err(|e| format!("{}: {e}", app_dir.display()))?;
8    for f in ["manifest.json", "package.json", "src-tauri/Cargo.toml", "src-tauri/tauri.conf.json"] {
9        if !app_dir.join(f).exists() { return Err(format!("missing {f} in {}", app_dir.display())); }
10    }
11
12    let has_backend = archive_backend(&app_dir)?;
13    let pm = detect_pm(&app_dir);
14
15    log(&format!("[bundle] installing dependencies ({pm})"));
16    exec(pm, &["install"], &app_dir, log)?;
17    if app_dir.join("backend/package.json").exists() {
18        exec(pm, &["install"], &app_dir.join("backend"), log)?;
19    }
20
21    log(&format!("[bundle] building frontend ({pm})"));
22    exec(pm, &["run", "build"], &app_dir, log)?;
23
24    log("[bundle] cargo tauri build");
25    let cfg = if has_backend {
26        r#"{"build":{"beforeBuildCommand":""},"bundle":{"resources":{"resources/":"resources/"}}}"#
27    } else {
28        r#"{"build":{"beforeBuildCommand":""}}"#
29    };
30    exec("cargo", &["tauri", "build", "--config", cfg], &app_dir, log)?;
31
32    if has_backend { let _ = fs::remove_file(app_dir.join("src-tauri/resources/backend.tar.gz")); }
33
34    Ok(app_dir.join("src-tauri/target/release/bundle"))
35}
36
37const SKIP_DIRS: &[&str] = &["node_modules", ".git", "target", ".rootcx"];
38const SKIP_EXTS: &[&str] = &[".test.ts", ".test.js", ".spec.ts", ".spec.js", ".map"];
39
40fn archive_backend(app_dir: &Path) -> Result<bool, String> {
41    let backend = app_dir.join("backend");
42    if !backend.is_dir() { return Ok(false); }
43    let res = app_dir.join("src-tauri/resources");
44    fs::create_dir_all(&res).map_err(|e| format!("create resources dir: {e}"))?;
45    let gz = flate2::write::GzEncoder::new(
46        fs::File::create(res.join("backend.tar.gz")).map_err(|e| format!("create tar.gz: {e}"))?,
47        flate2::Compression::default(),
48    );
49    let mut tar = tar::Builder::new(gz);
50    tar_filtered(&mut tar, &backend, Path::new("."))?;
51    tar.into_inner().map_err(|e| format!("tar finish: {e}"))?.finish().map_err(|e| format!("gz finish: {e}"))?;
52    Ok(true)
53}
54
55fn tar_filtered<W: Write>(tar: &mut tar::Builder<W>, src: &Path, prefix: &Path) -> Result<(), String> {
56    for entry in fs::read_dir(src).into_iter().flatten().flatten() {
57        let name = entry.file_name();
58        let name_str = name.to_string_lossy();
59        if name_str.starts_with('.') { continue; }
60        let disk = entry.path();
61
62        if disk.is_dir() {
63            if SKIP_DIRS.contains(&name_str.as_ref()) { continue; }
64            tar_filtered(tar, &disk, &prefix.join(&name))?;
65        } else if disk.is_file() {
66            if SKIP_EXTS.iter().any(|ext| name_str.ends_with(ext)) { continue; }
67            tar.append_path_with_name(&disk, prefix.join(&name))
68                .map_err(|e| format!("tar {}: {e}", disk.display()))?;
69        }
70    }
71    Ok(())
72}
73
74fn detect_pm(dir: &Path) -> &'static str {
75    if dir.join("bun.lock").exists() || dir.join("bun.lockb").exists() { "bun" }
76    else if dir.join("pnpm-lock.yaml").exists() { "pnpm" }
77    else { "npm" }
78}
79
80fn exec(program: &str, args: &[&str], cwd: &Path, log: &(dyn Fn(&str) + Sync)) -> Result<(), String> {
81    let mut child = Command::new(program).args(args).current_dir(cwd)
82        .stdout(Stdio::piped()).stderr(Stdio::piped())
83        .spawn().map_err(|e| format!("{program}: {e}"))?;
84    let (stdout, stderr) = (child.stdout.take().unwrap(), child.stderr.take().unwrap());
85    std::thread::scope(|s| {
86        s.spawn(|| BufReader::new(stderr).lines().flatten().for_each(|l| log(&l)));
87        BufReader::new(stdout).lines().flatten().for_each(|l| log(&l));
88    });
89    let status = child.wait().map_err(|e| format!("{program}: {e}"))?;
90    if status.success() { Ok(()) } else { Err(format!("{program} exited {}", status.code().unwrap_or(-1))) }
91}