diff --git a/e2e/backend/test_aqua_cosign b/e2e/backend/test_aqua_cosign old mode 100755 new mode 100644 diff --git a/e2e/backend/test_aqua_github_attestations b/e2e/backend/test_aqua_github_attestations old mode 100755 new mode 100644 diff --git a/e2e/backend/test_aqua_slsa b/e2e/backend/test_aqua_slsa old mode 100755 new mode 100644 diff --git a/e2e/backend/test_backend_missing_deps b/e2e/backend/test_backend_missing_deps new file mode 100644 index 0000000000..c6d7aeb60e --- /dev/null +++ b/e2e/backend/test_backend_missing_deps @@ -0,0 +1,130 @@ +#!/usr/bin/env bash + +# Test that package manager backends show helpful errors when their dependencies are missing + +# Create a PATH that has mise but not package managers +# ROOT is set by run_test script +MISE_DIR="${CARGO_TARGET_DIR:-$ROOT/target}/debug" +export PATH="$MISE_DIR:/usr/bin:/bin:/usr/sbin:/sbin" + +# Track overall test status +TEST_FAILED=0 + +# Helper function to check if a command exists in PATH +check_command_missing() { + local cmd=$1 + if command -v "$cmd" >/dev/null 2>&1; then + echo "ERROR: $cmd is found in PATH, test cannot verify missing dependency behavior" + exit 1 + fi +} + +# Helper function to test an expected error message +test_error_message() { + local test_name=$1 + local output=$2 + local expected=$3 + + if [[ $output == *"$expected"* ]]; then + echo "✓ $test_name" + else + echo "ERROR: $test_name failed" + echo "Expected: $expected" + echo "Got: $output" + TEST_FAILED=1 + return 1 + fi +} + +# Helper function to test a command that should fail with expected error +test_backend_error() { + local backend_name=$1 + local command=$2 + local expected_error=$3 + + local output + if output=$(eval "$command" 2>&1); then + echo "ERROR: $backend_name command should have failed but succeeded" + TEST_FAILED=1 + return 1 + else + output=$(eval "$command" 2>&1 || true) + test_error_message "$backend_name shows correct error" "$output" "$expected_error" + fi +} + +# Test npm backend +test_npm() { + echo "Testing npm backend with missing npm..." + check_command_missing "npm" + + # Test ls-remote + test_backend_error "npm" "mise ls-remote npm:test-package" "npm is required but not found" + + # Check for helpful install instructions + local output + output=$(mise ls-remote npm:test-package 2>&1 || true) + test_error_message "npm suggests installing node" "$output" "mise use node@latest" + test_error_message "npm mentions npm is needed for queries" "$output" "npm is required for querying package information" + + # Test install + test_backend_error "npm" "mise install npm:test-package@latest" "npm is required but not found" + + # Test with bun mode enabled but npm still missing + echo "Testing npm backend with bun mode enabled but npm missing..." + export MISE_NPM_BUN=true + test_backend_error "npm (bun mode)" "mise ls-remote npm:test-package" "npm is required but not found" + output=$(mise ls-remote npm:test-package 2>&1 || true) + test_error_message "npm (bun mode) shows npm is required for queries" "$output" "npm is required for querying package information" + unset MISE_NPM_BUN +} + +# Test cargo backend +test_cargo() { + echo "Testing cargo backend with missing cargo..." + + # Remove cargo from PATH if it exists + local new_path="" + local IFS=':' + read -ra PATHS <<<"$PATH" + for p in "${PATHS[@]}"; do + if [[ ! -x "$p/cargo" ]]; then + [[ -z $new_path ]] && new_path="$p" || new_path="$new_path:$p" + fi + done + export PATH="$new_path" + + check_command_missing "cargo" + test_backend_error "cargo" "mise install cargo:tiny@latest" "cargo is required but not found" +} + +# Test go backend +test_go() { + echo "Testing go backend with missing go..." + + # Remove go from PATH if it exists + local new_path="" + local IFS=':' + read -ra PATHS <<<"$PATH" + for p in "${PATHS[@]}"; do + if [[ ! -x "$p/go" ]]; then + [[ -z $new_path ]] && new_path="$p" || new_path="$new_path:$p" + fi + done + export PATH="$new_path" + + check_command_missing "go" + test_backend_error "go" "mise ls-remote go:github.com/test/test" "go is required but not found" +} + +# Run all tests +test_npm +test_cargo +test_go + +if [ $TEST_FAILED -eq 0 ]; then + echo "✓ backend dependency error test passed" +else + echo "✗ Some tests failed" + exit 1 +fi diff --git a/e2e/backend/test_dotnet b/e2e/backend/test_dotnet old mode 100755 new mode 100644 diff --git a/e2e/backend/test_github_docker_compose b/e2e/backend/test_github_docker_compose old mode 100755 new mode 100644 diff --git a/e2e/backend/test_github_tools b/e2e/backend/test_github_tools old mode 100755 new mode 100644 diff --git a/e2e/backend/test_github_url_tracking b/e2e/backend/test_github_url_tracking old mode 100755 new mode 100644 diff --git a/e2e/backend/test_http_binary_clean b/e2e/backend/test_http_binary_clean old mode 100755 new mode 100644 diff --git a/e2e/backend/test_http_caching b/e2e/backend/test_http_caching old mode 100755 new mode 100644 diff --git a/e2e/backend/test_npm_bun b/e2e/backend/test_npm_bun index 55a846e85c..990d69fb9f 100644 --- a/e2e/backend/test_npm_bun +++ b/e2e/backend/test_npm_bun @@ -1,12 +1,80 @@ #!/usr/bin/env bash -cat >mise.toml </dev/null 2>&1; then + echo "npm not available in test environment, installing node..." + # Disable GPG verification for faster test + export MISE_GPG_VERIFY=false + assert_succeed "mise use node@20" +fi -assert "mise x npm:prettier@3.1.0 -- prettier -v" "3.1.0" -assert "FORCE_COLOR=0 mise x npm:@antfu/ni@0.21.12 -- ni -v 2>/dev/null | head -n1" "@antfu/ni v0.21.12" -assert_succeed "mise install npm:tldr@3.4.0" +# Ensure bun is available +if ! command -v bun >/dev/null 2>&1; then + echo "bun not available in test environment, installing..." + assert_succeed "mise use bun@latest" +fi + +# Test 1: With npm available but bun mode disabled (default) +echo "Testing npm backend with npm available..." +unset MISE_NPM_BUN + +# Test listing versions - should succeed +assert_succeed "mise ls-remote npm:tiny >/dev/null" +echo "✓ npm backend lists versions using npm" + +# Test latest version - should succeed +assert_succeed "mise latest npm:tiny >/dev/null" +echo "✓ npm backend gets latest version using npm" + +# Test 2: With bun mode enabled - npm backend should still use npm for version queries +echo "Testing npm backend with bun mode enabled..." +export MISE_NPM_BUN=true + +# The npm backend always uses npm for version queries (bun info requires package.json) +# This is documented in the TODOs in src/backend/npm.rs +assert_succeed "mise ls-remote npm:tiny >/dev/null" +echo "✓ npm backend uses npm for version queries even in bun mode" + +# Test latest version - should succeed +assert_succeed "mise latest npm:tiny >/dev/null" +echo "✓ npm backend gets latest version using npm even in bun mode" + +unset MISE_NPM_BUN + +# Test 3: Test installation with bun mode +echo "Testing npm package installation with bun..." +export MISE_NPM_BUN=true + +# Clean up any previous installation +mise uninstall npm:tiny@latest >/dev/null 2>&1 || true + +# Install a small package using bun +assert_succeed "mise install npm:tiny@latest >/dev/null 2>&1" +echo "✓ npm backend successfully installs package using bun" + +# Verify the package was installed by executing it +assert_succeed "mise exec npm:tiny@latest -- tiny --version >/dev/null" +echo "✓ Installed package can be executed" + +# Clean up +mise uninstall npm:tiny@latest >/dev/null 2>&1 || true + +unset MISE_NPM_BUN + +# Test 4: Test installation with npm mode (default) +echo "Testing npm package installation with npm (default)..." + +# Clean up any previous installation +mise uninstall npm:tiny@latest >/dev/null 2>&1 || true + +# Install using npm (default) +assert_succeed "mise install npm:tiny@latest >/dev/null 2>&1" +echo "✓ npm backend successfully installs package using npm" + +# Clean up +mise uninstall npm:tiny@latest >/dev/null 2>&1 || true + +echo "✓ npm bun behavior test completed" diff --git a/e2e/backend/test_vfox_backend_npm b/e2e/backend/test_vfox_backend_npm old mode 100755 new mode 100644 diff --git a/e2e/cli/test_env_redacted_flags b/e2e/cli/test_env_redacted_flags old mode 100755 new mode 100644 diff --git a/e2e/cli/test_error_display b/e2e/cli/test_error_display old mode 100755 new mode 100644 diff --git a/e2e/cli/test_generate_tool_stub b/e2e/cli/test_generate_tool_stub old mode 100755 new mode 100644 diff --git a/e2e/cli/test_install_dry_run b/e2e/cli/test_install_dry_run old mode 100755 new mode 100644 diff --git a/e2e/cli/test_install_parallel_failure b/e2e/cli/test_install_parallel_failure old mode 100755 new mode 100644 diff --git a/e2e/cli/test_lock b/e2e/cli/test_lock old mode 100755 new mode 100644 diff --git a/e2e/cli/test_lock_creation b/e2e/cli/test_lock_creation old mode 100755 new mode 100644 diff --git a/e2e/cli/test_lock_future b/e2e/cli/test_lock_future old mode 100755 new mode 100644 diff --git a/e2e/cli/test_mcp b/e2e/cli/test_mcp old mode 100755 new mode 100644 diff --git a/e2e/cli/test_mcp_protocol b/e2e/cli/test_mcp_protocol old mode 100755 new mode 100644 diff --git a/e2e/cli/test_nonexistent_cwd b/e2e/cli/test_nonexistent_cwd old mode 100755 new mode 100644 diff --git a/e2e/cli/test_set_env b/e2e/cli/test_set_env old mode 100755 new mode 100644 diff --git a/e2e/cli/test_tool b/e2e/cli/test_tool old mode 100755 new mode 100644 diff --git a/e2e/cli/test_tool_stub_basic b/e2e/cli/test_tool_stub_basic old mode 100755 new mode 100644 diff --git a/e2e/cli/test_tool_stub_errors b/e2e/cli/test_tool_stub_errors old mode 100755 new mode 100644 diff --git a/e2e/cli/test_tool_stub_http b/e2e/cli/test_tool_stub_http old mode 100755 new mode 100644 diff --git a/e2e/cli/test_upgrade_parallel_failure b/e2e/cli/test_upgrade_parallel_failure old mode 100755 new mode 100644 diff --git a/e2e/cli/test_use_dry_run b/e2e/cli/test_use_dry_run old mode 100755 new mode 100644 diff --git a/e2e/core/test_java_url_tracking b/e2e/core/test_java_url_tracking old mode 100755 new mode 100644 diff --git a/e2e/env/test_env_path_node_regression b/e2e/env/test_env_path_node_regression old mode 100755 new mode 100644 diff --git a/e2e/env/test_env_path_resolution b/e2e/env/test_env_path_resolution old mode 100755 new mode 100644 diff --git a/e2e/env/test_poetry_path b/e2e/env/test_poetry_path old mode 100755 new mode 100644 diff --git a/e2e/generate/test_generate_devcontainer b/e2e/generate/test_generate_devcontainer old mode 100755 new mode 100644 diff --git a/e2e/generate/test_generate_task_docs b/e2e/generate/test_generate_task_docs old mode 100755 new mode 100644 diff --git a/e2e/generate/test_generate_tool_stub b/e2e/generate/test_generate_tool_stub old mode 100755 new mode 100644 diff --git a/e2e/generate/test_generate_tool_stub_jq b/e2e/generate/test_generate_tool_stub_jq old mode 100755 new mode 100644 diff --git a/e2e/secrets/test_secrets_non_strict b/e2e/secrets/test_secrets_non_strict old mode 100755 new mode 100644 diff --git a/e2e/tasks/test_task_depends_post_multiple b/e2e/tasks/test_task_depends_post_multiple old mode 100755 new mode 100644 diff --git a/e2e/tasks/test_task_display_truncation b/e2e/tasks/test_task_display_truncation old mode 100755 new mode 100644 diff --git a/e2e/tasks/test_task_env_directives b/e2e/tasks/test_task_env_directives old mode 100755 new mode 100644 diff --git a/e2e/tasks/test_task_ls_usage b/e2e/tasks/test_task_ls_usage old mode 100755 new mode 100644 diff --git a/e2e/tasks/test_task_parallel_execution b/e2e/tasks/test_task_parallel_execution old mode 100755 new mode 100644 diff --git a/e2e/tasks/test_task_remote_git_https b/e2e/tasks/test_task_remote_git_https old mode 100755 new mode 100644 diff --git a/e2e/tasks/test_task_remote_git_ssh b/e2e/tasks/test_task_remote_git_ssh old mode 100755 new mode 100644 diff --git a/e2e/tasks/test_task_sequence_failure b/e2e/tasks/test_task_sequence_failure old mode 100755 new mode 100644 diff --git a/e2e/tasks/test_task_timeout b/e2e/tasks/test_task_timeout old mode 100755 new mode 100644 diff --git a/src/backend/cargo.rs b/src/backend/cargo.rs index d4804f5f6b..b985484118 100644 --- a/src/backend/cargo.rs +++ b/src/backend/cargo.rs @@ -61,6 +61,16 @@ impl Backend for CargoBackend { } async fn install_version_(&self, ctx: &InstallContext, tv: ToolVersion) -> Result { + // Check if cargo is available + self.ensure_dependency( + &ctx.config, + "cargo", + "To use cargo packages with mise, you need to install Rust first:\n\ + mise use rust@latest\n\n\ + Or install Rust via https://rustup.rs/", + ) + .await?; + let config = ctx.config.clone(); let install_arg = format!("{}@{}", self.tool_name(), tv.version); let registry_name = &Settings::get().cargo.registry_name; diff --git a/src/backend/dotnet.rs b/src/backend/dotnet.rs index 5cc6fb8bad..a75b39725c 100644 --- a/src/backend/dotnet.rs +++ b/src/backend/dotnet.rs @@ -64,6 +64,16 @@ impl Backend for DotnetBackend { ) -> eyre::Result { Settings::get().ensure_experimental("dotnet backend")?; + // Check if dotnet is available + self.ensure_dependency( + &ctx.config, + "dotnet", + "To use dotnet tools with mise, you need to install .NET SDK first:\n\ + mise use dotnet@latest\n\n\ + Or install .NET SDK via https://dotnet.microsoft.com/download", + ) + .await?; + let mut cli = CmdLineRunner::new("dotnet") .arg("tool") .arg("install") diff --git a/src/backend/gem.rs b/src/backend/gem.rs index e07462eea5..0876a66b2c 100644 --- a/src/backend/gem.rs +++ b/src/backend/gem.rs @@ -47,6 +47,15 @@ impl Backend for GemBackend { async fn install_version_(&self, ctx: &InstallContext, tv: ToolVersion) -> Result { Settings::get().ensure_experimental("gem backend")?; + // Check if gem is available + self.ensure_dependency( + &ctx.config, + "gem", + "To use gem packages with mise, you need to install Ruby first:\n\ + mise use ruby@latest", + ) + .await?; + CmdLineRunner::new("gem") .arg("install") .arg(self.tool_name()) diff --git a/src/backend/go.rs b/src/backend/go.rs index d2a3bc8b55..4fb9bd92c1 100644 --- a/src/backend/go.rs +++ b/src/backend/go.rs @@ -1,11 +1,12 @@ +use crate::backend::Backend; use crate::backend::backend_type::BackendType; use crate::cli::args::BackendArg; use crate::cmd::CmdLineRunner; +use crate::config::Config; use crate::config::Settings; use crate::install_context::InstallContext; use crate::timeout; use crate::toolset::ToolVersion; -use crate::{backend::Backend, config::Config}; use async_trait::async_trait; use itertools::Itertools; use std::{fmt::Debug, sync::Arc}; @@ -31,6 +32,16 @@ impl Backend for GoBackend { } async fn _list_remote_versions(&self, config: &Arc) -> eyre::Result> { + // Check if go is available + self.ensure_dependency( + config, + "go", + "To use go packages with mise, you need to install Go first:\n\ + mise use go@latest\n\n\ + Or install Go via https://go.dev/dl/", + ) + .await?; + timeout::run_with_timeout_async( async || { let tool_name = self.tool_name(); @@ -87,6 +98,17 @@ impl Backend for GoBackend { tv: ToolVersion, ) -> eyre::Result { Settings::get().ensure_experimental("go backend")?; + + // Check if go is available + self.ensure_dependency( + &ctx.config, + "go", + "To use go packages with mise, you need to install Go first:\n\ + mise use go@latest\n\n\ + Or install Go via https://go.dev/dl/", + ) + .await?; + let opts = self.ba.opts(); let install = async |v| { diff --git a/src/backend/mod.rs b/src/backend/mod.rs index 45bc07e674..43db2813cf 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -715,6 +715,24 @@ pub trait Backend: Debug + Send + Sync { b.which(config, &tv, bin).await.ok().flatten() } + /// Check if a required dependency is available and return a helpful error if not. + /// This provides a consistent error message format across all backends. + async fn ensure_dependency( + &self, + config: &Arc, + program: &str, + install_instructions: &str, + ) -> eyre::Result<()> { + if self.dependency_which(config, program).await.is_none() { + bail!( + "{} is required but not found.\n\n{}", + program, + install_instructions + ); + } + Ok(()) + } + async fn dependency_env(&self, config: &Arc) -> eyre::Result> { self.dependency_toolset(config) .await? diff --git a/src/backend/npm.rs b/src/backend/npm.rs index 050d2177f9..a2404ca571 100644 --- a/src/backend/npm.rs +++ b/src/backend/npm.rs @@ -37,8 +37,15 @@ impl Backend for NPMBackend { } async fn _list_remote_versions(&self, config: &Arc) -> eyre::Result> { + // TODO: Add bun support for listing package versions without npm + // Currently bun info requires a package.json file, so we always use npm. + // Once bun provides a way to query registry without package.json, we can + // switch to using bun when npm.bun=true + self.ensure_npm_for_version_check(config).await?; timeout::run_with_timeout_async( async || { + // Always use npm for listing versions since bun info requires package.json + // bun is only used for actual package installation let raw = cmd!(NPM_PROGRAM, "view", self.tool_name(), "versions", "--json") .full_env(self.dependency_env(config).await?) .read()?; @@ -51,12 +58,17 @@ impl Backend for NPMBackend { } async fn latest_stable_version(&self, config: &Arc) -> eyre::Result> { + // TODO: Add bun support for getting latest version without npm + // See TODO in _list_remote_versions for details + self.ensure_npm_for_version_check(config).await?; let cache = self.latest_version_cache.lock().await; let this = self; timeout::run_with_timeout_async( async || { cache .get_or_try_init_async(async || { + // Always use npm for getting version info since bun info requires package.json + // bun is only used for actual package installation let raw = cmd!(NPM_PROGRAM, "view", this.tool_name(), "dist-tags", "--json") .full_env(this.dependency_env(config).await?) @@ -76,6 +88,7 @@ impl Backend for NPMBackend { } async fn install_version_(&self, ctx: &InstallContext, tv: ToolVersion) -> Result { + self.check_install_deps(&ctx.config).await?; if Settings::get().npm.bun { CmdLineRunner::new("bun") .arg("install") @@ -141,4 +154,45 @@ impl NPMBackend { ba: Arc::new(ba), } } + + /// Check dependencies for version checking (always needs npm) + async fn ensure_npm_for_version_check(&self, config: &Arc) -> Result<()> { + // We always need npm for querying package versions + // TODO: Once bun supports querying packages without package.json, this can be updated + self.ensure_dependency( + config, + NPM_PROGRAM, + "To use npm packages with mise, you need to install Node.js first:\n\ + mise use node@latest\n\n\ + Note: npm is required for querying package information, even when using bun for installation.", + ) + .await + } + + /// Check dependencies for package installation (npm or bun based on settings) + async fn check_install_deps(&self, config: &Arc) -> Result<()> { + if Settings::get().npm.bun { + // In bun mode, only bun is required for installation + self.ensure_dependency( + config, + "bun", + "To use npm packages with bun, you need to install bun first:\n\ + mise use bun@latest\n\n\ + Or switch back to npm by setting:\n\ + mise settings npm.bun=false", + ) + .await + } else { + // In npm mode, npm is required + self.ensure_dependency( + config, + NPM_PROGRAM, + "To use npm packages with mise, you need to install Node.js first:\n\ + mise use node@latest\n\n\ + Alternatively, you can use bun instead of npm by setting:\n\ + mise settings npm.bun=true", + ) + .await + } + } } diff --git a/src/backend/pipx.rs b/src/backend/pipx.rs index e7928596d3..65d9e7c8b1 100644 --- a/src/backend/pipx.rs +++ b/src/backend/pipx.rs @@ -158,15 +158,29 @@ impl Backend for PIPXBackend { } async fn install_version_(&self, ctx: &InstallContext, tv: ToolVersion) -> Result { + // Check if pipx is available (unless uvx is being used) + let use_uvx = self.uv_is_installed(&ctx.config).await + && Settings::get().pipx.uvx != Some(false) + && tv.request.options().get("uvx") != Some(&"false".to_string()); + + if !use_uvx { + self.ensure_dependency( + &ctx.config, + "pipx", + "To use pipx packages with mise, you need to install pipx first:\n\ + mise use pipx@latest\n\n\ + Alternatively, you can use uv/uvx by installing uv:\n\ + mise use uv@latest", + ) + .await?; + } + let pipx_request = self .tool_name() .parse::()? .pipx_request(&tv.version, &tv.request.options()); - if self.uv_is_installed(&ctx.config).await - && Settings::get().pipx.uvx != Some(false) - && tv.request.options().get("uvx") != Some(&"false".to_string()) - { + if use_uvx { ctx.pr .set_message(format!("uv tool install {pipx_request}")); let mut cmd = Self::uvx_cmd( diff --git a/src/backend/spm.rs b/src/backend/spm.rs index e143ef2918..003d7e3373 100644 --- a/src/backend/spm.rs +++ b/src/backend/spm.rs @@ -57,6 +57,16 @@ impl Backend for SPMBackend { let settings = Settings::get(); settings.ensure_experimental("spm backend")?; + // Check if swift is available + self.ensure_dependency( + &ctx.config, + "swift", + "To use Swift Package Manager (spm) tools with mise, you need to install Swift first:\n\ + mise use swift@latest\n\n\ + Or install Swift via https://swift.org/download/", + ) + .await?; + let repo = SwiftPackageRepo::new(&self.tool_name())?; let revision = if tv.version == "latest" { self.latest_stable_version(&ctx.config)