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