Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions docs/dev-tools/backends/http.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,35 @@ 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.
:::

### `format`

Explicitly specify the archive format when the URL lacks a file extension or has an incorrect extension:

```toml
[tools."http:my-tool"]
version = "1.0.0"
url = "https://example.com/releases/my-tool-v1.0.0"
format = "tar.xz" # Explicitly specify the format
```

::: info
If `format` is not specified, mise will automatically detect the format from the file extension in the URL. Only use `format` when the URL doesn't have a proper extension or when you need to override the detected format.
:::

### Platform-specific Format

You can specify different formats for different platforms:

```toml
[tools."http:my-tool"]
version = "1.0.0"

[tools."http:my-tool".platforms]
macos-x64 = { url = "https://example.com/releases/my-tool-v1.0.0-macos-x64", format = "tar.xz" }
linux-x64 = { url = "https://example.com/releases/my-tool-v1.0.0-linux-x64", format = "tar.gz" }
windows-x64 = { url = "https://example.com/releases/my-tool-v1.0.0-windows-x64", format = "zip" }
```

### `bin_path`

Specify the directory containing binaries within the extracted archive, or where to place the downloaded file. This supports templating with `{{version}}`:
Expand Down
35 changes: 35 additions & 0 deletions e2e/backend/test_http_format
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#!/usr/bin/env bash

# Test HTTP backend with explicit format parameter
# This simulates tools without proper file extensions

# Test 1: Explicit format with tar.gz
cat <<EOF >mise.toml
[tools."http:hello-format"]
version = "1.0.0"
url = "https://mise.jdx.dev/test-fixtures/test-fixtures_hello-world-1.0.0-tarball"
format = "tar.gz"
bin_path = "hello-world-1.0.0/bin"
postinstall = "chmod +x \$MISE_TOOL_INSTALL_PATH/hello-world-1.0.0/bin/hello-world"
EOF

mise install -v
mise env
assert_contains "mise x -- hello-world" "hello world"

# Test 2: Platform-specific format
cat <<EOF >mise.toml
[tools."http:hello-format-platform"]
version = "1.0.0"
bin_path = "hello-world-1.0.0/bin"
postinstall = "chmod +x \$MISE_TOOL_INSTALL_PATH/hello-world-1.0.0/bin/hello-world"

[tools."http:hello-format-platform".platforms]
darwin-arm64 = { url = "https://mise.jdx.dev/test-fixtures/test-fixtures_hello-world-1.0.0-tarball", format = "tar.gz" }
darwin-amd64 = { url = "https://mise.jdx.dev/test-fixtures/test-fixtures_hello-world-1.0.0-tarball", format = "tar.gz" }
linux-amd64 = { url = "https://mise.jdx.dev/test-fixtures/test-fixtures_hello-world-1.0.0-tarball", format = "tar.gz" }
linux-arm64 = { url = "https://mise.jdx.dev/test-fixtures/test-fixtures_hello-world-1.0.0-tarball", format = "tar.gz" }
EOF

mise install
assert_contains "mise x -- hello-world" "hello world"
28 changes: 24 additions & 4 deletions src/backend/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -155,14 +155,34 @@ impl HttpBackend {

file::create_dir_all(cache_path)?;

// Use TarFormat for format detection
let ext = file_path.extension().and_then(|s| s.to_str()).unwrap_or("");
let format = file::TarFormat::from_ext(ext);
// Handle `format` config
let file_path_with_ext = if let Some(added_extension) =
lookup_platform_key(opts, "format").or_else(|| opts.get("format").cloned())
{
let mut file_path = file_path.to_path_buf();
let current_ext = file_path.extension().and_then(|e| e.to_str()).unwrap_or("");
let new_ext = if current_ext.is_empty() {
added_extension.clone()
} else {
format!("{}.{}", current_ext, added_extension)
};
file_path.set_extension(new_ext);
Comment on lines +163 to +169
Copy link

Copilot AI Nov 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logic for combining extensions may produce incorrect results. If the file already has a valid extension like 'tar.gz' and the user specifies format='tar.gz', this would result in 'tar.gz.tar.gz'. Consider validating that the specified format isn't already present in the extension before appending it.

Suggested change
let current_ext = file_path.extension().and_then(|e| e.to_str()).unwrap_or("");
let new_ext = if current_ext.is_empty() {
added_extension.clone()
} else {
format!("{}.{}", current_ext, added_extension)
};
file_path.set_extension(new_ext);
let file_name = file_path.file_name().and_then(|n| n.to_str()).unwrap_or("");
// Only append the extension if it's not already present at the end of the file name
if !file_name.ends_with(&format!(".{}", added_extension)) {
let current_ext = file_path.extension().and_then(|e| e.to_str()).unwrap_or("");
let new_ext = if current_ext.is_empty() {
added_extension.clone()
} else {
format!("{}.{}", current_ext, added_extension)
};
file_path.set_extension(new_ext);
}

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was arguably intentional for simplicity sake. Trying to "maybe replace" the extension is error-prone (since files like foo-1.0.0 have the extension .0)

file_path
} else {
file_path.to_path_buf()
};

// Get file extension and detect format
let file_name = file_path.file_name().unwrap().to_string_lossy();
let ext = file_path_with_ext
.extension()
.and_then(|s| s.to_str())
.unwrap_or("");

// Use TarFormat for format detection
let format = file::TarFormat::from_ext(ext);

// Check if it's a compressed binary (not a tar archive)
let file_name = file_path_with_ext.file_name().unwrap().to_string_lossy();
let is_compressed_binary =
!file_name.contains(".tar") && matches!(ext, "gz" | "xz" | "bz2" | "zst");

Expand Down
Loading