Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
22 changes: 3 additions & 19 deletions src/cli/lock.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use std::collections::{BTreeMap, BTreeSet};
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use std::sync::Arc;

use crate::backend::backend_type::BackendType;
Expand Down Expand Up @@ -163,29 +163,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
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
186 changes: 186 additions & 0 deletions src/lockfile.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
use crate::backend::backend_type::BackendType;
use crate::backend::conda::CondaBackend;
use crate::backend::platform_target::PlatformTarget;
use crate::config::{Config, Settings};
use crate::env;
use crate::file;
use crate::file::display_path;
use crate::path::PathExt;
use crate::platform::Platform;
use crate::toolset::{ToolSource, ToolVersion, ToolVersionList, Toolset};
use eyre::{Report, Result, bail};
use itertools::Itertools;
Expand All @@ -15,6 +19,8 @@ use std::{
collections::{BTreeMap, BTreeSet, HashMap, HashSet},
sync::Arc,
};
use tokio::sync::Semaphore;
use tokio::task::JoinSet;
use toml_edit::DocumentMut;
use xx::regex;

Expand Down Expand Up @@ -644,6 +650,186 @@ pub fn update_lockfiles(config: &Config, ts: &Toolset, new_versions: &[ToolVersi
Ok(())
}

/// Determine target platforms for lockfile operations.
/// Returns the 5 common platforms + current platform + any existing platforms in the lockfile.
pub fn determine_target_platforms(lockfile_path: &Path) -> Vec<Platform> {
let lockfile = Lockfile::read(lockfile_path).ok();
determine_target_platforms_from_lockfile(lockfile.as_ref())
}

/// Determine target platforms using an already-loaded lockfile.
fn determine_target_platforms_from_lockfile(lockfile: Option<&Lockfile>) -> Vec<Platform> {
let mut platforms: BTreeSet<Platform> = Platform::common_platforms().into_iter().collect();
platforms.insert(Platform::current());
if let Some(lockfile) = lockfile {
for platform_key in lockfile.all_platform_keys() {
if let Ok(p) = Platform::parse(&platform_key)
&& p.validate().is_ok()
{
platforms.insert(p);
}
}
}
platforms.into_iter().collect()
}

/// After installing new tool versions, resolve checksums/URLs for all common platforms
/// so the lockfile is complete and doesn't change when other developers on different
/// platforms run `mise install`.
pub async fn auto_lock_new_versions(_config: &Config, new_versions: &[ToolVersion]) -> Result<()> {
if !Settings::get().lockfile || new_versions.is_empty() {
return Ok(());
}

// Group new_versions by lockfile path
let mut versions_by_lockfile: HashMap<PathBuf, Vec<&ToolVersion>> = HashMap::new();
for tv in new_versions {
if let Some(source_path) = tv.request.source().path() {
let (lockfile_path, _) = lockfile_path_for_config(source_path);
versions_by_lockfile
.entry(lockfile_path)
.or_default()
.push(tv);
}
Comment thread
cursor[bot] marked this conversation as resolved.
}

let settings = Settings::get();
let jobs = settings.jobs;

for (lockfile_path, versions) in versions_by_lockfile {
// Only update existing lockfiles (consistent with update_lockfiles)
if !lockfile_path.exists() {
continue;
}

let mut lockfile = Lockfile::read(&lockfile_path)
.unwrap_or_else(|err| handle_lockfile_read_error(err, &lockfile_path));

let target_platforms = determine_target_platforms_from_lockfile(Some(&lockfile));

let semaphore = Arc::new(Semaphore::new(jobs));
let mut jset: JoinSet<AutoLockResult> = JoinSet::new();

for tv in &versions {
let ba = tv.ba().clone();
let backend = crate::backend::get(&ba);

for platform in &target_platforms {
// Expand platform variants from the backend
let variants = if let Some(ref backend) = backend {
backend.platform_variants(platform)
} else {
vec![platform.clone()]
};

for variant in variants {
let platform_key = variant.to_key();

// Skip if this tool/version/platform already has both checksum and URL
if let Some(tools) = lockfile.tools.get(&ba.short)
&& let Some(tool) = tools.iter().find(|t| t.version == tv.version)
&& let Some(info) = tool.platforms.get(&platform_key)
&& info.checksum.is_some()
&& info.url.is_some()
{
continue;
}

let semaphore = semaphore.clone();
let ba = ba.clone();
let tv = (*tv).clone();
let backend = backend.clone();

jset.spawn(async move {
let _permit = semaphore.acquire().await;
let target = PlatformTarget::new(variant.clone());

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) => {
let conda_packages = if backend.get_type() == BackendType::Conda
{
let conda_backend = CondaBackend::from_arg(ba.clone());
conda_backend
.resolve_conda_packages(&tv, &target)
.await
.unwrap_or_default()
} else {
BTreeMap::new()
};
(Some(info), options, conda_packages)
}
Err(e) => {
debug!(
"auto-lock: failed to resolve {} for {}: {}",
ba.short,
variant.to_key(),
e
);
(None, options, BTreeMap::new())
}
}
} else {
(None, BTreeMap::new(), BTreeMap::new())
};

(
ba.short.clone(),
tv.version.clone(),
ba.full(),
variant,
info,
options,
conda_packages,
)
});
Comment thread
cursor[bot] marked this conversation as resolved.
}
}
}

// Collect results and update lockfile
while let Some(result) = jset.join_next().await {
match result {
Ok((short, version, backend, platform, info, options, conda_packages)) => {
let platform_key = platform.to_key();
if let Some(info) = info {
lockfile.set_platform_info(
&short,
&version,
Some(&backend),
&options,
&platform_key,
info,
);
}
for (basename, pkg_info) in conda_packages {
lockfile.set_conda_package(&platform_key, &basename, pkg_info);
}
}
Err(e) => {
debug!("auto-lock task failed: {}", e);
}
}
}

lockfile.save(&lockfile_path)?;
}

Ok(())
}

/// Result type for auto-lock tasks
type AutoLockResult = (
String,
String,
String,
Platform,
Option<PlatformInfo>,
BTreeMap<String, String>,
BTreeMap<String, crate::backend::conda::CondaPackageInfo>,
);

/// Merge tool entries with environment tracking and deduplication
/// Rules:
/// - Same version+options: if any has no env (base), keep only base entry; otherwise merge env arrays
Expand Down
Loading