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

export MISE_LOCKFILE=1

# === Test 1: mise use auto-locks all platforms ===
# When using a tool that supports cross-platform resolution (aqua tool),
# the lockfile should automatically get entries for all 5 common platforms,
# not just the current one.

cat <<EOF >mise.toml
[tools]
"aqua:jqlang/jq" = "1.7.1"
EOF

touch mise.lock
mise use "aqua:jqlang/jq@1.7.1"

# Verify all 5 common platforms are in the lockfile (auto-locked)
assert_contains "cat mise.lock" "platforms.linux-x64"
assert_contains "cat mise.lock" "platforms.linux-arm64"
assert_contains "cat mise.lock" "platforms.macos-x64"
assert_contains "cat mise.lock" "platforms.macos-arm64"
assert_contains "cat mise.lock" "platforms.windows-x64"
assert_contains "cat mise.lock" "jqlang/jq"

# === Test 2: subsequent install doesn't modify lockfile ===
# Simulates another developer running mise install - lockfile should not change
# because all platforms are already populated.

cp mise.lock mise.lock.before
mise install
assert "diff mise.lock mise.lock.before" ""

# === Test 3: mise install auto-locks a newly added tool ===

cat <<EOF >mise.toml
[tools]
"aqua:jqlang/jq" = "1.7.1"
"aqua:mikefarah/yq" = "4.44.6"
EOF

mise install
assert_contains "cat mise.lock" "mikefarah/yq"
assert_contains "cat mise.lock" "platforms.linux-x64"

rm -f mise.toml mise.lock mise.lock.before
121 changes: 17 additions & 104 deletions src/cli/lock.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
use std::collections::{BTreeMap, BTreeSet};
use std::path::PathBuf;
use std::collections::BTreeSet;
use std::path::{Path, PathBuf};
use std::sync::Arc;

use crate::backend::backend_type::BackendType;
use crate::backend::conda::CondaBackend;
use crate::backend::platform_target::PlatformTarget;
use crate::config::Config;
use crate::file::display_path;
use crate::lockfile::{self, CondaPackageInfo, Lockfile, PlatformInfo};
use crate::lockfile::{self, LockResolutionResult, Lockfile};
use crate::platform::Platform;
use crate::toolset::Toolset;
use crate::ui::multi_progress_report::MultiProgressReport;
Expand All @@ -17,17 +14,6 @@ use eyre::Result;
use tokio::sync::Semaphore;
use tokio::task::JoinSet;

/// Result type for lock task: (short_name, version, backend, platform, info, options, conda_packages)
type LockTaskResult = (
String,
String,
String,
Platform,
Option<PlatformInfo>,
BTreeMap<String, String>,
BTreeMap<String, CondaPackageInfo>,
);

/// Update lockfile checksums and URLs for all specified platforms
///
/// Updates checksums and download URLs for all platforms already specified in the lockfile.
Expand Down Expand Up @@ -163,29 +149,13 @@ impl Lock {
}
}

fn determine_target_platforms(&self, lockfile_path: &PathBuf) -> Result<Vec<Platform>> {
fn determine_target_platforms(&self, lockfile_path: &Path) -> Result<Vec<Platform>> {
if !self.platform.is_empty() {
// User specified platforms explicitly
return Platform::parse_multiple(&self.platform);
}

// Default: 5 common platforms + existing in lockfile + current platform
let mut platforms: BTreeSet<Platform> = Platform::common_platforms().into_iter().collect();
platforms.insert(Platform::current());

// Add any existing platforms from lockfile (only valid ones)
if let Ok(lockfile) = Lockfile::read(lockfile_path) {
for platform_key in lockfile.all_platform_keys() {
if let Ok(p) = Platform::parse(&platform_key) {
// Skip invalid platforms (e.g., tool-specific qualifiers like "wait-for-gh-rate-limit")
if p.validate().is_ok() {
platforms.insert(p);
}
}
}
}

Ok(platforms.into_iter().collect())
Ok(lockfile::determine_target_platforms(lockfile_path))
}

/// Collect tools that belong to a given lockfile pass (local or non-local).
Expand Down Expand Up @@ -342,7 +312,7 @@ impl Lock {
) -> Result<Vec<(String, String, bool)>> {
let jobs = self.jobs.unwrap_or(settings.jobs);
let semaphore = Arc::new(Semaphore::new(jobs));
let mut jset: JoinSet<LockTaskResult> = JoinSet::new();
let mut jset: JoinSet<LockResolutionResult> = JoinSet::new();
let mut results = Vec::new();

let mpr = MultiProgressReport::get();
Expand Down Expand Up @@ -375,60 +345,11 @@ impl Lock {
// Spawn tasks for each tool/platform variant combination
for (ba, tv, platform) in all_tasks {
let semaphore = semaphore.clone();
let backend = crate::backend::get(&ba);

jset.spawn(async move {
let _permit = semaphore.acquire().await;
let target = PlatformTarget::new(platform.clone());
let backend = crate::backend::get(&ba);

let (info, options, conda_packages) = if let Some(backend) = backend {
let options = backend.resolve_lockfile_options(&tv.request, &target);
match backend.resolve_lock_info(&tv, &target).await {
Ok(info) => {
// Resolve conda packages only for conda backend
let conda_packages = if backend.get_type() == BackendType::Conda {
let conda_backend = CondaBackend::from_arg(ba.clone());
match conda_backend.resolve_conda_packages(&tv, &target).await {
Ok(packages) => packages,
Err(e) => {
warn!(
"Failed to resolve conda packages for {} on {}: {}",
ba.short,
platform.to_key(),
e
);
BTreeMap::new()
}
}
} else {
BTreeMap::new()
};
(Some(info), options, conda_packages)
}
Err(e) => {
warn!(
"Failed to resolve {} for {}: {}",
ba.short,
platform.to_key(),
e
);
(None, options, BTreeMap::new())
}
}
} else {
warn!("Backend not found for {}", ba.short);
(None, BTreeMap::new(), BTreeMap::new())
};

(
ba.short.clone(),
tv.version.clone(),
ba.full(),
platform,
info,
options,
conda_packages,
)
lockfile::resolve_tool_lock_info(ba, tv, platform, backend).await
});
}

Expand All @@ -437,25 +358,17 @@ impl Lock {
while let Some(result) = jset.join_next().await {
completed += 1;
match result {
Ok((short, version, backend, platform, info, options, conda_packages)) => {
let platform_key = platform.to_key();
Ok(resolution) => {
let short = resolution.0.clone();
let version = resolution.1.clone();
let platform_key = resolution.3.to_key();
let ok = resolution.4.is_ok();
if let Err(msg) = &resolution.4 {
debug!("{msg}");
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Resolution failures silently downgraded from warn to debug

Medium Severity

When a user explicitly runs mise lock, resolution failures (e.g., "failed to resolve X for linux-arm64") are now logged at debug! level instead of the previous warn! level. This means failures are silently swallowed in normal usage and only visible with verbose logging. The debug! level makes sense for the background auto_lock_new_versions path, but for the explicit mise lock command, users expect to see warnings about platforms that failed to resolve.

Fix in Cursor Fix in Web

pr.set_message(format!("{}@{} {}", short, version, platform_key));
pr.set_position(completed);
let ok = info.is_some();
if let Some(info) = info {
lockfile.set_platform_info(
&short,
&version,
Some(&backend),
&options,
&platform_key,
info,
);
}
// Merge conda packages into the lockfile's shared section
for (basename, pkg_info) in conda_packages {
lockfile.set_conda_package(&platform_key, &basename, pkg_info);
}
lockfile::apply_lock_result(lockfile, resolution);
results.push((short, platform_key, ok));
}
Err(e) => {
Expand Down
7 changes: 7 additions & 0 deletions src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1675,6 +1675,13 @@ pub async fn rebuild_shims_and_runtime_symlinks(
lockfile::update_lockfiles(config, ts, new_versions)
.wrap_err("failed to update lockfiles")?;
});
if !new_versions.is_empty() {
measure!("auto-locking platforms", {
if let Err(e) = lockfile::auto_lock_new_versions(config, new_versions).await {
warn!("failed to auto-lock platforms for new versions: {e}");
}
});
}

Ok(())
}
Expand Down
Loading
Loading