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
40 changes: 40 additions & 0 deletions e2e/lockfile/test_lockfile_locked_mode
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#!/usr/bin/env bash

# Test --locked mode enforcement during install and dry-run

export MISE_LOCKFILE=1

detect_platform
PLATFORM="$MISE_PLATFORM"

# Use jq via aqua backend, which supports lockfile URLs
cat <<'EOF' >mise.toml
[tools]
jq = "1.7.1"
EOF

# Uninstall jq so it's not already present
mise uninstall jq@1.7.1 2>/dev/null || true

# --- Test 1: --locked fails when no lockfile URL exists ---
cat <<EOF >mise.lock
[[tools.jq]]
version = "1.7.1"
backend = "aqua:jqlang/jq"
EOF

assert_fail_contains "mise install --locked 2>&1" "No lockfile URL found"

# --- Test 2: --locked --dry-run also fails when no lockfile URL exists ---
# This is the key behavior: locked validation must run before dry-run short-circuit
assert_fail_contains "mise install --locked --dry-run 2>&1" "No lockfile URL found"

# --- Test 3: --locked --dry-run succeeds when lockfile URL exists ---
cat <<EOF >mise.lock
[[tools.jq]]
version = "1.7.1"
backend = "aqua:jqlang/jq"
"platforms.$PLATFORM" = { url = "https://example.com/jq-1.7.1.tar.gz" }
EOF

assert_contains "mise install --locked --dry-run 2>&1" "would install"
40 changes: 21 additions & 19 deletions src/backend/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -861,6 +861,27 @@ pub trait Backend: Debug + Send + Sync {
ctx: InstallContext,
tv: ToolVersion,
) -> eyre::Result<ToolVersion> {
// Check for --locked mode: if enabled and no lockfile URL exists, fail early
// Exempt tool stubs from lockfile requirements since they are ephemeral
// Also exempt backends that don't support URL locking (e.g., Rust uses rustup)
// This must run before the dry-run check so that --locked --dry-run still validates
if ctx.locked && !tv.request.source().is_tool_stub() && self.supports_lockfile_url() {
let platform_key = self.get_platform_key();
let has_lockfile_url = tv
.lock_platforms
.get(&platform_key)
.and_then(|p| p.url.as_ref())
.is_some();
if !has_lockfile_url {
bail!(
"No lockfile URL found for {} on platform {} (--locked mode)\n\
hint: Run `mise lock` to generate lockfile URLs, or disable locked mode",
tv.style(),
platform_key
);
}
}

// Handle dry-run mode early to avoid plugin installation
if ctx.dry_run {
use crate::ui::progress_report::ProgressIcon;
Expand Down Expand Up @@ -896,25 +917,6 @@ pub trait Backend: Debug + Send + Sync {
} else if self.is_version_installed(&ctx.config, &tv, true) {
return Ok(tv);
}
// Check for --locked mode: if enabled and no lockfile URL exists, fail early
// Exempt tool stubs from lockfile requirements since they are ephemeral
// Also exempt backends that don't support URL locking (e.g., Rust uses rustup)
if ctx.locked && !tv.request.source().is_tool_stub() && self.supports_lockfile_url() {
let platform_key = self.get_platform_key();
let has_lockfile_url = tv
.lock_platforms
.get(&platform_key)
.and_then(|p| p.url.as_ref())
.is_some();
if !has_lockfile_url {
bail!(
"No lockfile URL found for {} on platform {} (--locked mode)\n\
hint: Run `mise lock` to generate lockfile URLs, or disable locked mode",
tv.style(),
platform_key
);
}
}

// Track the installation asynchronously (fire-and-forget)
// Do this before install so the request has time to complete during installation
Expand Down
Loading