feat: new task pipeline runner

This commit is contained in:
Folke Lemaitre 2022-11-28 11:04:32 +01:00
parent 97f44f9f65
commit ab1b512545
No known key found for this signature in database
GPG Key ID: 41F8B1FBACAE2040
7 changed files with 427 additions and 298 deletions

View File

@ -1,109 +1,86 @@
local Config = require("lazy.core.config") local Config = require("lazy.core.config")
local Task = require("lazy.manage.task")
local Runner = require("lazy.manage.runner") local Runner = require("lazy.manage.runner")
local Plugin = require("lazy.core.plugin") local Plugin = require("lazy.core.plugin")
local M = {} local M = {}
---@alias ManagerOpts {wait?: boolean, plugins?: LazyPlugin[], clear?: boolean, show?: boolean} ---@class ManagerOpts
---@field wait? boolean
---@field clear? boolean
---@field interactive? boolean
---@param operation TaskType ---@param ropts RunnerOpts
---@param opts? ManagerOpts ---@param opts? ManagerOpts
---@param filter? fun(plugin:LazyPlugin):boolean? function M.run(ropts, opts)
function M.run(operation, opts, filter)
opts = opts or {} opts = opts or {}
local plugins = opts.plugins or Config.plugins if opts.interactive == nil then
opts.interactive = Config.options.interactive
end
if ropts.interactive == nil then
ropts.interactive = opts.interactive
end
if opts.clear then if opts.clear then
M.clear() M.clear()
end end
if opts.show then if opts.interactive then
require("lazy.view").show() require("lazy.view").show()
end end
---@type Runner ---@type Runner
local runner = Runner.new() local runner = Runner.new(ropts)
runner:start()
-- install missing plugins
for _, plugin in pairs(plugins) do
if filter == nil or filter(plugin) then
runner:add(Task.new(plugin, operation))
end
end
vim.cmd([[do User LazyRender]]) vim.cmd([[do User LazyRender]])
-- wait for install to finish -- wait for post-install to finish
runner:wait(function() runner:wait(function()
-- check if we need to do any post-install hooks vim.cmd([[do User LazyRender]])
for _, plugin in ipairs(runner:plugins()) do
if plugin.dirty then
runner:add(Task.new(plugin, "docs"))
if plugin.opt == false or plugin.run then
runner:add(Task.new(plugin, "run"))
end
end
plugin.dirty = false
if opts.show and operation == "update" and plugin.updated and plugin.updated.from ~= plugin.updated.to then
runner:add(Task.new(plugin, "log", {
log = {
from = plugin.updated.from,
to = plugin.updated.to,
},
}))
end
end
-- wait for post-install to finish
runner:wait(function()
vim.cmd([[do User LazyRender]])
end)
end) end)
if opts.wait then if opts.wait then
runner:wait() runner:wait()
end end
return runner
end end
---@param opts? ManagerOpts ---@param opts? ManagerOpts
function M.install(opts) function M.install(opts)
---@param plugin LazyPlugin M.run({
M.run("install", opts, function(plugin) pipeline = { "git.install", { "plugin.docs", "plugin.run" } },
return plugin.uri and not plugin.installed plugins = function(plugin)
end) return plugin.uri and not plugin.installed
end,
}, opts)
end end
---@param opts? ManagerOpts ---@param opts? ManagerOpts
function M.update(opts) function M.update(opts)
---@param plugin LazyPlugin M.run({
M.run("update", opts, function(plugin) pipeline = { "git.update", { "plugin.docs", "plugin.run" }, "git.log" },
return plugin.uri and plugin.installed plugins = function(plugin)
end) return plugin.uri and plugin.installed
end,
}, opts)
end end
---@param opts? ManagerOpts ---@param opts? ManagerOpts
function M.log(opts) function M.log(opts)
---@param plugin LazyPlugin M.run({
M.run("log", opts, function(plugin) pipeline = { "git.log" },
return plugin.uri and plugin.installed plugins = function(plugin)
end) return plugin.uri and plugin.installed
end end,
}, opts)
---@param opts? ManagerOpts
function M.docs(opts)
---@param plugin LazyPlugin
M.run("docs", opts, function(plugin)
return plugin.installed
end)
end end
---@param opts? ManagerOpts ---@param opts? ManagerOpts
function M.clean(opts) function M.clean(opts)
opts = opts or {}
Plugin.update_state(true) Plugin.update_state(true)
opts.plugins = vim.tbl_values(Config.to_clean) M.run({
M.run("clean", opts) pipeline = { "plugin.clean" },
plugins = Config.to_clean,
}, opts)
end end
function M.clear() function M.clear()
@ -114,7 +91,7 @@ function M.clear()
if plugin.tasks then if plugin.tasks then
---@param task LazyTask ---@param task LazyTask
plugin.tasks = vim.tbl_filter(function(task) plugin.tasks = vim.tbl_filter(function(task)
return task.running return task:is_running()
end, plugin.tasks) end, plugin.tasks)
end end
end end

View File

@ -1,24 +1,124 @@
local Task = require("lazy.manage.task")
local Config = require("lazy.core.config")
---@alias LazyPipeline (TaskType|TaskType[])[]
---@class RunnerOpts
---@field pipeline LazyPipeline
---@field interactive? boolean
---@field plugins? LazyPlugin[]|fun(plugin:LazyPlugin):any?
---@class Runner ---@class Runner
---@field _tasks LazyTask[] ---@field _tasks LazyTask[]
---@field _plugins LazyPlugin[]
---@field _running boolean
---@field _on_done fun()[]
---@field _waiting fun()[]
---@field _opts RunnerOpts
local Runner = {} local Runner = {}
function Runner.new() ---@param opts RunnerOpts
local self = setmetatable({}, { function Runner.new(opts)
__index = Runner, local self = setmetatable({}, { __index = Runner })
}) self._opts = opts or {}
self._tasks = {} self._tasks = {}
local plugins = self._opts.plugins
if type(plugins) == "function" then
self._plugins = vim.tbl_filter(plugins, Config.plugins)
else
self._plugins = plugins or Config.plugins
end
self._running = false
self._on_done = {}
self._waiting = {}
return self return self
end end
---@param task LazyTask ---@param plugin LazyPlugin
function Runner:add(task) ---@param pipeline LazyPipeline
table.insert(self._tasks, task) function Runner:_run(plugin, pipeline)
task:start() if #pipeline == 0 then
return
end
local ops = table.remove(pipeline, 1)
if ops == "wait" then
return table.insert(self._waiting, function()
self:_run(plugin, pipeline)
end)
end
ops = type(ops) == "string" and { ops } or ops
---@cast ops TaskType[]
---@type LazyTask[]
local tasks = {}
local function on_done()
for _, task in ipairs(tasks) do
if task.error or not task:is_done() then
return
end
end
self:_run(plugin, pipeline)
end
for _, op in ipairs(ops) do
local task = self:queue(plugin, op, { on_done = on_done })
if task then
table.insert(tasks, task)
end
end
for _, task in ipairs(tasks) do
task:start()
end
end end
function Runner:is_empty() ---@param plugin LazyPlugin
return #self._tasks == 0 ---@param task_type TaskType
---@param opts? TaskOptions
---@return LazyTask?
function Runner:queue(plugin, task_type, opts)
local def = vim.split(task_type, ".", { plain = true })
assert(#def == 2)
---@type LazyTaskDef
local task_def = require("lazy.manage.task." .. def[1])[def[2]]
assert(task_def)
if not task_def.needed or task_def.needed(plugin, self._opts) then
local task = Task.new(plugin, def[2], task_def.run, opts)
table.insert(self._tasks, task)
return task
end
end
function Runner:start()
for _, plugin in pairs(self._plugins) do
self:_run(plugin, vim.deepcopy(self._opts.pipeline))
end
self._running = true
local check = vim.loop.new_check()
check:start(function()
for _, task in ipairs(self._tasks) do
if task:is_running() then
return
end
end
if #self._waiting > 0 then
for _, cb in ipairs(self._waiting) do
cb()
end
self._waiting = {}
return
end
check:stop()
self._running = false
for _, cb in ipairs(self._on_done) do
vim.schedule(cb)
end
self._on_done = {}
end)
end end
---@return LazyPlugin[] ---@return LazyPlugin[]
@ -33,33 +133,19 @@ function Runner:tasks()
return self._tasks return self._tasks
end end
-- Execute the callback async when done.
-- When no callback is specified, this will wait sync
---@param cb? fun() ---@param cb? fun()
function Runner:wait(cb) function Runner:wait(cb)
if #self._tasks == 0 then if #self._tasks == 0 or not self._running then
return cb and cb() return cb and cb()
end end
local done = false if cb then
local check = vim.loop.new_check() table.insert(self._on_done, cb)
else
check:start(function() -- sync wait
for _, task in ipairs(self._tasks) do while self._running do
if task.running then
return
end
end
check:stop()
done = true
if cb then
vim.schedule(cb)
end
end)
if not cb then
while not done do
vim.wait(100) vim.wait(100)
end end
end end

View File

@ -0,0 +1,114 @@
local Util = require("lazy.util")
---@type table<string, LazyTaskDef>
local M = {}
M.log = {
needed = function(plugin, opts)
if opts.interactive ~= true or not Util.file_exists(plugin.dir .. "/.git") then
return false
end
return plugin.updated == nil or plugin.updated.from ~= plugin.updated.to
end,
run = function(self)
local args = {
"log",
"--pretty=format:%h %s (%cr)",
"--abbrev-commit",
"--decorate",
"--date=short",
"--color=never",
}
if self.plugin.updated then
table.insert(args, self.plugin.updated.from .. ".." .. (self.plugin.updated.to or "HEAD"))
else
table.insert(args, "--since=7 days ago")
end
self:spawn("git", {
args = args,
cwd = self.plugin.dir,
})
end,
}
M.update = {
run = function(self)
if Util.file_exists(self.plugin.uri) then
if vim.loop.fs_realpath(self.plugin.uri) ~= vim.loop.fs_realpath(self.plugin.dir) then
vim.loop.fs_unlink(self.plugin.dir)
vim.loop.fs_symlink(self.plugin.uri, self.plugin.dir, {
dir = true,
})
vim.opt.runtimepath:append(self.plugin.uri)
end
else
local args = {
"pull",
"--tags",
"--recurse-submodules",
"--update-shallow",
"--progress",
}
local git = assert(Util.git_info(self.plugin.dir))
self:spawn("git", {
args = args,
cwd = self.plugin.dir,
on_exit = function(ok)
if ok then
local git_new = assert(Util.git_info(self.plugin.dir))
self.plugin.updated = {
from = git.hash,
to = git_new.hash,
}
self.plugin.dirty = not vim.deep_equal(git, git_new)
end
end,
})
end
end,
}
M.install = {
run = function(self)
if Util.file_exists(self.plugin.uri) then
vim.loop.fs_symlink(self.plugin.uri, self.plugin.dir, {
dir = true,
})
vim.opt.runtimepath:append(self.plugin.uri)
else
local args = {
"clone",
self.plugin.uri,
-- "--depth=1",
"--filter=blob:none",
-- "--filter=tree:0",
"--recurse-submodules",
"--single-branch",
"--shallow-submodules",
"--progress",
}
if self.plugin.branch then
vim.list_extend(args, {
"-b",
self.plugin.branch,
})
end
table.insert(args, self.plugin.dir)
self:spawn("git", {
args = args,
on_exit = function(ok)
if ok then
self.plugin.installed = true
self.plugin.dirty = true
end
end,
})
end
end,
}
return M

View File

@ -1,36 +1,40 @@
local Process = require("lazy.manage.process") local Process = require("lazy.manage.process")
local Loader = require("lazy.core.loader")
local Util = require("lazy.util") ---@class LazyTaskDef
---@field needed? fun(plugin:LazyPlugin, opts:RunnerOpts):any?
---@field run fun(task:LazyTask)
---@alias LazyTaskState fun():boolean?
---@class LazyTask ---@class LazyTask
---@field plugin LazyPlugin ---@field plugin LazyPlugin
---@field type TaskType ---@field type TaskType
---@field running boolean ---@field output string
---@field opts TaskOptions ---@field status string
---@field error? string
---@field private _task fun(task:LazyTask)
---@field private _running LazyPluginState[]
---@field private _started boolean
---@field private _opts TaskOptions
local Task = {} local Task = {}
---@alias TaskType "update"|"install"|"run"|"clean"|"log"|"docs" ---@alias TaskType "update"|"install"|"run"|"clean"|"log"|"docs"
---@class TaskOptions ---@class TaskOptions
---@field on_done? fun(task:LazyTask) ---@field on_done? fun(task:LazyTask)
local options = {
log = {
since = "7 days ago",
---@type string
from = nil,
---@type string
to = nil,
},
}
---@param plugin LazyPlugin ---@param plugin LazyPlugin
---@param type TaskType ---@param type TaskType
---@param opts? TaskOptions ---@param opts? TaskOptions
function Task.new(plugin, type, opts) ---@param task fun(task:LazyTask)
function Task.new(plugin, type, task, opts)
local self = setmetatable({}, { local self = setmetatable({}, {
__index = Task, __index = Task,
}) })
self.opts = vim.tbl_deep_extend("force", {}, options, opts or {}) self._opts = opts or {}
self._running = {}
self._task = task
self._started = false
self.plugin = plugin self.plugin = plugin
self.type = type self.type = type
self.output = "" self.output = ""
@ -40,10 +44,40 @@ function Task.new(plugin, type, opts)
return self return self
end end
function Task:_done() function Task:has_started()
self.running = false return self._started
if self.opts.on_done then end
self.opts.on_done(self)
function Task:is_done()
return self:has_started() and not self:is_running()
end
function Task:is_running()
for _, state in ipairs(self._running) do
if state() then
return true
end
end
return false
end
function Task:start()
self._started = true
---@type boolean, string|any
local ok, err = pcall(self._task, self)
if not ok then
self.error = err or "failed"
end
self:_check()
end
---@private
function Task:_check()
if self:is_running() then
return
end
if self._opts.on_done then
self._opts.on_done(self)
end end
vim.cmd("do User LazyRender") vim.cmd("do User LazyRender")
vim.api.nvim_exec_autocmds("User", { vim.api.nvim_exec_autocmds("User", {
@ -52,99 +86,25 @@ function Task:_done()
}) })
end end
function Task:clean() ---@param fn fun()
local dir = self.plugin.dir:gsub("/+$", "") function Task:schedule(fn)
local stat = vim.loop.fs_lstat(dir) local done = false
table.insert(self._running, function()
if stat.type == "directory" then return not done
Util.walk(dir, function(path, _, type) end)
if type == "directory" then vim.schedule(function()
vim.loop.fs_rmdir(path) ---@type boolean, string|any
else local ok, err = pcall(fn)
vim.loop.fs_unlink(path) if not ok then
end self.error = err or "failed"
end)
vim.loop.fs_rmdir(dir)
else
vim.loop.fs_unlink(dir)
end
self.plugin.installed = false
self:_done()
end
function Task:install()
if Util.file_exists(self.plugin.uri) then
vim.loop.fs_symlink(self.plugin.uri, self.plugin.dir, {
dir = true,
})
vim.opt.runtimepath:append(self.plugin.uri)
self:_done()
else
local args = {
"clone",
self.plugin.uri,
-- "--depth=1",
"--filter=blob:none",
-- "--filter=tree:0",
"--recurse-submodules",
"--single-branch",
"--shallow-submodules",
"--progress",
}
if self.plugin.branch then
vim.list_extend(args, {
"-b",
self.plugin.branch,
})
end end
done = true
table.insert(args, self.plugin.dir) self:_check()
self:spawn("git", { end)
args = args,
on_exit = function(ok)
if ok then
self.plugin.installed = true
self.plugin.dirty = true
end
end,
})
end
end
function Task:run()
Loader.load(self.plugin, { task = "run" }, { load_start = true })
local run = self.plugin.run
if run then
if type(run) == "string" and run:sub(1, 1) == ":" then
local cmd = vim.api.nvim_parse_cmd(run:sub(2), {})
self.output = vim.api.nvim_cmd(cmd, { output = true })
elseif type(run) == "function" then
run()
else
local args = vim.split(run, "%s+")
return self:spawn(table.remove(args, 1), {
args = args,
cwd = self.plugin.dir,
})
end
end
-- FIXME: the spawn above wont be finished yet
self:_done()
end
function Task:docs()
local docs = self.plugin.dir .. "/doc/"
if Util.file_exists(docs) then
self.output = vim.api.nvim_cmd({ cmd = "helptags", args = { docs } }, { output = true })
end
self:_done()
end end
---@param cmd string ---@param cmd string
---@param opts ProcessOpts ---@param opts? ProcessOpts
function Task:spawn(cmd, opts) function Task:spawn(cmd, opts)
opts = opts or {} opts = opts or {}
local on_line = opts.on_line local on_line = opts.on_line
@ -152,116 +112,42 @@ function Task:spawn(cmd, opts)
function opts.on_line(line) function opts.on_line(line)
self.status = line self.status = line
if on_line then if on_line then
pcall(on_line, line) pcall(on_line, line)
end end
vim.cmd("do User LazyRender") vim.cmd("do User LazyRender")
end end
---@param output string
function opts.on_exit(ok, output) function opts.on_exit(ok, output)
self.output = output self.output = self.output .. output
if not ok then if not ok then
self.error = output self.error = self.error and (self.error .. "\n" .. output) or output
end end
if on_exit then if on_exit then
pcall(on_exit, ok, output) pcall(on_exit, ok, output)
end end
self:_check()
self:_done()
end end
local proc = Process.spawn(cmd, opts)
Process.spawn(cmd, opts) table.insert(self._running, function()
end return proc and not proc:is_closing()
function Task:start()
self.running = true
local ok, err = pcall(function()
if self.type == "update" then
self:update()
elseif self.type == "install" then
self:install()
elseif self.type == "run" then
self:run()
elseif self.type == "clean" then
self:clean()
elseif self.type == "log" then
self:log()
elseif self.type == "docs" then
self:docs()
end
end) end)
if not ok then
self.error = err or "failed"
self:_done()
end
end end
function Task:log() ---@param tasks (LazyTask?)[]
if not Util.file_exists(self.plugin.dir .. "/.git") then function Task.all_done(tasks)
self:_done() for _, task in ipairs(tasks) do
return if task and not task:is_done() then
end return false
local args = {
"log",
"--pretty=format:%h %s (%cr)",
"--abbrev-commit",
"--decorate",
"--date=short",
"--color=never",
}
if self.opts.log.from then
table.insert(args, self.opts.log.from .. ".." .. (self.opts.log.to or "HEAD"))
else
table.insert(args, "--since=" .. self.opts.log.since)
end
self:spawn("git", {
args = args,
cwd = self.plugin.dir,
})
end
function Task:update()
if Util.file_exists(self.plugin.uri) then
if vim.loop.fs_realpath(self.plugin.uri) ~= vim.loop.fs_realpath(self.plugin.dir) then
vim.loop.fs_unlink(self.plugin.dir)
vim.loop.fs_symlink(self.plugin.uri, self.plugin.dir, {
dir = true,
})
vim.opt.runtimepath:append(self.plugin.uri)
end end
self:_done() end
else return true
local args = { end
"pull",
"--tags",
"--recurse-submodules",
"--update-shallow",
"--progress",
}
local git = assert(Util.git_info(self.plugin.dir))
self:spawn("git", { function Task:wait()
args = args, while self:is_running() do
cwd = self.plugin.dir, vim.wait(10)
on_exit = function(ok)
if ok then
local git_new = assert(Util.git_info(self.plugin.dir))
self.plugin.updated = {
from = git.hash,
to = git_new.hash,
}
self.plugin.dirty = not vim.deep_equal(git, git_new)
end
end,
})
end end
end end

View File

@ -0,0 +1,66 @@
local Util = require("lazy.util")
local Loader = require("lazy.core.loader")
---@type table<string, LazyTaskDef>
local M = {}
M.run = {
needed = function(plugin)
return plugin.dirty and (plugin.opt == false or plugin.run)
end,
run = function(self)
Loader.load(self.plugin, { task = "run" }, { load_start = true })
local run = self.plugin.run
if run then
if type(run) == "string" and run:sub(1, 1) == ":" then
local cmd = vim.api.nvim_parse_cmd(run:sub(2), {})
self.output = vim.api.nvim_cmd(cmd, { output = true })
elseif type(run) == "function" then
run()
else
local args = vim.split(run, "%s+")
return self:spawn(table.remove(args, 1), {
args = args,
cwd = self.plugin.dir,
})
end
end
end,
}
M.clean = {
run = function(self)
local dir = self.plugin.dir:gsub("/+$", "")
local stat = vim.loop.fs_lstat(dir)
if stat.type == "directory" then
Util.walk(dir, function(path, _, type)
if type == "directory" then
vim.loop.fs_rmdir(path)
else
vim.loop.fs_unlink(path)
end
end)
vim.loop.fs_rmdir(dir)
else
vim.loop.fs_unlink(dir)
end
self.plugin.installed = false
end,
}
M.docs = {
needed = function(plugin)
return plugin.dirty
end,
run = function(self)
local docs = self.plugin.dir .. "/doc/"
if Util.file_exists(docs) then
self.output = vim.api.nvim_cmd({ cmd = "helptags", args = { docs } }, { output = true })
end
end,
}
return M

View File

@ -50,7 +50,7 @@ function M:update()
if plugin.tasks then if plugin.tasks then
for _, task in ipairs(plugin.tasks) do for _, task in ipairs(plugin.tasks) do
self.progress.total = self.progress.total + 1 self.progress.total = self.progress.total + 1
if not task.running then if not task:is_running() then
self.progress.done = self.progress.done + 1 self.progress.done = self.progress.done + 1
end end
end end
@ -215,7 +215,7 @@ function M:diagnostics(plugin)
end end
end end
for _, task in ipairs(plugin.tasks or {}) do for _, task in ipairs(plugin.tasks or {}) do
if task.running then if task:is_running() then
self:diagnostic({ self:diagnostic({
severity = vim.diagnostic.severity.WARN, severity = vim.diagnostic.severity.WARN,
message = task.type .. (task.status == "" and "" or (": " .. task.status)), message = task.type .. (task.status == "" and "" or (": " .. task.status)),

View File

@ -25,7 +25,7 @@ return {
{ {
filter = function(plugin) filter = function(plugin)
return has_task(plugin, function(task) return has_task(plugin, function(task)
return task.running return task:is_running()
end) end)
end, end,
title = "Working", title = "Working",