Skip to main content

canic_host/canister_build/
context.rs

1use std::{fs, path::PathBuf, process::Command};
2
3use canic_core::ids::{BuildNetwork, RELEASE_BUILD_ID_ENV, ReleaseBuildId};
4
5use crate::icp::LocalReplicaTarget;
6
7use super::{
8    CanisterBuildProfile,
9    process::{icp_ancestor_process_id, parent_process_id},
10};
11
12/// Exact authority for one canister artifact build.
13///
14
15#[derive(Clone, Debug, Eq, PartialEq)]
16pub struct WorkspaceBuildContext {
17    pub role: String,
18    pub profile: CanisterBuildProfile,
19    pub environment: String,
20    pub build_network: BuildNetwork,
21    pub workspace_root: PathBuf,
22    pub icp_root: PathBuf,
23    pub config_path: PathBuf,
24    pub local_replica: Option<LocalReplicaTarget>,
25    pub refresh_canonical_infrastructure_did: bool,
26    pub release_build_id: Option<ReleaseBuildId>,
27}
28
29impl WorkspaceBuildContext {
30    #[must_use]
31    pub fn lines(&self) -> Vec<String> {
32        let mut lines = vec![
33            "Canic build:".to_string(),
34            format!("role: {}", self.role),
35            format!("profile: {}", self.profile.target_dir_name()),
36            format!("environment: {}", self.environment),
37            format!("build network: {}", self.build_network),
38            format!("workspace: {}", self.workspace_root.display()),
39        ];
40
41        if self.icp_root != self.workspace_root {
42            lines.push(format!("icp root: {}", self.icp_root.display()));
43        }
44        if let Some(release_build_id) = self.release_build_id {
45            lines.push(format!("release build: {release_build_id}"));
46        }
47
48        lines
49    }
50
51    /// Return a copy using a different Cargo profile for one child build.
52    #[must_use]
53    pub fn with_profile(&self, profile: CanisterBuildProfile) -> Self {
54        let mut context = self.clone();
55        context.profile = profile;
56        context
57    }
58
59    /// Return a copy selecting another role in the same workspace authority.
60    #[must_use]
61    pub fn with_role(&self, role: impl Into<String>) -> Self {
62        let mut context = self.clone();
63        context.role = role.into();
64        context
65    }
66
67    /// Return a copy bound to one already-durable release-build plan.
68    #[must_use]
69    pub fn with_release_build_id(&self, release_build_id: ReleaseBuildId) -> Self {
70        let mut context = self.clone();
71        context.release_build_id = Some(release_build_id);
72        context
73    }
74
75    /// Resolve an output namespace owned by this exact build authority.
76    #[must_use]
77    pub(crate) fn artifact_root(&self) -> PathBuf {
78        self.release_build_id.map_or_else(
79            || self.icp_root.join(".icp/local/canisters"),
80            |release_build_id| {
81                self.icp_root
82                    .join(".canic/release-builds")
83                    .join(release_build_id.to_string())
84                    .join("artifacts")
85            },
86        )
87    }
88
89    /// Apply the exact Canic build authority to one child command.
90    pub fn apply_to_command(&self, command: &mut Command) {
91        crate::build_environment::apply(command);
92        command
93            .env_remove(canic_core::role_contract::CANONICAL_CANDID_BUILD_ENV)
94            .env_remove(RELEASE_BUILD_ID_ENV)
95            .env_remove(canic_core::role_contract::build_context::PROTOCOL_BUILD_CONTEXT_ENV)
96            .env("ICP_ENVIRONMENT", self.build_network.as_str())
97            .env(
98                canic_core::role_contract::CANONICAL_BUILD_ICP_ROOT_ENV,
99                &self.icp_root,
100            )
101            .env(
102                canic_core::role_contract::CANONICAL_BUILD_CONFIG_PATH_ENV,
103                &self.config_path,
104            );
105        if let Some(release_build_id) = self.release_build_id {
106            command.env(RELEASE_BUILD_ID_ENV, release_build_id.to_string());
107        }
108    }
109}
110
111// Print the current build context once per caller session so caller builds
112// stay readable without repeating root/profile diagnostics for every canister.
113pub fn print_workspace_build_context_once(
114    context: &WorkspaceBuildContext,
115) -> Result<(), Box<dyn std::error::Error>> {
116    if workspace_build_context_once(context)? {
117        eprintln!("{}", context.lines().join("\n"));
118    }
119
120    Ok(())
121}
122
123// Return whether this caller should print its explicit build context.
124pub fn workspace_build_context_once(
125    context: &WorkspaceBuildContext,
126) -> Result<bool, Box<dyn std::error::Error>> {
127    let marker_dir = context.icp_root.join(".icp");
128    fs::create_dir_all(&marker_dir)?;
129
130    let marker_key = icp_ancestor_process_id()
131        .or_else(parent_process_id)
132        .unwrap_or_else(std::process::id)
133        .to_string();
134    let marker_file = marker_dir.join(format!(".canic-build-context-{marker_key}"));
135
136    if marker_file.exists() {
137        return Ok(false);
138    }
139
140    fs::write(&marker_file, [])?;
141    Ok(true)
142}