1use crate::{
2 args::{
3 first_arg_is_help, first_arg_is_version, flag_arg, parse_matches, path_option,
4 string_option, value_arg,
5 },
6 version_text,
7};
8use canic_host::release_set::{
9 config_path, configured_install_targets, dfx_root, emit_root_release_set_manifest,
10 emit_root_release_set_manifest_if_ready, load_root_release_set_manifest, resolve_artifact_root,
11 resume_root_bootstrap, root_release_set_manifest_path, stage_root_release_set, workspace_root,
12};
13use clap::{ArgMatches, Command as ClapCommand};
14use std::{env, ffi::OsString, path::PathBuf};
15use thiserror::Error as ThisError;
16
17const DEFAULT_ROOT_TARGET: &str = "root";
18
19#[derive(Debug, ThisError)]
24pub enum ReleaseSetCommandError {
25 #[error("{0}")]
26 Usage(&'static str),
27
28 #[error(transparent)]
29 ReleaseSet(#[from] Box<dyn std::error::Error>),
30}
31
32#[derive(Clone, Debug, Eq, PartialEq)]
37enum ReleaseSetCommand {
38 Targets(TargetsOptions),
39 Manifest(ManifestOptions),
40 Stage(StageOptions),
41}
42
43#[derive(Clone, Debug, Eq, PartialEq)]
48struct TargetsOptions {
49 config_path: Option<PathBuf>,
50 root_target: String,
51}
52
53#[derive(Clone, Debug, Eq, PartialEq)]
58struct ManifestOptions {
59 if_ready: bool,
60}
61
62#[derive(Clone, Debug, Eq, PartialEq)]
67struct StageOptions {
68 root_target: String,
69}
70
71pub fn run<I>(args: I) -> Result<(), ReleaseSetCommandError>
73where
74 I: IntoIterator<Item = OsString>,
75{
76 let args = args.into_iter().collect::<Vec<_>>();
77 if first_arg_is_help(&args) {
78 println!("{}", usage());
79 return Ok(());
80 }
81 if first_arg_is_version(&args) {
82 println!("{}", version_text());
83 return Ok(());
84 }
85
86 match ReleaseSetCommand::parse(args)? {
87 ReleaseSetCommand::Targets(options) => run_targets(options),
88 ReleaseSetCommand::Manifest(options) => run_manifest(options),
89 ReleaseSetCommand::Stage(options) => run_stage(options),
90 }
91 .map_err(ReleaseSetCommandError::from)
92}
93
94impl ReleaseSetCommand {
95 fn parse<I>(args: I) -> Result<Self, ReleaseSetCommandError>
97 where
98 I: IntoIterator<Item = OsString>,
99 {
100 let mut args = args.into_iter();
101 let Some(command) = args.next().and_then(|arg| arg.into_string().ok()) else {
102 return Err(ReleaseSetCommandError::Usage(usage()));
103 };
104
105 match command.as_str() {
106 "targets" => Ok(Self::Targets(TargetsOptions::parse(args)?)),
107 "manifest" => Ok(Self::Manifest(ManifestOptions::parse(args)?)),
108 "stage" => Ok(Self::Stage(StageOptions::parse(args)?)),
109 _ => Err(ReleaseSetCommandError::Usage(usage())),
110 }
111 }
112}
113
114impl TargetsOptions {
115 fn parse<I>(args: I) -> Result<Self, ReleaseSetCommandError>
117 where
118 I: IntoIterator<Item = OsString>,
119 {
120 let matches = parse_release_set_options(targets_command(), args, targets_usage())?;
121
122 Ok(Self {
123 config_path: path_option(&matches, "config"),
124 root_target: string_option(&matches, "root")
125 .unwrap_or_else(|| DEFAULT_ROOT_TARGET.to_string()),
126 })
127 }
128}
129
130impl ManifestOptions {
131 fn parse<I>(args: I) -> Result<Self, ReleaseSetCommandError>
133 where
134 I: IntoIterator<Item = OsString>,
135 {
136 let matches = parse_release_set_options(manifest_command(), args, manifest_usage())?;
137
138 Ok(Self {
139 if_ready: matches.get_flag("if-ready"),
140 })
141 }
142}
143
144impl StageOptions {
145 fn parse<I>(args: I) -> Result<Self, ReleaseSetCommandError>
147 where
148 I: IntoIterator<Item = OsString>,
149 {
150 let matches = parse_release_set_options(stage_command(), args, stage_usage())?;
151
152 Ok(Self {
153 root_target: string_option(&matches, "root-canister")
154 .or_else(|| env::var("ROOT_CANISTER").ok())
155 .unwrap_or_else(|| DEFAULT_ROOT_TARGET.to_string()),
156 })
157 }
158}
159
160fn parse_release_set_options<I>(
162 command: ClapCommand,
163 args: I,
164 usage: &'static str,
165) -> Result<ArgMatches, ReleaseSetCommandError>
166where
167 I: IntoIterator<Item = OsString>,
168{
169 parse_matches(command, args).map_err(|_| ReleaseSetCommandError::Usage(usage))
170}
171
172fn targets_command() -> ClapCommand {
174 ClapCommand::new("targets")
175 .disable_help_flag(true)
176 .arg(value_arg("config").long("config"))
177 .arg(value_arg("root").long("root"))
178}
179
180fn manifest_command() -> ClapCommand {
182 ClapCommand::new("manifest")
183 .disable_help_flag(true)
184 .arg(flag_arg("if-ready").long("if-ready"))
185}
186
187fn stage_command() -> ClapCommand {
189 ClapCommand::new("stage")
190 .disable_help_flag(true)
191 .arg(value_arg("root-canister"))
192}
193
194fn run_targets(options: TargetsOptions) -> Result<(), Box<dyn std::error::Error>> {
196 let workspace_root = workspace_root()?;
197 let config_path = options
198 .config_path
199 .unwrap_or_else(|| config_path(&workspace_root));
200
201 for role in configured_install_targets(&config_path, &options.root_target)? {
202 println!("{role}");
203 }
204
205 Ok(())
206}
207
208fn run_manifest(options: ManifestOptions) -> Result<(), Box<dyn std::error::Error>> {
210 let workspace_root = workspace_root()?;
211 let dfx_root = dfx_root()?;
212 let network = env::var("DFX_NETWORK").unwrap_or_else(|_| "local".to_string());
213 let manifest_path = if options.if_ready {
214 emit_root_release_set_manifest_if_ready(&workspace_root, &dfx_root, &network)?
215 } else {
216 Some(emit_root_release_set_manifest(
217 &workspace_root,
218 &dfx_root,
219 &network,
220 )?)
221 };
222
223 if let Some(path) = manifest_path {
224 println!("{}", path.display());
225 }
226
227 Ok(())
228}
229
230fn run_stage(options: StageOptions) -> Result<(), Box<dyn std::error::Error>> {
232 let dfx_root = dfx_root()?;
233 let network = env::var("DFX_NETWORK").unwrap_or_else(|_| "local".to_string());
234 let artifact_root = resolve_artifact_root(&dfx_root, &network)?;
235 let manifest_path = root_release_set_manifest_path(&artifact_root)?;
236 let manifest = load_root_release_set_manifest(&manifest_path)?;
237
238 stage_root_release_set(&dfx_root, &options.root_target, &manifest)?;
239 resume_root_bootstrap(&options.root_target)?;
240 Ok(())
241}
242
243const fn usage() -> &'static str {
245 "usage: canic release-set <command> [<args>]\n\ncommands:\n targets List root plus ordinary install targets from canic.toml.\n manifest Emit the current root release-set manifest from local build artifacts.\n stage Stage the current root release set and resume root bootstrap."
246}
247
248const fn targets_usage() -> &'static str {
250 "usage: canic release-set targets [--config <canic.toml>] [--root <dfx-canister-name>]"
251}
252
253const fn manifest_usage() -> &'static str {
255 "usage: canic release-set manifest [--if-ready]"
256}
257
258const fn stage_usage() -> &'static str {
260 "usage: canic release-set stage [root-canister]"
261}
262
263#[cfg(test)]
264mod tests {
265 use super::*;
266
267 #[test]
269 fn parses_targets_options() {
270 let parsed = ReleaseSetCommand::parse([
271 OsString::from("targets"),
272 OsString::from("--config"),
273 OsString::from("canisters/demo/canic.toml"),
274 OsString::from("--root"),
275 OsString::from("custom_root"),
276 ])
277 .expect("parse targets");
278
279 let ReleaseSetCommand::Targets(options) = parsed else {
280 panic!("expected targets command");
281 };
282
283 assert_eq!(
284 options.config_path,
285 Some(PathBuf::from("canisters/demo/canic.toml"))
286 );
287 assert_eq!(options.root_target, "custom_root");
288 }
289
290 #[test]
292 fn parses_manifest_options() {
293 let parsed =
294 ReleaseSetCommand::parse([OsString::from("manifest"), OsString::from("--if-ready")])
295 .expect("parse manifest");
296
297 let ReleaseSetCommand::Manifest(options) = parsed else {
298 panic!("expected manifest command");
299 };
300
301 assert!(options.if_ready);
302 }
303
304 #[test]
306 fn parses_stage_root_target() {
307 let parsed =
308 ReleaseSetCommand::parse([OsString::from("stage"), OsString::from("custom_root")])
309 .expect("parse stage");
310
311 let ReleaseSetCommand::Stage(options) = parsed else {
312 panic!("expected stage command");
313 };
314
315 assert_eq!(options.root_target, "custom_root");
316 }
317}