diff options
author | phosphene47 <phosphene47@chiya.dev> | 2021-11-14 04:18:54 +1100 |
---|---|---|
committer | phosphene47 <phosphene47@chiya.dev> | 2021-11-14 04:18:54 +1100 |
commit | a8e711124f8f3999f054966064d5d3bb4056f864 (patch) | |
tree | 1e5081d8136f85a0764f05c87740256b19f29e18 | |
download | dep-a8e711124f8f3999f054966064d5d3bb4056f864.tar.gz dep-a8e711124f8f3999f054966064d5d3bb4056f864.tar.bz2 dep-a8e711124f8f3999f054966064d5d3bb4056f864.zip |
Initial commit
Diffstat (limited to '')
-rw-r--r-- | lua/dep.lua | 406 | ||||
-rw-r--r-- | lua/dep/log.lua | 59 | ||||
-rw-r--r-- | lua/dep/proc.lua | 74 | ||||
-rw-r--r-- | stylua.toml | 2 |
4 files changed, 541 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 diff --git a/stylua.toml b/stylua.toml new file mode 100644 index 0000000..0435f67 --- /dev/null +++ b/stylua.toml @@ -0,0 +1,2 @@ +indent_type = "Spaces" +indent_width = 2 |