Skip to main content

canic_cli/release_set/
mod.rs

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///
20/// ReleaseSetCommandError
21///
22
23#[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///
33/// ReleaseSetCommand
34///
35
36#[derive(Clone, Debug, Eq, PartialEq)]
37enum ReleaseSetCommand {
38    Targets(TargetsOptions),
39    Manifest(ManifestOptions),
40    Stage(StageOptions),
41}
42
43///
44/// TargetsOptions
45///
46
47#[derive(Clone, Debug, Eq, PartialEq)]
48struct TargetsOptions {
49    config_path: Option<PathBuf>,
50    root_target: String,
51}
52
53///
54/// ManifestOptions
55///
56
57#[derive(Clone, Debug, Eq, PartialEq)]
58struct ManifestOptions {
59    if_ready: bool,
60}
61
62///
63/// StageOptions
64///
65
66#[derive(Clone, Debug, Eq, PartialEq)]
67struct StageOptions {
68    root_target: String,
69}
70
71/// Run the release-set command family.
72pub 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    // Parse the selected release-set subcommand.
96    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    // Parse install-target listing options.
116    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    // Parse root release-set manifest emission options.
132    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    // Parse root release-set staging options.
146    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
160// Parse one release-set subcommand option set.
161fn 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
172// Build the install-target parser.
173fn 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
180// Build the manifest emission parser.
181fn manifest_command() -> ClapCommand {
182    ClapCommand::new("manifest")
183        .disable_help_flag(true)
184        .arg(flag_arg("if-ready").long("if-ready"))
185}
186
187// Build the release staging parser.
188fn stage_command() -> ClapCommand {
189    ClapCommand::new("stage")
190        .disable_help_flag(true)
191        .arg(value_arg("root-canister"))
192}
193
194// Print configured install targets in the order the install flow uses.
195fn 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
208// Emit the root release-set manifest from current build artifacts.
209fn 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
230// Stage the current root release set and resume root bootstrap.
231fn 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
243// Return release-set command family usage.
244const 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
248// Return release-set target listing usage.
249const fn targets_usage() -> &'static str {
250    "usage: canic release-set targets [--config <canic.toml>] [--root <dfx-canister-name>]"
251}
252
253// Return release-set manifest usage.
254const fn manifest_usage() -> &'static str {
255    "usage: canic release-set manifest [--if-ready]"
256}
257
258// Return release-set stage usage.
259const 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    // Ensure target listing options preserve config and root inputs.
268    #[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    // Ensure manifest emission accepts the readiness gate flag.
291    #[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    // Ensure stage accepts an explicit root target.
305    #[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}