1use crate::{
2 artifact_io::{
3 embed_candid_metadata, maybe_shrink_wasm_artifact, write_bytes_atomically,
4 write_gzip_artifact, write_wasm_artifact,
5 },
6 bootstrap_store::{BootstrapWasmStoreBuildOutput, build_bootstrap_wasm_store_artifact},
7 cargo_command, icp_environment_from_env,
8 release_set::{
9 canister_manifest_path, emit_root_release_set_manifest_if_ready, icp_root, workspace_root,
10 },
11 remove_optional_file, should_export_candid_artifacts,
12};
13use std::{
14 env, fs,
15 path::{Path, PathBuf},
16 process::Command,
17};
18use toml::Value as TomlValue;
19
20pub use crate::build_profile::CanisterBuildProfile;
21
22const ROOT_ROLE: &str = "root";
23const WASM_STORE_ROLE: &str = "wasm_store";
24const LOCAL_ARTIFACT_ROOT_RELATIVE: &str = ".icp/local/canisters";
25const WASM_TARGET: &str = "wasm32-unknown-unknown";
26
27#[derive(Clone, Debug)]
32pub struct CanisterArtifactBuildOutput {
33 pub artifact_root: PathBuf,
34 pub wasm_path: PathBuf,
35 pub wasm_gz_path: PathBuf,
36 pub did_path: PathBuf,
37 pub manifest_path: Option<PathBuf>,
38}
39
40#[derive(Clone, Debug, Eq, PartialEq)]
45pub struct WorkspaceBuildContext {
46 pub profile: String,
47 pub requested_profile: String,
48 pub network: String,
49 pub workspace_root: PathBuf,
50 pub icp_root: PathBuf,
51}
52
53impl WorkspaceBuildContext {
54 #[must_use]
55 pub fn lines(&self) -> Vec<String> {
56 let mut lines = vec![
57 "Canic build:".to_string(),
58 format!("profile: {}", self.profile),
59 format!("network: {}", self.network),
60 format!("workspace: {}", self.workspace_root.display()),
61 ];
62
63 if self.requested_profile != "unset" {
64 lines.push(format!("requested profile: {}", self.requested_profile));
65 }
66 if self.icp_root != self.workspace_root {
67 lines.push(format!("icp root: {}", self.icp_root.display()));
68 }
69
70 lines
71 }
72}
73
74pub fn print_current_workspace_build_context_once(
77 profile: CanisterBuildProfile,
78) -> Result<(), Box<dyn std::error::Error>> {
79 if let Some(context) = current_workspace_build_context_once(profile)? {
80 eprintln!("{}", context.lines().join("\n"));
81 }
82
83 Ok(())
84}
85
86pub fn current_workspace_build_context_once(
88 profile: CanisterBuildProfile,
89) -> Result<Option<WorkspaceBuildContext>, Box<dyn std::error::Error>> {
90 let workspace_root = workspace_root()?;
91 let icp_root = icp_root()?;
92 let marker_dir = icp_root.join(".icp");
93 fs::create_dir_all(&marker_dir)?;
94
95 let requested_profile = env::var("CANIC_WASM_PROFILE").unwrap_or_else(|_| "unset".to_string());
96 let network = icp_environment_from_env();
97 let marker_key = icp_ancestor_process_id()
98 .or_else(parent_process_id)
99 .unwrap_or_else(std::process::id)
100 .to_string();
101 let marker_file = marker_dir.join(format!(".canic-build-context-{marker_key}"));
102
103 if marker_file.exists() {
104 return Ok(None);
105 }
106
107 fs::write(&marker_file, [])?;
108 Ok(Some(WorkspaceBuildContext {
109 profile: profile.target_dir_name().to_string(),
110 requested_profile,
111 network,
112 workspace_root,
113 icp_root,
114 }))
115}
116
117pub fn build_current_workspace_canister_artifact(
119 canister_name: &str,
120 profile: CanisterBuildProfile,
121) -> Result<CanisterArtifactBuildOutput, Box<dyn std::error::Error>> {
122 let workspace_root = workspace_root()?;
123 let icp_root = icp_root()?;
124 build_canister_artifact(&workspace_root, &icp_root, canister_name, profile)
125}
126
127pub fn copy_icp_wasm_output(
133 canister_name: &str,
134 output: &CanisterArtifactBuildOutput,
135) -> Result<(), Box<dyn std::error::Error>> {
136 let Some(path) = env::var_os("ICP_WASM_OUTPUT_PATH").map(PathBuf::from) else {
137 return Ok(());
138 };
139
140 if !output.wasm_path.is_file() {
141 return Err(format!(
142 "missing ICP wasm output source for {canister_name}: {}",
143 output.wasm_path.display()
144 )
145 .into());
146 }
147
148 if let Some(parent) = path.parent() {
149 fs::create_dir_all(parent)?;
150 }
151 fs::copy(&output.wasm_path, Path::new(&path))?;
152 Ok(())
153}
154
155fn build_canister_artifact(
157 workspace_root: &Path,
158 icp_root: &Path,
159 canister_name: &str,
160 profile: CanisterBuildProfile,
161) -> Result<CanisterArtifactBuildOutput, Box<dyn std::error::Error>> {
162 if canister_name == WASM_STORE_ROLE {
163 return build_hidden_wasm_store_artifact(workspace_root, icp_root, profile);
164 }
165
166 let canister_manifest_path = canister_manifest_path(workspace_root, canister_name);
167 let canister_package_name = load_canister_package_name(&canister_manifest_path)?;
168 let artifact_root = icp_root
169 .join(LOCAL_ARTIFACT_ROOT_RELATIVE)
170 .join(canister_name);
171 let wasm_path = artifact_root.join(format!("{canister_name}.wasm"));
172 let wasm_gz_path = artifact_root.join(format!("{canister_name}.wasm.gz"));
173 let did_path = artifact_root.join(format!("{canister_name}.did"));
174 let require_embedded_release_artifacts = canister_name == ROOT_ROLE;
175
176 if require_embedded_release_artifacts {
177 build_bootstrap_wasm_store_artifact(workspace_root, icp_root, profile)?;
178 }
179
180 fs::create_dir_all(&artifact_root)?;
181 remove_stale_icp_candid_sidecars(&artifact_root)?;
182
183 let release_wasm_path = run_canister_build(
184 workspace_root,
185 icp_root,
186 &canister_manifest_path,
187 &canister_package_name,
188 profile,
189 require_embedded_release_artifacts,
190 )?;
191 write_wasm_artifact(&release_wasm_path, &wasm_path)?;
192 maybe_shrink_wasm_artifact(&wasm_path)?;
193
194 let network = icp_environment_from_env();
195 if should_export_candid_artifacts(&network) {
196 let debug_wasm_path = run_canister_build(
197 workspace_root,
198 icp_root,
199 &canister_manifest_path,
200 &canister_package_name,
201 CanisterBuildProfile::Debug,
202 require_embedded_release_artifacts,
203 )?;
204 extract_candid(&debug_wasm_path, &did_path)?;
205 embed_candid_metadata(&wasm_path, &did_path)?;
206 } else {
207 remove_optional_file(&did_path)?;
208 }
209 write_gzip_artifact(&wasm_path, &wasm_gz_path)?;
210
211 let manifest_path =
212 emit_root_release_set_manifest_if_ready(workspace_root, icp_root, &network)?;
213
214 Ok(CanisterArtifactBuildOutput {
215 artifact_root,
216 wasm_path,
217 wasm_gz_path,
218 did_path,
219 manifest_path,
220 })
221}
222
223fn load_canister_package_name(manifest_path: &Path) -> Result<String, Box<dyn std::error::Error>> {
226 let manifest_source = fs::read_to_string(manifest_path)?;
227 let manifest = toml::from_str::<TomlValue>(&manifest_source)?;
228 let package_name = manifest
229 .get("package")
230 .and_then(TomlValue::as_table)
231 .and_then(|package| package.get("name"))
232 .and_then(TomlValue::as_str)
233 .ok_or_else(|| format!("missing package.name in {}", manifest_path.display()))?;
234
235 Ok(package_name.to_string())
236}
237
238fn run_canister_build(
240 workspace_root: &Path,
241 icp_root: &Path,
242 manifest_path: &Path,
243 package_name: &str,
244 profile: CanisterBuildProfile,
245 require_embedded_release_artifacts: bool,
246) -> Result<PathBuf, Box<dyn std::error::Error>> {
247 let target_root = std::env::var_os("CARGO_TARGET_DIR")
248 .map_or_else(|| workspace_root.join("target"), PathBuf::from);
249 let mut command = cargo_command();
250 command
251 .current_dir(workspace_root)
252 .env("CARGO_TARGET_DIR", &target_root)
253 .env("CANIC_ICP_ROOT", icp_root)
254 .args([
255 "build",
256 "--manifest-path",
257 &manifest_path.display().to_string(),
258 "--target",
259 WASM_TARGET,
260 ])
261 .args(profile.cargo_args());
262
263 if require_embedded_release_artifacts {
264 command.env("CANIC_REQUIRE_EMBEDDED_RELEASE_ARTIFACTS", "1");
265 }
266
267 let output = command.output()?;
268 if !output.status.success() {
269 return Err(format!(
270 "cargo build failed for {}: {}",
271 manifest_path.display(),
272 String::from_utf8_lossy(&output.stderr)
273 )
274 .into());
275 }
276
277 Ok(target_root
278 .join(WASM_TARGET)
279 .join(profile.target_dir_name())
280 .join(format!("{}.wasm", package_name.replace('-', "_"))))
281}
282
283fn extract_candid(
285 debug_wasm_path: &Path,
286 did_path: &Path,
287) -> Result<(), Box<dyn std::error::Error>> {
288 let output = Command::new("candid-extractor")
289 .arg(debug_wasm_path)
290 .output()
291 .map_err(|err| {
292 format!(
293 "failed to run candid-extractor for {}: {err}",
294 debug_wasm_path.display()
295 )
296 })?;
297
298 if !output.status.success() {
299 return Err(format!(
300 "candid-extractor failed for {}: {}",
301 debug_wasm_path.display(),
302 String::from_utf8_lossy(&output.stderr)
303 )
304 .into());
305 }
306
307 write_bytes_atomically(did_path, &output.stdout)?;
308 Ok(())
309}
310
311fn remove_stale_icp_candid_sidecars(artifact_root: &Path) -> std::io::Result<()> {
314 for relative in [
315 "constructor.did",
316 "service.did",
317 "service.did.d.ts",
318 "service.did.js",
319 ] {
320 let path = artifact_root.join(relative);
321 match fs::remove_file(path) {
322 Ok(()) => {}
323 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
324 Err(err) => return Err(err),
325 }
326 }
327
328 Ok(())
329}
330
331fn build_hidden_wasm_store_artifact(
333 workspace_root: &Path,
334 icp_root: &Path,
335 profile: CanisterBuildProfile,
336) -> Result<CanisterArtifactBuildOutput, Box<dyn std::error::Error>> {
337 let output = build_bootstrap_wasm_store_artifact(workspace_root, icp_root, profile)?;
338 Ok(map_bootstrap_output(output))
339}
340
341fn map_bootstrap_output(output: BootstrapWasmStoreBuildOutput) -> CanisterArtifactBuildOutput {
343 CanisterArtifactBuildOutput {
344 artifact_root: output.artifact_root,
345 wasm_path: output.wasm_path,
346 wasm_gz_path: output.wasm_gz_path,
347 did_path: output.did_path,
348 manifest_path: None,
349 }
350}
351
352fn parent_process_id() -> Option<u32> {
354 let stat = fs::read_to_string("/proc/self/stat").ok()?;
355 parse_parent_process_id(&stat)
356}
357
358fn icp_ancestor_process_id() -> Option<u32> {
360 let mut pid = parent_process_id()?;
361 loop {
362 if process_comm(pid).as_deref() == Some("icp") {
363 return Some(pid);
364 }
365
366 let parent = process_parent_id(pid)?;
367 if parent == 0 || parent == pid {
368 return None;
369 }
370 pid = parent;
371 }
372}
373
374fn process_parent_id(pid: u32) -> Option<u32> {
376 let stat = fs::read_to_string(format!("/proc/{pid}/stat")).ok()?;
377 parse_parent_process_id(&stat)
378}
379
380fn process_comm(pid: u32) -> Option<String> {
382 fs::read_to_string(format!("/proc/{pid}/comm"))
383 .ok()
384 .map(|comm| comm.trim().to_string())
385}
386
387fn parse_parent_process_id(stat: &str) -> Option<u32> {
389 let (_, suffix) = stat.rsplit_once(") ")?;
390 let mut parts = suffix.split_whitespace();
391 let _state = parts.next()?;
392 parts.next()?.parse::<u32>().ok()
393}
394
395#[cfg(test)]
396mod tests {
397 use super::{parse_parent_process_id, remove_stale_icp_candid_sidecars};
398 use crate::test_support::temp_dir;
399 use std::fs;
400
401 #[test]
402 fn parse_parent_process_id_accepts_proc_stat_shape() {
403 let stat = "12345 (build_canister_ar) S 67890 0 0 0";
404 assert_eq!(parse_parent_process_id(stat), Some(67890));
405 }
406
407 #[test]
408 fn remove_stale_icp_candid_sidecars_keeps_primary_role_did() {
409 let temp_root = temp_dir("canic-canister-build-sidecars");
410 let _ = fs::remove_dir_all(&temp_root);
411 fs::create_dir_all(&temp_root).unwrap();
412
413 for name in [
414 "constructor.did",
415 "service.did",
416 "service.did.d.ts",
417 "service.did.js",
418 "app.did",
419 ] {
420 fs::write(temp_root.join(name), "x").unwrap();
421 }
422
423 remove_stale_icp_candid_sidecars(&temp_root).unwrap();
424
425 assert!(!temp_root.join("constructor.did").exists());
426 assert!(!temp_root.join("service.did").exists());
427 assert!(!temp_root.join("service.did.d.ts").exists());
428 assert!(!temp_root.join("service.did.js").exists());
429 assert!(temp_root.join("app.did").exists());
430
431 let _ = fs::remove_dir_all(temp_root);
432 }
433}