Skip to main content

canic_host/canister_build/
context.rs

1use std::{fs, path::PathBuf, process::Command};
2
3use crate::icp::LocalReplicaTarget;
4
5use super::{
6    CanisterBuildProfile,
7    process::{icp_ancestor_process_id, parent_process_id},
8};
9
10/// Exact authority for one canister artifact build.
11///
12
13#[derive(Clone, Debug, Eq, PartialEq)]
14pub struct WorkspaceBuildContext {
15    pub role: String,
16    pub profile: CanisterBuildProfile,
17    pub environment: String,
18    pub build_network: String,
19    pub workspace_root: PathBuf,
20    pub icp_root: PathBuf,
21    pub config_path: PathBuf,
22    pub local_replica: Option<LocalReplicaTarget>,
23    pub refresh_canonical_wasm_store_did: bool,
24}
25
26impl WorkspaceBuildContext {
27    #[must_use]
28    pub fn lines(&self) -> Vec<String> {
29        let mut lines = vec![
30            "Canic build:".to_string(),
31            format!("role: {}", self.role),
32            format!("profile: {}", self.profile.target_dir_name()),
33            format!("environment: {}", self.environment),
34            format!("build network: {}", self.build_network),
35            format!("workspace: {}", self.workspace_root.display()),
36        ];
37
38        if self.icp_root != self.workspace_root {
39            lines.push(format!("icp root: {}", self.icp_root.display()));
40        }
41
42        lines
43    }
44
45    /// Return a copy using a different Cargo profile for one child build.
46    #[must_use]
47    pub fn with_profile(&self, profile: CanisterBuildProfile) -> Self {
48        let mut context = self.clone();
49        context.profile = profile;
50        context
51    }
52
53    /// Return a copy selecting another role in the same workspace authority.
54    #[must_use]
55    pub fn with_role(&self, role: impl Into<String>) -> Self {
56        let mut context = self.clone();
57        context.role = role.into();
58        context
59    }
60
61    /// Apply the exact Canic build authority to one child command.
62    pub fn apply_to_command(&self, command: &mut Command) {
63        command
64            .env("ICP_ENVIRONMENT", &self.build_network)
65            .env(
66                canic_core::role_contract::CANONICAL_BUILD_ICP_ROOT_ENV,
67                &self.icp_root,
68            )
69            .env(
70                canic_core::role_contract::CANONICAL_BUILD_CONFIG_PATH_ENV,
71                &self.config_path,
72            );
73    }
74}
75
76// Print the current build context once per caller session so caller builds
77// stay readable without repeating root/profile diagnostics for every canister.
78pub fn print_workspace_build_context_once(
79    context: &WorkspaceBuildContext,
80) -> Result<(), Box<dyn std::error::Error>> {
81    if workspace_build_context_once(context)? {
82        eprintln!("{}", context.lines().join("\n"));
83    }
84
85    Ok(())
86}
87
88// Return whether this caller should print its explicit build context.
89pub fn workspace_build_context_once(
90    context: &WorkspaceBuildContext,
91) -> Result<bool, Box<dyn std::error::Error>> {
92    let marker_dir = context.icp_root.join(".icp");
93    fs::create_dir_all(&marker_dir)?;
94
95    let marker_key = icp_ancestor_process_id()
96        .or_else(parent_process_id)
97        .unwrap_or_else(std::process::id)
98        .to_string();
99    let marker_file = marker_dir.join(format!(".canic-build-context-{marker_key}"));
100
101    if marker_file.exists() {
102        return Ok(false);
103    }
104
105    fs::write(&marker_file, [])?;
106    Ok(true)
107}