1use crate::{
2 bootstrap_store::{
3 BootstrapWasmStoreBuildOutput, BootstrapWasmStoreBuildProfile,
4 build_bootstrap_wasm_store_artifact,
5 },
6 cargo_command,
7 release_set::{
8 canister_manifest_path, dfx_root, emit_root_release_set_manifest_if_ready, workspace_root,
9 },
10};
11use flate2::{Compression, GzBuilder};
12use std::{
13 fs,
14 io::{Read, Write},
15 path::{Path, PathBuf},
16 process::Command,
17};
18use toml::Value as TomlValue;
19
20const ROOT_ROLE: &str = "root";
21const WASM_STORE_ROLE: &str = "wasm_store";
22const LOCAL_ARTIFACT_ROOT_RELATIVE: &str = ".dfx/local/canisters";
23const WASM_TARGET: &str = "wasm32-unknown-unknown";
24
25#[derive(Clone, Copy, Debug, Eq, PartialEq)]
30pub enum CanisterBuildProfile {
31 Debug,
32 Fast,
33 Release,
34}
35
36impl CanisterBuildProfile {
37 #[must_use]
39 pub fn current() -> Self {
40 match std::env::var("CANIC_WASM_PROFILE").ok().as_deref() {
41 Some("debug") => Self::Debug,
42 Some("fast") => Self::Fast,
43 _ => Self::Release,
44 }
45 }
46
47 #[must_use]
49 pub const fn cargo_args(self) -> &'static [&'static str] {
50 match self {
51 Self::Debug => &[],
52 Self::Fast => &["--profile", "fast"],
53 Self::Release => &["--release"],
54 }
55 }
56
57 #[must_use]
59 pub const fn target_dir_name(self) -> &'static str {
60 match self {
61 Self::Debug => "debug",
62 Self::Fast => "fast",
63 Self::Release => "release",
64 }
65 }
66}
67
68impl From<CanisterBuildProfile> for BootstrapWasmStoreBuildProfile {
69 fn from(value: CanisterBuildProfile) -> Self {
71 match value {
72 CanisterBuildProfile::Debug => Self::Debug,
73 CanisterBuildProfile::Fast => Self::Fast,
74 CanisterBuildProfile::Release => Self::Release,
75 }
76 }
77}
78
79#[derive(Clone, Debug)]
84pub struct CanisterArtifactBuildOutput {
85 pub artifact_root: PathBuf,
86 pub wasm_path: PathBuf,
87 pub wasm_gz_path: PathBuf,
88 pub did_path: PathBuf,
89 pub manifest_path: Option<PathBuf>,
90}
91
92pub fn build_current_workspace_canister_artifact(
94 canister_name: &str,
95 profile: CanisterBuildProfile,
96) -> Result<CanisterArtifactBuildOutput, Box<dyn std::error::Error>> {
97 let workspace_root = workspace_root()?;
98 let dfx_root = dfx_root()?;
99 build_canister_artifact(&workspace_root, &dfx_root, canister_name, profile)
100}
101
102pub fn build_canister_artifact(
104 workspace_root: &Path,
105 dfx_root: &Path,
106 canister_name: &str,
107 profile: CanisterBuildProfile,
108) -> Result<CanisterArtifactBuildOutput, Box<dyn std::error::Error>> {
109 if canister_name == WASM_STORE_ROLE {
110 return build_hidden_wasm_store_artifact(workspace_root, dfx_root, profile);
111 }
112
113 let canister_manifest_path = canister_manifest_path(workspace_root, canister_name);
114 let canister_package_name = load_canister_package_name(&canister_manifest_path)?;
115 let artifact_root = dfx_root
116 .join(LOCAL_ARTIFACT_ROOT_RELATIVE)
117 .join(canister_name);
118 let wasm_path = artifact_root.join(format!("{canister_name}.wasm"));
119 let wasm_gz_path = artifact_root.join(format!("{canister_name}.wasm.gz"));
120 let did_path = artifact_root.join(format!("{canister_name}.did"));
121 let require_embedded_release_artifacts = canister_name == ROOT_ROLE;
122
123 if require_embedded_release_artifacts {
124 build_bootstrap_wasm_store_artifact(workspace_root, dfx_root, profile.into())?;
125 }
126
127 fs::create_dir_all(&artifact_root)?;
128 remove_stale_dfx_candid_sidecars(&artifact_root)?;
129
130 let release_wasm_path = run_canister_build(
131 workspace_root,
132 dfx_root,
133 &canister_manifest_path,
134 &canister_package_name,
135 profile,
136 require_embedded_release_artifacts,
137 )?;
138 write_wasm_artifact(&release_wasm_path, &wasm_path)?;
139 maybe_shrink_wasm_artifact(&wasm_path)?;
140 write_gzip_artifact(&wasm_path, &wasm_gz_path)?;
141
142 let debug_wasm_path = run_canister_build(
143 workspace_root,
144 dfx_root,
145 &canister_manifest_path,
146 &canister_package_name,
147 CanisterBuildProfile::Debug,
148 require_embedded_release_artifacts,
149 )?;
150 extract_candid(&debug_wasm_path, &did_path)?;
151
152 let network = std::env::var("DFX_NETWORK").unwrap_or_else(|_| "local".to_string());
153 let manifest_path =
154 emit_root_release_set_manifest_if_ready(workspace_root, dfx_root, &network)?;
155
156 Ok(CanisterArtifactBuildOutput {
157 artifact_root,
158 wasm_path,
159 wasm_gz_path,
160 did_path,
161 manifest_path,
162 })
163}
164
165fn load_canister_package_name(manifest_path: &Path) -> Result<String, Box<dyn std::error::Error>> {
168 let manifest_source = fs::read_to_string(manifest_path)?;
169 let manifest = toml::from_str::<TomlValue>(&manifest_source)?;
170 let package_name = manifest
171 .get("package")
172 .and_then(TomlValue::as_table)
173 .and_then(|package| package.get("name"))
174 .and_then(TomlValue::as_str)
175 .ok_or_else(|| format!("missing package.name in {}", manifest_path.display()))?;
176
177 Ok(package_name.to_string())
178}
179
180fn run_canister_build(
182 workspace_root: &Path,
183 dfx_root: &Path,
184 manifest_path: &Path,
185 package_name: &str,
186 profile: CanisterBuildProfile,
187 require_embedded_release_artifacts: bool,
188) -> Result<PathBuf, Box<dyn std::error::Error>> {
189 let target_root = std::env::var_os("CARGO_TARGET_DIR")
190 .map_or_else(|| workspace_root.join("target"), PathBuf::from);
191 let mut command = cargo_command();
192 command
193 .current_dir(workspace_root)
194 .env("CARGO_TARGET_DIR", &target_root)
195 .env("CANIC_DFX_ROOT", dfx_root)
196 .args([
197 "build",
198 "--manifest-path",
199 &manifest_path.display().to_string(),
200 "--target",
201 WASM_TARGET,
202 ])
203 .args(profile.cargo_args());
204
205 if require_embedded_release_artifacts {
206 command.env("CANIC_REQUIRE_EMBEDDED_RELEASE_ARTIFACTS", "1");
207 }
208
209 let output = command.output()?;
210 if !output.status.success() {
211 return Err(format!(
212 "cargo build failed for {}: {}",
213 manifest_path.display(),
214 String::from_utf8_lossy(&output.stderr)
215 )
216 .into());
217 }
218
219 Ok(target_root
220 .join(WASM_TARGET)
221 .join(profile.target_dir_name())
222 .join(format!("{}.wasm", package_name.replace('-', "_"))))
223}
224
225fn extract_candid(
227 debug_wasm_path: &Path,
228 did_path: &Path,
229) -> Result<(), Box<dyn std::error::Error>> {
230 let output = Command::new("candid-extractor")
231 .arg(debug_wasm_path)
232 .output()
233 .map_err(|err| {
234 format!(
235 "failed to run candid-extractor for {}: {err}",
236 debug_wasm_path.display()
237 )
238 })?;
239
240 if !output.status.success() {
241 return Err(format!(
242 "candid-extractor failed for {}: {}",
243 debug_wasm_path.display(),
244 String::from_utf8_lossy(&output.stderr)
245 )
246 .into());
247 }
248
249 write_bytes_atomically(did_path, &output.stdout)?;
250 Ok(())
251}
252
253fn remove_stale_dfx_candid_sidecars(artifact_root: &Path) -> std::io::Result<()> {
256 for relative in [
257 "constructor.did",
258 "service.did",
259 "service.did.d.ts",
260 "service.did.js",
261 ] {
262 let path = artifact_root.join(relative);
263 match fs::remove_file(path) {
264 Ok(()) => {}
265 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
266 Err(err) => return Err(err),
267 }
268 }
269
270 Ok(())
271}
272
273fn build_hidden_wasm_store_artifact(
275 workspace_root: &Path,
276 dfx_root: &Path,
277 profile: CanisterBuildProfile,
278) -> Result<CanisterArtifactBuildOutput, Box<dyn std::error::Error>> {
279 let output = build_bootstrap_wasm_store_artifact(workspace_root, dfx_root, profile.into())?;
280 Ok(map_bootstrap_output(output))
281}
282
283fn maybe_shrink_wasm_artifact(wasm_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
286 let shrunk_path = wasm_path.with_extension("wasm.shrunk");
287 match Command::new("ic-wasm")
288 .arg(wasm_path)
289 .arg("-o")
290 .arg(&shrunk_path)
291 .arg("shrink")
292 .status()
293 {
294 Ok(status) if status.success() => {
295 fs::rename(shrunk_path, wasm_path)?;
296 }
297 Ok(_) => {
298 let _ = fs::remove_file(shrunk_path);
299 }
300 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
301 Err(err) => {
302 return Err(format!("failed to run ic-wasm for {}: {err}", wasm_path.display()).into());
303 }
304 }
305
306 Ok(())
307}
308
309fn map_bootstrap_output(output: BootstrapWasmStoreBuildOutput) -> CanisterArtifactBuildOutput {
311 CanisterArtifactBuildOutput {
312 artifact_root: output.artifact_root,
313 wasm_path: output.wasm_path,
314 wasm_gz_path: output.wasm_gz_path,
315 did_path: output.did_path,
316 manifest_path: None,
317 }
318}
319
320fn write_wasm_artifact(
322 source_path: &Path,
323 target_path: &Path,
324) -> Result<(), Box<dyn std::error::Error>> {
325 let bytes = fs::read(source_path)?;
326 write_bytes_atomically(target_path, &bytes)?;
327 Ok(())
328}
329
330fn write_gzip_artifact(
332 wasm_path: &Path,
333 wasm_gz_path: &Path,
334) -> Result<(), Box<dyn std::error::Error>> {
335 let mut wasm_bytes = Vec::new();
336 fs::File::open(wasm_path)?.read_to_end(&mut wasm_bytes)?;
337
338 let mut encoder = GzBuilder::new()
339 .mtime(0)
340 .write(Vec::new(), Compression::best());
341 encoder.write_all(&wasm_bytes)?;
342 let gz_bytes = encoder.finish()?;
343 write_bytes_atomically(wasm_gz_path, &gz_bytes)?;
344 Ok(())
345}
346
347fn write_bytes_atomically(
349 target_path: &Path,
350 bytes: &[u8],
351) -> Result<(), Box<dyn std::error::Error>> {
352 let tmp_path = target_path.with_extension(format!(
353 "{}.tmp",
354 target_path
355 .extension()
356 .and_then(|extension| extension.to_str())
357 .unwrap_or_default()
358 ));
359 fs::write(&tmp_path, bytes)?;
360 fs::rename(tmp_path, target_path)?;
361 Ok(())
362}
363
364#[cfg(test)]
365mod tests {
366 use super::remove_stale_dfx_candid_sidecars;
367 use std::fs;
368
369 #[test]
370 fn remove_stale_dfx_candid_sidecars_keeps_primary_role_did() {
371 let temp_root = std::env::temp_dir().join(format!(
372 "canic-canister-build-sidecars-{}",
373 std::process::id()
374 ));
375 let _ = fs::remove_dir_all(&temp_root);
376 fs::create_dir_all(&temp_root).unwrap();
377
378 for name in [
379 "constructor.did",
380 "service.did",
381 "service.did.d.ts",
382 "service.did.js",
383 "app.did",
384 ] {
385 fs::write(temp_root.join(name), "x").unwrap();
386 }
387
388 remove_stale_dfx_candid_sidecars(&temp_root).unwrap();
389
390 assert!(!temp_root.join("constructor.did").exists());
391 assert!(!temp_root.join("service.did").exists());
392 assert!(!temp_root.join("service.did.d.ts").exists());
393 assert!(!temp_root.join("service.did.js").exists());
394 assert!(temp_root.join("app.did").exists());
395
396 let _ = fs::remove_dir_all(temp_root);
397 }
398}