diff options
author | phosphene47 <phosphene47@chiya.dev> | 2021-11-14 18:04:59 +1100 |
---|---|---|
committer | phosphene47 <phosphene47@chiya.dev> | 2021-11-14 18:04:59 +1100 |
commit | 78f440dcdd857eb876e459d6bf49510e1d77cdb8 (patch) | |
tree | 353bfa981e22c105e762d8cab65cf25903373c52 /lua | |
parent | a8e711124f8f3999f054966064d5d3bb4056f864 (diff) | |
download | dep-78f440dcdd857eb876e459d6bf49510e1d77cdb8.tar.gz dep-78f440dcdd857eb876e459d6bf49510e1d77cdb8.tar.bz2 dep-78f440dcdd857eb876e459d6bf49510e1d77cdb8.zip |
Fix many bugs and make things actually work
Diffstat (limited to '')
-rw-r--r-- | lua/dep.lua | 575 | ||||
-rw-r--r-- | lua/dep/log.lua | 44 | ||||
-rw-r--r-- | lua/dep/proc.lua | 82 |
3 files changed, 495 insertions, 206 deletions
diff --git a/lua/dep.lua b/lua/dep.lua index 8078b62..30e01d4 100644 --- a/lua/dep.lua +++ b/lua/dep.lua @@ -3,21 +3,44 @@ local proc = require("dep/proc") logger:open() -local base_dir +local initialized, config_path, base_dir local packages, package_roots -local function register(arg) - if type(arg) ~= "table" then - arg = { arg } +local function get_name(id) + local name = id:match("^[%w-_.]+/([%w-_.]+)$") + if name then + return name + else + error(string.format('invalid name "%s"; must be in the format "user/package"', id)) + end +end + +local function link_dependency(parent, child) + if not parent.dependents[child.id] then + parent.dependents[child.id] = child + parent.dependents[#parent.dependents + 1] = child + end + + if not child.dependencies[parent.id] then + child.dependencies[parent.id], child.root = parent, false + child.dependencies[#child.dependencies + 1] = parent + end +end + +local function register(spec, overrides) + overrides = overrides or {} + + if type(spec) ~= "table" then + spec = { spec } end - local id = arg[1] + local id = spec[1] local package = packages[id] if not package then package = { id = id, - enabled = false, + enabled = true, exists = false, added = false, configured = false, @@ -31,51 +54,96 @@ local function register(arg) } packages[id] = package + packages[#packages + 1] = 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.name = spec.as or package.name or get_name(id) + package.url = spec.url or package.url or ("https://github.com/" .. id .. ".git") + package.branch = spec.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 + package.pin = overrides.pin or spec.pin or package.pin + package.enabled = not overrides.disable and not spec.disable 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]) + package.on_setup[#package.on_setup + 1] = spec.setup + package.on_config[#package.on_config + 1] = spec.config + package.on_load[#package.on_load + 1] = spec[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 + if type(spec.requires) == "table" then + for i = 1, #spec.requires do + link_dependency(register(spec.requires[i]), package) + end + elseif spec.requires then + link_dependency(register(spec.requires), package) 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 + if type(spec.deps) == "table" then + for i = 1, #spec.deps do + link_dependency(package, register(spec.deps[i])) + end + elseif spec.deps then + link_dependency(package, register(spec.deps)) end + + return package end -local function register_recursive(list) - for _, arg in ipairs(list) do - register(arg) +local function register_recursive(list, overrides) + overrides = overrides or {} + overrides = { + pin = overrides.pin or list.pin, + disable = overrides.disable or list.disable, + } + + for i = 1, #list do + local ok, err = pcall(register, list[i], overrides) + if not ok then + error(string.format("%s (spec=%s)", err, vim.inspect(list[i]))) + end end - for _, module in ipairs(list.modules or {}) do - if type(module) == "string" then - module = require(module) + if list.modules then + for i = 1, #list.modules do + local name, module = "<unnamed module>", list.modules[i] + + if type(module) == "string" then + name, module = module, require(module) + end + + name = module.name or name + + local ok, err = pcall(register_recursive, module, overrides) + if not ok then + error(string.format("%s <- %s", err, name)) + end end + end +end + +local function sort_dependencies() + local function compare(a, b) + local a_deps = #a.dependencies + local b_deps = #b.dependencies - register_recursive(module) + if a_deps == b_deps then + return a.id < b.id + else + return a_deps < b_deps + end + end + + table.sort(packages, compare) + + for i = 1, #packages do + table.sort(packages[i].dependencies, compare) + table.sort(packages[i].dependents, compare) end end @@ -83,23 +151,25 @@ local function find_cycle() local index = 0 local indexes = {} local lowlink = {} - local set = {} local stack = {} + -- use tarjan algorithm to find circular dependencies (strongly connected components) local function connect(package) - indexes[package.id], lowlink[package.id], set[package.id] = index, index, true + indexes[package.id], lowlink[package.id] = index, index + stack[#stack + 1], stack[package.id] = package, true index = index + 1 - table.insert(stack, package) - for _, dependent in pairs(package.dependents) do - if indexes[dependent.id] == nil then + for i = 1, #package.dependents do + local dependent = package.dependents[i] + + if not indexes[dependent.id] 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 + elseif stack[dependent.id] then lowlink[package.id] = math.min(lowlink[package.id], indexes[dependent.id]) end end @@ -109,20 +179,23 @@ local function find_cycle() local node repeat - node = table.remove(stack) - set[node.id] = nil - table.insert(cycle, node) + node = stack[#stack] + stack[#stack], stack[node.id] = nil, nil + cycle[#cycle + 1] = node until node == package - -- only consider multi-node components - if #cycle > 2 then + -- a node is by definition strongly connected to itself + -- ignore single-node components unless it explicitly specified itself as a dependency + if #cycle > 2 or package.dependents[package.id] then return cycle end end end - for _, package in pairs(packages) do - if indexes[package.id] == nil then + for i = 1, #packages do + local package = packages[i] + + if not indexes[package.id] then local cycle = connect(package) if cycle then return cycle @@ -131,22 +204,41 @@ local function find_cycle() end end +local function ensure_acyclic() + local cycle = find_cycle() + + if cycle then + local names = {} + for i = 1, #cycle do + names[i] = cycle[i].id + end + error("circular dependency detected in package graph: " .. table.concat(names, " -> ")) + end +end + local function find_roots() - for _, package in pairs(packages) do + for i = 1, #packages do + local package = packages[i] if package.root then - table.insert(package_roots, package) + package_roots[#package_roots + 1] = package end end end local function run_hooks(package, type) - for _, cb in ipairs(package["on_" .. type]) do - local ok, err = pcall(cb) + local hooks = package[type] + + for i = 1, #hooks do + local ok, err = pcall(hooks[i]) if not ok then return false, err end end + if #hooks ~= 0 then + logger:log("hook", string.format("ran %d %s for %s", #hooks, #hooks == 1 and "hook" or "hooks", package.id)) + end + return true end @@ -155,6 +247,7 @@ local function ensure_added(package) local ok, err = pcall(vim.cmd, "packadd " .. package.name) if ok then package.added = true + logger:log("vim", string.format("packadd completed for %s", package.id)) else return false, err end @@ -163,13 +256,21 @@ local function ensure_added(package) return true end -local function configure_recursive(package, force) +local function configure_recursive(package) 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") + for i = 1, #package.dependencies do + if not package.dependencies[i].configured then + return + end + end + + local propagate = false + + if not package.configured then + local ok, err = run_hooks(package, "on_setup") if not ok then logger:log("error", string.format("failed to set up %s; reason: %s", package.id, err)) return @@ -181,96 +282,128 @@ local function configure_recursive(package, force) return end - ok, err = run_hooks(package, "config") + ok, err = run_hooks(package, "on_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 + propagate = true + + logger:log("config", string.format("configured %s", package.id)) end - for _, dependent in pairs(package.dependents) do - configure_recursive(dependent, force) + for i = 1, #package.dependents do + local dependent = package.dependents[i] + + dependent.configured = dependent.configured and not propagate + configure_recursive(dependent) end end -local function load_recursive(package, force) +local function load_recursive(package) if not package.exists or not package.enabled then return end - if not package.loaded or force then + for i = 1, #package.dependencies do + if not package.dependencies[i].loaded then + return + end + end + + local propagate = false + + if not package.loaded 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") + ok, err = run_hooks(package, "on_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 + propagate = true + + logger:log("load", string.format("loaded %s", package.id)) end - for _, dependent in pairs(package.dependents) do + for i = 1, #package.dependents do + local dependent = package.dependents[i] + + dependent.loaded = dependent.loaded and not propagate load_recursive(dependent, force) end end local function reload_meta() - vim.cmd([[ - silent! helptags ALL - silent! UpdateRemotePlugins - ]]) + local ok, err = pcall( + vim.cmd, + [[ + silent! helptags ALL + silent! UpdateRemotePlugins + ]] + ) + + if ok then + logger:log("vim", "reloaded helptags and remote plugins") + else + logger:log("error", string.format("failed to reload helptags and remote plugins; reason: %s", err)) + end end local function reload_all() - for _, package in pairs(package_roots) do - configure_recursive(package) + for i = 1, #package_roots do + configure_recursive(package_roots[i]) end - for _, package in pairs(package_roots) do - load_recursive(package) + for i = 1, #package_roots do + load_recursive(package_roots[i]) 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 = {} + vim.loop.fs_scandir( + base_dir, + vim.schedule_wrap(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 + while handle do + local name = vim.loop.fs_scandir_next(handle) + if name then + queue[name] = base_dir .. name + else + break + end end - end - for _, package in pairs(packages) do - queue[package.name] = nil - end + for i = 1, #packages do + queue[packages[i].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)) + for name, dir in pairs(queue) do + -- todo: make this async + local ok = vim.fn.delete(dir, "rf") + if ok then + logger:log("clean", string.format("deleted %s", name)) + else + logger:log("error", string.format("failed to delete %s", name)) + end end end - end - end) + end) + ) end local function sync(package, cb) @@ -283,34 +416,37 @@ local function sync(package, cb) return end - local function cb_err(err) + local function log_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) + proc.git_rev_parse(package.dir, "HEAD", function(err, before) if err then - cb_err(before) + log_err(before) + cb(err) else - proc.git_fetch(package.dir, package.branch or "HEAD", function(err, message) + proc.git_fetch(package.dir, "origin", package.branch or "HEAD", function(err, message) if err then - cb_err(message) + log_err(message) + cb(err) else - proc.git_reset(package.dir, package.branch or "HEAD", function(err, message) + proc.git_rev_parse(package.dir, "FETCH_HEAD", function(err, after) if err then - cb_err(message) + log_err(after) + cb(err) + elseif before == after then + logger:log("skip", string.format("skipped %s", package.id)) + cb(err) else - proc.get_current_commit(package.dir, function(err, after) + proc.git_reset(package.dir, after, function(err, message) if err then - cb_err(after) + log_err(message) 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 + package.added, package.configured = false, false + logger:log("update", string.format("updated %s; %s -> %s", package.id, before, after)) end + + cb(err) end) end end) @@ -326,22 +462,146 @@ local function sync(package, cb) package.exists, package.added, package.configured = true, false, false logger:log("install", string.format("installed %s", package.id)) end + + cb(err) end) end end local function sync_list(list) local progress = 0 + local has_errors = false + + local function done(err) + progress = progress + 1 + has_errors = has_errors or err + + if progress == #list then + clean() + reload_all() - for _, package in ipairs(list) do - sync(package, function(err) - progress = progress + 1 - if progress == #list then - clean() - reload_all() + if has_errors then + logger:log("error", "there were errors during sync; see :messages or :DepLog for more information") end - end) + end + end + + for i = 1, #list do + sync(list[i], done) + end +end + +local function print_list(list) + local window = vim.api.nvim_get_current_win() + local buffer = vim.api.nvim_create_buf(true, true) + local line = 0 + local indent = 0 + + local function print(chunks) + local concat = {} + local column = 0 + + for i = 1, indent do + concat[#concat + 1] = " " + column = column + 2 + end + + if not chunks then + chunks = {} + elseif type(chunks) == "string" then + chunks = { { chunks } } + end + + for i = 1, #chunks do + local chunk = chunks[i] + concat[#concat + 1] = chunk[1] + chunk.offset, column = column, column + #chunk[1] + end + + vim.api.nvim_buf_set_lines(buffer, line, -1, false, { table.concat(concat) }) + + for i = 1, #chunks do + local chunk = chunks[i] + if chunk[2] then + vim.api.nvim_buf_add_highlight(buffer, -1, chunk[2], line, chunk.offset, chunk.offset + #chunk[1]) + end + end + + line = line + 1 + end + + print("Installed packages:") + indent = 1 + + local loaded = {} + + local function dry_load(package) + for i = 1, #package.dependencies do + if not loaded[package.dependencies[i].id] then + return + end + end + + loaded[package.id] = true + + local line = { + { "- ", "Comment" }, + { package.id, "Underlined" }, + } + + if not package.exists then + line[#line + 1] = { " *not installed", "Comment" } + end + + if not package.loaded then + line[#line + 1] = { " *not loaded", "Comment" } + end + + if not package.enabled then + line[#line + 1] = { " *disabled", "Comment" } + end + + if package.pin then + line[#line + 1] = { " *pinned", "Comment" } + end + + print(line) + + for i = 1, #package.dependents do + dry_load(package.dependents[i]) + end end + + for i = 1, #package_roots do + dry_load(package_roots[i]) + end + + indent = 0 + print() + print("Dependency graph:") + + local function walk_graph(package) + indent = indent + 1 + + print({ + { "| ", "Comment" }, + { package.id, "Underlined" }, + }) + + for i = 1, #package.dependents do + walk_graph(package.dependents[i]) + end + + indent = indent - 1 + end + + for i = 1, #package_roots do + walk_graph(package_roots[i]) + end + + vim.api.nvim_buf_set_option(buffer, "bufhidden", "wipe") + vim.api.nvim_buf_set_option(buffer, "modifiable", false) + vim.api.nvim_win_set_buf(window, buffer) end vim.cmd([[ @@ -349,58 +609,77 @@ vim.cmd([[ command! DepList lua require("dep").list() command! DepClean lua require("dep").clean() command! DepLog lua require("dep").open_log() + command! DepConfig lua require("dep").open_config() ]]) +local function wrap_api(name, fn) + return function(...) + if initialized then + local ok, err = pcall(fn, ...) + if not ok then + logger:log("error", err) + end + else + logger:log("error", string.format("cannot call %s; dep is not initialized", name)) + end + end +end + --todo: prevent multiple execution of async routines return setmetatable({ - sync = function() - local targets = {} + sync = wrap_api("dep.sync", function() + sync_list(packages) + end), - for _, package in pairs(packages) do - table.insert(targets, package) - end + list = wrap_api("dep.list", function() + print_list(packages) + end), - sync_list(targets) - end, + clean = wrap_api("dep.clean", clean), - open_log = function() + open_log = wrap_api("dep.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 = {}, {} + end), - 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) + open_config = wrap_api("dep.open_config", function() + vim.cmd("sp " .. config_path) + end), +}, { + __call = function(self, config) + config_path = debug.getinfo(2, "S").source:sub(2) + initialized, err = pcall(function() + base_dir = config.base_dir or (vim.fn.stdpath("data") .. "/site/pack/deps/start/") + packages, package_roots = {}, {} + + register("chiyadev/dep") + register_recursive(config) + sort_dependencies() + ensure_acyclic() + 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 - error("circular dependency detected in package graph: " .. table.concat(names, " -> ")) - end - find_roots() - reload_all() + local targets = {} - local should_sync = function(package) - if config.sync == "new" or config.sync == nil then - return not package.exists - else - return config.sync == "always" + for i = 1, #packages do + local package = packages[i] + if should_sync(package) then + targets[#targets + 1] = package + end end - end - local targets = {} + sync_list(targets) + end) - for _, package in pairs(packages) do - if should_sync(package) then - table.insert(targets, package) - end + if not initialized then + logger:log("error", err) end - - sync_list(targets) end, }) diff --git a/lua/dep/log.lua b/lua/dep/log.lua index 915b214..e61e129 100644 --- a/lua/dep/log.lua +++ b/lua/dep/log.lua @@ -4,6 +4,8 @@ local logger = { } local colors = { + skip = "Constant", + clean = "Boolean", install = "MoreMsg", update = "WarningMsg", delete = "Directory", @@ -32,28 +34,26 @@ function logger:close() 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 +function logger:log(op, message) + local source = debug.getinfo(2, "Sl").short_src + + vim.schedule(function() + if type(message) ~= "string" then + message = vim.inspect(message) + end + + if not self.silent and colors[op] then + vim.api.nvim_echo({ + { "[dep]", "Identifier" }, + { " " }, + { message, colors[op] }, + }, true, {}) + end + + if self.pipe then + self.pipe:write(string.format("[%s] %s: %s\n", os.date(), source, message)) + end + end) end return logger diff --git a/lua/dep/proc.lua b/lua/dep/proc.lua index 38d91fe..376a98c 100644 --- a/lua/dep/proc.lua +++ b/lua/dep/proc.lua @@ -2,73 +2,83 @@ 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, pid, buffer = nil, nil, {} + local stdout = vim.loop.new_pipe() + local stderr = vim.loop.new_pipe() - local handle = vim.loop.spawn( + handle, pid = vim.loop.spawn( process, - { args = args, cwd = cwd, env = env, stdio = { nil, out, out } }, + { args = args, cwd = cwd, env = env, stdio = { nil, stdout, stderr } }, vim.schedule_wrap(function(code) handle:close() local output = table.concat(buffer) + if output:sub(-1) == "\n" then + output = output:sub(1, -2) + end + logger:log( process, - string.format('executed `%s` with args: "%s"\n%s', process, table.concat(args, '", "'), output) + string.format( + 'executed `%s` (code=%s, pid=%s) with args: "%s"\n%s', + process, + code, + pid, + table.concat(args, '", "'), + output + ) ) - cb(code, output) + cb(code ~= 0, output) end) ) - vim.loop.read_start( - out, - vim.schedule_wrap(function(_, data) - if data then - table.insert(buffer, data) - else - out:close() - end - end) - ) + vim.loop.read_start(stdout, function(_, data) + if data then + buffer[#buffer + 1] = data + else + stdout:close() + end + end) + + vim.loop.read_start(stderr, function(_, data) + if data then + buffer[#buffer + 1] = data + else + stderr: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) +function proc.git_rev_parse(dir, arg, cb) + local args = { "rev-parse", "--short", arg } + + proc.exec("git", args, dir, git_env, cb) end function proc.git_clone(dir, url, branch, cb) - local args = { "--depth=1", "--recurse-submodules", "--shallow-submodules", url, dir } + local args = { "clone", "--depth=1", "--recurse-submodules", "--shallow-submodules", url, dir } if branch then - table.insert(args, "--branch=" .. branch) + args[#args + 1] = "--branch=" .. branch end - exec("git", args, nil, git_env, cb) + proc.exec("git", args, nil, git_env, cb) end -function proc.git_fetch(dir, branch, cb) - local args = { "--depth=1", "--recurse-submodules" } +function proc.git_fetch(dir, remote, refspec, cb) + local args = { "fetch", "--depth=1", "--recurse-submodules", remote, refspec } - if branch then - table.insert(args, "origin") - table.insert(args, branch) - end - - exec("git", args, dir, git_env, cb) + proc.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 +function proc.git_reset(dir, treeish, cb) + local args = { "reset", "--hard", "--recurse-submodules", treeish, "--" } - exec("git", args, dir, git_env, cb) + proc.exec("git", args, dir, git_env, cb) end return proc |