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}