use std::env;
use std::path::{Path, PathBuf};
use std::process::Command;
const SRC_REL: &str = "assets/static/admin.css";
const TEMPLATES_REL: &str = "assets/templates";
fn main() {
let manifest = PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR"));
let src = manifest.join(SRC_REL);
let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR"));
let dest = out_dir.join("admin.css");
println!("cargo:rerun-if-changed={SRC_REL}");
println!("cargo:rerun-if-changed={TEMPLATES_REL}");
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-env-changed=RUSTIO_SKIP_TAILWIND");
if env::var_os("RUSTIO_SKIP_TAILWIND").is_some() {
passthrough(&src, &dest, "RUSTIO_SKIP_TAILWIND set");
return;
}
if try_run("tailwindcss", &compile_args(&src, &dest)) {
return;
}
if try_run("npx", &npx_args(&src, &dest)) {
return;
}
passthrough(
&src,
&dest,
"no tailwindcss binary or npx found in PATH; \
install via `brew install tailwindcss` (single binary) or \
`npm i -g @tailwindcss/cli`, or set RUSTIO_SKIP_TAILWIND=1",
);
}
fn compile_args(src: &Path, dest: &Path) -> Vec<String> {
vec![
"-i".into(),
src.display().to_string(),
"-o".into(),
dest.display().to_string(),
"--minify".into(),
]
}
fn npx_args(src: &Path, dest: &Path) -> Vec<String> {
let mut args = vec!["-y".into(), "@tailwindcss/cli".into()];
args.extend(compile_args(src, dest));
args
}
fn try_run(cmd: &str, args: &[String]) -> bool {
let result = Command::new(cmd).args(args).status();
match result {
Ok(status) if status.success() => true,
Ok(status) => {
println!(
"cargo:warning={cmd} exited with {status}; falling through to the next compile strategy"
);
false
}
Err(_) => false,
}
}
fn passthrough(src: &Path, dest: &Path, reason: &str) {
println!("cargo:warning=admin.css served unprocessed: {reason}");
let source = std::fs::read_to_string(src)
.unwrap_or_else(|e| panic!("failed to read {} for passthrough: {e}", src.display()));
let stripped = strip_tailwind_only_syntax(&source);
std::fs::write(dest, stripped)
.unwrap_or_else(|e| panic!("failed to write {} for passthrough: {e}", dest.display()));
}
fn strip_tailwind_only_syntax(source: &str) -> String {
let bytes = source.as_bytes();
let mut out = String::with_capacity(source.len());
let mut i = 0usize;
let mut in_comment = false;
while i < bytes.len() {
if !in_comment && i + 1 < bytes.len() && bytes[i] == b'/' && bytes[i + 1] == b'*' {
in_comment = true;
out.push('/');
out.push('*');
i += 2;
continue;
}
if in_comment && i + 1 < bytes.len() && bytes[i] == b'*' && bytes[i + 1] == b'/' {
in_comment = false;
out.push('*');
out.push('/');
i += 2;
continue;
}
if !in_comment && bytes[i] == b'@' {
let rest = &source[i..];
if let Some(rest_after) = rest.strip_prefix("@import") {
let trimmed = rest_after.trim_start();
if trimmed.starts_with("\"tailwindcss\"") || trimmed.starts_with("'tailwindcss'") {
if let Some(end) = rest.find(';') {
i += end + 1;
continue;
}
}
}
if let Some(after_kw) = rest.strip_prefix("@theme") {
let trimmed = after_kw.trim_start();
if trimmed.starts_with('{') {
let brace_offset = rest.find('{').unwrap();
let abs_brace = i + brace_offset;
let mut depth = 0i32;
let mut idx = abs_brace;
while idx < bytes.len() {
let ch = bytes[idx];
if ch == b'{' {
depth += 1;
} else if ch == b'}' {
depth -= 1;
if depth == 0 {
idx += 1;
break;
}
}
idx += 1;
}
i = idx;
continue;
}
}
}
let ch_end = source[i..].chars().next().unwrap().len_utf8();
out.push_str(&source[i..i + ch_end]);
i += ch_end;
}
out
}
#[cfg(test)]
mod tests {
use super::strip_tailwind_only_syntax;
#[test]
fn strips_at_theme_rule_but_keeps_comment_mention() {
let src = r#"
/* :root — variables.
* @theme — same tokens, just Tailwind-side.
*/
:root { --color-canvas: #1B1B1F; }
@theme {
--color-accent: #FF6A3D;
}
.body { background: var(--color-canvas); }
"#;
let out = strip_tailwind_only_syntax(src);
assert!(out.contains("@theme — same tokens"));
assert!(!out.contains("--color-accent: #FF6A3D"));
assert!(out.contains(":root { --color-canvas: #1B1B1F; }"));
assert!(out.contains(".body { background: var(--color-canvas); }"));
}
#[test]
fn strips_import_tailwindcss_and_nothing_else() {
let src = "@import \"tailwindcss\";\n@import \"other.css\";\n.x { color: red; }";
let out = strip_tailwind_only_syntax(src);
assert!(!out.contains("\"tailwindcss\""));
assert!(out.contains("@import \"other.css\""));
assert!(out.contains(".x { color: red; }"));
}
}