canic 0.20.4

Canic — a canister orchestration and management toolkit for the Internet Computer
Documentation
use canic_core::bootstrap::compiled::ConfigModel;
use std::{
    env,
    fmt::Write as _,
    fs,
    path::{Path, PathBuf},
};

const ROOT_RELEASE_ASSET_DIR: &str = "embedded_root_release_bundle";
const ROOT_WASM_STORE_BOOTSTRAP_ROLE: &str = "wasm_store";
const ROOT_WASM_STORE_BOOTSTRAP_RELEASE_SET_FILE: &str =
    "canic.root-wasm-store-bootstrap-release-set.rs";

#[must_use]
pub fn emit_root_wasm_store_bootstrap_release_set(config_path: &Path) -> bool {
    let manifest_dir = PathBuf::from(
        env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR must be set for root build"),
    );
    let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR must be set for root build"));
    let workspace_root = discover_workspace_root(&manifest_dir);
    let artifact_root = discover_release_artifact_root(&workspace_root);
    let generated_path = out_dir.join(ROOT_WASM_STORE_BOOTSTRAP_RELEASE_SET_FILE);
    let strict_artifacts =
        env::var("CANIC_REQUIRE_EMBEDDED_RELEASE_ARTIFACTS").is_ok_and(|value| value == "1");
    let artifact_path = artifact_root
        .join(ROOT_WASM_STORE_BOOTSTRAP_ROLE)
        .join(format!("{ROOT_WASM_STORE_BOOTSTRAP_ROLE}.wasm.gz"));

    println!("cargo:rerun-if-changed={}", workspace_root.display());
    println!("cargo:rerun-if-changed={}", config_path.display());
    println!("cargo:rerun-if-changed={}", artifact_root.display());
    println!("cargo:rerun-if-changed={}", artifact_path.display());
    println!("cargo:rerun-if-env-changed=CANIC_REQUIRE_EMBEDDED_RELEASE_ARTIFACTS");

    if !artifact_path.is_file() {
        assert!(
            !strict_artifacts,
            "root bootstrap requires the build-produced wasm_store artifact at {}; build wasm_store through the normal DFX/custom build path first",
            artifact_path.display()
        );

        println!(
            "cargo:warning=skipping embedded wasm_store bootstrap release set: missing build-produced artifact at {}",
            artifact_path.display()
        );
        return false;
    }

    let artifact_path = artifact_path
        .canonicalize()
        .expect("canonicalize build-produced wasm_store bootstrap artifact");
    println!(
        "cargo:warning=embedding build-produced wasm_store bootstrap artifact from {}",
        artifact_path.display()
    );

    let generated = render_root_wasm_store_bootstrap_release_set_source(&artifact_path);
    fs::write(&generated_path, generated)
        .expect("write embedded wasm_store bootstrap release set source");

    let generated_abs = generated_path
        .canonicalize()
        .expect("canonicalize embedded wasm_store bootstrap release set source path");
    println!(
        "cargo:rustc-env=CANIC_ROOT_WASM_STORE_BOOTSTRAP_RELEASE_SET_PATH={}",
        generated_abs.display()
    );
    println!("cargo:rerun-if-changed={}", generated_abs.display());
    true
}

#[must_use]
pub fn emit_root_release_bundle(config_path: &Path, config: &ConfigModel) -> bool {
    let manifest_dir = PathBuf::from(
        env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR must be set for root build"),
    );
    let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR must be set for root build"));
    let workspace_root = discover_workspace_root(&manifest_dir);
    let artifact_root = discover_release_artifact_root(&workspace_root);
    let asset_dir = out_dir.join(ROOT_RELEASE_ASSET_DIR);

    fs::create_dir_all(&asset_dir).expect("create embedded root release asset dir");

    println!("cargo:rerun-if-changed={}", workspace_root.display());
    println!("cargo:rerun-if-changed={}", config_path.display());
    println!("cargo:rerun-if-changed={}", artifact_root.display());

    let mut built_entries = Vec::new();

    for role_name in configured_release_roles(config) {
        let artifact_path = resolve_release_wasm_path(&workspace_root, &artifact_root, &role_name);
        if !artifact_path.is_file() {
            println!(
                "cargo:warning=canic root bundle skipped role '{role_name}': missing built artifact at {}; build dependencies first if this role should bootstrap automatically",
                artifact_path.display()
            );
            continue;
        }

        let out_wasm = asset_dir.join(format!("{role_name}.wasm"));
        fs::copy(&artifact_path, &out_wasm).unwrap_or_else(|err| {
            panic!(
                "copy embedded release wasm for role '{role_name}' from {} to {} failed: {err}",
                artifact_path.display(),
                out_wasm.display()
            )
        });
        built_entries.push((role_name, out_wasm));
    }

    let generated = render_root_release_bundle_source(&built_entries);
    let generated_path = out_dir.join("canic.root-release-bundle.rs");
    fs::write(&generated_path, generated).expect("write embedded root release bundle source");

    let generated_abs = generated_path
        .canonicalize()
        .expect("canonicalize embedded root release bundle source path");
    println!(
        "cargo:rustc-env=CANIC_ROOT_RELEASE_BUNDLE_PATH={}",
        generated_abs.display()
    );
    println!("cargo:rerun-if-changed={}", generated_abs.display());
    true
}

fn discover_workspace_root(manifest_dir: &Path) -> PathBuf {
    for candidate in manifest_dir.ancestors() {
        let cargo_toml = candidate.join("Cargo.toml");
        if !cargo_toml.is_file() {
            continue;
        }

        let cargo_toml_text = fs::read_to_string(&cargo_toml)
            .unwrap_or_else(|err| panic!("read {} failed: {err}", cargo_toml.display()));

        if cargo_toml_text.contains("[workspace]") {
            return candidate.to_path_buf();
        }
    }

    panic!(
        "unable to discover workspace root from {}; expected an ancestor Cargo.toml with [workspace]",
        manifest_dir.display()
    );
}

fn discover_release_artifact_root(workspace_root: &Path) -> PathBuf {
    let network = env::var("DFX_NETWORK").unwrap_or_else(|_| "local".to_string());
    let network_root = workspace_root.join(".dfx").join(&network).join("canisters");
    if network_root.is_dir() {
        return network_root;
    }

    let local_root = workspace_root.join(".dfx").join("local").join("canisters");
    if local_root.is_dir() {
        return local_root;
    }

    network_root
}

fn discover_cargo_target_root(workspace_root: &Path) -> PathBuf {
    env::var_os("CARGO_TARGET_DIR").map_or_else(|| workspace_root.join("target"), PathBuf::from)
}

fn configured_release_roles(config: &ConfigModel) -> Vec<String> {
    let mut roles = Vec::new();

    for subnet in config.subnets.values() {
        for role in subnet.canisters.keys() {
            if role.is_root() || role.is_wasm_store() {
                continue;
            }

            let role_name = role.as_str().to_string();
            if !roles.iter().any(|existing| existing == &role_name) {
                roles.push(role_name);
            }
        }
    }

    roles.sort();
    roles
}

fn resolve_release_wasm_path(
    workspace_root: &Path,
    artifact_root: &Path,
    role_name: &str,
) -> PathBuf {
    let dfx_path = artifact_root
        .join(role_name)
        .join(format!("{role_name}.wasm"));
    if dfx_path.is_file() {
        return dfx_path;
    }

    discover_cargo_target_root(workspace_root)
        .join("wasm32-unknown-unknown")
        .join("release")
        .join(format!("canister_{role_name}.wasm"))
}

fn render_root_release_bundle_source(entries: &[(String, PathBuf)]) -> String {
    let mut rendered = String::from("&[\n");

    for (role_name, wasm_path) in entries {
        let path = wasm_path.to_string_lossy();
        rendered.push_str("    canic::__internal::core::bootstrap::EmbeddedRootReleaseEntry {\n");
        let _ = writeln!(rendered, "        role: {role_name:?},");
        let _ = writeln!(rendered, "        wasm_module: include_bytes!({path:?}),");
        rendered.push_str("    },\n");
    }

    rendered.push(']');
    rendered.push('\n');
    rendered
}

fn render_root_wasm_store_bootstrap_release_set_source(artifact_path: &Path) -> String {
    let path = artifact_path.to_string_lossy();
    let mut rendered = String::from("&[\n");

    rendered.push_str("    canic::__internal::core::bootstrap::EmbeddedRootReleaseEntry {\n");
    let _ = writeln!(
        rendered,
        "        role: {ROOT_WASM_STORE_BOOTSTRAP_ROLE:?},"
    );
    let _ = writeln!(rendered, "        wasm_module: include_bytes!({path:?}),");
    rendered.push_str("    },\n");
    rendered.push(']');
    rendered.push('\n');
    rendered
}