Initial commit
This commit is contained in:
406
lua/dep.lua
Normal file
406
lua/dep.lua
Normal file
@ -0,0 +1,406 @@
|
||||
local logger = require("dep/log")
|
||||
local proc = require("dep/proc")
|
||||
|
||||
logger:open()
|
||||
|
||||
local base_dir
|
||||
local packages, package_roots
|
||||
|
||||
local function register(arg)
|
||||
if type(arg) ~= "table" then
|
||||
arg = { arg }
|
||||
end
|
||||
|
||||
local id = arg[1]
|
||||
local package = packages[id]
|
||||
|
||||
if not package then
|
||||
package = {
|
||||
id = id,
|
||||
enabled = false,
|
||||
exists = false,
|
||||
added = false,
|
||||
configured = false,
|
||||
loaded = false,
|
||||
on_setup = {},
|
||||
on_config = {},
|
||||
on_load = {},
|
||||
root = true,
|
||||
dependencies = {}, -- inward edges
|
||||
dependents = {}, -- outward edges
|
||||
}
|
||||
|
||||
packages[id] = package
|
||||
end
|
||||
|
||||
local prev_dir = package.dir -- optimization
|
||||
|
||||
-- meta
|
||||
package.name = arg.as or package.name or id:match("^[%w-_.]+/([%w-_.]+)$")
|
||||
package.url = arg.url or package.url or ("https://github.com/" .. id .. ".git")
|
||||
package.branch = arg.branch or package.branch
|
||||
package.dir = base_dir .. package.name
|
||||
package.pin = arg.pin or package.pin
|
||||
package.enabled = not arg.disabled and package.enabled
|
||||
|
||||
if prev_dir ~= package.dir then
|
||||
package.exists = vim.fn.isdirectory(package.dir) ~= 0
|
||||
package.configured = package.exists
|
||||
end
|
||||
|
||||
table.insert(package.on_setup, arg.setup)
|
||||
table.insert(package.on_config, arg.config)
|
||||
table.insert(package.on_load, arg[2])
|
||||
|
||||
for _, req in ipairs(type(arg.requires) == "table" and arg.requires or { arg.requires }) do
|
||||
local parent, child = register(req), package
|
||||
parent.dependents[child.id] = child
|
||||
child.dependencies[parent.id], child.root = parent, false
|
||||
end
|
||||
|
||||
for _, dep in ipairs(type(arg.deps) == "table" and arg.deps or { arg.deps }) do
|
||||
local parent, child = package, register(dep)
|
||||
parent.dependents[child.id] = child
|
||||
child.dependencies[parent.id], child.root = parent, false
|
||||
end
|
||||
end
|
||||
|
||||
local function register_recursive(list)
|
||||
for _, arg in ipairs(list) do
|
||||
register(arg)
|
||||
end
|
||||
|
||||
for _, module in ipairs(list.modules or {}) do
|
||||
if type(module) == "string" then
|
||||
module = require(module)
|
||||
end
|
||||
|
||||
register_recursive(module)
|
||||
end
|
||||
end
|
||||
|
||||
local function find_cycle()
|
||||
local index = 0
|
||||
local indexes = {}
|
||||
local lowlink = {}
|
||||
local set = {}
|
||||
local stack = {}
|
||||
|
||||
local function connect(package)
|
||||
indexes[package.id], lowlink[package.id], set[package.id] = index, index, true
|
||||
index = index + 1
|
||||
table.insert(stack, package)
|
||||
|
||||
for _, dependent in pairs(package.dependents) do
|
||||
if indexes[dependent.id] == nil then
|
||||
local cycle = connect(dependent)
|
||||
if cycle then
|
||||
return cycle
|
||||
else
|
||||
lowlink[package.id] = math.min(lowlink[package.id], lowlink[dependent.id])
|
||||
end
|
||||
elseif set[dependent.id] then
|
||||
lowlink[package.id] = math.min(lowlink[package.id], indexes[dependent.id])
|
||||
end
|
||||
end
|
||||
|
||||
if lowlink[package.id] == indexes[package.id] then
|
||||
local cycle = { package }
|
||||
local node
|
||||
|
||||
repeat
|
||||
node = table.remove(stack)
|
||||
set[node.id] = nil
|
||||
table.insert(cycle, node)
|
||||
until node == package
|
||||
|
||||
-- only consider multi-node components
|
||||
if #cycle > 2 then
|
||||
return cycle
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
for _, package in pairs(packages) do
|
||||
if indexes[package.id] == nil then
|
||||
local cycle = connect(package)
|
||||
if cycle then
|
||||
return cycle
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function find_roots()
|
||||
for _, package in pairs(packages) do
|
||||
if package.root then
|
||||
table.insert(package_roots, package)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function run_hooks(package, type)
|
||||
for _, cb in ipairs(package["on_" .. type]) do
|
||||
local ok, err = pcall(cb)
|
||||
if not ok then
|
||||
return false, err
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
local function ensure_added(package)
|
||||
if not package.added then
|
||||
local ok, err = pcall(vim.cmd, "packadd " .. package.name)
|
||||
if ok then
|
||||
package.added = true
|
||||
else
|
||||
return false, err
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
local function configure_recursive(package, force)
|
||||
if not package.exists or not package.enabled then
|
||||
return
|
||||
end
|
||||
|
||||
if not package.configured or force then
|
||||
local ok, err = run_hooks(package, "setup")
|
||||
if not ok then
|
||||
logger:log("error", string.format("failed to set up %s; reason: %s", package.id, err))
|
||||
return
|
||||
end
|
||||
|
||||
ok, err = ensure_added(package)
|
||||
if not ok then
|
||||
logger:log("error", string.format("failed to configure %s; reason: %s", package.id, err))
|
||||
return
|
||||
end
|
||||
|
||||
ok, err = run_hooks(package, "config")
|
||||
if not ok then
|
||||
logger:log("error", string.format("failed to configure %s; reason: %s", package.id, err))
|
||||
return
|
||||
end
|
||||
|
||||
package.configured, package.loaded = true, false
|
||||
force = true
|
||||
end
|
||||
|
||||
for _, dependent in pairs(package.dependents) do
|
||||
configure_recursive(dependent, force)
|
||||
end
|
||||
end
|
||||
|
||||
local function load_recursive(package, force)
|
||||
if not package.exists or not package.enabled then
|
||||
return
|
||||
end
|
||||
|
||||
if not package.loaded or force then
|
||||
local ok, err = ensure_added(package)
|
||||
if not ok then
|
||||
logger:log("error", string.format("failed to configure %s; reason: %s", package.id, err))
|
||||
return
|
||||
end
|
||||
|
||||
ok, err = run_hooks(package, "load")
|
||||
if not ok then
|
||||
logger:log("error", string.format("failed to load %s; reason: %s", package.id, err))
|
||||
return
|
||||
end
|
||||
|
||||
package.loaded = true
|
||||
force = true
|
||||
end
|
||||
|
||||
for _, dependent in pairs(package.dependents) do
|
||||
load_recursive(dependent, force)
|
||||
end
|
||||
end
|
||||
|
||||
local function reload_meta()
|
||||
vim.cmd([[
|
||||
silent! helptags ALL
|
||||
silent! UpdateRemotePlugins
|
||||
]])
|
||||
end
|
||||
|
||||
local function reload_all()
|
||||
for _, package in pairs(package_roots) do
|
||||
configure_recursive(package)
|
||||
end
|
||||
|
||||
for _, package in pairs(package_roots) do
|
||||
load_recursive(package)
|
||||
end
|
||||
|
||||
reload_meta()
|
||||
end
|
||||
|
||||
local function clean()
|
||||
vim.loop.fs_scandir(base_dir, function(err, handle)
|
||||
if err then
|
||||
logger:log("error", string.format("failed to clean; reason: %s", err))
|
||||
else
|
||||
local queue = {}
|
||||
|
||||
while handle do
|
||||
local name = vim.loop.fs_scandir_next(handle)
|
||||
if name then
|
||||
queue[name] = base_dir .. name
|
||||
else
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
for _, package in pairs(packages) do
|
||||
queue[package.name] = nil
|
||||
end
|
||||
|
||||
for name, dir in pairs(queue) do
|
||||
-- todo: make this async
|
||||
local ok = vim.fn.delete(dir, "rf")
|
||||
if not ok then
|
||||
logger:log("error", string.format("failed to delete %s", name))
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
local function sync(package, cb)
|
||||
if not package.enabled then
|
||||
return
|
||||
end
|
||||
|
||||
if package.exists then
|
||||
if package.pin then
|
||||
return
|
||||
end
|
||||
|
||||
local function cb_err(err)
|
||||
logger:log("error", string.format("failed to update %s; reason: %s", package.id, err))
|
||||
cb(err)
|
||||
end
|
||||
|
||||
proc.git_current_commit(package.dir, function(err, before)
|
||||
if err then
|
||||
cb_err(before)
|
||||
else
|
||||
proc.git_fetch(package.dir, package.branch or "HEAD", function(err, message)
|
||||
if err then
|
||||
cb_err(message)
|
||||
else
|
||||
proc.git_reset(package.dir, package.branch or "HEAD", function(err, message)
|
||||
if err then
|
||||
cb_err(message)
|
||||
else
|
||||
proc.get_current_commit(package.dir, function(err, after)
|
||||
if err then
|
||||
cb_err(after)
|
||||
else
|
||||
if before == after then
|
||||
logger:log("skip", string.format("skipped %s", package.id))
|
||||
else
|
||||
package.added, package.configured = false, false
|
||||
logger:log("update", string.format("updated %s", package.id))
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
end)
|
||||
end
|
||||
end)
|
||||
end
|
||||
end)
|
||||
else
|
||||
proc.git_clone(package.dir, package.url, package.branch, function(err, message)
|
||||
if err then
|
||||
logger:log("error", string.format("failed to install %s; reason: %s", package.id, message))
|
||||
else
|
||||
package.exists, package.added, package.configured = true, false, false
|
||||
logger:log("install", string.format("installed %s", package.id))
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
local function sync_list(list)
|
||||
local progress = 0
|
||||
|
||||
for _, package in ipairs(list) do
|
||||
sync(package, function(err)
|
||||
progress = progress + 1
|
||||
if progress == #list then
|
||||
clean()
|
||||
reload_all()
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
vim.cmd([[
|
||||
command! DepSync lua require("dep").sync()
|
||||
command! DepList lua require("dep").list()
|
||||
command! DepClean lua require("dep").clean()
|
||||
command! DepLog lua require("dep").open_log()
|
||||
]])
|
||||
|
||||
--todo: prevent multiple execution of async routines
|
||||
return setmetatable({
|
||||
sync = function()
|
||||
local targets = {}
|
||||
|
||||
for _, package in pairs(packages) do
|
||||
table.insert(targets, package)
|
||||
end
|
||||
|
||||
sync_list(targets)
|
||||
end,
|
||||
|
||||
open_log = function()
|
||||
vim.cmd("sp " .. logger.path)
|
||||
end,
|
||||
}, {
|
||||
__call = function(config)
|
||||
base_dir = config.base_dir or (vim.fn.stdpath("data") .. "/site/pack/deps/start/")
|
||||
packages, package_roots = {}, {}
|
||||
|
||||
register_recursive({ "chiyadev/dep", modules = { config } })
|
||||
|
||||
local cycle = find_cycle()
|
||||
if cycle then
|
||||
local names = {}
|
||||
for _, package in ipairs(cycle) do
|
||||
table.insert(names, package.id)
|
||||
end
|
||||
error("circular dependency detected in package graph: " .. table.concat(names, " -> "))
|
||||
end
|
||||
|
||||
find_roots()
|
||||
reload_all()
|
||||
|
||||
local should_sync = function(package)
|
||||
if config.sync == "new" or config.sync == nil then
|
||||
return not package.exists
|
||||
else
|
||||
return config.sync == "always"
|
||||
end
|
||||
end
|
||||
|
||||
local targets = {}
|
||||
|
||||
for _, package in pairs(packages) do
|
||||
if should_sync(package) then
|
||||
table.insert(targets, package)
|
||||
end
|
||||
end
|
||||
|
||||
sync_list(targets)
|
||||
end,
|
||||
})
|
59
lua/dep/log.lua
Normal file
59
lua/dep/log.lua
Normal file
@ -0,0 +1,59 @@
|
||||
local logger = {
|
||||
path = vim.fn.stdpath("cache") .. "/dep.log",
|
||||
silent = false,
|
||||
}
|
||||
|
||||
local colors = {
|
||||
install = "MoreMsg",
|
||||
update = "WarningMsg",
|
||||
delete = "Directory",
|
||||
error = "ErrorMsg",
|
||||
}
|
||||
|
||||
function logger:open(path)
|
||||
self:close()
|
||||
|
||||
self.path = path or self.path
|
||||
self.handle = vim.loop.fs_open(self.path, "w", 0x1A4) -- 0644
|
||||
self.pipe = vim.loop.new_pipe()
|
||||
|
||||
self.pipe:open(self.handle)
|
||||
end
|
||||
|
||||
function logger:close()
|
||||
if self.pipe then
|
||||
self.pipe:close()
|
||||
self.pipe = nil
|
||||
end
|
||||
|
||||
if self.handle then
|
||||
vim.loop.fs_close(self.handle)
|
||||
self.handle = nil
|
||||
end
|
||||
end
|
||||
|
||||
function logger:log(op, message, cb)
|
||||
if not self.silent and colors[op] then
|
||||
vim.api.nvim_echo({
|
||||
{ "[dep]", "Identifier" },
|
||||
{ " " },
|
||||
{ message, colors[op] },
|
||||
}, false, {})
|
||||
end
|
||||
|
||||
if self.pipe then
|
||||
local source = debug.getinfo(2, "Sl").short_src
|
||||
local message = string.format("[%s] %s: %s\n", os.date(), source, message)
|
||||
|
||||
self.pipe:write(
|
||||
message,
|
||||
vim.schedule_wrap(function(err)
|
||||
if cb then
|
||||
cb(err)
|
||||
end
|
||||
end)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
return logger
|
74
lua/dep/proc.lua
Normal file
74
lua/dep/proc.lua
Normal file
@ -0,0 +1,74 @@
|
||||
local logger = require("dep/log")
|
||||
local proc = {}
|
||||
|
||||
function proc.exec(process, args, cwd, env, cb)
|
||||
local out = vim.loop.new_pipe()
|
||||
local buffer = {}
|
||||
|
||||
local handle = vim.loop.spawn(
|
||||
process,
|
||||
{ args = args, cwd = cwd, env = env, stdio = { nil, out, out } },
|
||||
vim.schedule_wrap(function(code)
|
||||
handle:close()
|
||||
|
||||
local output = table.concat(buffer)
|
||||
|
||||
logger:log(
|
||||
process,
|
||||
string.format('executed `%s` with args: "%s"\n%s', process, table.concat(args, '", "'), output)
|
||||
)
|
||||
|
||||
cb(code, output)
|
||||
end)
|
||||
)
|
||||
|
||||
vim.loop.read_start(
|
||||
out,
|
||||
vim.schedule_wrap(function(_, data)
|
||||
if data then
|
||||
table.insert(buffer, data)
|
||||
else
|
||||
out:close()
|
||||
end
|
||||
end)
|
||||
)
|
||||
end
|
||||
|
||||
local git_env = { "GIT_TERMINAL_PROMPT=0" }
|
||||
|
||||
function proc.git_current_commit(dir, cb)
|
||||
exec("git", { "rev-parse", "HEAD" }, dir, git_env, cb)
|
||||
end
|
||||
|
||||
function proc.git_clone(dir, url, branch, cb)
|
||||
local args = { "--depth=1", "--recurse-submodules", "--shallow-submodules", url, dir }
|
||||
|
||||
if branch then
|
||||
table.insert(args, "--branch=" .. branch)
|
||||
end
|
||||
|
||||
exec("git", args, nil, git_env, cb)
|
||||
end
|
||||
|
||||
function proc.git_fetch(dir, branch, cb)
|
||||
local args = { "--depth=1", "--recurse-submodules" }
|
||||
|
||||
if branch then
|
||||
table.insert(args, "origin")
|
||||
table.insert(args, branch)
|
||||
end
|
||||
|
||||
exec("git", args, dir, git_env, cb)
|
||||
end
|
||||
|
||||
function proc.git_reset(dir, branch, cb)
|
||||
local args = { "--hard", "--recurse-submodules" }
|
||||
|
||||
if branch then
|
||||
table.insert("origin/" .. branch)
|
||||
end
|
||||
|
||||
exec("git", args, dir, git_env, cb)
|
||||
end
|
||||
|
||||
return proc
|
2
stylua.toml
Normal file
2
stylua.toml
Normal file
@ -0,0 +1,2 @@
|
||||
indent_type = "Spaces"
|
||||
indent_width = 2
|
Reference in New Issue
Block a user