rustolio-build-utils 0.1.0

Build Utilities to use in the build.rs for the rustolio framework
Documentation
//
// SPDX-License-Identifier: MPL-2.0
//
// Copyright (c) 2026 Tobias Binnewies. All rights reserved.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
//

mod fs;
mod paths;
mod tailwind;

use std::env;

pub use rustolio_utils::{prelude::*, Error, Result};

pub fn handle_lib() -> Result<()> {
    if !is_wasm_build()? {
        return Ok(());
    }
    tailwind::push_info()?;
    fs::copy(
        &paths::static_dir()?,
        &paths::web_build_dir()?.join(cargo_pkg_name()?),
    )?;
    Ok(())
}

pub fn handle_bin() -> Result<()> {
    if !is_wasm_build()? {
        return Ok(());
    }
    force_rebuild()?; // To always run tailwind
    create_default_static_files()?;
    tailwind::execute()?;
    fs::copy(&paths::web_build_dir()?, &paths::pkg_dir()?)?;
    fs::copy(&paths::static_dir()?, &paths::pkg_dir()?)?;
    Ok(())
}

fn create_default_static_files() -> Result<()> {
    let static_dir = paths::static_dir()?;

    let index_html = default_index_html()?;
    fs::create(&static_dir.join("index.html"), &index_html)?;

    Ok(())
}

fn default_index_html() -> Result<String> {
    const INDEX_CONTENT: &str = r#"
<!DOCTYPE html>
<html lang="en-US">
  <head>
    <meta charset="utf-8" />
    <title>{PRETTY_PKG_NAME}</title>
    {LINKS}
  </head>
  <body>
    <div id="root"></div>
    <script type="module">
      import init, { entry } from "/./{PKG_NAME}.js"
      init().then(() => {
        entry()
      })
    </script>
  </body>
</html>
"#;
    const TAILWIND_LINK: &str = r#"<link rel="stylesheet" href="/./tailwind.css" />"#;

    let mut links = Vec::new();
    if tailwind::enabled()? {
        links.push(TAILWIND_LINK.to_string());
    }
    let links = links.join("\n    ");

    Ok(INDEX_CONTENT
        .replace("{LINKS}", &links)
        .replace("{PRETTY_PKG_NAME}", &pretty_pkg_name()?)
        .replace("{PKG_NAME}", &cargo_pkg_name()?.replace("-", "_")))
}

fn is_wasm_build() -> Result<bool> {
    Ok(env::var("CARGO_CFG_TARGET_ARCH").context("CARGO_CFG_TARGET_ARCH not set")? == "wasm32")
}

/// E.g. "my_cool_app" -> "My Cool App"
fn pretty_pkg_name() -> Result<String> {
    Ok(cargo_pkg_name()?
        .replace("_", " ")
        .replace("-", " ")
        .split(' ')
        .map(|s| {
            let mut c = s.chars();
            match c.next() {
                None => String::new(),
                Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
            }
        })
        .collect::<Vec<_>>()
        .join(" "))
}

fn cargo_pkg_name() -> Result<String> {
    env::var("CARGO_PKG_NAME").context("CARGO_PKG_NAME not set")
}

fn force_rebuild() -> Result<()> {
    let dummy_timestamp = paths::out_dir()?.join("dummy.timestamp");

    // Always rerun when this dummy file changes
    println!(
        "cargo:rerun-if-changed={}",
        dummy_timestamp
            .to_str()
            .context("Failed to convert to str")?
    );

    // Update the timestamp
    let timestamp = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap()
        .as_secs();
    std::fs::write(dummy_timestamp, timestamp.to_string()).context("Failed to write dummy file")?;
    Ok(())
}