1use crate::icp;
2use crate::release_set::{
3 configured_fleet_name, configured_install_targets, configured_local_root_create_cycles,
4 emit_root_release_set_manifest_with_config, icp_call_on_network, icp_root,
5 load_root_release_set_manifest, resolve_artifact_root, resume_root_bootstrap,
6 stage_root_release_set, workspace_root,
7};
8use canic_core::{cdk::types::Principal, protocol};
9use config_selection::resolve_install_config_path;
10use serde::Deserialize;
11use serde_json::Value;
12use std::{
13 env,
14 path::{Path, PathBuf},
15 process::Command,
16 thread,
17 time::{Duration, Instant, SystemTime, UNIX_EPOCH},
18};
19
20mod config_selection;
21mod state;
22
23pub use config_selection::discover_canic_config_choices;
24use state::{INSTALL_STATE_SCHEMA_VERSION, validate_fleet_name, write_install_state};
25pub use state::{InstallState, read_named_fleet_install_state};
26
27#[cfg(test)]
28mod tests;
29
30#[cfg(test)]
31use config_selection::config_selection_error;
32#[cfg(test)]
33use state::{fleet_install_state_path, read_fleet_install_state};
34
35#[derive(Clone, Debug)]
40pub struct InstallRootOptions {
41 pub root_canister: String,
42 pub root_build_target: String,
43 pub network: String,
44 pub ready_timeout_seconds: u64,
45 pub config_path: Option<String>,
46 pub interactive_config_selection: bool,
47}
48
49#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
54struct BootstrapStatusSnapshot {
55 ready: bool,
56 phase: String,
57 last_error: Option<String>,
58}
59
60#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
65struct InstallTimingSummary {
66 create_canisters: Duration,
67 build_all: Duration,
68 emit_manifest: Duration,
69 install_root: Duration,
70 stage_release_set: Duration,
71 resume_bootstrap: Duration,
72 wait_ready: Duration,
73}
74
75const LOCAL_ICP_READY_TIMEOUT_SECONDS: u64 = 30;
76
77pub fn discover_current_canic_config_choices() -> Result<Vec<PathBuf>, Box<dyn std::error::Error>> {
79 let workspace_root = workspace_root()?;
80 config_selection::discover_workspace_canic_config_choices(&workspace_root)
81}
82
83pub fn install_root(options: InstallRootOptions) -> Result<(), Box<dyn std::error::Error>> {
85 let workspace_root = workspace_root()?;
86 let icp_root = icp_root()?;
87 let config_path = resolve_install_config_path(
88 &workspace_root,
89 options.config_path.as_deref(),
90 options.interactive_config_selection,
91 )?;
92 let fleet_name = configured_fleet_name(&config_path)?;
93 validate_fleet_name(&fleet_name)?;
94 let total_started_at = Instant::now();
95 let mut timings = InstallTimingSummary::default();
96
97 println!(
98 "Installing fleet {} against ICP_ENVIRONMENT={}",
99 fleet_name, options.network
100 );
101 ensure_icp_environment_ready(&icp_root, &options.network)?;
102 let create_started_at = Instant::now();
103 if Principal::from_text(&options.root_canister).is_err() {
104 let mut create = icp_canister_command_in_network(&icp_root);
105 create.args(["create", &options.root_canister, "-q"]);
106 add_local_root_create_cycles_arg(&mut create, &config_path, &options.network)?;
107 add_icp_environment_target(&mut create, &options.network);
108 run_command(&mut create)?;
109 }
110 timings.create_canisters = create_started_at.elapsed();
111
112 let build_targets = configured_install_targets(&config_path, &options.root_build_target)?;
113 let build_session_id = install_build_session_id();
114 let build_started_at = Instant::now();
115 run_canic_build_targets(
116 &icp_root,
117 &options.network,
118 &build_targets,
119 &build_session_id,
120 &config_path,
121 )?;
122 timings.build_all = build_started_at.elapsed();
123
124 let emit_manifest_started_at = Instant::now();
125 let manifest_path = emit_root_release_set_manifest_with_config(
126 &workspace_root,
127 &icp_root,
128 &options.network,
129 &config_path,
130 )?;
131 timings.emit_manifest = emit_manifest_started_at.elapsed();
132
133 let root_wasm = resolve_artifact_root(&icp_root, &options.network)?
134 .join(&options.root_build_target)
135 .join(format!("{}.wasm", options.root_build_target));
136 let mut install = icp_canister_command_in_network(&icp_root);
137 install.args([
138 "install",
139 &options.root_canister,
140 "--mode=reinstall",
141 "-y",
142 "--wasm",
143 ]);
144 install.arg(&root_wasm);
145 install.args(["--args", "(variant { Prime })"]);
146 add_icp_environment_target(&mut install, &options.network);
147 let install_started_at = Instant::now();
148 run_command(&mut install)?;
149 timings.install_root = install_started_at.elapsed();
150
151 let manifest = load_root_release_set_manifest(&manifest_path)?;
152 let stage_started_at = Instant::now();
153 stage_root_release_set(
154 &icp_root,
155 &options.network,
156 &options.root_canister,
157 &manifest,
158 )?;
159 timings.stage_release_set = stage_started_at.elapsed();
160 let resume_started_at = Instant::now();
161 resume_root_bootstrap(&options.network, &options.root_canister)?;
162 timings.resume_bootstrap = resume_started_at.elapsed();
163 let ready_started_at = Instant::now();
164 let ready_result = wait_for_root_ready(
165 &options.network,
166 &options.root_canister,
167 options.ready_timeout_seconds,
168 );
169 timings.wait_ready = ready_started_at.elapsed();
170 if let Err(err) = ready_result {
171 print_install_timing_summary(&timings, total_started_at.elapsed());
172 return Err(err);
173 }
174
175 print_install_timing_summary(&timings, total_started_at.elapsed());
176 let state = build_install_state(
177 &options,
178 &workspace_root,
179 &icp_root,
180 &config_path,
181 &manifest_path,
182 &fleet_name,
183 )?;
184 let state_path = write_install_state(&icp_root, &options.network, &state)?;
185 print_install_result_summary(&options.network, &state.fleet, &state_path);
186 Ok(())
187}
188
189fn build_install_state(
191 options: &InstallRootOptions,
192 workspace_root: &Path,
193 icp_root: &Path,
194 config_path: &Path,
195 release_set_manifest_path: &Path,
196 fleet_name: &str,
197) -> Result<InstallState, Box<dyn std::error::Error>> {
198 Ok(InstallState {
199 schema_version: INSTALL_STATE_SCHEMA_VERSION,
200 fleet: fleet_name.to_string(),
201 installed_at_unix_secs: current_unix_secs()?,
202 network: options.network.clone(),
203 root_target: options.root_canister.clone(),
204 root_canister_id: resolve_root_canister_id(
205 icp_root,
206 &options.network,
207 &options.root_canister,
208 )?,
209 root_build_target: options.root_build_target.clone(),
210 workspace_root: workspace_root.display().to_string(),
211 icp_root: icp_root.display().to_string(),
212 config_path: config_path.display().to_string(),
213 release_set_manifest_path: release_set_manifest_path.display().to_string(),
214 })
215}
216
217fn resolve_root_canister_id(
219 icp_root: &Path,
220 network: &str,
221 root_canister: &str,
222) -> Result<String, Box<dyn std::error::Error>> {
223 if Principal::from_text(root_canister).is_ok() {
224 return Ok(root_canister.to_string());
225 }
226
227 let mut command = icp_canister_command_in_network(icp_root);
228 command.args(["status", root_canister, "-i"]);
229 add_icp_environment_target(&mut command, network);
230 Ok(run_command_stdout(&mut command)?.trim().to_string())
231}
232
233fn current_unix_secs() -> Result<u64, Box<dyn std::error::Error>> {
235 Ok(SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs())
236}
237
238fn run_canic_build_targets(
240 icp_root: &Path,
241 network: &str,
242 targets: &[String],
243 build_session_id: &str,
244 config_path: &Path,
245) -> Result<(), Box<dyn std::error::Error>> {
246 println!("Build artifacts:");
247 println!("{:<16} {:<18} {:>10}", "CANISTER", "PROGRESS", "ELAPSED");
248
249 for (index, target) in targets.iter().enumerate() {
250 let mut command = canic_build_target_command(icp_root, network, target, build_session_id);
251 command.env("CANIC_CONFIG_PATH", config_path);
252 let started_at = Instant::now();
253 let output = command.output()?;
254 let elapsed = started_at.elapsed();
255
256 if !output.status.success() {
257 return Err(format!(
258 "canic build failed for {target}: {}\nstdout:\n{}\nstderr:\n{}",
259 output.status,
260 String::from_utf8_lossy(&output.stdout).trim(),
261 String::from_utf8_lossy(&output.stderr).trim()
262 )
263 .into());
264 }
265
266 println!(
267 "{:<16} {:<18} {:>9.2}s",
268 target,
269 progress_bar(index + 1, targets.len(), 10),
270 elapsed.as_secs_f64()
271 );
272 }
273
274 println!();
275 Ok(())
276}
277
278fn canic_build_target_command(
281 _icp_root: &Path,
282 network: &str,
283 target: &str,
284 build_session_id: &str,
285) -> Command {
286 let mut command = canic_command();
287 command
288 .env("CANIC_BUILD_CONTEXT_SESSION", build_session_id)
289 .env("ICP_ENVIRONMENT", network)
290 .args(["build", target]);
291 command
292}
293
294fn canic_command() -> Command {
297 std::env::current_exe().map_or_else(|_| Command::new("canic"), Command::new)
298}
299
300fn install_build_session_id() -> String {
301 let unique = SystemTime::now()
302 .duration_since(UNIX_EPOCH)
303 .map_or(0, |duration| duration.as_nanos());
304 format!("install-root-{}-{unique}", std::process::id())
305}
306
307fn add_local_root_create_cycles_arg(
308 command: &mut Command,
309 config_path: &Path,
310 network: &str,
311) -> Result<(), Box<dyn std::error::Error>> {
312 if network != "local" {
313 return Ok(());
314 }
315
316 let cycles = configured_local_root_create_cycles(config_path)?;
317 command.args(["--cycles", &cycles.to_string()]);
318 Ok(())
319}
320
321fn progress_bar(current: usize, total: usize, width: usize) -> String {
322 if total == 0 || width == 0 {
323 return "[] 0/0".to_string();
324 }
325
326 let filled = current.saturating_mul(width).div_ceil(total);
327 let filled = filled.min(width);
328 format!(
329 "[{}{}] {current}/{total}",
330 "#".repeat(filled),
331 " ".repeat(width - filled)
332 )
333}
334
335fn ensure_icp_environment_ready(
337 icp_root: &Path,
338 network: &str,
339) -> Result<(), Box<dyn std::error::Error>> {
340 if icp_ping(network)? {
341 return Ok(());
342 }
343
344 if network == "local" && local_icp_autostart_enabled() {
345 println!("Local icp environment is not reachable; starting a clean local replica");
346 let mut stop = icp_stop_command(icp_root);
347 let _ = run_command_allow_failure(&mut stop)?;
348
349 let mut start = icp_start_local_command(icp_root);
350 run_command(&mut start)?;
351 wait_for_icp_ping(
352 network,
353 Duration::from_secs(LOCAL_ICP_READY_TIMEOUT_SECONDS),
354 )?;
355 return Ok(());
356 }
357
358 Err(format!(
359 "icp environment is not running for network '{network}'\nStart the target replica externally and rerun."
360 )
361 .into())
362}
363
364fn icp_ping(network: &str) -> Result<bool, Box<dyn std::error::Error>> {
366 Ok(icp::default_command()
367 .args(["network", "ping", network])
368 .output()?
369 .status
370 .success())
371}
372
373fn local_icp_autostart_enabled() -> bool {
375 parse_local_icp_autostart(env::var("CANIC_AUTO_START_LOCAL_ICP").ok().as_deref())
376}
377
378fn parse_local_icp_autostart(value: Option<&str>) -> bool {
379 !matches!(
380 value.map(str::trim).map(str::to_ascii_lowercase).as_deref(),
381 Some("0" | "false" | "no" | "off")
382 )
383}
384
385fn icp_stop_command(icp_root: &Path) -> Command {
387 let mut command = icp_command_in_network(icp_root, "local");
388 command.args(["network", "stop", "local"]);
389 command
390}
391
392fn icp_start_local_command(icp_root: &Path) -> Command {
394 let mut command = icp_command_in_network(icp_root, "local");
395 command.args(["network", "start", "local", "--background"]);
396 command
397}
398
399fn wait_for_icp_ping(network: &str, timeout: Duration) -> Result<(), Box<dyn std::error::Error>> {
401 let start = Instant::now();
402 while start.elapsed() < timeout {
403 if icp_ping(network)? {
404 return Ok(());
405 }
406 thread::sleep(Duration::from_millis(500));
407 }
408
409 Err(format!(
410 "icp environment did not become ready for network '{network}' within {}s",
411 timeout.as_secs()
412 )
413 .into())
414}
415
416fn wait_for_root_ready(
418 network: &str,
419 root_canister: &str,
420 timeout_seconds: u64,
421) -> Result<(), Box<dyn std::error::Error>> {
422 let start = std::time::Instant::now();
423 let mut next_report = 0_u64;
424
425 println!("Waiting for {root_canister} to report canic_ready (timeout {timeout_seconds}s)");
426
427 loop {
428 if root_ready(network, root_canister)? {
429 println!(
430 "{root_canister} reported canic_ready after {}s",
431 start.elapsed().as_secs()
432 );
433 return Ok(());
434 }
435
436 if let Some(status) = root_bootstrap_status(network, root_canister)?
437 && let Some(last_error) = status.last_error.as_deref()
438 {
439 eprintln!(
440 "root bootstrap reported failure during phase '{}' : {}",
441 status.phase, last_error
442 );
443 eprintln!(
444 "Diagnostic: icp canister -n {network} call {root_canister} canic_bootstrap_status"
445 );
446 print_raw_call(network, root_canister, protocol::CANIC_BOOTSTRAP_STATUS);
447 eprintln!(
448 "Diagnostic: icp canister -n {network} call {root_canister} canic_subnet_registry"
449 );
450 print_raw_call(network, root_canister, "canic_subnet_registry");
451 eprintln!(
452 "Diagnostic: icp canister -n {network} call {root_canister} canic_wasm_store_bootstrap_debug"
453 );
454 print_raw_call(network, root_canister, "canic_wasm_store_bootstrap_debug");
455 eprintln!(
456 "Diagnostic: icp canister -n {network} call {root_canister} canic_wasm_store_overview"
457 );
458 print_raw_call(network, root_canister, "canic_wasm_store_overview");
459 eprintln!("Diagnostic: icp canister -n {network} call {root_canister} canic_log");
460 print_recent_root_logs(network, root_canister);
461 return Err(format!(
462 "root bootstrap failed during phase '{}' : {}",
463 status.phase, last_error
464 )
465 .into());
466 }
467
468 let elapsed = start.elapsed().as_secs();
469 if elapsed >= timeout_seconds {
470 eprintln!("root did not report canic_ready within {timeout_seconds}s");
471 eprintln!(
472 "Diagnostic: icp canister -n {network} call {root_canister} canic_bootstrap_status"
473 );
474 print_raw_call(network, root_canister, protocol::CANIC_BOOTSTRAP_STATUS);
475 eprintln!(
476 "Diagnostic: icp canister -n {network} call {root_canister} canic_subnet_registry"
477 );
478 print_raw_call(network, root_canister, "canic_subnet_registry");
479 eprintln!(
480 "Diagnostic: icp canister -n {network} call {root_canister} canic_wasm_store_bootstrap_debug"
481 );
482 print_raw_call(network, root_canister, "canic_wasm_store_bootstrap_debug");
483 eprintln!(
484 "Diagnostic: icp canister -n {network} call {root_canister} canic_wasm_store_overview"
485 );
486 print_raw_call(network, root_canister, "canic_wasm_store_overview");
487 eprintln!("Diagnostic: icp canister -n {network} call {root_canister} canic_log");
488 print_recent_root_logs(network, root_canister);
489 return Err("root did not become ready".into());
490 }
491
492 if elapsed >= next_report {
493 println!("Still waiting for {root_canister} canic_ready ({elapsed}s elapsed)");
494 if let Some(status) = root_bootstrap_status(network, root_canister)? {
495 match status.last_error.as_deref() {
496 Some(last_error) => println!(
497 "Current bootstrap status: phase={} ready={} error={}",
498 status.phase, status.ready, last_error
499 ),
500 None => println!(
501 "Current bootstrap status: phase={} ready={}",
502 status.phase, status.ready
503 ),
504 }
505 }
506 if let Ok(registry_json) = icp_call_on_network(
507 network,
508 root_canister,
509 "canic_subnet_registry",
510 None,
511 Some("json"),
512 ) {
513 println!("Current subnet registry roles:");
514 println!(" {}", registry_roles(®istry_json));
515 }
516 next_report = elapsed + 5;
517 }
518
519 thread::sleep(Duration::from_secs(1));
520 }
521}
522
523fn root_ready(network: &str, root_canister: &str) -> Result<bool, Box<dyn std::error::Error>> {
525 let output = icp_call_on_network(network, root_canister, "canic_ready", None, Some("json"))?;
526 let data = serde_json::from_str::<Value>(&output)?;
527 Ok(parse_root_ready_value(&data))
528}
529
530fn root_bootstrap_status(
532 network: &str,
533 root_canister: &str,
534) -> Result<Option<BootstrapStatusSnapshot>, Box<dyn std::error::Error>> {
535 let output = match icp_call_on_network(
536 network,
537 root_canister,
538 protocol::CANIC_BOOTSTRAP_STATUS,
539 None,
540 Some("json"),
541 ) {
542 Ok(output) => output,
543 Err(err) => {
544 let message = err.to_string();
545 if message.contains("has no query method")
546 || message.contains("method not found")
547 || message.contains("Canister has no query method")
548 {
549 return Ok(None);
550 }
551 return Err(err);
552 }
553 };
554 let data = serde_json::from_str::<Value>(&output)?;
555 Ok(parse_bootstrap_status_value(&data))
556}
557
558fn parse_root_ready_value(data: &Value) -> bool {
560 matches!(data, Value::Bool(true))
561 || matches!(data.get("Ok"), Some(Value::Bool(true)))
562 || data
563 .get("response_candid")
564 .and_then(Value::as_str)
565 .is_some_and(|value| value.trim() == "(true)")
566}
567
568fn parse_bootstrap_status_value(data: &Value) -> Option<BootstrapStatusSnapshot> {
569 serde_json::from_value::<BootstrapStatusSnapshot>(data.clone())
570 .ok()
571 .or_else(|| {
572 data.get("Ok")
573 .cloned()
574 .and_then(|ok| serde_json::from_value::<BootstrapStatusSnapshot>(ok).ok())
575 })
576 .or_else(|| {
577 data.get("response_candid")
578 .and_then(Value::as_str)
579 .and_then(parse_bootstrap_status_candid)
580 })
581}
582
583fn parse_bootstrap_status_candid(candid: &str) -> Option<BootstrapStatusSnapshot> {
584 let ready = if candid.contains("3_870_990_435 = true") || candid.contains("ready = true") {
585 true
586 } else if candid.contains("3_870_990_435 = false") || candid.contains("ready = false") {
587 false
588 } else {
589 return None;
590 };
591
592 let phase = extract_candid_text_field(candid, "3_253_282_875")
593 .or_else(|| extract_candid_text_field(candid, "phase"))
594 .unwrap_or_else(|| {
595 if ready {
596 "ready".to_string()
597 } else {
598 "unknown".to_string()
599 }
600 });
601 let last_error = extract_candid_text_field(candid, "89_620_959")
602 .or_else(|| extract_candid_text_field(candid, "last_error"));
603
604 Some(BootstrapStatusSnapshot {
605 ready,
606 phase,
607 last_error,
608 })
609}
610
611fn extract_candid_text_field(candid: &str, label: &str) -> Option<String> {
612 let (_, tail) = candid.split_once(&format!("{label} = "))?;
613 let tail = tail.trim_start();
614 let quoted = tail
615 .strip_prefix("opt \"")
616 .or_else(|| tail.strip_prefix('"'))?;
617 let mut value = String::new();
618 let mut escaped = false;
619 for ch in quoted.chars() {
620 if escaped {
621 value.push(ch);
622 escaped = false;
623 continue;
624 }
625 if ch == '\\' {
626 escaped = true;
627 continue;
628 }
629 if ch == '"' {
630 return Some(value);
631 }
632 value.push(ch);
633 }
634 None
635}
636
637fn print_install_timing_summary(timings: &InstallTimingSummary, total: Duration) {
638 println!("Install timing summary:");
639 println!("{:<20} {:>10}", "phase", "elapsed");
640 println!("{:<20} {:>10}", "--------------------", "----------");
641 print_timing_row("create_canisters", timings.create_canisters);
642 print_timing_row("build_all", timings.build_all);
643 print_timing_row("emit_manifest", timings.emit_manifest);
644 print_timing_row("install_root", timings.install_root);
645 print_timing_row("stage_release_set", timings.stage_release_set);
646 print_timing_row("resume_bootstrap", timings.resume_bootstrap);
647 print_timing_row("wait_ready", timings.wait_ready);
648 print_timing_row("total", total);
649}
650
651fn print_timing_row(label: &str, duration: Duration) {
652 println!("{label:<20} {:>9.2}s", duration.as_secs_f64());
653}
654
655fn print_install_result_summary(network: &str, fleet: &str, state_path: &Path) {
657 println!("Install result:");
658 println!("{:<14} success", "status");
659 println!("{:<14} {}", "fleet", fleet);
660 println!("{:<14} {}", "install_state", state_path.display());
661 println!(
662 "{:<14} canic list {} --network {}",
663 "smoke_check", fleet, network
664 );
665}
666
667fn print_recent_root_logs(network: &str, root_canister: &str) {
669 let page_args = r"(null, null, null, record { limit = 8; offset = 0 })";
670 let Ok(logs_json) = icp_call_on_network(
671 network,
672 root_canister,
673 "canic_log",
674 Some(page_args),
675 Some("json"),
676 ) else {
677 return;
678 };
679 let Ok(data) = serde_json::from_str::<Value>(&logs_json) else {
680 return;
681 };
682 let entries = data
683 .get("Ok")
684 .and_then(|ok| ok.get("entries"))
685 .and_then(Value::as_array)
686 .cloned()
687 .unwrap_or_default();
688
689 if entries.is_empty() {
690 println!(" <no runtime log entries>");
691 return;
692 }
693
694 for entry in entries.iter().rev() {
695 let level = entry.get("level").and_then(Value::as_str).unwrap_or("Info");
696 let topic = entry.get("topic").and_then(Value::as_str).unwrap_or("");
697 let message = entry
698 .get("message")
699 .and_then(Value::as_str)
700 .unwrap_or("")
701 .replace('\n', "\\n");
702 let topic_prefix = if topic.is_empty() {
703 String::new()
704 } else {
705 format!("[{topic}] ")
706 };
707 println!(" {level} {topic_prefix}{message}");
708 }
709}
710
711fn registry_roles(registry_json: &str) -> String {
713 serde_json::from_str::<Value>(registry_json)
714 .ok()
715 .and_then(|data| {
716 data.get("Ok").and_then(Value::as_array).map(|entries| {
717 entries
718 .iter()
719 .filter_map(|entry| {
720 entry
721 .get("role")
722 .and_then(Value::as_str)
723 .map(str::to_string)
724 })
725 .collect::<Vec<_>>()
726 })
727 })
728 .map_or_else(
729 || "<unavailable>".to_string(),
730 |roles| {
731 if roles.is_empty() {
732 "<empty>".to_string()
733 } else {
734 roles.join(", ")
735 }
736 },
737 )
738}
739
740fn run_command(command: &mut Command) -> Result<(), Box<dyn std::error::Error>> {
742 icp::run_status(command).map_err(Into::into)
743}
744
745fn run_command_stdout(command: &mut Command) -> Result<String, Box<dyn std::error::Error>> {
747 icp::run_output(command).map_err(Into::into)
748}
749
750fn run_command_allow_failure(
752 command: &mut Command,
753) -> Result<std::process::ExitStatus, Box<dyn std::error::Error>> {
754 Ok(command.status()?)
755}
756
757fn print_raw_call(network: &str, root_canister: &str, method: &str) {
759 let mut command = icp_root().map_or_else(
760 |_| icp_command_on_network(network),
761 |root| icp_command_in_network(&root, network),
762 );
763 let _ = command
764 .arg("canister")
765 .args(["call", root_canister, method, "()", "-e", network])
766 .status();
767}
768
769fn icp_command_on_network(network: &str) -> Command {
772 let mut command = icp::default_command();
773 command.env("ICP_ENVIRONMENT", network);
774 command
775}
776
777fn icp_command_in_network(icp_root: &Path, network: &str) -> Command {
779 let mut command = icp::default_command_in(icp_root);
780 command.env("ICP_ENVIRONMENT", network);
781 command
782}
783
784fn icp_canister_command_in_network(icp_root: &Path) -> Command {
786 let mut command = icp::default_command_in(icp_root);
787 command.arg("canister");
788 command
789}
790
791fn add_icp_environment_target(command: &mut Command, network: &str) {
792 icp::add_target_args(command, Some(network), None);
793}