diff --git a/lua/dep.lua b/lua/dep.lua index 46cb1fb..1b63ed5 100644 --- a/lua/dep.lua +++ b/lua/dep.lua @@ -2,15 +2,10 @@ local logger = require('dep.log') local git = require('dep.git') local packager = require('dep.package') ----all functions for convenience ----@type table +-- all functions for convenience local M = {} ----@type boolean -local initialized - ----performance logging ----@type table +-- performance logging local perf = {} --- get execution time of a function @@ -30,13 +25,13 @@ function M.registertree(speclist, overrides) overrides = overrides or {} -- recurse the packages + local over = overrides for _, spec in pairs(speclist) do - -- make sure the overrides override and take into account the packages spec ---@diagnostic disable-next-line: missing-fields - overrides = { - pin = overrides.pin or spec.pin, - disable = overrides.disable or spec.disable + over = { + pin = over.pin or spec.pin, + disable = over.disable or spec.disable } local ok = packager:new(spec, overrides) @@ -48,8 +43,48 @@ function M.registertree(speclist, overrides) end end +--- clean out old packages +function M.clean() + vim.loop.fs_scandir( + packager.get_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] = packager.get_base_dir()..name + else + break + end + end + + -- keep packages that still exist + for _, package in pairs(packager.get_packages()) do + queue[package.name] = nil + end + + for name, dir in pairs(queue) do + local co = coroutine.create(function() + 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) + coroutine.resume(co) + end + end + end) + ) +end + --- reload all packages in package table spec ----@param force boolean|nil force all packages to load +---@param force boolean? force all packages to load function M.reload(force) local reloaded = packager.get_root():loadtree(force) @@ -72,63 +107,6 @@ function M.reload(force) end end ---- check if there's a circular dependency in the package tree -function M.findcycle() - local index = 0 - local indexes = {} - local lowlink = {} - local stack = {} - - -- use tarjan algorithm to find circular dependencies (strongly connected - -- components) - local function connect(package) - indexes[package.id], lowlink[package.id] = index, index - stack[#stack + 1], stack[package.id] = package, true - index = index + 1 - - 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 stack[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 = stack[#stack] - stack[#stack], stack[node.id] = nil, nil - cycle[#cycle + 1] = node - until node == package - - -- 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(packager.get_packages()) do - if not indexes[package.id] then - local cycle = connect(package) - if cycle then - return cycle - end - end - end -end - --- sync a tree of plugins ---@param tree package[] tree of plugins ---@param cb function? callback @@ -141,16 +119,15 @@ function M.synctree(tree, cb) has_errors = has_errors or err if progress == #tree then - -- TODO: implement clean - -- clean() - M.reload() - if has_errors then logger:log("error", "there were errors during sync; see :messages or :DepLog for more information") else logger:log("update", "synchronized %s %s", #tree, #tree == 1 and "package" or "packages") end + M.clean() + M.reload() + if cb then cb() end @@ -158,7 +135,10 @@ function M.synctree(tree, cb) end for _, package in pairs(tree) do - git.sync(package, done) + local co = coroutine.create(function() + git.sync(package, done) + end) + coroutine.resume(co) end end @@ -167,8 +147,8 @@ return function(opts) logger.pipe = logger:setup() --- make comparison for table.sort - ---@param a table package spec a - ---@param b table package spec b + ---@param a package package spec a + ---@param b package package spec b ---@return boolean local function comp(a, b) -- NOTE: this doesn't have to be in any real order, it just has to be @@ -177,7 +157,7 @@ return function(opts) return a.id < b.id end - initialized, err = pcall(function() + local initialized, err = pcall(function() packager.set_base_dir(opts.base_dir or vim.fn.stdpath("data").."/site/pack/deps/opt/") M.benchmark("load", function() -- register all packages @@ -199,7 +179,10 @@ return function(opts) end -- make sure there arent any circular dependencies - M.findcycle() + local ok = packager.findcycle(packager.get_packages()) + if type(ok) == "table" then + logger:log("error", "found a cycle in the package spec here: %s", vim.inspect(ok)) + end end) -- load packages @@ -218,12 +201,13 @@ return function(opts) -- get all package that need syncing local targets = {} - for i, package in pairs(packager.get_packages()) do + for _, package in pairs(packager.get_packages()) do if shouldsync(package) then - targets[i] = package + table.insert(targets, package) end end + -- install all targets M.synctree(targets) end) @@ -231,17 +215,18 @@ return function(opts) logger:log("error", err) end + -- add some user commands vim.api.nvim_create_user_command("DepLog", function() vim.cmd('vsp '..logger.path) vim.opt_local.readonly = true + -- make the log auto update while it's open local w = vim.uv.new_fs_event() local function watch_file(fname) local fullpath = vim.api.nvim_call_function( 'fnamemodify', { fname, ':p' }) w:start(fullpath, {}, vim.schedule_wrap(function(...) vim.api.nvim_command('checktime') - -- Debounce: stop/start. w:stop() watch_file(fname) end)) @@ -258,5 +243,11 @@ return function(opts) M.reload() end, {}) + vim.api.nvim_create_user_command("DepClean", function() + -- clean AND reload to make sure that all old packages are gone + M.clean() + M.reload() + end, {}) + logger:cleanup() end diff --git a/lua/dep/git.lua b/lua/dep/git.lua index fc9736f..23dde0f 100644 --- a/lua/dep/git.lua +++ b/lua/dep/git.lua @@ -1,3 +1,7 @@ +-- TODO: clean this up, it's a mess +-- the nesting of all the proc calls is really annoying, and I need to find a +-- cleaner way to do it + local logger = require('dep.log') local proc = require('dep.proc') @@ -7,31 +11,47 @@ local git = {} ---@param package package package to update/install ---@param cb function callback function git.sync(package, cb) - if package.exists then - git.update(package, cb) - else - git.install(package, cb) + local function sync() + -- update or install + if package.exists then + git.update(package, cb) + else + git.install(package, cb) + end end + + -- handle arbitrary branches here + if package.branch then + proc.git_resolve_branch(package.url, package.branch, function(err, message) + if not err then + package.branch = message + sync() + end + end) + else + sync() + end +end + +--- configure a package +---@param package table package spec +local function configurepkg(package) + package:runhooks("on_config") + + logger:log("config", "package: %s configured", package.id) + package.configured = true end --- install a given package ---@param package package package to install ---@param cb function callback function git.install(package, cb) - local function configurepkg() - package:runhooks("on_config") - - logger:log("config", "package: %s configured", package.id) - package.configured = true - end - if not package.enabled then cb() return end - logger:log("error", "%s: doesn't exist", package.id) proc.git_clone(package.dir, package.url, package.branch, function(err, message) if err then logger:log("error", "failed to install %s; reason: %s", @@ -44,15 +64,15 @@ function git.install(package, cb) else package.exists = true package:unconfiguretree() - configurepkg() logger:log("install", "installed %s", package.id) + configurepkg(package) end end) else package.exists = true package:unconfiguretree() - configurepkg() logger:log("install", "installed %s", package.id) + configurepkg(package) end end @@ -74,15 +94,6 @@ function git.update(package, cb) logger:log("error", "failed to update %s; reason: %s", package.id, err) end - --- configure a package - ---@param pkg table package spec - local function configurepkg(pkg) - package:runhooks("on_config") - - logger:log("config", "package: %s configured", pkg.id) - package.configured = true - end - if package.pin then cb() return @@ -112,8 +123,8 @@ function git.update(package, cb) cb(err) else package:unconfiguretree() - configurepkg(package) logger:log("update", "updated %s; %s -> %s", package.id, before, after) + configurepkg(package) end end) end @@ -124,7 +135,7 @@ function git.update(package, cb) log_err(message) cb(err) else - proc.git_rev_parse(package.dir, "FETCH_HEAD", function(err, after) + proc.git_rev_parse(package.dir, "FETCH_HEAD^{commit}", function(err, after) if err then log_err(after) cb(err) @@ -137,8 +148,8 @@ function git.update(package, cb) log_err(message) else package:unconfiguretree() - configurepkg(package) logger:log("update", "updated %s; %s -> %s", package.id, before, after) + configurepkg(package) end cb(err) diff --git a/lua/dep/log.lua b/lua/dep/log.lua index 3993856..a7a49ab 100644 --- a/lua/dep/log.lua +++ b/lua/dep/log.lua @@ -76,7 +76,7 @@ function logger:log(level, message, ...) -- write to the pipe if it's open if logger.pipe then - logger.pipe:write(string.format("[%s] %s:%s: %s\n", os.date("%Y/%m/%d"), source.short_src:gsub('.*%/', ''), source.currentline, message)) + logger.pipe:write(string.format("[%s] %s:%s: %s\n", os.date("%T"), source.short_src:gsub('.*%/', ''), source.currentline, message)) end end) end diff --git a/lua/dep/package.lua b/lua/dep/package.lua index 4ba5ea4..fe206fe 100644 --- a/lua/dep/package.lua +++ b/lua/dep/package.lua @@ -23,13 +23,13 @@ local logger = require('dep.log') ---@field lazy boolean if the package is lazy loaded in any way ---@field added boolean if the package has been added in vim ---@field configured boolean if the package has been configured +---@field lazied boolean if the packages lazy loading has been set ---@field loaded boolean if a package has been loaded ---@field subtree_loaded boolean is the subtree has been loaded ---@field on_config function[] table of functions to run on config ---@field on_setup function[] table of function to run on setup ---@field on_load function[] table of functions to run on load ----@field lazy_load function[] table of functions to run which will tell the ---- package when to load +---@field lazy_load function[] table of functions to run which will tell the package when to load ---@field requirements package[] this package's requirements ---@field dependents package[] packages that require this package ---@field perf table performance metrics for the package @@ -71,7 +71,7 @@ local function check_spec(spec) local name = spec[1]:match("^[%w-_.]+/([%w-_.]+)$") if not name then - logger:log("spec",'invalid name "%s"; must be in the format "user/package"', spec[1]) + logger:log("spec", 'invalid name "%s"; must be in the format "user/package"', spec[1]) return false end end @@ -153,6 +153,7 @@ local function check_spec(spec) return false end + -- turn an id into a spec if (is == "string") then spec.reqs = { spec.reqs } end @@ -165,6 +166,7 @@ local function check_spec(spec) return false end + -- turn an id into a spec if (is == "string") then spec.deps = { spec.deps } end @@ -212,31 +214,33 @@ function package:new(spec, overrides) local id = spec[1] local o = packages[id] or {} + self.__index = self setmetatable(o, self) -- if package hasn't been registered already, get the inital spec regisitered if not o.id then - o.id = id -- id of the package - o.enabled = true -- whether it's going to be used - o.exists = false -- if the package exists on the filesystem - o.lazy = false -- if the package is lazy loaded in any way - o.added = false -- if the package has been added in vim - o.configured = false -- if the package has been configured - o.loaded = false -- if a package has been loaded + o.id = id + o.enabled = true + o.exists = false + o.lazy = false + o.added = false + o.lazied = false + o.configured = false + o.loaded = false o.subtree_loaded = false - o.on_config = {} -- table of functions to run on config - o.on_setup = {} -- table of function to run on setup - o.on_load = {} -- table of functions to run on load - o.lazy_load = {} -- table of functions to run which will tell the package - -- when to load - o.requirements = {} -- this package's requirements - o.dependents = {} -- packages that require this package + o.on_config = {} + o.on_setup = {} + o.on_load = {} + o.lazy_load = {} + + o.requirements = {} + o.dependents = {} o.perf = {} packages[id] = o end - o.name = spec.as or o.name or id + o.name = spec.as or o.name or id:match("^[%w-_.]+/([%w-_.]+)$") o.url = spec.url or o.url or ("https://github.com/"..id..".git") o.branch = spec.branch or o.branch o.dir = base_dir..o.name @@ -287,26 +291,32 @@ function package:new(spec, overrides) if spec.deps then ---it is the correct type as asserted in check_spec() ---@diagnostic disable-next-line: param-type-mismatch - for _, v in pairs(spec.deps) do - local pkg = package:new(v) - if type(pkg) ~= "table" then + for _, dep in pairs(spec.deps) do + local pkg = package:new(dep) + if not pkg then return false end - o:link_dependency(nil, pkg) + o:link_dependency(o, pkg) -- if the child package is lazy loaded make sure the child package -- is only loaded when the parent package has finished loading - if package.lazy then - table.insert(package.on_load, function() - o:loadtree(true) + if o.lazy then + table.insert(o.on_load, function() + local ok = o:loadtree(true) + if not ok then + logger:log("lazy", + "failed to run loadtree for %s, some packages may not be loaded", + o.id) + end end) + + -- tell the dep that it's gonna be lazy + pkg.lazy = true table.insert(pkg.lazy_load, function(_) end) end end end - self.__index = self - return o end @@ -316,14 +326,23 @@ function package.set_base_dir(_base_dir) base_dir = _base_dir end +--- get the base directory for packages +---@return string base_dir +---@nodiscard +function package.get_base_dir() + return base_dir +end + --- get the root directory ---@return package root +---@nodiscard function package.get_root() return root end --- get the packages in dep ---@return package root +---@nodiscard function package.get_packages() return packages end @@ -376,12 +395,12 @@ function package:ensureadded(force) end end + -- run setup hooks + pkg:runhooks("on_setup") + -- now start loading our plugin local start = os.clock() - -- run setup hooks - self:runhooks("on_setup") - -- trigger the packadd for the plugin ---@diagnostic disable-next-line: param-type-mismatch local ok, err = pcall(vim.cmd, "packadd "..pkg.name) @@ -394,23 +413,24 @@ function package:ensureadded(force) logger:log("vim", "packadd completed for %s", pkg.id) -- set the package to loaded - self.loaded = true - logger:log("load", "loaded %s", self.id) + pkg.loaded = true + logger:log("load", "loaded %s", pkg.id) -- trigger the on_load hooks - ok, err = self:runhooks("on_load") + ok, err = pkg:runhooks("on_load") if not ok then - logger:log("error", "failed to load %s; reason: %s", self.id, err) + logger:log("error", "failed to load %s; reason: %s", pkg.id, err) return end end -- make sure the package is lazy loaded if need be - if not self.added and not self.lazy or force then + if not self.added and not self.loaded and not self.lazy or force then loadpkg(self) elseif not self.added and self.lazy then logger:log("lazy", "registering %d lazy hooks for %s", #self.lazy_load, self.id) + self.lazied = true for _, load_cond in pairs(self.lazy_load) do -- configure the lazy loader for the user local l = require('lazy.utils'):new() @@ -436,26 +456,31 @@ end --- load all packages in package tree ---@param force boolean? force lazy packages to load ---@return boolean boolean if tree was successfully loaded +---@nodiscard function package:loadtree(force) + -- if the package doesn't exist or isn't enabled then don't load it if not self.exists or not self.enabled then - logger:log("load", "package %s doesn't exist or is not enabled", self.id) return false end - if self.subtree_loaded then - logger:log("load", "package %s's subtree is already loaded", self.id) + -- if the subtree is loaded then it's already loaded unless it needs forcing + if not force and self.subtree_loaded then return true end + -- if the package isn't lazy check that it's requirements are loaded if not self.lazy then for _, requirement in pairs(self.requirements) do - if not requirement.loaded then - logger:log("load", "package %s requires %s to be loaded first", self.id, requirement.id) + if not requirement.loaded and not requirement.lazy then + logger:log("error", "failed to load %s; requirement: %s isn't loaded", + self.id, requirement.id) return false end end end + -- if the package isn't loaded and isn't lazy then it should probably be + -- loaded if not self.loaded then local ok, err = self:ensureadded(force) if not ok then @@ -464,8 +489,9 @@ function package:loadtree(force) end end - package.subtree_loaded = true + self.subtree_loaded = true + -- make sure the dependants are loaded for _, dependant in pairs(self.dependents) do self.subtree_loaded = dependant:loadtree(force) and self.subtree_loaded end @@ -490,4 +516,68 @@ function package:unconfiguretree() end end +--- check a list of packages for any cycles +---@param pkgs package[] list of packages +---@return package[]|false cycle the cycle that was found or false if not found +---@nodisacard +function package.findcycle(pkgs) + local index = 0 + local indexes = {} + local lowlink = {} + local stack = {} + + --- use tarjan algorithm to find circular dependencies (strongly connected + --- components) + ---@param pkg package + local function connect(pkg) + indexes[pkg.id], lowlink[pkg.id] = index, index + stack[#stack + 1], stack[pkg.id] = pkg, true + index = index + 1 + + for i = 1, #pkg.dependents do + local dependent = pkg.dependents[i] + + if not indexes[dependent.id] then + local cycle = connect(dependent) + if cycle then + return cycle + else + lowlink[pkg.id] = math.min(lowlink[pkg.id], lowlink[dependent.id]) + end + elseif stack[dependent.id] then + lowlink[pkg.id] = math.min(lowlink[pkg.id], indexes[dependent.id]) + end + end + + if lowlink[pkg.id] == indexes[pkg.id] then + local cycle = { pkg } + local node + + repeat + node = stack[#stack] + stack[#stack], stack[node.id] = nil, nil + cycle[#cycle + 1] = node + until node == pkg + + -- 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 pkg.dependents[pkg.id] then + return cycle + end + end + end + + -- actually check the cycle + for _, pkg in pairs(pkgs) do + if not indexes[package.id] then + local cycle = connect(pkg) + if cycle then + return cycle + end + end + end + + return false +end + return package diff --git a/lua/dep/proc.lua b/lua/dep/proc.lua index 85c1858..a23f4cd 100644 --- a/lua/dep/proc.lua +++ b/lua/dep/proc.lua @@ -1,12 +1,18 @@ local proc = {} +--- execute a process +---@param process string the program +---@param args string[] the args +---@param cwd string? the pwd +---@param env table env +---@param cb function callback function proc.exec(process, args, cwd, env, cb) local buffer = {} local function cb_output(_, data, _) table.insert(buffer, table.concat(data)) end - local function cb_exit(job_id, exit_code, _) + local function cb_exit(_, exit_code, _) local output = table.concat(buffer) cb(exit_code ~= 0, output) end @@ -62,54 +68,56 @@ function proc.git_checkout(dir, branch, commit, cb) end function proc.git_resolve_branch(url, branch, cb) - if string.match(branch or "", "*") ~= "*" then + -- if the branch doesn't contain a * then return the branch + if not string.match(branch, "*") then cb(false, branch) return end - local buffer = {} + local buffer = {} local function cb_output(_, data, _) if data[1] ~= "" then buffer = data end end - vim.fn.jobstart({ "git", "ls-remote", "--tags", "--sort", "v:refname", url, }, + vim.fn.jobstart({ "git", "ls-remote", "--tags", "--sort", "v:refname", url }, { cwd = nil, - env = { GIT_TERMINAL_PROMPT = 0 }, + env = git_env, stdin = nil, on_stdout = cb_output, on_stderr = cb_output, on_exit = function(_, exit_code, _) - if exit_code == 0 then - -- get a list of all versions - local versions = {} - for _, v in pairs(buffer) do - local s, e = string.find(v, "refs/tags/.+") - if not s or not e then - goto continue - end + if exit_code ~= 0 then + return + end - local tag = string.sub(v, s, e) - tag = string.gsub(tag, "refs/tags/", "") - tag = string.gsub(tag, "%^{}", "") - - table.insert(versions, tag) - ::continue:: + -- get a list of all versions + local versions = {} + for _, v in pairs(buffer) do + local s, e = string.find(v, "refs/tags/.+") + if not s or not e then + goto continue end - -- match the chosen version against all versions - for i = #versions, 1, -1 do - if branch == "*" then - cb(false, versions[i]) + local tag = string.sub(v, s, e) + tag = tag:gsub("refs/tags/", ""):gsub("%^{}", "") + + table.insert(versions, tag) + ::continue:: + end + + -- match the chosen version against all versions + for i = #versions, 1, -1 do + if branch == "*" then + cb(false, versions[i]) + return + else + local r = string.match(versions[i], branch) + if r then + cb(false, r) return - else - local r = string.match(versions[i], branch) - if r then - cb(false, r) - return - end end end end diff --git a/lua/lazy/utils.lua b/lua/lazy/utils.lua index 4c4841b..8dc960e 100644 --- a/lua/lazy/utils.lua +++ b/lua/lazy/utils.lua @@ -37,7 +37,7 @@ end ---@param opts vim.api.keyset.user_command? options function lazy:cmd(name, opts) opts = opts or {} - vim.api.nvim_create_user_command(name, function(o) + vim.api.nvim_create_user_command(name, opts['callback'] or function() self:cleanup() end, opts) @@ -49,9 +49,8 @@ end ---@param opts vim.api.keyset.create_autocmd? options function lazy:auto(event, opts) opts = opts or {} - opts['once'] = true - - opts['callback'] = function() + opts['once'] = opts['once'] or true + opts['callback'] = opts['callback'] or function() self:cleanup() end @@ -64,7 +63,7 @@ end ---@param opts vim.keymap.set.Opts? options function lazy:keymap(mode, bind, opts) opts = opts or {} - vim.keymap.set(mode, bind, function() + vim.keymap.set(mode, bind, opts['callback'] or function() self:cleanup() -- register keymap unload