1use std::{
2 env,
3 error::Error,
4 fmt,
5 io::{self, Read, Write},
6 path::{Path, PathBuf},
7 process::{Command, Stdio},
8 thread,
9};
10
11use serde::{Deserialize, Serialize};
12
13const LOCAL_NETWORK: &str = "local";
14pub const REQUIRED_ICP_CLI_VERSION: &str = "0.3.2";
15pub const ICP_CLI_SUPPORTED_VERSION_RANGE: &str = ">=0.3.2, <0.4.0";
16pub(crate) const CANIC_ICP_LOCAL_NETWORK_URL_ENV: &str = "CANIC_ICP_LOCAL_NETWORK_URL";
17pub(crate) const CANIC_ICP_LOCAL_ROOT_KEY_ENV: &str = "CANIC_ICP_LOCAL_ROOT_KEY";
18
19#[derive(Clone, Debug, Eq, PartialEq)]
24pub struct IcpRawOutput {
25 pub success: bool,
26 pub status: String,
27 pub stdout: Vec<u8>,
28 pub stderr: Vec<u8>,
29}
30
31#[derive(Debug)]
36pub enum IcpCommandError {
37 Io(std::io::Error),
38 MissingCli {
39 executable: String,
40 },
41 IncompatibleCliVersion {
42 executable: String,
43 found: String,
44 },
45 Failed {
46 command: String,
47 stderr: String,
48 },
49 Json {
50 command: String,
51 output: String,
52 source: serde_json::Error,
53 },
54 SnapshotIdUnavailable {
55 output: String,
56 },
57}
58
59impl fmt::Display for IcpCommandError {
60 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
62 match self {
63 Self::Io(err) => write!(formatter, "{err}"),
64 Self::MissingCli { executable } => {
65 write!(
66 formatter,
67 "icp-cli executable not found: {executable}\nrequired: icp-cli {ICP_CLI_SUPPORTED_VERSION_RANGE}\nnext: install icp-cli {REQUIRED_ICP_CLI_VERSION} from https://github.com/dfinity/icp-cli/releases/tag/v{REQUIRED_ICP_CLI_VERSION}, or pass top-level --icp <path>"
68 )
69 }
70 Self::IncompatibleCliVersion { executable, found } => {
71 write!(
72 formatter,
73 "unsupported icp-cli version for {executable}\nfound: {found}\nrequired: icp-cli {ICP_CLI_SUPPORTED_VERSION_RANGE}\nnext: install icp-cli {REQUIRED_ICP_CLI_VERSION} from https://github.com/dfinity/icp-cli/releases/tag/v{REQUIRED_ICP_CLI_VERSION}, or pass top-level --icp <path>"
74 )
75 }
76 Self::Failed { command, stderr } => {
77 write!(formatter, "icp command failed: {command}\n{stderr}")
78 }
79 Self::Json {
80 command,
81 output,
82 source,
83 } => {
84 write!(
85 formatter,
86 "could not parse icp json output for {command}: {source}\n{output}"
87 )
88 }
89 Self::SnapshotIdUnavailable { output } => {
90 write!(
91 formatter,
92 "could not parse snapshot id from icp output: {output}"
93 )
94 }
95 }
96 }
97}
98
99impl Error for IcpCommandError {
100 fn source(&self) -> Option<&(dyn Error + 'static)> {
102 match self {
103 Self::Io(err) => Some(err),
104 Self::Json { source, .. } => Some(source),
105 Self::Failed { .. }
106 | Self::IncompatibleCliVersion { .. }
107 | Self::MissingCli { .. }
108 | Self::SnapshotIdUnavailable { .. } => None,
109 }
110 }
111}
112
113impl From<std::io::Error> for IcpCommandError {
114 fn from(err: std::io::Error) -> Self {
116 Self::Io(err)
117 }
118}
119
120#[derive(Clone, Debug, Eq, PartialEq)]
125pub struct IcpCli {
126 executable: String,
127 environment: Option<String>,
128 network: Option<String>,
129 cwd: Option<PathBuf>,
130}
131
132#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
136pub struct IcpCliVersion {
137 pub major: u64,
138 pub minor: u64,
139 pub patch: u64,
140}
141
142#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
147pub struct IcpSnapshotCreateReceipt {
148 pub snapshot_id: String,
149 pub taken_at_timestamp: Option<u64>,
150 pub total_size_bytes: Option<u64>,
151}
152
153#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
158pub struct IcpSnapshotUploadReceipt {
159 pub snapshot_id: String,
160}
161
162#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
167pub struct IcpCanisterStatusReport {
168 pub id: String,
169 pub name: Option<String>,
170 pub status: String,
171 pub settings: Option<IcpCanisterStatusSettings>,
172 pub module_hash: Option<String>,
173 pub memory_size: Option<String>,
174 pub cycles: Option<String>,
175 pub reserved_cycles: Option<String>,
176 pub idle_cycles_burned_per_day: Option<String>,
177}
178
179#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
184pub struct IcpCanisterStatusSettings {
185 #[serde(default)]
186 pub controllers: Vec<String>,
187 pub compute_allocation: Option<String>,
188 pub memory_allocation: Option<String>,
189 pub freezing_threshold: Option<String>,
190 pub reserved_cycles_limit: Option<String>,
191 pub wasm_memory_limit: Option<String>,
192 pub wasm_memory_threshold: Option<String>,
193 pub log_memory_limit: Option<String>,
194}
195
196impl IcpCli {
197 #[must_use]
199 pub fn new(
200 executable: impl Into<String>,
201 environment: Option<String>,
202 network: Option<String>,
203 ) -> Self {
204 Self {
205 executable: executable.into(),
206 environment,
207 network,
208 cwd: None,
209 }
210 }
211
212 #[must_use]
214 pub fn with_cwd(mut self, cwd: impl Into<PathBuf>) -> Self {
215 self.cwd = Some(cwd.into());
216 self
217 }
218
219 #[must_use]
221 pub fn environment(&self) -> Option<&str> {
222 self.environment.as_deref()
223 }
224
225 #[must_use]
227 pub fn network(&self) -> Option<&str> {
228 self.network.as_deref()
229 }
230
231 #[must_use]
233 pub fn command(&self) -> Command {
234 let mut command = Command::new(&self.executable);
235 if let Some(cwd) = &self.cwd {
236 command.current_dir(cwd);
237 add_project_root_override_arg(&mut command, cwd);
238 }
239 command
240 }
241
242 #[must_use]
244 pub fn command_in(&self, cwd: &Path) -> Command {
245 let mut command = Command::new(&self.executable);
246 command.current_dir(cwd);
247 add_project_root_override_arg(&mut command, cwd);
248 command
249 }
250
251 #[must_use]
253 pub fn canister_command(&self) -> Command {
254 let mut command = self.command();
255 command.arg("canister");
256 command
257 }
258
259 pub fn version(&self) -> Result<String, IcpCommandError> {
261 let mut command = self.command();
262 command.arg("--version");
263 run_output_unchecked(&mut command)
264 }
265
266 pub fn compatible_version(&self) -> Result<String, IcpCommandError> {
268 compatible_version_output(&self.executable, self.cwd.as_deref())
269 }
270
271 pub fn ensure_compatible(&self) -> Result<(), IcpCommandError> {
273 self.compatible_version().map(|_| ())
274 }
275
276 pub fn local_replica_start(
278 &self,
279 background: bool,
280 debug: bool,
281 ) -> Result<String, IcpCommandError> {
282 let mut command = self.local_replica_command("start");
283 run_local_replica_start_command(&mut command, background, debug)
284 }
285
286 pub fn local_replica_start_in(
288 &self,
289 cwd: &Path,
290 background: bool,
291 debug: bool,
292 ) -> Result<String, IcpCommandError> {
293 let mut command = self.local_replica_command_in("start", cwd);
294 run_local_replica_start_command(&mut command, background, debug)
295 }
296
297 pub fn local_replica_status(&self, debug: bool) -> Result<String, IcpCommandError> {
299 let mut command = self.local_replica_command("status");
300 add_debug_arg(&mut command, debug);
301 run_output_with_stderr(&mut command)
302 }
303
304 pub fn local_replica_status_in(
306 &self,
307 cwd: &Path,
308 debug: bool,
309 ) -> Result<String, IcpCommandError> {
310 let mut command = self.local_replica_command_in("status", cwd);
311 add_debug_arg(&mut command, debug);
312 run_output_with_stderr(&mut command)
313 }
314
315 pub fn local_replica_status_json(
317 &self,
318 debug: bool,
319 ) -> Result<serde_json::Value, IcpCommandError> {
320 let mut command = self.local_replica_command("status");
321 add_debug_arg(&mut command, debug);
322 command.arg("--json");
323 run_json(&mut command)
324 }
325
326 pub fn local_replica_status_json_in(
328 &self,
329 cwd: &Path,
330 debug: bool,
331 ) -> Result<serde_json::Value, IcpCommandError> {
332 let mut command = self.local_replica_command_in("status", cwd);
333 add_debug_arg(&mut command, debug);
334 command.arg("--json");
335 run_json(&mut command)
336 }
337
338 pub fn local_replica_project_running(&self, debug: bool) -> Result<bool, IcpCommandError> {
340 let mut command = self.local_replica_command("status");
341 add_debug_arg(&mut command, debug);
342 run_success(&mut command)
343 }
344
345 pub fn local_replica_project_running_in(
347 &self,
348 cwd: &Path,
349 debug: bool,
350 ) -> Result<bool, IcpCommandError> {
351 let mut command = self.local_replica_command_in("status", cwd);
352 add_debug_arg(&mut command, debug);
353 run_success(&mut command)
354 }
355
356 pub fn local_replica_ping(&self, debug: bool) -> Result<bool, IcpCommandError> {
358 let mut command = self.local_replica_command("ping");
359 add_debug_arg(&mut command, debug);
360 run_success(&mut command)
361 }
362
363 pub fn local_replica_stop(&self, debug: bool) -> Result<String, IcpCommandError> {
365 let mut command = self.local_replica_command("stop");
366 add_debug_arg(&mut command, debug);
367 run_output_with_stderr(&mut command)
368 }
369
370 pub fn local_replica_stop_in(
372 &self,
373 cwd: &Path,
374 debug: bool,
375 ) -> Result<String, IcpCommandError> {
376 let mut command = self.local_replica_command_in("stop", cwd);
377 add_debug_arg(&mut command, debug);
378 run_output_with_stderr(&mut command)
379 }
380
381 #[must_use]
383 pub fn local_replica_start_display(&self, background: bool, debug: bool) -> String {
384 let mut command = self.local_replica_command("start");
385 add_debug_arg(&mut command, debug);
386 if background {
387 command.arg("--background");
388 }
389 command_display(&command)
390 }
391
392 #[must_use]
394 pub fn local_replica_status_display(&self, debug: bool) -> String {
395 let mut command = self.local_replica_command("status");
396 add_debug_arg(&mut command, debug);
397 command_display(&command)
398 }
399
400 #[must_use]
402 pub fn local_replica_stop_display(&self, debug: bool) -> String {
403 let mut command = self.local_replica_command("stop");
404 add_debug_arg(&mut command, debug);
405 command_display(&command)
406 }
407
408 fn local_replica_command(&self, action: &str) -> Command {
409 let mut command = self.command();
410 command.args(["network", action]);
411 self.add_local_network_target(&mut command);
412 command
413 }
414
415 fn local_replica_command_in(&self, action: &str, cwd: &Path) -> Command {
416 let mut command = self.command_in(cwd);
417 command.args(["network", action]);
418 self.add_local_network_target(&mut command);
419 command
420 }
421
422 pub fn canister_call_output(
424 &self,
425 canister: &str,
426 method: &str,
427 output: Option<&str>,
428 ) -> Result<String, IcpCommandError> {
429 self.canister_call_output_with_candid(canister, method, output, None)
430 }
431
432 pub fn canister_call_output_with_candid(
434 &self,
435 canister: &str,
436 method: &str,
437 output: Option<&str>,
438 candid_path: Option<&Path>,
439 ) -> Result<String, IcpCommandError> {
440 let mut command = self.canister_command();
441 command.args(["call", canister, method]);
442 command.arg("()");
443 add_candid_arg(&mut command, candid_path);
444 if let Some(output) = output {
445 add_output_arg(&mut command, output);
446 }
447 self.add_target_args(&mut command);
448 run_output(&mut command)
449 }
450
451 pub fn canister_call_arg_output(
453 &self,
454 canister: &str,
455 method: &str,
456 arg: &str,
457 output: Option<&str>,
458 ) -> Result<String, IcpCommandError> {
459 self.canister_call_arg_output_with_candid(canister, method, arg, output, None)
460 }
461
462 pub fn canister_call_arg_output_with_candid(
464 &self,
465 canister: &str,
466 method: &str,
467 arg: &str,
468 output: Option<&str>,
469 candid_path: Option<&Path>,
470 ) -> Result<String, IcpCommandError> {
471 let mut command = self.canister_command();
472 command.args(["call", canister, method]);
473 command.arg(arg);
474 add_candid_arg(&mut command, candid_path);
475 if let Some(output) = output {
476 add_output_arg(&mut command, output);
477 }
478 self.add_target_args(&mut command);
479 run_output(&mut command)
480 }
481
482 pub fn canister_query_output(
484 &self,
485 canister: &str,
486 method: &str,
487 output: Option<&str>,
488 ) -> Result<String, IcpCommandError> {
489 self.canister_query_output_with_candid(canister, method, output, None)
490 }
491
492 pub fn canister_query_output_with_candid(
494 &self,
495 canister: &str,
496 method: &str,
497 output: Option<&str>,
498 candid_path: Option<&Path>,
499 ) -> Result<String, IcpCommandError> {
500 let mut command = self.canister_command();
501 command.args(["call", canister, method]);
502 command.arg("()");
503 command.arg("--query");
504 add_candid_arg(&mut command, candid_path);
505 if let Some(output) = output {
506 add_output_arg(&mut command, output);
507 }
508 self.add_target_args(&mut command);
509 run_output(&mut command)
510 }
511
512 pub fn canister_query_arg_output(
514 &self,
515 canister: &str,
516 method: &str,
517 arg: &str,
518 output: Option<&str>,
519 ) -> Result<String, IcpCommandError> {
520 self.canister_query_arg_output_with_candid(canister, method, arg, output, None)
521 }
522
523 pub fn canister_query_arg_output_with_candid(
525 &self,
526 canister: &str,
527 method: &str,
528 arg: &str,
529 output: Option<&str>,
530 candid_path: Option<&Path>,
531 ) -> Result<String, IcpCommandError> {
532 let mut command = self.canister_command();
533 command.args(["call", canister, method]);
534 command.arg(arg);
535 command.arg("--query");
536 add_candid_arg(&mut command, candid_path);
537 if let Some(output) = output {
538 add_output_arg(&mut command, output);
539 }
540 self.add_target_args(&mut command);
541 run_output(&mut command)
542 }
543
544 pub fn canister_metadata_output(
546 &self,
547 canister: &str,
548 metadata_name: &str,
549 ) -> Result<String, IcpCommandError> {
550 let mut command = self.canister_command();
551 command.args(["metadata", canister, metadata_name]);
552 self.add_target_args(&mut command);
553 run_output(&mut command)
554 }
555
556 pub fn canister_status(&self, canister: &str) -> Result<String, IcpCommandError> {
558 let mut command = self.canister_command();
559 command.args(["status", canister]);
560 self.add_target_args(&mut command);
561 run_output(&mut command)
562 }
563
564 pub fn canister_top_up_output(
566 &self,
567 canister: &str,
568 amount_cycles: u128,
569 ) -> Result<String, IcpCommandError> {
570 let mut command = self.canister_command();
571 command.args(["top-up", "--amount"]);
572 command.arg(amount_cycles.to_string());
573 command.arg(canister);
574 self.add_target_args(&mut command);
575 run_output_with_stderr(&mut command)
576 }
577
578 pub fn canister_status_report(
580 &self,
581 canister: &str,
582 ) -> Result<IcpCanisterStatusReport, IcpCommandError> {
583 let mut command = self.canister_command();
584 command.args(["status", canister]);
585 command.arg("--json");
586 self.add_target_args(&mut command);
587 run_json(&mut command)
588 }
589
590 pub fn snapshot_create_receipt(
592 &self,
593 canister: &str,
594 ) -> Result<IcpSnapshotCreateReceipt, IcpCommandError> {
595 let mut command = self.canister_command();
596 command.args(["snapshot", "create", canister]);
597 command.arg("--json");
598 self.add_target_args(&mut command);
599 run_json(&mut command)
600 }
601
602 pub fn snapshot_create_id(&self, canister: &str) -> Result<String, IcpCommandError> {
604 Ok(self.snapshot_create_receipt(canister)?.snapshot_id)
605 }
606
607 pub fn stop_canister(&self, canister: &str) -> Result<(), IcpCommandError> {
609 let mut command = self.canister_command();
610 command.args(["stop", canister]);
611 self.add_target_args(&mut command);
612 run_status(&mut command)
613 }
614
615 pub fn start_canister(&self, canister: &str) -> Result<(), IcpCommandError> {
617 let mut command = self.canister_command();
618 command.args(["start", canister]);
619 self.add_target_args(&mut command);
620 run_status(&mut command)
621 }
622
623 pub fn snapshot_download(
625 &self,
626 canister: &str,
627 snapshot_id: &str,
628 artifact_path: &Path,
629 ) -> Result<(), IcpCommandError> {
630 let mut command = self.canister_command();
631 command.args(["snapshot", "download", canister, snapshot_id, "--output"]);
632 command.arg(artifact_path);
633 self.add_target_args(&mut command);
634 run_status(&mut command)
635 }
636
637 pub fn snapshot_upload(
639 &self,
640 canister: &str,
641 artifact_path: &Path,
642 ) -> Result<String, IcpCommandError> {
643 Ok(self
644 .snapshot_upload_receipt(canister, artifact_path)?
645 .snapshot_id)
646 }
647
648 pub fn snapshot_upload_receipt(
650 &self,
651 canister: &str,
652 artifact_path: &Path,
653 ) -> Result<IcpSnapshotUploadReceipt, IcpCommandError> {
654 let mut command = self.canister_command();
655 command.args(["snapshot", "upload", canister, "--input"]);
656 command.arg(artifact_path);
657 command.arg("--resume");
658 command.arg("--json");
659 self.add_target_args(&mut command);
660 run_json(&mut command)
661 }
662
663 pub fn snapshot_restore(
665 &self,
666 canister: &str,
667 snapshot_id: &str,
668 ) -> Result<(), IcpCommandError> {
669 let mut command = self.canister_command();
670 command.args(["snapshot", "restore", canister, snapshot_id]);
671 self.add_target_args(&mut command);
672 run_status(&mut command)
673 }
674
675 #[must_use]
677 pub fn snapshot_create_display(&self, canister: &str) -> String {
678 let mut command = self.canister_command();
679 command.args(["snapshot", "create", canister]);
680 command.arg("--json");
681 self.add_target_args(&mut command);
682 command_display(&command)
683 }
684
685 #[must_use]
687 pub fn snapshot_download_display(
688 &self,
689 canister: &str,
690 snapshot_id: &str,
691 artifact_path: &Path,
692 ) -> String {
693 let mut command = self.canister_command();
694 command.args(["snapshot", "download", canister, snapshot_id, "--output"]);
695 command.arg(artifact_path);
696 self.add_target_args(&mut command);
697 command_display(&command)
698 }
699
700 #[must_use]
702 pub fn snapshot_upload_display(&self, canister: &str, artifact_path: &Path) -> String {
703 let mut command = self.canister_command();
704 command.args(["snapshot", "upload", canister, "--input"]);
705 command.arg(artifact_path);
706 command.arg("--resume");
707 command.arg("--json");
708 self.add_target_args(&mut command);
709 command_display(&command)
710 }
711
712 #[must_use]
714 pub fn snapshot_restore_display(&self, canister: &str, snapshot_id: &str) -> String {
715 let mut command = self.canister_command();
716 command.args(["snapshot", "restore", canister, snapshot_id]);
717 self.add_target_args(&mut command);
718 command_display(&command)
719 }
720
721 #[must_use]
723 pub fn stop_canister_display(&self, canister: &str) -> String {
724 let mut command = self.canister_command();
725 command.args(["stop", canister]);
726 self.add_target_args(&mut command);
727 command_display(&command)
728 }
729
730 #[must_use]
732 pub fn start_canister_display(&self, canister: &str) -> String {
733 let mut command = self.canister_command();
734 command.args(["start", canister]);
735 self.add_target_args(&mut command);
736 command_display(&command)
737 }
738
739 #[must_use]
741 pub fn canister_top_up_display(&self, canister: &str, amount_cycles: u128) -> String {
742 let mut command = self.canister_command();
743 command.args(["top-up", "--amount"]);
744 command.arg(amount_cycles.to_string());
745 command.arg(canister);
746 self.add_target_args(&mut command);
747 command_display(&command)
748 }
749
750 #[must_use]
752 pub fn canister_query_output_display(
753 &self,
754 canister: &str,
755 method: &str,
756 output: Option<&str>,
757 ) -> String {
758 self.canister_query_output_display_with_candid(canister, method, output, None)
759 }
760
761 #[must_use]
763 pub fn canister_query_output_display_with_candid(
764 &self,
765 canister: &str,
766 method: &str,
767 output: Option<&str>,
768 candid_path: Option<&Path>,
769 ) -> String {
770 let mut command = self.canister_command();
771 command.args(["call", canister, method]);
772 command.arg("()");
773 command.arg("--query");
774 add_candid_arg(&mut command, candid_path);
775 if let Some(output) = output {
776 add_output_arg(&mut command, output);
777 }
778 self.add_target_args(&mut command);
779 command_display(&command)
780 }
781
782 #[must_use]
784 pub fn canister_call_arg_output_display(
785 &self,
786 canister: &str,
787 method: &str,
788 arg: &str,
789 output: Option<&str>,
790 ) -> String {
791 self.canister_call_arg_output_display_with_candid(canister, method, arg, output, None)
792 }
793
794 #[must_use]
796 pub fn canister_call_arg_output_display_with_candid(
797 &self,
798 canister: &str,
799 method: &str,
800 arg: &str,
801 output: Option<&str>,
802 candid_path: Option<&Path>,
803 ) -> String {
804 let mut command = self.canister_command();
805 command.args(["call", canister, method]);
806 command.arg(arg);
807 add_candid_arg(&mut command, candid_path);
808 if let Some(output) = output {
809 add_output_arg(&mut command, output);
810 }
811 self.add_target_args(&mut command);
812 command_display(&command)
813 }
814
815 fn add_target_args(&self, command: &mut Command) {
816 add_target_args(command, self.environment(), self.network());
817 }
818
819 fn add_local_network_target(&self, command: &mut Command) {
820 if let Some(environment) = self.environment() {
821 command.args(["-e", environment]);
822 } else if let Some(network) = self.network() {
823 command.arg(network);
824 } else {
825 command.arg(LOCAL_NETWORK);
826 }
827 }
828}
829
830#[must_use]
832pub fn default_command() -> Command {
833 IcpCli::new("icp", None, None).command()
834}
835
836#[must_use]
838pub fn default_command_in(cwd: &Path) -> Command {
839 IcpCli::new("icp", None, None).command_in(cwd)
840}
841
842pub fn add_target_args(command: &mut Command, environment: Option<&str>, network: Option<&str>) {
844 if let Some(environment) = environment {
845 if environment == LOCAL_NETWORK
846 && let Some(url) = env::var_os(CANIC_ICP_LOCAL_NETWORK_URL_ENV)
847 {
848 command.env_remove("ICP_ENVIRONMENT");
849 command.arg("-n").arg(url);
850 if let Some(root_key) = env::var_os(CANIC_ICP_LOCAL_ROOT_KEY_ENV) {
851 command.arg("-k").arg(root_key);
852 }
853 return;
854 }
855 command.args(["-e", environment]);
856 } else if let Some(network) = network {
857 command.args(["-n", network]);
858 }
859}
860
861pub fn add_output_arg(command: &mut Command, output: &str) {
863 if output == "json" {
864 command.arg("--json");
865 } else {
866 command.args(["--output", output]);
867 }
868}
869
870pub fn add_candid_arg(command: &mut Command, candid_path: Option<&Path>) {
872 if let Some(candid_path) = candid_path {
873 command.arg("--candid").arg(candid_path);
874 }
875}
876
877#[must_use]
879pub fn local_canister_candid_path(icp_root: &Path, environment: &str, role: &str) -> PathBuf {
880 icp_root
881 .join(".icp")
882 .join(environment)
883 .join("canisters")
884 .join(role)
885 .join(format!("{role}.did"))
886}
887
888#[must_use]
890pub fn existing_local_canister_candid_path(
891 icp_root: &Path,
892 environment: &str,
893 role: &str,
894) -> Option<PathBuf> {
895 let path = local_canister_candid_path(icp_root, environment, role);
896 path.is_file().then_some(path)
897}
898
899pub fn add_debug_arg(command: &mut Command, debug: bool) {
901 if debug {
902 command.arg("--debug");
903 }
904}
905
906pub fn ensure_command_compatible(command: &Command) -> Result<(), IcpCommandError> {
908 let executable = command.get_program().to_string_lossy();
909 compatible_version_output(executable.as_ref(), command.get_current_dir()).map(|_| ())
910}
911
912fn add_project_root_override_arg(command: &mut Command, cwd: &Path) {
913 command.arg("--project-root-override").arg(cwd);
914}
915
916fn run_local_replica_start_command(
917 command: &mut Command,
918 background: bool,
919 debug: bool,
920) -> Result<String, IcpCommandError> {
921 add_debug_arg(command, debug);
922 if background {
923 command.arg("--background");
924 return run_output_with_stderr(command);
925 }
926 run_status_inherit(command)?;
927 Ok(String::new())
928}
929
930pub fn run_output(command: &mut Command) -> Result<String, IcpCommandError> {
932 ensure_command_compatible(command)?;
933 run_output_unchecked(command)
934}
935
936fn run_output_unchecked(command: &mut Command) -> Result<String, IcpCommandError> {
937 let display = command_display(command);
938 let output = command.output()?;
939 if output.status.success() {
940 Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
941 } else {
942 Err(IcpCommandError::Failed {
943 command: display,
944 stderr: command_stderr(&output),
945 })
946 }
947}
948
949pub fn run_output_with_stderr(command: &mut Command) -> Result<String, IcpCommandError> {
951 ensure_command_compatible(command)?;
952 let display = command_display(command);
953 let output = command.output()?;
954 if output.status.success() {
955 let mut text = String::from_utf8_lossy(&output.stdout).to_string();
956 text.push_str(&String::from_utf8_lossy(&output.stderr));
957 Ok(text.trim().to_string())
958 } else {
959 Err(IcpCommandError::Failed {
960 command: display,
961 stderr: command_stderr(&output),
962 })
963 }
964}
965
966pub fn run_json<T>(command: &mut Command) -> Result<T, IcpCommandError>
968where
969 T: serde::de::DeserializeOwned,
970{
971 ensure_command_compatible(command)?;
972 let display = command_display(command);
973 let output = command.output()?;
974 if output.status.success() {
975 let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
976 serde_json::from_str(&stdout).map_err(|source| IcpCommandError::Json {
977 command: display,
978 output: stdout,
979 source,
980 })
981 } else {
982 Err(IcpCommandError::Failed {
983 command: display,
984 stderr: command_stderr(&output),
985 })
986 }
987}
988
989pub fn run_status(command: &mut Command) -> Result<(), IcpCommandError> {
991 ensure_command_compatible(command)?;
992 let display = command_display(command);
993 let output = command.output()?;
994 if output.status.success() {
995 Ok(())
996 } else {
997 Err(IcpCommandError::Failed {
998 command: display,
999 stderr: command_stderr(&output),
1000 })
1001 }
1002}
1003
1004pub fn run_status_inherit(command: &mut Command) -> Result<(), IcpCommandError> {
1006 ensure_command_compatible(command)?;
1007 let display = command_display(command);
1008 let mut child = command
1009 .stdout(Stdio::inherit())
1010 .stderr(Stdio::piped())
1011 .spawn()?;
1012 let stderr_handle = child
1013 .stderr
1014 .take()
1015 .map(|stderr| thread::spawn(move || stream_and_capture_stderr(stderr)));
1016 let status = child.wait()?;
1017 let stderr = match stderr_handle {
1018 Some(handle) => match handle.join() {
1019 Ok(result) => result?,
1020 Err(_) => Vec::new(),
1021 },
1022 None => Vec::new(),
1023 };
1024 if status.success() {
1025 Ok(())
1026 } else {
1027 let stderr = if stderr.is_empty() {
1028 format!("command exited with status {}", exit_status_label(status))
1029 } else {
1030 String::from_utf8_lossy(&stderr).to_string()
1031 };
1032 Err(IcpCommandError::Failed {
1033 command: display,
1034 stderr,
1035 })
1036 }
1037}
1038
1039fn stream_and_capture_stderr(mut stderr: impl Read) -> io::Result<Vec<u8>> {
1040 let mut captured = Vec::new();
1041 let mut buffer = [0_u8; 8192];
1042 let mut terminal = io::stderr().lock();
1043 loop {
1044 let read = stderr.read(&mut buffer)?;
1045 if read == 0 {
1046 break;
1047 }
1048 terminal.write_all(&buffer[..read])?;
1049 captured.extend_from_slice(&buffer[..read]);
1050 }
1051 terminal.flush()?;
1052 Ok(captured)
1053}
1054
1055pub fn run_success(command: &mut Command) -> Result<bool, IcpCommandError> {
1057 ensure_command_compatible(command)?;
1058 Ok(command.output()?.status.success())
1059}
1060
1061pub fn run_raw_output(program: &str, args: &[String]) -> Result<IcpRawOutput, std::io::Error> {
1063 if is_icp_program(program) {
1064 compatible_version_output(program, None)
1065 .map_err(|err| io::Error::other(err.to_string()))?;
1066 }
1067 let output = Command::new(program).args(args).output()?;
1068 Ok(IcpRawOutput {
1069 success: output.status.success(),
1070 status: exit_status_label(output.status),
1071 stdout: output.stdout,
1072 stderr: output.stderr,
1073 })
1074}
1075
1076fn is_icp_program(program: &str) -> bool {
1077 Path::new(program)
1078 .file_name()
1079 .is_some_and(|file_name| file_name == "icp")
1080}
1081
1082#[must_use]
1084pub fn command_display(command: &Command) -> String {
1085 let mut parts = vec![command.get_program().to_string_lossy().to_string()];
1086 parts.extend(
1087 command
1088 .get_args()
1089 .map(|arg| arg.to_string_lossy().to_string()),
1090 );
1091 parts.join(" ")
1092}
1093
1094#[must_use]
1096pub fn parse_snapshot_id(output: &str) -> Option<String> {
1097 let trimmed = output.trim();
1098 if is_snapshot_id_token(trimmed) {
1099 return Some(trimmed.to_string());
1100 }
1101
1102 output
1103 .lines()
1104 .flat_map(|line| {
1105 line.split(|c: char| c.is_whitespace() || matches!(c, '"' | '\'' | ':' | ','))
1106 })
1107 .find(|part| is_snapshot_id_token(part))
1108 .map(str::to_string)
1109}
1110
1111#[must_use]
1113pub fn parse_icp_cli_version(output: &str) -> Option<IcpCliVersion> {
1114 output
1115 .split_whitespace()
1116 .find_map(parse_icp_cli_version_token)
1117}
1118
1119#[must_use]
1121pub const fn is_supported_icp_cli_version(version: IcpCliVersion) -> bool {
1122 version.major == 0 && version.minor == 3 && version.patch >= 2
1123}
1124
1125fn compatible_version_output(
1126 executable: &str,
1127 cwd: Option<&Path>,
1128) -> Result<String, IcpCommandError> {
1129 let output = icp_version_output(executable, cwd)?;
1130 if let Some(version) = parse_icp_cli_version(&output)
1131 && is_supported_icp_cli_version(version)
1132 {
1133 return Ok(output);
1134 }
1135 Err(IcpCommandError::IncompatibleCliVersion {
1136 executable: executable.to_string(),
1137 found: output,
1138 })
1139}
1140
1141fn icp_version_output(executable: &str, cwd: Option<&Path>) -> Result<String, IcpCommandError> {
1142 let mut command = Command::new(executable);
1143 if let Some(cwd) = cwd {
1144 command.current_dir(cwd);
1145 }
1146 command.arg("--version");
1147 let display = command_display(&command);
1148 let output = command.output().map_err(|err| {
1149 if err.kind() == io::ErrorKind::NotFound {
1150 IcpCommandError::MissingCli {
1151 executable: executable.to_string(),
1152 }
1153 } else {
1154 IcpCommandError::Io(err)
1155 }
1156 })?;
1157 if output.status.success() {
1158 Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
1159 } else {
1160 Err(IcpCommandError::Failed {
1161 command: display,
1162 stderr: command_stderr(&output),
1163 })
1164 }
1165}
1166
1167fn parse_icp_cli_version_token(token: &str) -> Option<IcpCliVersion> {
1168 let token = token
1169 .trim_matches(|c: char| matches!(c, ',' | ';' | ')' | '('))
1170 .trim_start_matches('v');
1171 let mut parts = token.split('.');
1172 let major = parts.next()?.parse::<u64>().ok()?;
1173 let minor = parts.next()?.parse::<u64>().ok()?;
1174 let patch_token = parts.next()?;
1175 let patch_digits = patch_token
1176 .chars()
1177 .take_while(char::is_ascii_digit)
1178 .collect::<String>();
1179 if patch_digits.is_empty() || parts.next().is_some() {
1180 return None;
1181 }
1182 Some(IcpCliVersion {
1183 major,
1184 minor,
1185 patch: patch_digits.parse::<u64>().ok()?,
1186 })
1187}
1188
1189fn is_snapshot_id_token(value: &str) -> bool {
1191 !value.is_empty()
1192 && value.len().is_multiple_of(2)
1193 && value.chars().all(|c| c.is_ascii_hexdigit())
1194}
1195
1196fn command_stderr(output: &std::process::Output) -> String {
1198 let stderr = String::from_utf8_lossy(&output.stderr);
1199 if stderr.trim().is_empty() {
1200 String::from_utf8_lossy(&output.stdout).to_string()
1201 } else {
1202 stderr.to_string()
1203 }
1204}
1205
1206fn exit_status_label(status: std::process::ExitStatus) -> String {
1208 status
1209 .code()
1210 .map_or_else(|| "signal".to_string(), |code| code.to_string())
1211}
1212
1213#[cfg(test)]
1214mod tests;