aboutsummaryrefslogtreecommitdiffstats
path: root/lua
diff options
context:
space:
mode:
authorphosphene47 <phosphene47@chiya.dev>2021-11-14 04:18:54 +1100
committerphosphene47 <phosphene47@chiya.dev>2021-11-14 04:18:54 +1100
commita8e711124f8f3999f054966064d5d3bb4056f864 (patch)
tree1e5081d8136f85a0764f05c87740256b19f29e18 /lua
downloaddep-a8e711124f8f3999f054966064d5d3bb4056f864.tar.gz
dep-a8e711124f8f3999f054966064d5d3bb4056f864.tar.bz2
dep-a8e711124f8f3999f054966064d5d3bb4056f864.zip
Initial commit
Diffstat (limited to '')
-rw-r--r--lua/dep.lua406
-rw-r--r--lua/dep/log.lua59
-rw-r--r--lua/dep/proc.lua74
3 files changed, 539 insertions, 0 deletions
diff --git a/lua/dep.lua b/lua/dep.lua
new file mode 100644
index 0000000..8078b62
--- /dev/null
+++ b/lua/dep.lua
@@ -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,
+})
diff --git a/lua/dep/log.lua b/lua/dep/log.lua
new file mode 100644
index 0000000..915b214
--- /dev/null
+++ b/lua/dep/log.lua
@@ -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
diff --git a/lua/dep/proc.lua b/lua/dep/proc.lua
new file mode 100644
index 0000000..38d91fe
--- /dev/null
+++ b/lua/dep/proc.lua
@@ -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