1use crate::{
2 cargo_command,
3 release_set::{config_path, dfx_root, workspace_root},
4};
5use flate2::{Compression, GzBuilder};
6use serde::Deserialize;
7use std::{
8 fmt::Write as _,
9 fs,
10 io::{Read, Write},
11 path::{Path, PathBuf},
12 process::Command,
13};
14
15const WASM_STORE_ROLE: &str = "wasm_store";
16const WASM_STORE_ARTIFACTS_RELATIVE: &str = ".dfx/local/canisters/wasm_store";
17const GENERATED_WRAPPER_RELATIVE: &str = ".dfx/local/generated/canic-wasm-store";
18const CANONICAL_WASM_STORE_MANIFEST_RELATIVE: &str = "crates/canic-wasm-store/Cargo.toml";
19const CANONICAL_WASM_STORE_DID_FILE: &str = "wasm_store.did";
20const CANONICAL_WASM_STORE_CRATE_NAME: &str = "canister_wasm_store";
21const GENERATED_WRAPPER_PACKAGE_NAME: &str = "canic-generated-wasm-store";
22const CANIC_FAMILY_CRATES: &[&str] = &[
23 "canic-cdk",
24 "canic-control-plane",
25 "canic-core",
26 "canic-macros",
27 "canic-memory",
28];
29
30#[derive(Clone, Copy, Debug, Eq, PartialEq)]
35pub enum BootstrapWasmStoreBuildProfile {
36 Debug,
37 Fast,
38 Release,
39}
40
41impl BootstrapWasmStoreBuildProfile {
42 #[must_use]
43 pub fn current() -> Self {
44 match std::env::var("CANIC_WASM_PROFILE").ok().as_deref() {
45 Some("debug") => Self::Debug,
46 Some("fast") => Self::Fast,
47 _ => Self::Release,
48 }
49 }
50
51 #[must_use]
52 pub const fn cargo_args(self) -> &'static [&'static str] {
53 match self {
54 Self::Debug => &[],
55 Self::Fast => &["--profile", "fast"],
56 Self::Release => &["--release"],
57 }
58 }
59
60 #[must_use]
61 pub const fn target_dir_name(self) -> &'static str {
62 match self {
63 Self::Debug => "debug",
64 Self::Fast => "fast",
65 Self::Release => "release",
66 }
67 }
68
69 #[must_use]
70 pub const fn profile_marker(self) -> &'static str {
71 self.target_dir_name()
72 }
73}
74
75#[derive(Clone, Debug)]
80pub struct BootstrapWasmStoreBuildOutput {
81 pub artifact_root: PathBuf,
82 pub wasm_path: PathBuf,
83 pub wasm_gz_path: PathBuf,
84 pub did_path: PathBuf,
85}
86
87#[derive(Clone, Debug)]
88struct BootstrapWasmStoreSource {
89 manifest_path: PathBuf,
90 source_root: PathBuf,
91}
92
93#[derive(Clone, Debug, Deserialize)]
94struct CargoMetadata {
95 packages: Vec<CargoMetadataPackage>,
96}
97
98#[derive(Clone, Debug, Deserialize)]
99struct CargoMetadataPackage {
100 name: String,
101 version: String,
102 manifest_path: PathBuf,
103}
104
105pub fn build_bootstrap_wasm_store_artifact(
108 workspace_root: &Path,
109 dfx_root: &Path,
110 profile: BootstrapWasmStoreBuildProfile,
111) -> Result<BootstrapWasmStoreBuildOutput, Box<dyn std::error::Error>> {
112 let source = resolve_bootstrap_wasm_store_source(workspace_root, dfx_root)?;
113 let artifact_root = dfx_root.join(WASM_STORE_ARTIFACTS_RELATIVE);
114 fs::create_dir_all(&artifact_root)?;
115
116 run_wasm_store_cargo_build(
117 workspace_root,
118 &source.manifest_path,
119 &config_path(workspace_root),
120 profile,
121 )?;
122
123 let target_root = std::env::var_os("CARGO_TARGET_DIR")
124 .map_or_else(|| workspace_root.join("target"), PathBuf::from);
125 let built_wasm_path = target_root
126 .join("wasm32-unknown-unknown")
127 .join(profile.target_dir_name())
128 .join(format!("{CANONICAL_WASM_STORE_CRATE_NAME}.wasm"));
129
130 let wasm_path = artifact_root.join(format!("{WASM_STORE_ROLE}.wasm"));
131 let wasm_gz_path = artifact_root.join(format!("{WASM_STORE_ROLE}.wasm.gz"));
132 let did_path = artifact_root.join(format!("{WASM_STORE_ROLE}.did"));
133 let profile_path = artifact_root.join(".build-profile");
134
135 fs::copy(&built_wasm_path, &wasm_path)?;
136 maybe_shrink_wasm_artifact(&wasm_path)?;
137 write_gzip_artifact(&wasm_path, &wasm_gz_path)?;
138 fs::write(profile_path, profile.profile_marker())?;
139 ensure_wasm_store_did(workspace_root, &source, profile, &did_path)?;
140
141 Ok(BootstrapWasmStoreBuildOutput {
142 artifact_root,
143 wasm_path,
144 wasm_gz_path,
145 did_path,
146 })
147}
148
149pub fn build_current_workspace_bootstrap_wasm_store_artifact(
152 profile: BootstrapWasmStoreBuildProfile,
153) -> Result<BootstrapWasmStoreBuildOutput, Box<dyn std::error::Error>> {
154 let workspace_root = workspace_root()?;
155 let dfx_root = dfx_root()?;
156 build_bootstrap_wasm_store_artifact(&workspace_root, &dfx_root, profile)
157}
158
159fn resolve_bootstrap_wasm_store_source(
162 workspace_root: &Path,
163 dfx_root: &Path,
164) -> Result<BootstrapWasmStoreSource, Box<dyn std::error::Error>> {
165 let metadata = cargo_metadata(workspace_root)?;
166 let canic_manifest_path = metadata
167 .packages
168 .iter()
169 .find(|package| package.name == "canic")
170 .map(|package| package.manifest_path.clone())
171 .ok_or_else(|| {
172 "unable to locate resolved 'canic' package in cargo metadata; downstreams that build the implicit wasm_store must depend on 'canic'."
173 .to_string()
174 })?;
175
176 if let Some(source) = resolve_canonical_bootstrap_wasm_store_source(
177 workspace_root,
178 &metadata,
179 &canic_manifest_path,
180 ) {
181 return Ok(source);
182 }
183
184 let wrapper_root =
185 ensure_generated_wasm_store_wrapper(dfx_root, workspace_root, &canic_manifest_path)?;
186 Ok(BootstrapWasmStoreSource {
187 manifest_path: wrapper_root.join("Cargo.toml"),
188 source_root: wrapper_root.clone(),
189 })
190}
191
192fn resolve_canonical_bootstrap_wasm_store_source(
195 workspace_root: &Path,
196 metadata: &CargoMetadata,
197 canic_manifest_path: &Path,
198) -> Option<BootstrapWasmStoreSource> {
199 let workspace_manifest = workspace_root.join(CANONICAL_WASM_STORE_MANIFEST_RELATIVE);
200 if workspace_manifest.is_file() {
201 let source_root = workspace_manifest
202 .parent()
203 .expect("manifest path must have parent")
204 .to_path_buf();
205 return Some(BootstrapWasmStoreSource {
206 manifest_path: workspace_manifest,
207 source_root,
208 });
209 }
210
211 if let Some(package) = metadata
212 .packages
213 .iter()
214 .find(|package| package.name == "canic-wasm-store")
215 {
216 let source_root = package
217 .manifest_path
218 .parent()
219 .expect("manifest path must have parent")
220 .to_path_buf();
221 return Some(BootstrapWasmStoreSource {
222 manifest_path: package.manifest_path.clone(),
223 source_root,
224 });
225 }
226
227 let canic_root = canic_manifest_path
228 .parent()
229 .expect("canic manifest path must have parent");
230 let sibling_root = canic_root.parent().expect("canic root must have parent");
231 let canic_version = metadata
232 .packages
233 .iter()
234 .find(|package| package.name == "canic")
235 .map(|package| package.version.clone())
236 .unwrap_or_default();
237
238 let local_sibling = sibling_root.join("canic-wasm-store").join("Cargo.toml");
239 if local_sibling.is_file() {
240 let source_root = local_sibling
241 .parent()
242 .expect("manifest path must have parent")
243 .to_path_buf();
244 return Some(BootstrapWasmStoreSource {
245 manifest_path: local_sibling,
246 source_root,
247 });
248 }
249
250 if !canic_version.is_empty() {
251 let registry_sibling = sibling_root
252 .join(format!("canic-wasm-store-{canic_version}"))
253 .join("Cargo.toml");
254 if registry_sibling.is_file() {
255 let source_root = registry_sibling
256 .parent()
257 .expect("manifest path must have parent")
258 .to_path_buf();
259 return Some(BootstrapWasmStoreSource {
260 manifest_path: registry_sibling,
261 source_root,
262 });
263 }
264 }
265
266 None
267}
268
269fn cargo_metadata(workspace_root: &Path) -> Result<CargoMetadata, Box<dyn std::error::Error>> {
271 let output = cargo_command()
272 .current_dir(workspace_root)
273 .args([
274 "metadata",
275 "--format-version=1",
276 "--manifest-path",
277 &workspace_root.join("Cargo.toml").display().to_string(),
278 ])
279 .output()?;
280
281 if !output.status.success() {
282 return Err(format!(
283 "cargo metadata failed: {}",
284 String::from_utf8_lossy(&output.stderr)
285 )
286 .into());
287 }
288
289 Ok(serde_json::from_slice(&output.stdout)?)
290}
291
292fn ensure_generated_wasm_store_wrapper(
294 dfx_root: &Path,
295 workspace_root: &Path,
296 canic_manifest_path: &Path,
297) -> Result<PathBuf, Box<dyn std::error::Error>> {
298 let wrapper_root = dfx_root.join(GENERATED_WRAPPER_RELATIVE);
299 fs::create_dir_all(wrapper_root.join("src"))?;
300
301 let canic_root = canic_manifest_path
302 .parent()
303 .expect("canic manifest path must have parent");
304 let patch_table = generated_wasm_store_wrapper_patch_table(canic_manifest_path);
305 let mut cargo_toml = format!(
306 "[package]\n\
307name = \"{GENERATED_WRAPPER_PACKAGE_NAME}\"\n\
308version = \"0.0.0\"\n\
309edition = \"2024\"\n\
310publish = false\n\n\
311[workspace]\n\n\
312[lib]\n\
313name = \"{CANONICAL_WASM_STORE_CRATE_NAME}\"\n\
314crate-type = [\"cdylib\", \"rlib\"]\n\n\
315[dependencies]\n\
316canic = {{ path = \"{}\", features = [\"control-plane\"] }}\n\
317ic-cdk = \"0.20.0\"\n\
318candid = {{ version = \"0.10\", default-features = false }}\n\n\
319[build-dependencies]\n\
320canic = {{ path = \"{}\" }}\n",
321 canic_root.display(),
322 canic_root.display()
323 );
324
325 cargo_toml.push_str(
326 "\n[profile.release]\n\
327opt-level = \"z\"\n\
328lto = true\n\
329codegen-units = 1\n\
330strip = \"symbols\"\n\
331debug = false\n\
332panic = \"abort\"\n\
333overflow-checks = false\n\
334incremental = false\n\
335\n\
336[profile.fast]\n\
337inherits = \"release\"\n\
338lto = false\n\
339codegen-units = 16\n\
340incremental = true\n",
341 );
342
343 if !patch_table.is_empty() {
344 cargo_toml.push('\n');
345 cargo_toml.push_str(&patch_table);
346 }
347
348 fs::write(wrapper_root.join("Cargo.toml"), cargo_toml)?;
349 fs::write(
350 wrapper_root.join("build.rs"),
351 "fn main() {\n let config_path = std::env::var(\"CANIC_CONFIG_PATH\")\n .expect(\"CANIC_CONFIG_PATH must be set for generated wasm_store wrapper\");\n\n canic::build!(config_path);\n}\n",
352 )?;
353 fs::write(
354 wrapper_root.join("src/lib.rs"),
355 "#![allow(clippy::unused_async)]\n\ncanic::start_wasm_store!();\ncanic::cdk::export_candid_debug!();\n",
356 )?;
357
358 let workspace_lock = workspace_root.join("Cargo.lock");
359 if workspace_lock.is_file() {
360 fs::copy(workspace_lock, wrapper_root.join("Cargo.lock"))?;
361 }
362
363 Ok(wrapper_root)
364}
365
366fn generated_wasm_store_wrapper_patch_table(canic_manifest_path: &Path) -> String {
368 let canic_root = canic_manifest_path
369 .parent()
370 .expect("canic manifest path must have parent");
371 let sibling_root = canic_root.parent().expect("canic root must have parent");
372 let registry_version = registry_package_version_suffix(canic_manifest_path, "canic");
373 let mut rendered = String::new();
374
375 for crate_name in CANIC_FAMILY_CRATES {
376 let mut manifest_path = sibling_root.join(crate_name).join("Cargo.toml");
377
378 if !manifest_path.is_file() {
379 manifest_path =
380 find_versioned_sibling_manifest(sibling_root, crate_name, registry_version)
381 .unwrap_or_default();
382 }
383
384 if !manifest_path.is_file() {
385 continue;
386 }
387
388 let crate_root = manifest_path
389 .parent()
390 .expect("manifest path must have parent");
391 let _ = writeln!(
392 rendered,
393 "{crate_name} = {{ path = \"{}\" }}",
394 crate_root.display()
395 );
396 }
397
398 if rendered.is_empty() {
399 String::new()
400 } else {
401 format!("[patch.crates-io]\n{rendered}")
402 }
403}
404
405fn registry_package_version_suffix<'a>(
406 manifest_path: &'a Path,
407 crate_name: &str,
408) -> Option<&'a str> {
409 let parent_name = manifest_path.parent()?.file_name()?.to_str()?;
410 parent_name.strip_prefix(&format!("{crate_name}-"))
411}
412
413fn find_versioned_sibling_manifest(
415 sibling_root: &Path,
416 crate_name: &str,
417 version_hint: Option<&str>,
418) -> Option<PathBuf> {
419 if let Some(version) = version_hint {
420 let preferred = sibling_root
421 .join(format!("{crate_name}-{version}"))
422 .join("Cargo.toml");
423 if preferred.is_file() {
424 return Some(preferred);
425 }
426 }
427
428 let mut candidates = fs::read_dir(sibling_root).ok()?;
429 while let Some(Ok(entry)) = candidates.next() {
430 let file_name = entry.file_name();
431 let file_name = file_name.to_string_lossy();
432 if !file_name.starts_with(&format!("{crate_name}-")) {
433 continue;
434 }
435
436 let manifest_path = entry.path().join("Cargo.toml");
437 if manifest_path.is_file() {
438 return Some(manifest_path);
439 }
440 }
441
442 None
443}
444
445fn run_wasm_store_cargo_build(
447 workspace_root: &Path,
448 manifest_path: &Path,
449 config_path: &Path,
450 profile: BootstrapWasmStoreBuildProfile,
451) -> Result<(), Box<dyn std::error::Error>> {
452 let mut command = cargo_command();
453 command
454 .current_dir(workspace_root)
455 .env("CANIC_CONFIG_PATH", config_path)
456 .env(
457 "CARGO_TARGET_DIR",
458 std::env::var_os("CARGO_TARGET_DIR")
459 .map_or_else(|| workspace_root.join("target"), PathBuf::from),
460 )
461 .args([
462 "build",
463 "--manifest-path",
464 &manifest_path.display().to_string(),
465 "--target",
466 "wasm32-unknown-unknown",
467 ])
468 .args(profile.cargo_args());
469
470 let output = command.output()?;
471 if output.status.success() {
472 return Ok(());
473 }
474
475 Err(format!(
476 "cargo build failed for bootstrap wasm_store: {}",
477 String::from_utf8_lossy(&output.stderr)
478 )
479 .into())
480}
481
482fn ensure_wasm_store_did(
484 workspace_root: &Path,
485 source: &BootstrapWasmStoreSource,
486 profile: BootstrapWasmStoreBuildProfile,
487 artifact_did_path: &Path,
488) -> Result<(), Box<dyn std::error::Error>> {
489 let source_did_path = source.source_root.join(CANONICAL_WASM_STORE_DID_FILE);
490
491 if source_did_path.is_file() && !refresh_canonical_wasm_store_did_enabled() {
495 fs::copy(source_did_path, artifact_did_path)?;
496 return Ok(());
497 }
498
499 run_wasm_store_cargo_build(
500 workspace_root,
501 &source.manifest_path,
502 &config_path(workspace_root),
503 BootstrapWasmStoreBuildProfile::Debug,
504 )?;
505
506 let target_root = std::env::var_os("CARGO_TARGET_DIR")
507 .map_or_else(|| workspace_root.join("target"), PathBuf::from);
508 let debug_wasm_path = target_root
509 .join("wasm32-unknown-unknown")
510 .join(BootstrapWasmStoreBuildProfile::Debug.target_dir_name())
511 .join(format!("{CANONICAL_WASM_STORE_CRATE_NAME}.wasm"));
512 let output = Command::new("candid-extractor")
513 .arg(&debug_wasm_path)
514 .output()?;
515
516 if !output.status.success() {
517 return Err(format!(
518 "candid-extractor failed for bootstrap wasm_store: {}",
519 String::from_utf8_lossy(&output.stderr)
520 )
521 .into());
522 }
523
524 if source_did_path
525 .parent()
526 .expect("bootstrap wasm_store did path must have parent")
527 .exists()
528 {
529 fs::write(&source_did_path, &output.stdout)?;
530 }
531 fs::copy(source_did_path, artifact_did_path)?;
532 if profile == BootstrapWasmStoreBuildProfile::Debug {
533 let artifact_root = artifact_did_path
534 .parent()
535 .expect("artifact did path must have parent");
536 let wasm_path = artifact_root.join(format!("{WASM_STORE_ROLE}.wasm"));
537 let wasm_gz_path = artifact_root.join(format!("{WASM_STORE_ROLE}.wasm.gz"));
538 if wasm_path.is_file() && wasm_gz_path.is_file() {
539 fs::write(artifact_root.join(".build-profile"), "debug")?;
540 }
541 }
542
543 Ok(())
544}
545
546fn refresh_canonical_wasm_store_did_enabled() -> bool {
549 matches!(
550 std::env::var("CANIC_REFRESH_WASM_STORE_DID")
551 .ok()
552 .as_deref(),
553 Some("1" | "true" | "TRUE" | "yes" | "YES")
554 )
555}
556
557fn maybe_shrink_wasm_artifact(wasm_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
560 let shrunk_path = wasm_path.with_extension("wasm.shrunk");
561 match Command::new("ic-wasm")
562 .arg(wasm_path)
563 .arg("-o")
564 .arg(&shrunk_path)
565 .arg("shrink")
566 .status()
567 {
568 Ok(status) if status.success() => {
569 fs::rename(shrunk_path, wasm_path)?;
570 }
571 Ok(_) => {
572 let _ = fs::remove_file(shrunk_path);
573 }
574 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
575 Err(err) => return Err(err.into()),
576 }
577
578 Ok(())
579}
580
581fn write_gzip_artifact(
583 wasm_path: &Path,
584 wasm_gz_path: &Path,
585) -> Result<(), Box<dyn std::error::Error>> {
586 let mut wasm_bytes = Vec::new();
587 fs::File::open(wasm_path)?.read_to_end(&mut wasm_bytes)?;
588
589 let mut encoder = GzBuilder::new()
590 .mtime(0)
591 .write(Vec::new(), Compression::best());
592 encoder.write_all(&wasm_bytes)?;
593 let gz_bytes = encoder.finish()?;
594 fs::write(wasm_gz_path, gz_bytes)?;
595 Ok(())
596}