canic-host 0.92.9

Host-side build, install, deployment, and fleet-template library for Canic workspaces
Documentation
use std::path::Path;

use crate::evidence_envelope::file_input_fingerprint;

use crate::canister_build::{
    ArtifactTransformKind, ArtifactTransformMode, ArtifactTransformOutcome,
};

use super::model::{
    ArtifactProvenanceKindV1, ArtifactProvenanceV1, ArtifactTransformKindV1,
    ArtifactTransformModeV1, ArtifactTransformOutcomeV1, ArtifactTransformProvenanceV1,
    BuildProvenanceRequest,
};

pub(super) fn artifact_provenance(
    request: &BuildProvenanceRequest,
) -> Result<Vec<ArtifactProvenanceV1>, Box<dyn std::error::Error>> {
    let mut artifacts = Vec::new();
    push_artifact(
        &mut artifacts,
        request,
        ArtifactProvenanceKindV1::Wasm,
        &request.output.wasm_path,
    )?;
    push_artifact(
        &mut artifacts,
        request,
        ArtifactProvenanceKindV1::WasmGzip,
        &request.output.wasm_gz_path,
    )?;
    push_existing_artifact(
        &mut artifacts,
        request,
        ArtifactProvenanceKindV1::Candid,
        &request.output.did_path,
    )?;
    Ok(artifacts)
}

pub(super) fn artifact_transform_provenance(
    request: &BuildProvenanceRequest,
) -> Result<Vec<ArtifactTransformProvenanceV1>, Box<dyn std::error::Error>> {
    request
        .output
        .transforms
        .iter()
        .map(|transform| {
            if transform.role.trim().is_empty() {
                return Err("artifact transform role must not be empty".into());
            }
            if transform.tool.trim().is_empty() {
                return Err("artifact transform tool must not be empty".into());
            }
            match transform.outcome {
                ArtifactTransformOutcome::Applied
                    if transform
                        .tool_version
                        .as_deref()
                        .is_none_or(|version| version.trim().is_empty()) =>
                {
                    return Err("applied artifact transform must record a tool version".into());
                }
                ArtifactTransformOutcome::ToolUnavailable
                | ArtifactTransformOutcome::NotRequested
                    if transform.tool_version.is_some() =>
                {
                    return Err(
                        "unapplied artifact transform must not record a tool version".into(),
                    );
                }
                _ => {}
            }
            Ok(ArtifactTransformProvenanceV1 {
                role: transform.role.clone(),
                transform: match transform.transform {
                    ArtifactTransformKind::Shrink => ArtifactTransformKindV1::Shrink,
                    ArtifactTransformKind::CandidMetadata => {
                        ArtifactTransformKindV1::CandidMetadata
                    }
                },
                mode: match transform.mode {
                    ArtifactTransformMode::Optional => ArtifactTransformModeV1::Optional,
                },
                tool: transform.tool.clone(),
                tool_version: transform.tool_version.clone(),
                outcome: match transform.outcome {
                    ArtifactTransformOutcome::Applied => ArtifactTransformOutcomeV1::Applied,
                    ArtifactTransformOutcome::ToolUnavailable => {
                        ArtifactTransformOutcomeV1::ToolUnavailable
                    }
                    ArtifactTransformOutcome::NotRequested => {
                        ArtifactTransformOutcomeV1::NotRequested
                    }
                },
            })
        })
        .collect()
}

fn push_existing_artifact(
    artifacts: &mut Vec<ArtifactProvenanceV1>,
    request: &BuildProvenanceRequest,
    kind: ArtifactProvenanceKindV1,
    path: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
    if path.is_file() {
        push_artifact(artifacts, request, kind, path)?;
    }
    Ok(())
}

fn push_artifact(
    artifacts: &mut Vec<ArtifactProvenanceV1>,
    request: &BuildProvenanceRequest,
    kind: ArtifactProvenanceKindV1,
    path: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
    let fingerprint =
        file_input_fingerprint("build_artifact", path, &request.workspace_root, None, None)?;
    artifacts.push(ArtifactProvenanceV1 {
        role: request.role.clone(),
        fleet: request.fleet.clone(),
        artifact_kind: kind,
        path: fingerprint.path,
        path_display: fingerprint.path_display,
        hash_algorithm: "sha256".to_string(),
        sha256: fingerprint
            .sha256
            .ok_or_else(|| format!("missing sha256 for {}", path.display()))?,
        size_bytes: fingerprint
            .size_bytes
            .ok_or_else(|| format!("missing size for {}", path.display()))?,
        produced_by: "canic build".to_string(),
    });
    Ok(())
}