c75a866c02
The shipped default named one private, internet-unreachable host, so an unconfigured install failed against a server it could never reach rather than saying what to set. - drop the registry default and make the option required - reject a non-string, empty, or non-http(s) registry with the option name and how to set it - ask for a registry only when a run has to resolve one, so clean, list, and specs with an explicit url keep working without it
254 lines
6.4 KiB
Lua
254 lines
6.4 KiB
Lua
local fs = require('arti-pack.fs')
|
|
local installer = require('arti-pack.installer')
|
|
local Lock = require('arti-pack.lock')
|
|
local spec_mod = require('arti-pack.spec')
|
|
|
|
local M = {}
|
|
|
|
-- registry has no default: it is site-specific, so any shipped value would point
|
|
-- every install at a host it cannot reach. See M.setup.
|
|
local DEFAULTS = {
|
|
plugins = {},
|
|
auto_sync = true,
|
|
concurrency = 4,
|
|
timeout = 120,
|
|
}
|
|
|
|
M.config = vim.deepcopy(DEFAULTS)
|
|
|
|
local running = false
|
|
|
|
local function paths()
|
|
local data = vim.fn.stdpath('data')
|
|
return {
|
|
pack_dir = fs.join(data, 'site', 'pack', 'arti-pack'),
|
|
lock_path = fs.join(data, 'arti-pack', 'lock.json'),
|
|
cache_dir = fs.join(vim.fn.stdpath('cache'), 'arti-pack'),
|
|
}
|
|
end
|
|
|
|
local function notify(msg, level)
|
|
vim.notify('[arti-pack] ' .. msg, level or vim.log.levels.INFO)
|
|
end
|
|
|
|
local function context()
|
|
local p = paths()
|
|
return {
|
|
registry = M.config.registry,
|
|
pack_dir = p.pack_dir,
|
|
cache_dir = p.cache_dir,
|
|
concurrency = M.config.concurrency,
|
|
timeout = M.config.timeout,
|
|
lock = Lock.new(p.lock_path):load(),
|
|
}
|
|
end
|
|
|
|
-- A spec carrying its own url never touches the registry, so only demand one
|
|
-- when something in this run would have to be resolved against it.
|
|
local function registry_error(specs, registry)
|
|
for _, spec in ipairs(specs) do
|
|
if not spec.url then
|
|
local _, err = spec_mod.check_registry(registry)
|
|
return err
|
|
end
|
|
end
|
|
end
|
|
|
|
local function installed_dir(ctx, name, opt)
|
|
return fs.join(ctx.pack_dir, opt and 'opt' or 'start', name)
|
|
end
|
|
|
|
local function finalise(ctx, results)
|
|
local counts = { installed = 0, updated = 0, failed = 0 }
|
|
|
|
for _, res in ipairs(results) do
|
|
counts[res.status] = (counts[res.status] or 0) + 1
|
|
if res.status == 'failed' then
|
|
notify(string.format('%s: %s', res.name, res.err), vim.log.levels.ERROR)
|
|
end
|
|
end
|
|
|
|
ctx.lock:save()
|
|
|
|
for _, res in ipairs(results) do
|
|
if res.status ~= 'failed' then
|
|
local entry = ctx.lock:get(res.name)
|
|
local dir = installed_dir(ctx, res.name, entry and entry.opt)
|
|
local doc = fs.join(dir, 'doc')
|
|
if fs.exists(doc) then
|
|
pcall(vim.cmd, 'helptags ' .. vim.fn.fnameescape(doc))
|
|
end
|
|
if not (entry and entry.opt) then
|
|
pcall(vim.cmd.packadd, res.name)
|
|
end
|
|
end
|
|
end
|
|
|
|
local parts = {}
|
|
for _, key in ipairs({ 'installed', 'updated', 'failed' }) do
|
|
if counts[key] > 0 then
|
|
table.insert(parts, string.format('%d %s', counts[key], key))
|
|
end
|
|
end
|
|
|
|
if #parts == 0 then
|
|
notify('everything up to date')
|
|
else
|
|
notify(table.concat(parts, ', '), counts.failed > 0 and vim.log.levels.WARN or vim.log.levels.INFO)
|
|
end
|
|
end
|
|
|
|
function M.sync(opts)
|
|
opts = opts or {}
|
|
if running then
|
|
return notify('a sync is already running', vim.log.levels.WARN)
|
|
end
|
|
|
|
local specs, errors = spec_mod.parse_all(M.config.plugins)
|
|
for _, err in ipairs(errors) do
|
|
notify(err, vim.log.levels.ERROR)
|
|
end
|
|
|
|
local ctx = context()
|
|
local pending = {}
|
|
for _, spec in ipairs(specs) do
|
|
local current = ctx.lock:is_current(spec) and fs.exists(installed_dir(ctx, spec.name, spec.opt))
|
|
if opts.force or not current then
|
|
table.insert(pending, spec)
|
|
end
|
|
end
|
|
|
|
if #pending == 0 then
|
|
return notify('everything up to date')
|
|
end
|
|
|
|
local reg_err = registry_error(pending, ctx.registry)
|
|
if reg_err then
|
|
return notify(reg_err, vim.log.levels.ERROR)
|
|
end
|
|
|
|
running = true
|
|
notify(string.format('syncing %d plugin(s)', #pending))
|
|
installer.run(ctx, pending, function(results)
|
|
running = false
|
|
finalise(ctx, results)
|
|
end)
|
|
end
|
|
|
|
function M.clean()
|
|
local specs, errors = spec_mod.parse_all(M.config.plugins)
|
|
for _, err in ipairs(errors) do
|
|
notify(err, vim.log.levels.ERROR)
|
|
end
|
|
-- An unparseable entry still names a plugin the user wants, so the set of
|
|
-- wanted names is incomplete; deleting from it would remove a live install.
|
|
if #errors > 0 then
|
|
return notify('clean aborted, no plugin spec may be in error', vim.log.levels.WARN)
|
|
end
|
|
|
|
local wanted = {}
|
|
for _, spec in ipairs(specs) do
|
|
wanted[spec.name] = true
|
|
end
|
|
|
|
local ctx = context()
|
|
local removed = {}
|
|
|
|
for _, sub in ipairs({ 'start', 'opt' }) do
|
|
local root = fs.join(ctx.pack_dir, sub)
|
|
for _, entry in ipairs(fs.entries(root)) do
|
|
if not wanted[entry.name] then
|
|
fs.rm_rf(fs.join(root, entry.name))
|
|
table.insert(removed, entry.name)
|
|
end
|
|
end
|
|
end
|
|
|
|
for _, name in ipairs(ctx.lock:names()) do
|
|
if not wanted[name] then
|
|
ctx.lock:remove(name)
|
|
end
|
|
end
|
|
ctx.lock:save()
|
|
|
|
if #removed == 0 then
|
|
notify('nothing to clean')
|
|
else
|
|
table.sort(removed)
|
|
notify('removed ' .. table.concat(removed, ', '))
|
|
end
|
|
end
|
|
|
|
function M.list()
|
|
local specs, errors = spec_mod.parse_all(M.config.plugins)
|
|
local ctx = context()
|
|
local lines = {}
|
|
|
|
for _, spec in ipairs(specs) do
|
|
local entry = ctx.lock:get(spec.name)
|
|
local state
|
|
if not entry then
|
|
state = 'missing'
|
|
elseif not ctx.lock:is_current(spec) then
|
|
state = 'outdated (' .. tostring(entry.tag) .. ')'
|
|
elseif not fs.exists(installed_dir(ctx, spec.name, spec.opt)) then
|
|
state = 'gone from disk'
|
|
else
|
|
state = 'installed'
|
|
end
|
|
table.insert(
|
|
lines,
|
|
string.format('%-32s %-14s %-5s %s', spec.source, spec.tag, spec.opt and 'opt' or 'start', state)
|
|
)
|
|
end
|
|
|
|
for _, name in ipairs(ctx.lock:names()) do
|
|
local known = false
|
|
for _, spec in ipairs(specs) do
|
|
known = known or spec.name == name
|
|
end
|
|
if not known then
|
|
table.insert(lines, string.format('%-32s %-14s %-5s %s', name, '-', '-', 'orphaned'))
|
|
end
|
|
end
|
|
|
|
for _, err in ipairs(errors) do
|
|
table.insert(lines, 'error: ' .. err)
|
|
end
|
|
|
|
if #lines == 0 then
|
|
lines = { 'no plugins configured' }
|
|
end
|
|
notify(table.concat(lines, '\n'))
|
|
return lines
|
|
end
|
|
|
|
function M.setup(opts)
|
|
opts = opts or {}
|
|
M.config = vim.tbl_deep_extend('force', vim.deepcopy(DEFAULTS), opts)
|
|
-- tbl_deep_extend merges lists element-wise, which would splice defaults into
|
|
-- a user's plugin list; the caller's list is always authoritative.
|
|
M.config.plugins = opts.plugins or {}
|
|
|
|
if opts.registry ~= nil then
|
|
local _, err = spec_mod.check_registry(opts.registry)
|
|
if err then
|
|
notify(err, vim.log.levels.ERROR)
|
|
end
|
|
end
|
|
|
|
local p = paths()
|
|
fs.mkdirp(fs.join(p.pack_dir, 'start'))
|
|
fs.mkdirp(fs.join(p.pack_dir, 'opt'))
|
|
|
|
if M.config.auto_sync then
|
|
vim.schedule(function()
|
|
M.sync()
|
|
end)
|
|
end
|
|
|
|
return M
|
|
end
|
|
|
|
return M
|