diff --git a/docs/dev-tools/backends/http.md b/docs/dev-tools/backends/http.md index 6f96e7184a..df78db1a84 100644 --- a/docs/dev-tools/backends/http.md +++ b/docs/dev-tools/backends/http.md @@ -170,6 +170,23 @@ bin = "docker-compose" # Rename from docker-compose-linux-x86_64 to docker-comp When downloading single binaries (not archives), mise automatically removes OS/arch suffixes from the filename. For example, `docker-compose-linux-x86_64` becomes `docker-compose` automatically. Use the `bin` option only when you need a specific custom name. ::: +### `rename_exe` + +Rename the executable inside an extracted archive to a specific name. This is useful when archives contain binaries with platform-specific names or when installing kubectl plugins that need specific naming: + +```toml +[tools."http:openunison-cli"] +version = "1.0.0" +url = "https://nexus.tremolo.io/repository/openunison-cli/openunison-cli-v{{version}}-linux.zip" +rename_exe = "kubectl-openunison-cli" # Rename extracted binary for kubectl plugin +``` + +This works by searching for the first executable in the extracted directory (or `bin_path` if specified) and renaming it to the specified name. + +::: tip +Use `bin` for renaming single binary downloads, and `rename_exe` for renaming executables inside archives. +::: + ### `format` Explicitly specify the archive format when the URL lacks a file extension or has an incorrect extension: diff --git a/e2e/backend/test_http_rename_exe b/e2e/backend/test_http_rename_exe new file mode 100644 index 0000000000..719bd48da0 --- /dev/null +++ b/e2e/backend/test_http_rename_exe @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +# Test HTTP backend rename_exe option for renaming executables in archives + +set -euo pipefail +export MISE_EXPERIMENTAL=1 + +# Test: rename_exe renames the executable inside the archive +# The hello-world archive contains hello-world-1.0.0/bin/hello-world +# We use strip_components=1 and bin_path=bin to get to the binary, +# then rename_exe to rename it to "my-hello" +cat <mise.toml +[tools] +"http:hello-rename" = { version = "1.0.0", url = "https://mise.jdx.dev/test-fixtures/hello-world-1.0.0.tar.gz", strip_components = 1, bin_path = "bin", rename_exe = "my-hello", postinstall = "chmod +x \$MISE_TOOL_INSTALL_PATH/bin/my-hello" } +EOF + +mise install +mise env + +# Verify the renamed binary works +assert_contains "mise x -- my-hello" "hello world" + +# Test: rename_exe with bin_path - binary should be renamed in the bin_path directory +cat <mise.toml +[tools] +"http:fd-rename" = { version = "8.7.0", url = "https://mise.jdx.dev/test-fixtures/fd-8.7.0.tar.gz", bin_path = "fd-8.7.0/bin", rename_exe = "my-fd", postinstall = "chmod +x \$MISE_TOOL_INSTALL_PATH/fd-8.7.0/bin/my-fd" } +EOF + +mise install +mise env + +# Verify the renamed binary works +assert_contains "mise x -- my-fd --version" "8.7.0" diff --git a/src/backend/http.rs b/src/backend/http.rs index c264f62831..c1cde397a0 100644 --- a/src/backend/http.rs +++ b/src/backend/http.rs @@ -3,7 +3,7 @@ use crate::backend::VersionInfo; use crate::backend::backend_type::BackendType; use crate::backend::static_helpers::{ clean_binary_name, get_filename_from_url, list_available_platforms_with_key, - lookup_platform_key, template_string, verify_artifact, + lookup_platform_key, rename_executable_in_dir, template_string, verify_artifact, }; use crate::backend::version_list; use crate::cli::args::BackendArg; @@ -168,6 +168,16 @@ impl HttpBackend { parts.push(format!("strip_{strip}")); } + // Include rename_exe in cache key since it modifies the extracted content + if let Some(rename) = get_opt(opts, "rename_exe") { + parts.push(format!("rename_{rename}")); + // When rename_exe is used, bin_path affects where the rename happens, + // so different bin_path values result in different cached content + if let Some(bin_path) = get_opt(opts, "bin_path") { + parts.push(format!("binpath_{bin_path}")); + } + } + let key = parts.join("_"); debug!("Cache key: {}", key); Ok(key) @@ -236,6 +246,7 @@ impl HttpBackend { /// Extract artifact to cache with atomic rename fn extract_to_cache( &self, + tv: &ToolVersion, file_path: &Path, cache_key: &str, url: &str, @@ -261,7 +272,7 @@ impl HttpBackend { } // Perform extraction - let extraction_type = self.extract_artifact(&tmp_path, file_path, opts, pr)?; + let extraction_type = self.extract_artifact(tv, &tmp_path, file_path, opts, pr)?; // Atomic replace if cache_path.exists() { @@ -278,6 +289,7 @@ impl HttpBackend { /// Extract a single artifact to the given directory fn extract_artifact( &self, + tv: &ToolVersion, dest: &Path, file_path: &Path, opts: &ToolVersionOptions, @@ -292,7 +304,7 @@ impl HttpBackend { } else if file_info.format == file::TarFormat::Raw { self.extract_raw_file(dest, file_path, &file_info, opts, pr) } else { - self.extract_archive(dest, file_path, &file_info, opts, pr) + self.extract_archive(tv, dest, file_path, &file_info, opts, pr) } } @@ -371,6 +383,7 @@ impl HttpBackend { /// Extract an archive (tar, zip, etc.) fn extract_archive( &self, + tv: &ToolVersion, dest: &Path, file_path: &Path, file_info: &FileInfo, @@ -397,6 +410,20 @@ impl HttpBackend { }; file::untar(file_path, dest, &tar_opts)?; + + // Handle rename_exe option for archives + if let Some(rename_to) = get_opt(opts, "rename_exe") { + let search_dir = if let Some(bin_path_template) = get_opt(opts, "bin_path") { + let bin_path = template_string(&bin_path_template, tv); + dest.join(&bin_path) + } else { + dest.to_path_buf() + }; + // rsplit('/') always yields at least one element (the full string if no delimiter) + let tool_name = self.ba.tool_name.rsplit('/').next().unwrap(); + rename_executable_in_dir(&search_dir, &rename_to, Some(tool_name))?; + } + Ok(ExtractionType::Archive) } @@ -574,6 +601,7 @@ pub fn install_time_option_keys() -> Vec { "version_json_path".into(), "version_expr".into(), "format".into(), + "rename_exe".into(), ] } @@ -680,7 +708,14 @@ impl Backend for HttpBackend { self.extraction_type_from_cache(&cache_key, &file_info) } else { ctx.pr.set_message("extracting to cache".into()); - self.extract_to_cache(&file_path, &cache_key, &url, &opts, Some(ctx.pr.as_ref()))? + self.extract_to_cache( + &tv, + &file_path, + &cache_key, + &url, + &opts, + Some(ctx.pr.as_ref()), + )? }; // Create symlinks diff --git a/src/backend/static_helpers.rs b/src/backend/static_helpers.rs index f68d6f2918..366f275f03 100644 --- a/src/backend/static_helpers.rs +++ b/src/backend/static_helpers.rs @@ -623,7 +623,7 @@ fn should_skip_file(file_name: &str, strict: bool) -> bool { /// - `tool_name`: Optional hint for finding non-executable files by name matching. /// When provided, if no executable is found, will search for files matching the tool name /// and make them executable before renaming. -fn rename_executable_in_dir( +pub fn rename_executable_in_dir( dir: &Path, new_name: &str, tool_name: Option<&str>,