1mod manifest;
6
7#[cfg(test)]
8mod tests;
9
10use crate::{
11 config_discovery::{
12 ConfigDiscoveryError, current_canic_workspace_root,
13 discover_workspace_canic_config_choices, workspace_app_roots,
14 },
15 icp_config::manifest::IcpManifest,
16 release_set::{AppConfigError, AppConfigSnapshot, WorkspaceDiscoveryError, icp_root},
17 workspace_discovery::discover_icp_root_from,
18};
19use canic_core::ids::BuildNetwork;
20use std::{
21 collections::{BTreeMap, BTreeSet},
22 fs,
23 io::Read as _,
24 path::{Path, PathBuf},
25};
26use thiserror::Error as ThisError;
27
28pub use manifest::IcpManifestError;
29
30const ICP_CONFIG_FILE: &str = "icp.yaml";
31pub const DEFAULT_LOCAL_GATEWAY_PORT: u16 = 8000;
32
33#[derive(Debug, ThisError)]
38pub enum IcpConfigError {
39 #[error("{}: {source}", path.display())]
40 Manifest {
41 path: PathBuf,
42 source: IcpManifestError,
43 },
44 #[error("could not find icp.yaml from {}", start.display())]
45 NoIcpRoot { start: PathBuf },
46
47 #[error("{0}")]
48 Config(String),
49
50 #[error(transparent)]
51 ConfigDiscovery(#[from] ConfigDiscoveryError),
52
53 #[error(transparent)]
54 AppConfig(#[from] AppConfigError),
55
56 #[error(transparent)]
57 WorkspaceDiscovery(#[from] WorkspaceDiscoveryError),
58
59 #[error(transparent)]
60 Io(#[from] std::io::Error),
61}
62
63#[derive(Clone, Debug, Eq, PartialEq)]
68pub struct IcpProjectConfigReport {
69 pub path: PathBuf,
70 pub icp_root: PathBuf,
71 pub icp_yaml_present: bool,
72 pub canisters: Vec<String>,
73 pub environments: Vec<String>,
74 pub missing_canisters: Vec<String>,
75 pub missing_environments: Vec<String>,
76 pub missing_environment_canisters: BTreeMap<String, Vec<String>>,
78 pub local_network_present: bool,
79}
80
81impl IcpProjectConfigReport {
82 #[must_use]
83 pub fn is_ready(&self) -> bool {
84 self.icp_yaml_present
85 && self.local_network_present
86 && self.missing_canisters.is_empty()
87 && self.missing_environments.is_empty()
88 && self.missing_environment_canisters.is_empty()
89 }
90
91 #[must_use]
92 pub fn issues(&self) -> Vec<String> {
93 let mut issues = Vec::new();
94 if !self.icp_yaml_present {
95 issues.push(format!("missing {}", self.path.display()));
96 }
97 if !self.local_network_present {
98 issues.push("missing local network entry".to_string());
99 }
100 if !self.missing_canisters.is_empty() {
101 issues.push(format!(
102 "missing canisters: {}",
103 self.missing_canisters.join(", ")
104 ));
105 }
106 if !self.missing_environments.is_empty() {
107 issues.push(format!(
108 "missing environments: {}",
109 self.missing_environments.join(", ")
110 ));
111 }
112 for (environment, canisters) in &self.missing_environment_canisters {
113 issues.push(format!(
114 "environment {environment} excludes required canisters: {}",
115 canisters.join(", ")
116 ));
117 }
118 issues
119 }
120}
121
122pub(crate) fn configured_local_gateway_port() -> Result<u16, IcpConfigError> {
124 let root = current_icp_root()?;
125 configured_local_gateway_port_from_root(&root)
126}
127
128pub fn configured_local_gateway_port_from_root(root: &Path) -> Result<u16, IcpConfigError> {
130 let path = root.join(ICP_CONFIG_FILE);
131 let (source, _) = read_optional_icp_yaml(&path)?;
132 local_gateway_port_from_yaml(&source)
133 .map_err(|source| IcpConfigError::Manifest { path, source })
134}
135
136pub fn resolve_icp_build_network_from_root(
141 root: &Path,
142 environment: &str,
143) -> Result<BuildNetwork, IcpConfigError> {
144 let path = root.join(ICP_CONFIG_FILE);
145 let (source, _) = read_optional_icp_yaml(&path)?;
146 resolve_icp_build_network_from_yaml(&source, environment)
147 .map_err(|source| IcpConfigError::Manifest { path, source })
148}
149
150pub fn inspect_canic_icp_yaml(
152 app_filter: Option<&str>,
153) -> Result<IcpProjectConfigReport, IcpConfigError> {
154 let root = resolve_current_canic_icp_root()?;
155 inspect_canic_icp_yaml_from_root(&root, app_filter)
156}
157
158pub fn inspect_canic_icp_yaml_from_root(
160 root: &Path,
161 app_filter: Option<&str>,
162) -> Result<IcpProjectConfigReport, IcpConfigError> {
163 let path = root.join(ICP_CONFIG_FILE);
164 let (source, icp_yaml_present) = read_optional_icp_yaml(&path)?;
165 let spec = discover_project_spec(root, app_filter)?;
166 let manifest = IcpManifest::parse(&source).map_err(|source| IcpConfigError::Manifest {
167 path: path.clone(),
168 source,
169 })?;
170 let local_network_present = icp_yaml_present;
171
172 let missing_canisters = spec
173 .canisters
174 .iter()
175 .filter(|name| !manifest.has_canister(name))
176 .cloned()
177 .collect::<Vec<_>>();
178 let missing_environments = spec
179 .environments
180 .keys()
181 .filter(|name| !manifest.has_environment(name))
182 .cloned()
183 .collect::<Vec<_>>();
184
185 let missing_environment_canisters = spec
186 .environments
187 .iter()
188 .filter(|(environment, _)| manifest.has_environment(environment))
189 .filter_map(|(environment, roles)| {
190 let missing = roles
191 .iter()
192 .filter(|role| !manifest.contains(environment, role))
193 .cloned()
194 .collect::<Vec<_>>();
195 (!missing.is_empty()).then(|| (environment.clone(), missing))
196 })
197 .collect();
198
199 Ok(IcpProjectConfigReport {
200 path,
201 icp_root: root.to_path_buf(),
202 icp_yaml_present,
203 canisters: spec.canisters,
204 environments: spec.environments.into_keys().collect(),
205 missing_canisters,
206 missing_environments,
207 missing_environment_canisters,
208 local_network_present,
209 })
210}
211
212fn read_optional_icp_yaml(path: &Path) -> Result<(String, bool), IcpConfigError> {
213 match fs::File::open(path) {
214 Ok(file) => {
215 let mut source = String::new();
216 file.take(1024 * 1024 + 1).read_to_string(&mut source)?;
217 Ok((source, true))
218 }
219 Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok((String::new(), false)),
220 Err(err) => Err(err.into()),
221 }
222}
223
224fn current_icp_root() -> Result<PathBuf, IcpConfigError> {
225 let start = std::env::current_dir().map_err(WorkspaceDiscoveryError::CurrentDirectory)?;
226 discover_icp_root_from(&start)?.ok_or(IcpConfigError::NoIcpRoot { start })
227}
228
229pub fn resolve_current_canic_icp_root() -> Result<PathBuf, IcpConfigError> {
231 let root = current_canic_workspace_root()?.canonicalize()?;
232 if !discover_workspace_canic_config_choices(&root)?.is_empty() {
233 return Ok(root);
234 }
235
236 Ok(icp_root()?)
237}
238
239#[derive(Clone, Debug, Eq, PartialEq)]
244struct CanicIcpSpec {
245 canisters: Vec<String>,
246 environments: BTreeMap<String, Vec<String>>,
247}
248
249fn discover_project_spec(
250 root: &Path,
251 app_filter: Option<&str>,
252) -> Result<CanicIcpSpec, IcpConfigError> {
253 let choices = discover_workspace_canic_config_choices(root)?;
254 if choices.is_empty() {
255 return Err(IcpConfigError::Config(format!(
256 "no Canic App configs found under {}\nCreate apps/<app>/canic.toml, add matching entries to icp.yaml, then retry the requested command.",
257 display_workspace_app_roots(root)
258 )));
259 }
260
261 let mut canisters = Vec::<String>::new();
262 let mut seen_canisters = BTreeSet::<String>::new();
263 let mut environments = BTreeMap::<String, Vec<String>>::new();
264 let mut matched_filter = app_filter.is_none();
265
266 for config_path in choices {
267 let config = AppConfigSnapshot::load(&config_path)?;
268 let app = config.app_id().to_string();
269 if let Some(filter) = app_filter {
270 if filter != app {
271 continue;
272 }
273 matched_filter = true;
274 }
275
276 let roles = config.deployable_roles();
277 for role in &roles {
278 if seen_canisters.insert(role.clone()) {
279 canisters.push(role.clone());
280 }
281 }
282 environments.insert(app, roles);
283 }
284
285 if let Some(app) = app_filter
286 && !matched_filter
287 {
288 return Err(IcpConfigError::Config(format!(
289 "no Canic App config found for {app}\nExpected a config under {} with `[app].name = \"{app}\"`.",
290 display_workspace_app_roots(root)
291 )));
292 }
293
294 Ok(CanicIcpSpec {
295 canisters,
296 environments,
297 })
298}
299
300fn display_workspace_app_roots(root: &Path) -> String {
301 workspace_app_roots(root)
302 .into_iter()
303 .map(|path| path.display().to_string())
304 .collect::<Vec<_>>()
305 .join(" or ")
306}
307
308fn resolve_icp_build_network_from_yaml(
309 source: &str,
310 environment: &str,
311) -> Result<BuildNetwork, IcpManifestError> {
312 IcpManifest::parse(source)?.build_network(environment)
313}
314
315fn local_gateway_port_from_yaml(source: &str) -> Result<u16, IcpManifestError> {
316 IcpManifest::parse(source)?.local_gateway_port()
317}