Skip to content
Merged
156 changes: 89 additions & 67 deletions crates/icp-cli/src/commands/build/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::collections::HashMap;

use anyhow::Context as _;
use anyhow::{Context as _, anyhow};
use camino_tempfile::tempdir;
use clap::Parser;
use futures::{StreamExt, stream::FuturesOrdered};
Expand All @@ -9,10 +9,7 @@ use icp::{
fs::read,
};

use crate::{
commands::Context,
progress::{ProgressManager, ScriptProgressHandler},
};
use crate::{commands::Context, progress::ProgressManager};

#[derive(Parser, Debug)]
pub struct Cmd {
Expand All @@ -37,6 +34,9 @@ pub enum CommandError {
#[error("failed to store build artifact")]
ArtifactStore,

#[error("failed to join build output")]
JoinError(#[from] tokio::task::JoinError),

#[error(transparent)]
Unexpected(#[from] anyhow::Error),
}
Expand Down Expand Up @@ -77,83 +77,105 @@ pub async fn exec(ctx: &Context, cmd: Cmd) -> Result<(), CommandError> {
// Iterate through each resolved canister and trigger its build process.
for (_, (canister_path, c)) in cs {
// Create progress bar with standard configuration
let pb = progress_manager.create_progress_bar(&c.name);
let mut pb = progress_manager.create_multi_step_progress_bar(&c.name, "Build");

// Create an async closure that handles the build process for this specific canister
let build_fn = {
let fut = {
let c = c.clone();
let pb = pb.clone();

async move {
// Create a temporary directory for build artifacts
let build_dir =
tempdir().context("failed to create a temporary build directory")?;

// Prepare a path for our output wasm
let wasm_output_path = build_dir.path().join("out.wasm");

let step_count = c.build.steps.len();
for (i, step) in c.build.steps.iter().enumerate() {
// Indicate to user the current step being executed
let current_step = i + 1;
let pb_hdr = format!("Building: {step} {current_step} of {step_count}");

let script_handler = ScriptProgressHandler::new(pb.clone(), pb_hdr.clone());

// Setup script progress handling and receiver join handle
let (tx, rx) = script_handler.setup_output_handler();

// Perform build step
ctx.builder
.build(
step, // step
&Params {
path: canister_path.to_owned(),
output: wasm_output_path.to_owned(),
},
Some(tx),
)
.await?;

// Ensure background receiver drains all messages
let _ = rx.await;
// Define the build logic
let build_result = async {
// Create a temporary directory for build artifacts
let build_dir =
tempdir().context("failed to create a temporary build directory")?;

// Prepare a path for our output wasm
let wasm_output_path = build_dir.path().join("out.wasm");

let step_count = c.build.steps.len();
for (i, step) in c.build.steps.iter().enumerate() {
// Indicate to user the current step being executed
let current_step = i + 1;
let pb_hdr = format!("\nBuilding: {step} {current_step} of {step_count}");
let tx = pb.begin_step(pb_hdr);

// Perform build step
let build_result = ctx
.builder
.build(
step, // step
&Params {
path: canister_path.to_owned(),
output: wasm_output_path.to_owned(),
},
Some(tx),
)
.await;

// Ensure background receiver drains all messages
pb.end_step().await;

if let Err(e) = build_result {
return Err(CommandError::Build(e));
}
}

// Verify a file exists in the wasm output path
if !wasm_output_path.exists() {
return Err(CommandError::MissingOutput);
}

// Load wasm output
let wasm = read(&wasm_output_path).context(CommandError::ReadOutput)?;

// TODO(or.ricon): Verify wasm output is valid wasm (consider using wasmparser)

// Save the wasm artifact
ctx.artifacts
.save(&c.name, &wasm)
.context(CommandError::ArtifactStore)?;

Ok::<_, CommandError>(())
}

// Verify a file exists in the wasm output path
if !wasm_output_path.exists() {
return Err(CommandError::MissingOutput);
.await;

// Execute with progress tracking for final state
let result = ProgressManager::execute_with_progress(
&pb,
async { build_result },
|| "Built successfully".to_string(),
|err| format!("Failed to build canister: {err}"),
)
.await;

// After progress bar is finished, dump the output if build failed
if let Err(e) = &result {
pb.dump_output(ctx);
let _ = ctx
.term
.write_line(&format!("Failed to build canister: {e}"));
Comment on lines +152 to +157
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My gut feeling says there's still some way for the output to get messed up because of progress bars, but I don't see anything specific.

}

// Load wasm output
let wasm = read(&wasm_output_path).context(CommandError::ReadOutput)?;

// TODO(or.ricon): Verify wasm output is valid wasm (consider using wasmparser)

// Save the wasm artifact
ctx.artifacts
.save(&c.name, &wasm)
.context(CommandError::ArtifactStore)?;

Ok::<_, CommandError>(())
result
}
};

futs.push_back(async move {
// Execute the build function with progress tracking
ProgressManager::execute_with_progress(
pb,
build_fn,
|| "Built successfully".to_string(),
|err| format!("Failed to build canister: {err}"),
)
.await
});
futs.push_back(fut);
}

// Consume the set of futures and abort if an error occurs
let mut found_error = false;
while let Some(res) = futs.next().await {
// TODO(or.ricon): Handle canister build failures
res?;
if res.is_err() {
found_error = true;
}
}

if found_error {
return Err(CommandError::Unexpected(anyhow!(
"One or more canisters failed to build"
)));
}

Ok(())
Expand Down
2 changes: 1 addition & 1 deletion crates/icp-cli/src/commands/canister/binding_env_vars.rs
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ pub async fn exec(ctx: &Context, cmd: Cmd) -> Result<(), CommandError> {
futs.push_back(async move {
// Execute the install function with progress tracking
ProgressManager::execute_with_progress(
pb,
&pb,
settings_fn,
|| "Environment variables updated successfully".to_string(),
|err| format!("Failed to update environment variables: {err}"),
Expand Down
2 changes: 1 addition & 1 deletion crates/icp-cli/src/commands/canister/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,7 @@ pub async fn exec(ctx: &Context, cmd: Cmd) -> Result<(), CommandError> {
futs.push_back(async move {
// Execute the create function with custom progress tracking
let mut result = ProgressManager::execute_with_custom_progress(
pb,
&pb,
create_fn,
|| "Created successfully".to_string(),
|err| match err {
Expand Down
2 changes: 1 addition & 1 deletion crates/icp-cli/src/commands/canister/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ pub async fn exec(ctx: &Context, cmd: Cmd) -> Result<(), CommandError> {
futs.push_back(async move {
// Execute the install function with progress tracking
ProgressManager::execute_with_progress(
pb,
&pb,
install_fn,
|| "Installed successfully".to_string(),
|err| format!("Failed to install canister: {err}"),
Expand Down
111 changes: 69 additions & 42 deletions crates/icp-cli/src/commands/sync/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use std::collections::HashMap;

use anyhow::anyhow;
use clap::Parser;
use futures::{StreamExt, stream::FuturesOrdered};
use icp::{
Expand All @@ -11,7 +12,7 @@ use icp::{
use crate::{
commands::Context,
options::{EnvironmentOpt, IdentityOpt},
progress::{ProgressManager, ScriptProgressHandler},
progress::ProgressManager,
store_id::{Key, LookupError},
};

Expand Down Expand Up @@ -130,7 +131,7 @@ pub async fn exec(ctx: &Context, cmd: Cmd) -> Result<(), CommandError> {
// Iterate through each resolved canister and trigger its sync process.
for (_, (canister_path, c)) in cs {
// Create progress bar with standard configuration
let pb = progress_manager.create_progress_bar(&c.name);
let mut pb = progress_manager.create_multi_step_progress_bar(&c.name, "Sync");

// Get canister principal ID
let cid = ctx.ids.lookup(&Key {
Expand All @@ -140,57 +141,83 @@ pub async fn exec(ctx: &Context, cmd: Cmd) -> Result<(), CommandError> {
})?;

// Create an async closure that handles the sync process for this specific canister
let sync_fn = {
let pb = pb.clone();
let fut = {
let agent = agent.clone();
let c = c.clone();

async move {
for step in &c.sync.steps {
// Indicate to user the current step being executed
let pb_hdr = format!("Syncing: {step}");

let script_handler = ScriptProgressHandler::new(pb.clone(), pb_hdr.clone());

// Setup script progress handling and receiver join handle
let (tx, rx) = script_handler.setup_output_handler();

// Execute step
ctx.syncer
.sync(
step, // step
&Params {
path: canister_path.to_owned(),
cid: cid.to_owned(),
},
&agent,
Some(tx),
)
.await?;

// Ensure background receiver drains all messages
let _ = rx.await;
// Define the sync logic
let sync_result = async {
let step_count = c.sync.steps.len();
for (i, step) in c.sync.steps.iter().enumerate() {
// Indicate to user the current step being executed
let current_step = i + 1;
let pb_hdr = format!("\nSyncing: {step} {current_step} of {step_count}");

let tx = pb.begin_step(pb_hdr);

// Execute step
let sync_result = ctx
.syncer
.sync(
step, // step
&Params {
path: canister_path.to_owned(),
cid: cid.to_owned(),
},
&agent,
Some(tx),
)
.await;

// Ensure background receiver drains all messages
pb.end_step().await;

if let Err(e) = sync_result {
return Err(CommandError::Synchronize(e));
}
}

Ok::<_, CommandError>(())
}
.await;

// Execute with progress tracking for final state
let result = ProgressManager::execute_with_progress(
&pb,
async { sync_result },
|| format!("Synced successfully: {cid}"),
|err| format!("Failed to sync canister: {err}"),
)
.await;

// After progress bar is finished, dump the output if sync failed
if let Err(e) = &result {
pb.dump_output(ctx);
let _ = ctx
.term
.write_line(&format!("Failed to sync canister: {e}"));
}

Ok::<_, CommandError>(())
result
}
};

futs.push_back(async move {
// Execute the sync function with progress tracking
ProgressManager::execute_with_progress(
pb,
sync_fn,
|| format!("Synced successfully: {cid}"),
|err| format!("Failed to sync canister: {err}"),
)
.await
});
futs.push_back(fut);
}

// Consume the set of futures and abort if an error occurs
// Consume the set of futures and collect errors
let mut found_error = false;
while let Some(res) = futs.next().await {
// TODO(or.ricon): Handle canister sync failures
res?;
if res.is_err() {
found_error = true;
}
}

if found_error {
return Err(CommandError::Synchronize(SynchronizeError::Unexpected(
anyhow!("One or more canisters failed to sync"),
)));
}

Ok(())
Expand Down
Loading
Loading