Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
fd2bc60
fix(http): use TOML parsing for tool options to fix platform switching
jdx Mar 3, 2026
1ebe9d5
fix(http): store tool opts as native TOML in .mise-installs.toml
jdx Mar 4, 2026
6becea8
refactor: extract split_bracketed_opts/strip_opts helpers
jdx Mar 4, 2026
049a3b0
refactor: extract EPHEMERAL_OPT_KEYS constant for filtered option keys
jdx Mar 4, 2026
61bee69
[autofix.ci] apply automated fixes
autofix-ci[bot] Mar 4, 2026
e2cc9bc
[autofix.ci] apply automated fixes (attempt 2/3)
autofix-ci[bot] Mar 4, 2026
60c1ef3
fix(http): persist only user-specified opts, not registry defaults
jdx Mar 4, 2026
0872419
refactor: extract str_to_toml_value helper, fix darwin platform key i…
jdx Mar 4, 2026
5532006
fix(http): preserve native TOML opts without string round-trip
jdx Mar 4, 2026
d3cd3a5
fix(http): strip brackets from full when writing backend meta
jdx Mar 4, 2026
c07b2fb
fix(http): unify opts write precedence and filter ephemeral keys
jdx Mar 4, 2026
d9e41a1
fix(http): give manifest opts precedence over registry defaults
jdx Mar 4, 2026
2adacbc
fix(http): fix opts precedence so user opts override manifest cache
jdx Mar 4, 2026
2dc7ecd
refactor: store tool opts as native toml::Value instead of strings
jdx Mar 4, 2026
781a962
[autofix.ci] apply automated fixes
autofix-ci[bot] Mar 4, 2026
cdf0736
[autofix.ci] apply automated fixes (attempt 2/3)
autofix-ci[bot] Mar 4, 2026
3189980
fix: convert scalar TOML opts to strings to fix strip_components
jdx Mar 4, 2026
2e59229
fix: store native toml::Value in tool stub opts for platform URL lookup
jdx Mar 5, 2026
dd62f65
[autofix.ci] apply automated fixes
autofix-ci[bot] Mar 5, 2026
8ccab41
[autofix.ci] apply automated fixes (attempt 2/3)
autofix-ci[bot] Mar 5, 2026
6c8ff28
fix(http): address PR feedback for native TOML opts
jdx Mar 6, 2026
eabd6e9
refactor: extract opts_as_strings() helper to deduplicate toml::Value…
jdx Mar 7, 2026
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
40 changes: 40 additions & 0 deletions e2e/backend/test_http_platform_switch
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#!/usr/bin/env bash

# Test switching from single url to platform-specific urls
# Regression test for https://github.com/jdx/mise/discussions/7034
#
# When a tool is first installed with a single `url`, the install state stores
# the options (including url) in the manifest. If the user then changes the
# config to use platform-specific urls via [tools."http:X".platforms], the
# stale cached options could shadow the new config, causing
# "Http backend requires 'url' option" error.

# Step 1: Install with a single url
cat <<EOF >mise.toml
[tools]
"http:hello-plat-switch" = { version = "1.0.0", url = "https://mise.jdx.dev/test-fixtures/hello-world-1.0.0.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
assert_contains "mise x -- hello-world" "hello world"

# Step 2: Switch to platform-specific urls (same tool name, same version)
# This previously failed with "Http backend requires 'url' option" because
# the stale cached platforms data from parse_tool_options was mangled and
# shadowed the correct config values during merge.
cat <<EOF >mise.toml
[tools."http:hello-plat-switch"]
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-plat-switch".platforms]
linux-x64 = { url = "https://mise.jdx.dev/test-fixtures/hello-world-1.0.0.tar.gz" }
linux-arm64 = { url = "https://mise.jdx.dev/test-fixtures/hello-world-1.0.0.tar.gz" }
darwin-arm64 = { url = "https://mise.jdx.dev/test-fixtures/hello-world-1.0.0.tar.gz" }
darwin-x64 = { url = "https://mise.jdx.dev/test-fixtures/hello-world-1.0.0.tar.gz" }
EOF

# This should succeed, not fail with "Http backend requires 'url' option"
mise install
assert_contains "mise x -- hello-world" "hello world"
4 changes: 2 additions & 2 deletions src/backend/asdf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -170,11 +170,11 @@ impl AsdfBackend {
tv: &ToolVersion,
) -> Result<ScriptManager> {
let mut sm = self.plugin.script_man.clone();
for (key, value) in tv.request.options().opts {
for (key, value) in tv.request.options().opts_as_strings() {
let k = format!("RTX_TOOL_OPTS__{}", key.to_uppercase());
sm = sm.with_env(k, value.clone());
let k = format!("MISE_TOOL_OPTS__{}", key.to_uppercase());
sm = sm.with_env(k, value.clone());
sm = sm.with_env(k, value);
}
for (key, value) in tv.request.options().install_env {
sm = sm.with_env(key, value.clone());
Expand Down
6 changes: 4 additions & 2 deletions src/backend/cargo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,9 @@ impl Backend for CargoBackend {
};

let opts = tv.request.options();
if let Some(bin) = lookup_platform_key(&opts, "bin").or_else(|| opts.get("bin").cloned()) {
if let Some(bin) =
lookup_platform_key(&opts, "bin").or_else(|| opts.get("bin").map(|s| s.to_string()))
{
cmd = cmd.arg(format!("--bin={bin}"));
}
if opts
Expand Down Expand Up @@ -177,7 +179,7 @@ impl Backend for CargoBackend {
// These options affect what gets compiled/installed
for key in ["features", "default-features", "bin"] {
if let Some(value) = opts.get(key) {
result.insert(key.to_string(), value.clone());
result.insert(key.to_string(), value.to_string());
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/backend/conda.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ impl CondaBackend {
self.ba
.opts()
.get("channel")
.cloned()
.map(|s| s.to_string())
.unwrap_or_else(|| Settings::get().conda.channel.clone())
}

Expand Down
2 changes: 1 addition & 1 deletion src/backend/external_plugin_cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ fn render_cache_key(config: &Config, tv: &ToolVersion, cache_key: &[String]) ->
fn parse_template(config: &Config, tv: &ToolVersion, tmpl: &str) -> eyre::Result<String> {
let mut ctx = BASE_CONTEXT.clone();
ctx.insert("project_root", &config.project_root);
ctx.insert("opts", &tv.request.options().opts);
ctx.insert("opts", &tv.request.options().opts_as_strings());
get_tera(
config
.project_root
Expand Down
27 changes: 14 additions & 13 deletions src/backend/github.rs
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,7 @@ impl Backend for UnifiedGitBackend {
// These options affect which artifact is downloaded
for key in ["asset_pattern", "url", "version_prefix"] {
if let Some(value) = opts.get(key) {
result.insert(key.to_string(), value.clone());
result.insert(key.to_string(), value.to_string());
}
}

Expand Down Expand Up @@ -383,7 +383,6 @@ impl UnifiedGitBackend {

fn get_api_url(&self, opts: &ToolVersionOptions) -> String {
opts.get("api_url")
.map(|s| s.as_str())
.unwrap_or(if self.is_gitlab() {
DEFAULT_GITLAB_API_BASE_URL
} else if self.is_forgejo() {
Expand All @@ -407,7 +406,7 @@ impl UnifiedGitBackend {

// Check if we'll verify checksum
let has_checksum = lookup_platform_key(opts, "checksum")
.or_else(|| opts.get("checksum").cloned())
.or_else(|| opts.get("checksum").map(|s| s.to_string()))
.is_some();

// Store the asset URL and digest (if available) in the tool version
Expand Down Expand Up @@ -493,8 +492,8 @@ impl UnifiedGitBackend {
/// Discovers bin paths in the installation directory
fn discover_bin_paths(&self, tv: &ToolVersion) -> Result<Vec<std::path::PathBuf>> {
let opts = tv.request.options();
if let Some(bin_path_template) =
lookup_platform_key(&opts, "bin_path").or_else(|| opts.get("bin_path").cloned())
if let Some(bin_path_template) = lookup_platform_key(&opts, "bin_path")
.or_else(|| opts.get("bin_path").map(|s| s.to_string()))
{
let bin_path = template_string(&bin_path_template, tv);
return Ok(vec![tv.install_path().join(&bin_path)]);
Expand Down Expand Up @@ -599,7 +598,7 @@ impl UnifiedGitBackend {
}

let version = &tv.version;
let version_prefix = opts.get("version_prefix").map(|s| s.as_str());
let version_prefix = opts.get("version_prefix");
if self.is_gitlab() {
try_with_v_prefix(version, version_prefix, |candidate| async move {
self.resolve_gitlab_asset_url_for_target(
Expand Down Expand Up @@ -655,7 +654,7 @@ impl UnifiedGitBackend {

// Try explicit pattern first
if let Some(pattern) = lookup_platform_key_for_target(opts, "asset_pattern", target)
.or_else(|| opts.get("asset_pattern").cloned())
.or_else(|| opts.get("asset_pattern").map(|s| s.to_string()))
{
// Template the pattern for the target platform
let templated_pattern = template_string_for_target(&pattern, tv, target);
Expand Down Expand Up @@ -752,7 +751,7 @@ impl UnifiedGitBackend {

// Try explicit pattern first
if let Some(pattern) = lookup_platform_key_for_target(opts, "asset_pattern", target)
.or_else(|| opts.get("asset_pattern").cloned())
.or_else(|| opts.get("asset_pattern").map(|s| s.to_string()))
{
// Template the pattern for the target platform
let templated_pattern = template_string_for_target(&pattern, tv, target);
Expand Down Expand Up @@ -847,7 +846,7 @@ impl UnifiedGitBackend {

// Try explicit pattern first
if let Some(pattern) = lookup_platform_key_for_target(opts, "asset_pattern", target)
.or_else(|| opts.get("asset_pattern").cloned())
.or_else(|| opts.get("asset_pattern").map(|s| s.to_string()))
{
// Template the pattern for the target platform
let templated_pattern = template_string_for_target(&pattern, tv, target);
Expand Down Expand Up @@ -1007,7 +1006,7 @@ impl UnifiedGitBackend {
fn get_filter_bins(&self, tv: &ToolVersion) -> Option<Vec<String>> {
let opts = tv.request.options();
let filter_bins = lookup_platform_key(&opts, "filter_bins")
.or_else(|| opts.get("filter_bins").cloned())?;
.or_else(|| opts.get("filter_bins").map(|s| s.to_string()))?;

Some(
filter_bins
Expand Down Expand Up @@ -1194,7 +1193,7 @@ impl UnifiedGitBackend {
let version = &tv.version;

// Try to get the release (with version prefix support)
let version_prefix = opts.get("version_prefix").map(|s| s.as_str());
let version_prefix = opts.get("version_prefix");
let release =
match try_with_v_prefix_and_repo(version, version_prefix, Some(&repo), |candidate| {
let api_url = api_url.clone();
Expand Down Expand Up @@ -1429,8 +1428,10 @@ mod tests {

// Test with custom version prefix
let mut opts = ToolVersionOptions::default();
opts.opts
.insert("version_prefix".to_string(), "release-".to_string());
opts.opts.insert(
"version_prefix".to_string(),
toml::Value::String("release-".to_string()),
);

assert_eq!(
backend.strip_version_prefix("release-1.0.0", &opts),
Expand Down
2 changes: 1 addition & 1 deletion src/backend/go.rs
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ impl Backend for GoBackend {

// tags affect compilation
if let Some(value) = opts.get("tags") {
result.insert("tags".to_string(), value.clone());
result.insert("tags".to_string(), value.to_string());
}

result
Expand Down
10 changes: 5 additions & 5 deletions src/backend/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ const METADATA_FILE: &str = "metadata.json";

/// Helper to get an option value with platform-specific fallback
fn get_opt(opts: &ToolVersionOptions, key: &str) -> Option<String> {
lookup_platform_key(opts, key).or_else(|| opts.get(key).cloned())
lookup_platform_key(opts, key).or_else(|| opts.get(key).map(|s| s.to_string()))
}

/// Metadata stored alongside cached extractions
Expand Down Expand Up @@ -571,13 +571,13 @@ impl HttpBackend {
};

let url = match opts.get("version_list_url") {
Some(url) => url.clone(),
Some(url) => url.to_string(),
None => return Ok(vec![]),
};

let regex = opts.get("version_regex").map(|s| s.as_str());
let json_path = opts.get("version_json_path").map(|s| s.as_str());
let version_expr = opts.get("version_expr").map(|s| s.as_str());
let regex = opts.get("version_regex");
let json_path = opts.get("version_json_path");
let version_expr = opts.get("version_expr");

version_list::fetch_versions(&url, regex, json_path, version_expr).await
}
Expand Down
4 changes: 2 additions & 2 deletions src/backend/pipx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ impl Backend for PIPXBackend {
// 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());
&& tv.request.options().get("uvx") != Some("false");

if !use_uvx {
self.warn_if_dependency_missing(
Expand Down Expand Up @@ -268,7 +268,7 @@ impl Backend for PIPXBackend {
// These options affect what gets installed
for key in ["extras", "pipx_args", "uvx_args", "uvx"] {
if let Some(value) = opts.get(key) {
result.insert(key.to_string(), value.clone());
result.insert(key.to_string(), value.to_string());
}
}

Expand Down
11 changes: 6 additions & 5 deletions src/backend/spm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -264,8 +264,9 @@ impl GitProvider {
fn from_ba(ba: &BackendArg) -> Self {
let opts = ba.opts();

let default_provider = GitProviderKind::GitHub.as_ref().to_string();
let provider = opts.get("provider").unwrap_or(&default_provider);
let provider = opts
.get("provider")
.unwrap_or(GitProviderKind::GitHub.as_ref());
let kind = if ba.tool_name.contains("gitlab.com") {
GitProviderKind::GitLab
} else {
Expand Down Expand Up @@ -365,7 +366,7 @@ mod tests {
"tool".to_string(),
Some(ToolVersionOptions {
opts: indexmap![
"provider".to_string() => "gitlab".to_string()
"provider".to_string() => toml::Value::String("gitlab".to_string())
],
..Default::default()
})
Expand All @@ -381,8 +382,8 @@ mod tests {
"tool".to_string(),
Some(ToolVersionOptions {
opts: indexmap![
"api_url".to_string() => "https://gitlab.acme.com/api/v4".to_string(),
"provider".to_string() => "gitlab".to_string(),
"api_url".to_string() => toml::Value::String("https://gitlab.acme.com/api/v4".to_string()),
"provider".to_string() => toml::Value::String("gitlab".to_string()),
],
..Default::default()
})
Expand Down
Loading
Loading