From a8e711124f8f3999f054966064d5d3bb4056f864 Mon Sep 17 00:00:00 2001 From: phosphene47 Date: Sun, 14 Nov 2021 04:18:54 +1100 Subject: Initial commit --- lua/dep.lua | 406 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 406 insertions(+) create mode 100644 lua/dep.lua (limited to 'lua/dep.lua') 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, +}) -- cgit v1.2.1