Add vimpack: archive-based Neovim plugin manager
## Why
Cloning plugins from git ties the editor to GitHub uptime and moving branches; archives at a pinned version from our own registry are reproducible and reachable offline.
## How
- Add a registry resolver interface `(spec, registry) -> {url, headers, filename, sha256?}` with builtin `template` and `artifactapi` kinds, a per-plugin registry override and a `url` escape hatch.
- Take auth tokens only from an env var or a `token_cmd`; reject a literal token at setup.
- Download with curl, verify an optional sha256, extract to a staging dir and rename into `pack/vimpack/{start,opt}/<name>`, restoring the previous tree if the swap fails.
- Track installed name/version/sha256 in `stdpath('data')/vimpack/manifest.json` and skip plugins already at the wanted version.
- Add `:VimpackInstall`, `:VimpackUpdate`, `:VimpackClean` and `:VimpackStatus`, running downloads through `vim.system` under a concurrency cap.
- Hash with a pure-Lua SHA-256; `vim.fn.sha256()` rejects binary archives with E976.
- Add a dependency-free `tests/minimal.lua` harness and 57 specs covering resolution, auth, manifest, planning, checksum mismatch, orphan detection and end-to-end install.
- Add `.woodpecker/{test,lint,pre-commit}.yaml`, luacheck/stylua config and pre-commit hooks.
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
-- End-to-end install over file:// URLs: real curl, real tar, real swap.
|
||||
local checksum = require('vimpack.checksum')
|
||||
local config = require('vimpack.config')
|
||||
local fsmod = require('vimpack.fs')
|
||||
local installer = require('vimpack.installer')
|
||||
local manifest = require('vimpack.manifest')
|
||||
local plan = require('vimpack.plan')
|
||||
|
||||
local function make_tarball(root, version, body)
|
||||
local src = vim.fs.joinpath(root, 'src', 'demo.nvim-' .. version, 'lua')
|
||||
vim.fn.mkdir(src, 'p')
|
||||
vim.fn.writefile({ body }, vim.fs.joinpath(src, 'demo.lua'))
|
||||
local archive = vim.fs.joinpath(root, 'demo-' .. version .. '.tar.gz')
|
||||
vim.fn.system({ 'tar', '-czf', archive, '-C', vim.fs.joinpath(root, 'src'), 'demo.nvim-' .. version })
|
||||
return archive
|
||||
end
|
||||
|
||||
local function install(cfg, data, pack_root)
|
||||
local built = plan.build(cfg, data, fsmod.scan(pack_root))
|
||||
local items = vim.list_extend(vim.deepcopy(built.install), built.update)
|
||||
local results
|
||||
installer.run(items, {
|
||||
manifest = data,
|
||||
pack_root = pack_root,
|
||||
timeout = 30000,
|
||||
concurrency = 2,
|
||||
}, function(r)
|
||||
results = r
|
||||
end)
|
||||
vim.wait(30000, function()
|
||||
return results ~= nil
|
||||
end)
|
||||
return results, items
|
||||
end
|
||||
|
||||
describe('install end to end', function()
|
||||
it('downloads, extracts, swaps and records the manifest', function()
|
||||
local root = tmpdir()
|
||||
local pack_root = vim.fs.joinpath(root, 'pack')
|
||||
local archive = make_tarball(root, '1.0.0', 'return 1')
|
||||
local cfg = assert(config.normalize({
|
||||
plugins = { { name = 'demo.nvim', version = '1.0.0', url = 'file://' .. archive } },
|
||||
}))
|
||||
local data = manifest.empty()
|
||||
|
||||
local results = install(cfg, data, pack_root)
|
||||
assert_eq(1, #results)
|
||||
assert_true(results[1].ok, tostring(results[1].err))
|
||||
|
||||
local installed = vim.fs.joinpath(pack_root, 'start', 'demo.nvim', 'lua', 'demo.lua')
|
||||
assert_true(fsmod.exists(installed), 'expected ' .. installed)
|
||||
assert_eq({ 'return 1' }, vim.fn.readfile(installed))
|
||||
|
||||
local entry = manifest.get(data, 'demo.nvim')
|
||||
assert_eq('1.0.0', entry.version)
|
||||
assert_eq(assert(checksum.sha256_file(archive)), entry.sha256)
|
||||
|
||||
-- Second pass is a no-op: nothing left to install.
|
||||
local _, items = install(cfg, data, pack_root)
|
||||
assert_eq(0, #items)
|
||||
end)
|
||||
|
||||
it('replaces the old tree on a version bump', function()
|
||||
local root = tmpdir()
|
||||
local pack_root = vim.fs.joinpath(root, 'pack')
|
||||
local data = manifest.empty()
|
||||
|
||||
local v1 = make_tarball(root, '1.0.0', 'return 1')
|
||||
local cfg1 = assert(config.normalize({
|
||||
plugins = { { name = 'demo.nvim', version = '1.0.0', url = 'file://' .. v1 } },
|
||||
}))
|
||||
assert_true(install(cfg1, data, pack_root)[1].ok)
|
||||
|
||||
local v2 = make_tarball(root, '2.0.0', 'return 2')
|
||||
local cfg2 = assert(config.normalize({
|
||||
plugins = { { name = 'demo.nvim', version = '2.0.0', url = 'file://' .. v2 } },
|
||||
}))
|
||||
local results = install(cfg2, data, pack_root)
|
||||
assert_true(results[1].ok, tostring(results[1].err))
|
||||
assert_eq({ 'return 2' }, vim.fn.readfile(vim.fs.joinpath(pack_root, 'start', 'demo.nvim', 'lua', 'demo.lua')))
|
||||
assert_eq('2.0.0', manifest.get(data, 'demo.nvim').version)
|
||||
end)
|
||||
|
||||
it('aborts on a checksum mismatch and keeps the previous install', function()
|
||||
local root = tmpdir()
|
||||
local pack_root = vim.fs.joinpath(root, 'pack')
|
||||
local data = manifest.empty()
|
||||
|
||||
local v1 = make_tarball(root, '1.0.0', 'return 1')
|
||||
assert_true(install(
|
||||
assert(config.normalize({
|
||||
plugins = { { name = 'demo.nvim', version = '1.0.0', url = 'file://' .. v1 } },
|
||||
})),
|
||||
data,
|
||||
pack_root
|
||||
)[1].ok)
|
||||
|
||||
local v2 = make_tarball(root, '2.0.0', 'return 2')
|
||||
local results = install(
|
||||
assert(config.normalize({
|
||||
plugins = {
|
||||
{ name = 'demo.nvim', version = '2.0.0', url = 'file://' .. v2, sha256 = string.rep('a', 64) },
|
||||
},
|
||||
})),
|
||||
data,
|
||||
pack_root
|
||||
)
|
||||
assert_true(not results[1].ok)
|
||||
assert_match('checksum mismatch', results[1].err)
|
||||
assert_eq({ 'return 1' }, vim.fn.readfile(vim.fs.joinpath(pack_root, 'start', 'demo.nvim', 'lua', 'demo.lua')))
|
||||
assert_eq('1.0.0', manifest.get(data, 'demo.nvim').version)
|
||||
end)
|
||||
|
||||
it('names the url when the download fails', function()
|
||||
local root = tmpdir()
|
||||
local missing = 'file://' .. vim.fs.joinpath(root, 'nope.tar.gz')
|
||||
local results = install(
|
||||
assert(config.normalize({
|
||||
plugins = { { name = 'demo.nvim', version = '1.0.0', url = missing } },
|
||||
})),
|
||||
manifest.empty(),
|
||||
vim.fs.joinpath(root, 'pack')
|
||||
)
|
||||
assert_true(not results[1].ok)
|
||||
assert_match('download failed', results[1].err)
|
||||
assert_match(missing, results[1].err)
|
||||
end)
|
||||
|
||||
it('moves a plugin from start to opt', function()
|
||||
local root = tmpdir()
|
||||
local pack_root = vim.fs.joinpath(root, 'pack')
|
||||
local data = manifest.empty()
|
||||
local archive = make_tarball(root, '1.0.0', 'return 1')
|
||||
|
||||
assert_true(install(
|
||||
assert(config.normalize({
|
||||
plugins = { { name = 'demo.nvim', version = '1.0.0', url = 'file://' .. archive } },
|
||||
})),
|
||||
data,
|
||||
pack_root
|
||||
)[1].ok)
|
||||
|
||||
local results = install(
|
||||
assert(config.normalize({
|
||||
plugins = { { name = 'demo.nvim', version = '1.0.0', url = 'file://' .. archive, opt = true } },
|
||||
})),
|
||||
data,
|
||||
pack_root
|
||||
)
|
||||
assert_true(results[1].ok, tostring(results[1].err))
|
||||
assert_true(fsmod.exists(vim.fs.joinpath(pack_root, 'opt', 'demo.nvim', 'lua', 'demo.lua')))
|
||||
assert_true(not fsmod.exists(vim.fs.joinpath(pack_root, 'start', 'demo.nvim')))
|
||||
end)
|
||||
end)
|
||||
Reference in New Issue
Block a user