From 8bcc8bc0b1925d7819d2a8fbf98f8a84ea5e4fe7 Mon Sep 17 00:00:00 2001 From: Squibid Date: Tue, 22 Apr 2025 17:31:35 -0500 Subject: [PATCH] try to clean it up (not working yet) --- lua/dep.lua | 718 +++----------------------------------------- lua/dep/git.lua | 155 ++++++++++ lua/dep/package.lua | 489 ++++++++++++++++++++++++++++++ lua/lazy/utils.lua | 1 - 4 files changed, 681 insertions(+), 682 deletions(-) create mode 100644 lua/dep/git.lua create mode 100644 lua/dep/package.lua diff --git a/lua/dep.lua b/lua/dep.lua index 58dcc90..adfde51 100644 --- a/lua/dep.lua +++ b/lua/dep.lua @@ -1,5 +1,6 @@ local logger = require('dep.log') -local proc = require('dep.proc') +local git = require('dep.git') +local packager = require('dep.package') ---all functions for convenience ---@type table @@ -8,22 +9,10 @@ local M = {} ---@type boolean local initialized ----root package ----@type table -local root - ---performance logging ---@type table local perf = {} ----table of every package where their id is the index ----@type table -local packages = {} - ----path to root of where plugins are downloaded ----@type string -local base_dir - --- get execution time of a function ---@param name string name of performance output ---@param code function function to run @@ -34,305 +23,35 @@ function M.benchmark(name, code, ...) perf[name] = os.clock() - start end ---- get name of package ----@param id string id of the package ----@return string id ----@nodiscard -function M.getpkgname(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 - ---- tell the parent it has a child and the child it has a parent ----@param parent table parent package ----@param child table child package -function M.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] = parent - child.dependencies[#child.dependencies + 1] = parent - end -end - ---- register a new package according to the spec ----@param spec table|string the package spec to register ----@param overrides table|nil a package spec that is used to override options ----@return table package a table containing the package spec ----@nodiscard -function M.registerpkg(spec, overrides) - overrides = overrides or {} - - if type(spec) ~= "table" then - spec = { spec } - end - - local id = spec[1] - local package = packages[id] - - -- if package hasn't been registered already, get the inital spec regisitered - if not package then - package = { - id = id, -- id of the package - enabled = true, -- whether it's going to be used - exists = false, -- if the package exists on the filesystem - lazy = false, -- if the package is lazy loaded in any way - added = false, -- if the package has been added in vim - configured = false, -- if the package has been configured - loaded = false, -- if a package has been loaded - subtree_loaded = false, - on_config = {}, -- table of functions to run on config - on_setup = {}, -- table of function to run on setup - on_load = {}, -- table of functions to run on load - lazy_load = {}, -- table of functions to run which will tell the package - -- when to load - dependencies = {}, -- this package's requirements - dependents = {}, -- packages that require this package - perf = {} - } - - packages[id] = package - end - - -- register the rest of the package spec - package.name = spec.as or package.name or M.getpkgname(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.commit = spec.commit - package.pin = overrides.pin or spec.pin or package.pin - package.enabled = not overrides.disable and not spec.disable and package.enabled - package.lazy = spec.lazy or package.lazy - - -- make sure that the package exists - package.exists = vim.fn.isdirectory(package.dir) ~= 0 - package.configured = package.exists - - -- register all the callback functions - table.insert(package.on_config, spec.config) - table.insert(package.on_load, spec.load) - table.insert(package.lazy_load, spec.lazy) - - -- if the current package isn't the root package then it depends on the root - -- package - if root and package ~= root then - M.link_dependency(root, package) - end - - -- link the dependencies - if spec.requires then - if type(spec.requires) == "string" then - spec.requires = { spec.requires } - end - for _, v in pairs(spec.requires) do - M.link_dependency(M.registerpkg(v), package) - end - end - - -- and link the dependents - if spec.deps then - if type(spec.deps) == "string" then - spec.deps = { spec.deps } - end - for _, v in pairs(spec.deps) do - local p = M.registerpkg(v) - M.link_dependency(package, p) - - -- 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() - M.loadtree(package, true) - end) - table.insert(p.lazy_load, function(_) end) - end - end - end - - return package -end - --- recurse over all packages and register them ----@param speclist table table of specs ----@param overrides table|nil a package spec that is used to override options +---@param speclist spec[] table of specs +---@param overrides spec? a package spec that is used to override options function M.registertree(speclist, overrides) overrides = overrides or {} - -- make sure the overrides override and take into account the packages spec - overrides = { - pin = overrides.pin or speclist.pin, - disable = overrides.disable or speclist.disable - } - -- recurse the packages - for i = 1, #speclist do - local ok, err = pcall(M.registerpkg, speclist[i], 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 + } + + local ok = packager:new(spec, overrides) -- if erroring print out the spec and the error if not ok then - error(string.format("%s (spec=%s)", err, vim.inspect(speclist[i]))) + error(string.format("%s (spec=%s)", err, vim.inspect(spec))) end end end ---- run specified hooks on specified package ----@param package table package spec ----@param type string which hook to run ----@return boolean, string|nil -function M.runhooks(package, type) - local hooks = package[type] - if #hooks == 0 then - return true - end - - local start = os.clock() - - -- chdir into the package directory to make running external commands - -- from hooks easier. - local last_cwd = vim.fn.getcwd() - vim.fn.chdir(package.dir) - - for i = 1, #hooks do - local ok, err = pcall(hooks[i]) - if not ok then - vim.fn.chdir(last_cwd) - - package.error = true - return false, err - end - end - - vim.fn.chdir(last_cwd) - package.perf[type] = os.clock() - start - - logger:log("hook", "triggered %d %s %s for %s", #hooks, type, - #hooks == 1 and "hook" or "hooks", package.id) - - return true -end - ---- make sure a package has been loaded ----@param package table package ----@param force boolean? force lazy packages to load ----@return boolean|table return true or false if loaded or package spec if lazy loaded -function M.ensureadded(package, force) - -- print("adding ~ "..package.id) - local function loadpkg(pkg) - -- make sure to load the dependencies first - for _, p in pairs(pkg.dependencies) do - if not p.loaded then - M.ensureadded(p, true) - end - end - - -- now start loading our plugin - local start = os.clock() - - -- trigger the packadd for the plugin - local ok, err = pcall(vim.cmd, "packadd " .. pkg.name) - if not ok then - pkg.error = true - return false, err - end - - pkg.added = true - pkg.perf.pack = os.clock() - start - logger:log("vim", "packadd completed for %s", pkg.id) - - -- set the package to loaded - package.loaded = true - logger:log("load", "loaded %s", package.id) - - -- trigger the on_load hooks - ok, err = M.runhooks(package, "on_load") - if not ok then - logger:log("error", "failed to load %s; reason: %s", package.id, err) - return - end - end - - if not package.added and not package.lazy or force then - loadpkg(package) - elseif not package.added and package.lazy then - logger:log("lazy", "registering %d lazy hooks for %s", #package.lazy_load, - package.id) - for _, load_cond in pairs(package.lazy_load) do - -- configure the lazy loader for the user - local l = require('lazy.utils'):new() - if l == true then - logger:log("lazy", "failed to get lazy utils") - return false - end - l:set_load(function() - logger:log("lazy", "triggered %d lazy hooks for %s", #package.lazy_load, - package.id) - loadpkg(package) - end) - - -- run it - load_cond(l) - end - return package - end - - return true -end - ---- load all packages in package tree ----@param package table package spec table ----@param force boolean? force lazy packages to load ----@return boolean boolean if tree was successfully loaded -function M.loadtree(package, force) - if not package.exists or not package.enabled or package.error then - return false - end - - if package.subtree_loaded then - return true - end - - if not package.lazy then - for i = 1, #package.dependencies do - if not package.dependencies[i].loaded then - return false - end - end - end - - if not package.loaded then - local ok, err = M.ensureadded(package, force) - if not ok then - logger:log("error", "failed to load %s; reason: %s", package.id, err) - return false - end - end - - package.subtree_loaded = true - - for i = 1, #package.dependents do - package.subtree_loaded = M.loadtree(package.dependents[i], force) and package.subtree_loaded - end - - return package.subtree_loaded -end - --- reload all packages in package table spec ---@param force boolean|nil force all packages to load function M.reload(force) - -- cleanup any previous errors in the package table - for i in pairs(packages) do - packages[i].error = false - end - - local reloaded = M.loadtree(root, force) + local reloaded = packager.get_root():loadtree(force) if reloaded then local ok, err @@ -400,9 +119,7 @@ function M.findcycle() end end - for i = 1, #packages do - local package = packages[i] - + for _, package in pairs(packager.get_packages()) do if not indexes[package.id] then local cycle = connect(package) if cycle then @@ -412,147 +129,8 @@ function M.findcycle() end end ---- unconfigure a packages tree ----@param package table package to unconfigure -function M.unconfiguretree(package) - -- unconfigure dependencies - for i = 1, #package.dependencies do - package.dependencies[i].subtree_loaded = false - end - - -- unconfigure dependents - for i = 1, #package.dependents do - package.dependents[i].loaded = false - package.dependents[i].added = false - package.dependents[i].configured = false - - package.dependents[i].subtree_loaded = false - end -end - ---- update/download a package ----@param package table package spec ----@param cb function callback -function M.sync(package, cb) - if not package.enabled then - cb() - return - end - - local function log_err(err) - 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) - M.runhooks(package, "on_config") - - logger:log("config", "package: %s configured", pkg.id) - package.configured = true - end - - if package.exists then - if package.pin then - cb() - return - end - - local function logerr(err) - logger:log("error", "failed to update %s; reason: %s", package.id, err) - end - - proc.git_rev_parse(package.dir, "HEAD", function(err, before) - if err then - logerr(before) - cb(err) - else - if package.commit then - proc.git_checkout(package.dir, package.branch, package.commit, function(err, message) - if err then - log_err(message) - cb(err) - else - proc.git_rev_parse(package.dir, package.commit, function(err, after) - if err then - log_err(after) - cb(err) - elseif before == after then - logger:log("skip", "skipped %s", package.id) - cb(err) - else - M.unconfiguretree(package) - configurepkg(package) - logger:log("update", "updated %s; %s -> %s", package.id, before, after) - end - end) - end - end) - else - proc.git_fetch(package.dir, "origin", package.branch or "HEAD", function(err, message) - if err then - log_err(message) - cb(err) - else - proc.git_rev_parse(package.dir, "FETCH_HEAD", function(err, after) - if err then - log_err(after) - cb(err) - elseif before == after then - logger:log("skip", "skipped %s", package.id) - cb(err) - else - proc.git_reset(package.dir, after, function(err, message) - if err then - log_err(message) - else - M.unconfiguretree(package) - configurepkg(package) - logger:log("update", "updated %s; %s -> %s", package.id, before, after) - end - - cb(err) - end) - end - end) - end - end) - end - end - end) - else - 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", - package.id, message) - else - if package.commit then - proc.git_checkout(package.dir, package.branch, package.commit, function(err, message) - if err then - logger:log("error", "failed to checkout %s; reason: %s", package.id, message) - else - package.exists = true - M.unconfiguretree(package) - configurepkg(package) - logger:log("install", "installed %s", package.id) - end - end) - else - package.exists = true - M.unconfiguretree(package) - configurepkg(package) - logger:log("install", "installed %s", package.id) - end - end - - cb(err) - end) - end -end - --- sync a tree of plugins ----@param tree table tree of plugins +---@param tree package[] tree of plugins ---@param cb function? callback function M.synctree(tree, cb) local progress = 0 @@ -579,234 +157,11 @@ function M.synctree(tree, cb) end end - for i in pairs(tree) do - M.sync(tree[i], done) + for _, package in pairs(tree) do + git.sync(package, done) end end - -local function print_list(cb) - local function get_commits(cb) - local results = {} - local done = 0 - for i = 1, #packages do - local package = packages[i] - - if package.exists then - proc.git_rev_parse(package.dir, "HEAD", function(err, commit) - if not err then - results[package.id] = commit - end - - done = done + 1 - if done == #packages then - cb(results) - end - end) - else - done = done + 1 - end - end - end - - get_commits(function(commits) - local buffer = vim.api.nvim_create_buf(true, true) - local line, indent = 0, 0 - - local function print(chunks) - local concat = {} - local column = 0 - - for _ = 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(string.format("Installed packages (%s):", #packages)) - indent = 1 - - local loaded = {} - - local function dry_load(package) - if loaded[package.id] then - return - end - - for i = 1, #package.dependencies do - if not loaded[package.dependencies[i].id] then - return - end - end - - loaded[package.id], loaded[#loaded + 1] = true, package - - local chunk = { - { string.format("[%s] ", commits[package.id] or " "), "Comment" }, - { package.id, "Underlined" }, - } - - if not package.exists then - chunk[#chunk + 1] = { " *not installed", "Comment" } - end - - if not package.loaded then - chunk[#chunk + 1] = { " *not loaded", "Comment" } - end - - if not package.enabled then - chunk[#chunk + 1] = { " *disabled", "Comment" } - end - - if package.pin then - chunk[#chunk + 1] = { " *pinned", "Comment" } - end - - print(chunk) - - for i = 1, #package.dependents do - dry_load(package.dependents[i]) - end - end - - dry_load(root) - indent = 0 - - print() - print("Load time (μs):") - indent = 1 - local profiles = {} - - for i = 1, #packages do - local package = packages[i] - local profile = { - package = package, - total = 0, - setup = package.perf.on_setup or 0, - load = package.perf.on_load or 0, - pack = package.perf.pack or 0, - - "total", - "setup", - "pack", - "load", - } - - if package == root then - for k, v in pairs(perf) do - if profile[k] then - profile[k] = profile[k] + v - end - end - end - - for j = 1, #profile do - profile.total = profile.total + profile[profile[j]] - end - - profiles[#profiles + 1] = profile - end - - table.sort(profiles, function(a, b) - return a.total > b.total - end) - - for i = 1, #profiles do - local profile = profiles[i] - local chunk = { - { "- ", "Comment" }, - { profile.package.id, "Underlined" }, - { string.rep(" ", 40 - #profile.package.id) }, - } - - for j = 1, #profile do - local key, value = profile[j], profile[profile[j]] - chunk[#chunk + 1] = { string.format(" %5s ", key), "Comment" } - chunk[#chunk + 1] = { string.format("%4d", value * 1000000) } - end - - print(chunk) - end - - indent = 0 - print() - print("Dependency graph:") - - local function walk_graph(package) - local chunk = { - { "| ", "Comment" }, - { package.id, "Underlined" }, - } - - local function add_edges(p) - for i = 1, #p.dependencies do - local dependency = p.dependencies[i] - - if dependency ~= root and not chunk[dependency.id] then -- don't convolute the list - chunk[#chunk + 1] = { " " .. dependency.id, "Comment" } - chunk[dependency.id] = true - add_edges(dependency) - end - end - end - - add_edges(package) - print(chunk) - - for i = 1, #package.dependents do - indent = indent + 1 - walk_graph(package.dependents[i]) - indent = indent - 1 - end - end - - walk_graph(root) - - -- print() - -- print("Debug information:") - - -- local debug = {} - -- for l in vim.inspect(packages):gmatch("[^\n]+") do - -- debug[#debug + 1] = l - -- end - - -- vim.api.nvim_buf_set_lines(buffer, line, -1, false, debug) - -- vim.api.nvim_buf_set_name(buffer, "packages.dep") - -- vim.api.nvim_buf_set_option(buffer, "bufhidden", "wipe") - -- vim.api.nvim_buf_set_option(buffer, "modifiable", false) - - vim.cmd("vsp") - vim.api.nvim_win_set_buf(0, buffer) - - if cb then - cb() - end - end) -end - -- basically the main function of our program return function(opts) logger.pipe = logger:setup() @@ -823,19 +178,24 @@ return function(opts) end initialized, err = pcall(function() - base_dir = opts.base_dir or vim.fn.stdpath("data").."/site/pack/deps/opt/" + packager.set_base_dir(opts.base_dir or vim.fn.stdpath("data").."/site/pack/deps/opt/") M.benchmark("load", function() -- register all packages - root = M.registerpkg("squibid/dep") + local root = packager:new({ + "squibid/dep", + url = "https://git.squi.bid/dep", + pin = true + }) + if not root then + logger:log("error", "couldn't register root package") + return + end M.registertree(opts) - -- sort packages - table.sort(packages, comp) - -- sort package dependencies - for i = 1, #packages do - table.sort(packages[i].dependencies, comp) - table.sort(packages[i].dependents, comp) + for _, package in pairs(root.get_packages()) do + table.sort(package.requirements, comp) + table.sort(package.dependents, comp) end -- make sure there arent any circular dependencies @@ -858,9 +218,9 @@ return function(opts) -- get all package that need syncing local targets = {} - for i in pairs(packages) do - if shouldsync(packages[i]) then - targets[i] = packages[i] + for i, package in pairs(packager.get_packages()) do + if shouldsync(package) then + targets[i] = package end end @@ -891,16 +251,12 @@ return function(opts) end, {}) vim.api.nvim_create_user_command("DepSync", function() - M.synctree(packages) + M.synctree(packager.get_packages()) end, {}) vim.api.nvim_create_user_command("DepReload", function() M.reload() end, {}) - vim.api.nvim_create_user_command("DepList", function() - print_list() - end, {}) - logger:cleanup() end diff --git a/lua/dep/git.lua b/lua/dep/git.lua new file mode 100644 index 0000000..fc9736f --- /dev/null +++ b/lua/dep/git.lua @@ -0,0 +1,155 @@ +local logger = require('dep.log') +local proc = require('dep.proc') + +local git = {} + +--- install or update a given package +---@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) + end +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", + package.id, message) + else + if package.commit then + proc.git_checkout(package.dir, package.branch, package.commit, function(err, message) + if err then + logger:log("error", "failed to checkout %s; reason: %s", package.id, message) + else + package.exists = true + package:unconfiguretree() + configurepkg() + logger:log("install", "installed %s", package.id) + end + end) + else + package.exists = true + package:unconfiguretree() + configurepkg() + logger:log("install", "installed %s", package.id) + end + end + + cb(err) + end) + +end + +--- update a package +---@param package package package to update +---@param cb function callback +function git.update(package, cb) + if not package.enabled then + cb() + return + end + + local function log_err(err) + 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 + end + + local function logerr(err) + logger:log("error", "failed to update %s; reason: %s", package.id, err) + end + + proc.git_rev_parse(package.dir, "HEAD", function(err, before) + if err then + logerr(before) + cb(err) + else + if package.commit then + proc.git_checkout(package.dir, package.branch, package.commit, function(err, message) + if err then + log_err(message) + cb(err) + else + proc.git_rev_parse(package.dir, package.commit, function(err, after) + if err then + log_err(after) + cb(err) + elseif before == after then + logger:log("skip", "skipped %s", package.id) + cb(err) + else + package:unconfiguretree() + configurepkg(package) + logger:log("update", "updated %s; %s -> %s", package.id, before, after) + end + end) + end + end) + else + proc.git_fetch(package.dir, "origin", package.branch or "HEAD", function(err, message) + if err then + log_err(message) + cb(err) + else + proc.git_rev_parse(package.dir, "FETCH_HEAD", function(err, after) + if err then + log_err(after) + cb(err) + elseif before == after then + logger:log("skip", "skipped %s", package.id) + cb(err) + else + proc.git_reset(package.dir, after, function(err, message) + if err then + log_err(message) + else + package:unconfiguretree() + configurepkg(package) + logger:log("update", "updated %s; %s -> %s", package.id, before, after) + end + + cb(err) + end) + end + end) + end + end) + end + end + end) +end + +return git diff --git a/lua/dep/package.lua b/lua/dep/package.lua new file mode 100644 index 0000000..ff587d1 --- /dev/null +++ b/lua/dep/package.lua @@ -0,0 +1,489 @@ +local logger = require('dep.log') + +---@class spec +---@field [1] string id +---@field setup function? code to run before the package is loaded +---@field load function? code to run after the package is loaded +---@field config function? code to run after the package is installed/updated +---@field lazy function? code to run which determines when the package is loaded +---@field as string? overrides the short name of the package which is usually set +--- to a substring of all the chars after '/' in spec[1] +---@field url string? the url to the git repository hosting the package +---@field branch string? the branch which the version of the package resides +---@field commit string? the commit which the version of the package resides +---@field disable boolean? if true disables the package from being loaded +---@field pin boolean? if true disables the package from being installed/updated +---@field reqs spec|spec[]|string? packages that this package requires +---@field deps spec|spec[]|string? packages that depend on this package + +---@class package +---@field id string id of the package +---@field enabled boolean whether it's going to be used +---@field exists boolean if the package exists on the filesystem +---@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 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 requirements package[] this package's requirements +---@field dependents package[] packages that require this package +---@field perf table performance metrics for the package +---@field name string the name of the package +---@field url string the url of the package +---@field branch string the branch of the package +---@field dir string the directory of the package +---@field commit string the commit of the package +---@field pin boolean whether to pin the package or not +local package = {} + +--- the base directory for the packages +---@type string +local base_dir + +--- the root package +---@type package +local root + +--- list of all package in dep +---@type package[] +local packages = {} + +--- check a spec to see if it's correct +---@param spec spec|string the specification to check +---@return spec|false spec if the spec is ok or false +local function check_spec(spec) + -- make sure spec is a table + if type(spec) == "string" then + spec = { spec } + end + + -- make sure all the data is correct + do -- spec[1] + if type(spec[1]) ~= "string" then + logger:log("spec", "spec[1] must be a string") + return false + end + + 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]) + return false + end + end + + if spec.setup ~= nil then -- spec.setup + if type(spec.setup) ~= "function" then + logger:log("spec", "spec.setup must be a function in %s", spec[1]) + return false + end + end + + if spec.load ~= nil then -- spec.load + if type(spec.load) ~= "function" then + logger:log("spec", "spec.load must be a function in %s", spec[1]) + return false + end + end + + if spec.config ~= nil then -- spec.config + if type(spec.config) ~= "function" then + logger:log("spec", "spec.config must be a function in %s", spec[1]) + return false + end + end + + if spec.lazy ~= nil then -- spec.lazy + if type(spec.lazy) ~= "function" then + logger:log("spec", "spec.lazy must be a function in %s", spec[1]) + return false + end + end + + if spec.as ~= nil then -- spec.as + if type(spec.as) ~= "string" then + logger:log("spec", "spec.as must be a string in %s", spec[1]) + return false + end + end + + if spec.url ~= nil then -- spec.url + if type(spec.url) ~= "string" then + logger:log("spec", "spec.url must be a string in %s", spec[1]) + return false + end + end + + if spec.branch ~= nil then -- spec.branch + if type(spec.branch) ~= "string" then + logger:log("spec", "spec.branch must be a string in %s", spec[1]) + return false + end + end + + if spec.commit ~= nil then -- spec.commit + if type(spec.commit) ~= "string" then + logger:log("spec", "spec.commit must be a string in %s", spec[1]) + return false + end + end + + if spec.disable ~= nil then -- spec.disable + if type(spec.disable) ~= "boolean" then + logger:log("spec", "spec.disable must be a boolean in %s", spec[1]) + return false + end + end + + if spec.pin ~= nil then -- spec.pin + if type(spec.pin) ~= "boolean" then + logger:log("spec", "spec.pin must be a boolean in %s", spec[1]) + return false + end + end + + if spec.reqs ~= nil then -- spec.reqs + local is = type(spec.reqs) + if is ~= "table" and is ~= "string" then + logger:log("spec", "spec.reqs must be a table or a string in %s", spec[1]) + return false + end + + if (is == "string") then + spec.reqs = { spec.reqs } + end + end + + if spec.deps ~= nil then -- spec.deps + local is = type(spec.deps) + if is ~= "table" and is ~= "string" then + logger:log("spec", "spec.deps must be a table or a string in %s", spec[1]) + return false + end + + if (is == "string") then + spec.deps = { spec.deps } + end + end + + return spec +end + +--- tell the parent it has a child and the child it has a parent +---@param parent package? parent package if nil defaults to self +---@param child package child package +function package:link_dependency(parent, child) + parent = parent or self + + if not parent.dependents[child.id] then + parent.dependents[child.id] = child + table.insert(parent.dependents, child) + end + + if not child.requirements[parent.id] then + child.requirements[parent.id] = parent + table.insert(child.requirements, parent) + end +end + +--- create a new package instance +---@param spec spec|string a package spec to use for the new package +---@param overrides spec? a package spec that is used to overried this package +---@return package|false package an instance of the package or false on failure +---@nodisacard +function package:new(spec, overrides) + overrides = overrides or {} + + -- ensure that the spec is ok + local new_spec = check_spec(spec) + if new_spec == false then + logger:log("spec", vim.inspect(spec)) + logger:log("error", "spec check failed, check DepLog") + return false + else + spec = new_spec + end + + -- start initializing the package + local id = spec[1] + + local o = packages[id] or {} + 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.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.perf = {} + + packages[id] = o + end + + o.name = spec.as or o.name or id + 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 + o.commit = spec.commit + o.pin = overrides.pin or spec.pin or o.pin + o.enabled = not overrides.disable and not spec.disable and o.enabled + o.lazy = spec.lazy ~= nil or o.lazy + + -- make sure that the package exists + o.exists = vim.fn.isdirectory(o.dir) ~= 0 + o.configured = o.exists + + -- register all the callback functions + if spec.config ~= nil then + table.insert(o.on_config, spec.config) + end + if spec.setup ~= nil then + table.insert(o.on_setup, spec.setup) + end + if spec.load ~= nil then + table.insert(o.on_load, spec.load) + end + if spec.lazy ~= nil then + table.insert(o.lazy_load, spec.lazy) + end + + -- if the current package isn't the root package then it depends on the root + -- package + if root and package ~= root then + o:link_dependency(root, o) + elseif not root then + root = o + end + + -- link the dependencies + if spec.reqs then + ---it is the correct type as asserted in check_spec() + ---@diagnostic disable-next-line: param-type-mismatch + for _, req in pairs(spec.reqs) do + local pkg = package:new(req) + if type(pkg) == "table" then + o:link_dependency(pkg, o) + end + end + end + + -- and link the dependents + 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 + return false + end + o:link_dependency(nil, 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) + end) + table.insert(pkg.lazy_load, function(_) end) + end + end + end + + self.__index = self + + return o +end + +--- set the base directory for packages +---@param _base_dir string base directory +function package.set_base_dir(_base_dir) + base_dir = _base_dir +end + +--- get the root directory +---@return package root +function package.get_root() + return root +end + +--- get the packages in dep +---@return package root +function package.get_packages() + return packages +end + +--- run specified hooks on the current package +---@param type "on_load"|"on_config"|"on_setup" which hook to run +---@return boolean, string|nil +function package:runhooks(type) + local hooks = self[type] + if #hooks == 0 then + return true + end + + local start = os.clock() + + -- chdir into the package directory to make running external commands + -- from hooks easier. + local last_cwd = vim.fn.getcwd() + vim.fn.chdir(self.dir) + + for i = 1, #hooks do + local ok, err = pcall(hooks[i]) + if not ok then + vim.fn.chdir(last_cwd) + + return false, err + end + end + + vim.fn.chdir(last_cwd) + self.perf[type] = os.clock() - start + + logger:log("hook", "triggered %d %s %s for %s", #hooks, type, + #hooks == 1 and "hook" or "hooks", self.id) + + return true +end + +--- make sure a package has been loaded +---@param force boolean? force lazy packages to load +---@return boolean|table return true or false if loaded or package spec if lazy loaded +function package:ensureadded(force) + --- load a package + ---@param pkg package + local function loadpkg(pkg) + -- make sure to load the dependencies first + for _, p in pairs(pkg.requirements) do + if not p.loaded then + p:ensureadded(true) + end + end + + -- now start loading our plugin + local start = os.clock() + + -- run setup hooks + self:runhooks("on_setup") + + -- trigger the packadd for the plugin + local ok, err = pcall(vim.cmd, "packadd "..pkg.name) + if not ok then + return false, err + end + + pkg.added = true + pkg.perf.pack = os.clock() - start + logger:log("vim", "packadd completed for %s", pkg.id) + + -- set the package to loaded + self.loaded = true + logger:log("load", "loaded %s", self.id) + + -- trigger the on_load hooks + ok, err = self:runhooks("on_load") + if not ok then + logger:log("error", "failed to load %s; reason: %s", self.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 + loadpkg(self) + elseif not self.added and self.lazy then + logger:log("lazy", "registering %d lazy hooks for %s", #self.lazy_load, + self.id) + for _, load_cond in pairs(self.lazy_load) do + -- configure the lazy loader for the user + local l = require('lazy.utils'):new() + if l == true then + logger:log("lazy", "failed to get lazy utils") + return false + end + l:set_load(function() + logger:log("lazy", "triggered %d lazy hooks for %s", #self.lazy_load, + self.id) + loadpkg(self) + end) + + -- run it + load_cond(l) + end + return self + end + + return true +end + +--- load all packages in package tree +---@param force boolean? force lazy packages to load +---@return boolean boolean if tree was successfully loaded +function package:loadtree(force) + if not self.exists or not self.enabled then + return false + end + + if self.subtree_loaded then + return true + end + + if not self.lazy then + for _, requirement in pairs(self.requirements) do + if not requirement.loaded then + return false + end + end + end + + if not self.loaded then + local ok, err = self:ensureadded(force) + if not ok then + logger:log("error", "failed to load %s; reason: %s", self.id, err) + return false + end + end + + package.subtree_loaded = true + + for _, dependant in pairs(self.dependents) do + self.subtree_loaded = dependant:loadtree(force) and self.subtree_loaded + end + + return self.subtree_loaded +end + +--- unconfigure a packages tree +function package:unconfiguretree() + -- unconfigure requirements + for _, requirement in pairs(self.requirements) do + requirement.subtree_loaded = false + end + + -- unconfigure dependents + for _, dependant in pairs(self.dependents) do + dependant.loaded = false + dependant.added = false + dependant.configured = false + + dependant.subtree_loaded = false + end +end + +return package diff --git a/lua/lazy/utils.lua b/lua/lazy/utils.lua index 4487914..4c4841b 100644 --- a/lua/lazy/utils.lua +++ b/lua/lazy/utils.lua @@ -11,7 +11,6 @@ function lazy:new() local o = {} setmetatable(o, self) - o.command_ids = {} o.auto_ids = {} o.keybind_ids = {}