canic-host 0.69.5

Host-side build, install, deployment, and fleet-template library for Canic workspaces
Documentation
use super::InstallRootOptions;
use super::operations::EmitRootManifestOperation;
use super::phase_receipts::{
    CompletedInstallPhase, install_deployment_truth_phase_receipt, receipt_with_execution_context,
};
use super::receipt_io::write_install_deployment_truth_receipt;
use crate::deployment_truth::{DeploymentCheckV1, DeploymentExecutionContextV1, DeploymentPlanV1};
use crate::release_set::{
    ReleaseSetEntry, RootReleaseSetManifest, resolve_artifact_root, root_release_set_manifest_path,
};
use canic_core::CANIC_WASM_CHUNK_BYTES;
use canic_core::cdk::utils::hash::wasm_hash_hex;
use std::{
    fs,
    path::{Path, PathBuf},
    time::{Duration, Instant},
};

pub(super) fn validate_plan_artifacts_with_phase(
    plan: &DeploymentPlanV1,
    icp_root: &Path,
    network: &str,
) -> Result<(CompletedInstallPhase, Duration), Box<dyn std::error::Error>> {
    let started_at = super::current_unix_timestamp_label()?;
    let started = Instant::now();
    validate_plan_artifact_paths(plan, icp_root, network)?;
    let duration = started.elapsed();
    let role_names = plan
        .role_artifacts
        .iter()
        .map(|artifact| artifact.role.clone())
        .collect::<Vec<_>>();
    let phase = CompletedInstallPhase {
        phase: "materialize_artifacts",
        attempted_action: "validate supplied deployment plan artifacts",
        started_at,
        finished_at: Some(super::current_unix_timestamp_label()?),
        evidence: vec![format!("deployment_plan:{}", plan.plan_id)],
        role_names,
    };
    Ok((phase, duration))
}

pub(super) fn emit_manifest_with_deployment_truth_receipt(
    workspace_root: &Path,
    icp_root: &Path,
    options: &InstallRootOptions,
    config_path: &Path,
    deployment_name: &str,
    deployment_truth_check: &DeploymentCheckV1,
    execution_context: &DeploymentExecutionContextV1,
) -> Result<(PathBuf, Duration), Box<dyn std::error::Error>> {
    let operation =
        EmitRootManifestOperation::new(workspace_root, icp_root, &options.network, config_path);
    let emit_manifest_started_at_label = super::current_unix_timestamp_label()?;
    let emit_manifest_started_at = Instant::now();
    let manifest_path = if let Some(plan) = &options.deployment_plan_override {
        emit_root_release_set_manifest_from_plan(icp_root, &options.network, plan)?
    } else {
        operation.execute()?
    };
    let emit_manifest_duration = emit_manifest_started_at.elapsed();
    let emit_manifest_receipt = receipt_with_execution_context(
        install_deployment_truth_phase_receipt(
            deployment_truth_check,
            "emit_manifest",
            emit_manifest_started_at_label,
            Some(super::current_unix_timestamp_label()?),
            "emit root release-set manifest",
            crate::deployment_truth::ObservationStatusV1::Observed,
            EmitRootManifestOperation::evidence(&manifest_path),
        ),
        execution_context,
    );
    let emit_manifest_receipt_path = write_install_deployment_truth_receipt(
        icp_root,
        &options.network,
        deployment_name,
        &emit_manifest_receipt,
    )?;
    println!(
        "Deployment truth receipt JSON: {}",
        emit_manifest_receipt_path.display()
    );
    Ok((manifest_path, emit_manifest_duration))
}

pub(super) fn root_wasm_for_install_plan(
    icp_root: &Path,
    network: &str,
    root_build_target: &str,
    plan: Option<&DeploymentPlanV1>,
) -> Result<PathBuf, Box<dyn std::error::Error>> {
    let artifact_root = resolve_artifact_root(icp_root, network)?;
    if let Some(plan) = plan {
        let root_artifact = plan
            .role_artifacts
            .iter()
            .find(|artifact| artifact.role == "root")
            .ok_or_else(|| "deployment plan is missing root role artifact".to_string())?;
        return Ok(plan_role_wasm_path(icp_root, &artifact_root, root_artifact));
    }

    Ok(artifact_root
        .join(root_build_target)
        .join(format!("{root_build_target}.wasm")))
}

fn validate_plan_artifact_paths(
    plan: &DeploymentPlanV1,
    icp_root: &Path,
    network: &str,
) -> Result<(), Box<dyn std::error::Error>> {
    let artifact_root = resolve_artifact_root(icp_root, network)?;
    let root_artifact = plan
        .role_artifacts
        .iter()
        .find(|artifact| artifact.role == "root")
        .ok_or_else(|| "deployment plan is missing root role artifact".to_string())?;
    let root_wasm = plan_role_wasm_path(icp_root, &artifact_root, root_artifact);
    if !root_wasm.is_file() {
        return Err(format!(
            "deployment plan root wasm artifact does not exist: {}",
            root_wasm.display()
        )
        .into());
    }

    for artifact in plan_release_role_artifacts(plan) {
        let wasm_gz = plan_role_wasm_gz_path(icp_root, &artifact_root, artifact);
        if !wasm_gz.is_file() {
            return Err(format!(
                "deployment plan role {} wasm.gz artifact does not exist: {}",
                artifact.role,
                wasm_gz.display()
            )
            .into());
        }
    }
    Ok(())
}

fn emit_root_release_set_manifest_from_plan(
    icp_root: &Path,
    network: &str,
    plan: &DeploymentPlanV1,
) -> Result<PathBuf, Box<dyn std::error::Error>> {
    let artifact_root = resolve_artifact_root(icp_root, network)?;
    let manifest_path = root_release_set_manifest_path(&artifact_root)?;
    let entries = plan_release_role_artifacts(plan)
        .map(|artifact| release_set_entry_from_plan_artifact(icp_root, &artifact_root, artifact))
        .collect::<Result<Vec<_>, _>>()?;
    let manifest = RootReleaseSetManifest {
        release_version: plan
            .deployment_identity
            .canic_version
            .clone()
            .unwrap_or_else(|| plan.plan_id.clone()),
        entries,
    };

    fs::write(&manifest_path, serde_json::to_vec_pretty(&manifest)?)?;
    Ok(manifest_path)
}

fn release_set_entry_from_plan_artifact(
    icp_root: &Path,
    artifact_root: &Path,
    artifact: &crate::deployment_truth::RoleArtifactV1,
) -> Result<ReleaseSetEntry, Box<dyn std::error::Error>> {
    let artifact_path = plan_role_wasm_gz_path(icp_root, artifact_root, artifact);
    let artifact_relative_path = artifact_path
        .strip_prefix(icp_root)
        .map_err(|_| {
            format!(
                "deployment plan artifact {} is not under ICP root {}",
                artifact_path.display(),
                icp_root.display()
            )
        })?
        .to_string_lossy()
        .to_string();
    let wasm_module = fs::read(&artifact_path)?;
    let chunk_hashes = wasm_module
        .chunks(CANIC_WASM_CHUNK_BYTES)
        .map(wasm_hash_hex)
        .collect::<Vec<_>>();

    Ok(ReleaseSetEntry {
        role: artifact.role.clone(),
        template_id: format!("embedded:{}", artifact.role),
        artifact_relative_path,
        payload_size_bytes: wasm_module.len() as u64,
        payload_sha256_hex: wasm_hash_hex(&wasm_module),
        chunk_size_bytes: CANIC_WASM_CHUNK_BYTES as u64,
        chunk_sha256_hex: chunk_hashes,
    })
}

fn plan_release_role_artifacts(
    plan: &DeploymentPlanV1,
) -> impl Iterator<Item = &crate::deployment_truth::RoleArtifactV1> {
    plan.role_artifacts
        .iter()
        .filter(|artifact| !matches!(artifact.role.as_str(), "root" | "wasm_store"))
}

fn plan_role_wasm_path(
    icp_root: &Path,
    artifact_root: &Path,
    artifact: &crate::deployment_truth::RoleArtifactV1,
) -> PathBuf {
    artifact.wasm_path.as_ref().map_or_else(
        || {
            artifact_root
                .join(&artifact.role)
                .join(format!("{}.wasm", artifact.role))
        },
        |path| plan_artifact_path(icp_root, path),
    )
}

fn plan_role_wasm_gz_path(
    icp_root: &Path,
    artifact_root: &Path,
    artifact: &crate::deployment_truth::RoleArtifactV1,
) -> PathBuf {
    artifact.wasm_gz_path.as_ref().map_or_else(
        || {
            artifact_root
                .join(&artifact.role)
                .join(format!("{}.wasm.gz", artifact.role))
        },
        |path| plan_artifact_path(icp_root, path),
    )
}

fn plan_artifact_path(icp_root: &Path, path: &str) -> PathBuf {
    let path = PathBuf::from(path);
    if path.is_absolute() {
        path
    } else {
        icp_root.join(path)
    }
}