From ab2b0c3fe4d5aabd163041d30a897215085c0ef5 Mon Sep 17 00:00:00 2001 From: jdx <216188+jdx@users.noreply.github.com> Date: Fri, 12 Sep 2025 18:40:41 -0500 Subject: [PATCH 1/3] fix(tool-stub): detect binary names from single-file downloads MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When generating tool stubs for single binary files (not archives), the binary name should be extracted from the URL by removing platform/arch suffixes. This allows the bin field to be correctly set when it differs from the stub filename. For example, a stub named "jq-test" downloading "jq-macos-arm64" will now correctly set bin="jq" since the actual binary is "jq", not "jq-macos-arm64". 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- e2e/cli/test_generate_tool_stub | 118 ++++++++++++++++++++++++++++++++ src/cli/generate/tool_stub.rs | 75 +++++++++++++++++++- 2 files changed, 191 insertions(+), 2 deletions(-) create mode 100755 e2e/cli/test_generate_tool_stub diff --git a/e2e/cli/test_generate_tool_stub b/e2e/cli/test_generate_tool_stub new file mode 100755 index 0000000000..35587a388c --- /dev/null +++ b/e2e/cli/test_generate_tool_stub @@ -0,0 +1,118 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Test that tool-stub generation correctly sets the bin field + +assert() { + local actual="$1" + local expected="$2" + local message="${3:-Assertion failed}" + if [[ $actual != "$expected" ]]; then + echo "FAIL: $message" + echo " Expected: '$expected'" + echo " Actual: '$actual'" + exit 1 + fi +} + +assert_contains() { + local haystack="$1" + local needle="$2" + local message="${3:-Assertion failed}" + if [[ ! $haystack =~ $needle ]]; then + echo "FAIL: $message" + echo " Expected to contain: '$needle'" + echo " Actual: '$haystack'" + exit 1 + fi +} + +assert_not_contains() { + local haystack="$1" + local needle="$2" + local message="${3:-Assertion failed}" + if [[ $haystack =~ $needle ]]; then + echo "FAIL: $message" + echo " Expected NOT to contain: '$needle'" + echo " Actual: '$haystack'" + exit 1 + fi +} + +# Create a temporary directory for testing +TEST_DIR=$(mktemp -d) +cd "$TEST_DIR" +trap 'rm -rf "$TEST_DIR"' EXIT + +echo "Testing tool-stub generation with single binary download (jq)..." + +# Generate a tool stub for jq (single binary file, not an archive) +mise generate tool-stub jq-test \ + --platform-url "macos-arm64:https://github.com/jqlang/jq/releases/download/jq-1.7.1/jq-macos-arm64" \ + --platform-url "linux-x64:https://github.com/jqlang/jq/releases/download/jq-1.7.1/jq-linux-amd64" \ + --skip-download + +# Check that the file was created +[[ -f jq-test ]] || { + echo "FAIL: Tool stub file should be created" + exit 1 +} + +# Check the content of the generated stub +STUB_CONTENT=$(cat jq-test) + +# The bin field should be set to "jq" since the downloaded binary is named "jq" +# but our stub is named "jq-test" +assert_contains "$STUB_CONTENT" 'bin = "jq"' "The bin field should be set to 'jq'" + +echo "Testing tool-stub generation with archive (ripgrep)..." + +# Generate a tool stub for ripgrep (archive with binary inside) +mise generate tool-stub rg-test \ + --platform-url "linux-x64:https://github.com/BurntSushi/ripgrep/releases/download/14.1.1/ripgrep-14.1.1-x86_64-unknown-linux-musl.tar.gz" \ + --skip-download + +# Check that the file was created +[[ -f rg-test ]] || { + echo "FAIL: Tool stub file should be created" + exit 1 +} + +# Check the content +STUB_CONTENT=$(cat rg-test) + +# For archives, when we skip download, the bin field won't be set +# But let's test that bin is properly handled when explicitly provided +mise generate tool-stub rg-explicit \ + --platform-url "linux-x64:https://github.com/BurntSushi/ripgrep/releases/download/14.1.1/ripgrep-14.1.1-x86_64-unknown-linux-musl.tar.gz" \ + --bin "rg" \ + --skip-download + +STUB_CONTENT=$(cat rg-explicit) +assert_contains "$STUB_CONTENT" 'bin = "rg"' "The bin field should be set when explicitly provided" + +echo "Testing that bin field is omitted when it matches the stub name..." + +# Generate a stub where the binary name matches the stub name +mise generate tool-stub jq \ + --platform-url "macos-arm64:https://github.com/jqlang/jq/releases/download/jq-1.7.1/jq-macos-arm64" \ + --bin "jq" \ + --skip-download + +STUB_CONTENT=$(cat jq) +# When bin matches the stub name, it should be omitted +assert_not_contains "$STUB_CONTENT" 'bin =' "The bin field should be omitted when it matches stub name" + +echo "Testing with actual download to detect binary path..." + +# Test with a small binary that we can actually download +# Using jq as it's a single binary and relatively small +mise generate tool-stub jq-download \ + --platform-url "https://github.com/jqlang/jq/releases/download/jq-1.7.1/jq-linux-amd64" + +STUB_CONTENT=$(cat jq-download) +# For a single binary download, the bin should be set to the actual binary name "jq" +# not "jq-download" (the stub name) +assert_contains "$STUB_CONTENT" 'bin = "jq"' "The bin field should be set to the actual binary name" + +echo "All tests passed!" diff --git a/src/cli/generate/tool_stub.rs b/src/cli/generate/tool_stub.rs index b45d358023..538c5e1731 100644 --- a/src/cli/generate/tool_stub.rs +++ b/src/cli/generate/tool_stub.rs @@ -265,6 +265,19 @@ impl ToolStub { } } } + } else { + // When skipping download, still try to guess the binary name from the URL + if !explicit_platform_bins.contains_key(&platform) && self.bin.is_none() { + let filename = get_filename_from_url(&url); + // Only set bin for non-archive formats where we can guess the binary name + if !self.is_archive_format(&url) { + let detected_bin = extract_tool_name_from_filename(&filename); + if detected_bin != stub_filename { + platform_table["bin"] = toml_edit::value(&detected_bin); + detected_bin_paths.push(detected_bin); + } + } + } } } @@ -394,9 +407,10 @@ impl ToolStub { } } } else { - // For single binary files, just use the tool name + // For single binary files, try to extract the tool name from the filename + // Remove common platform suffixes to get the actual binary name pr.finish(); - Some(self.get_tool_name()) + Some(extract_tool_name_from_filename(&filename)) }; Ok((checksum, size, bin_path)) @@ -649,6 +663,63 @@ fn format_size_comment(bytes: u64) -> String { format!(" # {}", format_size(bytes, BINARY)) } +fn extract_tool_name_from_filename(filename: &str) -> String { + // Remove file extension if present + let name = std::path::Path::new(filename) + .file_stem() + .and_then(|s| s.to_str()) + .unwrap_or(filename); + + // Common platform/architecture suffixes to remove + let suffixes = [ + "-linux-x64", + "-linux-x86_64", + "-linux-amd64", + "-linux-arm64", + "-linux-aarch64", + "-darwin-x64", + "-darwin-x86_64", + "-darwin-amd64", + "-darwin-arm64", + "-darwin-aarch64", + "-macos-x64", + "-macos-x86_64", + "-macos-amd64", + "-macos-arm64", + "-macos-aarch64", + "-windows-x64", + "-windows-x86_64", + "-windows-amd64", + "-windows-arm64", + "-windows-aarch64", + "-win-x64", + "-win-x86_64", + "-win-amd64", + "-win-arm64", + "-win-aarch64", + "-x86_64", + "-x64", + "-amd64", + "-arm64", + "-aarch64", + "-linux", + "-darwin", + "-macos", + "-windows", + "-win", + ]; + + let mut result = name.to_string(); + for suffix in &suffixes { + if let Some(pos) = result.rfind(suffix) { + result.truncate(pos); + break; + } + } + + result +} + static AFTER_LONG_HELP: &str = color_print::cstr!( r#"Examples: From de5608c8e6fe1ec989f753f72d1a4d55d16b1875 Mon Sep 17 00:00:00 2001 From: jdx <216188+jdx@users.noreply.github.com> Date: Sat, 13 Sep 2025 07:39:40 -0500 Subject: [PATCH 2/3] test: simplify tool-stub e2e test to focus on git-branchless Simplified the test to just generate a git-branchless tool stub and attempt to execute it, which directly reproduces the original issue where the bin field was empty. --- e2e/cli/test_generate_tool_stub | 141 ++++++++------------------------ 1 file changed, 34 insertions(+), 107 deletions(-) diff --git a/e2e/cli/test_generate_tool_stub b/e2e/cli/test_generate_tool_stub index 35587a388c..f0f6043a12 100755 --- a/e2e/cli/test_generate_tool_stub +++ b/e2e/cli/test_generate_tool_stub @@ -1,118 +1,45 @@ #!/usr/bin/env bash set -euo pipefail -# Test that tool-stub generation correctly sets the bin field +# Test that tool-stub generation works for git-branchless -assert() { - local actual="$1" - local expected="$2" - local message="${3:-Assertion failed}" - if [[ $actual != "$expected" ]]; then - echo "FAIL: $message" - echo " Expected: '$expected'" - echo " Actual: '$actual'" - exit 1 - fi -} +echo "Testing tool-stub generation for git-branchless..." -assert_contains() { - local haystack="$1" - local needle="$2" - local message="${3:-Assertion failed}" - if [[ ! $haystack =~ $needle ]]; then - echo "FAIL: $message" - echo " Expected to contain: '$needle'" - echo " Actual: '$haystack'" - exit 1 - fi -} - -assert_not_contains() { - local haystack="$1" - local needle="$2" - local message="${3:-Assertion failed}" - if [[ $haystack =~ $needle ]]; then - echo "FAIL: $message" - echo " Expected NOT to contain: '$needle'" - echo " Actual: '$haystack'" - exit 1 - fi -} - -# Create a temporary directory for testing -TEST_DIR=$(mktemp -d) -cd "$TEST_DIR" -trap 'rm -rf "$TEST_DIR"' EXIT - -echo "Testing tool-stub generation with single binary download (jq)..." - -# Generate a tool stub for jq (single binary file, not an archive) -mise generate tool-stub jq-test \ - --platform-url "macos-arm64:https://github.com/jqlang/jq/releases/download/jq-1.7.1/jq-macos-arm64" \ - --platform-url "linux-x64:https://github.com/jqlang/jq/releases/download/jq-1.7.1/jq-linux-amd64" \ - --skip-download +# Generate a tool stub for git-branchless +mise generate tool-stub git-branchless \ + --platform-url 'https://github.com/arxanas/git-branchless/releases/download/v0.10.0/git-branchless-v0.10.0-x86_64-unknown-linux-musl.tar.gz' # Check that the file was created -[[ -f jq-test ]] || { - echo "FAIL: Tool stub file should be created" +if [[ ! -f git-branchless ]]; then + echo "FAIL: Tool stub file was not created" exit 1 -} - -# Check the content of the generated stub -STUB_CONTENT=$(cat jq-test) +fi -# The bin field should be set to "jq" since the downloaded binary is named "jq" -# but our stub is named "jq-test" -assert_contains "$STUB_CONTENT" 'bin = "jq"' "The bin field should be set to 'jq'" - -echo "Testing tool-stub generation with archive (ripgrep)..." - -# Generate a tool stub for ripgrep (archive with binary inside) -mise generate tool-stub rg-test \ - --platform-url "linux-x64:https://github.com/BurntSushi/ripgrep/releases/download/14.1.1/ripgrep-14.1.1-x86_64-unknown-linux-musl.tar.gz" \ - --skip-download - -# Check that the file was created -[[ -f rg-test ]] || { - echo "FAIL: Tool stub file should be created" +# Check that it's executable +if [[ ! -x git-branchless ]]; then + echo "FAIL: Tool stub is not executable" exit 1 -} - -# Check the content -STUB_CONTENT=$(cat rg-test) - -# For archives, when we skip download, the bin field won't be set -# But let's test that bin is properly handled when explicitly provided -mise generate tool-stub rg-explicit \ - --platform-url "linux-x64:https://github.com/BurntSushi/ripgrep/releases/download/14.1.1/ripgrep-14.1.1-x86_64-unknown-linux-musl.tar.gz" \ - --bin "rg" \ - --skip-download - -STUB_CONTENT=$(cat rg-explicit) -assert_contains "$STUB_CONTENT" 'bin = "rg"' "The bin field should be set when explicitly provided" - -echo "Testing that bin field is omitted when it matches the stub name..." - -# Generate a stub where the binary name matches the stub name -mise generate tool-stub jq \ - --platform-url "macos-arm64:https://github.com/jqlang/jq/releases/download/jq-1.7.1/jq-macos-arm64" \ - --bin "jq" \ - --skip-download - -STUB_CONTENT=$(cat jq) -# When bin matches the stub name, it should be omitted -assert_not_contains "$STUB_CONTENT" 'bin =' "The bin field should be omitted when it matches stub name" - -echo "Testing with actual download to detect binary path..." - -# Test with a small binary that we can actually download -# Using jq as it's a single binary and relatively small -mise generate tool-stub jq-download \ - --platform-url "https://github.com/jqlang/jq/releases/download/jq-1.7.1/jq-linux-amd64" - -STUB_CONTENT=$(cat jq-download) -# For a single binary download, the bin should be set to the actual binary name "jq" -# not "jq-download" (the stub name) -assert_contains "$STUB_CONTENT" 'bin = "jq"' "The bin field should be set to the actual binary name" +fi + +echo "Tool stub created successfully" + +# Show the stub content for debugging +echo "Stub content:" +cat git-branchless + +# Try to execute it to see the version +# This will install the tool if we're on Linux x64, otherwise will show an error about platform +echo "Attempting to execute git-branchless --version..." +OUTPUT=$(./git-branchless --version 2>&1 || true) +echo "Output: $OUTPUT" + +if echo "$OUTPUT" | grep -q "git-branchless"; then + echo "SUCCESS: git-branchless executed successfully" +elif echo "$OUTPUT" | grep -q "No URL configured for platform"; then + echo "Expected platform error (not on linux-x64), but tool stub works" +else + echo "FAIL: Unexpected output when executing git-branchless" + exit 1 +fi -echo "All tests passed!" +echo "Test passed!" From fe717df9f04a2de0507825b5b61c59c016a79321 Mon Sep 17 00:00:00 2001 From: jdx <216188+jdx@users.noreply.github.com> Date: Sat, 13 Sep 2025 07:51:20 -0500 Subject: [PATCH 3/3] fix(tool-stub): handle empty bin path after strip_components When strip_components removes all path components (e.g., stripping 'git-branchless' results in empty string), don't set an empty bin field. Instead, keep the original binary name. This fixes the issue where tool stubs had bin="" which prevented the HTTP backend from using its auto-detection logic. Also updated CLAUDE.md with correct debug environment variables. --- CLAUDE.md | 3 ++ src/cli/generate/tool_stub.rs | 81 +++-------------------------------- 2 files changed, 10 insertions(+), 74 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index b4487d121c..1ba66ed71a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -11,6 +11,9 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co - `mise run test:e2e` - Run end-to-end tests only - `mise run snapshots` - Update test snapshots with `cargo insta` +### Debugging +- Use `MISE_DEBUG=1` or `MISE_TRACE=1` environment variables to enable debug output (not `RUST_LOG`) + ### Code Quality and Testing - `mise run lint` - Run all linting tasks - `mise run lint-fix` - Run linting and automatically fix issues diff --git a/src/cli/generate/tool_stub.rs b/src/cli/generate/tool_stub.rs index 538c5e1731..1fd63acef6 100644 --- a/src/cli/generate/tool_stub.rs +++ b/src/cli/generate/tool_stub.rs @@ -265,19 +265,6 @@ impl ToolStub { } } } - } else { - // When skipping download, still try to guess the binary name from the URL - if !explicit_platform_bins.contains_key(&platform) && self.bin.is_none() { - let filename = get_filename_from_url(&url); - // Only set bin for non-archive formats where we can guess the binary name - if !self.is_archive_format(&url) { - let detected_bin = extract_tool_name_from_filename(&filename); - if detected_bin != stub_filename { - platform_table["bin"] = toml_edit::value(&detected_bin); - detected_bin_paths.push(detected_bin); - } - } - } } } @@ -407,10 +394,9 @@ impl ToolStub { } } } else { - // For single binary files, try to extract the tool name from the filename - // Remove common platform suffixes to get the actual binary name + // For single binary files, just use the tool name pr.finish(); - Some(extract_tool_name_from_filename(&filename)) + Some(self.get_tool_name()) }; Ok((checksum, size, bin_path)) @@ -458,7 +444,11 @@ impl ToolStub { if will_strip { let path = std::path::Path::new(&selected_exe); if let Ok(stripped) = path.strip_prefix(path.components().next().unwrap()) { - return Ok(stripped.to_string_lossy().to_string()); + let stripped_str = stripped.to_string_lossy().to_string(); + // Don't return empty string if stripping removed everything + if !stripped_str.is_empty() { + return Ok(stripped_str); + } } } @@ -663,63 +653,6 @@ fn format_size_comment(bytes: u64) -> String { format!(" # {}", format_size(bytes, BINARY)) } -fn extract_tool_name_from_filename(filename: &str) -> String { - // Remove file extension if present - let name = std::path::Path::new(filename) - .file_stem() - .and_then(|s| s.to_str()) - .unwrap_or(filename); - - // Common platform/architecture suffixes to remove - let suffixes = [ - "-linux-x64", - "-linux-x86_64", - "-linux-amd64", - "-linux-arm64", - "-linux-aarch64", - "-darwin-x64", - "-darwin-x86_64", - "-darwin-amd64", - "-darwin-arm64", - "-darwin-aarch64", - "-macos-x64", - "-macos-x86_64", - "-macos-amd64", - "-macos-arm64", - "-macos-aarch64", - "-windows-x64", - "-windows-x86_64", - "-windows-amd64", - "-windows-arm64", - "-windows-aarch64", - "-win-x64", - "-win-x86_64", - "-win-amd64", - "-win-arm64", - "-win-aarch64", - "-x86_64", - "-x64", - "-amd64", - "-arm64", - "-aarch64", - "-linux", - "-darwin", - "-macos", - "-windows", - "-win", - ]; - - let mut result = name.to_string(); - for suffix in &suffixes { - if let Some(pos) = result.rfind(suffix) { - result.truncate(pos); - break; - } - } - - result -} - static AFTER_LONG_HELP: &str = color_print::cstr!( r#"Examples: