holochain_cli_bundle 0.3.0-beta-dev.29

DNA and hApp bundling functionality for the `hc` Holochain CLI utility
#![forbid(missing_docs)]
//! Binary `hc-dna` command executable.

use clap::{Parser, Subcommand};
use holochain_types::prelude::{AppManifest, DnaManifest, ValidatedDnaManifest};
use holochain_types::web_app::WebAppManifest;
use holochain_util::ffs;
use mr_bundle::{Location, Manifest};
use std::path::Path;
use std::path::PathBuf;

use crate::error::HcBundleResult;

/// The file extension to use for DNA bundles.
pub const DNA_BUNDLE_EXT: &str = "dna";

/// The file extension to use for hApp bundles.
pub const APP_BUNDLE_EXT: &str = "happ";

/// The file extension to use for Web-hApp bundles.
pub const WEB_APP_BUNDLE_EXT: &str = "webhapp";

/// Work with Holochain DNA bundles.
#[derive(Debug, Parser)]
#[command(version, about)]
pub struct HcDnaBundle {
    /// The `hc dna` subcommand to run.
    #[command(subcommand)]
    pub subcommand: HcDnaBundleSubcommand,
}

#[derive(Debug, Subcommand)]
pub enum HcDnaBundleSubcommand {
    /// Create a new, empty Holochain DNA bundle working directory and create a new
    /// sample `dna.yaml` manifest inside.
    Init {
        /// The path to create the working directory.
        path: PathBuf,
    },

    /// Pack into the `[name].dna` bundle according to the `dna.yaml` manifest,
    /// found inside the working directory. The `[name]` is taken from the `name`
    /// property of the manifest file.
    ///
    /// e.g.:
    ///
    /// $ hc dna pack ./some/directory/foo
    ///
    /// creates a file `./some/directory/foo/[name].dna`, based on
    /// `./some/directory/foo/dna.yaml`.
    Pack {
        /// The path to the working directory containing a `dna.yaml` manifest.
        path: std::path::PathBuf,

        /// Specify the output path for the packed bundle file.
        ///
        /// If not specified, the `[name].dna` bundle will be placed inside the
        /// provided working directory.
        #[arg(short = 'o', long)]
        output: Option<PathBuf>,

        /// Output shared object "dylib" files
        /// that can be used to run this happ on iOS
        #[arg(long)]
        dylib_ios: bool,
    },

    /// Unpack parts of the `.dna` bundle file into a specific directory.
    ///
    /// e.g.:
    ///
    /// $ hc dna unpack ./some/dir/my-dna.dna
    ///
    /// creates a new directory `./some/dir/my-dna`, containining a new `dna.yaml`
    /// manifest.
    // #[arg(short = 'u', long)]
    Unpack {
        /// The path to the bundle to unpack.
        path: std::path::PathBuf,

        /// Specify the directory for the unpacked content.
        ///
        /// If not specified, the directory will be placed alongside the
        /// bundle file, with the same name as the bundle file name.
        #[arg(short = 'o', long)]
        output: Option<PathBuf>,

        /// Don't attempt to parse the manifest. Useful if you have a manifest
        /// of an outdated format. This command will allow you to unpack the
        /// manifest so that it may be modified and repacked into a valid bundle.
        #[arg(short = 'r', long)]
        raw: bool,

        /// Overwrite an existing directory, if one exists.
        #[arg(short = 'f', long)]
        force: bool,
    },

    /// Print the schema for a DNA manifest
    Schema,
}

/// Work with Holochain hApp bundles.
#[derive(Debug, Parser)]
#[command(version, about)]
pub struct HcAppBundle {
    /// The `hc app` subcommand to run.
    #[command(subcommand)]
    pub subcommand: HcAppBundleSubcommand,
}

#[derive(Debug, Subcommand)]
pub enum HcAppBundleSubcommand {
    /// Create a new, empty Holochain app (hApp) working directory and create a new
    /// sample `happ.yaml` manifest inside.
    Init {
        /// The path to create the working directory.
        path: PathBuf,
    },

    /// Pack into the `[name].happ` bundle according to the `happ.yaml` manifest,
    /// found inside the working directory. The `[name]` is taken from the `name`
    /// property of the manifest file.
    ///
    /// e.g.:
    ///
    /// $ hc app pack ./some/directory/foo
    ///
    /// creates a file `./some/directory/foo/[name].happ`, based on
    /// `./some/directory/foo/happ.yaml`.
    Pack {
        /// The path to the working directory containing a `happ.yaml` manifest.
        path: std::path::PathBuf,

        /// Specify the output path for the packed bundle file.
        ///
        /// If not specified, the `[name].happ` bundle will be placed inside the
        /// provided working directory.
        #[arg(short = 'o', long)]
        output: Option<PathBuf>,

        /// Also run `dna pack` on all DNA manifests
        /// to be bundled into this hApp.
        /// There must exist a `dna.yaml` file in the same directory
        /// as each of the DNA files specified in the manifest.
        #[arg(short, long)]
        recursive: bool,
    },

    /// Unpack parts of the `.happ` bundle file into a specific directory.
    ///
    /// e.g.:
    ///
    /// $ hc app unpack ./some/dir/my-app.happ
    ///
    /// creates a new directory `./some/dir/my-app`, containining a new `happ.yaml`
    /// manifest.
    // #[arg(short = 'u', long)]
    Unpack {
        /// The path to the bundle to unpack.
        path: std::path::PathBuf,

        /// Specify the directory for the unpacked content.
        ///
        /// If not specified, the directory will be placed alongside the
        /// bundle file, with the same name as the bundle file name.
        #[arg(short = 'o', long)]
        output: Option<PathBuf>,

        /// Don't attempt to parse the manifest. Useful if you have a manifest
        /// of an outdated format. This command will allow you to unpack the
        /// manifest so that it may be modified and repacked into a valid bundle.
        #[arg(short = 'r', long)]
        raw: bool,

        /// Overwrite an existing directory, if one exists.
        #[arg(short = 'f', long)]
        force: bool,
    },

    /// Print the schema for a hApp manifest
    Schema,
}

/// Work with Holochain web-hApp bundles.
#[derive(Debug, Parser)]
#[command(version, about)]
pub struct HcWebAppBundle {
    /// The `hc web-app` subcommand to run.
    #[command(subcommand)]
    pub subcommand: HcWebAppBundleSubcommand,
}

#[derive(Debug, Subcommand)]
pub enum HcWebAppBundleSubcommand {
    /// Create a new, empty Holochain web app working directory and create a new
    /// sample `web-happ.yaml` manifest inside.
    Init {
        /// The path to create the working directory.
        path: PathBuf,
    },

    /// Pack into the `[name].webhapp` bundle according to the `web-happ.yaml` manifest,
    /// found inside the working directory. The `[name]` is taken from the `name`
    /// property of the manifest file.
    ///
    /// e.g.:
    ///
    /// $ hc web-app pack ./some/directory/foo
    ///
    /// creates a file `./some/directory/foo/[name].webhapp`, based on
    /// `./some/directory/foo/web-happ.yaml`.
    Pack {
        /// The path to the working directory containing a `web-happ.yaml` manifest.
        path: std::path::PathBuf,

        /// Specify the output path for the packed bundle file.
        ///
        /// If not specified, the `[name].webhapp` bundle will be placed inside the
        /// provided working directory.
        #[arg(short = 'o', long)]
        output: Option<PathBuf>,

        /// Also run `app pack` and `dna pack` on all app and DNA manifests
        /// to be bundled into this hApp.
        /// There must exist a `happ.yaml` file file in the same directory
        /// as the hApp file specified in the manifest,
        /// as well as `dna.yaml` files in the same directories
        /// as each of the DNA files specified in the hApps' manifests.
        #[arg(short, long)]
        recursive: bool,
    },

    /// Unpack parts of the `.webhapp` bundle file into a specific directory.
    ///
    /// e.g.:
    ///
    /// $ hc web-app unpack ./some/dir/my-app.webhapp
    ///
    /// creates a new directory `./some/dir/my-app`, containining a new `web-happ.yaml`
    /// manifest.
    // #[arg(short = 'u', long)]
    Unpack {
        /// The path to the bundle to unpack.
        path: std::path::PathBuf,

        /// Specify the directory for the unpacked content.
        ///
        /// If not specified, the directory will be placed alongside the
        /// bundle file, with the same name as the bundle file name.
        #[arg(short = 'o', long)]
        output: Option<PathBuf>,

        /// Don't attempt to parse the manifest. Useful if you have a manifest
        /// of an outdated format. This command will allow you to unpack the
        /// manifest so that it may be modified and repacked into a valid bundle.
        #[arg(short = 'r', long)]
        raw: bool,

        /// Overwrite an existing directory, if one exists.
        #[arg(short = 'f', long)]
        force: bool,
    },

    /// Print the schema for a web hApp manifest
    Schema,
}

// These impls are here to make the code for the three `Hc_Bundle` subcommand wrappers
// somewhat consistent with the main subcommand wrapper and that of `hc-sandbox`,
// in which it's the wrapper struct that contains the `run` function.
// The reason the `run` function is on these subcommands' sub-subcommand enums
// is that the recursive packing functions call them directly on the variants
// and don't want to bother instantiating a wrapper just for that.

impl HcDnaBundle {
    /// Run this subcommand, passing off all the work to the sub-sub-command enum
    pub async fn run(self) -> anyhow::Result<()> {
        self.subcommand.run().await
    }
}

impl HcAppBundle {
    /// Run this subcommand, passing off all the work to the sub-sub-command enum
    pub async fn run(self) -> anyhow::Result<()> {
        self.subcommand.run().await
    }
}

impl HcWebAppBundle {
    /// Run this subcommand, passing off all the work to the sub-sub-command enum
    pub async fn run(self) -> anyhow::Result<()> {
        self.subcommand.run().await
    }
}

impl HcDnaBundleSubcommand {
    /// Run this command
    pub async fn run(self) -> anyhow::Result<()> {
        match self {
            Self::Init { path } => {
                crate::init::init_dna(path).await?;
            }
            Self::Pack {
                path,
                output,
                dylib_ios,
            } => {
                let name = get_dna_name(&path).await?;
                let (bundle_path, _) =
                    crate::packing::pack::<ValidatedDnaManifest>(&path, output, name, dylib_ios)
                        .await?;
                println!("Wrote bundle {}", bundle_path.to_string_lossy());
            }
            Self::Unpack {
                path,
                output,
                raw,
                force,
            } => {
                let dir_path = if raw {
                    crate::packing::unpack_raw(
                        DNA_BUNDLE_EXT,
                        &path,
                        output,
                        ValidatedDnaManifest::path().as_ref(),
                        force,
                    )
                    .await?
                } else {
                    crate::packing::unpack::<ValidatedDnaManifest>(
                        DNA_BUNDLE_EXT,
                        &path,
                        output,
                        force,
                    )
                    .await?
                };
                println!("Unpacked to directory {}", dir_path.to_string_lossy());
            }
            Self::Schema => {
                println!("{}", include_str!("../schema/dna-manifest.schema.json"));
            }
        }
        Ok(())
    }
}

impl HcAppBundleSubcommand {
    /// Run this command
    pub async fn run(self) -> anyhow::Result<()> {
        match self {
            Self::Init { path } => {
                crate::init::init_app(path).await?;
            }
            Self::Pack {
                path,
                output,
                recursive,
            } => {
                let name = get_app_name(&path).await?;

                if recursive {
                    app_pack_recursive(&path).await?;
                }

                let (bundle_path, _) =
                    crate::packing::pack::<AppManifest>(&path, output, name, false).await?;
                println!("Wrote bundle {}", bundle_path.to_string_lossy());
            }
            Self::Unpack {
                path,
                output,
                raw,
                force,
            } => {
                let dir_path = if raw {
                    crate::packing::unpack_raw(
                        APP_BUNDLE_EXT,
                        &path,
                        output,
                        AppManifest::path().as_ref(),
                        force,
                    )
                    .await?
                } else {
                    crate::packing::unpack::<AppManifest>(APP_BUNDLE_EXT, &path, output, force)
                        .await?
                };
                println!("Unpacked to directory {}", dir_path.to_string_lossy());
            }
            Self::Schema => {
                println!("{}", include_str!("../schema/happ-manifest.schema.json"));
            }
        }
        Ok(())
    }
}

impl HcWebAppBundleSubcommand {
    /// Run this command
    pub async fn run(self) -> anyhow::Result<()> {
        match self {
            Self::Init { path } => {
                crate::init::init_web_app(path).await?;
            }
            Self::Pack {
                path,
                output,
                recursive,
            } => {
                let name = get_web_app_name(&path).await?;

                if recursive {
                    web_app_pack_recursive(&path).await?;
                }

                let (bundle_path, _) =
                    crate::packing::pack::<WebAppManifest>(&path, output, name, false).await?;
                println!("Wrote bundle {}", bundle_path.to_string_lossy());
            }
            Self::Unpack {
                path,
                output,
                raw,
                force,
            } => {
                let dir_path = if raw {
                    crate::packing::unpack_raw(
                        WEB_APP_BUNDLE_EXT,
                        &path,
                        output,
                        WebAppManifest::path().as_ref(),
                        force,
                    )
                    .await?
                } else {
                    crate::packing::unpack::<WebAppManifest>(
                        WEB_APP_BUNDLE_EXT,
                        &path,
                        output,
                        force,
                    )
                    .await?
                };
                println!("Unpacked to directory {}", dir_path.to_string_lossy());
            }
            Self::Schema => {
                println!(
                    "{}",
                    include_str!("../schema/web-happ-manifest.schema.json")
                );
            }
        }
        Ok(())
    }
}

/// Load a [ValidatedDnaManifest] manifest from the given path and return its `name` field.
pub async fn get_dna_name(manifest_path: &Path) -> HcBundleResult<String> {
    let manifest_path = manifest_path.to_path_buf();
    let manifest_path = manifest_path.join(ValidatedDnaManifest::path());
    let manifest_yaml = ffs::read_to_string(&manifest_path).await?;
    let manifest: DnaManifest = serde_yaml::from_str(&manifest_yaml)?;
    Ok(manifest.name())
}

/// Load an [AppManifest] manifest from the given path and return its `app_name` field.
pub async fn get_app_name(manifest_path: &Path) -> HcBundleResult<String> {
    let manifest_path = manifest_path.to_path_buf();
    let manifest_path = manifest_path.join(AppManifest::path());
    let manifest_yaml = ffs::read_to_string(&manifest_path).await?;
    let manifest: AppManifest = serde_yaml::from_str(&manifest_yaml)?;
    Ok(manifest.app_name().to_string())
}

/// Load a [WebAppManifest] manifest from the given path and return its `app_name` field.
pub async fn get_web_app_name(manifest_path: &Path) -> HcBundleResult<String> {
    let manifest_path = manifest_path.to_path_buf();
    let manifest_path = manifest_path.join(WebAppManifest::path());
    let manifest_yaml = ffs::read_to_string(&manifest_path).await?;
    let manifest: WebAppManifest = serde_yaml::from_str(&manifest_yaml)?;
    Ok(manifest.app_name().to_string())
}

/// Pack the app's manifest and all its DNAs if their location is bundled
pub async fn web_app_pack_recursive(web_app_workdir_path: &PathBuf) -> anyhow::Result<()> {
    let canonical_web_app_workdir_path = ffs::canonicalize(web_app_workdir_path).await?;

    let web_app_manifest_path = canonical_web_app_workdir_path.join(WebAppManifest::path());

    let web_app_manifest: WebAppManifest =
        serde_yaml::from_reader(std::fs::File::open(&web_app_manifest_path)?)?;

    let app_bundle_location = web_app_manifest.happ_bundle_location();

    if let Location::Bundled(mut bundled_app_location) = app_bundle_location {
        // Remove the "APP_NAME.happ" portion of the path
        bundled_app_location.pop();

        // Join the web-app manifest location with the location of the app's workdir location
        let app_workdir_location = PathBuf::new()
            .join(web_app_workdir_path)
            .join(bundled_app_location);

        // Pack all the bundled DNAs and the app's manifest
        HcAppBundleSubcommand::Pack {
            path: ffs::canonicalize(app_workdir_location).await?,
            output: None,
            recursive: true,
        }
        .run()
        .await?;
    }

    Ok(())
}

/// Pack all the app's DNAs if their location is bundled
pub async fn app_pack_recursive(app_workdir_path: &PathBuf) -> anyhow::Result<()> {
    let app_workdir_path = ffs::canonicalize(app_workdir_path).await?;

    let app_manifest_path = app_workdir_path.join(AppManifest::path());
    let f = std::fs::File::open(&app_manifest_path)?;

    let manifest: AppManifest = serde_yaml::from_reader(f)?;

    let dnas_workdir_locations =
        bundled_dnas_workdir_locations(&app_manifest_path, &manifest).await?;

    for dna_workdir_location in dnas_workdir_locations {
        HcDnaBundleSubcommand::Pack {
            path: dna_workdir_location,
            output: None,
            dylib_ios: false,
        }
        .run()
        .await?;
    }

    Ok(())
}

/// Returns all the locations of the workdirs for the bundled DNAs in the given app manifest
pub async fn bundled_dnas_workdir_locations(
    app_manifest_path: &Path,
    app_manifest: &AppManifest,
) -> anyhow::Result<Vec<PathBuf>> {
    let mut dna_locations: Vec<PathBuf> = vec![];

    let mut app_workdir_location = app_manifest_path.to_path_buf();
    app_workdir_location.pop();

    for app_role in app_manifest.app_roles() {
        if let Some(Location::Bundled(mut dna_bundle_location)) = app_role.dna.location {
            // Remove the "DNA_NAME.yaml" portion of the path
            dna_bundle_location.pop();

            // Join the app's workdir location with the DNA bundle location, which is relative to it
            let dna_workdir_location = PathBuf::new()
                .join(&app_workdir_location)
                .join(&dna_bundle_location);

            dna_locations.push(ffs::canonicalize(dna_workdir_location).await?);
        }
    }

    Ok(dna_locations)
}