1use crate::release_set::dfx_root;
2use serde::{Deserialize, Serialize};
3use std::{fs, path::Path, path::PathBuf};
4
5pub(super) const INSTALL_STATE_SCHEMA_VERSION: u32 = 1;
6const INSTALL_STATE_FILE: &str = "install-state.json";
7const CURRENT_FLEET_FILE: &str = "current-fleet";
8
9#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
14pub struct InstallState {
15 pub schema_version: u32,
16 #[serde(default = "default_fleet_name")]
17 pub fleet: String,
18 pub installed_at_unix_secs: u64,
19 pub network: String,
20 pub root_target: String,
21 pub root_canister_id: String,
22 pub root_build_target: String,
23 pub workspace_root: String,
24 pub dfx_root: String,
25 pub config_path: String,
26 pub release_set_manifest_path: String,
27}
28
29#[derive(Clone, Debug, Eq, PartialEq)]
34pub struct FleetSummary {
35 pub name: String,
36 pub current: bool,
37 pub state: InstallState,
38}
39
40pub(super) fn read_install_state(
42 dfx_root: &Path,
43 network: &str,
44) -> Result<Option<InstallState>, Box<dyn std::error::Error>> {
45 if let Some(fleet) = read_selected_fleet_name(dfx_root, network)? {
46 return read_fleet_install_state(dfx_root, network, &fleet);
47 }
48
49 read_legacy_install_state(dfx_root, network)
50}
51
52pub(super) fn read_fleet_install_state(
54 dfx_root: &Path,
55 network: &str,
56 fleet: &str,
57) -> Result<Option<InstallState>, Box<dyn std::error::Error>> {
58 validate_fleet_name(fleet)?;
59 let path = fleet_install_state_path(dfx_root, network, fleet);
60 if !path.is_file() {
61 return Ok(None);
62 }
63
64 let bytes = fs::read(&path)?;
65 let mut state: InstallState = serde_json::from_slice(&bytes)?;
66 if state.fleet.is_empty() {
67 state.fleet = fleet.to_string();
68 }
69 Ok(Some(state))
70}
71
72pub fn read_current_install_state(
74 network: &str,
75) -> Result<Option<InstallState>, Box<dyn std::error::Error>> {
76 let dfx_root = dfx_root()?;
77 read_install_state(&dfx_root, network)
78}
79
80pub fn read_current_or_fleet_install_state(
82 network: &str,
83 fleet: Option<&str>,
84) -> Result<Option<InstallState>, Box<dyn std::error::Error>> {
85 let dfx_root = dfx_root()?;
86 match fleet {
87 Some(fleet) => read_fleet_install_state(&dfx_root, network, fleet),
88 None => read_install_state(&dfx_root, network),
89 }
90}
91
92pub fn list_current_fleets(network: &str) -> Result<Vec<FleetSummary>, Box<dyn std::error::Error>> {
94 let dfx_root = dfx_root()?;
95 list_fleets(&dfx_root, network)
96}
97
98pub(super) fn list_fleets(
100 dfx_root: &Path,
101 network: &str,
102) -> Result<Vec<FleetSummary>, Box<dyn std::error::Error>> {
103 let current = read_selected_fleet_name(dfx_root, network)?;
104 let mut fleets = Vec::new();
105 let dir = fleets_dir(dfx_root, network);
106 if dir.is_dir() {
107 for entry in fs::read_dir(&dir)? {
108 let entry = entry?;
109 let path = entry.path();
110 if path.extension().and_then(|ext| ext.to_str()) != Some("json") {
111 continue;
112 }
113 let Some(name) = path.file_stem().and_then(|stem| stem.to_str()) else {
114 continue;
115 };
116 if let Some(state) = read_fleet_install_state(dfx_root, network, name)? {
117 fleets.push(FleetSummary {
118 name: name.to_string(),
119 current: current.as_deref() == Some(name),
120 state,
121 });
122 }
123 }
124 }
125
126 if fleets.is_empty()
127 && let Some(state) = read_legacy_install_state(dfx_root, network)?
128 {
129 fleets.push(FleetSummary {
130 name: state.fleet.clone(),
131 current: true,
132 state,
133 });
134 }
135
136 fleets.sort_by(|left, right| left.name.cmp(&right.name));
137 Ok(fleets)
138}
139
140pub fn select_current_fleet(
142 network: &str,
143 fleet: &str,
144) -> Result<InstallState, Box<dyn std::error::Error>> {
145 let dfx_root = dfx_root()?;
146 select_fleet(&dfx_root, network, fleet)
147}
148
149fn select_fleet(
151 dfx_root: &Path,
152 network: &str,
153 fleet: &str,
154) -> Result<InstallState, Box<dyn std::error::Error>> {
155 let Some(state) = read_fleet_install_state(dfx_root, network, fleet)?.or_else(|| {
156 matching_legacy_fleet_state(dfx_root, network, fleet)
157 .ok()
158 .flatten()
159 }) else {
160 return Err(format!("unknown fleet {fleet} on network {network}").into());
161 };
162 if fleet_install_state_path(dfx_root, network, fleet).is_file() {
163 write_current_fleet_name(dfx_root, network, fleet)?;
164 } else {
165 write_install_state(dfx_root, network, &state)?;
166 }
167 Ok(state)
168}
169
170#[must_use]
172fn install_state_path(dfx_root: &Path, network: &str) -> PathBuf {
173 dfx_root
174 .join(".canic")
175 .join(network)
176 .join(INSTALL_STATE_FILE)
177}
178
179#[must_use]
181pub(super) fn fleet_install_state_path(dfx_root: &Path, network: &str, fleet: &str) -> PathBuf {
182 fleets_dir(dfx_root, network).join(format!("{fleet}.json"))
183}
184
185#[must_use]
187pub(super) fn current_fleet_path(dfx_root: &Path, network: &str) -> PathBuf {
188 dfx_root
189 .join(".canic")
190 .join(network)
191 .join(CURRENT_FLEET_FILE)
192}
193
194fn fleets_dir(dfx_root: &Path, network: &str) -> PathBuf {
196 dfx_root.join(".canic").join(network).join("fleets")
197}
198
199pub(super) fn write_install_state(
201 dfx_root: &Path,
202 network: &str,
203 state: &InstallState,
204) -> Result<PathBuf, Box<dyn std::error::Error>> {
205 validate_fleet_name(&state.fleet)?;
206 let path = fleet_install_state_path(dfx_root, network, &state.fleet);
207 if let Some(parent) = path.parent() {
208 fs::create_dir_all(parent)?;
209 }
210 fs::write(&path, serde_json::to_vec_pretty(state)?)?;
211 write_current_fleet_name(dfx_root, network, &state.fleet)?;
212 Ok(path)
213}
214
215fn read_legacy_install_state(
217 dfx_root: &Path,
218 network: &str,
219) -> Result<Option<InstallState>, Box<dyn std::error::Error>> {
220 let path = install_state_path(dfx_root, network);
221 if !path.is_file() {
222 return Ok(None);
223 }
224
225 let bytes = fs::read(&path)?;
226 let state: InstallState = serde_json::from_slice(&bytes)?;
227 if state.fleet.is_empty() {
228 return Err(format!(
229 "install state at {} is missing required fleet name; reinstall from a config with [fleet].name",
230 path.display()
231 )
232 .into());
233 }
234 Ok(Some(state))
235}
236
237fn matching_legacy_fleet_state(
239 dfx_root: &Path,
240 network: &str,
241 fleet: &str,
242) -> Result<Option<InstallState>, Box<dyn std::error::Error>> {
243 Ok(read_legacy_install_state(dfx_root, network)?.filter(|state| state.fleet == fleet))
244}
245
246fn read_selected_fleet_name(
248 dfx_root: &Path,
249 network: &str,
250) -> Result<Option<String>, Box<dyn std::error::Error>> {
251 let path = current_fleet_path(dfx_root, network);
252 if !path.is_file() {
253 return Ok(None);
254 }
255
256 let name = fs::read_to_string(path)?.trim().to_string();
257 validate_fleet_name(&name)?;
258 Ok(Some(name))
259}
260
261fn write_current_fleet_name(
263 dfx_root: &Path,
264 network: &str,
265 fleet: &str,
266) -> Result<(), Box<dyn std::error::Error>> {
267 validate_fleet_name(fleet)?;
268 let path = current_fleet_path(dfx_root, network);
269 if let Some(parent) = path.parent() {
270 fs::create_dir_all(parent)?;
271 }
272 fs::write(path, format!("{fleet}\n"))?;
273 Ok(())
274}
275
276const fn default_fleet_name() -> String {
278 String::new()
279}
280
281pub(super) fn validate_fleet_name(name: &str) -> Result<(), Box<dyn std::error::Error>> {
283 let valid = !name.is_empty()
284 && name
285 .bytes()
286 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_'));
287 if valid {
288 Ok(())
289 } else {
290 Err(format!("invalid fleet name {name:?}; use letters, numbers, '-' or '_'").into())
291 }
292}