holochain_cli_bundle/
cli.rs

1//! CLI definitions.
2
3use crate::error::HcBundleResult;
4use anyhow::Context;
5use clap::{Parser, Subcommand};
6use holochain_types::dna::DnaBundle;
7use holochain_types::prelude::{AppManifest, DnaManifest, ValidatedDnaManifest};
8use holochain_types::web_app::WebAppManifest;
9use holochain_util::ffs;
10use mr_bundle::{FileSystemBundler, Manifest};
11use std::path::Path;
12use std::path::PathBuf;
13
14/// The file extension to use for DNA bundles.
15pub const DNA_BUNDLE_EXT: &str = "dna";
16
17/// The file extension to use for hApp bundles.
18pub const APP_BUNDLE_EXT: &str = "happ";
19
20/// The file extension to use for Web-hApp bundles.
21pub const WEB_APP_BUNDLE_EXT: &str = "webhapp";
22
23/// Work with Holochain DNA bundles.
24#[derive(Debug, Parser)]
25#[command(version, about)]
26pub struct HcDnaBundle {
27    /// The `hc dna` subcommand to run.
28    #[command(subcommand)]
29    pub subcommand: HcDnaBundleSubcommand,
30}
31
32#[derive(Debug, Subcommand)]
33pub enum HcDnaBundleSubcommand {
34    /// Create a new, empty Holochain DNA bundle working directory and create a new
35    /// sample `dna.yaml` manifest inside.
36    Init {
37        /// The path to create the working directory.
38        path: PathBuf,
39    },
40
41    /// Pack into the `[name].dna` bundle according to the `dna.yaml` manifest,
42    /// found inside the working directory. The `[name]` is taken from the `name`
43    /// property of the manifest file.
44    ///
45    /// e.g.:
46    ///
47    /// $ hc dna pack ./some/directory/foo
48    ///
49    /// creates a file `./some/directory/foo/[name].dna`, based on
50    /// `./some/directory/foo/dna.yaml`.
51    Pack {
52        /// The path to the working directory containing a `dna.yaml` manifest.
53        path: PathBuf,
54
55        /// Specify the output path for the packed bundle file.
56        ///
57        /// If not specified, the `[name].dna` bundle will be placed inside the
58        /// provided working directory.
59        #[arg(short = 'o', long)]
60        output: Option<PathBuf>,
61    },
62
63    /// Unpack parts of the `.dna` bundle file into a specific directory.
64    ///
65    /// e.g.:
66    ///
67    /// $ hc dna unpack ./some/dir/my-dna.dna
68    ///
69    /// creates a new directory `./some/dir/my-dna`, containining a new `dna.yaml`
70    /// manifest.
71    // #[arg(short = 'u', long)]
72    Unpack {
73        /// The path to the bundle to unpack.
74        path: std::path::PathBuf,
75
76        /// Specify the directory for the unpacked content.
77        ///
78        /// If not specified, the directory will be placed alongside the
79        /// bundle file, with the same name as the bundle file name.
80        #[arg(short = 'o', long)]
81        output: Option<PathBuf>,
82
83        /// Don't attempt to parse the manifest. Useful if you have a manifest
84        /// of an outdated format. This command will allow you to unpack the
85        /// manifest so that it may be modified and repacked into a valid bundle.
86        #[arg(short = 'r', long)]
87        raw: bool,
88
89        /// Overwrite an existing directory, if one exists.
90        #[arg(short = 'f', long)]
91        force: bool,
92    },
93
94    /// Print the schema for a DNA manifest
95    Schema,
96    /// Print the Base64 hash for a DNA file
97    Hash {
98        /// The path to the dna file.
99        path: std::path::PathBuf,
100    },
101}
102
103/// Work with Holochain hApp bundles.
104#[derive(Debug, Parser)]
105#[command(version, about)]
106pub struct HcAppBundle {
107    /// The `hc app` subcommand to run.
108    #[command(subcommand)]
109    pub subcommand: HcAppBundleSubcommand,
110}
111
112#[derive(Debug, Subcommand)]
113pub enum HcAppBundleSubcommand {
114    /// Create a new, empty Holochain app (hApp) working directory and create a new
115    /// sample `happ.yaml` manifest inside.
116    Init {
117        /// The path to create the working directory.
118        path: PathBuf,
119    },
120
121    /// Pack into the `[name].happ` bundle according to the `happ.yaml` manifest,
122    /// found inside the working directory. The `[name]` is taken from the `name`
123    /// property of the manifest file.
124    ///
125    /// e.g.:
126    ///
127    /// $ hc app pack ./some/directory/foo
128    ///
129    /// creates a file `./some/directory/foo/[name].happ`, based on
130    /// `./some/directory/foo/happ.yaml`.
131    Pack {
132        /// The path to the working directory containing a `happ.yaml` manifest.
133        path: PathBuf,
134
135        /// Specify the output path for the packed bundle file.
136        ///
137        /// If not specified, the `[name].happ` bundle will be placed inside the
138        /// provided working directory.
139        #[arg(short = 'o', long)]
140        output: Option<PathBuf>,
141
142        /// Also run `dna pack` on all DNA manifests
143        /// to be bundled into this hApp.
144        /// There must exist a `dna.yaml` file in the same directory
145        /// as each of the DNA files specified in the manifest.
146        #[arg(short, long)]
147        recursive: bool,
148    },
149
150    /// Unpack parts of the `.happ` bundle file into a specific directory.
151    ///
152    /// e.g.:
153    ///
154    /// $ hc app unpack ./some/dir/my-app.happ
155    ///
156    /// creates a new directory `./some/dir/my-app`, containining a new `happ.yaml`
157    /// manifest.
158    // #[arg(short = 'u', long)]
159    Unpack {
160        /// The path to the bundle to unpack.
161        path: PathBuf,
162
163        /// Specify the directory for the unpacked content.
164        ///
165        /// If not specified, the directory will be placed alongside the
166        /// bundle file, with the same name as the bundle file name.
167        #[arg(short = 'o', long)]
168        output: Option<PathBuf>,
169
170        /// Don't attempt to parse the manifest. Useful if you have a manifest
171        /// of an outdated format. This command will allow you to unpack the
172        /// manifest so that it may be modified and repacked into a valid bundle.
173        #[arg(short = 'r', long)]
174        raw: bool,
175
176        /// Overwrite an existing directory, if one exists.
177        #[arg(short = 'f', long)]
178        force: bool,
179    },
180
181    /// Print the schema for a hApp manifest
182    Schema,
183}
184
185/// Work with Holochain web-hApp bundles.
186#[derive(Debug, Parser)]
187#[command(version, about)]
188pub struct HcWebAppBundle {
189    /// The `hc web-app` subcommand to run.
190    #[command(subcommand)]
191    pub subcommand: HcWebAppBundleSubcommand,
192}
193
194#[derive(Debug, Subcommand)]
195pub enum HcWebAppBundleSubcommand {
196    /// Create a new, empty Holochain web app working directory and create a new
197    /// sample `web-happ.yaml` manifest inside.
198    Init {
199        /// The path to create the working directory.
200        path: PathBuf,
201    },
202
203    /// Pack into the `[name].webhapp` bundle according to the `web-happ.yaml` manifest,
204    /// found inside the working directory. The `[name]` is taken from the `name`
205    /// property of the manifest file.
206    ///
207    /// e.g.:
208    ///
209    /// $ hc web-app pack ./some/directory/foo
210    ///
211    /// creates a file `./some/directory/foo/[name].webhapp`, based on
212    /// `./some/directory/foo/web-happ.yaml`.
213    Pack {
214        /// The path to the working directory containing a `web-happ.yaml` manifest.
215        path: std::path::PathBuf,
216
217        /// Specify the output path for the packed bundle file.
218        ///
219        /// If not specified, the `[name].webhapp` bundle will be placed inside the
220        /// provided working directory.
221        #[arg(short = 'o', long)]
222        output: Option<PathBuf>,
223
224        /// Also run `app pack` and `dna pack` on all app and DNA manifests
225        /// to be bundled into this hApp.
226        /// There must exist a `happ.yaml` file file in the same directory
227        /// as the hApp file specified in the manifest,
228        /// as well as `dna.yaml` files in the same directories
229        /// as each of the DNA files specified in the hApps' manifests.
230        #[arg(short, long)]
231        recursive: bool,
232    },
233
234    /// Unpack parts of the `.webhapp` bundle file into a specific directory.
235    ///
236    /// e.g.:
237    ///
238    /// $ hc web-app unpack ./some/dir/my-app.webhapp
239    ///
240    /// creates a new directory `./some/dir/my-app`, containining a new `web-happ.yaml`
241    /// manifest.
242    // #[arg(short = 'u', long)]
243    Unpack {
244        /// The path to the bundle to unpack.
245        path: std::path::PathBuf,
246
247        /// Specify the directory for the unpacked content.
248        ///
249        /// If not specified, the directory will be placed alongside the
250        /// bundle file, with the same name as the bundle file name.
251        #[arg(short = 'o', long)]
252        output: Option<PathBuf>,
253
254        /// Don't attempt to parse the manifest. Useful if you have a manifest
255        /// of an outdated format. This command will allow you to unpack the
256        /// manifest so that it may be modified and repacked into a valid bundle.
257        #[arg(short = 'r', long)]
258        raw: bool,
259
260        /// Overwrite an existing directory, if one exists.
261        #[arg(short = 'f', long)]
262        force: bool,
263    },
264
265    /// Print the schema for a web hApp manifest
266    Schema,
267}
268
269// These impls are here to make the code for the three `Hc_Bundle` subcommand wrappers
270// somewhat consistent with the main subcommand wrapper and that of `hc-sandbox`,
271// in which it's the wrapper struct that contains the `run` function.
272// The reason the `run` function is on these subcommands' sub-subcommand enums
273// is that the recursive packing functions call them directly on the variants
274// and don't want to bother instantiating a wrapper just for that.
275
276impl HcDnaBundle {
277    /// Run this subcommand, passing off all the work to the sub-sub-command enum
278    pub async fn run(self) -> anyhow::Result<()> {
279        self.subcommand.run().await
280    }
281}
282
283impl HcAppBundle {
284    /// Run this subcommand, passing off all the work to the sub-sub-command enum
285    pub async fn run(self) -> anyhow::Result<()> {
286        self.subcommand.run().await
287    }
288}
289
290impl HcWebAppBundle {
291    /// Run this subcommand, passing off all the work to the sub-sub-command enum
292    pub async fn run(self) -> anyhow::Result<()> {
293        self.subcommand.run().await
294    }
295}
296
297impl HcDnaBundleSubcommand {
298    /// Run this command
299    pub async fn run(self) -> anyhow::Result<()> {
300        match self {
301            Self::Init { path } => {
302                crate::init::init_dna(path).await?;
303            }
304            Self::Pack { path, output } => {
305                let name = get_dna_name(&path).await?;
306                let bundle_path =
307                    crate::packing::pack::<ValidatedDnaManifest>(&path, output, name).await?;
308                println!("Wrote bundle {}", bundle_path.to_string_lossy());
309            }
310            Self::Unpack {
311                path,
312                output,
313                raw,
314                force,
315            } => {
316                let dir_path = if raw {
317                    crate::packing::expand_unknown_bundle(
318                        &path,
319                        DNA_BUNDLE_EXT,
320                        ValidatedDnaManifest::file_name(),
321                        output,
322                        force,
323                    )
324                    .await?
325                } else {
326                    crate::packing::expand_bundle::<ValidatedDnaManifest>(&path, output, force)
327                        .await?
328                };
329                println!("Unpacked to directory {}", dir_path.to_string_lossy());
330            }
331            Self::Schema => {
332                let schema = schemars::schema_for!(DnaManifest);
333                let schema_string = serde_json::to_string_pretty(&schema)
334                    .context("Failed to pretty print schema")?;
335
336                println!("{schema_string}");
337            }
338            Self::Hash { path } => {
339                let bundle = FileSystemBundler::load_from::<ValidatedDnaManifest>(path)
340                    .await
341                    .map(DnaBundle::from)?;
342                let dna_hash_b64 = bundle.to_dna_file().await?.0.dna_hash().to_string();
343                println!("{dna_hash_b64}");
344            }
345        }
346        Ok(())
347    }
348}
349
350impl HcAppBundleSubcommand {
351    /// Run this command
352    pub async fn run(self) -> anyhow::Result<()> {
353        match self {
354            Self::Init { path } => {
355                crate::init::init_app(path).await?;
356            }
357            Self::Pack {
358                path,
359                output,
360                recursive,
361            } => {
362                let name = get_app_name(&path).await?;
363
364                if recursive {
365                    app_pack_recursive(&path).await?;
366                }
367
368                let bundle_path = crate::packing::pack::<AppManifest>(&path, output, name).await?;
369                println!("Wrote bundle {}", bundle_path.to_string_lossy());
370            }
371            Self::Unpack {
372                path,
373                output,
374                raw,
375                force,
376            } => {
377                let dir_path = if raw {
378                    crate::packing::expand_unknown_bundle(
379                        &path,
380                        APP_BUNDLE_EXT,
381                        AppManifest::file_name(),
382                        output,
383                        force,
384                    )
385                    .await?
386                } else {
387                    crate::packing::expand_bundle::<AppManifest>(&path, output, force).await?
388                };
389                println!("Unpacked to directory {}", dir_path.to_string_lossy());
390            }
391            Self::Schema => {
392                let schema = schemars::schema_for!(AppManifest);
393                let schema_string = serde_json::to_string_pretty(&schema)
394                    .context("Failed to pretty print schema")?;
395
396                println!("{schema_string}");
397            }
398        }
399        Ok(())
400    }
401}
402
403impl HcWebAppBundleSubcommand {
404    /// Run this command
405    pub async fn run(self) -> anyhow::Result<()> {
406        match self {
407            Self::Init { path } => {
408                crate::init::init_web_app(path).await?;
409            }
410            Self::Pack {
411                path,
412                output,
413                recursive,
414            } => {
415                let name = get_web_app_name(&path).await?;
416
417                if recursive {
418                    web_app_pack_recursive(&path).await?;
419                }
420
421                let bundle_path =
422                    crate::packing::pack::<WebAppManifest>(&path, output, name).await?;
423                println!("Wrote bundle {}", bundle_path.to_string_lossy());
424            }
425            Self::Unpack {
426                path,
427                output,
428                raw,
429                force,
430            } => {
431                let dir_path = if raw {
432                    crate::packing::expand_unknown_bundle(
433                        &path,
434                        WEB_APP_BUNDLE_EXT,
435                        WebAppManifest::file_name(),
436                        output,
437                        force,
438                    )
439                    .await?
440                } else {
441                    crate::packing::expand_bundle::<WebAppManifest>(&path, output, force).await?
442                };
443                println!("Unpacked to directory {}", dir_path.to_string_lossy());
444            }
445            Self::Schema => {
446                let schema = schemars::schema_for!(WebAppManifest);
447                let schema_string = serde_json::to_string_pretty(&schema)
448                    .context("Failed to pretty print schema")?;
449
450                println!("{schema_string}");
451            }
452        }
453        Ok(())
454    }
455}
456
457/// Load a [ValidatedDnaManifest] manifest from the given path and return its `name` field.
458pub async fn get_dna_name(manifest_path: &Path) -> HcBundleResult<String> {
459    let manifest_path = manifest_path.to_path_buf();
460    let manifest_path = manifest_path.join(ValidatedDnaManifest::file_name());
461    let manifest_yaml = ffs::read_to_string(&manifest_path).await?;
462    let manifest: DnaManifest = serde_yaml::from_str(&manifest_yaml)?;
463    Ok(manifest.name())
464}
465
466/// Load an [AppManifest] manifest from the given path and return its `app_name` field.
467pub async fn get_app_name(manifest_path: &Path) -> HcBundleResult<String> {
468    let manifest_path = manifest_path.to_path_buf();
469    let manifest_path = manifest_path.join(AppManifest::file_name());
470    let manifest_yaml = ffs::read_to_string(&manifest_path).await?;
471    let manifest: AppManifest = serde_yaml::from_str(&manifest_yaml)?;
472    Ok(manifest.app_name().to_string())
473}
474
475/// Load a [WebAppManifest] manifest from the given path and return its `app_name` field.
476pub async fn get_web_app_name(manifest_path: &Path) -> HcBundleResult<String> {
477    let manifest_path = manifest_path.to_path_buf();
478    let manifest_path = manifest_path.join(WebAppManifest::file_name());
479    let manifest_yaml = ffs::read_to_string(&manifest_path).await?;
480    let manifest: WebAppManifest = serde_yaml::from_str(&manifest_yaml)?;
481    Ok(manifest.app_name().to_string())
482}
483
484/// Pack the app's manifest and all its DNAs if their location is bundled
485pub async fn web_app_pack_recursive(web_app_workdir_path: &PathBuf) -> anyhow::Result<()> {
486    let canonical_web_app_workdir_path = ffs::canonicalize(web_app_workdir_path).await?;
487
488    let web_app_manifest_path = canonical_web_app_workdir_path.join(WebAppManifest::file_name());
489
490    let web_app_manifest: WebAppManifest =
491        serde_yaml::from_reader(std::fs::File::open(&web_app_manifest_path)?)?;
492
493    let app_bundle_location = web_app_manifest.happ_bundle_location();
494
495    // Remove the "APP_NAME.happ" portion of the path
496    let mut bundled_app_location = PathBuf::from(app_bundle_location);
497    bundled_app_location.pop();
498
499    // Join the web-app manifest location with the location of the app's workdir location
500    let app_workdir_location = PathBuf::new()
501        .join(web_app_workdir_path)
502        .join(bundled_app_location);
503
504    // Pack all the bundled DNAs and the app's manifest
505    HcAppBundleSubcommand::Pack {
506        path: ffs::canonicalize(app_workdir_location).await?,
507        output: None,
508        recursive: true,
509    }
510    .run()
511    .await?;
512
513    Ok(())
514}
515
516/// Pack all the app's DNAs if their location is bundled
517pub async fn app_pack_recursive(app_workdir_path: &PathBuf) -> anyhow::Result<()> {
518    let app_workdir_path = ffs::canonicalize(app_workdir_path).await?;
519
520    let app_manifest_path = app_workdir_path.join(AppManifest::file_name());
521    let f = std::fs::File::open(&app_manifest_path)?;
522
523    let manifest: AppManifest = serde_yaml::from_reader(f)?;
524
525    let dnas_workdir_locations =
526        bundled_dnas_workdir_locations(&app_manifest_path, &manifest).await?;
527
528    for dna_workdir_location in dnas_workdir_locations {
529        HcDnaBundleSubcommand::Pack {
530            path: dna_workdir_location,
531            output: None,
532        }
533        .run()
534        .await?;
535    }
536
537    Ok(())
538}
539
540/// Returns all the locations of the workdirs for the bundled DNAs in the given app manifest
541pub async fn bundled_dnas_workdir_locations(
542    app_manifest_path: &Path,
543    app_manifest: &AppManifest,
544) -> anyhow::Result<Vec<PathBuf>> {
545    let mut dna_locations: Vec<PathBuf> = vec![];
546
547    let mut app_workdir_location = app_manifest_path.to_path_buf();
548    app_workdir_location.pop();
549
550    for app_role in app_manifest.app_roles() {
551        if let Some(file) = app_role.dna.path {
552            let mut dna_bundle_location = PathBuf::from(file);
553
554            // Remove the "DNA_NAME.yaml" portion of the path
555            dna_bundle_location.pop();
556
557            // Join the app's workdir location with the DNA bundle location, which is relative to it
558            let dna_workdir_location = PathBuf::new()
559                .join(&app_workdir_location)
560                .join(&dna_bundle_location);
561
562            dna_locations.push(ffs::canonicalize(dna_workdir_location).await?);
563        }
564    }
565
566    Ok(dna_locations)
567}