From 12a8ea14af0b917df9008d8c5ee584dea7962266 Mon Sep 17 00:00:00 2001 From: Max Date: Mon, 6 Nov 2023 14:57:36 +0100 Subject: [PATCH 1/4] feat: Read install arg from file (#3431) # Description This PR adds support for providing install arguments from a file. This is motivated by these issues: * It is impossible to pass a large canister init arg as the length of shell arguments is limited by the terminal (which in turn may be limited by the operating system). * For shorter arguments, it is more convenient to write `--argument-file somefile.hex` than `--argument "$(cat somefile.hex)"`. * The `dfx canister call` method supports `--argument-file` for similar reasons. It would be nice to be consistent. --- CHANGELOG.md | 5 +++++ docs/cli-reference/dfx-canister.md | 1 + e2e/tests-dfx/install.bash | 27 +++++++++++++++++++++++- src/dfx/src/commands/canister/install.rs | 23 ++++++++++++++++++-- 4 files changed, 53 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a496863420..55e6e868d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ # UNRELEASED +=== feat: Read dfx canister install argument from a file + +Enables passing large arguments that cannot be passed directly in the command line using the `--argument-file` flag. For example `dfx canister install --argument-file ./my/argument/file.txt my_canister_name`. + + ### feat: change `list_permitted` and `list_authorized` to an update call. This requires the `list_authorized` and `list_permitted` methods to be called as an update and disables the ability to diff --git a/docs/cli-reference/dfx-canister.md b/docs/cli-reference/dfx-canister.md index 555b83df78..ffc06eed56 100644 --- a/docs/cli-reference/dfx-canister.md +++ b/docs/cli-reference/dfx-canister.md @@ -438,6 +438,7 @@ You can use the following optional flags with the `dfx canister install` command | Flag | Description | |-----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `--argument-file` | Specifies the file from which to read the argument to pass to the init method. Stdin may be referred to as `-`. | | `--async-call` | Enables you to continue without waiting for the result of the installation to be returned by polling the Internet Computer or the local canister execution environment. | | `--upgrade-unchanged` | Upgrade the canister even if the .wasm did not change. | diff --git a/e2e/tests-dfx/install.bash b/e2e/tests-dfx/install.bash index 04fcb84fa9..5511205a07 100644 --- a/e2e/tests-dfx/install.bash +++ b/e2e/tests-dfx/install.bash @@ -182,7 +182,32 @@ teardown() { assert_contains db07e7e24f6f8ddf53c33a610713259a7c1eb71c270b819ebd311e2d223267f0 } +@test "installing one canister with an argument succeeds" { + dfx_start + assert_command dfx canister create e2e_project_backend + assert_command dfx build e2e_project_backend + assert_command dfx canister install e2e_project_backend --argument '()' +} + +@test "installing with an argument in a file succeeds" { + dfx_start + assert_command dfx canister create e2e_project_backend + assert_command dfx build e2e_project_backend + TMPFILE="$(mktemp)" + echo '()' >"$TMPFILE" + assert_command dfx canister install e2e_project_backend --argument-file "$TMPFILE" +} + +@test "installing with an argument on stdin succeeds" { + dfx_start + assert_command dfx canister create e2e_project_backend + assert_command dfx build e2e_project_backend + TMPFILE="$(mktemp)" + echo '()' >"$TMPFILE" + assert_command dfx canister install e2e_project_backend --argument-file - <"$TMPFILE" +} + @test "installing multiple canisters with arguments fails" { - assert_command_fail dfx canister install --all --argument hello + assert_command_fail dfx canister install --all --argument '()' assert_contains "error: the argument '--all' cannot be used with '--argument '" } diff --git a/src/dfx/src/commands/canister/install.rs b/src/dfx/src/commands/canister/install.rs index 2985cbb347..304c5eab69 100644 --- a/src/dfx/src/commands/canister/install.rs +++ b/src/dfx/src/commands/canister/install.rs @@ -3,8 +3,12 @@ use crate::lib::environment::Environment; use crate::lib::error::DfxResult; use crate::lib::operations::canister::install_canister::install_canister; use crate::lib::root_key::fetch_root_key_if_needed; +use crate::util::clap::parsers::file_or_stdin_parser; use crate::util::get_candid_init_type; -use crate::{lib::canister_info::CanisterInfo, util::blob_from_arguments}; +use crate::{ + lib::canister_info::CanisterInfo, + util::{arguments_from_file, blob_from_arguments}, +}; use dfx_core::identity::CallSender; use anyhow::{anyhow, bail, Context}; @@ -40,9 +44,17 @@ pub struct CanisterInstallOpts { upgrade_unchanged: bool, /// Specifies the argument to pass to the method. - #[arg(long)] + #[arg(long, conflicts_with("argument_file"))] argument: Option, + /// Specifies the file from which to read the argument to pass to the method. + #[arg( + long, + value_parser = file_or_stdin_parser, + conflicts_with("argument") + )] + argument_file: Option, + /// Specifies the data type for the argument when making the call using an argument. #[arg(long, requires("argument"), value_parser = ["idl", "raw"])] argument_type: Option, @@ -107,7 +119,14 @@ pub async fn exec( let canister_id = Principal::from_text(canister).or_else(|_| canister_id_store.get(canister))?; + + let arguments_from_file = opts + .argument_file + .map(|v| arguments_from_file(&v)) + .transpose()?; let arguments = opts.argument.as_deref(); + let arguments = arguments_from_file.as_deref().or(arguments); + let arg_type = opts.argument_type.as_deref(); let canister_info = config.as_ref() .ok_or_else(|| anyhow!("Cannot find dfx configuration file in the current working directory. Did you forget to create one?")) From 79c6786d643bee3177543f9409278d28a6b972a6 Mon Sep 17 00:00:00 2001 From: Eric Swanson <64809312+ericswanson-dfinity@users.noreply.github.com> Date: Tue, 7 Nov 2023 12:15:05 -0800 Subject: [PATCH 2/4] fix: output .env file is now relative to project root, not cwd (#3435) dfx.json can specify `output_env_file` (the default for new projects is `".env"`), and some commands accept `--output-env-file .env` on the command line. If `dfx deploy`, `dfx build`, or `dfx canister install` were executed in a subdirectory of a project, they would create/read this file in that subdirectory, rather than the same directory as dfx.json (the project root). With this change, the location of the env file is taken to be relative to the project root, and furthermore must be contained within the project directory. Also fixed three places that could output "No such file or directory (OS error 2)" without telling which path wasn't found. Fixes: https://dfinity.atlassian.net/browse/SDK-1028 --- CHANGELOG.md | 6 ++++ e2e/tests-dfx/dotenv.bash | 37 ++++++++++++++++++++++++ src/dfx-core/src/config/model/dfinity.rs | 30 +++++++++++++++++++ src/dfx-core/src/error/config.rs | 16 ++++++++++ src/dfx/src/commands/build.rs | 4 +-- src/dfx/src/commands/canister/install.rs | 8 ++--- src/dfx/src/commands/deploy.rs | 4 +-- src/dfx/src/lib/builders/mod.rs | 8 ++--- 8 files changed, 97 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 55e6e868d5..03fe44d2ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ # UNRELEASED +=== fix: output_env_file is now considered relative to project root + +The .env file location, whether specified as `output_env_file` in dfx.json +or `--output-env-file ` on the commandline, is now considered relative +to the project root, rather than relative to the current working directory. + === feat: Read dfx canister install argument from a file Enables passing large arguments that cannot be passed directly in the command line using the `--argument-file` flag. For example `dfx canister install --argument-file ./my/argument/file.txt my_canister_name`. diff --git a/e2e/tests-dfx/dotenv.bash b/e2e/tests-dfx/dotenv.bash index 1f6f2ab755..2f92e07d0e 100644 --- a/e2e/tests-dfx/dotenv.bash +++ b/e2e/tests-dfx/dotenv.bash @@ -14,6 +14,43 @@ teardown() { standard_teardown } + +@test "puts .env in project root" { + dfx_start + jq '.canisters["e2e_project_backend"].post_install="echo post install backend"' dfx.json | sponge dfx.json + jq '.canisters["e2e_project_frontend"].post_install="echo post install frontend"' dfx.json | sponge dfx.json + + mkdir subdir + mkdir subdir/canister-install-all subdir/canister-install-single + mkdir subdir/build-all subdir/build-single + mkdir subdir/deploy-single subdir/deploy-all + dfx canister create --all + ( cd subdir/build-single && dfx build e2e_project_frontend ) + ( cd subdir/build-all && dfx build --all ) + ( cd subdir/canister-install-single && dfx canister install e2e_project_backend ) + dfx canister uninstall-code e2e_project_backend + ( cd subdir/canister-install-all && dfx canister install --all ) + rm -rf .dfx + ( cd subdir/deploy-single && dfx deploy e2e_project_backend) + ( cd subdir/deploy-all && dfx deploy ) + + assert_command find . -name .env + assert_eq "./.env" +} + +@test "the output_env_file must be contained within project" { + dfx_start + mkdir ../outside + + assert_command_fail dfx deploy --output-env-file nonexistent/.env + assert_contains "failed to canonicalize output_env_file" + assert_contains "working-dir/e2e_project/nonexistent: No such file or directory" + assert_command_fail dfx deploy --output-env-file /etc/passwd + assert_contains "The output_env_file must be a relative path, but is /etc/passwd" + assert_command_fail dfx deploy --output-env-file ../outside/.env + assert_match "The output_env_file must be within the project root, but is .*/working-dir/e2e_project/../outside/.env" +} + @test "writes environment variables to .env" { dfx_start dfx canister create --all diff --git a/src/dfx-core/src/config/model/dfinity.rs b/src/dfx-core/src/config/model/dfinity.rs index e67d9a7c38..17c8b1d8a7 100644 --- a/src/dfx-core/src/config/model/dfinity.rs +++ b/src/dfx-core/src/config/model/dfinity.rs @@ -3,6 +3,7 @@ use crate::config::directories::get_user_dfx_config_dir; use crate::config::model::bitcoin_adapter::BitcoinAdapterLogLevel; use crate::config::model::canister_http_adapter::HttpAdapterLogLevel; +use crate::error::config::GetOutputEnvFileError; use crate::error::dfx_config::AddDependenciesError::CanisterCircularDependency; use crate::error::dfx_config::GetCanisterNamesWithDependenciesError::AddDependenciesFailed; use crate::error::dfx_config::GetComputeAllocationError::GetComputeAllocationFailed; @@ -1013,6 +1014,35 @@ impl Config { ) } + // returns the path to the output env file if any, guaranteed to be + // a child relative to the project root + pub fn get_output_env_file( + &self, + from_cmdline: Option, + ) -> Result, GetOutputEnvFileError> { + from_cmdline + .or(self.config.output_env_file.clone()) + .map(|p| { + if p.is_relative() { + let p = self.get_project_root().join(p); + + // cannot canonicalize a path that doesn't exist, but the parent should exist + let env_parent = + crate::fs::parent(&p).map_err(GetOutputEnvFileError::Parent)?; + let env_parent = crate::fs::canonicalize(&env_parent) + .map_err(GetOutputEnvFileError::Canonicalize)?; + if !env_parent.starts_with(self.get_project_root()) { + Err(GetOutputEnvFileError::OutputEnvFileMustBeInProjectRoot(p)) + } else { + Ok(self.get_project_root().join(p)) + } + } else { + Err(GetOutputEnvFileError::OutputEnvFileMustBeRelative(p)) + } + }) + .transpose() + } + pub fn save(&self) -> Result<(), StructuredFileError> { save_json_file(&self.path, &self.json) } diff --git a/src/dfx-core/src/error/config.rs b/src/dfx-core/src/error/config.rs index 1121332e42..1658ba575e 100644 --- a/src/dfx-core/src/error/config.rs +++ b/src/dfx-core/src/error/config.rs @@ -1,5 +1,6 @@ use crate::error::fs::FsError; use crate::error::get_user_home::GetUserHomeError; +use std::path::PathBuf; use thiserror::Error; #[derive(Error, Debug)] @@ -13,3 +14,18 @@ pub enum ConfigError { #[error("Failed to determine shared network data directory: {0}")] DetermineSharedNetworkDirectoryFailed(GetUserHomeError), } + +#[derive(Error, Debug)] +pub enum GetOutputEnvFileError { + #[error("failed to canonicalize output_env_file")] + Canonicalize(#[source] FsError), + + #[error("The output_env_file must be within the project root, but is {}", .0.display())] + OutputEnvFileMustBeInProjectRoot(PathBuf), + + #[error("The output_env_file must be a relative path, but is {}", .0.display())] + OutputEnvFileMustBeRelative(PathBuf), + + #[error(transparent)] + Parent(FsError), +} diff --git a/src/dfx/src/commands/build.rs b/src/dfx/src/commands/build.rs index d9abf3412b..22d58f3dcf 100644 --- a/src/dfx/src/commands/build.rs +++ b/src/dfx/src/commands/build.rs @@ -40,9 +40,7 @@ pub fn exec(env: &dyn Environment, opts: CanisterBuildOpts) -> DfxResult { // Read the config. let config = env.get_config_or_anyhow()?; - let env_file = opts - .output_env_file - .or_else(|| config.get_config().output_env_file.clone()); + let env_file = config.get_output_env_file(opts.output_env_file)?; // Check the cache. This will only install the cache if there isn't one installed // already. diff --git a/src/dfx/src/commands/canister/install.rs b/src/dfx/src/commands/canister/install.rs index 304c5eab69..0b01dd243d 100644 --- a/src/dfx/src/commands/canister/install.rs +++ b/src/dfx/src/commands/canister/install.rs @@ -155,9 +155,7 @@ pub async fn exec( } else { let canister_info = canister_info?; let config = config.unwrap(); - let env_file = opts - .output_env_file - .or_else(|| config.get_config().output_env_file.clone()); + let env_file = config.get_output_env_file(opts.output_env_file)?; let idl_path = canister_info.get_constructor_idl_path(); let init_type = get_candid_init_type(&idl_path); let install_args = || blob_from_arguments(arguments, None, arg_type, &init_type); @@ -182,9 +180,7 @@ pub async fn exec( } else if opts.all { // Install all canisters. let config = env.get_config_or_anyhow()?; - let env_file = opts - .output_env_file - .or_else(|| config.get_config().output_env_file.clone()); + let env_file = config.get_output_env_file(opts.output_env_file)?; if let Some(canisters) = &config.get_config().canisters { for canister in canisters.keys() { if pull_canisters_in_config.contains_key(canister) { diff --git a/src/dfx/src/commands/deploy.rs b/src/dfx/src/commands/deploy.rs index 7a429dcd85..5058ecb88f 100644 --- a/src/dfx/src/commands/deploy.rs +++ b/src/dfx/src/commands/deploy.rs @@ -115,9 +115,7 @@ pub fn exec(env: &dyn Environment, opts: DeployOpts) -> DfxResult { .map_err(|err| anyhow!(err)) .context("Failed to parse InstallMode.")?; let config = env.get_config_or_anyhow()?; - let env_file = opts - .output_env_file - .or_else(|| config.get_config().output_env_file.clone()); + let env_file = config.get_output_env_file(opts.output_env_file)?; let with_cycles = opts.with_cycles; diff --git a/src/dfx/src/lib/builders/mod.rs b/src/dfx/src/lib/builders/mod.rs index cfa2934da2..0a553ef1ff 100644 --- a/src/dfx/src/lib/builders/mod.rs +++ b/src/dfx/src/lib/builders/mod.rs @@ -448,7 +448,7 @@ fn write_environment_variables(vars: &[Env<'_>], write_path: &Path) -> DfxResult // the section is correctly formed let end_pos = end_pos + END_TAG.len() + start_pos + START_TAG.len(); existing_file.replace_range(start_pos..end_pos, &write_string); - fs::write(write_path, existing_file)?; + dfx_core::fs::write(write_path, existing_file)?; return Ok(()); } else { // the file has been edited, so we don't know how much to delete, so we append instead @@ -456,10 +456,10 @@ fn write_environment_variables(vars: &[Env<'_>], write_path: &Path) -> DfxResult } // append to the existing file existing_file.push_str(&write_string); - fs::write(write_path, existing_file)?; + dfx_core::fs::write(write_path, existing_file)?; } else { // no existing file, okay to clobber - fs::write(write_path, write_string)?; + dfx_core::fs::write(write_path, write_string)?; } Ok(()) } @@ -501,7 +501,7 @@ impl BuildConfig { idl_root: canister_root.join("idl/"), // TODO: possibly move to `network_root.join("idl/")` lsp_root: network_root.join("lsp/"), canisters_to_build: None, - env_file: config_intf.output_env_file.clone(), + env_file: config.get_output_env_file(None)?, }) } From 485426826886b8280dc4fab015d3084d04e6c234 Mon Sep 17 00:00:00 2001 From: Eric Swanson <64809312+ericswanson-dfinity@users.noreply.github.com> Date: Tue, 7 Nov 2023 12:50:22 -0800 Subject: [PATCH 3/4] fix: dfx extension install will no longer create a corrupt cache directory (#3436) Running `dfx extension install ` immediately after installing a new dfx version, or after `dfx cache delete`, would result in a cache directory that contained only an `extensions` subdirectory. Later, dfx would see that the cache directory exists and therefore not install it. Then, commands like `dfx start` or `dfx build` would fail due to missing files. Fixes: https://dfinity.atlassian.net/browse/SDK-1240 --- CHANGELOG.md | 8 ++++++++ e2e/tests-dfx/extension.bash | 6 ++++++ src/dfx/src/commands/extension/install.rs | 4 ++++ 3 files changed, 18 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 03fe44d2ed..e459166cb6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ # UNRELEASED +=== fix: dfx extension install can no longer create a corrupt cache directory + +Running `dfx cache delete && dfx extension install nns` would previously +create a cache directory containing only an `extensions` subdirectory. +dfx only looks for the existence of a cache version subdirectory to +determine whether it has been installed. The end result was that later +commands would fail when the cache did not contain expected files. + === fix: output_env_file is now considered relative to project root The .env file location, whether specified as `output_env_file` in dfx.json diff --git a/e2e/tests-dfx/extension.bash b/e2e/tests-dfx/extension.bash index f5688d51ef..bf2c627940 100644 --- a/e2e/tests-dfx/extension.bash +++ b/e2e/tests-dfx/extension.bash @@ -13,6 +13,12 @@ teardown() { standard_teardown } +@test "extension install with an empty cache does not create a corrupt cache" { + dfx cache delete + dfx extension install nns --version 0.2.1 + dfx_start +} + @test "install extension from official registry" { assert_command_fail dfx snsx diff --git a/src/dfx/src/commands/extension/install.rs b/src/dfx/src/commands/extension/install.rs index 6435915951..58e6b472d7 100644 --- a/src/dfx/src/commands/extension/install.rs +++ b/src/dfx/src/commands/extension/install.rs @@ -1,4 +1,5 @@ use crate::commands::DfxCommand; +use crate::config::cache::DiskBasedCache; use crate::lib::environment::Environment; use crate::lib::error::DfxResult; use clap::Parser; @@ -19,6 +20,9 @@ pub struct InstallOpts { } pub fn exec(env: &dyn Environment, opts: InstallOpts) -> DfxResult<()> { + // creating an `extensions` directory in an otherwise empty cache directory would + // cause the cache to be considered "installed" and later commands would fail + DiskBasedCache::install(&env.get_cache().version_str())?; let spinner = env.new_spinner(format!("Installing extension: {}", opts.name).into()); let mgr = env.new_extension_manager()?; let effective_extension_name = opts.install_as.clone().unwrap_or_else(|| opts.name.clone()); From 119a6b1a9de07dc49cff99a90a0443ee3f4408bc Mon Sep 17 00:00:00 2001 From: Marcin Nowak-Liebiediew Date: Thu, 9 Nov 2023 13:41:16 +0100 Subject: [PATCH 4/4] ci: add workflow to update bitcoin canister sources (#3438) --- .github/workflows/bitcoin-canister-update.yml | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 .github/workflows/bitcoin-canister-update.yml diff --git a/.github/workflows/bitcoin-canister-update.yml b/.github/workflows/bitcoin-canister-update.yml new file mode 100644 index 0000000000..55dad64246 --- /dev/null +++ b/.github/workflows/bitcoin-canister-update.yml @@ -0,0 +1,69 @@ +name: Check Bitcoin Canister Release Update + +on: + workflow_dispatch: + schedule: + - cron: '0 0 * * *' # Runs at UTC midnight every day + +jobs: + check-update: + runs-on: ubuntu-latest + + steps: + - name: Checkout dfx repository + uses: actions/checkout@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Fetch Bitcoin Canister latest release tag + env: + GH_TOKEN: "${{ secrets.NIV_UPDATER_TOKEN }}" + run: | + LATEST_TAG=$(gh release view --repo dfinity/bitcoin-canister --json tagName -q .tagName) + echo "Latest tag is $LATEST_TAG" + echo "LATEST_TAG=$LATEST_TAG" >> $GITHUB_ENV + + - name: Check if the latest release tag has been updated + run: | + URL_ENCODED_CURRENT_TAG=$(jq -r '.["ic-btc-canister"].version' nix/sources.json) + CURRENT_TAG=$(python -c "import sys, urllib.parse as ul; print(ul.unquote_plus(sys.argv[1]))" "$URL_ENCODED_CURRENT_TAG") + echo "Current tag is $CURRENT_TAG" + if [[ "$CURRENT_TAG" == "$LATEST_TAG" ]]; then + echo "No update is required." + exit 1 + else + echo "An update is required." + fi + + + - name: install Nix + uses: cachix/install-nix-action@v21 + with: + nix_path: nixpkgs=channel:nixos-unstable + + - name: install niv (dependency manager for Nix projects) + run: nix-env -i niv -f '' + + - name: install packages from nix/sources.json + run: niv update + + - name: update sources + run: | + URL_ENCODED_LATEST_TAG=$(echo -n "$LATEST_TAG" | python -c 'import sys, urllib.parse; print(urllib.parse.quote(sys.stdin.read().strip(), safe=""))') + niv update ic-btc-canister -a version=$URL_ENCODED_LATEST_TAG + ./scripts/write-dfx-asset-sources.sh + + - name: Update dfx to use the latest Bitcoin Canister version + env: + GH_TOKEN: "${{ secrets.NIV_UPDATER_TOKEN }}" + run: | + git config user.name github-actions + git config user.email github-actions@github.com + git checkout -b bot/update-bitcoin-canister/$LATEST_TAG + git add . + git commit -m "Update Bitcoin Canister to $LATEST_TAG" + git push --set-upstream origin bot/update-bitcoin-canister/$LATEST_TAG + PR_TITLE="chore: Update Bitcoin Canister Version to $LATEST_TAG" + PR_BODY="This PR updates the Bitcoin Canister version to the latest tag: $LATEST_TAG" + gh pr create --title "$PR_TITLE" --body "$PR_BODY" --base master --head $(git branch --show-current) +