diff --git a/Cargo.lock b/Cargo.lock index 05d9d44518..13aaee5992 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -631,7 +631,7 @@ dependencies = [ [[package]] name = "console" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "clicolors-control 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -877,7 +877,7 @@ dependencies = [ "futures 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)", "hex 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", "ic-http-agent 0.1.0", - "indicatif 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)", + "indicatif 0.13.0 (registry+https://github.com/rust-lang/crates.io-index)", "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "libflate 0.1.27 (registry+https://github.com/rust-lang/crates.io-index)", "mockall 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1362,10 +1362,10 @@ dependencies = [ [[package]] name = "indicatif" -version = "0.12.0" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "console 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", + "console 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "number_prefix 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "regex 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -3324,7 +3324,7 @@ dependencies = [ "checksum cmake 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)" = "81fb25b677f8bf1eb325017cb6bb8452f87969db0fedb4f757b297bee78a7c62" "checksum colored 1.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "6cdb90b60f2927f8d76139c72dbde7e10c3a2bc47c8594c9c7a66529f2687c03" "checksum console 0.7.7 (registry+https://github.com/rust-lang/crates.io-index)" = "8ca57c2c14b8a2bf3105bc9d15574aad80babf6a9c44b1058034cdf8bd169628" -"checksum console 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "62828f51cfa18f8c31d3d55a43c6ce6af3f87f754cba9fbba7ff38089b9f5612" +"checksum console 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)" = "f5d540c2d34ac9dd0deb5f3b5f54c36c79efa78f6b3ad19106a554d07a7b5d9f" "checksum const-random 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "7b641a8c9867e341f3295564203b1c250eb8ce6cb6126e007941f78c4d2ed7fe" "checksum const-random-macro 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c750ec12b83377637110d5a57f5ae08e895b06c4b16e2bdbf1a94ef717428c59" "checksum constant_time_eq 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "995a44c877f9212528ccc74b21a232f66ad69001e40ede5bcee2ac9ef2657120" @@ -3400,7 +3400,7 @@ dependencies = [ "checksum idna 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "38f09e0f0b1fb55fdee1f17470ad800da77af5186a1a76c026b679358b7e844e" "checksum idna 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "02e2673c30ee86b5b96a9cb52ad15718aa1f966f5ab9ad54a8b95d5ca33120a9" "checksum indexmap 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712d7b3ea5827fcb9d4fda14bf4da5f136f0db2ae9c8f4bd4e2d1c6fde4e6db2" -"checksum indicatif 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a8d596a9576eaa1446996092642d72bfef35cf47243129b7ab883baf5faec31e" +"checksum indicatif 0.13.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8572bccfb0665e70b7faf44ee28841b8e0823450cd4ad562a76b5a3c4bf48487" "checksum iovec 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "dbe6e417e7d0975db6512b90796e8ce223145ac4e33c377e4a42882a0e88bb08" "checksum ipconfig 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "aa79fa216fbe60834a9c0737d7fcd30425b32d1c58854663e24d4c4b328ed83f" "checksum itertools 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5b8467d9c1cebe26feb08c640139247fac215782d35371ade9a2136ed6085358" diff --git a/e2e/assets/import_error_mo/main.mo b/e2e/assets/import_error_mo/main.mo new file mode 100644 index 0000000000..226741f7a4 --- /dev/null +++ b/e2e/assets/import_error_mo/main.mo @@ -0,0 +1,3 @@ +import X "canister:random"; +actor {}; + diff --git a/e2e/assets/import_error_mo/patch.bash b/e2e/assets/import_error_mo/patch.bash new file mode 100644 index 0000000000..606370d6f6 --- /dev/null +++ b/e2e/assets/import_error_mo/patch.bash @@ -0,0 +1 @@ +dfx config canisters/e2e_project/main main.mo diff --git a/e2e/assets/matrix_multiply_mo/matrix.mo b/e2e/assets/matrix_multiply_mo/matrix.mo index f5cd1285a5..02c1683032 100644 --- a/e2e/assets/matrix_multiply_mo/matrix.mo +++ b/e2e/assets/matrix_multiply_mo/matrix.mo @@ -1,6 +1,5 @@ import A "mo:stdlib/array.mo"; -import D "canister:dot_product"; -import T "canister:transpose"; +import M "secret_import.mo"; type Matrix = [[Int]]; @@ -10,14 +9,14 @@ actor { assert (a[0].len() == b.len()); let n = a.len(); let k = b[0].len(); - let bt = await T.transpose(b); + let bt = await M.T.transpose(b); let res : [[var Int]] = A.tabulate<[var Int]>(n, func (_:Nat):[var Int] = A.init(k, 0)); var i = 0; while (i < n) { - await D.init(a[i]); + await M.D.init(a[i]); var j = 0; while (j < k) { - res[i][j] := await D.dot_product_with(bt[j]); + res[i][j] := await M.D.dot_product_with(bt[j]); j += 1; }; i += 1; diff --git a/e2e/assets/matrix_multiply_mo/secret_import.mo b/e2e/assets/matrix_multiply_mo/secret_import.mo new file mode 100644 index 0000000000..8238b45354 --- /dev/null +++ b/e2e/assets/matrix_multiply_mo/secret_import.mo @@ -0,0 +1,6 @@ +import T_ "canister:transpose"; +import D_ "canister:dot_product"; +module { + public let T = T_; + public let D = D_; +}; diff --git a/e2e/assets/warning_mo/main.mo b/e2e/assets/warning_mo/main.mo new file mode 100644 index 0000000000..86103ce8a6 --- /dev/null +++ b/e2e/assets/warning_mo/main.mo @@ -0,0 +1,4 @@ +func returnsLargeTuple() : (Nat,Nat,Nat,Nat,Nat,Nat,Nat,Nat,Nat,Nat,Nat,Nat,Nat,Nat,Nat,Nat,Nat,Nat,Nat,Nat) = (1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20); +func wantsLargeTuple(1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20) = (); +wantsLargeTuple(returnsLargeTuple()); +actor {}; diff --git a/e2e/assets/warning_mo/patch.bash b/e2e/assets/warning_mo/patch.bash new file mode 100644 index 0000000000..606370d6f6 --- /dev/null +++ b/e2e/assets/warning_mo/patch.bash @@ -0,0 +1 @@ +dfx config canisters/e2e_project/main main.mo diff --git a/e2e/build.bash b/e2e/build.bash index 0b84457030..63cb7a294a 100644 --- a/e2e/build.bash +++ b/e2e/build.bash @@ -40,3 +40,15 @@ teardown() { assert_command dfx build [[ -f canisters/e2e_project/_canister.id ]] } + +@test "build outputs warning" { + install_asset warning_mo + assert_command dfx build + assert_match "warning, this pattern consuming type" +} + +@test "build fails on unknown imports" { + install_asset import_error_mo + assert_command_fail dfx build + assert_match "Cannot find canister random" +} diff --git a/src/dfx/Cargo.toml b/src/dfx/Cargo.toml index 842c32b2b6..7391795b37 100644 --- a/src/dfx/Cargo.toml +++ b/src/dfx/Cargo.toml @@ -28,7 +28,7 @@ flate2 = "1.0.11" futures = "0.1.28" hex = "0.3.2" ic-http-agent = { "path" = "../ic_http_agent" } -indicatif = "0.12.0" +indicatif = "0.13.0" lazy_static = "1.4.0" libflate = "0.1.27" mockall = "0.6.0" diff --git a/src/dfx/src/commands/build.rs b/src/dfx/src/commands/build.rs index 0f709994d9..f98a8dc268 100644 --- a/src/dfx/src/commands/build.rs +++ b/src/dfx/src/commands/build.rs @@ -8,13 +8,15 @@ use crate::lib::error::{BuildErrorKind, DfxError, DfxResult}; use crate::lib::message::UserMessage; use crate::util::assets; use clap::{App, Arg, ArgMatches, SubCommand}; +use console::Style; use ic_http_agent::CanisterId; -use indicatif::{ProgressBar, ProgressDrawTarget}; +use indicatif::{ProgressBar, ProgressDrawTarget, ProgressStyle}; use rand::{thread_rng, Rng}; use std::collections::{HashMap, HashSet}; use std::ffi::OsStr; use std::io::Read; -use std::path::Path; +use std::path::{Path, PathBuf}; +use std::process::Output; type AssetMap = HashMap; type CanisterIdMap = HashMap; @@ -63,168 +65,181 @@ fn get_asset_fn(assets: &AssetMap) -> String { ) } -/// Compile a motoko file. -#[allow(clippy::too_many_arguments)] -fn motoko_compile( - cache: &dyn Cache, - profile: Option, - content: &str, - input_path: &Path, - output_path: &Path, - idl_path: &Path, - id_map: &CanisterIdMap, - assets: &AssetMap, -) -> DfxResult { - // Invoke the compiler in debug (development) or release mode, based on the current profile: - let arg_profile = match profile { - Some(Profile::Release) => "--release", - _ => "--debug", - }; +enum BuildTarget { + Release, + Debug, + IDL, +} - let mo_rts_path = cache.get_binary_command_path("mo-rts.wasm")?; - let stdlib_path = cache.get_binary_command_path("stdlib")?; +struct MotokoParams<'a> { + build_target: BuildTarget, + idl_path: &'a Path, + idl_map: &'a CanisterIdMap, + output: &'a Path, + // The following fields will not be used by self.to_args() + // TODO move input into self.to_args once inject_code is deprecated. + input: &'a Path, + verbose: bool, + surpress_warning: bool, + inject_code: bool, +} - let mut content = content.to_string(); - // Because we don't have an AST (yet) we need to do some regex magic. - // Find `actor {` - // TODO: remove this once entire process once store assets is supported by the client. - // See https://github.com/dfinity-lab/dfinity/pull/2106 for reference. - let re = regex::Regex::new(r"\bactor\s.*?\{") - .map_err(|_| DfxError::Unknown("Could not create regex.".to_string()))?; - if let Some(actor_idx) = re.find(&content) { - let (before, after) = content.split_at(actor_idx.end()); - content = before.to_string() + get_asset_fn(assets).as_str() + after; +impl MotokoParams<'_> { + fn to_args(&self, cmd: &mut std::process::Command) { + cmd.arg("-o").arg(self.output); + match self.build_target { + BuildTarget::Release => cmd.args(&["-c", "--release"]), + BuildTarget::Debug => cmd.args(&["-c", "--debug"]), + BuildTarget::IDL => cmd.arg("--idl"), + }; + if !self.idl_map.is_empty() { + cmd.arg("--actor-idl").arg(self.idl_path); + for (name, canister_id) in self.idl_map.iter() { + cmd.args(&["--actor-alias", name, canister_id]); + } + }; } +} - let mut rng = thread_rng(); - let input_path = input_path.with_extension(format!("mo-{}", rng.gen::())); - std::fs::write(&input_path, content.as_bytes())?; +/// Compile a motoko file. +fn motoko_compile(cache: &dyn Cache, params: &MotokoParams<'_>, assets: &AssetMap) -> DfxResult { + let mut cmd = cache.get_binary_command("moc")?; - let mut alias = Vec::new(); - for (name, canister_id) in id_map.iter() { - alias.push("--actor-alias"); - alias.push(name); - alias.push(canister_id); - } + let mo_rts_path = cache.get_binary_command_path("mo-rts.wasm")?; + let stdlib_path = cache.get_binary_command_path("stdlib")?; + let input_path = if params.inject_code { + let input_path = params.input; + let mut content = std::fs::read_to_string(input_path)?; + // Because we don't have an AST (yet) we need to do some regex magic. + // Find `actor {` + // TODO: remove this once entire process once store assets is supported by the client. + // See https://github.com/dfinity-lab/dfinity/pull/2106 for reference. + let re = regex::Regex::new(r"\bactor\s.*?\{") + .map_err(|_| DfxError::Unknown("Could not create regex.".to_string()))?; + if let Some(actor_idx) = re.find(&content) { + let (before, after) = content.split_at(actor_idx.end()); + content = before.to_string() + get_asset_fn(assets).as_str() + after; + } - let process = cache - .get_binary_command("moc")? + let mut rng = thread_rng(); + let input_path = input_path.with_extension(format!("mo-{}", rng.gen::())); + std::fs::write(&input_path, content.as_bytes())?; + input_path.clone() + } else { + params.input.to_path_buf() + }; + + cmd.arg(&input_path); + params.to_args(&mut cmd); + let cmd = cmd .env("MOC_RTS", mo_rts_path.as_path()) - .arg("-c") - .arg(&input_path) - .arg(arg_profile) - .arg("-o") - .arg(&output_path) + // TODO Move packages flags into params.to_args once dfx supports custom packages .arg("--package") .arg("stdlib") - .arg(&stdlib_path.as_path()) - .arg("--actor-idl") - .arg(&idl_path) - .args(&alias) - .spawn()?; - - let output = process.wait_with_output()?; + .arg(&stdlib_path.as_path()); + run_command(cmd, params.verbose, params.surpress_warning)?; - if !output.status.success() { - Err(DfxError::BuildError(BuildErrorKind::MotokoCompilerError( - // We choose to join the strings and not the vector in case there is a weird - // incorrect character at the end of stdout. - String::from_utf8_lossy(&output.stdout).to_string(), - String::from_utf8_lossy(&output.stderr).to_string(), - ))) - } else { + if params.inject_code { std::fs::remove_file(input_path)?; - Ok(()) } + Ok(()) } -fn find_deps(cache: &dyn Cache, input_path: &Path) -> DfxResult> { - let output = cache - .get_binary_command("moc")? - .arg("--print-deps") - .arg(&input_path) - .output()?; +#[derive(Debug, PartialEq, Hash, Eq)] +enum MotokoImport { + Canister(String), + Local(PathBuf), +} - if !output.status.success() { - Err(DfxError::BuildError(BuildErrorKind::MotokoCompilerError( - String::from_utf8_lossy(&output.stdout).to_string(), - String::from_utf8_lossy(&output.stderr).to_string(), - ))) - } else { - let output = String::from_utf8_lossy(&output.stdout); - let mut deps = HashSet::new(); - for dep in output.lines() { - let prefix: Vec<_> = dep.split(':').collect(); - match prefix[0] { - "canister" => { - deps.insert(prefix[1].to_string()); - } - "ic" => (), - // TODO I should trace down local imports - "mo" => (), - _other => (), +struct MotokoImports(HashSet); + +impl MotokoImports { + pub fn get_canisters(&self) -> HashSet { + let mut res = HashSet::new(); + for dep in self.0.iter() { + if let MotokoImport::Canister(ref name) = dep { + res.insert(name.to_owned()); } } - Ok(deps) + res } } -fn didl_compile( - cache: &dyn Cache, - input_path: &Path, - output_path: &Path, - idl_path: &Path, - id_map: &CanisterIdMap, -) -> DfxResult { - let stdlib_path = cache.get_binary_command_path("stdlib")?; - - let mut alias = Vec::new(); - for (name, canister_id) in id_map.iter() { - alias.push("--actor-alias"); - alias.push(name); - alias.push(canister_id); +fn find_deps(cache: &dyn Cache, input_path: &Path, deps: &mut MotokoImports) -> DfxResult { + let import = MotokoImport::Local(input_path.to_path_buf()); + if deps.0.contains(&import) { + return Ok(()); } - - let output = cache - .get_binary_command("moc")? - .arg("--idl") - .arg(&input_path) - .arg("-o") - .arg(&output_path) - .arg("--package") - .arg("stdlib") - .arg(&stdlib_path.as_path()) - .arg("--actor-idl") - .arg(&idl_path) - .args(&alias) - .output()?; - - if !output.status.success() { - Err(DfxError::BuildError(BuildErrorKind::IdlGenerationError( - String::from_utf8_lossy(&output.stdout).to_string() - + &String::from_utf8_lossy(&output.stderr), - ))) - } else { - Ok(()) + deps.0.insert(import); + + let mut cmd = cache.get_binary_command("moc")?; + let cmd = cmd.arg("--print-deps").arg(&input_path); + let output = run_command(cmd, false, false)?; + + let output = String::from_utf8_lossy(&output.stdout); + for dep in output.lines() { + let prefix: Vec<_> = dep.split(':').collect(); + match prefix[0] { + "canister" => { + if prefix.len() != 2 { + return Err(DfxError::BuildError(BuildErrorKind::DependencyError( + format!("Illegal canister import {}", dep), + ))); + } + deps.0.insert(MotokoImport::Canister(prefix[1].to_string())); + } + // TODO trace canister id once dfx supports downloading IDL from remote canisters + "ic" => (), + // TODO trace mo package once dfx supports packages + "mo" => (), + file => { + let path = input_path + .parent() + .expect("Cannot use root.") + .join(file) + .canonicalize() + .expect("Cannot canonicalize local import file"); + if path.is_file() { + find_deps(cache, &path, deps)?; + } else { + return Err(DfxError::BuildError(BuildErrorKind::DependencyError( + format!("Cannot find import file {}", path.display()), + ))); + } + } + } } + Ok(()) } fn build_did_js(cache: &dyn Cache, input_path: &Path, output_path: &Path) -> DfxResult { - let output = cache - .get_binary_command("didc")? - .arg("--js") - .arg(&input_path) - .arg("-o") - .arg(&output_path) - .output()?; + let mut cmd = cache.get_binary_command("didc")?; + let cmd = cmd.arg("--js").arg(&input_path).arg("-o").arg(&output_path); + run_command(cmd, false, false)?; + Ok(()) +} +fn run_command( + cmd: &mut std::process::Command, + verbose: bool, + surpress_warning: bool, +) -> DfxResult { + if verbose { + println!("{:?}", cmd); + } + let output = cmd.output()?; if !output.status.success() { - Err(DfxError::BuildError(BuildErrorKind::DidJsGenerationError( - String::from_utf8_lossy(&output.stdout).to_string() - + &String::from_utf8_lossy(&output.stderr), + Err(DfxError::BuildError(BuildErrorKind::CompilerError( + format!("{:?}", cmd), + String::from_utf8_lossy(&output.stdout).to_string(), + String::from_utf8_lossy(&output.stderr).to_string(), ))) } else { - Ok(()) + if !surpress_warning && !output.stderr.is_empty() { + // Cannot use eprintln, because it would interfere with the progress bar. + println!("{}", String::from_utf8_lossy(&output.stderr)); + } + Ok(output) } } @@ -252,57 +267,6 @@ fn build_canister_js(canister_id: &CanisterId, canister_info: &CanisterInfo) -> Ok(()) } -fn build_idl( - env: &dyn Environment, - config: &Config, - name: &str, - id_map: &CanisterIdMap, -) -> DfxResult { - let canister_info = CanisterInfo::load(config, name).map_err(|_| { - BuildError(BuildErrorKind::CanisterNameIsNotInConfigError( - name.to_owned(), - )) - })?; - - let input_path = canister_info.get_main_path(); - - let idl_path = canister_info - .get_output_root() - .parent() - .unwrap() - .join("idl"); - match input_path.extension().and_then(OsStr::to_str) { - Some("mo") => { - let canister_id = canister_info - .get_canister_id() - .ok_or_else(|| DfxError::BuildError(BuildErrorKind::CouldNotReadCanisterId()))?; - let canister_id = canister_id.to_text().split_off(3); - - let output_idl_path = idl_path.join(canister_id).with_extension("did"); - - std::fs::create_dir_all(&idl_path)?; - - let cache = env.get_cache(); - didl_compile( - cache.as_ref(), - &input_path, - &output_idl_path, - &idl_path, - id_map, - )?; - Ok(()) - } - Some(ext) => Err(DfxError::BuildError(BuildErrorKind::InvalidExtension( - ext.to_owned(), - ))), - None => Err(DfxError::BuildError(BuildErrorKind::InvalidExtension( - "".to_owned(), - ))), - }?; - - Ok(()) -} - fn build_file( env: &dyn Environment, config: &Config, @@ -321,11 +285,7 @@ fn build_file( let input_path = canister_info.get_main_path(); let output_wasm_path = canister_info.get_output_wasm_path(); - let idl_path = canister_info - .get_output_root() - .parent() - .unwrap() - .join("idl"); + match input_path.extension().and_then(OsStr::to_str) { // TODO(SDK-441): Revisit supporting compilation from WAT files. Some("wat") => { @@ -340,36 +300,52 @@ fn build_file( } Some("mo") => { - let canister_id = canister_info - .get_canister_id() - .ok_or_else(|| DfxError::BuildError(BuildErrorKind::CouldNotReadCanisterId()))?; - - let output_idl_path = canister_info.get_output_idl_path(); - let output_did_js_path = canister_info.get_output_did_js_path(); - std::fs::create_dir_all(canister_info.get_output_root())?; - - let content = std::fs::read_to_string(input_path)?; let cache = env.get_cache(); - motoko_compile( - cache.as_ref(), - profile, - &content, - &input_path, - &output_wasm_path, - &idl_path, - &id_map, - assets, - )?; - didl_compile( - cache.as_ref(), - &input_path, - &output_idl_path, - &idl_path, - id_map, - )?; - build_did_js(cache.as_ref(), &output_idl_path, &output_did_js_path)?; - build_canister_js(&canister_id, &canister_info)?; + let idl_dir_path = canister_info.get_idl_dir_path(); + std::fs::create_dir_all(&idl_dir_path)?; + // Generate wasm + let params = MotokoParams { + build_target: match profile { + Some(Profile::Release) => BuildTarget::Release, + _ => BuildTarget::Debug, + }, + surpress_warning: false, + inject_code: true, + verbose: false, + input: &input_path, + output: &output_wasm_path, + idl_path: &idl_dir_path, + idl_map: &id_map, + }; + motoko_compile(cache.as_ref(), ¶ms, assets)?; + // Generate IDL + let output_idl_path = canister_info.get_output_idl_path(); + let idl_file_path = canister_info + .get_idl_file_path() + .ok_or_else(|| DfxError::BuildError(BuildErrorKind::CouldNotReadCanisterId()))?; + let params = MotokoParams { + build_target: BuildTarget::IDL, + // Surpress the warnings the second time we call moc + surpress_warning: true, + inject_code: false, + verbose: false, + input: &input_path, + output: &output_idl_path, + idl_path: &idl_dir_path, + idl_map: &id_map, + }; + motoko_compile(cache.as_ref(), ¶ms, &HashMap::new())?; + std::fs::copy(&output_idl_path, &idl_file_path)?; + // Generate JS code + if canister_info.has_frontend() { + let output_did_js_path = canister_info.get_output_did_js_path(); + let canister_id = canister_info.get_canister_id().ok_or_else(|| { + DfxError::BuildError(BuildErrorKind::CouldNotReadCanisterId()) + })?; + build_did_js(cache.as_ref(), &output_idl_path, &output_did_js_path)?; + build_canister_js(&canister_id, &canister_info)?; + } Ok(()) } @@ -385,35 +361,60 @@ fn build_file( } struct BuildSequence { - pub canisters: Vec, + canisters: Vec, seen: HashSet, deps: CanisterDependencyMap, + id_map: CanisterIdMap, } impl BuildSequence { - pub fn from(deps: CanisterDependencyMap) -> Self { - BuildSequence { + fn new(deps: CanisterDependencyMap, id_map: CanisterIdMap) -> DfxResult { + let mut res = BuildSequence { canisters: Vec::new(), seen: HashSet::new(), deps, + id_map, + }; + res.build_dependency()?; + Ok(res) + } + fn get_ids(&self, name: &str) -> CanisterIdMap { + let mut res = HashMap::new(); + // It's okay to unwrap because we have already traversed the dependency graph without errors. + let deps = self.deps.get(name).unwrap(); + for canister in deps.iter() { + let id = self.id_map.get(canister).unwrap(); + res.insert(canister.to_owned(), id.to_owned()); } + res } - pub fn build_dependency(&mut self) { + fn build_dependency(&mut self) -> DfxResult { let names: Vec<_> = self.deps.keys().cloned().collect(); for name in names { - self.dfs(&name); + self.dfs(&name)?; } + Ok(()) } - fn dfs(&mut self, canister: &str) { + fn dfs(&mut self, canister: &str) -> DfxResult { if self.seen.contains(canister) { - return; + return Ok(()); } self.seen.insert(canister.to_string()); - let deps = self.deps.get(canister).unwrap().clone(); + let deps = self + .deps + .get(canister) + .ok_or_else(|| { + DfxError::BuildError(BuildErrorKind::DependencyError(format!( + "Cannot find canister {}", + canister + ))) + })? + .clone(); for dep in deps { - self.dfs(&dep); + self.dfs(&dep)?; } self.canisters.push(canister.to_string()); + Ok(()) } } @@ -427,14 +428,16 @@ pub fn exec(env: &dyn Environment, args: &ArgMatches<'_>) -> DfxResult { // already. env.get_cache().install()?; - let build_stage_bar = ProgressBar::new_spinner(); - build_stage_bar.set_draw_target(ProgressDrawTarget::stderr()); - build_stage_bar.set_message("Building canisters..."); - build_stage_bar.enable_steady_tick(80); + let green = Style::new().green().bold(); + + let status_bar = ProgressBar::new_spinner(); + status_bar.set_draw_target(ProgressDrawTarget::stderr()); + status_bar.set_message("Building canisters..."); + status_bar.enable_steady_tick(80); let maybe_canisters = &config.get_config().canisters; if maybe_canisters.is_none() { - build_stage_bar.finish_with_message("No canisters, nothing to build."); + status_bar.finish_with_message("No canisters, nothing to build."); return Ok(()); } let canisters = maybe_canisters.as_ref().unwrap(); @@ -444,7 +447,7 @@ pub fn exec(env: &dyn Environment, args: &ArgMatches<'_>) -> DfxResult { let mut deps = HashMap::new(); for name in canisters.keys() { let canister_info = CanisterInfo::load(&config, name)?; - build_stage_bar.set_message(&format!("Building canister {}...", name)); + status_bar.set_message("Generating canister ids..."); // Write the CID. std::fs::create_dir_all( canister_info @@ -462,86 +465,62 @@ pub fn exec(env: &dyn Environment, args: &ArgMatches<'_>) -> DfxResult { id_map.insert(name.to_owned(), canister_id); let input_path = canister_info.get_main_path(); - let canister_deps = find_deps(env.get_cache().as_ref(), &input_path)?; - deps.insert(name.to_owned(), canister_deps); + let mut canister_deps = MotokoImports(HashSet::new()); + find_deps(env.get_cache().as_ref(), &input_path, &mut canister_deps)?; + deps.insert(name.to_owned(), canister_deps.get_canisters()); } // Sort dependency - let mut seq = BuildSequence::from(deps); - seq.build_dependency(); + status_bar.set_message("Analyzing build dependency..."); + let seq = BuildSequence::new(deps, id_map)?; + status_bar.finish_and_clear(); - // Build canister IDL - for name in &seq.canisters { - let canister_info = CanisterInfo::load(&config, name)?; - let idl_path = canister_info - .get_output_root() - .parent() - .expect("Cannot use root."); - let idl_path = idl_path.join("idl"); - std::fs::create_dir_all(&idl_path)?; - - match build_idl(env, &config, name, &id_map) { - Ok(()) => {} - Err(e) => { - build_stage_bar.finish_with_message(&format!( - r#"Failed to build IDL for canister "{}":"#, - name - )); - eprintln!("{:?}", e); - return Err(e); - } - } - } + let num_stages = seq.canisters.len() as u64 + 2; + let build_stage_bar = ProgressBar::new(num_stages); + build_stage_bar.set_draw_target(ProgressDrawTarget::stderr()); + build_stage_bar.set_style( + ProgressStyle::default_bar() + .template("[{wide_bar}] {pos}/{len}") + .progress_chars("=> "), + ); + build_stage_bar.enable_steady_tick(80); // Build canister for name in &seq.canisters { - match build_file(env, &config, name, &id_map, &HashMap::new()) { + build_stage_bar.println(&format!("{} canister {}", green.apply_to("Building"), name)); + match build_file(env, &config, name, &seq.get_ids(name), &HashMap::new()) { Ok(()) => {} Err(e) => { - build_stage_bar - .finish_with_message(&format!(r#"Failed to build canister "{}":"#, name)); - eprintln!("{:?}", e); + build_stage_bar.abandon(); return Err(e); } } + build_stage_bar.inc(1); } - build_stage_bar.finish_with_message("Done building canisters..."); // If there is not a package.json, we don't have a frontend and can quit early. if !config.get_project_root().join("package.json").exists() || args.is_present("skip-frontend") { + build_stage_bar.finish_and_clear(); return Ok(()); } - let build_stage_bar = ProgressBar::new_spinner(); - build_stage_bar.set_draw_target(ProgressDrawTarget::stderr()); - build_stage_bar.set_message("Building frontend..."); - build_stage_bar.enable_steady_tick(80); + build_stage_bar.println(&format!("{} frontend", green.apply_to("Building"))); - let mut process = std::process::Command::new("npm") - .arg("run") + let mut cmd = std::process::Command::new("npm"); + cmd.arg("run") .arg("build") .env("DFX_VERSION", &format!("{}", dfx_version())) .current_dir(config.get_project_root()) .stdout(std::process::Stdio::piped()) - .stderr(std::process::Stdio::piped()) - .spawn()?; - - let status = process.wait()?; - - if !status.success() { - let mut str = String::new(); - process.stderr.unwrap().read_to_string(&mut str)?; - eprintln!("NPM failed to run:\n{}", str); - return Err(DfxError::BuildError(BuildErrorKind::FrontendBuildError())); - } + .stderr(std::process::Stdio::piped()); + run_command(&mut cmd, false, false)?; - build_stage_bar.finish_with_message("Done building frontend..."); - - let build_stage_bar = ProgressBar::new_spinner(); - build_stage_bar.set_draw_target(ProgressDrawTarget::stderr()); - build_stage_bar.set_message("Bundling frontend assets in the canister..."); - build_stage_bar.enable_steady_tick(80); + build_stage_bar.inc(1); + build_stage_bar.println(&format!( + "{} frontend assets in the canister", + green.apply_to("Bundling") + )); let frontends: Vec = canisters .iter() @@ -575,16 +554,26 @@ pub fn exec(env: &dyn Environment, args: &ArgMatches<'_>) -> DfxResult { } } - match build_file(env, &config, &name, &id_map, &assets) { + match build_file(env, &config, &name, &seq.get_ids(&name), &assets) { Ok(()) => {} Err(e) => { - build_stage_bar - .finish_with_message(&format!(r#"Failed to build canister "{}":"#, name)); + build_stage_bar.abandon(); return Err(e); } } } + // Remove generated IDL files + // We don't want to simply remove the whole directory, as in the future, + // we may want to keep the IDL files downloaded from network. + for name in &seq.canisters { + let canister_info = CanisterInfo::load(&config, name)?; + let idl_file_path = canister_info + .get_idl_file_path() + .ok_or_else(|| DfxError::BuildError(BuildErrorKind::CouldNotReadCanisterId()))?; + std::fs::remove_file(idl_file_path)?; + } + build_stage_bar.finish_and_clear(); Ok(()) } @@ -630,26 +619,31 @@ mod tests { }); let input_path = temp_dir().join("file").with_extension("mo"); - - motoko_compile( - &cache, - None, - "", - &input_path, - Path::new("/out/file.wasm"), - Path::new("."), - &HashMap::new(), - &HashMap::new(), - ) - .expect("Function failed."); - didl_compile( - &cache, - Path::new("/in/file.mo"), - Path::new("/out/file.did"), - Path::new("."), - &HashMap::new(), - ) - .expect("Function failed (didl_compile)"); + fs::File::create(input_path.clone()).expect("Could not create file."); + + let params = MotokoParams { + build_target: BuildTarget::Debug, + surpress_warning: false, + inject_code: true, + verbose: false, + input: &input_path, + output: Path::new("/out/file.wasm"), + idl_path: Path::new("."), + idl_map: &HashMap::new(), + }; + motoko_compile(&cache, ¶ms, &HashMap::new()).expect("Function failed."); + + let params = MotokoParams { + build_target: BuildTarget::IDL, + surpress_warning: false, + inject_code: false, + verbose: false, + input: Path::new("/in/file.mo"), + output: Path::new("/out/file.did"), + idl_path: Path::new("."), + idl_map: &HashMap::new(), + }; + motoko_compile(&cache, ¶ms, &HashMap::new()).expect("Function failed (didl_compile)"); build_did_js( &cache, Path::new("/out/file.did"), @@ -665,8 +659,8 @@ mod tests { .expect("Could not read temp file."); let re = regex::Regex::new( - &r"moc -c .*?.mo-[0-9]+ --debug -o /out/file.wasm --package stdlib stdlib --actor-idl . - moc --idl /in/file.mo -o /out/file.did --package stdlib stdlib --actor-idl . + &r"moc .*?.mo-[0-9]+ -o /out/file.wasm -c --debug --package stdlib stdlib + moc /in/file.mo -o /out/file.did --idl --package stdlib stdlib didc --js /out/file.did -o /out/file.did.js" .replace(" ", ""), ) diff --git a/src/dfx/src/lib/canister_info.rs b/src/dfx/src/lib/canister_info.rs index fca0691117..047a3de2c8 100644 --- a/src/dfx/src/lib/canister_info.rs +++ b/src/dfx/src/lib/canister_info.rs @@ -16,6 +16,7 @@ pub struct CanisterInfo { input_path: PathBuf, output_root: PathBuf, + idl_path: PathBuf, output_wasm_path: PathBuf, output_idl_path: PathBuf, @@ -39,6 +40,7 @@ impl CanisterInfo { .get_build() .get_output("build/"), ); + let idl_path = build_root.join("idl/"); let canister_map = (&config.get_config().canisters).as_ref().ok_or_else(|| { DfxError::Unknown("No canisters in the configuration file.".to_string()) @@ -75,6 +77,7 @@ impl CanisterInfo { input_path, output_root, + idl_path, output_wasm_path, output_idl_path, output_did_js_path, @@ -112,6 +115,18 @@ impl CanisterInfo { pub fn get_output_root(&self) -> &Path { self.output_root.as_path() } + pub fn get_idl_dir_path(&self) -> &Path { + self.idl_path.as_path() + } + pub fn get_idl_file_path(&self) -> Option { + let idl_path = self.get_idl_dir_path(); + let canister_id = self.get_canister_id()?; + Some( + idl_path + .join(canister_id.to_text().split_off(3)) + .with_extension("did"), + ) + } pub fn get_canister_id_path(&self) -> &Path { self.canister_id_path.as_path() } diff --git a/src/dfx/src/lib/error/build.rs b/src/dfx/src/lib/error/build.rs index 4f524331ee..4b1e73ebb8 100644 --- a/src/dfx/src/lib/error/build.rs +++ b/src/dfx/src/lib/error/build.rs @@ -7,13 +7,10 @@ pub enum BuildErrorKind { InvalidExtension(String), /// A compiler error happened. - MotokoCompilerError(String, String), + CompilerError(String, String, String), - /// An error happened during the generation of the Idl. - IdlGenerationError(String), - - /// An error happened while generating the JS representation of the interface description. - DidJsGenerationError(String), + /// An error happened while dependency analysis. + DependencyError(String), /// An error happened while creating the JS canister bindings. CanisterJsGenerationError(String), @@ -24,9 +21,6 @@ pub enum BuildErrorKind { /// Could not find the canister to build in the config. CanisterNameIsNotInConfigError(String), - // The frontend failed. - FrontendBuildError(), - // Cannot find or read the canister ID. CouldNotReadCanisterId(), } @@ -37,17 +31,13 @@ impl fmt::Display for BuildErrorKind { match self { InvalidExtension(ext) => f.write_fmt(format_args!("Invalid extension: {}", ext)), - MotokoCompilerError(stdout, stderr) => f.write_fmt(format_args!( - "Motoko returned an error:\n{}\n{}", - stdout, stderr + CompilerError(cmd, stdout, stderr) => f.write_fmt(format_args!( + "Command {}\n returned an error:\n{}{}", + cmd, stdout, stderr )), - IdlGenerationError(stdout) => f.write_fmt(format_args!( - "IDL generation returned an error:\n{}", - stdout - )), - DidJsGenerationError(stdout) => f.write_fmt(format_args!( - "IDL to JS generation returned an error:\n{}", - stdout + DependencyError(msg) => f.write_fmt(format_args!( + "Error while performing dependency analysis: {}", + msg )), CanisterJsGenerationError(stdout) => f.write_fmt(format_args!( "Creating canister JS bindings returned an error:\n{}", @@ -60,7 +50,6 @@ impl fmt::Display for BuildErrorKind { r#"Could not find the canister named "{}" in the dfx.json configuration."#, name, )), - FrontendBuildError() => f.write_str("Frontend build stage failed."), CouldNotReadCanisterId() => f.write_str("The canister ID could not be found."), } }