diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..6313b56c --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/.github/scripts/init-docker.sh b/.github/scripts/init-docker.sh new file mode 100644 index 00000000..c7d97f87 --- /dev/null +++ b/.github/scripts/init-docker.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +set -ex +apt-get update +DEBIAN_FRONTEND=noninteractive apt-get install -y docker.io +nohup dockerd -H tcp://127.0.0.1:2375 >/var/log/dockerd.log 2>&1 & +for i in $(seq 1 30); do + if docker -H tcp://127.0.0.1:2375 info >/dev/null 2>&1; then + echo Docker ready + { + echo DOCKER_HOST=tcp://127.0.0.1:2375 + echo ICP_CLI_DOCKER_WSL2_MODE="$DISTRO" + } >> $GITHUB_ENV + exit 0 + fi + sleep 1 +done +cat /var/log/dockerd.log +exit 1 diff --git a/.github/scripts/provision-windows-build.ps1 b/.github/scripts/provision-windows-build.ps1 new file mode 100644 index 00000000..67872fc5 --- /dev/null +++ b/.github/scripts/provision-windows-build.ps1 @@ -0,0 +1,15 @@ +#Requires -Version 7.3 +$ErrorActionPreference = 'Stop' +$PSNativeCommandUseErrorActionPreference = $true + +# enormous speedup in CI, but not mandatory +cd C:\vcpkg +git checkout 2025.12.12 +.\bootstrap-vcpkg.bat -disableMetrics +$env:VCPKG_BUILD_TYPE = 'release' +.\vcpkg install openssl:x64-windows-static-md +.\vcpkg integrate install +'OPENSSL_STATIC=1' >> $env:GITHUB_ENV +'OPENSSL_NO_VENDOR=1' >> $env:GITHUB_ENV + +'WSLENV=GITHUB_ENV/p' >> $env:GITHUB_ENV diff --git a/.github/scripts/test-matrix.py b/.github/scripts/test-matrix.py index ad143232..87aab9d5 100644 --- a/.github/scripts/test-matrix.py +++ b/.github/scripts/test-matrix.py @@ -14,11 +14,15 @@ def test_names(): include = [] for test in test_names(): - # Ubuntu: run everything + # Ubuntu/Windows: run everything include.append({ "test": test, "os": "ubuntu-22.04" }) + include.append({ + "test": test, + "os": "windows-2025" + }) # macOS: only run selected tests if test in MACOS_TESTS: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b33079cc..a801694c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,6 +11,12 @@ env: CARGO_NET_RETRY: 10 # Use the local .curlrc CURL_HOME: . + # Disable incremental compilation + CARGO_INCREMENTAL: 0 + +defaults: + run: + shell: bash jobs: discover: @@ -30,15 +36,11 @@ jobs: matrix: # Make the os matrix match .github/scripts/test-matrix.py so that # we optimize caching - os: [ubuntu-22.04, macos-15] + os: [ubuntu-22.04, macos-15, windows-2025] steps: - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 - - name: Setup image (Linux) - if: ${{ contains(matrix.os, 'ubuntu') }} - run: ./.github/scripts/provision-linux-build.sh - - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: path: | @@ -48,8 +50,17 @@ jobs: ~/.cargo/registry/cache/ ~/.cargo/git/db/ target/ + ${{ contains(matrix.os, 'windows') && 'C:/vcpkg' || '' }} key: ${{ matrix.os }}-cargo-${{ hashFiles('rust-toolchain.toml') }}-${{ hashFiles('**/Cargo.lock') }} + - name: Setup image (Linux) + if: ${{ contains(matrix.os, 'ubuntu') }} + run: ./.github/scripts/provision-linux-build.sh + - name: Setup image (Windows) + if: ${{ contains(matrix.os, 'windows') }} + run: ./.github/scripts/provision-windows-build.ps1 + shell: pwsh + - uses: t1m0thyj/unlock-keyring@e481cdc8833d4417a58f40734e8f197183317047 if: ${{ contains(matrix.os, 'ubuntu') }} @@ -68,10 +79,6 @@ jobs: steps: - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 - - name: Setup image (Linux) - if: ${{ contains(matrix.os, 'ubuntu') }} - run: ./.github/scripts/provision-linux-build.sh - - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: path: | @@ -81,8 +88,31 @@ jobs: ~/.cargo/registry/cache/ ~/.cargo/git/db/ target/ + ${{ contains(matrix.os, 'windows') && 'C:/vcpkg/installed' || '' }} key: ${{ matrix.os }}-cargo-${{ hashFiles('rust-toolchain.toml') }}-${{ hashFiles('**/Cargo.lock') }} + - name: Setup image (Linux) + if: ${{ contains(matrix.os, 'ubuntu') }} + run: ./.github/scripts/provision-linux-build.sh + + - name: Setup image (Windows) + if: ${{ contains(matrix.os, 'windows') }} + run: ./.github/scripts/provision-windows-build.ps1 + shell: pwsh + + - name: Setup WSL2 (Windows) + if: ${{ contains(matrix.os, 'windows') }} + uses: Vampire/setup-wsl@6a8db447be7ed35f2f499c02c6e60ff77ef11278 # v6.0.0 + with: + distribution: Ubuntu-22.04 + + - name: Setup Docker in WSL2 (Windows) + if: ${{ contains(matrix.os, 'windows') }} + run: .github/scripts/init-docker.sh + env: + DISTRO: Ubuntu-22.04 + shell: wsl-bash_Ubuntu-22.04 {0} + - uses: t1m0thyj/unlock-keyring@e481cdc8833d4417a58f40734e8f197183317047 if: ${{ contains(matrix.os, 'ubuntu') }} @@ -105,6 +135,7 @@ jobs: - name: install network launcher env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + if: ${{ !contains(matrix.os, 'windows') }} run: | VERSION=v11.0.0 OS="" diff --git a/Cargo.lock b/Cargo.lock index 4ada3186..9e750596 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3166,6 +3166,8 @@ dependencies = [ "tracing", "url", "uuid", + "winreg", + "wslpath2", "zeroize", ] @@ -3197,6 +3199,7 @@ dependencies = [ "clap-markdown", "console 0.16.1", "dialoguer 0.11.0", + "dunce", "elliptic-curve", "futures", "hex", @@ -3224,11 +3227,13 @@ dependencies = [ "regex", "reqwest", "sec1", + "send_ctrlc", "serde", "serde_json", "serde_yaml", "serial_test", "sha2 0.10.9", + "shellwords", "snafu", "sysinfo", "test-tag", @@ -3238,6 +3243,7 @@ dependencies = [ "tracing-subscriber", "url", "uuid", + "wslpath2", ] [[package]] @@ -5613,6 +5619,16 @@ dependencies = [ "serde_core", ] +[[package]] +name = "send_ctrlc" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d07beb664b54f51140baf2769d12d5eb07d0e3eccee78fb95c3e76c2644a4cad" +dependencies = [ + "libc", + "windows-sys 0.61.1", +] + [[package]] name = "serde" version = "1.0.228" @@ -7262,6 +7278,16 @@ dependencies = [ "memchr", ] +[[package]] +name = "winreg" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" +dependencies = [ + "cfg-if", + "windows-sys 0.59.0", +] + [[package]] name = "wit-bindgen" version = "0.46.0" @@ -7274,6 +7300,12 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" +[[package]] +name = "wslpath2" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2db8388b8fbbf9d67e346efc2c1cb216dde5b78981e2eae644a071c160f3acd5" + [[package]] name = "wyz" version = "0.5.1" diff --git a/Cargo.toml b/Cargo.toml index d2c17aa5..aaaac847 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -85,6 +85,8 @@ tracing = "0.1.41" tracing-subscriber = "0.3.20" url = "2.5.4" uuid = { version = "1.16.0", features = ["serde", "v4"] } +winreg = "0.55" +wslpath2 = "0.1" zeroize = "1.8.1" [workspace.dependencies.reqwest] diff --git a/crates/icp-cli/Cargo.toml b/crates/icp-cli/Cargo.toml index 25e8883a..ea69a813 100644 --- a/crates/icp-cli/Cargo.toml +++ b/crates/icp-cli/Cargo.toml @@ -25,6 +25,7 @@ clap-markdown.workspace = true clap.workspace = true console.workspace = true dialoguer.workspace = true +dunce.workspace = true elliptic-curve.workspace = true futures.workspace = true hex.workspace = true @@ -52,6 +53,7 @@ sec1.workspace = true serde_json.workspace = true serde_yaml.workspace = true serde.workspace = true +shellwords.workspace = true snafu.workspace = true sysinfo.workspace = true tiny-bip39.workspace = true @@ -59,20 +61,24 @@ tokio.workspace = true tracing-subscriber.workspace = true tracing.workspace = true url.workspace = true +wslpath2.workspace = true [dev-dependencies] assert_cmd = "2" camino-tempfile = "1" indoc.workspace = true icp = { workspace = true } -nix = { version = "0.30.1", features = ["process", "signal"] } predicates = "3" rand.workspace = true +send_ctrlc = "0.6" serde_yaml.workspace = true serial_test = { version = "3.2.0", features = ["file_locks"] } test-tag = "0.1" uuid.workspace = true +[target.'cfg(unix)'.dev-dependencies] +nix = { version = "0.30.1", features = ["process", "signal"] } + [lints] workspace = true diff --git a/crates/icp-cli/src/commands/canister/settings/update.rs b/crates/icp-cli/src/commands/canister/settings/update.rs index 05a43d76..cab834a5 100644 --- a/crates/icp-cli/src/commands/canister/settings/update.rs +++ b/crates/icp-cli/src/commands/canister/settings/update.rs @@ -1,11 +1,10 @@ use anyhow::bail; use byte_unit::{Byte, Unit}; use clap::{ArgAction, Args}; -use console::Term; use ic_agent::export::Principal; use ic_management_canister_types::{CanisterStatusResult, EnvironmentVariable, LogVisibility}; use icp::ProjectLoadError; -use icp::context::{CanisterSelection, Context}; +use icp::context::{CanisterSelection, Context, TermWriter}; use std::collections::{HashMap, HashSet}; use std::io::Write; @@ -413,7 +412,7 @@ fn get_environment_variables( } fn maybe_warn_on_env_vars_change( - mut term: &Term, + mut term: &TermWriter, configured_settings: &icp::canister::Settings, environment_variables_opt: &EnvironmentVariableOpt, ) -> Result<(), anyhow::Error> { diff --git a/crates/icp-cli/src/logging.rs b/crates/icp-cli/src/logging.rs index f22f6cb4..722cded9 100644 --- a/crates/icp-cli/src/logging.rs +++ b/crates/icp-cli/src/logging.rs @@ -1,10 +1,4 @@ -use std::io::Write; -#[cfg(unix)] -use std::os::fd::{AsRawFd, RawFd}; -#[cfg(windows)] -use std::os::windows::io::{AsRawHandle, RawHandle}; - -use tracing::{Level, Subscriber, debug}; +use tracing::{Level, Subscriber}; use tracing_subscriber::{ Layer, filter::{Filtered, Targets}, @@ -12,42 +6,6 @@ use tracing_subscriber::{ registry::LookupSpan, }; -#[derive(Debug)] -pub(crate) struct TermWriter { - pub(crate) debug: bool, - pub(crate) writer: Box, -} - -impl Write for TermWriter { - fn write(&mut self, buf: &[u8]) -> std::io::Result { - if !self.debug { - self.writer.write(buf)?; - } - debug!("{}", String::from_utf8_lossy(buf).trim()); - Ok(buf.len()) - } - - fn flush(&mut self) -> std::io::Result<()> { - if !self.debug { - self.writer.flush()?; - } - Ok(()) - } -} - -#[cfg(unix)] -impl AsRawFd for TermWriter { - fn as_raw_fd(&self) -> RawFd { - self.writer.as_raw_fd() - } -} -#[cfg(windows)] -impl AsRawHandle for TermWriter { - fn as_raw_handle(&self) -> RawHandle { - self.writer.as_raw_handle() - } -} - type DebugLayer = Filtered< tracing_subscriber::fmt::Layer>, Targets, diff --git a/crates/icp-cli/src/main.rs b/crates/icp-cli/src/main.rs index dd819877..7ac0ba40 100644 --- a/crates/icp-cli/src/main.rs +++ b/crates/icp-cli/src/main.rs @@ -2,7 +2,7 @@ use anyhow::Error; use clap::{CommandFactory, Parser}; use commands::Command; use console::Term; -use icp::prelude::*; +use icp::{context::TermWriter, prelude::*}; use tracing::{Instrument, Level, debug, subscriber::set_global_default, trace_span}; use tracing_subscriber::{ Layer, Registry, @@ -11,7 +11,7 @@ use tracing_subscriber::{ }; use crate::{ - logging::{TermWriter, debug_layer}, + logging::debug_layer, telemetry::EventLayer, version::{git_sha, icp_cli_version_str}, }; @@ -109,14 +109,10 @@ async fn main() -> Result<(), Error> { } }; - // Printing for user-facing messages - let term = Term::read_write_pair( - std::io::stdin(), - TermWriter { - debug: cli.debug, - writer: Box::new(std::io::stdout()), - }, - ); + let term = TermWriter { + debug: cli.debug, + raw_term: Term::stdout(), + }; // Logging and Telemetry let (debug_layer, event_layer) = ( diff --git a/crates/icp-cli/src/operations/build.rs b/crates/icp-cli/src/operations/build.rs index 096150d1..277fbcfc 100644 --- a/crates/icp-cli/src/operations/build.rs +++ b/crates/icp-cli/src/operations/build.rs @@ -1,11 +1,11 @@ use std::sync::Arc; use camino_tempfile::tempdir; -use console::Term; use futures::{StreamExt, stream::FuturesOrdered}; use icp::{ Canister, canister::build::{Build, BuildError, Params}, + context::TermWriter, prelude::*, }; use snafu::{ResultExt, Snafu}; @@ -82,7 +82,7 @@ pub(crate) async fn build_many_with_progress_bar( canisters: Vec<(PathBuf, Canister)>, builder: Arc, artifacts: Arc, - term: &Term, + term: &TermWriter, debug: bool, ) -> Result<(), anyhow::Error> { let mut futs = FuturesOrdered::new(); diff --git a/crates/icp-cli/src/operations/sync.rs b/crates/icp-cli/src/operations/sync.rs index 9411b577..cc7f5f15 100644 --- a/crates/icp-cli/src/operations/sync.rs +++ b/crates/icp-cli/src/operations/sync.rs @@ -1,9 +1,9 @@ -use console::Term; use futures::{StreamExt, stream::FuturesOrdered}; use ic_agent::{Agent, export::Principal}; use icp::{ Canister, canister::sync::{Params, Synchronize, SynchronizeError}, + context::TermWriter, prelude::PathBuf, }; use snafu::prelude::*; @@ -19,7 +19,7 @@ pub struct SyncOperationError; async fn sync_canister( syncer: &Arc, agent: &Agent, - _term: &Term, + _term: &TermWriter, canister_path: PathBuf, canister_id: Principal, canister_info: &Canister, @@ -60,7 +60,7 @@ async fn sync_canister( pub(crate) async fn sync_many( syncer: Arc, agent: Agent, - term: Arc, + term: Arc, canisters: Vec<(Principal, PathBuf, Canister)>, debug: bool, ) -> Result<(), SyncOperationError> { diff --git a/crates/icp-cli/tests/build_adapter_tests.rs b/crates/icp-cli/tests/build_adapter_tests.rs index 89b2fa0f..0df31d2d 100644 --- a/crates/icp-cli/tests/build_adapter_tests.rs +++ b/crates/icp-cli/tests/build_adapter_tests.rs @@ -24,7 +24,7 @@ fn build_adapter_pre_built_path() { build: steps: - type: pre-built - path: {wasm} + path: '{wasm}' "#}; write_string( @@ -66,7 +66,7 @@ fn build_adapter_pre_built_path_invalid_checksum() { build: steps: - type: pre-built - path: {wasm} + path: '{wasm}' sha256: invalid "#}; @@ -114,7 +114,7 @@ fn build_adapter_pre_built_path_valid_checksum() { build: steps: - type: pre-built - path: {wasm} + path: '{wasm}' sha256: {actual} "#}; diff --git a/crates/icp-cli/tests/build_tests.rs b/crates/icp-cli/tests/build_tests.rs index 1a502890..24668fdf 100644 --- a/crates/icp-cli/tests/build_tests.rs +++ b/crates/icp-cli/tests/build_tests.rs @@ -27,7 +27,7 @@ fn build_adapter_script_single() { build: steps: - type: script - command: cp {path} "$ICP_WASM_OUTPUT_PATH" + command: cp '{path}' "$ICP_WASM_OUTPUT_PATH" "#}; write_string( @@ -64,7 +64,7 @@ fn build_adapter_script_multiple() { - type: script command: echo "before" - type: script - command: cp {path} "$ICP_WASM_OUTPUT_PATH" + command: cp '{path}' "$ICP_WASM_OUTPUT_PATH" - type: script command: echo "after" "#}; @@ -254,7 +254,7 @@ fn build_adapter_script_with_explicit_sh_c() { build: steps: - type: script - command: sh -c 'echo "nested shell" > {path} && cp {path} "$ICP_WASM_OUTPUT_PATH"' + command: sh -c 'echo "nested shell" > '"'"'{path}'"'"' && cp '"'"'{path}'"'"' "$ICP_WASM_OUTPUT_PATH"' "#}; write_string( @@ -373,17 +373,17 @@ fn build_multiple_canisters() { build: steps: - type: script - command: echo "building canister-a" && cp {path} "$ICP_WASM_OUTPUT_PATH" + command: echo "building canister-a" && cp '{path}' "$ICP_WASM_OUTPUT_PATH" - name: canister-b build: steps: - type: script - command: echo "building canister-b" && cp {path} "$ICP_WASM_OUTPUT_PATH" + command: echo "building canister-b" && cp '{path}' "$ICP_WASM_OUTPUT_PATH" - name: canister-c build: steps: - type: script - command: echo "building canister-c" && cp {path} "$ICP_WASM_OUTPUT_PATH" + command: echo "building canister-c" && cp '{path}' "$ICP_WASM_OUTPUT_PATH" "#}; write_string( @@ -423,17 +423,17 @@ fn build_all_canisters_in_environment() { build: steps: - type: script - command: echo "building canister-a" && cp {path} "$ICP_WASM_OUTPUT_PATH" + command: echo "building canister-a" && cp '{path}' "$ICP_WASM_OUTPUT_PATH" - name: canister-b build: steps: - type: script - command: echo "building canister-b" && cp {path} "$ICP_WASM_OUTPUT_PATH" + command: echo "building canister-b" && cp '{path}' "$ICP_WASM_OUTPUT_PATH" - name: canister-c build: steps: - type: script - command: echo "building canister-c" && cp {path} "$ICP_WASM_OUTPUT_PATH" + command: echo "building canister-c" && cp '{path}' "$ICP_WASM_OUTPUT_PATH" environments: - name: test-env diff --git a/crates/icp-cli/tests/canister_delete_tests.rs b/crates/icp-cli/tests/canister_delete_tests.rs index 265cfa2b..c6c79133 100644 --- a/crates/icp-cli/tests/canister_delete_tests.rs +++ b/crates/icp-cli/tests/canister_delete_tests.rs @@ -23,7 +23,7 @@ async fn canister_delete() { build: steps: - type: script - command: cp {wasm} "$ICP_WASM_OUTPUT_PATH" + command: cp '{wasm}' "$ICP_WASM_OUTPUT_PATH" {NETWORK_RANDOM_PORT} {ENVIRONMENT_RANDOM_PORT} diff --git a/crates/icp-cli/tests/canister_info_tests.rs b/crates/icp-cli/tests/canister_info_tests.rs index 4aaa12a1..b5595f38 100644 --- a/crates/icp-cli/tests/canister_info_tests.rs +++ b/crates/icp-cli/tests/canister_info_tests.rs @@ -23,7 +23,7 @@ async fn canister_status() { build: steps: - type: script - command: cp {wasm} "$ICP_WASM_OUTPUT_PATH" + command: cp '{wasm}' "$ICP_WASM_OUTPUT_PATH" {NETWORK_RANDOM_PORT} {ENVIRONMENT_RANDOM_PORT} diff --git a/crates/icp-cli/tests/canister_install_tests.rs b/crates/icp-cli/tests/canister_install_tests.rs index 0cf92089..f4783e97 100644 --- a/crates/icp-cli/tests/canister_install_tests.rs +++ b/crates/icp-cli/tests/canister_install_tests.rs @@ -26,7 +26,7 @@ async fn canister_install() { build: steps: - type: script - command: cp {wasm} "$ICP_WASM_OUTPUT_PATH" + command: cp '{wasm}' "$ICP_WASM_OUTPUT_PATH" {NETWORK_RANDOM_PORT} {ENVIRONMENT_RANDOM_PORT} @@ -202,6 +202,7 @@ async fn canister_install_with_wasm_flag() { .stdout(eq("(\"Hello, test!\")").trim()); } +#[cfg(unix)] // moc #[tokio::test] async fn canister_install_with_init_args_candid() { let ctx = TestContext::new(); @@ -265,6 +266,7 @@ async fn canister_install_with_init_args_candid() { .stdout(eq("(\"42\")").trim()); } +#[cfg(unix)] // moc #[tokio::test] async fn canister_install_with_init_args_hex() { let ctx = TestContext::new(); @@ -329,6 +331,7 @@ async fn canister_install_with_init_args_hex() { .stdout(eq("(\"100\")").trim()); } +#[cfg(unix)] // moc #[tokio::test] async fn canister_install_with_environment_init_args_override() { let ctx = TestContext::new(); @@ -397,6 +400,7 @@ async fn canister_install_with_environment_init_args_override() { .stdout(eq("(\"200\")").trim()); } +#[cfg(unix)] // moc #[tokio::test] async fn canister_install_with_invalid_init_args() { let ctx = TestContext::new(); @@ -469,7 +473,7 @@ async fn canister_install_with_environment_settings_override() { build: steps: - type: script - command: cp {wasm} "$ICP_WASM_OUTPUT_PATH" + command: cp '{wasm}' "$ICP_WASM_OUTPUT_PATH" settings: memory_allocation: 1073741824 diff --git a/crates/icp-cli/tests/canister_metadata_tests.rs b/crates/icp-cli/tests/canister_metadata_tests.rs index f4addb0e..2c0cd409 100644 --- a/crates/icp-cli/tests/canister_metadata_tests.rs +++ b/crates/icp-cli/tests/canister_metadata_tests.rs @@ -23,7 +23,7 @@ async fn canister_metadata() { build: steps: - type: script - command: cp {wasm} "$ICP_WASM_OUTPUT_PATH" + command: cp '{wasm}' "$ICP_WASM_OUTPUT_PATH" {NETWORK_RANDOM_PORT} {ENVIRONMENT_RANDOM_PORT} @@ -87,7 +87,7 @@ async fn canister_metadata_not_found() { build: steps: - type: script - command: cp {wasm} "$ICP_WASM_OUTPUT_PATH" + command: cp '{wasm}' "$ICP_WASM_OUTPUT_PATH" {NETWORK_RANDOM_PORT} {ENVIRONMENT_RANDOM_PORT} diff --git a/crates/icp-cli/tests/canister_settings_tests.rs b/crates/icp-cli/tests/canister_settings_tests.rs index 9260c882..c2c2626c 100644 --- a/crates/icp-cli/tests/canister_settings_tests.rs +++ b/crates/icp-cli/tests/canister_settings_tests.rs @@ -34,7 +34,7 @@ async fn canister_settings_update_controllers() { build: steps: - type: script - command: cp {wasm} "$ICP_WASM_OUTPUT_PATH" + command: cp '{wasm}' "$ICP_WASM_OUTPUT_PATH" {NETWORK_RANDOM_PORT} {ENVIRONMENT_RANDOM_PORT} @@ -339,7 +339,7 @@ async fn canister_settings_update_log_visibility() { build: steps: - type: script - command: cp {wasm} "$ICP_WASM_OUTPUT_PATH" + command: cp '{wasm}' "$ICP_WASM_OUTPUT_PATH" {NETWORK_RANDOM_PORT} {ENVIRONMENT_RANDOM_PORT} @@ -656,7 +656,7 @@ async fn canister_settings_update_miscellaneous() { build: steps: - type: script - command: cp {wasm} "$ICP_WASM_OUTPUT_PATH" + command: cp '{wasm}' "$ICP_WASM_OUTPUT_PATH" {NETWORK_RANDOM_PORT} {ENVIRONMENT_RANDOM_PORT} @@ -779,7 +779,7 @@ async fn canister_settings_update_environment_variables() { build: steps: - type: script - command: cp {wasm} "$ICP_WASM_OUTPUT_PATH" + command: cp '{wasm}' "$ICP_WASM_OUTPUT_PATH" {NETWORK_RANDOM_PORT} {ENVIRONMENT_RANDOM_PORT} @@ -964,7 +964,7 @@ async fn canister_settings_sync() { build: steps: - type: script - command: cp {wasm} "$ICP_WASM_OUTPUT_PATH" + command: cp '{wasm}' "$ICP_WASM_OUTPUT_PATH" {NETWORK_RANDOM_PORT} {ENVIRONMENT_RANDOM_PORT} @@ -1033,7 +1033,7 @@ async fn canister_settings_sync() { build: steps: - type: script - command: cp {wasm} "$ICP_WASM_OUTPUT_PATH" + command: cp '{wasm}' "$ICP_WASM_OUTPUT_PATH" settings: {NETWORK_RANDOM_PORT} @@ -1046,7 +1046,7 @@ async fn canister_settings_sync() { build: steps: - type: script - command: cp {wasm} "$ICP_WASM_OUTPUT_PATH" + command: cp '{wasm}' "$ICP_WASM_OUTPUT_PATH" settings: wasm_memory_limit: ~ @@ -1060,7 +1060,7 @@ async fn canister_settings_sync() { build: steps: - type: script - command: cp {wasm} "$ICP_WASM_OUTPUT_PATH" + command: cp '{wasm}' "$ICP_WASM_OUTPUT_PATH" settings: wasm_memory_limit: 4000000000 diff --git a/crates/icp-cli/tests/canister_start_tests.rs b/crates/icp-cli/tests/canister_start_tests.rs index e7b21e8c..48861772 100644 --- a/crates/icp-cli/tests/canister_start_tests.rs +++ b/crates/icp-cli/tests/canister_start_tests.rs @@ -26,7 +26,7 @@ async fn canister_start() { build: steps: - type: script - command: cp {wasm} "$ICP_WASM_OUTPUT_PATH" + command: cp '{wasm}' "$ICP_WASM_OUTPUT_PATH" {NETWORK_RANDOM_PORT} {ENVIRONMENT_RANDOM_PORT} diff --git a/crates/icp-cli/tests/canister_status_tests.rs b/crates/icp-cli/tests/canister_status_tests.rs index 4482dca2..71437a35 100644 --- a/crates/icp-cli/tests/canister_status_tests.rs +++ b/crates/icp-cli/tests/canister_status_tests.rs @@ -26,7 +26,7 @@ async fn canister_status() { build: steps: - type: script - command: cp {wasm} "$ICP_WASM_OUTPUT_PATH" + command: cp '{wasm}' "$ICP_WASM_OUTPUT_PATH" {NETWORK_RANDOM_PORT} {ENVIRONMENT_RANDOM_PORT} diff --git a/crates/icp-cli/tests/canister_stop_tests.rs b/crates/icp-cli/tests/canister_stop_tests.rs index 1c47eb22..70d30074 100644 --- a/crates/icp-cli/tests/canister_stop_tests.rs +++ b/crates/icp-cli/tests/canister_stop_tests.rs @@ -26,7 +26,7 @@ async fn canister_stop() { build: steps: - type: script - command: cp {wasm} "$ICP_WASM_OUTPUT_PATH" + command: cp '{wasm}' "$ICP_WASM_OUTPUT_PATH" {NETWORK_RANDOM_PORT} {ENVIRONMENT_RANDOM_PORT} diff --git a/crates/icp-cli/tests/common/context.rs b/crates/icp-cli/tests/common/context.rs index ae12d871..17676150 100644 --- a/crates/icp-cli/tests/common/context.rs +++ b/crates/icp-cli/tests/common/context.rs @@ -3,6 +3,7 @@ use std::{ env, ffi::OsString, fs::{self, create_dir_all}, + process::Stdio, }; use assert_cmd::Command; @@ -10,7 +11,6 @@ use camino_tempfile::{Utf8TempDir as TempDir, tempdir}; use candid::Principal; use ic_agent::Agent; use icp::{ - directories::{Access, Directories}, network::managed::{ launcher::{NetworkInstance, wait_for_launcher_status}, run::initialize_network, @@ -47,6 +47,10 @@ impl TestContext { let mock_cred_dir = home_dir.path().join("mock-keyring"); fs::create_dir(&mock_cred_dir).expect("failed to create mock keyring dir"); + // App files + let icp_home_dir = home_dir.path().join("icp"); + fs::create_dir(&icp_home_dir).expect("failed to create icp home dir"); + eprintln!("Test environment home directory: {}", home_dir.path()); // OS Path @@ -72,15 +76,19 @@ impl TestContext { // Isolate the command cmd.current_dir(self.home_path()); - cmd.env("HOME", self.home_path()); + #[cfg(unix)] + cmd.env("HOME", self.home_path()).env_remove("ICP_HOME"); + #[cfg(windows)] + cmd.env("ICP_HOME", self.home_path().join("icp")); cmd.env("PATH", self.os_path.clone()); - cmd.env_remove("ICP_HOME"); cmd.env("ICP_CLI_KEYRING_MOCK_DIR", self.mock_cred_dir.clone()); cmd } + #[cfg(unix)] pub(crate) async fn launcher_path(&self) -> PathBuf { + use icp::directories::{Access, Directories}; if let Ok(var) = env::var("ICP_CLI_NETWORK_LAUNCHER_PATH") { PathBuf::from(var) } else { @@ -112,6 +120,17 @@ impl TestContext { } } + pub(crate) async fn launcher_path_or_nothing(&self) -> PathBuf { + #[cfg(unix)] + { + self.launcher_path().await + } + #[cfg(windows)] + { + PathBuf::new() + } + } + fn build_os_path(bin_dir: &Path) -> OsString { let old_path = env::var_os("PATH").unwrap_or_default(); let mut new_path = bin_dir.as_os_str().to_owned(); @@ -174,15 +193,17 @@ impl TestContext { pub(crate) async fn start_network_in(&self, project_dir: &Path, name: &str) -> ChildGuard { let icp_path = env!("CARGO_BIN_EXE_icp"); let mut cmd = std::process::Command::new(icp_path); - cmd.current_dir(project_dir) - .env("HOME", self.home_path()) - .env_remove("ICP_HOME") - .arg("network") - .arg("start") - .arg(name); - - let launcher_path = self.launcher_path().await; - cmd.env("ICP_CLI_NETWORK_LAUNCHER_PATH", launcher_path); + cmd.current_dir(project_dir); + #[cfg(unix)] + cmd.env("HOME", self.home_path()).env_remove("ICP_HOME"); + #[cfg(windows)] + cmd.env("ICP_HOME", self.home_path().join("icp")); + cmd.arg("network").arg("start").arg(name); + #[cfg(unix)] + { + let launcher_path = self.launcher_path().await; + cmd.env("ICP_CLI_NETWORK_LAUNCHER_PATH", launcher_path); + } eprintln!("Running network in {project_dir}"); @@ -222,8 +243,6 @@ impl TestContext { project_dir: &Path, flags: &[&str], ) -> ChildGuard { - let launcher_path = self.launcher_path().await; - // Create network directory structure let network_dir = project_dir .join(".icp") @@ -241,26 +260,93 @@ impl TestContext { eprintln!("Starting network with custom flags"); // Spawn launcher - let mut cmd = std::process::Command::new(&launcher_path); - cmd.args(["--interface-version=1.0.0", "--status-dir"]); - cmd.arg(&launcher_dir); - cmd.args(flags); - cmd.stdout(std::process::Stdio::inherit()); - cmd.stderr(std::process::Stdio::inherit()); - - #[cfg(unix)] - { - use std::os::unix::process::CommandExt; - cmd.process_group(0); - } + let mut cmd = { + #[cfg(unix)] + { + let launcher_path = self.launcher_path().await; + let mut cmd = std::process::Command::new(&launcher_path); + cmd.args(["--interface-version=1.0.0", "--status-dir"]); + cmd.arg(&launcher_dir); + cmd.args(flags); + cmd.stdout(Stdio::inherit()); + cmd.stderr(Stdio::inherit()); + use std::os::unix::process::CommandExt; + cmd.process_group(0); + cmd + } + #[cfg(windows)] + { + let convert = std::env::var("ICP_CLI_DOCKER_WSL2_MODE").unwrap_or_default() == "1"; + let launcher_dir_param = if convert { + wslpath2::convert( + launcher_dir.as_str(), + None, + wslpath2::Conversion::WindowsToWsl, + true, + ) + .expect("Failed to convert launcher dir to WSL path") + } else { + launcher_dir.to_string() + }; + let mut cmd = std::process::Command::new("docker"); + cmd.args([ + "run", + "--rm", + "--platform", + if cfg!(target_arch = "aarch64") { + "linux/arm64" + } else { + "linux/amd64" + }, + "-v", + &format!("{launcher_dir_param}:/app/status"), + "--cidfile", + network_dir.join("container-id").as_str(), + "-P", + "ghcr.io/dfinity/icp-cli-network-launcher:v11.0.0", + ]); + cmd.args(flags); + cmd.stdout(Stdio::inherit()); + cmd.stderr(Stdio::inherit()); + cmd + } + }; let watcher = wait_for_launcher_status(&launcher_dir).expect("Failed to watch launcher status"); - let child = cmd.spawn().expect("failed to spawn launcher"); + let guard = ChildGuard::spawn(&mut cmd).expect("Failed to spawn network launcher"); + let child = &guard.child; let launcher_pid = child.id(); // Wait for port file using the function from icp-network let status = watcher.await.expect("Timeout waiting for port file"); - let gateway_port = status.gateway_port; + let gateway_port = { + #[cfg(unix)] + { + status.gateway_port + } + #[cfg(windows)] + { + let container_id = fs::read_to_string(network_dir.join("container-id")) + .expect("Failed to read container ID file"); + let container_id = container_id.trim(); + let out = Command::new("docker") + .args([ + "port", + container_id, + &format!("{}/tcp", status.gateway_port), + ]) + .output() + .expect("Failed to get gateway port from docker") + .stdout; + let out = str::from_utf8(&out).unwrap().trim(); + // Output is like "0.0.0.0:32768" - extract the port + out.rsplit(':') + .next() + .expect("Invalid docker port output") + .parse::() + .expect("Failed to parse port from docker port output") + } + }; eprintln!("Gateway started on port {gateway_port}"); let instance = NetworkInstance { @@ -315,8 +401,7 @@ impl TestContext { .unwrap(), ) .expect("Gateway URL should not be already initialized"); - // Wrap child in ChildGuard - ChildGuard { child } + guard } pub(crate) fn ping_until_healthy(&self, project_dir: &Path, name: &str) { @@ -363,8 +448,73 @@ impl TestContext { } } if elapsed > timeout { + let cid = std::fs::read_to_string(project_dir.join("container_id.txt")).unwrap(); + let logs = std::process::Command::new("docker") + .args(["logs", cid.trim()]) + .output() + .unwrap(); + let logs = format!( + "{}\n{}", + String::from_utf8_lossy(&logs.stdout), + String::from_utf8_lossy(&logs.stderr) + ); + let tail = std::process::Command::new("docker") + .args(["logs", cid.trim(), "--tail", "100"]) + .output() + .unwrap(); + let tail = format!( + "{}\n{}", + String::from_utf8_lossy(&tail.stdout), + String::from_utf8_lossy(&tail.stderr) + ); + let touch_status = std::process::Command::new("docker") + .args(["exec", cid.trim(), "touch", "/app/status/test.txt"]) + .status() + .unwrap(); + let ls = std::process::Command::new("docker") + .args(["exec", cid.trim(), "ls", "-la", "/app/status"]) + .output() + .unwrap(); + let ls = format!( + "{}\n{}", + String::from_utf8_lossy(&ls.stdout), + String::from_utf8_lossy(&ls.stderr) + ); + let mount = std::process::Command::new("docker") + .args(["exec", cid.trim(), "mount"]) + .output() + .unwrap(); + let mount = format!( + "{}\n{}", + String::from_utf8_lossy(&mount.stdout), + String::from_utf8_lossy(&mount.stderr) + ); + let inspect = std::process::Command::new("docker") + .args(["inspect", cid.trim()]) + .output() + .unwrap(); + let inspect = String::from_utf8_lossy(&inspect.stdout); + let inspect_v = serde_json::from_str::(&inspect).unwrap(); + let statusdir = inspect_v[0]["Mounts"][0]["Source"].as_str().unwrap(); + let hostside = + wslpath2::convert(statusdir, None, wslpath2::Conversion::WslToWindows, false) + .unwrap(); panic!( - "Timed out waiting for network descriptor at {descriptor_path} after {elapsed}s" + "\ +Timed out waiting for network descriptor at {descriptor_path} after {elapsed}s +Container logs: +{logs} +Status dir listing: +{ls} +Container inspect: +{inspect} +Mount output: +{mount} +Logs tail: +{tail} +Hostside file exists: {}, touch status: {}", + Path::new(&hostside).join("test.txt").exists(), + touch_status.code().unwrap() ); } std::thread::sleep(std::time::Duration::from_millis(100)); @@ -425,8 +575,18 @@ impl TestContext { } pub(crate) fn docker_pull_network(&self) { + let platform = if cfg!(target_arch = "aarch64") { + "linux/arm64" + } else { + "linux/amd64" + }; Command::new("docker") - .args(["pull", "ghcr.io/dfinity/icp-cli-network-launcher:v11.0.0"]) + .args([ + "pull", + "--platform", + platform, + "ghcr.io/dfinity/icp-cli-network-launcher:v11.0.0", + ]) .assert() .success(); } diff --git a/crates/icp-cli/tests/common/mod.rs b/crates/icp-cli/tests/common/mod.rs index 41b61a83..29174145 100644 --- a/crates/icp-cli/tests/common/mod.rs +++ b/crates/icp-cli/tests/common/mod.rs @@ -1,6 +1,6 @@ #![allow(dead_code)] -use std::process::{Child, Command}; +use std::process::Command; use httptest::{Expectation, Server, matchers::*, responders::*}; @@ -8,6 +8,7 @@ pub(crate) mod clients; mod context; pub(crate) use context::TestContext; +use send_ctrlc::{InterruptibleChild, InterruptibleCommand}; #[cfg(unix)] pub(crate) const PATH_SEPARATOR: &str = ":"; @@ -79,7 +80,7 @@ pub(crate) struct TestNetwork { } pub(crate) struct ChildGuard { - child: Child, + child: InterruptibleChild, } impl ChildGuard { @@ -89,7 +90,12 @@ impl ChildGuard { use std::os::unix::process::CommandExt; cmd.process_group(0); } - let child = cmd.spawn()?; + #[cfg(windows)] + { + use std::os::windows::process::CommandExt; + cmd.creation_flags(0x00000200); // CREATE_NEW_PROCESS_GROUP + } + let child = cmd.spawn_interruptible()?; Ok(Self { child }) } @@ -105,6 +111,13 @@ impl ChildGuard { // Give the process some time to shut down gracefully std::thread::sleep(std::time::Duration::from_secs(2)); } + #[cfg(windows)] + { + use send_ctrlc::Interruptible; + _ = self.child.terminate(); // CTRL_BREAK_EVENT, required to target process group + // Give the process some time to shut down gracefully + std::thread::sleep(std::time::Duration::from_secs(2)); + } } } diff --git a/crates/icp-cli/tests/deploy_tests.rs b/crates/icp-cli/tests/deploy_tests.rs index 1e1bf573..de17cadc 100644 --- a/crates/icp-cli/tests/deploy_tests.rs +++ b/crates/icp-cli/tests/deploy_tests.rs @@ -81,7 +81,7 @@ async fn deploy() { build: steps: - type: script - command: cp {wasm} "$ICP_WASM_OUTPUT_PATH" + command: cp '{wasm}' "$ICP_WASM_OUTPUT_PATH" {NETWORK_RANDOM_PORT} {ENVIRONMENT_RANDOM_PORT} @@ -147,7 +147,7 @@ async fn deploy_twice_should_succeed() { build: steps: - type: script - command: cp {wasm} "$ICP_WASM_OUTPUT_PATH" + command: cp '{wasm}' "$ICP_WASM_OUTPUT_PATH" {NETWORK_RANDOM_PORT} {ENVIRONMENT_RANDOM_PORT} @@ -355,7 +355,7 @@ async fn deploy_prints_canister_urls() { build: steps: - type: script - command: cp {wasm} "$ICP_WASM_OUTPUT_PATH" + command: cp '{wasm}' "$ICP_WASM_OUTPUT_PATH" {NETWORK_RANDOM_PORT} {ENVIRONMENT_RANDOM_PORT} diff --git a/crates/icp-cli/tests/identity_tests.rs b/crates/icp-cli/tests/identity_tests.rs index d74ed8c4..8bc439d0 100644 --- a/crates/icp-cli/tests/identity_tests.rs +++ b/crates/icp-cli/tests/identity_tests.rs @@ -328,7 +328,7 @@ async fn identity_storage_forms() { build: steps: - type: script - command: cp {wasm} "$ICP_WASM_OUTPUT_PATH" + command: cp '{wasm}' "$ICP_WASM_OUTPUT_PATH" {NETWORK_RANDOM_PORT} {ENVIRONMENT_RANDOM_PORT} diff --git a/crates/icp-cli/tests/network_status_tests.rs b/crates/icp-cli/tests/network_status_tests.rs index b1f92cb4..3b91dee2 100644 --- a/crates/icp-cli/tests/network_status_tests.rs +++ b/crates/icp-cli/tests/network_status_tests.rs @@ -8,7 +8,7 @@ use crate::common::{NETWORK_RANDOM_PORT, TestContext}; async fn status_when_network_running() { let ctx = TestContext::new(); let project_dir = ctx.create_project_dir("icp"); - let launcher_path = ctx.launcher_path().await; + let launcher_path = ctx.launcher_path_or_nothing().await; // Project manifest write_string(&project_dir.join("icp.yaml"), NETWORK_RANDOM_PORT) @@ -47,7 +47,7 @@ async fn status_when_network_running() { async fn status_with_json() { let ctx = TestContext::new(); let project_dir = ctx.create_project_dir("icp"); - let launcher_path = ctx.launcher_path().await; + let launcher_path = ctx.launcher_path_or_nothing().await; // Project manifest write_string(&project_dir.join("icp.yaml"), NETWORK_RANDOM_PORT) @@ -96,7 +96,7 @@ async fn status_with_json() { async fn status_fixed_port() { let ctx = TestContext::new(); let project_dir = ctx.create_project_dir("icp"); - let launcher_path = ctx.launcher_path().await; + let launcher_path = ctx.launcher_path_or_nothing().await; // Project manifest with fixed port write_string( diff --git a/crates/icp-cli/tests/network_tests.rs b/crates/icp-cli/tests/network_tests.rs index 08d1b7de..9bcc6317 100644 --- a/crates/icp-cli/tests/network_tests.rs +++ b/crates/icp-cli/tests/network_tests.rs @@ -11,7 +11,6 @@ use predicates::{ }; use serde_json::Value; use serial_test::file_serial; -use sysinfo::{ProcessesToUpdate, System}; use test_tag::tag; use crate::common::{ @@ -72,7 +71,7 @@ async fn network_same_port() { .failure() .stderr(contains(format!( "Error: port 8080 is in use by the sameport-network network of the project at '{}'", - project_dir_a.canonicalize().unwrap().display() + dunce::canonicalize(&project_dir_a).unwrap().display() ))); } @@ -165,7 +164,7 @@ async fn deploy_to_other_projects_network() { build: steps: - type: script - command: cp {wasm} "$ICP_WASM_OUTPUT_PATH" + command: cp '{wasm}' "$ICP_WASM_OUTPUT_PATH" networks: - name: network-a @@ -353,12 +352,24 @@ async fn network_run_and_stop_background() { .trim() .parse() .expect("Descriptor file should contain valid JSON"); - let background_launcher_pid = descriptor - .get("child-locator") - .and_then(|cl| cl.get("pid")) - .and_then(|pid| pid.as_u64()) - .expect("Descriptor should contain launcher PID"); - let background_launcher_pid = (background_launcher_pid as usize).into(); + #[cfg(unix)] + let background_launcher_pid = { + let background_launcher_pid = descriptor + .get("child-locator") + .and_then(|cl| cl.get("pid")) + .and_then(|pid| pid.as_u64()) + .expect("Descriptor should contain launcher PID"); + (background_launcher_pid as usize).into() + }; + #[cfg(windows)] + let background_container_id = { + let background_container_id = descriptor + .get("child-locator") + .and_then(|c| c.get("id")) + .and_then(|cid| cid.as_str()) + .expect("Descriptor should contain launcher container ID"); + background_container_id.to_string() + }; // Verify network is healthy with agent.status() let agent = ic_agent::Agent::builder() @@ -373,30 +384,53 @@ async fn network_run_and_stop_background() { ); // Stop the network - ctx.icp() + let mut stop = ctx + .icp() .current_dir(&project_dir) .args(["network", "stop", "random-network"]) .assert() - .success() - .stdout(contains(format!( + .success(); + #[cfg(unix)] + { + stop = stop.stdout(contains(format!( "Stopping background network (PID: {})", background_launcher_pid - ))) - .stdout(contains("Network stopped successfully")); + ))); + } + #[cfg(windows)] + { + stop = stop.stdout(contains(format!( + "Stopping background network (container ID: {})", + &background_container_id[..12] + ))); + } + stop.stdout(contains("Network stopped successfully")); - // Verify PID file is removed + // Verify descriptor file is removed assert!( !descriptor_file_path.exists(), "Descriptor file should be removed after stopping" ); // Verify launcher process is no longer running - let mut system = System::new(); - system.refresh_processes(ProcessesToUpdate::Some(&[background_launcher_pid]), true); - assert!( - system.process(background_launcher_pid).is_none(), - "Process should no longer be running" - ); + #[cfg(unix)] + { + use sysinfo::{ProcessesToUpdate, System}; + let mut system = System::new(); + system.refresh_processes(ProcessesToUpdate::Some(&[background_launcher_pid]), true); + assert!( + system.process(background_launcher_pid).is_none(), + "Process should no longer be running" + ); + } + #[cfg(windows)] + { + let output = std::process::Command::new("docker") + .args(["inspect", &background_container_id]) + .output() + .expect("Failed to run docker inspect"); + assert!(!output.status.success(), "Container should no longer exist"); + } // Verify network is no longer reachable let status_result = agent.status().await; diff --git a/crates/icp-cli/tests/project_tests.rs b/crates/icp-cli/tests/project_tests.rs index 2644d285..d0090d91 100644 --- a/crates/icp-cli/tests/project_tests.rs +++ b/crates/icp-cli/tests/project_tests.rs @@ -25,7 +25,7 @@ fn single_canister_project() { build: steps: - type: script - command: cp {path} "$ICP_WASM_OUTPUT_PATH" + command: cp '{path}' "$ICP_WASM_OUTPUT_PATH" "#}; write_string( @@ -71,7 +71,7 @@ fn multi_canister_project() { build: steps: - type: script - command: cp {path} "$ICP_WASM_OUTPUT_PATH" + command: cp '{path}' "$ICP_WASM_OUTPUT_PATH" "#}; create_dir_all(&project_dir.join("my-canister")).expect("failed to create canister directory"); @@ -119,7 +119,7 @@ fn glob_path() { build: steps: - type: script - command: cp {path} "$ICP_WASM_OUTPUT_PATH" + command: cp '{path}' "$ICP_WASM_OUTPUT_PATH" "#}; create_dir_all(&project_dir.join("canisters/my-canister")) diff --git a/crates/icp-cli/tests/sync_tests.rs b/crates/icp-cli/tests/sync_tests.rs index 8076991c..d970058d 100644 --- a/crates/icp-cli/tests/sync_tests.rs +++ b/crates/icp-cli/tests/sync_tests.rs @@ -30,7 +30,7 @@ async fn sync_adapter_script_single() { build: steps: - type: script - command: cp {wasm} "$ICP_WASM_OUTPUT_PATH" + command: cp '{wasm}' "$ICP_WASM_OUTPUT_PATH" sync: steps: - type: script @@ -102,7 +102,7 @@ async fn sync_adapter_script_multiple() { build: steps: - type: script - command: cp {wasm} "$ICP_WASM_OUTPUT_PATH" + command: cp '{wasm}' "$ICP_WASM_OUTPUT_PATH" sync: steps: - type: script @@ -314,7 +314,7 @@ async fn sync_multiple_canisters() { build: steps: - type: script - command: cp {wasm} "$ICP_WASM_OUTPUT_PATH" + command: cp '{wasm}' "$ICP_WASM_OUTPUT_PATH" sync: steps: - type: script @@ -323,7 +323,7 @@ async fn sync_multiple_canisters() { build: steps: - type: script - command: cp {wasm} "$ICP_WASM_OUTPUT_PATH" + command: cp '{wasm}' "$ICP_WASM_OUTPUT_PATH" sync: steps: - type: script @@ -332,7 +332,7 @@ async fn sync_multiple_canisters() { build: steps: - type: script - command: cp {wasm} "$ICP_WASM_OUTPUT_PATH" + command: cp '{wasm}' "$ICP_WASM_OUTPUT_PATH" sync: steps: - type: script @@ -408,7 +408,7 @@ async fn sync_all_canisters_in_environment() { build: steps: - type: script - command: cp {wasm} "$ICP_WASM_OUTPUT_PATH" + command: cp '{wasm}' "$ICP_WASM_OUTPUT_PATH" sync: steps: - type: script @@ -417,7 +417,7 @@ async fn sync_all_canisters_in_environment() { build: steps: - type: script - command: cp {wasm} "$ICP_WASM_OUTPUT_PATH" + command: cp '{wasm}' "$ICP_WASM_OUTPUT_PATH" sync: steps: - type: script @@ -426,7 +426,7 @@ async fn sync_all_canisters_in_environment() { build: steps: - type: script - command: cp {wasm} "$ICP_WASM_OUTPUT_PATH" + command: cp '{wasm}' "$ICP_WASM_OUTPUT_PATH" sync: steps: - type: script diff --git a/crates/icp/Cargo.toml b/crates/icp/Cargo.toml index 5d257fd1..6a9d9b70 100644 --- a/crates/icp/Cargo.toml +++ b/crates/icp/Cargo.toml @@ -62,7 +62,11 @@ tokio = { workspace = true, features = ["full"] } tracing = { workspace = true } url = { workspace = true } uuid = { workspace = true } +wslpath2 = { workspace = true } zeroize = { workspace = true } +[target.'cfg(windows)'.dependencies] +winreg = { workspace = true } + [dev-dependencies] jsonschema = { workspace = true } diff --git a/crates/icp/src/canister/build/script.rs b/crates/icp/src/canister/build/script.rs index 308f93d0..33fe0ab6 100644 --- a/crates/icp/src/canister/build/script.rs +++ b/crates/icp/src/canister/build/script.rs @@ -38,7 +38,7 @@ mod tests { // Define adapter let v = Adapter { command: CommandField::Command(format!( - "echo test > {} && echo {}", + "echo test > '{}' && echo '{}'", f.path(), f.path() )), @@ -72,10 +72,10 @@ mod tests { // Define adapter let v = Adapter { command: CommandField::Commands(vec![ - format!("echo cmd-1 >> {}", f.path()), - format!("echo cmd-2 >> {}", f.path()), - format!("echo cmd-3 >> {}", f.path()), - format!("echo {}", f.path()), + format!("echo cmd-1 >> '{}'", f.path()), + format!("echo cmd-2 >> '{}'", f.path()), + format!("echo cmd-3 >> '{}'", f.path()), + format!("echo '{}'", f.path()), ]), }; diff --git a/crates/icp/src/canister/script.rs b/crates/icp/src/canister/script.rs index 28866a28..afb3b4d6 100644 --- a/crates/icp/src/canister/script.rs +++ b/crates/icp/src/canister/script.rs @@ -36,6 +36,17 @@ pub enum ScriptError { #[snafu(display("command '{command}' failed with status code {code}"))] Status { command: String, code: String }, + + #[cfg(windows)] + #[snafu(display( + "failed to locate Git for Windows (if you prefer MSYS2, set ICP_CLI_BASH_PATH to the bash.exe path)" + ))] + LocateGit, + + #[snafu(display( + "WSL bash is not supported in the Windows version of icp-cli. Use the Linux version instead." + ))] + WslBash, } pub(super) async fn execute( @@ -152,8 +163,31 @@ fn shell_command(s: &str, cwd: &Path) -> Result { } .fail(); } - + #[cfg(unix)] let mut cmd = Command::new("sh"); + #[cfg(windows)] + let mut cmd = if let Ok(bash_path) = std::env::var("ICP_CLI_BASH_PATH") { + if bash_path == r"C:\Windows\System32\bash.exe" { + return WslBashSnafu.fail(); + } + Command::new(bash_path) + } else { + use winreg::{RegKey, enums::*}; + let git_for_windows_path = if let Ok(lm_path) = RegKey::predef(HKEY_LOCAL_MACHINE) + .open_subkey(r"SOFTWARE\GitForWindows") + .and_then(|key| key.get_value::("InstallPath")) + { + lm_path + } else if let Ok(cu_path) = RegKey::predef(HKEY_CURRENT_USER) + .open_subkey(r"SOFTWARE\GitForWindows") + .and_then(|key| key.get_value::("InstallPath")) + { + cu_path + } else { + return LocateGitSnafu.fail(); + }; + Command::new(PathBuf::from(git_for_windows_path).join("bin/bash.exe")) + }; cmd.args(["-c", s]); cmd.current_dir(cwd); Ok(cmd) diff --git a/crates/icp/src/context/init.rs b/crates/icp/src/context/init.rs index 8f7bd70b..2af6719e 100644 --- a/crates/icp/src/context/init.rs +++ b/crates/icp/src/context/init.rs @@ -1,12 +1,11 @@ use std::{env::current_dir, sync::Arc}; -use console::Term; use snafu::prelude::*; use crate::canister::build::Builder; use crate::canister::recipe::handlebars::Handlebars; use crate::canister::sync::Syncer; -use crate::context::Context; +use crate::context::{Context, TermWriter}; use crate::directories::{Access as _, Directories}; use crate::prelude::*; use crate::store_artifact::ArtifactStore; @@ -33,7 +32,7 @@ pub enum ContextInitError { pub fn initialize( project_root_override: Option, - term: Term, + term: TermWriter, debug: bool, password_func: PasswordFunc, ) -> Result { @@ -42,7 +41,7 @@ pub fn initialize( // Project Root let project_root_locate = Arc::new(manifest::ProjectRootLocateImpl::new( - current_dir() + dunce::canonicalize(current_dir().context(CwdSnafu)?) .context(CwdSnafu)? .try_into() .context(Utf8PathSnafu)?, // cwd diff --git a/crates/icp/src/context/mod.rs b/crates/icp/src/context/mod.rs index cc610276..2a7d11f9 100644 --- a/crates/icp/src/context/mod.rs +++ b/crates/icp/src/context/mod.rs @@ -1,5 +1,6 @@ use console::Term; -use std::sync::Arc; +use std::{io::Write, sync::Arc}; +use tracing::debug; use url::Url; use crate::{ @@ -69,7 +70,7 @@ pub enum CanisterSelection { pub struct Context { /// Terminal for printing messages for the user to see - pub term: Term, + pub term: TermWriter, /// Various cli-related directories (cache, configuration, etc). pub dirs: Arc, @@ -477,7 +478,10 @@ impl Context { /// Creates a test context with all mocks pub fn mocked() -> Context { Context { - term: Term::stderr(), + term: TermWriter { + debug: false, + raw_term: Term::stderr(), + }, dirs: Arc::new(crate::directories::UnimplementedMockDirs), ids: Arc::new(crate::store_id::mock::MockInMemoryIdStore::new()), artifacts: Arc::new(crate::store_artifact::MockInMemoryArtifactStore::new()), @@ -492,6 +496,49 @@ impl Context { } } +#[derive(Debug, Clone)] +pub struct TermWriter { + pub debug: bool, + pub raw_term: Term, +} + +impl TermWriter { + pub fn write_line(&self, line: &str) -> std::io::Result<()> { + if !self.debug { + writeln!(&self.raw_term, "{}", line)?; + } + debug!("{}", line); + Ok(()) + } +} + +impl Write for TermWriter { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + (&*self).write(buf) + } + + fn flush(&mut self) -> std::io::Result<()> { + (&*self).flush() + } +} + +impl Write for &TermWriter { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + if !self.debug { + (&self.raw_term).write(buf)?; + } + debug!("{}", String::from_utf8_lossy(buf).trim()); + Ok(buf.len()) + } + + fn flush(&mut self) -> std::io::Result<()> { + if !self.debug { + self.raw_term.flush()?; + } + Ok(()) + } +} + #[derive(Debug, Snafu)] pub enum GetIdentityError { #[snafu(display("failed to load identity"))] diff --git a/crates/icp/src/network/managed/docker.rs b/crates/icp/src/network/managed/docker.rs index 46dad327..abc14879 100644 --- a/crates/icp/src/network/managed/docker.rs +++ b/crates/icp/src/network/managed/docker.rs @@ -6,8 +6,13 @@ use bollard::{ Docker, errors::Error as BollardError, query_parameters::{ - CreateContainerOptions, CreateImageOptions, InspectContainerOptions, - RemoveContainerOptions, StartContainerOptions, StopContainerOptions, WaitContainerOptions, + CreateContainerOptions, + CreateImageOptions, + InspectContainerOptions, + // RemoveContainerOptions, + StartContainerOptions, + StopContainerOptions, + WaitContainerOptions, }, secret::{ContainerCreateBody, HostConfig, Mount, MountTypeEnum, PortBinding}, }; @@ -17,6 +22,7 @@ use itertools::Itertools; use snafu::ResultExt; use snafu::{OptionExt, Snafu}; use tokio::select; +use wslpath2::Conversion; use crate::network::{ ManagedImageConfig, @@ -42,7 +48,31 @@ pub async fn spawn_docker_launcher( status_dir, mounts, } = image_config; - let host_status_dir = Utf8TempDir::new().context(CreateStatusDirSnafu)?; + let platform = if let Some(p) = platform { + p.clone() + } else if cfg!(target_arch = "aarch64") { + "linux/arm64".to_string() + } else { + "linux/amd64".to_string() + }; + let wsl2_distro = std::env::var("ICP_CLI_DOCKER_WSL2_MODE").ok(); + let wsl2_distro = wsl2_distro.as_deref(); + let wsl2_convert = cfg!(windows) && wsl2_distro.is_some(); + let host_status_tmpdir = if wsl2_convert { + let host_tmp = wslpath2::convert("/tmp", wsl2_distro, Conversion::WslToWindows, true) + .map_err(|e| { + WslPathConvertSnafu { + msg: e.to_string(), + path: "/tmp", + } + .build() + })?; + Utf8TempDir::new_in(&host_tmp).context(WslCreateTmpDirSnafu)? + } else { + Utf8TempDir::new().context(CreateStatusDirSnafu)? + }; + let host_status_dir = host_status_tmpdir.path(); + let host_status_dir_param = convert_path(wsl2_convert, wsl2_distro, host_status_tmpdir.path())?; let socket = match std::env::var("DOCKER_HOST").ok() { Some(sock) => sock, #[cfg(unix)] @@ -77,8 +107,13 @@ pub async fn spawn_docker_launcher( #[cfg(windows)] None => r"\\.\pipe\docker_engine".to_string(), }; - let docker = Docker::connect_with_local(&socket, 120, bollard::API_DEFAULT_VERSION) - .context(ConnectDockerSnafu { socket: &socket })?; + let docker = if socket.starts_with("tcp://") || socket.starts_with("http://") { + let http_addr = socket.replace("tcp://", "http://"); + Docker::connect_with_http(&http_addr, 120, bollard::API_DEFAULT_VERSION) + } else { + Docker::connect_with_local(&socket, 120, bollard::API_DEFAULT_VERSION) + } + .context(ConnectDockerSnafu { socket: &socket })?; let portmap: HashMap<_, _> = port_mapping .iter() .map(|mapping| { @@ -101,6 +136,7 @@ pub async fn spawn_docker_launcher( let (host, rest) = m.split_once(':').context(ParseMountSnafu { mount: m })?; let host = dunce::canonicalize(host).context(ProcessMountSourceSnafu { path: host })?; let host = PathBuf::try_from(host.clone()).context(BadPathSnafu)?; + let host_param = convert_path(wsl2_convert, wsl2_distro, &host)?; let (target, flags) = match rest.split_once(':') { Some((t, f)) => (t, Some(f)), None => (rest, None), @@ -114,7 +150,7 @@ pub async fn spawn_docker_launcher( .transpose()?; Ok::<_, DockerLauncherError>(Mount { target: Some(target.to_string()), - source: Some(host.to_string()), + source: Some(host_param), typ: Some(MountTypeEnum::BIND), read_only, ..<_>::default() @@ -122,7 +158,7 @@ pub async fn spawn_docker_launcher( }) .chain([Ok(Mount { target: Some(status_dir.to_string()), - source: Some(host_status_dir.path().to_string()), + source: Some(host_status_dir_param), typ: Some(MountTypeEnum::BIND), read_only: Some(false), ..<_>::default() @@ -139,7 +175,7 @@ pub async fn spawn_docker_launcher( .create_image( Some(CreateImageOptions { from_image: Some(image.clone()), - platform: platform.clone().unwrap_or_default(), + platform: platform.clone(), ..<_>::default() }), None, @@ -154,8 +190,8 @@ pub async fn spawn_docker_launcher( }; let container_resp = docker .create_container( - platform.clone().map(|p| CreateContainerOptions { - platform: p, + Some(CreateContainerOptions { + platform: platform.clone(), ..<_>::default() }), ContainerCreateBody { @@ -176,8 +212,14 @@ pub async fn spawn_docker_launcher( host_config: Some(HostConfig { port_bindings: Some(portmap), mounts: Some(mounts), - binds: Some(volumes.to_vec()), + binds: Some( + volumes + .iter() + .map(|v| convert_volume(wsl2_convert, wsl2_distro, v)) + .try_collect()?, + ), shm_size: *shm_size, + ..<_>::default() }), ..<_>::default() @@ -187,6 +229,7 @@ pub async fn spawn_docker_launcher( .context(CreateContainerSnafu { image_name: image })?; let container_id = container_resp.id; eprintln!("Created container {}", &container_id[..12]); + std::fs::write("container_id.txt", &container_id).unwrap(); let guard = AsyncDropper::new(DockerDropGuard { container_id: Some(container_id), docker: Some(docker), @@ -194,7 +237,7 @@ pub async fn spawn_docker_launcher( }); let container_id = guard.container_id.as_ref().unwrap(); let docker = guard.docker.as_ref().unwrap(); - let watcher = wait_for_launcher_status(host_status_dir.path())?; + let watcher = wait_for_launcher_status(host_status_dir)?; docker .start_container(container_id, None::) .await @@ -306,26 +349,31 @@ pub async fn stop_docker_launcher( container_id: &str, rm_on_exit: bool, ) -> Result<(), StopContainerError> { - let docker = Docker::connect_with_local(socket, 120, bollard::API_DEFAULT_VERSION) - .context(ConnectSnafu { socket })?; + let docker = if socket.starts_with("tcp://") { + let http_addr = socket.replace("tcp://", "http://"); + Docker::connect_with_http(&http_addr, 120, bollard::API_DEFAULT_VERSION) + } else { + Docker::connect_with_local(socket, 120, bollard::API_DEFAULT_VERSION) + } + .context(ConnectSnafu { socket })?; stop(&docker, container_id, rm_on_exit).await } async fn stop( docker: &Docker, container_id: &str, - rm_on_exit: bool, + _rm_on_exit: bool, ) -> Result<(), StopContainerError> { docker .stop_container(container_id, None::) .await .context(StopSnafu { container_id })?; - if rm_on_exit { - docker - .remove_container(container_id, None::) - .await - .context(RemoveSnafu { container_id })?; - } + // if rm_on_exit { + // docker + // .remove_container(container_id, None::) + // .await + // .context(RemoveSnafu { container_id })?; + // } Ok(()) } @@ -348,6 +396,54 @@ pub enum StopContainerError { }, } +fn convert_path( + convert: bool, + distro: Option<&str>, + path: &Path, +) -> Result { + if convert { + wslpath2::convert(path.as_str(), distro, Conversion::WindowsToWsl, true).map_err(|e| { + WslPathConvertSnafu { + msg: e.to_string(), + path: path.to_path_buf(), + } + .build() + }) + } else { + Ok(path.to_string()) + } +} + +fn convert_volume( + convert: bool, + distro: Option<&str>, + volume: &str, +) -> Result { + // docker's actual parsing logic, clunky as it is + let (host, rest) = if volume.chars().next().unwrap().is_ascii_alphabetic() + && volume.chars().nth(1).unwrap() == ':' + { + let split_point = volume[2..] + .find(':') + .map(|idx| idx + 2) + .context(ParseMountSnafu { mount: volume })?; + (&volume[..split_point], &volume[split_point + 1..]) + } else { + volume + .split_once(':') + .context(ParseMountSnafu { mount: volume })? + }; + let host_param = if host.contains(&['/', '\\'][..]) { + let host_path = + dunce::canonicalize(host).context(ProcessMountSourceSnafu { path: host })?; + let host_path = PathBuf::try_from(host_path.clone()).context(BadPathSnafu)?; + convert_path(convert, distro, &host_path)? + } else { + host.to_string() + }; + Ok(format!("{host_param}:{rest}")) +} + #[derive(Debug, Snafu)] pub enum DockerLauncherError { #[snafu(display("failed to connect to docker daemon at {socket} (is it running?)"))] @@ -459,6 +555,10 @@ pub enum DockerLauncherError { source: std::str::Utf8Error, display: String, }, + #[snafu(display("failed to convert path to WSL2: {msg}"))] + WslPathConvertError { msg: String, path: PathBuf }, + #[snafu(display("failed to create temporary directory in WSL2"))] + WslCreateTmpDirError { source: std::io::Error }, } #[derive(Default)] diff --git a/crates/icp/src/network/managed/run.rs b/crates/icp/src/network/managed/run.rs index 3e06ceaa..09ed02d8 100644 --- a/crates/icp/src/network/managed/run.rs +++ b/crates/icp/src/network/managed/run.rs @@ -35,7 +35,7 @@ use uuid::Uuid; use crate::{ fs::{create_dir_all, lock::LockError, remove_dir_all}, network::{ - Gateway, Managed, ManagedMode, NetworkDirectory, Port, + Gateway, Managed, ManagedImageConfig, ManagedMode, NetworkDirectory, Port, config::{ChildLocator, NetworkDescriptorGatewayPort, NetworkDescriptorModel}, directory::{ClaimPortError, SaveNetworkDescriptorError, save_network_descriptors}, managed::{ @@ -157,6 +157,16 @@ async fn run_network_launcher( }; (ShutdownGuard::Container(guard), instance, gateway, locator) } + ManagedMode::Launcher { gateway } if cfg!(windows) /* todo machine setting for unix */ => { + let image_config = transform_native_launcher_to_container(gateway.clone()); + let (guard, instance, locator) = + spawn_docker_launcher(&image_config).await?; + let gateway = NetworkDescriptorGatewayPort { + port: instance.gateway_port, + fixed: false, + }; + (ShutdownGuard::Container(guard), instance, gateway, locator) + } ManagedMode::Launcher { gateway } => { if root.state_dir().exists() { remove_dir_all(&root.state_dir()).context(RemoveDirAllSnafu)?; @@ -175,10 +185,6 @@ async fn run_network_launcher( &root.state_dir(), ) .await?; - if background { - // background means we're using stdio files - otherwise the launcher already prints this - eprintln!("Network started on port {}", instance.gateway_port); - } let gateway = NetworkDescriptorGatewayPort { port: instance.gateway_port, fixed: matches!(gateway.port, Port::Fixed(_)), @@ -186,6 +192,10 @@ async fn run_network_launcher( (ShutdownGuard::Process(child), instance, gateway, locator) } }; + if background { + // background means we're using stdio files - otherwise the launcher already prints this + eprintln!("Network started on port {}", instance.gateway_port); + } let candid_ui_canister_id = initialize_network( &format!("http://localhost:{}", instance.gateway_port) .parse() @@ -233,6 +243,27 @@ async fn run_network_launcher( Ok(()) } +fn transform_native_launcher_to_container(gateway: Gateway) -> ManagedImageConfig { + let port = match gateway.port { + Port::Fixed(port) => port, + Port::Random => 0, + }; + ManagedImageConfig { + image: "ghcr.io/dfinity/icp-cli-network-launcher:latest".to_string(), + port_mapping: vec![format!("{port}:4943")], + rm_on_exit: true, + args: vec!["--ii".to_string()], + entrypoint: None, + environment: vec![], + volumes: vec![], + platform: None, + user: None, + shm_size: None, + status_dir: "/app/status".to_string(), + mounts: vec![], + } +} + enum ShutdownGuard { Container(AsyncDropper), Process(AsyncDropper), @@ -314,13 +345,13 @@ fn safe_eprintln(msg: &str) { async fn wait_for_shutdown(guard: &mut ShutdownGuard) -> ShutdownReason { match guard { ShutdownGuard::Container(_) => { - _ = ctrl_c().await; + stop_signal().await; safe_eprintln("Received Ctrl-C, shutting down PocketIC..."); ShutdownReason::CtrlC } ShutdownGuard::Process(child) => { select!( - _ = ctrl_c() => { + _ = stop_signal() => { safe_eprintln("Received Ctrl-C, shutting down PocketIC..."); ShutdownReason::CtrlC } @@ -333,6 +364,28 @@ async fn wait_for_shutdown(guard: &mut ShutdownGuard) -> ShutdownReason { } } +#[cfg(unix)] +async fn stop_signal() { + use tokio::signal::unix::{SignalKind, signal}; + let mut sigterm = signal(SignalKind::terminate()).unwrap(); + select! { + _ = ctrl_c() => {}, + _ = sigterm.recv() => {}, + } +} + +#[cfg(windows)] +async fn stop_signal() { + use tokio::signal::windows::{ctrl_break, ctrl_close}; + let mut ctrl_break = ctrl_break().unwrap(); + let mut ctrl_close = ctrl_close().unwrap(); + select! { + _ = ctrl_c() => {}, + _ = ctrl_break.recv() => {}, + _ = ctrl_close.recv() => {}, + } +} + /// Yields immediately if the child exits. pub async fn notice_child_exit(child: &mut Child) -> ChildExitError { loop { diff --git a/crates/icp/src/network/mod.rs b/crates/icp/src/network/mod.rs index a75ae613..0fc3f38e 100644 --- a/crates/icp/src/network/mod.rs +++ b/crates/icp/src/network/mod.rs @@ -84,26 +84,39 @@ pub enum ManagedMode { impl Default for ManagedMode { fn default() -> Self { + Self::default_for_port(0) + } +} + +impl ManagedMode { + pub fn default_for_port(port: u16) -> Self { ManagedMode::Launcher { - gateway: Gateway::default(), + gateway: Gateway { + host: default_host(), + port: if port == 0 { + Port::Random + } else { + Port::Fixed(port) + }, + }, } } } #[derive(Clone, Debug, Deserialize, PartialEq, JsonSchema, Serialize)] pub struct ManagedImageConfig { - image: String, - port_mapping: Vec, - rm_on_exit: bool, - args: Vec, - entrypoint: Option>, - environment: Vec, - volumes: Vec, - platform: Option, - user: Option, - shm_size: Option, - status_dir: String, - mounts: Vec, + pub image: String, + pub port_mapping: Vec, + pub rm_on_exit: bool, + pub args: Vec, + pub entrypoint: Option>, + pub environment: Vec, + pub volumes: Vec, + pub platform: Option, + pub user: Option, + pub shm_size: Option, + pub status_dir: String, + pub mounts: Vec, } #[derive(Clone, Debug, Deserialize, PartialEq, JsonSchema, Serialize)] diff --git a/dist-workspace.toml b/dist-workspace.toml index fb7980d4..c36a157a 100644 --- a/dist-workspace.toml +++ b/dist-workspace.toml @@ -10,7 +10,13 @@ ci = "github" # The installers to generate for each app installers = ["shell"] # Target platforms to build apps for (Rust target-triple syntax) -targets = ["aarch64-apple-darwin", "aarch64-unknown-linux-gnu", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu"] +targets = [ + "aarch64-apple-darwin", + "aarch64-unknown-linux-gnu", + "x86_64-apple-darwin", + "x86_64-unknown-linux-gnu", + "x86_64-pc-windows-msvc", +] # Path that installers should place binaries in install-path = "CARGO_HOME" # Whether to install an updater program diff --git a/docs/getting-started.md b/docs/getting-started.md index bdaab698..7a810d1d 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -254,6 +254,10 @@ Now that you have your first canister running, explore: - Ensure all required tools are installed and in PATH - Check language-specific prerequisites +**Build scripts fail on Windows** +- Build scripts require Git for Windows or MSYS2. If detection fails, set `ICP_CLI_BASH_PATH` to the path of your bash executable (e.g., `C:\Program Files\Git\bin\bash.exe` or `C:\msys64\usr\bin\bash.exe`) +- Note: WSL bash (`C:\Windows\System32\bash.exe`) is not supported + **Network connection fails** - Verify `icp network start` is running in another terminal - The network launcher is automatically downloaded on first use. If you experience issues, you can manually set `ICP_CLI_NETWORK_LAUNCHER_PATH` to a specific launcher binary for debugging