canic-host 0.92.5

Host-side build, install, deployment, and fleet-template library for Canic workspaces
Documentation
use crate::durable_io::write_bytes;
use std::{
    fs,
    io::{Read, Write},
    path::Path,
    process::Command,
};

use flate2::{Compression, GzBuilder};

// Apply `ic-wasm shrink` when available; absence of the optional tool is not
// fatal, but execution failures are surfaced because they usually mean bad IO.
pub fn maybe_shrink_wasm_artifact(wasm_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
    maybe_shrink_wasm_artifact_with_command("ic-wasm", wasm_path)
}

fn maybe_shrink_wasm_artifact_with_command(
    command_name: &str,
    wasm_path: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
    let shrunk_path = wasm_path.with_extension("wasm.shrunk");
    match Command::new(command_name)
        .arg(wasm_path)
        .arg("-o")
        .arg(&shrunk_path)
        .arg("shrink")
        .output()
    {
        Ok(output) if output.status.success() => {
            fs::rename(shrunk_path, wasm_path)?;
        }
        Ok(output) => {
            let _ = fs::remove_file(shrunk_path);
            return Err(format!(
                "ic-wasm shrink failed for {} with status {}: {}",
                wasm_path.display(),
                output.status,
                String::from_utf8_lossy(&output.stderr).trim()
            )
            .into());
        }
        Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
        Err(err) => {
            return Err(format!("failed to run ic-wasm for {}: {err}", wasm_path.display()).into());
        }
    }

    Ok(())
}

// Copy one `.wasm` artifact atomically into the local ICP artifact tree.
pub fn write_wasm_artifact(
    source_path: &Path,
    target_path: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
    let bytes = fs::read(source_path)?;
    write_bytes(target_path, &bytes)?;
    Ok(())
}

// Write one deterministic `.wasm.gz` artifact with a zeroed gzip timestamp.
pub fn write_gzip_artifact(
    wasm_path: &Path,
    wasm_gz_path: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
    let mut wasm_bytes = Vec::new();
    fs::File::open(wasm_path)?.read_to_end(&mut wasm_bytes)?;

    let mut encoder = GzBuilder::new()
        .mtime(0)
        .write(Vec::new(), Compression::best());
    encoder.write_all(&wasm_bytes)?;
    let gz_bytes = encoder.finish()?;
    write_bytes(wasm_gz_path, &gz_bytes)?;
    Ok(())
}

// Embed the extracted service interface for local artifacts so
// `icp canister metadata <canister> candid:service` introspection works during
// development. Production `ic` builds skip this path.
pub fn embed_candid_metadata(
    wasm_path: &Path,
    did_path: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
    embed_candid_metadata_with_command("ic-wasm", wasm_path, did_path)
}

fn embed_candid_metadata_with_command(
    command_name: &str,
    wasm_path: &Path,
    did_path: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
    let output = Command::new(command_name)
        .arg(wasm_path)
        .args(["-o"])
        .arg(wasm_path)
        .args(["metadata", "candid:service", "-f"])
        .arg(did_path)
        .args(["-v", "public"])
        .output();

    let output = match output {
        Ok(output) => output,
        Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(()),
        Err(err) => {
            return Err(format!(
                "failed to run ic-wasm metadata for {}: {err}",
                wasm_path.display()
            )
            .into());
        }
    };

    if !output.status.success() {
        return Err(format!(
            "ic-wasm metadata failed for {}: {}",
            wasm_path.display(),
            String::from_utf8_lossy(&output.stderr)
        )
        .into());
    }

    Ok(())
}

#[cfg(test)]
mod tests;