-
Notifications
You must be signed in to change notification settings - Fork 6
feat: Project bundling #530
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
adamspofford-dfinity
wants to merge
4
commits into
main
Choose a base branch
from
spofford/app-bundle
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from 1 commit
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| use anyhow::Context as _; | ||
| use clap::Args; | ||
| use icp::context::Context; | ||
| use icp::prelude::*; | ||
|
|
||
| use crate::operations::bundle::create_bundle; | ||
|
|
||
| /// Bundle a project into a self-contained deployable archive. | ||
| /// | ||
| /// Builds all project canisters and packages them with a rewritten manifest | ||
| /// into a `.tar.gz` file. The rewritten manifest replaces all build steps | ||
| /// with pre-built steps referencing the bundled WASM files. Asset sync | ||
| /// directories are included in the archive. | ||
| /// | ||
| /// Projects with script sync steps cannot be bundled. | ||
| #[derive(Args, Debug)] | ||
| pub(crate) struct BundleArgs { | ||
| /// Output path for the bundle archive (e.g. bundle.tar.gz) | ||
| #[arg(long, short)] | ||
| pub(crate) output: PathBuf, | ||
| } | ||
|
|
||
| pub(crate) async fn exec(ctx: &Context, args: &BundleArgs) -> Result<(), anyhow::Error> { | ||
| let project = ctx.project.load().await.context("failed to load project")?; | ||
|
|
||
| let canisters: Vec<_> = project.canisters.into_values().collect(); | ||
|
|
||
| create_bundle( | ||
| &project.dir, | ||
| canisters, | ||
| ctx.builder.clone(), | ||
| ctx.artifacts.clone(), | ||
| &ctx.dirs.package_cache()?, | ||
| ctx.debug, | ||
| &args.output, | ||
| ) | ||
| .await | ||
| .context("failed to create bundle")?; | ||
|
|
||
| Ok(()) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,11 @@ | ||
| use clap::Subcommand; | ||
|
|
||
| pub(crate) mod bundle; | ||
| pub(crate) mod show; | ||
|
|
||
| /// Display information about the current project | ||
| /// Manage the current project | ||
| #[derive(Debug, Subcommand)] | ||
| pub(crate) enum Command { | ||
| Show(show::ShowArgs), | ||
| Bundle(bundle::BundleArgs), | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,249 @@ | ||
| use std::{ | ||
| fs::File, | ||
| io::{BufWriter, Cursor}, | ||
| sync::Arc, | ||
| }; | ||
|
|
||
| use sha2::{Digest, Sha256}; | ||
|
|
||
| use flate2::{Compression, write::GzEncoder}; | ||
| use icp::{ | ||
| Canister, InitArgs, | ||
| canister::build::Build, | ||
| manifest::{ | ||
| ArgsFormat, BuildStep, BuildSteps, CanisterManifest, Instructions, Item, | ||
| LoadManifestFromPathError, ManifestInitArgs, PROJECT_MANIFEST, ProjectManifest, SyncStep, | ||
| SyncSteps, assets::DirField, prebuilt, | ||
| }, | ||
| package::PackageCache, | ||
| prelude::*, | ||
| store_artifact, | ||
| }; | ||
| use snafu::{ResultExt, Snafu}; | ||
| use tar::Builder; | ||
|
|
||
| use crate::operations::build::{BuildManyError, build_many_with_progress_bar}; | ||
|
|
||
| #[derive(Debug, Snafu)] | ||
| pub enum BundleError { | ||
| #[snafu(display( | ||
| "canister '{canister}' has a script sync step, which is not supported in bundles" | ||
| ))] | ||
| ScriptSyncStep { canister: String }, | ||
|
|
||
| #[snafu(transparent)] | ||
| Build { source: BuildManyError }, | ||
|
|
||
| #[snafu(display("failed to look up built artifact for canister '{canister}'"))] | ||
| LookupArtifact { | ||
| canister: String, | ||
| source: store_artifact::LookupArtifactError, | ||
| }, | ||
|
|
||
| #[snafu(display("failed to load project manifest for bundle"))] | ||
| LoadManifest { source: LoadManifestFromPathError }, | ||
|
|
||
| #[snafu(display("failed to serialize bundle manifest"))] | ||
| SerializeManifest { source: serde_yaml::Error }, | ||
|
|
||
| #[snafu(display("failed to add '{path}' to bundle archive"))] | ||
| WriteArchiveEntry { | ||
| path: PathBuf, | ||
| source: std::io::Error, | ||
| }, | ||
|
|
||
| #[snafu(display("failed to create bundle output file at '{path}'"))] | ||
| CreateOutput { | ||
| path: PathBuf, | ||
| source: std::io::Error, | ||
| }, | ||
|
|
||
| #[snafu(display("failed to finalize bundle archive"))] | ||
| FlushArchive { source: std::io::Error }, | ||
| } | ||
|
|
||
| pub(crate) async fn create_bundle( | ||
| project_dir: &Path, | ||
| canisters: Vec<(PathBuf, Canister)>, | ||
| builder: Arc<dyn Build>, | ||
| artifacts: Arc<dyn store_artifact::Access>, | ||
| pkg_cache: &PackageCache, | ||
| debug: bool, | ||
| output: &Path, | ||
| ) -> Result<(), BundleError> { | ||
| for (_, canister) in &canisters { | ||
| for step in &canister.sync.steps { | ||
| if matches!(step, SyncStep::Script(_)) { | ||
| return ScriptSyncStepSnafu { | ||
| canister: canister.name.clone(), | ||
| } | ||
| .fail(); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| build_many_with_progress_bar( | ||
| canisters.clone(), | ||
| builder, | ||
| artifacts.clone(), | ||
| pkg_cache, | ||
| debug, | ||
| ) | ||
| .await?; | ||
|
|
||
| // Re-read the raw manifest to preserve networks and environments verbatim. | ||
| let raw_manifest: ProjectManifest = | ||
| icp::manifest::load_manifest_from_path(&project_dir.join(PROJECT_MANIFEST)) | ||
| .await | ||
| .context(LoadManifestSnafu)?; | ||
|
|
||
| let mut bundle_canisters = Vec::new(); | ||
| let mut canister_wasms: Vec<(String, Vec<u8>)> = Vec::new(); | ||
| // (canister_path, canister_name, asset_dirs) | ||
| let mut asset_dirs: Vec<(PathBuf, String, Vec<String>)> = Vec::new(); | ||
|
|
||
| for (canister_path, canister) in &canisters { | ||
| let wasm = artifacts | ||
| .lookup(&canister.name) | ||
| .await | ||
| .context(LookupArtifactSnafu { | ||
| canister: canister.name.clone(), | ||
| })?; | ||
|
|
||
| let sha256 = hex::encode(Sha256::digest(&wasm)); | ||
| let wasm_filename = format!("{}.wasm", canister.name); | ||
|
|
||
| // Collect asset dirs and rewrite their paths for the bundle. | ||
|
adamspofford-dfinity marked this conversation as resolved.
|
||
| let mut bundle_sync_steps = Vec::new(); | ||
| let mut raw_asset_dirs = Vec::new(); | ||
|
|
||
| for step in &canister.sync.steps { | ||
| match step { | ||
| SyncStep::Script(_) => unreachable!("validated above"), | ||
| SyncStep::Assets(adapter) => { | ||
| let dirs = adapter.dir.as_vec(); | ||
| raw_asset_dirs.extend(dirs.clone()); | ||
|
|
||
| let prefixed_dirs: Vec<String> = dirs | ||
| .iter() | ||
| .map(|d| format!("{}/{d}", canister.name)) | ||
| .collect(); | ||
|
|
||
| let new_dir = if prefixed_dirs.len() == 1 { | ||
| DirField::Dir(prefixed_dirs.into_iter().next().unwrap()) | ||
| } else { | ||
| DirField::Dirs(prefixed_dirs) | ||
| }; | ||
|
|
||
| bundle_sync_steps.push(SyncStep::Assets(icp::manifest::assets::Adapter { | ||
| dir: new_dir, | ||
| })); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if !raw_asset_dirs.is_empty() { | ||
| asset_dirs.push((canister_path.clone(), canister.name.clone(), raw_asset_dirs)); | ||
| } | ||
|
|
||
| let init_args = canister.init_args.as_ref().map(convert_init_args); | ||
|
|
||
| let sync = if bundle_sync_steps.is_empty() { | ||
| None | ||
| } else { | ||
| Some(SyncSteps { | ||
| steps: bundle_sync_steps, | ||
| }) | ||
| }; | ||
|
|
||
| let canister_manifest = CanisterManifest { | ||
| name: canister.name.clone(), | ||
| settings: canister.settings.clone(), | ||
| init_args, | ||
| instructions: Instructions::BuildSync { | ||
| build: BuildSteps { | ||
| steps: vec![BuildStep::Prebuilt(prebuilt::Adapter { | ||
| source: prebuilt::SourceField::Local(prebuilt::LocalSource { | ||
| path: wasm_filename.as_str().into(), | ||
| }), | ||
| sha256: Some(sha256), | ||
| })], | ||
| }, | ||
| sync, | ||
| }, | ||
| }; | ||
|
|
||
| bundle_canisters.push(Item::Manifest(canister_manifest)); | ||
| canister_wasms.push((wasm_filename, wasm)); | ||
| } | ||
|
|
||
| let bundle_manifest = ProjectManifest { | ||
| canisters: bundle_canisters, | ||
| networks: raw_manifest.networks, | ||
| environments: raw_manifest.environments, | ||
| }; | ||
|
|
||
| let manifest_yaml = serde_yaml::to_string(&bundle_manifest).context(SerializeManifestSnafu)?; | ||
|
|
||
| let file = File::create(output.as_std_path()).context(CreateOutputSnafu { | ||
| path: output.to_path_buf(), | ||
| })?; | ||
| let gz = GzEncoder::new(BufWriter::new(file), Compression::default()); | ||
| let mut archive = Builder::new(gz); | ||
|
|
||
| // icp.yaml | ||
| let manifest_bytes = manifest_yaml.as_bytes(); | ||
| let mut header = tar::Header::new_gnu(); | ||
| header.set_size(manifest_bytes.len() as u64); | ||
| header.set_mode(0o644); | ||
| header.set_cksum(); | ||
| archive | ||
| .append_data(&mut header, "icp.yaml", Cursor::new(manifest_bytes)) | ||
| .context(WriteArchiveEntrySnafu { | ||
| path: PathBuf::from("icp.yaml"), | ||
| })?; | ||
|
|
||
| // WASM files | ||
| for (filename, wasm) in &canister_wasms { | ||
| let mut header = tar::Header::new_gnu(); | ||
| header.set_size(wasm.len() as u64); | ||
| header.set_mode(0o644); | ||
| header.set_cksum(); | ||
| archive | ||
| .append_data(&mut header, filename, Cursor::new(wasm)) | ||
| .context(WriteArchiveEntrySnafu { | ||
| path: PathBuf::from(filename), | ||
| })?; | ||
| } | ||
|
|
||
| // Asset directories | ||
| for (canister_path, canister_name, dirs) in &asset_dirs { | ||
| for dir in dirs { | ||
| let src_path = canister_path.join(dir); | ||
| let archive_prefix = format!("{canister_name}/{dir}"); | ||
| archive | ||
| .append_dir_all(&archive_prefix, src_path.as_std_path()) | ||
| .context(WriteArchiveEntrySnafu { | ||
|
adamspofford-dfinity marked this conversation as resolved.
|
||
| path: PathBuf::from(&archive_prefix), | ||
| })?; | ||
| } | ||
| } | ||
|
|
||
| let gz = archive.into_inner().context(FlushArchiveSnafu)?; | ||
| gz.finish().context(FlushArchiveSnafu)?; | ||
|
|
||
| Ok(()) | ||
| } | ||
|
|
||
| fn convert_init_args(args: &InitArgs) -> ManifestInitArgs { | ||
| match args { | ||
| InitArgs::Text { content, format } => ManifestInitArgs::Value { | ||
| value: content.clone(), | ||
| format: format.clone(), | ||
| }, | ||
| InitArgs::Binary(bytes) => ManifestInitArgs::Value { | ||
| value: hex::encode(bytes), | ||
| format: ArgsFormat::Hex, | ||
| }, | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.