//! build.rs — Static-asset pipeline for OxiCloud //! //! **Release mode** (`cargo build --release`): //! 1. Copies `static/` → `static-dist/` (processed mirror). //! 2. Resolves CSS `@import` chains → flat `main.css`. //! 3. Bundles all index.html CSS/JS → `app.{hash}.css` / `app.{hash}.js`. //! 4. Minifies every `.css` (lightningcss) and `.js` (oxc). //! 5. Rewrites `index.html` with bundled asset paths. //! 6. Minifies locale JSON files. //! 7. Updates `sw.js` cache manifest. //! 8. Writes HTML files to `$OUT_DIR` for `include_str!()`. //! //! **Debug mode** (`cargo build`): //! • Copies HTML files to `$OUT_DIR` for `include_str!()` only. use std::env; use std::fs; use std::io; use std::path::{Path, PathBuf}; use std::process::Command; // ─── HTML files embedded via include_str!() in Rust source ─────────────────── const HTML_INCLUDE: &[&str] = &[ "login.html", "profile.html", "admin.html", "device-verify.html", "nextcloud-login.html", "share.html", ]; // ═══════════════════════════════════════════════════════════════════════════════ // Entry point // ═══════════════════════════════════════════════════════════════════════════════ fn main() { let manifest_dir = penv("CARGO_MANIFEST_DIR"); let out_dir = penv("OUT_DIR"); let static_dir = manifest_dir.join("static"); println!("cargo:rerun-if-changed=static"); println!("cargo:rerun-if-changed=build.rs"); println!("cargo:rerun-if-env-changed=OXICLOUD_RUST_ASSETS"); git_status(); // The frontend is built by Vite into `static-dist/` and the Rust web layer // serves it directly — no `include_str!` HTML, no Rust-side bundling. The // pure-Rust asset pipeline below is retained, behind `OXICLOUD_RUST_ASSETS=1`, // for one-release rollback only. if env_or("OXICLOUD_RUST_ASSETS", "0") != "1" { return; } // ── Guard: Docker cacher stage has no static/ ──────────────────────────── if !static_dir.exists() { for name in HTML_INCLUDE { let _ = fs::write(out_dir.join(name), ""); } return; } let is_release = env_or("PROFILE", "debug") == "release"; if is_release { process_release(&manifest_dir, &static_dir, &out_dir); } else { // Debug: copy original HTML for include_str!() for name in HTML_INCLUDE { let src = static_dir.join(name); if src.exists() { let _ = fs::copy(&src, out_dir.join(name)); } } } } // ═══════════════════════════════════════════════════════════════════════════════ // Grab git values // Support Github, is treated (need upgrade if move to gitlab CircleCI, ...) // ═══════════════════════════════════════════════════════════════════════════════ fn git_status() { // Rerun the build script when the commit or branch changes println!("cargo:rerun-if-changed=.git/HEAD"); println!("cargo:rerun-if-changed=.git/refs/heads"); let git_hash = first_env(&["GITHUB_SHA", "CI_COMMIT_SHA", "CIRCLE_SHA1", "GIT_COMMIT"]) .or_else(|| git(&["rev-parse", "HEAD"])) .unwrap_or_else(|| "unknown".into()); println!("cargo:rustc-env=GIT_HASH={git_hash}"); let git_branch = first_env(&[ "GITHUB_HEAD_REF", // GitHub: PR source branch (empty on push) "GITHUB_REF_NAME", // GitHub: branch/tag on push "CI_COMMIT_REF_NAME", // GitLab "CIRCLE_BRANCH", // CircleCI "GIT_BRANCH", // Jenkins ]) .or_else(|| git(&["rev-parse", "--abbrev-ref", "HEAD"])) .filter(|b| b != "HEAD") // detached HEAD is not a real branch name .unwrap_or_else(|| "unknown".into()); println!("cargo:rustc-env=GIT_BRANCH={git_branch}"); // CI builds: rerun if the injected env changes for k in [ "GITHUB_SHA", "GITHUB_HEAD_REF", "GITHUB_REF_NAME", "CI_COMMIT_SHA", "CI_COMMIT_REF_NAME", "CIRCLE_SHA1", "CIRCLE_BRANCH", "GIT_COMMIT", "GIT_BRANCH", ] { println!("cargo:rerun-if-env-changed={k}"); } println!("cargo:warning=OxiCloud building with git hash: {git_hash} and branch: {git_branch}"); } fn git(args: &[&str]) -> Option { let out = Command::new("git").args(args).output().ok()?; out.status.success().then_some(())?; let s = String::from_utf8(out.stdout).ok()?.trim().to_string(); (!s.is_empty()).then_some(s) } fn first_env(keys: &[&str]) -> Option { keys.iter() .find_map(|k| env::var(k).ok()) .filter(|s| !s.is_empty()) } // ═══════════════════════════════════════════════════════════════════════════════ // Release pipeline // ═══════════════════════════════════════════════════════════════════════════════ fn process_release(manifest_dir: &Path, static_dir: &Path, out_dir: &Path) { let dist_dir = manifest_dir.join("static-dist"); // Start fresh if dist_dir.exists() { fs::remove_dir_all(&dist_dir).expect("clean static-dist"); } // 1. Mirror static/ → static-dist/ copy_dir_recursive(static_dir, &dist_dir).expect("copy static → static-dist"); let css_dir = static_dir.join("css"); // Read index.html once — used for both CSS and JS extraction. let index_html = fs::read_to_string(static_dir.join("index.html")).expect("read index.html"); // ── 2. Resolve main.css @imports ───────────────────────────────────────── let resolved_main = resolve_css_imports(&css_dir.join("main.css"), &css_dir); let minified_main = css_minify_safe(&resolved_main); fs::write(dist_dir.join("css/main.css"), &minified_main).expect("write main.css"); // ── 3. Build CSS bundle for index.html ─────────────────────────────────── // Derive the list of view CSS files directly from the tags in index.html // so build.rs never needs to be updated when a new stylesheet is added. let mut css_all = resolved_main; for view in extract_css_links(&index_html) { let p = css_dir.join(&view); if p.exists() { css_all.push_str(&fs::read_to_string(&p).unwrap_or_default()); css_all.push('\n'); } else { eprintln!( "cargo:warning=CSS link in index.html not found: {}", p.display() ); } } let css_bundle = css_minify_safe(&css_all); let css_hash = fnv_hash(css_bundle.as_bytes()); let css_name = format!("app.{css_hash}.css"); fs::write(dist_dir.join("css").join(&css_name), &css_bundle).expect("write css bundle"); // ── 4. Minify ALL individual CSS in static-dist/ ───────────────────────── minify_tree_css(&dist_dir.join("css")); // ── 5. Bundle all ES modules into one IIFE ─────────────────────────────── // Walk the import graph starting from every ", theme_init_js.trim())); } continue; } // ── Replace all stylesheet s with single bundle ──────────────── if t.starts_with("")); css_done = true; } continue; } // ── Replace all type="module" scripts with single bundle ───────────── if t.starts_with("" )); js_done = true; } continue; } // ── Drop "Styles" / "Scripts" section comments ─────────────────────── if t.starts_with("