Skip to main content

canic_host/icp/
command.rs

1use std::{
2    env,
3    path::{Path, PathBuf},
4    process::Command,
5};
6
7use super::{
8    error::IcpCommandError,
9    model::{CANIC_ICP_LOCAL_NETWORK_URL_ENV, CANIC_ICP_LOCAL_ROOT_KEY_ENV, IcpCli, LOCAL_NETWORK},
10    version::compatible_version_output,
11};
12
13impl IcpCli {
14    /// Build an ICP CLI command context from an executable path and optional target.
15    #[must_use]
16    pub fn new(
17        executable: impl Into<String>,
18        environment: Option<String>,
19        network: Option<String>,
20    ) -> Self {
21        Self {
22            executable: executable.into(),
23            environment,
24            network,
25            cwd: None,
26        }
27    }
28
29    /// Return a copy of this ICP CLI context rooted at one project directory.
30    #[must_use]
31    pub fn with_cwd(mut self, cwd: impl Into<PathBuf>) -> Self {
32        self.cwd = Some(cwd.into());
33        self
34    }
35
36    /// Return the optional ICP environment name carried by this command context.
37    #[must_use]
38    pub fn environment(&self) -> Option<&str> {
39        self.environment.as_deref()
40    }
41
42    /// Return the optional direct network name carried by this command context.
43    #[must_use]
44    pub fn network(&self) -> Option<&str> {
45        self.network.as_deref()
46    }
47
48    /// Build a base ICP CLI command from this context.
49    #[must_use]
50    pub fn command(&self) -> Command {
51        let mut command = Command::new(&self.executable);
52        if let Some(cwd) = &self.cwd {
53            command.current_dir(cwd);
54            add_project_root_override_arg(&mut command, cwd);
55        }
56        command
57    }
58
59    /// Build a base ICP CLI command rooted at one workspace directory.
60    #[must_use]
61    pub fn command_in(&self, cwd: &Path) -> Command {
62        let mut command = Command::new(&self.executable);
63        command.current_dir(cwd);
64        add_project_root_override_arg(&mut command, cwd);
65        command
66    }
67
68    /// Build an `icp canister ...` command with optional environment args applied.
69    #[must_use]
70    pub fn canister_command(&self) -> Command {
71        let mut command = self.command();
72        command.arg("canister");
73        command
74    }
75
76    pub(super) fn add_target_args(&self, command: &mut Command) {
77        add_target_args(command, self.environment(), self.network());
78    }
79
80    pub(super) fn add_local_network_target(&self, command: &mut Command) {
81        if let Some(environment) = self.environment() {
82            command.args(["-e", environment]);
83        } else if let Some(network) = self.network() {
84            command.arg(network);
85        } else {
86            command.arg(LOCAL_NETWORK);
87        }
88    }
89}
90
91/// Build a base `icp` command with the default executable.
92#[must_use]
93pub fn default_command() -> Command {
94    IcpCli::new("icp", None, None).command()
95}
96
97/// Build a base `icp` command rooted at one workspace directory.
98#[must_use]
99pub fn default_command_in(cwd: &Path) -> Command {
100    IcpCli::new("icp", None, None).command_in(cwd)
101}
102
103/// Add optional ICP CLI target arguments, preferring named environments.
104pub fn add_target_args(command: &mut Command, environment: Option<&str>, network: Option<&str>) {
105    if let Some(environment) = environment {
106        if environment == LOCAL_NETWORK
107            && let Some(url) = env::var_os(CANIC_ICP_LOCAL_NETWORK_URL_ENV)
108        {
109            command.env_remove("ICP_ENVIRONMENT");
110            command.arg("-n").arg(url);
111            if let Some(root_key) = env::var_os(CANIC_ICP_LOCAL_ROOT_KEY_ENV) {
112                command.arg("-k").arg(root_key);
113            }
114            return;
115        }
116        command.args(["-e", environment]);
117    } else if let Some(network) = network {
118        command.args(["-n", network]);
119    }
120}
121
122/// Add ICP CLI output formatting, handling JSON as its own flag.
123pub fn add_output_arg(command: &mut Command, output: &str) {
124    if output == "json" {
125        command.arg("--json");
126    } else {
127        command.args(["--output", output]);
128    }
129}
130
131/// Add an ICP CLI local Candid interface path when one is available.
132pub fn add_candid_arg(command: &mut Command, candid_path: Option<&Path>) {
133    if let Some(candid_path) = candid_path {
134        command.arg("--candid").arg(candid_path);
135    }
136}
137
138/// Return Canic's local ICP CLI Candid sidecar path for one role.
139#[must_use]
140pub fn local_canister_candid_path(icp_root: &Path, environment: &str, role: &str) -> PathBuf {
141    icp_root
142        .join(".icp")
143        .join(environment)
144        .join("canisters")
145        .join(role)
146        .join(format!("{role}.did"))
147}
148
149/// Return the local Candid sidecar path only when it exists on disk.
150#[must_use]
151pub fn existing_local_canister_candid_path(
152    icp_root: &Path,
153    environment: &str,
154    role: &str,
155) -> Option<PathBuf> {
156    let path = local_canister_candid_path(icp_root, environment, role);
157    path.is_file().then_some(path)
158}
159
160/// Add ICP CLI debug logging when requested.
161pub fn add_debug_arg(command: &mut Command, debug: bool) {
162    if debug {
163        command.arg("--debug");
164    }
165}
166
167/// Ensure a command points at a supported ICP CLI executable before spawning it.
168pub fn ensure_command_compatible(command: &Command) -> Result<(), IcpCommandError> {
169    let executable = command.get_program().to_string_lossy();
170    compatible_version_output(executable.as_ref(), command.get_current_dir()).map(|_| ())
171}
172
173fn add_project_root_override_arg(command: &mut Command, cwd: &Path) {
174    command.arg("--project-root-override").arg(cwd);
175}
176
177/// Render a command for diagnostics and dry-run previews.
178#[must_use]
179pub fn command_display(command: &Command) -> String {
180    let mut parts = vec![command.get_program().to_string_lossy().to_string()];
181    parts.extend(
182        command
183            .get_args()
184            .map(|arg| arg.to_string_lossy().to_string()),
185    );
186    parts.join(" ")
187}