Ctrl K
ComunidadGuías…

build.rs

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
use std::env;
use std::fs;
use std::io;
use std::path::{Path, PathBuf};

const FALLBACK_MARKER: &str = "AXON_FALLBACK_WEB_PANEL";

fn main() {
    println!("cargo:rerun-if-changed=apps/web/out");
    println!("cargo:rerun-if-env-changed=AXON_ALLOW_FALLBACK_WEB_ASSETS");
    println!("cargo:rerun-if-env-changed=AXON_CONFIG_PATH");
    // Only declare the config file when it actually exists. Cargo treats a
    // `rerun-if-changed` path it cannot stat as perpetually dirty, so declaring
    // a missing file re-runs this build script on EVERY cargo invocation.
    //
    // That is the normal state on CI: `build_config_path()` falls back to
    // `$HOME/.axon/config.toml`, which developers have and runners do not.
    // Measured with AXON_CONFIG_PATH pointed at a missing file — two identical
    // back-to-back `cargo check -p axon` runs, no source changes, and the build
    // script re-ran both times.
    //
    // Trade-off: if the file is created later, cargo will not notice until
    // something else dirties this crate. Acceptable — an explicit path is still
    // covered by the AXON_CONFIG_PATH rerun-if-env-changed above, and a one-time
    // missed pickup of a build-time fallback toggle beats re-running forever.
    if let Some(path) = build_config_path().filter(|path| path.is_file()) {
        println!("cargo:rerun-if-changed={}", path.display());
    }

    let manifest_dir =
        PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR set"));
    let source = manifest_dir.join("apps/web/out");

    match asset_state(&source) {
        Ok(AssetState::Ready) => {}
        Ok(
            AssetState::Missing
            | AssetState::Empty
            | AssetState::Incomplete(_)
            | AssetState::FallbackOnly,
        ) if allow_fallback_assets() => {
            println!(
                "cargo:warning=apps/web/out is not a complete web build; embedding fallback web panel"
            );
            fs::create_dir_all(&source).expect("create web assets fallback dir");
            write_fallback_index(&source).expect("write fallback web index");
        }
        Ok(AssetState::Missing | AssetState::Empty) => {
            panic!("apps/web/out is empty; run the web build before compiling axon")
        }
        Ok(AssetState::FallbackOnly) => {
            panic!(
                "apps/web/out contains only fallback assets; run the web build before compiling axon"
            )
        }
        Ok(AssetState::Incomplete(reason)) => {
            panic!("apps/web/out is incomplete ({reason}); run the web build before compiling axon")
        }
        Err(error) => panic!("apps/web/out is unreadable: {error}"),
    }
}

fn allow_fallback_assets() -> bool {
    let explicit_config = env::var_os("AXON_CONFIG_PATH").map(PathBuf::from);
    let home = build_config_home();
    allow_fallback_assets_from(
        env::var("AXON_ALLOW_FALLBACK_WEB_ASSETS").ok().as_deref(),
        explicit_config.as_deref(),
        &home,
    )
}

fn build_config_home() -> PathBuf {
    env::var_os("HOME")
        .map(PathBuf::from)
        .unwrap_or_else(|| PathBuf::from("."))
}

fn build_config_path() -> Option<PathBuf> {
    env::var_os("AXON_CONFIG_PATH")
        .map(PathBuf::from)
        .or_else(|| Some(build_config_home().join(".axon/config.toml")))
}

pub(crate) fn allow_fallback_assets_from(
    env_value: Option<&str>,
    explicit_config_path: Option<&Path>,
    home: &Path,
) -> bool {
    if env_value.is_some_and(|value| !value.trim().is_empty()) {
        return true;
    }

    let config_path = explicit_config_path
        .map(Path::to_path_buf)
        .unwrap_or_else(|| home.join(".axon/config.toml"));
    read_build_allow_fallback_web_assets(&config_path).unwrap_or(false)
}

fn read_build_allow_fallback_web_assets(path: &Path) -> io::Result<bool> {
    let contents = match fs::read_to_string(path) {
        Ok(contents) => contents,
        Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(false),
        Err(err) => return Err(err),
    };

    let mut in_build = false;
    for raw_line in contents.lines() {
        let line = raw_line.split('#').next().unwrap_or("").trim();
        if line.is_empty() {
            continue;
        }
        if line.starts_with('[') && line.ends_with(']') {
            in_build = line == "[build]";
            continue;
        }
        if !in_build {
            continue;
        }
        let Some((key, value)) = line.split_once('=') else {
            continue;
        };
        if key.trim() == "allow-fallback-web-assets" {
            return Ok(matches!(value.trim(), "true" | "\"true\"" | "'true'"));
        }
    }
    Ok(false)
}

enum AssetState {
    Missing,
    Empty,
    Incomplete(&'static str),
    FallbackOnly,
    Ready,
}

fn asset_state(path: &Path) -> io::Result<AssetState> {
    if !path.is_dir() {
        return Ok(AssetState::Missing);
    }

    let entries = fs::read_dir(path)?.collect::<Result<Vec<_>, _>>()?;
    if entries.is_empty() {
        return Ok(AssetState::Empty);
    }

    let index_path = path.join("index.html");
    if !index_path.is_file() {
        return Ok(AssetState::Incomplete("missing index.html"));
    }

    let index = fs::read_to_string(index_path)?;
    if index.contains(FALLBACK_MARKER) {
        return Ok(AssetState::FallbackOnly);
    }

    if !path.join("_next").is_dir() && !path.join("assets").is_dir() {
        return Ok(AssetState::Incomplete("missing static asset directory"));
    }

    Ok(AssetState::Ready)
}

fn write_fallback_index(destination: &Path) -> io::Result<()> {
    fs::write(
        destination.join("index.html"),
        r#"<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="axon-build" content="AXON_FALLBACK_WEB_PANEL" />
    <title>Axon</title>
  </head>
  <body>
    <main>
      <h1>Axon web panel is not built</h1>
      <p>Run <code>npm --prefix apps/web run build</code> to embed the full web panel.</p>
    </main>
  </body>
</html>
"#,
    )
}