From 52dca238ca03b27a87c28e23abb407848acf1734 Mon Sep 17 00:00:00 2001 From: jdx <216188+jdx@users.noreply.github.com> Date: Wed, 17 Dec 2025 10:58:27 -0600 Subject: [PATCH] fix(lockfile): place lockfile alongside config file MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Changed lockfile placement to be in the same directory as the config file, rather than always at the project root. For example: - `.config/mise.toml` → `.config/mise.lock` - `.mise/config.toml` → `.mise/mise.lock` For conf.d directories, the lockfile is placed in the parent directory so all conf.d files share the same lockfile: - `.mise/conf.d/foo.toml` → `.mise/mise.lock` Also updated `cli/lock.rs` to use `lockfile_path_for_config` for consistency between the lock command and lockfile readers. Fixes jdx/mise#7343 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- src/cli/lock.rs | 70 ++++++++++++++++++++++++++++---------------- src/lockfile.rs | 78 ++++++++++++++++++++++++++++++++++--------------- 2 files changed, 99 insertions(+), 49 deletions(-) diff --git a/src/cli/lock.rs b/src/cli/lock.rs index 8935df8459..d6b435bd0f 100644 --- a/src/cli/lock.rs +++ b/src/cli/lock.rs @@ -4,9 +4,8 @@ use std::sync::Arc; use crate::backend::platform_target::PlatformTarget; use crate::config::Config; -use crate::config::config_file::config_root; use crate::file::display_path; -use crate::lockfile::{Lockfile, PlatformInfo}; +use crate::lockfile::{self, Lockfile, PlatformInfo}; use crate::platform::Platform; use crate::toolset::Toolset; use crate::ui::multi_progress_report::MultiProgressReport; @@ -137,20 +136,26 @@ impl Lock { } fn get_lockfile_path(&self, config: &Config) -> PathBuf { - // Get config root from the first config file, or use current dir - let root = config - .config_files - .keys() - .next() - .map(|p| config_root::config_root(p)) - .unwrap_or_else(|| std::env::current_dir().unwrap_or_default()); - - let lockfile_name = if self.local { - "mise.local.lock" + // Get lockfile path from the first config file + if let Some(config_path) = config.config_files.keys().next() { + let (lockfile_path, _) = lockfile::lockfile_path_for_config(config_path); + if self.local { + // Replace mise.lock with mise.local.lock + lockfile_path.with_file_name("mise.local.lock") + } else { + lockfile_path + } } else { - "mise.lock" - }; - root.join(lockfile_name) + // Fallback to current dir + let lockfile_name = if self.local { + "mise.local.lock" + } else { + "mise.lock" + }; + std::env::current_dir() + .unwrap_or_default() + .join(lockfile_name) + } } fn determine_target_platforms(&self, lockfile_path: &PathBuf) -> Result> { @@ -183,24 +188,39 @@ impl Lock { config: &Config, ts: &Toolset, ) -> Vec<(crate::cli::args::BackendArg, crate::toolset::ToolVersion)> { - // Calculate target config_root (same logic as get_lockfile_path) - let target_root = config + // Calculate target lockfile directory (same logic as get_lockfile_path) + let target_lockfile_dir = config .config_files .keys() .next() - .map(|p| config_root::config_root(p)) + .map(|p| { + let (lockfile_path, _) = lockfile::lockfile_path_for_config(p); + lockfile_path + .parent() + .map(|p| p.to_path_buf()) + .unwrap_or_default() + }) .unwrap_or_else(|| std::env::current_dir().unwrap_or_default()); - // Collect tools from config files in the target config_root only + // Collect tools from config files that share the same lockfile directory let mut all_tools: Vec<_> = Vec::new(); let mut seen: BTreeSet<(String, String)> = BTreeSet::new(); + // Helper to get lockfile directory for a config path + let get_lockfile_dir = |path: &std::path::Path| -> PathBuf { + let (lockfile_path, _) = lockfile::lockfile_path_for_config(path); + lockfile_path + .parent() + .map(|p| p.to_path_buf()) + .unwrap_or_default() + }; + // First, get all tools from the resolved toolset (these are the "current" versions) - // but only if they come from a config file in the target config_root + // but only if they come from a config file with the same lockfile directory for (backend, tv) in ts.list_current_versions() { - // Check if this tool's source is in the target config_root + // Check if this tool's source shares the same lockfile directory if let Some(source_path) = tv.request.source().path() - && config_root::config_root(source_path) != target_root + && get_lockfile_dir(source_path) != target_lockfile_dir { continue; } @@ -210,10 +230,10 @@ impl Lock { } } - // Then, iterate config files in the target config_root to find tools that may have been overridden + // Then, iterate config files with the same lockfile directory to find tools that may have been overridden for (path, cf) in config.config_files.iter() { - // Skip config files not in the target config_root - if config_root::config_root(path) != target_root { + // Skip config files that don't share the same lockfile directory + if get_lockfile_dir(path) != target_lockfile_dir { continue; } if let Ok(trs) = cf.to_tool_request_set() { diff --git a/src/lockfile.rs b/src/lockfile.rs index def3533fb0..1560c2779f 100644 --- a/src/lockfile.rs +++ b/src/lockfile.rs @@ -1,4 +1,3 @@ -use crate::config::config_file::config_root; use crate::config::{Config, Settings}; use crate::env; use crate::file; @@ -244,6 +243,12 @@ impl Lockfile { /// Determines the lockfile path for a given config file path /// Returns (lockfile_path, is_local) +/// +/// Lockfiles are placed alongside their config files: +/// - `mise.toml` -> `mise.lock` +/// - `.config/mise.toml` -> `.config/mise.lock` +/// - `.mise/config.toml` -> `.mise/mise.lock` +/// - `.mise/conf.d/foo.toml` -> `.mise/mise.lock` (conf.d files share parent's lockfile) pub fn lockfile_path_for_config(config_path: &Path) -> (PathBuf, bool) { let is_local = is_local_config(config_path); let lockfile_name = if is_local { @@ -252,30 +257,20 @@ pub fn lockfile_path_for_config(config_path: &Path) -> (PathBuf, bool) { "mise.lock" }; - // Fast path: for simple project configs (mise.toml, mise.local.toml, etc.) - // just use the parent directory. This avoids the expensive config_root call. - if let Some(parent) = config_path.parent() { - let filename = config_path - .file_name() - .and_then(|n| n.to_str()) - .unwrap_or_default(); - let parent_name = parent - .file_name() - .and_then(|n| n.to_str()) - .unwrap_or_default(); - - // If the config is directly in a project dir (not in .mise, .config, etc.) - // we can skip the full config_root calculation - if !matches!(parent_name, ".mise" | "mise" | ".config" | "conf.d") - && (filename.starts_with("mise.") || filename.starts_with(".mise.")) - { - return (parent.join(lockfile_name), is_local); - } - } + let parent = config_path.parent().unwrap_or(Path::new(".")); + let parent_name = parent + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or_default(); - // Full path calculation for complex cases (.mise/, .config/mise/, etc.) - let root = config_root::config_root(config_path); - (root.join(lockfile_name), is_local) + // For conf.d files, place lockfile at parent of conf.d so all conf.d files share one lockfile + let lockfile_dir = if parent_name == "conf.d" { + parent.parent().unwrap_or(parent) + } else { + parent + }; + + (lockfile_dir.join(lockfile_name), is_local) } /// Checks if a config path is a "local" config (should go to mise.local.lock) @@ -1123,4 +1118,39 @@ options = { exe = "rg" } assert_eq!(lockfile.tools["ripgrep"][0].options.len(), 2); assert_eq!(lockfile.tools["ripgrep"][1].options.len(), 1); } + + #[test] + fn test_lockfile_path_for_config() { + // Simple case: mise.toml in project root + let (path, is_local) = lockfile_path_for_config(Path::new("/foo/bar/mise.toml")); + assert_eq!(path, PathBuf::from("/foo/bar/mise.lock")); + assert!(!is_local); + + // Local config + let (path, is_local) = lockfile_path_for_config(Path::new("/foo/bar/mise.local.toml")); + assert_eq!(path, PathBuf::from("/foo/bar/mise.local.lock")); + assert!(is_local); + + // Config in .config directory + let (path, is_local) = lockfile_path_for_config(Path::new("/foo/bar/.config/mise.toml")); + assert_eq!(path, PathBuf::from("/foo/bar/.config/mise.lock")); + assert!(!is_local); + + // Config in .mise directory + let (path, is_local) = lockfile_path_for_config(Path::new("/foo/bar/.mise/config.toml")); + assert_eq!(path, PathBuf::from("/foo/bar/.mise/mise.lock")); + assert!(!is_local); + + // Config in conf.d directory - should go to parent of conf.d + let (path, is_local) = + lockfile_path_for_config(Path::new("/foo/bar/.mise/conf.d/foo.toml")); + assert_eq!(path, PathBuf::from("/foo/bar/.mise/mise.lock")); + assert!(!is_local); + + // Config in .config/mise/conf.d directory + let (path, is_local) = + lockfile_path_for_config(Path::new("/foo/bar/.config/mise/conf.d/foo.toml")); + assert_eq!(path, PathBuf::from("/foo/bar/.config/mise/mise.lock")); + assert!(!is_local); + } }