Files
Jean-Gabriel Gill-Couture 9be4f63636
All checks were successful
Run Check Script / check (pull_request) Successful in 2m17s
fix(fleet-operator): build + embed Tailwind CSS in the container image
The image shipped empty CSS: build.rs shells out to the tailwindcss v4
CLI and silently falls back to an empty bundle when it's absent — which
it was in the rust:slim builder, so /static/tailwind.css served nothing.

- Dockerfile: install the pinned Tailwind v4 standalone CLI (curl) in the
  builder and set TAILWIND_REQUIRED=1.
- build.rs: when TAILWIND_REQUIRED is set (container/prod), a missing or
  failing CLI is now a hard build error instead of empty CSS; dev builds
  keep the soft fallback for the `serve-web --css-from` workflow. The env
  is a rerun trigger, so the first required build regenerates rather than
  reusing a cache-mounted empty bundle.

Verified: with the CLI on PATH the embedded bundle is ~26 KB; with
TAILWIND_REQUIRED=1 and no CLI the build fails as intended.
2026-06-01 23:26:38 -04:00

71 lines
2.9 KiB
Rust

//! Best-effort Tailwind CSS build for the `web-frontend` feature.
//!
//! When the standalone `tailwindcss` v4 CLI is on PATH and the
//! `web-frontend` feature is on, this script generates the production
//! CSS bundle into `$OUT_DIR/tailwind.css`. The binary embeds that file
//! via `include_bytes!`.
//!
//! If the CLI is missing or fails we write an *empty* CSS file rather
//! than failing the build — the dev workflow uses
//! `serve-web --css-from <path>` to read Tailwind output from a sidecar
//! `tailwindcss --watch` process, so the embedded copy is irrelevant
//! there. Production builds should ensure `tailwindcss` is on PATH; the
//! warning below shows up in `cargo build` output when it is not.
use std::path::PathBuf;
use std::process::Command;
fn main() {
let out_dir = PathBuf::from(std::env::var("OUT_DIR").unwrap());
let output = out_dir.join("tailwind.css");
if std::env::var_os("CARGO_FEATURE_WEB_FRONTEND").is_none() {
// Feature off — emit an empty placeholder so the `include_bytes!`
// path in src/frontend/assets.rs still compiles if anything
// references it (it should be cfg-gated, but belt-and-braces).
std::fs::write(&output, b"").unwrap();
return;
}
let manifest_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap());
let input = manifest_dir.join("style/input.css");
println!("cargo:rerun-if-changed=style/input.css");
println!("cargo:rerun-if-changed=src/frontend");
println!("cargo:rerun-if-changed=src/service");
// Toggling this re-runs the script, so the container build (which sets
// it) regenerates rather than reusing a cache-mounted empty bundle.
println!("cargo:rerun-if-env-changed=TAILWIND_REQUIRED");
// Container/production builds set TAILWIND_REQUIRED: a missing or
// failing CLI is a hard error there (never ship empty CSS). Dev builds
// leave it unset and fall back to empty, serving CSS via
// `serve-web --css-from <path>` against a `tailwindcss --watch` sidecar.
let required = std::env::var_os("TAILWIND_REQUIRED").is_some();
let fall_back = |reason: String| {
assert!(
!required,
"{reason}\nTAILWIND_REQUIRED is set: refusing to ship a frontend with empty CSS. \
Install the v4 standalone CLI: https://github.com/tailwindlabs/tailwindcss/releases"
);
println!(
"cargo:warning={reason}; embedded CSS will be empty \
(use `serve-web --css-from <path>` in dev)."
);
std::fs::write(&output, b"").unwrap();
};
match Command::new("tailwindcss")
.arg("--input")
.arg(&input)
.arg("--output")
.arg(&output)
.arg("--minify")
.status()
{
Ok(s) if s.success() => {}
Ok(s) => fall_back(format!("tailwindcss exited with status {s}")),
Err(e) => fall_back(format!("tailwindcss not invocable ({e})")),
}
}