mirror of
https://github.com/neovim/neovim.git
synced 2025-12-11 17:12:40 +00:00
feat(pack)!: synchronize lockfile with installed plugins when reading it
Problem: Lockfile can become out of sync with what is actually installed
on disk when user performs (somewhat reasonable) manual actions like:
- Delete lockfile and expect it to regenerate.
- Delete plugin directory without `vim.pack.del()`.
- Manually edit lock data in a bad way.
Solution: Synchronize lockfile data with installed plugins on every
lockfile read. In particular:
1. Install immediately all missing plugins with valid lock data.
This helps with "manually delete plugin directory" case by
prompting user to figure out how to properly delete a plugin.
2. Repair lock data for properly installed plugins.
This helps with "manually deleted lockfile", "manually edited
lockfile in an unexpected way", "installation terminated due to
timeout" cases.
3. Remove unrepairable corrupted lock data and their plugins. This
includes bad lock data for missing plugins and any lock data
for corrupted plugins (right now this only means that plugin
path is not a directory, but can be built upon).
Step 1 also improves usability in case there are lazy loaded plugins
that are rarely loaded (like on `FileType` event, for example):
- Previously starting with config+lockfile on a new machine only
installs rare `vim.pack.add()` plugin after it is called (while
an entry in lockfile would still be present). This could be
problematic if there is no Internet connection, for example.
- Now all plugins from the lockfile are installed before actually
executing the first `vim.pack.add()` call in 'init.lua'. And later
they are only loaded on a rare `vim.pack.add()` call.
---
Synchronizing lockfile on its every read makes it work more robustly
if other `vim.pack` functions are called without any `vim.pack.add()`.
---
Performance for a regular startup (good lockfile, everything is
installed) is not affected and usually even increased. The bottleneck
in this area is figuring out which plugins need to be installed.
Previously the check was done by `vim.uv.fs_stat()` for every plugin
in `vim.pack.add()`. Now it is replaced with a single `vim.fs.dir()`
traversal during lockfile sync while later using lockfile data to
figure out if plugin needs to be installed.
The single `vim.fs.dir` approach scales better than `vim.uv.fs_stat`,
but might be less performant if there are many plugins that will be
not loaded via `vim.pack.add()` during startup.
Rough estimate of how long the same steps (read lockfile and normalize
plugin array) take with a single `vim.pack.add()` filled with 43
plugins benchmarking:
- Before commit: ~700 ms
- After commit: ~550 ms
This commit is contained in:
@@ -18,8 +18,11 @@
|
||||
---located at `$XDG_CONFIG_HOME/nvim/nvim-pack-lock.json`. It is a JSON file that
|
||||
---is used to persistently track data about plugins.
|
||||
---For a more robust config treat lockfile like its part: put under version control, etc.
|
||||
---In this case initial install prefers revision from the lockfile instead of
|
||||
---inferring from `version`. Should not be edited by hand or deleted.
|
||||
---In this case all plugins from the lockfile will be installed at once and
|
||||
---at lockfile's revision (instead of inferring from `version`).
|
||||
---Should not be edited by hand. Corrupted data for installed plugins is repaired
|
||||
---(including after deleting whole file), but `version` fields will be missing
|
||||
---for not yet added plugins.
|
||||
---
|
||||
---Example workflows ~
|
||||
---
|
||||
@@ -357,7 +360,7 @@ end
|
||||
local function new_plug(spec, plug_dir)
|
||||
local spec_resolved = normalize_spec(spec)
|
||||
local path = vim.fs.joinpath(plug_dir or get_plug_dir(), spec_resolved.name)
|
||||
local info = { err = '', installed = uv.fs_stat(path) ~= nil }
|
||||
local info = { err = '', installed = plugin_lock.plugins[spec_resolved.name] ~= nil }
|
||||
return { spec = spec_resolved, path = path, info = info }
|
||||
end
|
||||
|
||||
@@ -652,7 +655,7 @@ local function install_list(plug_list, confirm)
|
||||
|
||||
-- Ensure that not fully installed plugins are absent on disk and in lockfile
|
||||
for _, p in ipairs(plug_list) do
|
||||
if not p.info.installed then
|
||||
if not (p.info.installed and uv.fs_stat(p.path) ~= nil) then
|
||||
plugin_lock.plugins[p.spec.name] = nil
|
||||
vim.fs.rm(p.path, { recursive = true, force = true })
|
||||
end
|
||||
@@ -760,27 +763,112 @@ local function lock_write()
|
||||
assert(uv.fs_close(fd))
|
||||
end
|
||||
|
||||
local function lock_read()
|
||||
--- @param names string[]
|
||||
local function lock_repair(names, plug_dir)
|
||||
--- @async
|
||||
local function f()
|
||||
for _, name in ipairs(names) do
|
||||
local path = vim.fs.joinpath(plug_dir, name)
|
||||
-- Try reusing existing table to preserve maybe present `version`
|
||||
local data = plugin_lock.plugins[name] or {}
|
||||
data.rev = git_get_hash('HEAD', path)
|
||||
data.src = git_cmd({ 'remote', 'get-url', 'origin' }, path)
|
||||
plugin_lock.plugins[name] = data
|
||||
end
|
||||
end
|
||||
async.run(f):wait()
|
||||
end
|
||||
|
||||
--- Sync lockfile data and installed plugins:
|
||||
--- - Install plugins that have proper lockfile data but are not on disk.
|
||||
--- - Repair corrupted lock data for installed plugins.
|
||||
--- - Remove unrepairable corrupted lock data and plugins.
|
||||
local function lock_sync(confirm)
|
||||
if type(plugin_lock.plugins) ~= 'table' then
|
||||
plugin_lock.plugins = {}
|
||||
end
|
||||
|
||||
-- Compute installed plugins
|
||||
-- NOTE: The directory traversal is done on every startup, but it is very fast.
|
||||
-- Also, single `vim.fs.dir()` scales better than on demand `uv.fs_stat()` checks.
|
||||
local plug_dir = get_plug_dir()
|
||||
local installed = {} --- @type table<string,string>
|
||||
for name, fs_type in vim.fs.dir(plug_dir) do
|
||||
installed[name] = fs_type
|
||||
plugin_lock.plugins[name] = plugin_lock.plugins[name] or {}
|
||||
end
|
||||
|
||||
-- Traverse once optimizing for "regular startup" (no repair, no install)
|
||||
local to_install = {} --- @type vim.pack.Plug[]
|
||||
local to_repair = {} --- @type string[]
|
||||
local to_remove = {} --- @type string[]
|
||||
for name, data in pairs(plugin_lock.plugins) do
|
||||
if type(data) ~= 'table' then
|
||||
data = {} ---@diagnostic disable-line: missing-fields
|
||||
plugin_lock.plugins[name] = data
|
||||
end
|
||||
|
||||
-- Deserialize `version`
|
||||
local version = data.version
|
||||
if type(version) == 'string' then
|
||||
data.version = version:match("^'(.+)'$") or vim.version.range(version)
|
||||
end
|
||||
|
||||
-- Synchronize
|
||||
local is_bad_lock = type(data.rev) ~= 'string' or type(data.src) ~= 'string'
|
||||
local is_bad_plugin = installed[name] and installed[name] ~= 'directory'
|
||||
if is_bad_lock or is_bad_plugin then
|
||||
local t = installed[name] == 'directory' and to_repair or to_remove
|
||||
t[#t + 1] = name
|
||||
elseif not installed[name] then
|
||||
local spec = { src = data.src, name = name, version = data.version }
|
||||
to_install[#to_install + 1] = new_plug(spec, plug_dir)
|
||||
end
|
||||
end
|
||||
|
||||
-- Perform actions if needed
|
||||
if #to_install > 0 then
|
||||
table.sort(to_install, function(a, b)
|
||||
return a.spec.name < b.spec.name
|
||||
end)
|
||||
install_list(to_install, confirm)
|
||||
lock_write()
|
||||
end
|
||||
|
||||
if #to_repair > 0 then
|
||||
lock_repair(to_repair, plug_dir)
|
||||
table.sort(to_repair)
|
||||
notify('Repaired corrupted lock data for plugins: ' .. table.concat(to_repair, ', '), 'WARN')
|
||||
lock_write()
|
||||
end
|
||||
|
||||
if #to_remove > 0 then
|
||||
for _, name in ipairs(to_remove) do
|
||||
plugin_lock.plugins[name] = nil
|
||||
vim.fs.rm(vim.fs.joinpath(plug_dir, name), { recursive = true, force = true })
|
||||
end
|
||||
table.sort(to_remove)
|
||||
notify('Removed corrupted lock data for plugins: ' .. table.concat(to_remove, ', '), 'WARN')
|
||||
lock_write()
|
||||
end
|
||||
end
|
||||
|
||||
local function lock_read(confirm)
|
||||
if plugin_lock then
|
||||
return
|
||||
end
|
||||
local fd = uv.fs_open(lock_get_path(), 'r', 438)
|
||||
if not fd then
|
||||
plugin_lock = { plugins = {} }
|
||||
return
|
||||
end
|
||||
local stat = assert(uv.fs_fstat(fd))
|
||||
local data = assert(uv.fs_read(fd, stat.size, 0))
|
||||
assert(uv.fs_close(fd))
|
||||
plugin_lock = vim.json.decode(data) --- @type vim.pack.Lock
|
||||
|
||||
-- Deserialize `version`
|
||||
for _, l_data in pairs(plugin_lock.plugins) do
|
||||
local version = l_data.version
|
||||
if type(version) == 'string' then
|
||||
l_data.version = version:match("^'(.+)'$") or vim.version.range(version)
|
||||
end
|
||||
local fd = uv.fs_open(lock_get_path(), 'r', 438)
|
||||
if fd then
|
||||
local stat = assert(uv.fs_fstat(fd))
|
||||
local data = assert(uv.fs_read(fd, stat.size, 0))
|
||||
assert(uv.fs_close(fd))
|
||||
plugin_lock = vim.json.decode(data)
|
||||
else
|
||||
plugin_lock = { plugins = {} }
|
||||
end
|
||||
|
||||
lock_sync(vim.F.if_nil(confirm, true))
|
||||
end
|
||||
|
||||
--- @class vim.pack.keyset.add
|
||||
@@ -821,6 +909,8 @@ function M.add(specs, opts)
|
||||
opts = vim.tbl_extend('force', { load = vim.v.vim_did_init == 1, confirm = true }, opts or {})
|
||||
vim.validate('opts', opts, 'table')
|
||||
|
||||
lock_read(opts.confirm)
|
||||
|
||||
local plug_dir = get_plug_dir()
|
||||
local plugs = {} --- @type vim.pack.Plug[]
|
||||
for i = 1, #specs do
|
||||
@@ -829,7 +919,6 @@ function M.add(specs, opts)
|
||||
plugs = normalize_plugs(plugs)
|
||||
|
||||
-- Pre-process
|
||||
lock_read()
|
||||
local plugs_to_install = {} --- @type vim.pack.Plug[]
|
||||
local needs_lock_write = false
|
||||
for _, p in ipairs(plugs) do
|
||||
|
||||
Reference in New Issue
Block a user