500 lines
14 KiB
Lua
500 lines
14 KiB
Lua
local logger = require('dep.log')
|
|
local spec_man = require("dep.spec")
|
|
local bench = require("dep.bench")
|
|
|
|
---@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 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 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 path string? the path of the package which overrides the url
|
|
---@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 = {}
|
|
|
|
--- 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 = spec_man.check(spec_man.correct_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 {}
|
|
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
|
|
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 = {}
|
|
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 spec_man.get_name(spec)
|
|
o.url = spec.url or o.url or ("https://github.com/"..id..".git")
|
|
o.path = spec.path and vim.fs.normalize(spec.path) or spec.path
|
|
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 o ~= 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 _, dep in pairs(spec.deps) do
|
|
local pkg = package:new(dep)
|
|
if not pkg then
|
|
return false
|
|
end
|
|
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 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
|
|
|
|
return o
|
|
end
|
|
|
|
--- set the base directory for packages
|
|
---@param _base_dir string base directory
|
|
function package.set_base_dir(_base_dir)
|
|
base_dir = vim.fs.normalize(_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[] packages
|
|
---@nodiscard
|
|
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
|
|
|
|
-- run setup hooks
|
|
pkg:runhooks("on_setup")
|
|
|
|
-- now start loading our plugin
|
|
local start = os.clock()
|
|
|
|
-- trigger the packadd for the plugin
|
|
---@diagnostic disable-next-line: param-type-mismatch
|
|
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
|
|
pkg.loaded = true
|
|
logger:log("load", "loaded %s", pkg.id)
|
|
|
|
-- trigger the on_load hooks
|
|
ok, err = pkg:runhooks("on_load")
|
|
if not ok then
|
|
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.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('dep.lazy.utils'):new()
|
|
if l == true then
|
|
logger:log("lazy", "failed to get lazy utils")
|
|
return false
|
|
end
|
|
l:set_load(function()
|
|
-- ensure that we can't attempt to load a plugin twice via lazy loading
|
|
if self.loaded then
|
|
return
|
|
end
|
|
|
|
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
|
|
---@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
|
|
return false
|
|
end
|
|
|
|
-- 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 and not requirement.lazy then
|
|
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
|
|
logger:log("error", "failed to load %s; reason: %s", self.id, err)
|
|
return false
|
|
end
|
|
end
|
|
|
|
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
|
|
|
|
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
|
|
|
|
--- 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
|
|
|
|
--- recurse over all packages and register them
|
|
---@param speclist speclist table of specs
|
|
---@param overrides spec? a package spec that is used to override options
|
|
function package.register_speclist(speclist, overrides)
|
|
overrides = overrides or {}
|
|
|
|
-- recurse the packages
|
|
local over = overrides
|
|
for _, spec in ipairs(speclist) do
|
|
-- make sure the overrides override and take into account the packages spec
|
|
---@diagnostic disable-next-line: missing-fields
|
|
over = {
|
|
pin = overrides.pin or spec.pin,
|
|
disable = overrides.disable or spec.disable
|
|
}
|
|
|
|
-- While a package can fail to load we just don't care, it will work itself
|
|
-- out. The goal is to make sure every plugin that can load does load, not
|
|
-- keep working plugins from loading because an unrelated one doesn't load.
|
|
package:new(spec, over)
|
|
end
|
|
end
|
|
|
|
--- reload the package
|
|
---@param self package the package to reload
|
|
---@param force boolean? force all packages to load
|
|
function package:reload(force)
|
|
local reloaded = self:loadtree(force)
|
|
|
|
if reloaded then
|
|
local ok, err
|
|
-- TODO: make a benchmark function
|
|
bench.mark("reload", function()
|
|
ok, err = pcall(vim.cmd,
|
|
[[
|
|
silent! helptags ALL
|
|
silent! UpdateRemotePlugins
|
|
]])
|
|
end)
|
|
|
|
if not ok then
|
|
logger:log("error",
|
|
"failed to reload helptags and remote plugins; reason: %s", err)
|
|
end
|
|
end
|
|
end
|
|
|
|
return package
|