Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 49 additions & 42 deletions dfx/src/commands/build.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::config::dfinity::Profile;
use crate::config::dfinity::{Config, Profile};
use crate::lib::canister_info::CanisterInfo;
use crate::lib::env::{BinaryResolverEnv, ProjectConfigEnv};
use crate::lib::error::{BuildErrorKind, DfxError, DfxResult};
use crate::lib::message::UserMessage;
Expand All @@ -15,9 +16,9 @@ pub fn construct() -> App<'static, 'static> {
/// Compile a motoko file.
fn motoko_compile<T: BinaryResolverEnv>(
env: &T,
profile: Option<Profile>,
input_path: &Path,
output_path: &Path,
profile: Option<Profile>,
) -> DfxResult {
// Invoke the compiler in debug (development) or release mode, based on the current profile:
let arg_profile = match profile {
Expand Down Expand Up @@ -101,36 +102,45 @@ fn build_user_lib<T: BinaryResolverEnv>(
}
}

fn build_file<T>(
env: &T,
profile: Option<Profile>,
input_path: &Path,
output_path: &Path,
) -> DfxResult
fn build_file<T>(env: &T, config: &Config, name: &str) -> DfxResult
where
T: BinaryResolverEnv,
{
let output_wasm_path = output_path.with_extension("wasm");
let canister_info = CanisterInfo::load(config, name)?;
let config = config.get_config();
let profile = config.profile.clone();
let input_path = canister_info.get_main_path();

let output_wasm_path = canister_info.get_output_wasm_path();
match input_path.extension().and_then(OsStr::to_str) {
// TODO(SDK-441): Revisit supporting compilation from WAT files.
Some("wat") => {
let wat = std::fs::read(input_path)?;
let wasm = wabt::wat2wasm(wat)
.map_err(|e| DfxError::BuildError(BuildErrorKind::WatCompileError(e)))?;

std::fs::create_dir_all(canister_info.get_output_root())?;
std::fs::write(&output_wasm_path, wasm)?;

// Write the CID.
canister_info.create_canister_id()?;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Could we rename the function rather than rely on a comment? I haven't looked yet but I assume that create_canister_id mutates canister_info or writes to dfx.json or both.

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.

It writes to build/<project_name>/canister.id. I'll refactor that.


Ok(())
}

Some("mo") => {
let output_idl_path = output_path.with_extension("did");
let output_js_path = output_path.with_extension("js");
let output_idl_path = canister_info.get_output_idl_path();
let output_js_path = canister_info.get_output_js_path();

motoko_compile(env, &input_path, &output_wasm_path, profile)?;
std::fs::create_dir_all(canister_info.get_output_root())?;

motoko_compile(env, profile, &input_path, &output_wasm_path)?;
didl_compile(env, &input_path, &output_idl_path)?;
build_user_lib(env, &output_idl_path, &output_js_path)?;

// Write the CID.
canister_info.create_canister_id()?;

Ok(())
}
Some(ext) => Err(DfxError::BuildError(BuildErrorKind::InvalidExtension(
Expand All @@ -153,34 +163,10 @@ where
.get_config()
.ok_or(DfxError::CommandMustBeRunInAProject)?;

// get_path() returns the full path of the config file. We need to get the dirname.
let project_root = config.get_path().parent().unwrap();

let build_root = project_root.join(
config
.get_config()
.get_defaults()
.get_build()
.get_output("build/"),
);

if let Some(canisters) = &config.get_config().canisters {
for (k, v) in canisters {
for k in canisters.keys() {
println!("Building {}...", k);
if let Some(path) = &v.main {
let path = Path::new(&path);
let input_as_path = project_root.join(path);
let path = path.strip_prefix("src/").unwrap_or(&path);
let output_path = build_root.join(path).with_extension("wasm");
std::fs::create_dir_all(output_path.parent().unwrap())?;

build_file(
env,
config.config.profile.clone(),
&input_as_path,
&output_path,
)?;
}
build_file(env, &config, &k)?;
}
}

Expand Down Expand Up @@ -234,13 +220,17 @@ mod tests {
out_file: &out_file,
};

build_file(
motoko_compile(
&env,
None,
Path::new("/in/file.mo"),
Path::new("/out/file.wasm"),
)
.expect("Function failed.");
didl_compile(&env, Path::new("/in/file.mo"), Path::new("/out/file.did"))
.expect("Function failed");
build_user_lib(&env, Path::new("/out/file.did"), Path::new("/out/file.js"))
.expect("Function failed");

out_file.flush().expect("Could not flush.");

Expand Down Expand Up @@ -280,14 +270,31 @@ mod tests {
let temp_dir = tempfile::tempdir().unwrap();
let temp_path = temp_dir.into_path();
let input_path = temp_path.join("input.wat");
let output_path = temp_path.join("output.wasm");
let output_path = temp_path.join("out/name/input.wasm");

assert!(!output_path.exists());

std::fs::write(input_path.as_path(), wat).expect("Could not create input.");
build_file(&env, None, input_path.as_path(), output_path.as_path())
.expect("Function failed.");
let config = Config::from_str_and_path(
temp_path.join("dfx.json"),
r#"
{
"canisters": {
"name": {
"main": "input.wat"
}
},
"defaults": {
"build": {
"output": "out/"
}
}
}
"#,
)
.unwrap();

build_file(&env, &config, "name").expect("Function failed - build_file");
assert!(output_path.exists());
}
}
31 changes: 17 additions & 14 deletions dfx/src/commands/canister/call.rs
Original file line number Diff line number Diff line change
@@ -1,23 +1,22 @@
use crate::lib::api_client::{call, request_status, QueryResponseReply, ReadResponse};
use crate::lib::env::ClientEnv;
use crate::lib::canister_info::CanisterInfo;
use crate::lib::env::{ClientEnv, ProjectConfigEnv};
use crate::lib::error::{DfxError, DfxResult};
use crate::lib::message::UserMessage;
use crate::util::clap::validators;
use crate::util::print_idl_blob;
use clap::{App, Arg, ArgMatches, SubCommand};
use ic_http_agent::{Blob, CanisterId};
use ic_http_agent::Blob;
use serde_idl::{Encode, IDLArgs};
use tokio::runtime::Runtime;

pub fn construct() -> App<'static, 'static> {
SubCommand::with_name("call")
.about(UserMessage::CallCanister.to_str())
.arg(
Arg::with_name("deployment_id")
Arg::with_name("canister_name")
.takes_value(true)
.help(UserMessage::DeploymentId.to_str())
.required(true)
.validator(validators::is_canister_id),
.help(UserMessage::CanisterName.to_str())
.required(true),
)
.arg(
Arg::with_name("method_name")
Expand Down Expand Up @@ -47,14 +46,18 @@ pub fn construct() -> App<'static, 'static> {

pub fn exec<T>(env: &T, args: &ArgMatches<'_>) -> DfxResult
where
T: ClientEnv,
T: ClientEnv + ProjectConfigEnv,
{
// Read the config.
let canister_id = args
.value_of("deployment_id")
.unwrap()
.parse::<CanisterId>()
.map_err(|e| DfxError::InvalidArgument(format!("Invalid deployment ID: {}", e)))?;
let config = env
.get_config()
.ok_or(DfxError::CommandMustBeRunInAProject)?;

let canister_name = args.value_of("canister_name").unwrap();
let canister_info = CanisterInfo::load(config, canister_name)?;
let canister_id = canister_info.get_canister_id().ok_or_else(|| {
DfxError::CannotFindBuildOutputForCanister(canister_info.get_name().to_owned())
})?;

let method_name = args.value_of("method_name").unwrap();
let arguments: Option<&str> = args.value_of("argument");
let arg_type: Option<&str> = args.value_of("type");
Expand Down
131 changes: 78 additions & 53 deletions dfx/src/commands/canister/install.rs
Original file line number Diff line number Diff line change
@@ -1,35 +1,80 @@
use crate::lib::api_client::{install_code, request_status, QueryResponseReply, ReadResponse};
use crate::lib::api_client::{
install_code, request_status, Client, QueryResponseReply, ReadResponse,
};
use crate::lib::canister_info::CanisterInfo;
use crate::lib::env::{ClientEnv, ProjectConfigEnv};
use crate::lib::error::{DfxError, DfxResult};
use crate::lib::message::UserMessage;
use crate::util::clap::validators;
use crate::util::print_idl_blob;
use clap::{App, Arg, ArgMatches, SubCommand};
use ic_http_agent::{Blob, CanisterId};
use std::path::PathBuf;
use ic_http_agent::{Blob, RequestId};
use tokio::runtime::Runtime;

pub fn construct() -> App<'static, 'static> {
SubCommand::with_name("install")
.about(UserMessage::InstallCanister.to_str())
.arg(
Arg::with_name("deployment_id")
Arg::with_name("canister_name")
.takes_value(true)
.help(UserMessage::SetDeploymentId.to_str())
.required(true)
.validator(validators::is_canister_id),
.help(UserMessage::CanisterName.to_str())
.required(false),
)
.arg(
Arg::with_name("async")
.help(UserMessage::AsyncResult.to_str())
.long("async")
.takes_value(false),
)
.arg(
Arg::with_name("wasm")
.help(UserMessage::WasmFile.to_str())
.required(true),
)
}

pub fn install_canister(client: &Client, canister_info: &CanisterInfo) -> DfxResult<RequestId> {
let canister_id = canister_info.get_canister_id().ok_or_else(|| {
DfxError::CannotFindBuildOutputForCanister(canister_info.get_name().to_owned())
})?;

eprintln!(
"Installing {} with ID {}...",
canister_info.get_name(),
canister_id.to_hex(),
);

let wasm_path = canister_info.get_output_wasm_path();
let wasm = std::fs::read(wasm_path)?;

let install = install_code(client.clone(), canister_id, Blob::from(wasm), None);

let mut runtime = Runtime::new().expect("Unable to create a runtime");
let request_id = runtime.block_on(install)?;

Ok(request_id)
}

pub fn wait_on_request_status(client: &Client, request_id: RequestId) -> DfxResult {
let request_status = request_status(client.clone(), request_id);
let mut runtime = Runtime::new().expect("Unable to create a runtime");

match runtime.block_on(request_status) {
Ok(ReadResponse::Pending) => {
eprintln!("Pending");
println!("0x{}", String::from(request_id));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can we do something like this?

eprint!("Request ID: ");
println!("0x{}", String::from(request_id));

I'm hoping this would read Request ID: 0x...

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Same with other places we print the ID.

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.

Sure. Note that this code hasn't been changed (copy-pasted) and we should most definitely rewrite it all when we move the client API to the http lib.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Question: Should we pipe things to different stdout and stderr? Why don't we use the same (stderr)?

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.

STDOUT is scriptable. STDERR is human readable. This is a simplification, but in essence that's what it is. Like in our scripts we use STDOUT of a command to use request-status.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for the clarification!

Ok(())
}
Ok(ReadResponse::Replied { reply }) => {
if let Some(QueryResponseReply { arg: blob }) = reply {
print_idl_blob(&blob)
.map_err(|e| DfxError::InvalidData(format!("Invalid IDL blob: {}", e)))?;
}
Ok(())
}
Ok(ReadResponse::Rejected {
reject_code,
reject_message,
}) => Err(DfxError::ClientError(reject_code, reject_message)),
// TODO(SDK-446): remove this matcher when moving api_client to ic_http_agent.
// `install` cannot return Unknown.
Ok(ReadResponse::Unknown) => Err(DfxError::Unknown("Unknown response".to_owned())),
Err(x) => Err(x),
}
}

pub fn exec<T>(env: &T, args: &ArgMatches<'_>) -> DfxResult
Expand All @@ -41,51 +86,31 @@ where
.get_config()
.ok_or(DfxError::CommandMustBeRunInAProject)?;

let project_root = config.get_path().parent().unwrap();

let canister_id = args
.value_of("deployment_id")
.unwrap()
.parse::<CanisterId>()
.map_err(|e| DfxError::InvalidArgument(format!("Invalid deployment ID: {}", e)))?;
let wasm_path = args.value_of("wasm").unwrap();
let wasm_path = PathBuf::from(project_root).join(wasm_path);

let wasm = std::fs::read(wasm_path)?;
let client = env.get_client();
if let Some(canister_name) = args.value_of("canister_name") {
let canister_info = CanisterInfo::load(config, canister_name)?;
let request_id = install_canister(&client, &canister_info)?;

let install = install_code(client, canister_id, Blob(wasm), None);

let mut runtime = Runtime::new().expect("Unable to create a runtime");
let request_id = runtime.block_on(install)?;

if args.is_present("async") {
println!("0x{}", String::from(request_id));
Ok(())
if args.is_present("async") {
println!("0x{}", String::from(request_id));
Ok(())
} else {
wait_on_request_status(&client, request_id)
}
} else {
let request_status = request_status(env.get_client(), request_id);
let mut runtime = Runtime::new().expect("Unable to create a runtime");
match runtime.block_on(request_status) {
Ok(ReadResponse::Pending) => {
eprintln!("Pending");
println!("0x{}", String::from(request_id));
Ok(())
}
Ok(ReadResponse::Replied { reply }) => {
if let Some(QueryResponseReply { arg: blob }) = reply {
print_idl_blob(&blob)
.map_err(|e| DfxError::InvalidData(format!("Invalid IDL blob: {}", e)))?;
// Install all canisters.
if let Some(canisters) = &config.get_config().canisters {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think we should consider showing an error saying that a canister name or an --all flag is required. Installing all canisters when a name is omitted seems like an easy way to overwrite canisters by accident.

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.

Done.

for canister_name in canisters.keys() {
let canister_info = CanisterInfo::load(config, canister_name)?;
let request_id = install_canister(&client, &canister_info)?;

if args.is_present("async") {
println!("0x{}", String::from(request_id));
} else {
wait_on_request_status(&client, request_id)?;
}
Ok(())
}
Ok(ReadResponse::Rejected {
reject_code,
reject_message,
}) => Err(DfxError::ClientError(reject_code, reject_message)),
// TODO(SDK-446): remove this matcher when moving api_client to ic_http_agent.
// `install` cannot return Unknown.
Ok(ReadResponse::Unknown) => Err(DfxError::Unknown("Unknown response".to_owned())),
Err(x) => Err(x),
}
Ok(())
}
}
Loading