From 2df4f9f83c60b67f71303c1acd11cfdcbbfca094 Mon Sep 17 00:00:00 2001 From: Jeff Dickey <216188+jdx@users.noreply.github.com> Date: Sun, 27 Oct 2024 07:32:38 -0500 Subject: [PATCH] feat: lockfiles --- .gitignore | 1 + docs/.vitepress/cli_commands.ts | 2 +- docs/cli/index.md | 1 - docs/cli/upgrade.md | 2 + e2e/lockfile/test_lockfile | 32 ++++++ man/man1/mise.1 | 3 - mise.usage.kdl | 6 +- schema/mise.json | 5 + settings.toml | 19 ++++ src/cli/current.rs | 2 +- src/cli/install.rs | 11 +- src/cli/settings/ls.rs | 2 + src/cli/settings/set.rs | 1 + src/cli/settings/unset.rs | 1 + src/cli/uninstall.rs | 3 +- src/cli/upgrade.rs | 7 +- src/cli/use.rs | 14 ++- src/cli/where.rs | 40 +++---- src/config/config_file/legacy_version.rs | 4 + src/config/config_file/mise_toml.rs | 4 + src/config/config_file/mod.rs | 1 + src/config/config_file/tool_versions.rs | 4 + src/config/mod.rs | 5 +- src/lockfile.rs | 138 +++++++++++++++++++++++ src/main.rs | 1 + src/toolset/mod.rs | 9 +- src/toolset/tool_request.rs | 23 +++- src/toolset/tool_version.rs | 6 + tasks/test/coverage | 2 +- 29 files changed, 298 insertions(+), 51 deletions(-) create mode 100644 e2e/lockfile/test_lockfile create mode 100644 src/lockfile.rs diff --git a/.gitignore b/.gitignore index 01fd0706b1..4b0ccc3d41 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ /dist/ /node_modules/ package-lock.json +.mise.lock *.log *.profraw diff --git a/docs/.vitepress/cli_commands.ts b/docs/.vitepress/cli_commands.ts index 7d5355b980..3a655ef244 100644 --- a/docs/.vitepress/cli_commands.ts +++ b/docs/.vitepress/cli_commands.ts @@ -74,7 +74,7 @@ export const commands: { [key: string]: Command } = { }, }, current: { - hide: false, + hide: true, }, deactivate: { hide: false, diff --git a/docs/cli/index.md b/docs/cli/index.md index 28050e9f07..65da168128 100644 --- a/docs/cli/index.md +++ b/docs/cli/index.md @@ -46,7 +46,6 @@ Answer yes to all confirmation prompts * [`mise config get [-f --file ] [KEY]`](/cli/config/get.md) * [`mise config ls [--no-header] [-J --json]`](/cli/config/ls.md) * [`mise config set [-f --file ] [-t --type ] `](/cli/config/set.md) -* [`mise current [PLUGIN]`](/cli/current.md) * [`mise deactivate`](/cli/deactivate.md) * [`mise direnv `](/cli/direnv.md) * [`mise direnv activate`](/cli/direnv/activate.md) diff --git a/docs/cli/upgrade.md b/docs/cli/upgrade.md index 7458367c9e..664ca4a036 100644 --- a/docs/cli/upgrade.md +++ b/docs/cli/upgrade.md @@ -10,6 +10,8 @@ By default, this keeps the range specified in mise.toml. So if you have node@20 upgrade to the latest 20.x.x version available. See the `--bump` flag to use the latest version and bump the version in mise.toml. +This will update mise.lock if it is enabled, see + ## Arguments ### `[TOOL@VERSION]...` diff --git a/e2e/lockfile/test_lockfile b/e2e/lockfile/test_lockfile new file mode 100644 index 0000000000..31e6a4c4e2 --- /dev/null +++ b/e2e/lockfile/test_lockfile @@ -0,0 +1,32 @@ +#!/usr/bin/env bash + +export MISE_LOCKFILE=1 +export MISE_EXPERIMENTAL=1 + +mise install tiny@1.0.0 +mise use tiny@1 +mise install tiny@1.0.1 +assert "mise config get -f .mise.toml tools.tiny" "1" +assert "mise where tiny" "$MISE_DATA_DIR/installs/tiny/1.0.0" +assert "mise ls tiny --json --current | jq -r '.[0].requested_version'" "1" +assert "mise ls tiny --json --current | jq -r '.[0].version'" "1.0.0" +assert "cat .mise.lock" '[tools] +tiny = "1.0.0"' + +mise use tiny@1 +assert "cat .mise.lock" '[tools] +tiny = "1.0.1"' +assert "mise ls tiny --json --current | jq -r '.[0].requested_version'" "1" +assert "mise ls tiny --json --current | jq -r '.[0].version'" "1.0.1" + +mise up tiny +assert "cat .mise.lock" '[tools] +tiny = "1.1.0"' +assert "mise ls tiny --json --current | jq -r '.[0].requested_version'" "1" +assert "mise ls tiny --json --current | jq -r '.[0].version'" "1.1.0" + +mise up tiny --bump +assert "cat .mise.lock" '[tools] +tiny = "3.1.0"' +assert "mise ls tiny --json --current | jq -r '.[0].requested_version'" "3" +assert "mise ls tiny --json --current | jq -r '.[0].version'" "3.1.0" diff --git a/man/man1/mise.1 b/man/man1/mise.1 index 076ce65ba2..abb52c594b 100644 --- a/man/man1/mise.1 +++ b/man/man1/mise.1 @@ -59,9 +59,6 @@ Generate shell completions mise\-config(1) Manage config files .TP -mise\-current(1) -Shows current active and installed runtime versions -.TP mise\-deactivate(1) Disable mise for current shell session .TP diff --git a/mise.usage.kdl b/mise.usage.kdl index 7b4535cd9a..0c7b1ae322 100644 --- a/mise.usage.kdl +++ b/mise.usage.kdl @@ -248,7 +248,7 @@ cmd "config" help="Manage config files" { arg "" help="The value to set the key to" } } -cmd "current" help="Shows current active and installed runtime versions" { +cmd "current" hide=true help="Shows current active and installed runtime versions" { long_help r"Shows current active and installed runtime versions This is similar to `mise ls --current`, but this only shows the runtime @@ -1333,7 +1333,9 @@ cmd "upgrade" help="Upgrades outdated tools" { By default, this keeps the range specified in mise.toml. So if you have node@20 set, it will upgrade to the latest 20.x.x version available. See the `--bump` flag to use the latest version -and bump the version in mise.toml." +and bump the version in mise.toml. + +This will update mise.lock if it is enabled, see https://mise.jdx.dev/configuration/settings.html#lockfile" after_long_help r"Examples: # Upgrades node to the latest version matching the range in mise.toml diff --git a/schema/mise.json b/schema/mise.json index 960e8d28cf..1fff57b0e3 100644 --- a/schema/mise.json +++ b/schema/mise.json @@ -284,6 +284,11 @@ "description": "Use libgit2 for git operations, set to false to shell out to git.", "type": "boolean" }, + "lockfile": { + "default": false, + "description": "Create and read lockfiles for tool versions.", + "type": "boolean" + }, "log_level": { "default": "info", "description": "Show more/less output.", diff --git a/settings.toml b/settings.toml index e5303e5958..69359e0b18 100644 --- a/settings.toml +++ b/settings.toml @@ -323,6 +323,25 @@ Use libgit2 for git operations. This is generally faster but may not be as compa system's libgit2 is not the same version as the one used by mise. """ +[lockfile] +env = "MISE_LOCKFILE" +type = "Bool" +default = false +description = "Create and read lockfiles for tool versions." +docs = """ +Create and read lockfiles for tool versions. This is useful when you'd like to have loose versions in mise.toml like this: + +```toml +[tools] +node = "22" +gh = "latest" +``` + +But you'd like the versions installed to be consistent within a project. When this is enabled, mise will automatically +create mise.lock files next to mise.toml files containing pinned versions. +When installing tools, mise will reference this lockfile if it exists and this setting is enabled to resolve versions. +""" + [log_level] env = "MISE_LOG_LEVEL" type = "String" diff --git a/src/cli/current.rs b/src/cli/current.rs index bbab555802..95de7c080d 100644 --- a/src/cli/current.rs +++ b/src/cli/current.rs @@ -12,7 +12,7 @@ use crate::toolset::{Toolset, ToolsetBuilder}; /// This is similar to `mise ls --current`, but this only shows the runtime /// and/or version. It's designed to fit into scripts more easily. #[derive(Debug, clap::Args)] -#[clap(verbatim_doc_comment, after_long_help = AFTER_LONG_HELP)] +#[clap(verbatim_doc_comment, hide = true, after_long_help = AFTER_LONG_HELP)] pub struct Current { /// Plugin to show versions of /// e.g.: ruby, node, cargo:eza, npm:prettier, etc. diff --git a/src/cli/install.rs b/src/cli/install.rs index 4f8337e59f..efc2f3c1d6 100644 --- a/src/cli/install.rs +++ b/src/cli/install.rs @@ -1,8 +1,9 @@ use crate::cli::args::{BackendArg, ToolArg}; use crate::config::Config; +use crate::lockfile; use crate::toolset::{InstallOptions, ToolRequest, ToolSource, ToolVersion, Toolset}; use crate::ui::multi_progress_report::MultiProgressReport; -use eyre::Result; +use eyre::{Result, WrapErr}; use itertools::Itertools; use std::collections::HashSet; @@ -64,7 +65,9 @@ impl Install { warn!("specify a version with `mise install @`"); return Ok(vec![]); } - ts.install_versions(config, tool_versions, &mpr, &self.install_opts()) + let versions = ts.install_versions(config, tool_versions, &mpr, &self.install_opts())?; + lockfile::update_lockfiles(&versions).wrap_err("failed to update lockfiles")?; + Ok(versions) } fn install_opts(&self) -> InstallOptions { @@ -125,7 +128,9 @@ impl Install { } let mpr = MultiProgressReport::get(); let mut ts = Toolset::from(trs.clone()); - ts.install_versions(config, versions, &mpr, &self.install_opts()) + let versions = ts.install_versions(config, versions, &mpr, &self.install_opts())?; + lockfile::update_lockfiles(&versions).wrap_err("failed to update lockfiles")?; + Ok(versions) } } diff --git a/src/cli/settings/ls.rs b/src/cli/settings/ls.rs index 61b9d8d5cd..9e7dea090d 100644 --- a/src/cli/settings/ls.rs +++ b/src/cli/settings/ls.rs @@ -84,6 +84,7 @@ mod tests { legacy_version_file = true legacy_version_file_disable_tools = [] libgit2 = true + lockfile = false not_found_auto_install = true paranoid = false pin = false @@ -153,6 +154,7 @@ mod tests { legacy_version_file legacy_version_file_disable_tools libgit2 + lockfile node not_found_auto_install paranoid diff --git a/src/cli/settings/set.rs b/src/cli/settings/set.rs index 69462386e4..a5860c2690 100644 --- a/src/cli/settings/set.rs +++ b/src/cli/settings/set.rs @@ -150,6 +150,7 @@ pub mod tests { legacy_version_file = false legacy_version_file_disable_tools = [] libgit2 = true + lockfile = false not_found_auto_install = true paranoid = false pin = false diff --git a/src/cli/settings/unset.rs b/src/cli/settings/unset.rs index 96f20a39ab..77ffc7e207 100644 --- a/src/cli/settings/unset.rs +++ b/src/cli/settings/unset.rs @@ -75,6 +75,7 @@ mod tests { legacy_version_file = true legacy_version_file_disable_tools = [] libgit2 = true + lockfile = false not_found_auto_install = true paranoid = false pin = false diff --git a/src/cli/uninstall.rs b/src/cli/uninstall.rs index 0c76fb4c01..25c29f771c 100644 --- a/src/cli/uninstall.rs +++ b/src/cli/uninstall.rs @@ -10,7 +10,7 @@ use crate::cli::args::ToolArg; use crate::config::Config; use crate::toolset::{ToolRequest, ToolSource, ToolVersion, ToolsetBuilder}; use crate::ui::multi_progress_report::MultiProgressReport; -use crate::{backend, runtime_symlinks, shims}; +use crate::{backend, lockfile, runtime_symlinks, shims}; /// Removes installed tool versions /// @@ -67,6 +67,7 @@ impl Uninstall { } } + lockfile::update_lockfiles(&[]).wrap_err("failed to update lockfiles")?; let ts = ToolsetBuilder::new().build(&config)?; shims::reshim(&ts, false).wrap_err("failed to reshim")?; runtime_symlinks::rebuild(&config)?; diff --git a/src/cli/upgrade.rs b/src/cli/upgrade.rs index 51051cd0de..2f62f175d0 100644 --- a/src/cli/upgrade.rs +++ b/src/cli/upgrade.rs @@ -4,7 +4,7 @@ use crate::file::display_path; use crate::toolset::{InstallOptions, OutdatedInfo, ToolVersion, ToolsetBuilder}; use crate::ui::multi_progress_report::MultiProgressReport; use crate::ui::progress_report::SingleReport; -use crate::{runtime_symlinks, shims, ui}; +use crate::{lockfile, runtime_symlinks, shims, ui}; use demand::DemandOption; use eyre::{Context, Result}; @@ -13,6 +13,8 @@ use eyre::{Context, Result}; /// By default, this keeps the range specified in mise.toml. So if you have node@20 set, it will /// upgrade to the latest 20.x.x version available. See the `--bump` flag to use the latest version /// and bump the version in mise.toml. +/// +/// This will update mise.lock if it is enabled, see https://mise.jdx.dev/configuration/settings.html#lockfile #[derive(Debug, clap::Args)] #[clap(visible_alias = "up", verbatim_doc_comment, after_long_help = AFTER_LONG_HELP)] pub struct Upgrade { @@ -142,7 +144,7 @@ impl Upgrade { latest_versions: true, }; let new_versions = outdated.iter().map(|o| o.tool_request.clone()).collect(); - ts.install_versions(config, new_versions, &mpr, &opts)?; + let versions = ts.install_versions(config, new_versions, &mpr, &opts)?; for (o, bump, mut cf) in config_file_updates { cf.replace_versions(o.tool_request.backend(), &[bump])?; @@ -154,6 +156,7 @@ impl Upgrade { self.uninstall_old_version(&o.tool_version, pr.as_ref())?; } + lockfile::update_lockfiles(&versions).wrap_err("failed to update lockfiles")?; let ts = ToolsetBuilder::new().with_args(&self.tool).build(config)?; shims::reshim(&ts, false).wrap_err("failed to reshim")?; runtime_symlinks::rebuild(config)?; diff --git a/src/cli/use.rs b/src/cli/use.rs index 116a6a3853..62bb0bdb54 100644 --- a/src/cli/use.rs +++ b/src/cli/use.rs @@ -1,7 +1,7 @@ use std::path::{Path, PathBuf}; use console::style; -use eyre::Result; +use eyre::{Result, WrapErr}; use itertools::Itertools; use crate::cli::args::{BackendArg, ToolArg}; @@ -13,7 +13,7 @@ use crate::env::{ use crate::file::display_path; use crate::toolset::{InstallOptions, ToolRequest, ToolSource, ToolVersion, ToolsetBuilder}; use crate::ui::multi_progress_report::MultiProgressReport; -use crate::{env, file}; +use crate::{env, file, lockfile}; /// Installs a tool and adds the version it to mise.toml. /// @@ -97,7 +97,7 @@ impl Use { None => ToolRequest::new(t.backend, "latest", ToolSource::Argument), }) .collect::>()?; - let versions = ts.install_versions( + let mut versions = ts.install_versions( &config, versions.clone(), &mpr, @@ -133,6 +133,14 @@ impl Use { cf.remove_plugin(plugin_name)?; } cf.save()?; + + for tv in &mut versions { + // update the source so the lockfile is updated correctly + tv.request.set_source(cf.source()); + } + + lockfile::update_lockfiles(&versions).wrap_err("failed to update lockfiles")?; + self.render_success_message(cf.as_ref(), &versions)?; Ok(()) } diff --git a/src/cli/where.rs b/src/cli/where.rs index 4e72e534a9..224edc1699 100644 --- a/src/cli/where.rs +++ b/src/cli/where.rs @@ -30,39 +30,29 @@ pub struct Where { impl Where { pub fn run(self) -> Result<()> { let config = Config::try_get()?; - let runtime = match self.tool.tvr { + let tvr = match self.tool.tvr { + Some(tvr) => tvr, None => match self.asdf_version { - Some(version) => self.tool.with_version(&version), + Some(version) => self.tool.with_version(&version).tvr.unwrap(), None => { - let ts = ToolsetBuilder::new() - .with_args(&[self.tool.clone()]) - .build(&config)?; - let v = ts - .versions + let ts = ToolsetBuilder::new().build(&config)?; + ts.versions .get(&self.tool.backend) - .and_then(|v| v.requests.first()) - .map(|r| r.version()); - self.tool.with_version(&v.unwrap_or(String::from("latest"))) + .and_then(|tvr| tvr.requests.first().cloned()) + .unwrap_or_else(|| self.tool.with_version("latest").tvr.unwrap()) } }, - _ => self.tool, }; - let plugin = backend::get(&runtime.backend); + let ba = tvr.backend(); + let backend = backend::get(ba); + let tv = tvr.resolve(backend.as_ref(), false)?; - match runtime - .tvr - .as_ref() - .map(|tvr| tvr.resolve(plugin.as_ref(), false)) - { - Some(Ok(tv)) if plugin.is_version_installed(&tv, true) => { - miseprintln!("{}", tv.install_path().to_string_lossy()); - Ok(()) - } - _ => Err(VersionNotInstalled( - runtime.backend.to_string(), - runtime.tvr.map(|tvr| tvr.version()).unwrap_or_default(), - ))?, + if backend.is_version_installed(&tv, true) { + miseprintln!("{}", tv.install_path().to_string_lossy()); + Ok(()) + } else { + Err(VersionNotInstalled(ba.to_string(), tvr.version()))? } } } diff --git a/src/config/config_file/legacy_version.rs b/src/config/config_file/legacy_version.rs index a5a4bd8ad6..30f0a76531 100644 --- a/src/config/config_file/legacy_version.rs +++ b/src/config/config_file/legacy_version.rs @@ -71,6 +71,10 @@ impl ConfigFile for LegacyVersionFile { unimplemented!() } + fn source(&self) -> ToolSource { + ToolSource::LegacyVersionFile(self.path.clone()) + } + fn to_tool_request_set(&self) -> Result { Ok(self.tools.clone()) } diff --git a/src/config/config_file/mise_toml.rs b/src/config/config_file/mise_toml.rs index 0f0ab8a0d6..3d3b60861f 100644 --- a/src/config/config_file/mise_toml.rs +++ b/src/config/config_file/mise_toml.rs @@ -320,6 +320,10 @@ impl ConfigFile for MiseToml { Ok(self.doc()?.to_string()) } + fn source(&self) -> ToolSource { + ToolSource::MiseToml(self.path.clone()) + } + fn to_tool_request_set(&self) -> eyre::Result { let source = ToolSource::MiseToml(self.path.clone()); let mut trs = ToolRequestSet::new(); diff --git a/src/config/config_file/mod.rs b/src/config/config_file/mod.rs index 9af7e1a7b7..ecd1b31db5 100644 --- a/src/config/config_file/mod.rs +++ b/src/config/config_file/mod.rs @@ -74,6 +74,7 @@ pub trait ConfigFile: Debug + Send + Sync { fn replace_versions(&mut self, fa: &BackendArg, versions: &[String]) -> eyre::Result<()>; fn save(&self) -> eyre::Result<()>; fn dump(&self) -> eyre::Result; + fn source(&self) -> ToolSource; fn to_toolset(&self) -> eyre::Result { Ok(self.to_tool_request_set()?.into()) } diff --git a/src/config/config_file/tool_versions.rs b/src/config/config_file/tool_versions.rs index 6af21e3b8c..d456230fe8 100644 --- a/src/config/config_file/tool_versions.rs +++ b/src/config/config_file/tool_versions.rs @@ -198,6 +198,10 @@ impl ConfigFile for ToolVersions { Ok(s.trim_end().to_string() + "\n") } + fn source(&self) -> ToolSource { + ToolSource::ToolVersions(self.path.clone()) + } + fn to_tool_request_set(&self) -> eyre::Result { Ok(self.tools.clone()) } diff --git a/src/config/mod.rs b/src/config/mod.rs index e82d6e88ba..559ef573aa 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -77,10 +77,7 @@ impl Config { .cloned() .collect_vec(); time!("load config_filenames"); - let config_paths = load_config_paths(&config_filenames) - .into_iter() - .unique_by(|p| p.canonicalize().unwrap_or_else(|_| p.clone())) - .collect_vec(); + let config_paths = load_config_paths(&config_filenames); time!("load config_paths"); trace!("config_paths: {config_paths:?}"); let config_files = load_all_config_files(&config_paths, &legacy_files)?; diff --git a/src/lockfile.rs b/src/lockfile.rs new file mode 100644 index 0000000000..e96e25c483 --- /dev/null +++ b/src/lockfile.rs @@ -0,0 +1,138 @@ +use crate::config::{Config, SETTINGS}; +use crate::file; +use crate::file::display_path; +use crate::toolset::{ToolSource, ToolVersion, ToolVersionList, ToolsetBuilder}; +use eyre::{Report, Result}; +use itertools::Itertools; +use once_cell::sync::Lazy; +use serde_derive::{Deserialize, Serialize}; +use std::collections::{BTreeMap, HashMap, HashSet}; +use std::path::{Path, PathBuf}; +use std::sync::Mutex; + +#[derive(Debug, Default, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Lockfile { + tools: BTreeMap, +} + +impl Lockfile { + pub fn read>(path: P) -> Result { + let content = file::read_to_string(path)?; + let lockfile: Lockfile = toml::from_str(&content)?; + Ok(lockfile) + } + + pub fn save>(&self, path: P) -> Result<()> { + SETTINGS.ensure_experimental("lockfile")?; + if self.is_empty() { + let _ = file::remove_file(path); + } else { + let content = toml::to_string_pretty(self)?; + file::write(path, content)?; + } + Ok(()) + } + + pub fn is_empty(&self) -> bool { + self.tools.is_empty() + } +} + +pub fn update_lockfiles(new_versions: &[ToolVersion]) -> Result<()> { + if !SETTINGS.lockfile { + return Ok(()); + } + SETTINGS.ensure_experimental("lockfile")?; + let config = Config::load()?; + let ts = ToolsetBuilder::new().build(&config)?; + let mut all_tool_names = HashSet::new(); + let mut tools_by_source = HashMap::new(); + for (source, group) in &ts.versions.iter().chunk_by(|(_, tvl)| &tvl.source) { + for (ba, tvl) in group { + tools_by_source + .entry(source.clone()) + .or_insert_with(HashMap::new) + .insert(ba.short.to_string(), tvl.clone()); + all_tool_names.insert(ba.short.to_string()); + } + } + + // add versions added within this session such as from `mise use` or `mise up` + for (backend, group) in &new_versions.iter().chunk_by(|tv| &tv.backend) { + let tvs = group.cloned().collect_vec(); + let source = tvs[0].request.source().clone(); + let mut tvl = ToolVersionList::new(backend.clone(), source.clone()); + tvl.versions.extend(tvs); + tools_by_source + .entry(source) + .or_insert_with(HashMap::new) + .insert(backend.short.to_string(), tvl); + } + + let lockfiles = config.config_files.keys().rev().collect_vec(); + debug!("updating {} lockfiles", lockfiles.len()); + + let empty = HashMap::new(); + for config_path in lockfiles { + let lockfile_path = config_path.with_extension("lock"); + let tool_source = ToolSource::MiseToml(config_path.clone()); + let tools = tools_by_source.get(&tool_source).unwrap_or(&empty); + trace!( + "updating {} tools in lockfile {}", + tools.len(), + display_path(&lockfile_path) + ); + let mut existing_lockfile = Lockfile::read(&lockfile_path) + .unwrap_or_else(|err| handle_missing_lockfile(err, &lockfile_path)); + + // there are tools that should remain in the lockfile even though they're not in this current toolset + // * tools that are disabled via settings + // * tools inside a parent config but are overridden by a child config (we just keep what was in the lockfile before, if anything) + existing_lockfile + .tools + .retain(|k, _| all_tool_names.contains(k) || SETTINGS.disable_tools.contains(k)); + + for (short, tvl) in tools { + for tv in &tvl.versions { + existing_lockfile + .tools + .insert(short.to_string(), tv.version.to_string()); + } + } + + existing_lockfile.save(&lockfile_path)?; + } + + Ok(()) +} + +pub fn get_locked_version(path: &Path, short: &str) -> Result> { + static CACHE: Lazy>> = Lazy::new(Default::default); + + if !SETTINGS.lockfile { + return Ok(None); + } + SETTINGS.ensure_experimental("lockfile")?; + + let mut cache = CACHE.lock().unwrap(); + let lockfile = cache.entry(path.to_path_buf()).or_insert_with(|| { + let lockfile_path = path.with_extension("lock"); + Lockfile::read(&lockfile_path) + .unwrap_or_else(|err| handle_missing_lockfile(err, &lockfile_path)) + }); + + Ok(lockfile.tools.get(short).cloned()) +} + +fn handle_missing_lockfile(err: Report, lockfile_path: &Path) -> Lockfile { + if let Some(io_err) = err.downcast_ref::() { + if io_err.kind() != std::io::ErrorKind::NotFound { + warn!( + "failed to read lockfile {}: {err:?}", + display_path(lockfile_path) + ); + } + } + Lockfile::default() +} diff --git a/src/main.rs b/src/main.rs index a8dd8bbbdc..a5f394dd69 100644 --- a/src/main.rs +++ b/src/main.rs @@ -40,6 +40,7 @@ mod hook_env; mod http; mod install_context; mod lock_file; +mod lockfile; pub(crate) mod logger; mod migrate; mod path_env; diff --git a/src/toolset/mod.rs b/src/toolset/mod.rs index f513acbf8f..583f2dc1e7 100644 --- a/src/toolset/mod.rs +++ b/src/toolset/mod.rs @@ -15,10 +15,10 @@ use crate::errors::Error; use crate::install_context::InstallContext; use crate::path_env::PathEnv; use crate::ui::multi_progress_report::MultiProgressReport; -use crate::{backend, env, runtime_symlinks, shims}; +use crate::{backend, env, lockfile, runtime_symlinks, shims}; pub use builder::ToolsetBuilder; use console::truncate_str; -use eyre::{eyre, Result}; +use eyre::{eyre, Result, WrapErr}; use indexmap::IndexMap; use itertools::Itertools; use rayon::prelude::*; @@ -144,7 +144,9 @@ impl Toolset { .filter(|tv| matches!(self.versions[&tv.backend].source, ToolSource::Argument)) .map(|tv| tv.request) .collect_vec(); - self.install_versions(config, versions, &mpr, opts) + let versions = self.install_versions(config, versions, &mpr, opts)?; + lockfile::update_lockfiles(vec![]).wrap_err("failed to update lockfiles")?; + Ok(versions) } pub fn list_missing_plugins(&self) -> Vec { @@ -528,6 +530,7 @@ impl Toolset { let mpr = MultiProgressReport::get(); let versions = self.install_versions(&config, versions.clone(), &mpr, &InstallOptions::new())?; + lockfile::update_lockfiles(&versions).wrap_err("failed to update lockfiles")?; return Ok(Some(versions)); } } diff --git a/src/toolset/tool_request.rs b/src/toolset/tool_request.rs index a1783d835a..fa46c7227a 100644 --- a/src/toolset/tool_request.rs +++ b/src/toolset/tool_request.rs @@ -5,11 +5,11 @@ use eyre::{bail, Result}; use versions::{Chunk, Version}; use xx::file; -use crate::backend; use crate::backend::Backend; use crate::cli::args::BackendArg; use crate::runtime_symlinks::is_runtime_symlink; use crate::toolset::{ToolSource, ToolVersion, ToolVersionOptions}; +use crate::{backend, lockfile}; #[derive(Debug, Clone, Hash, PartialEq, Eq)] pub enum ToolRequest { @@ -99,6 +99,17 @@ impl ToolRequest { } Ok(tvr) } + pub fn set_source(&mut self, source: ToolSource) -> Self { + match self { + Self::Version { source: s, .. } + | Self::Prefix { source: s, .. } + | Self::Ref { source: s, .. } + | Self::Path(_, _, s) + | Self::Sub { source: s, .. } + | Self::System(_, s) => *s = source, + } + self.clone() + } pub fn backend(&self) -> &BackendArg { match self { Self::Version { backend: f, .. } @@ -192,7 +203,17 @@ impl ToolRequest { } } + pub fn lockfile_resolve(&self) -> Result> { + if let Some(path) = self.source().path() { + return lockfile::get_locked_version(path, &self.backend().short); + } + Ok(None) + } + pub fn local_resolve(&self, v: &str) -> eyre::Result> { + if let Some(v) = self.lockfile_resolve()? { + return Ok(Some(v)); + } let backend = backend::get(self.backend()); let matches = backend.list_installed_versions_matching(v)?; if matches.iter().any(|m| m == v) { diff --git a/src/toolset/tool_version.rs b/src/toolset/tool_version.rs index 23b6ff89ca..28220245c0 100644 --- a/src/toolset/tool_version.rs +++ b/src/toolset/tool_version.rs @@ -39,6 +39,12 @@ impl ToolVersion { request: ToolRequest, latest_versions: bool, ) -> Result { + if !latest_versions { + if let Some(v) = request.lockfile_resolve()? { + let tv = Self::new(backend, request.clone(), v); + return Ok(tv); + } + } if let Some(plugin) = backend.plugin() { if !plugin.is_installed() { let tv = Self::new(backend, request.clone(), request.version()); diff --git a/tasks/test/coverage b/tasks/test/coverage index ced1b136c7..bce5dda1ac 100755 --- a/tasks/test/coverage +++ b/tasks/test/coverage @@ -12,7 +12,7 @@ export CARGO_TARGET_DIR="${CARGO_TARGET_DIR:-$PWD/target}" export PATH="${CARGO_TARGET_DIR}/debug:$PATH" echo "::group::Build w/ coverage" -cargo build --all-features +cargo build -q --all-features echo "::endgroup::" ./e2e/run_all_tests if [[ "${TEST_TRANCHE:-}" == 0 ]]; then