oranda_generate_css/
lib.rs

1pub mod errors;
2
3extern crate axoasset;
4extern crate camino;
5extern crate directories;
6extern crate miette;
7extern crate thiserror;
8
9use crate::errors::Result;
10use axoasset::LocalAsset;
11use camino::{Utf8Path, Utf8PathBuf};
12use directories::ProjectDirs;
13use std::env;
14use std::io::Write;
15use std::process::Command;
16
17const MANIFEST_PATH: &str = env!("CARGO_MANIFEST_DIR");
18const CSS_SRC_PATH: &str = "../oranda-css/css/main.css";
19const TAILWIND_SRC_PATH: &str = "../oranda-css/tailwind.config.js";
20const DEFAULT_CSS_OUTPUT_DIR: &str = "../oranda-css/dist";
21
22fn manifest_dir() -> &'static Utf8Path {
23    Utf8Path::new(MANIFEST_PATH)
24}
25
26pub fn default_css_output_dir() -> Utf8PathBuf {
27    manifest_dir().join(DEFAULT_CSS_OUTPUT_DIR)
28}
29
30pub fn build_css(dist_dir: &Utf8Path) -> Result<()> {
31    let binary_path = tailwind_path()?;
32
33    tracing::info!("Building oranda CSS using Tailwind...");
34    let css_src_path = manifest_dir().join(CSS_SRC_PATH);
35    let tailwind_config_path = manifest_dir().join(TAILWIND_SRC_PATH);
36    let output_path = dist_dir.join("oranda.css");
37    let output = Command::new(binary_path)
38        .args([
39            "-c",
40            tailwind_config_path.as_str(),
41            "-i",
42            css_src_path.as_str(),
43            "-o",
44            output_path.as_str(),
45            "--minify",
46        ])
47        .output()?;
48    std::io::stderr().write_all(&output.stderr)?;
49    output
50        .status
51        .success()
52        .then_some(true)
53        .expect("Tailwind failed to compile CSS!");
54
55    Ok(())
56}
57
58/// Returns the path to execute the Tailwind binary.
59///
60/// If a `tailwindcss` binary already exists on the current path (determined
61/// using `tailwindcss --help`), then the existing Tailwind is used. Otherwise,
62/// a Tailwind binary is installed from GitHub releases into the user's cache
63/// directory.
64fn tailwind_path() -> Result<Utf8PathBuf> {
65    // First, see if tailwind is already present
66    let result = Command::new("tailwindcss").arg("--help").status();
67    if let Ok(status) = result {
68        if status.success() {
69            tracing::info!("Found Tailwind binary on the path!");
70
71            return Ok(Utf8PathBuf::from("tailwindcss"));
72        }
73        // Otherwise, no tailwind binary exists.
74    } else {
75        tracing::info!(?result, "Couldn't find Tailwind binary");
76    }
77
78    // Fetch our cache dir
79    let project_dir = ProjectDirs::from("dev", "axo", "oranda")
80        .expect("Unable to create cache dir for downloading Tailwind!");
81    let cache_dir = project_dir.cache_dir();
82    // Figure out our target "double" (tailwind has weird naming around this)
83    let double = match (env::consts::OS, env::consts::ARCH) {
84        ("linux", "x86_64") => "linux-x64",
85        ("linux", "aarch64") => "linux-arm64",
86        ("linux", "arm") => "linux-armv7",
87        ("macos", "x86_64") => "macos-x64",
88        ("macos", "aarch64") => "macos-arm64",
89        ("windows", "x86_64") => "windows-x64.exe",
90        ("windows", "aarch64") => "windows-arm64.exe",
91        _ => "linux-x64",
92    };
93    let mut binary_path = Utf8PathBuf::from(cache_dir.display().to_string());
94    LocalAsset::create_dir_all(&binary_path)?;
95    binary_path.push(format!("tailwindcss-{double}"));
96    if !binary_path.exists() {
97        // Fetch the binary from GitHub if it doesn't exist
98        tracing::info!("Fetching Tailwind binary from GitHub release...");
99        let url = format!(
100			"https://github.com/tailwindlabs/tailwindcss/releases/latest/download/tailwindcss-{double}"
101		);
102        let handle = tokio::runtime::Handle::current();
103        let response = handle.block_on(reqwest::get(url))?;
104        let bytes = handle.block_on(response.bytes())?;
105        let file = LocalAsset::new(&binary_path, Vec::from(bytes))?;
106        file.write(
107            binary_path
108                .parent()
109                .expect("Tailwind binary path has no parent!?"),
110        )?;
111
112        // On non-Windows platforms, we need to mark the file as executable
113        #[cfg(target_family = "unix")]
114        {
115            use std::os::unix::prelude::PermissionsExt;
116            let user_execute = std::fs::Permissions::from_mode(0o755);
117            std::fs::set_permissions(&binary_path, user_execute)?;
118        }
119    }
120
121    Ok(binary_path)
122}