From d15c4f2c6e98e470e1784fc7c2dbed5b0bf8391d Mon Sep 17 00:00:00 2001 From: Eric Swanson <64809312+ericswanson-dfinity@users.noreply.github.com> Date: Tue, 24 Sep 2024 15:31:31 -0600 Subject: [PATCH 1/5] refactor: direct_or_shell_command (#3924) --- src/dfx/src/lib/builders/mod.rs | 18 +++--------------- src/dfx/src/util/command.rs | 21 +++++++++++++++++++++ src/dfx/src/util/mod.rs | 1 + 3 files changed, 25 insertions(+), 15 deletions(-) create mode 100644 src/dfx/src/util/command.rs diff --git a/src/dfx/src/lib/builders/mod.rs b/src/dfx/src/lib/builders/mod.rs index 3e9ae991ed..2ba74ec65f 100644 --- a/src/dfx/src/lib/builders/mod.rs +++ b/src/dfx/src/lib/builders/mod.rs @@ -18,7 +18,7 @@ use std::fmt::Write; use std::fs; use std::io::Read; use std::path::{Path, PathBuf}; -use std::process::{Command, Stdio}; +use std::process::Stdio; use std::sync::Arc; mod assets; @@ -27,6 +27,7 @@ mod motoko; mod pull; mod rust; +use crate::util::command::direct_or_shell_command; pub use custom::custom_download; #[derive(Debug)] @@ -336,20 +337,7 @@ pub fn execute_command( if command.is_empty() { return Ok(vec![]); } - let words = shell_words::split(command) - .with_context(|| format!("Cannot parse command '{}'.", command))?; - let canonical_result = dfx_core::fs::canonicalize(&cwd.join(&words[0])); - let mut cmd = if words.len() == 1 && canonical_result.is_ok() { - // If the command is a file, execute it directly. - let file = canonical_result.unwrap(); - Command::new(file) - } else { - // Execute the command in `sh -c` to allow pipes. - let mut sh_cmd = Command::new("sh"); - sh_cmd.args(["-c", command]); - sh_cmd - }; - cmd.current_dir(cwd); + let mut cmd = direct_or_shell_command(command, cwd)?; if !catch_output { cmd.stdin(Stdio::inherit()) diff --git a/src/dfx/src/util/command.rs b/src/dfx/src/util/command.rs new file mode 100644 index 0000000000..dabcdf61cb --- /dev/null +++ b/src/dfx/src/util/command.rs @@ -0,0 +1,21 @@ +use crate::lib::error::DfxResult; +use anyhow::Context; +use std::path::Path; +use std::process::Command; + +pub fn direct_or_shell_command(s: &str, cwd: &Path) -> DfxResult { + let words = shell_words::split(s).with_context(|| format!("Cannot parse command '{}'.", s))?; + let canonical_result = dfx_core::fs::canonicalize(&cwd.join(&words[0])); + let mut cmd = if words.len() == 1 && canonical_result.is_ok() { + // If the command is a file, execute it directly. + let file = canonical_result.unwrap(); + Command::new(file) + } else { + // Execute the command in `sh -c` to allow pipes. + let mut sh_cmd = Command::new("sh"); + sh_cmd.args(["-c", s]); + sh_cmd + }; + cmd.current_dir(cwd); + Ok(cmd) +} diff --git a/src/dfx/src/util/mod.rs b/src/dfx/src/util/mod.rs index c7543e0ad4..073c4c5e14 100644 --- a/src/dfx/src/util/mod.rs +++ b/src/dfx/src/util/mod.rs @@ -24,6 +24,7 @@ use std::time::Duration; pub mod assets; pub mod clap; +pub mod command; pub mod currency_conversion; pub mod stderr_wrapper; pub mod url; From 1179330979f6dc5b386fc694436a4f57fea2efaa Mon Sep 17 00:00:00 2001 From: DFINITY bot <58022693+dfinity-bot@users.noreply.github.com> Date: Wed, 25 Sep 2024 11:08:46 +0200 Subject: [PATCH 2/5] chore: update Motoko version to 0.13.0 (#3925) --- CHANGELOG.md | 4 ++++ e2e/tests-dfx/upgrade_check.bash | 2 +- nix/sources.json | 18 +++++++++--------- src/dfx/assets/dfx-asset-sources.toml | 16 ++++++++-------- 4 files changed, 22 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ed94b1a3e..a0ed6aeed9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ ## Dependencies +### Motoko + +Updated Motoko to [0.13.0](https://github.com/dfinity/motoko/releases/tag/0.13.0) + ### Replica Updated replica to elected commit 179973553248415fc85679d853b48b0e0ec231c6. diff --git a/e2e/tests-dfx/upgrade_check.bash b/e2e/tests-dfx/upgrade_check.bash index e4ba03f680..eb8d89e4e2 100644 --- a/e2e/tests-dfx/upgrade_check.bash +++ b/e2e/tests-dfx/upgrade_check.bash @@ -87,7 +87,7 @@ teardown() { jq '.canisters.hello_backend.main="v5.mo"' dfx.json | sponge dfx.json echo yes | ( assert_command dfx deploy - assert_match "Stable interface compatibility check issued an ERROR" + assert_match "Stable interface compatibility check issued a WARNING" ) assert_command dfx canister call hello_backend read '()' assert_match "(0 : int)" diff --git a/nix/sources.json b/nix/sources.json index 80ce442402..512a89ee9f 100644 --- a/nix/sources.json +++ b/nix/sources.json @@ -130,27 +130,27 @@ "builtin": false, "description": "The Motoko base library", "owner": "dfinity", - "sha256": "1ilqh5f8y5dwf7vx9g627bw6vprg42z9qm8cl24kdcaakqvb7p8l", + "sha256": "02nf25bm21rmyh425arh0srb52bgsyrbqmnmy7r8zd1lfd6jnpfc", "type": "tarball", - "url": "https://github.com/dfinity/motoko/releases/download/0.12.1/motoko-base-library.tar.gz", + "url": "https://github.com/dfinity/motoko/releases/download/0.13.0/motoko-base-library.tar.gz", "url_template": "https://github.com/dfinity/motoko/releases/download//motoko-base-library.tar.gz", - "version": "0.12.1" + "version": "0.13.0" }, "motoko-x86_64-darwin": { "builtin": false, - "sha256": "0yjzfavxfllmg1dnrbmm1aw3l9kk10dxsa8jy5c284iwz6ka3ks1", + "sha256": "1bknwvhh44ysdr1xg3x856x528b55752yn6x808lwjyi1yysnqcc", "type": "file", - "url": "https://github.com/dfinity/motoko/releases/download/0.12.1/motoko-Darwin-x86_64-0.12.1.tar.gz", + "url": "https://github.com/dfinity/motoko/releases/download/0.13.0/motoko-Darwin-x86_64-0.13.0.tar.gz", "url_template": "https://github.com/dfinity/motoko/releases/download//motoko-Darwin-x86_64-.tar.gz", - "version": "0.12.1" + "version": "0.13.0" }, "motoko-x86_64-linux": { "builtin": false, - "sha256": "1qmyrnirwgrafgqdwv4bw5803gzinvqa7mfk7qnlmmhgm5csjp8y", + "sha256": "1njn9cyalgj33imysblbbpxpqp0fvadckk5b8wsw92l67fksp0mf", "type": "file", - "url": "https://github.com/dfinity/motoko/releases/download/0.12.1/motoko-Linux-x86_64-0.12.1.tar.gz", + "url": "https://github.com/dfinity/motoko/releases/download/0.13.0/motoko-Linux-x86_64-0.13.0.tar.gz", "url_template": "https://github.com/dfinity/motoko/releases/download//motoko-Linux-x86_64-.tar.gz", - "version": "0.12.1" + "version": "0.13.0" }, "pocket-ic-x86_64-darwin": { "rev": "179973553248415fc85679d853b48b0e0ec231c6", diff --git a/src/dfx/assets/dfx-asset-sources.toml b/src/dfx/assets/dfx-asset-sources.toml index b38e3d2383..944a26ba4e 100644 --- a/src/dfx/assets/dfx-asset-sources.toml +++ b/src/dfx/assets/dfx-asset-sources.toml @@ -22,8 +22,8 @@ url = 'https://download.dfinity.systems/ic/179973553248415fc85679d853b48b0e0ec23 sha256 = 'c46651590e9b4b01403d7620856b9c7ea2aa8f60ec5eed0cb6a4113bed01ad5c' [x86_64-darwin.motoko] -url = 'https://github.com/dfinity/motoko/releases/download/0.12.1/motoko-Darwin-x86_64-0.12.1.tar.gz' -sha256 = '41cfa1a6f93c122458f11229dd1b0873263ab80ab5ae6c5b789552d7b7725f7a' +url = 'https://github.com/dfinity/motoko/releases/download/0.13.0/motoko-Darwin-x86_64-0.13.0.tar.gz' +sha256 = '8c61abbd0fd14b4e1140dd582fca29652151ba29a88fd7436eda1302e1e676ae' # The replica, canister_sandbox and compiler_sandbox binaries must have the same revision. [x86_64-darwin.replica] @@ -53,8 +53,8 @@ url = 'https://download.dfinity.systems/ic/179973553248415fc85679d853b48b0e0ec23 sha256 = '65a73e56616b65e55ab526f4c03235a82604ff902e086473953fa038c940020a' [x86_64-darwin.motoko-base] -url = 'https://github.com/dfinity/motoko/releases/download/0.12.1/motoko-base-library.tar.gz' -sha256 = '21a9c67658033a9cfafae2b9b1a601b5536e460621ede835f879a1c76eaa0a45' +url = 'https://github.com/dfinity/motoko/releases/download/0.13.0/motoko-base-library.tar.gz' +sha256 = 'fcefaf998b0ddb75100f46cc23ac8a384ef4d7b26374a38f353055b2c8fa3fb6' [x86_64-darwin.ic-btc-canister] url = 'https://github.com/dfinity/bitcoin-canister/releases/download/release%2F2023-10-13/ic-btc-canister.wasm.gz' @@ -81,8 +81,8 @@ url = 'https://download.dfinity.systems/ic/179973553248415fc85679d853b48b0e0ec23 sha256 = 'd03ab6254f5a90917a06e60c5c848db18dc9abadb940ecd6f5d71caabd76774d' [x86_64-linux.motoko] -url = 'https://github.com/dfinity/motoko/releases/download/0.12.1/motoko-Linux-x86_64-0.12.1.tar.gz' -sha256 = '1e5da959a90fd64a2d3ed3d5a3f0b6f1bf0150e18b6cdef0732a3f9ea3cdbee2' +url = 'https://github.com/dfinity/motoko/releases/download/0.13.0/motoko-Linux-x86_64-0.13.0.tar.gz' +sha256 = 'ae82aba73b868ac43547abccc99ada0e5c7cfb5d8b2eed6b1c433eaa3c4b56da' # The replica, canister_sandbox and compiler_sandbox binaries must have the same revision. [x86_64-linux.replica] @@ -112,8 +112,8 @@ url = 'https://download.dfinity.systems/ic/179973553248415fc85679d853b48b0e0ec23 sha256 = 'a0f883537a23a14df3b1f21987e089a0884dbc226f818bc2cc2e3df028042ad8' [x86_64-linux.motoko-base] -url = 'https://github.com/dfinity/motoko/releases/download/0.12.1/motoko-base-library.tar.gz' -sha256 = '21a9c67658033a9cfafae2b9b1a601b5536e460621ede835f879a1c76eaa0a45' +url = 'https://github.com/dfinity/motoko/releases/download/0.13.0/motoko-base-library.tar.gz' +sha256 = 'fcefaf998b0ddb75100f46cc23ac8a384ef4d7b26374a38f353055b2c8fa3fb6' [x86_64-linux.ic-btc-canister] url = 'https://github.com/dfinity/bitcoin-canister/releases/download/release%2F2023-10-13/ic-btc-canister.wasm.gz' From 07bd312bff18fd41fb4f8ba9065181c35516db15 Mon Sep 17 00:00:00 2001 From: Gabor Greif Date: Wed, 25 Sep 2024 23:29:40 +0300 Subject: [PATCH 3/5] chore: explain `dfxvm` in the `README.md` (#3922) --- README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ea58610a73..57f258f374 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ The `IC SDK` installation script installs several components in default location | Component | Description | Default location | |--------------|----------------------------------------------------------------------------------------------------|-----------------------------------------------| | dfx | Command-line interface (CLI) | `/usr/local/bin/dfx` | +| dfxvm | Command-line interface, version manager | `/usr/local/bin/dfxvm` | | moc | Motoko runtime compiler | `~/.cache/dfinity/versions//moc` | | replica | Internet Computer local network binary | `~/.cache/dfinity/versions//replica` | | uninstall.sh | Script to remove the SDK and all of its components | `~/.cache/dfinity/uninstall.sh` | @@ -28,8 +29,10 @@ There are a few components above worth expanding on: 1. **dfx** - `dfx` is the command-line interface for the `IC SDK`. This is why many commands for the IC SDK start with the command "`dfx ..`" such as `dfx new` or `dfx stop`. -2. **Canister Development Kit (CDK)** - A CDK is an adapter used by the IC SDK so a programming language has the features needed to create and manage canisters. -The IC SDK comes with a few CDKs already installed for you so you can use them in the language of yoru choice. That is why there is a [Rust CDK](https://github.com/dfinity/cdk-rs), [Python CDK](https://demergent-labs.github.io/kybra/), +2. **dfxvm** - `dfxvm` is the version manager for `dfx`, i.e. a CLI for selecting and managing installed `dfx` versions. + +3. **Canister Development Kit (CDK)** - A CDK is an adapter used by the IC SDK so a programming language has the features needed to create and manage canisters. +The IC SDK comes with a few CDKs already installed for you so you can use them in the language of your choice. That is why there is a [Rust CDK](https://github.com/dfinity/cdk-rs), [Python CDK](https://demergent-labs.github.io/kybra/), [TypeScript CDK](https://demergent-labs.github.io/azle/), etc... Since CDKs are components used the SDK, some developer choose to use the CDK directly (without the `IC SDK`), but typically are used as part of the whole `IC SDK`. From bc98b43a05d4ac1c2977fbe151593c3d12e96c8d Mon Sep 17 00:00:00 2001 From: Adam Spofford <93943719+adamspofford-dfinity@users.noreply.github.com> Date: Thu, 26 Sep 2024 09:05:09 -0700 Subject: [PATCH 4/5] fix: Fix cdylib crate name logic (#3926) --- src/dfx/src/lib/canister_info/rust.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/dfx/src/lib/canister_info/rust.rs b/src/dfx/src/lib/canister_info/rust.rs index 82a9e753cc..a3b7266c26 100644 --- a/src/dfx/src/lib/canister_info/rust.rs +++ b/src/dfx/src/lib/canister_info/rust.rs @@ -63,7 +63,10 @@ impl CanisterInfoFactory for RustCanisterInfo { (format!("crate `{package}`"), package.clone()) }; let mut candidate_targets = package_info.targets.iter().filter(|x| { - x.name == crate_name && x.crate_types.iter().any(|c| c == "cdylib" || c == "bin") + x.crate_types.iter().any(|c| { + (c == "cdylib" && x.name == crate_name.replace('-', "_")) + || (c == "bin" && x.name == crate_name) + }) }); let Some(target) = candidate_targets.next() else { if let Some(wrong_type_crate) = From ba7bc4f61b6635ef3007b7e9793a2cc2d57721bb Mon Sep 17 00:00:00 2001 From: Eric Swanson <64809312+ericswanson-dfinity@users.noreply.github.com> Date: Fri, 27 Sep 2024 01:41:32 -0600 Subject: [PATCH 5/5] refactor: project template properties (#3927) --- src/dfx-core/src/config/project_templates.rs | 23 +- src/dfx/src/commands/new.rs | 233 +++++++++++++------ src/dfx/src/lib/builders/mod.rs | 2 +- src/dfx/src/lib/project/templates.rs | 126 ++++++---- 4 files changed, 256 insertions(+), 128 deletions(-) diff --git a/src/dfx-core/src/config/project_templates.rs b/src/dfx-core/src/config/project_templates.rs index 6dbe27e6b8..28c5565131 100644 --- a/src/dfx-core/src/config/project_templates.rs +++ b/src/dfx-core/src/config/project_templates.rs @@ -1,5 +1,6 @@ use itertools::Itertools; use std::collections::BTreeMap; +use std::fmt::Display; use std::io; use std::sync::OnceLock; @@ -17,11 +18,18 @@ pub enum Category { Frontend, FrontendTest, Extra, + Support, } #[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] pub struct ProjectTemplateName(pub String); +impl Display for ProjectTemplateName { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + #[derive(Debug, Clone)] pub struct ProjectTemplate { /// The name of the template as specified on the command line, @@ -38,14 +46,17 @@ pub struct ProjectTemplate { /// as well as for interactive selection pub category: Category, - /// If true, run `cargo update` after creating the project - pub update_cargo_lockfile: bool, + /// Other project templates to patch in alongside this one + pub requirements: Vec, + + /// Run a command after adding the canister to dfx.json + pub post_create: Vec, - /// If true, patch in the any_js template files - pub has_js: bool, + /// If set, display a spinner while this command runs + pub post_create_spinner_message: Option, - /// If true, run npm install - pub install_node_dependencies: bool, + /// If the post-create command fails, display this warning but don't fail + pub post_create_failure_warning: Option, /// The sort order is fixed rather than settable in properties: /// For backend: diff --git a/src/dfx/src/commands/new.rs b/src/dfx/src/commands/new.rs index 0b1ab2aa8a..15c7a388b7 100644 --- a/src/dfx/src/commands/new.rs +++ b/src/dfx/src/commands/new.rs @@ -6,7 +6,8 @@ use crate::lib::manifest::{get_latest_version, is_upgrade_necessary}; use crate::lib::program; use crate::util::assets; use crate::util::clap::parsers::project_name_parser; -use anyhow::{anyhow, bail, ensure, Context}; +use crate::util::command::direct_or_shell_command; +use anyhow::{anyhow, bail, ensure, Context, Error}; use clap::builder::PossibleValuesParser; use clap::Parser; use console::{style, Style}; @@ -21,10 +22,10 @@ use fn_error_context::context; use indicatif::HumanBytes; use semver::Version; use slog::{info, warn, Logger}; -use std::collections::BTreeMap; +use std::collections::{BTreeMap, HashMap}; use std::io::{self, IsTerminal, Read}; use std::path::{Path, PathBuf}; -use std::process::{Command, Stdio}; +use std::process::{Command, ExitStatus, Stdio}; use std::time::Duration; use tar::Archive; @@ -216,6 +217,13 @@ pub fn init_git(log: &Logger, project_name: &Path) -> DfxResult { Ok(()) } +fn replace_variables(mut s: String, variables: &BTreeMap) -> String { + variables.iter().for_each(|(name, value)| { + s = s.replace(&format!("__{name}__"), value); + }); + s +} + #[context("Failed to unpack archive to {}.", root.to_string_lossy())] fn write_files_from_entries( log: &Logger, @@ -236,25 +244,17 @@ fn write_files_from_entries( let v = match String::from_utf8(v) { Err(err) => err.into_bytes(), - Ok(mut s) => { - // Perform replacements. - variables.iter().for_each(|(name, value)| { - s = s.replace(&format!("__{name}__"), value); - }); - s.into_bytes() - } + Ok(s) => replace_variables(s, variables).into_bytes(), }; // Perform path replacements. - let mut p = root + let p = root .join(file.header().path()?) .to_str() .expect("Non unicode project name path.") .to_string(); - variables.iter().for_each(|(name, value)| { - p = p.replace(&format!("__{name}__"), value); - }); + let p = replace_variables(p, variables); let p = PathBuf::from(p); if p.extension() == Some("json-patch".as_ref()) { @@ -269,27 +269,12 @@ fn write_files_from_entries( Ok(()) } -#[context("Failed to run 'npm install'.")] -fn npm_install(location: &Path) -> DfxResult { - Command::new(program::NPM) - .arg("install") - .arg("--quiet") - .arg("--no-progress") - .arg("--workspaces") - .arg("--if-present") - .stdout(Stdio::inherit()) - .stderr(Stdio::inherit()) - .current_dir(location) - .spawn() - .map_err(DfxError::from) -} - #[context("Failed to scaffold frontend code.")] fn scaffold_frontend_code( env: &dyn Environment, dry_run: bool, project_name: &Path, - frontend: ProjectTemplate, + frontend: &ProjectTemplate, frontend_tests: Option, agent_version: &Option, variables: &BTreeMap, @@ -318,7 +303,7 @@ fn scaffold_frontend_code( project_name_str.to_uppercase(), ); - write_project_template_resources(log, &frontend, project_name, dry_run, &variables)?; + write_project_template_resources(log, frontend, project_name, dry_run, &variables)?; if let Some(frontend_tests) = frontend_tests { write_project_template_resources( @@ -331,19 +316,8 @@ fn scaffold_frontend_code( } // Only install node dependencies if we're not running in dry run. - if !dry_run && frontend.install_node_dependencies { - // Install node modules. Error is not blocking, we just show a message instead. - if node_installed { - let b = env.new_spinner("Installing node dependencies...".into()); - - if npm_install(project_name)?.wait().is_ok() { - b.finish_with_message("Done.".into()); - } else { - b.finish_with_message( - "An error occurred. See the messages above for more details.".into(), - ); - } - } + if !dry_run { + run_post_create_command(env, project_name, frontend, &variables)?; } } else { if !node_installed { @@ -371,7 +345,6 @@ fn scaffold_frontend_code( variables, )?; } - Ok(()) } @@ -520,20 +493,15 @@ pub fn exec(env: &dyn Environment, mut opts: NewOpts) -> DfxResult { None }; - if backend.has_js || frontend.as_ref().map_or(false, |t| t.has_js) { - write_files_from_entries( - log, - &mut assets::new_project_js_files().context("Failed to get JS config archive.")?, - project_name, - dry_run, - &variables, - )?; + let requirements = get_requirements(&backend, frontend.as_ref(), &extras)?; + for requirement in &requirements { + write_project_template_resources(log, requirement, project_name, dry_run, &variables)?; } write_project_template_resources(log, &backend, project_name, dry_run, &variables)?; - for extra in extras { - write_project_template_resources(log, &extra, project_name, dry_run, &variables)?; + for extra in &extras { + write_project_template_resources(log, extra, project_name, dry_run, &variables)?; } if let Some(frontend) = frontend { @@ -541,7 +509,7 @@ pub fn exec(env: &dyn Environment, mut opts: NewOpts) -> DfxResult { env, dry_run, project_name, - frontend, + &frontend, frontend_tests, &opts.agent_version, &variables, @@ -578,26 +546,12 @@ pub fn exec(env: &dyn Environment, mut opts: NewOpts) -> DfxResult { init_git(log, project_name)?; } - if backend.update_cargo_lockfile { - // dfx build will use --locked, so update the lockfile beforehand - const MSG: &str = "You will need to run it yourself (or a similar command like `cargo vendor`), because `dfx build` will use the --locked flag with Cargo."; - if let Ok(code) = Command::new("cargo") - .arg("update") - .arg("--manifest-path") - .arg(project_name.join("Cargo.toml")) - .stderr(Stdio::inherit()) - .stdout(Stdio::inherit()) - .status() - { - if !code.success() { - warn!(log, "Failed to run `cargo update`. {MSG}"); - } - } else { - warn!( - log, - "Failed to run `cargo update` - is Cargo installed? {MSG}" - ) - } + run_post_create_command(env, project_name, &backend, &variables)?; + for extra in extras { + run_post_create_command(env, project_name, &extra, &variables)? + } + for requirement in &requirements { + run_post_create_command(env, project_name, requirement, &variables)?; } } @@ -615,6 +569,133 @@ pub fn exec(env: &dyn Environment, mut opts: NewOpts) -> DfxResult { Ok(()) } +fn get_requirements( + backend: &ProjectTemplate, + frontend: Option<&ProjectTemplate>, + extras: &[ProjectTemplate], +) -> DfxResult> { + let mut requirements = vec![]; + + let mut have = HashMap::new(); + have.insert(backend.name.clone(), backend.clone()); + if let Some(frontend) = frontend { + have.insert(frontend.name.clone(), frontend.clone()); + } + for extra in extras { + have.insert(extra.name.clone(), extra.clone()); + } + + loop { + let new_requirements = have + .iter() + .flat_map(|(_, template)| template.requirements.clone()) + .filter(|requirement| !have.contains_key(requirement)) + .collect::>(); + + for new_requirement in &new_requirements { + let Some(requirement) = find_project_template(new_requirement) else { + bail!("Did not find required project template {}", new_requirement) + }; + have.insert(requirement.name.clone(), requirement.clone()); + requirements.push(requirement); + } + + if new_requirements.is_empty() { + break; + } + } + + Ok(requirements) +} + +fn run_post_create_command( + env: &dyn Environment, + root: &Path, + project_template: &ProjectTemplate, + variables: &BTreeMap, +) -> DfxResult { + let log = env.get_logger(); + + for command in &project_template.post_create { + let command = replace_variables(command.clone(), variables); + let mut cmd = direct_or_shell_command(&command, root)?; + + let spinner = project_template + .post_create_spinner_message + .as_ref() + .map(|msg| env.new_spinner(msg.clone().into())); + + let status = cmd + .stderr(Stdio::inherit()) + .stdout(Stdio::inherit()) + .status() + .with_context(|| { + format!( + "Failed to run post-create command '{}' for project template '{}.", + &command, &project_template.name + ) + }); + + if let Some(spinner) = spinner { + let message = match status { + Ok(status) if status.success() => "Done.", + _ => "Failed.", + }; + spinner.finish_with_message(message.into()); + } + if let Some(warning) = &project_template.post_create_failure_warning { + warn_on_post_create_error(log, status, &command, warning); + } else { + fail_on_post_create_error(command, status)?; + } + } + Ok(()) +} + +fn warn_on_post_create_error( + log: &Logger, + status: Result, + command: &str, + warning: &str, +) { + match status { + Ok(status) if status.success() => {} + Ok(status) => match status.code() { + Some(code) => { + warn!( + log, + "Post-create command '{command}' failed with exit code {code}. {warning}", + ); + } + None => { + warn!(log, "Post-create command '{command}' failed. {warning}"); + } + }, + Err(e) => { + warn!( + log, + "Failed to execute post-create command '{command}': {e}. {warning}" + ); + } + } +} + +fn fail_on_post_create_error( + command: String, + status: Result, +) -> Result<(), Error> { + let status = status?; + if !status.success() { + match status.code() { + Some(code) => { + bail!("Post-create command '{command}' failed with exit code {code}.") + } + None => bail!("Post-create command '{command}' failed."), + } + } + Ok(()) +} + fn write_project_template_resources( logger: &Logger, template: &ProjectTemplate, diff --git a/src/dfx/src/lib/builders/mod.rs b/src/dfx/src/lib/builders/mod.rs index 2ba74ec65f..0344c27b0e 100644 --- a/src/dfx/src/lib/builders/mod.rs +++ b/src/dfx/src/lib/builders/mod.rs @@ -3,6 +3,7 @@ use crate::lib::canister_info::CanisterInfo; use crate::lib::environment::Environment; use crate::lib::error::{BuildError, DfxError, DfxResult}; use crate::lib::models::canister::CanisterPool; +use crate::util::command::direct_or_shell_command; use anyhow::{bail, Context}; use candid::Principal as CanisterId; use candid_parser::utils::CandidSource; @@ -27,7 +28,6 @@ mod motoko; mod pull; mod rust; -use crate::util::command::direct_or_shell_command; pub use custom::custom_download; #[derive(Debug)] diff --git a/src/dfx/src/lib/project/templates.rs b/src/dfx/src/lib/project/templates.rs index 609837fde7..364556a504 100644 --- a/src/dfx/src/lib/project/templates.rs +++ b/src/dfx/src/lib/project/templates.rs @@ -3,6 +3,12 @@ use dfx_core::config::project_templates::{ Category, ProjectTemplate, ProjectTemplateName, ResourceLocation, }; +const NPM_INSTALL: &str = "npm install --quiet --no-progress --workspaces --if-present"; +const NPM_INSTALL_SPINNER_MESSAGE: &str = "Installing node dependencies..."; +const NPM_INSTALL_FAILURE_WARNING: &str = + "An error occurred. See the messages above for more details."; +const CARGO_UPDATE_FAILURE_MESSAGE: &str = "You will need to run it yourself (or a similar command like `cargo vendor`), because `dfx build` will use the --locked flag with Cargo."; + pub fn builtin_templates() -> Vec { let motoko = ProjectTemplate { name: ProjectTemplateName("motoko".to_string()), @@ -11,10 +17,11 @@ pub fn builtin_templates() -> Vec { get_archive_fn: assets::new_project_motoko_files, }, category: Category::Backend, + post_create: vec![], + post_create_failure_warning: None, + post_create_spinner_message: None, + requirements: vec![], sort_order: 0, - update_cargo_lockfile: false, - has_js: false, - install_node_dependencies: false, }; let rust = ProjectTemplate { @@ -24,10 +31,11 @@ pub fn builtin_templates() -> Vec { get_archive_fn: assets::new_project_rust_files, }, category: Category::Backend, + post_create: vec!["cargo update".to_string()], + post_create_failure_warning: Some(CARGO_UPDATE_FAILURE_MESSAGE.to_string()), + post_create_spinner_message: None, + requirements: vec![], sort_order: 1, - update_cargo_lockfile: true, - has_js: false, - install_node_dependencies: false, }; let azle = ProjectTemplate { @@ -37,10 +45,11 @@ pub fn builtin_templates() -> Vec { get_archive_fn: assets::new_project_azle_files, }, category: Category::Backend, + post_create: vec![], + post_create_failure_warning: None, + post_create_spinner_message: None, + requirements: vec![ProjectTemplateName("dfx_js_base".to_string())], sort_order: 2, - update_cargo_lockfile: false, - has_js: true, - install_node_dependencies: false, }; let kybra = ProjectTemplate { @@ -50,10 +59,11 @@ pub fn builtin_templates() -> Vec { get_archive_fn: assets::new_project_kybra_files, }, category: Category::Backend, + post_create: vec![], + post_create_failure_warning: None, + post_create_spinner_message: None, + requirements: vec![], sort_order: 2, - update_cargo_lockfile: false, - has_js: false, - install_node_dependencies: false, }; let sveltekit = ProjectTemplate { @@ -63,10 +73,11 @@ pub fn builtin_templates() -> Vec { get_archive_fn: assets::new_project_svelte_files, }, category: Category::Frontend, + post_create: vec![NPM_INSTALL.to_string()], + post_create_failure_warning: Some(NPM_INSTALL_FAILURE_WARNING.to_string()), + post_create_spinner_message: Some(NPM_INSTALL_SPINNER_MESSAGE.to_string()), + requirements: vec![ProjectTemplateName("dfx_js_base".to_string())], sort_order: 0, - update_cargo_lockfile: false, - has_js: true, - install_node_dependencies: true, }; let react = ProjectTemplate { @@ -76,10 +87,11 @@ pub fn builtin_templates() -> Vec { get_archive_fn: assets::new_project_react_files, }, category: Category::Frontend, + post_create: vec![NPM_INSTALL.to_string()], + post_create_failure_warning: Some(NPM_INSTALL_FAILURE_WARNING.to_string()), + post_create_spinner_message: Some(NPM_INSTALL_SPINNER_MESSAGE.to_string()), + requirements: vec![ProjectTemplateName("dfx_js_base".to_string())], sort_order: 1, - update_cargo_lockfile: false, - has_js: true, - install_node_dependencies: true, }; let vue = ProjectTemplate { @@ -89,10 +101,11 @@ pub fn builtin_templates() -> Vec { get_archive_fn: assets::new_project_vue_files, }, category: Category::Frontend, + post_create: vec![NPM_INSTALL.to_string()], + post_create_failure_warning: Some(NPM_INSTALL_FAILURE_WARNING.to_string()), + post_create_spinner_message: Some(NPM_INSTALL_SPINNER_MESSAGE.to_string()), + requirements: vec![ProjectTemplateName("dfx_js_base".to_string())], sort_order: 2, - update_cargo_lockfile: false, - has_js: true, - install_node_dependencies: true, }; let vanilla = ProjectTemplate { @@ -102,10 +115,11 @@ pub fn builtin_templates() -> Vec { get_archive_fn: assets::new_project_vanillajs_files, }, category: Category::Frontend, + post_create: vec![NPM_INSTALL.to_string()], + post_create_failure_warning: Some(NPM_INSTALL_FAILURE_WARNING.to_string()), + post_create_spinner_message: Some(NPM_INSTALL_SPINNER_MESSAGE.to_string()), + requirements: vec![ProjectTemplateName("dfx_js_base".to_string())], sort_order: 3, - update_cargo_lockfile: false, - has_js: true, - install_node_dependencies: true, }; let simple_assets = ProjectTemplate { @@ -115,10 +129,11 @@ pub fn builtin_templates() -> Vec { get_archive_fn: assets::new_project_assets_files, }, category: Category::Frontend, + post_create: vec![], + post_create_failure_warning: None, + post_create_spinner_message: None, + requirements: vec![], sort_order: 4, - update_cargo_lockfile: false, - has_js: false, - install_node_dependencies: false, }; let sveltekit_tests = ProjectTemplate { @@ -128,10 +143,11 @@ pub fn builtin_templates() -> Vec { get_archive_fn: assets::new_project_svelte_test_files, }, category: Category::FrontendTest, + post_create: vec![], + post_create_failure_warning: None, + post_create_spinner_message: None, + requirements: vec![], sort_order: 0, - update_cargo_lockfile: false, - has_js: false, - install_node_dependencies: false, }; let react_tests = ProjectTemplate { @@ -141,10 +157,11 @@ pub fn builtin_templates() -> Vec { get_archive_fn: assets::new_project_react_test_files, }, category: Category::FrontendTest, + post_create: vec![], + post_create_failure_warning: None, + post_create_spinner_message: None, + requirements: vec![], sort_order: 0, - update_cargo_lockfile: false, - has_js: false, - install_node_dependencies: false, }; let vue_tests = ProjectTemplate { @@ -154,10 +171,11 @@ pub fn builtin_templates() -> Vec { get_archive_fn: assets::new_project_vue_test_files, }, category: Category::FrontendTest, + post_create: vec![], + post_create_failure_warning: None, + post_create_spinner_message: None, + requirements: vec![], sort_order: 0, - update_cargo_lockfile: false, - has_js: false, - install_node_dependencies: false, }; let vanillajs_tests = ProjectTemplate { @@ -167,10 +185,11 @@ pub fn builtin_templates() -> Vec { get_archive_fn: assets::new_project_vanillajs_test_files, }, category: Category::FrontendTest, + post_create: vec![], + post_create_failure_warning: None, + post_create_spinner_message: None, + requirements: vec![], sort_order: 0, - update_cargo_lockfile: false, - has_js: false, - install_node_dependencies: false, }; let internet_identity = ProjectTemplate { @@ -180,10 +199,11 @@ pub fn builtin_templates() -> Vec { get_archive_fn: assets::new_project_internet_identity_files, }, category: Category::Extra, + post_create: vec![], + post_create_failure_warning: None, + post_create_spinner_message: None, + requirements: vec![], sort_order: 0, - update_cargo_lockfile: false, - has_js: false, - install_node_dependencies: false, }; let bitcoin = ProjectTemplate { @@ -193,10 +213,25 @@ pub fn builtin_templates() -> Vec { get_archive_fn: assets::new_project_bitcoin_files, }, category: Category::Extra, + post_create: vec![], + post_create_failure_warning: None, + post_create_spinner_message: None, + requirements: vec![], sort_order: 1, - update_cargo_lockfile: false, - has_js: false, - install_node_dependencies: false, + }; + + let js_base = ProjectTemplate { + name: ProjectTemplateName("dfx_js_base".to_string()), + display: ">".to_string(), + resource_location: ResourceLocation::Bundled { + get_archive_fn: assets::new_project_js_files, + }, + category: Category::Support, + post_create: vec![], + post_create_failure_warning: None, + post_create_spinner_message: None, + requirements: vec![], + sort_order: 2, }; vec![ @@ -215,5 +250,6 @@ pub fn builtin_templates() -> Vec { vanillajs_tests, internet_identity, bitcoin, + js_base, ] }