274 lines
8.7 KiB
Lua
274 lines
8.7 KiB
Lua
--
|
|
-- Copyright (c) 2022 chiya.dev
|
|
--
|
|
-- Use of this source code is governed by the MIT License
|
|
-- which can be found in the LICENSE file and at:
|
|
--
|
|
-- https://chiya.dev/licenses/mit.txt
|
|
--
|
|
local require, type, setmetatable, error, table, assert, math, os, debug =
|
|
require, type, setmetatable, error, table, assert, math, os, debug
|
|
local logger = require("dep.log").global
|
|
|
|
local function parse_name_from_id(id)
|
|
local name = id:match("^[%w-_.]+/([%w-_.]+)$")
|
|
if name then
|
|
return name
|
|
else
|
|
error(string.format('invalid package name "%s"; must be in the format "user/package"', id))
|
|
end
|
|
end
|
|
|
|
local function is_nonempty_str(s)
|
|
return type(s) == "string" and #s ~= 0
|
|
end
|
|
|
|
--- Package information.
|
|
local Package = setmetatable({
|
|
__metatable = "Package",
|
|
__index = {
|
|
--- Runs all registered hooks of the given type.
|
|
run_hooks = function(self, hook)
|
|
local hooks = self["on_" .. hook]
|
|
if not hooks or #hooks == 0 then
|
|
return true
|
|
end
|
|
|
|
local start = os.clock()
|
|
for i = 1, #hooks do
|
|
local ok, err = xpcall(hooks[i], debug.traceback)
|
|
if not ok then
|
|
return false, err
|
|
end
|
|
end
|
|
|
|
local elapsed = os.clock() - start
|
|
self.perf.hooks[hook] = elapsed
|
|
|
|
logger:log(
|
|
"hook",
|
|
"triggered %d %s %s for %s in %dms",
|
|
#hooks,
|
|
hook,
|
|
#hooks == 1 and "hook" or "hooks",
|
|
self.id,
|
|
elapsed
|
|
)
|
|
|
|
return true
|
|
end,
|
|
},
|
|
}, {
|
|
--- Constructs a new `Package` with the given identifier.
|
|
__call = function(mt, id)
|
|
local name = parse_name_from_id(id)
|
|
return setmetatable({
|
|
id = id,
|
|
name = name,
|
|
url = "https://github.com/" .. id .. ".git",
|
|
enabled = true,
|
|
exists = false,
|
|
added = false,
|
|
configured = false,
|
|
loaded = false,
|
|
dependencies = {},
|
|
dependents = {},
|
|
subtree_configured = false,
|
|
subtree_loaded = false,
|
|
on_setup = {},
|
|
on_config = {},
|
|
on_load = {},
|
|
perf = { hooks = {} },
|
|
}, mt)
|
|
end,
|
|
})
|
|
|
|
--- Manages a set of packages.
|
|
local PackageStore = setmetatable({
|
|
__metatable = "PackageStore",
|
|
__index = {
|
|
--- Links the given packages such that the parent must load before the child.
|
|
link_dependency = function(self, 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,
|
|
|
|
--- Ensures the given package spec table is valid.
|
|
validate_spec = function(self, spec)
|
|
assert(spec[1] ~= nil, "package id missing from spec")
|
|
assert(type(spec[1]) == "string", "package id must be a string")
|
|
parse_name_from_id(spec[1])
|
|
|
|
assert(spec.as == nil or is_nonempty_str(spec.as), "package name must be a string")
|
|
assert(spec.url == nil or type(spec.url) == "string", "package url must be a string") -- TODO: validate url or path
|
|
assert(spec.branch == nil or is_nonempty_str(spec.branch), "package branch must be a string")
|
|
assert(spec.pin == nil or type(spec.pin) == "boolean", "package pin must be a boolean")
|
|
assert(spec.disable == nil or type(spec.disable) == "boolean", "package disable must be a boolean")
|
|
|
|
assert(
|
|
spec.requires == nil or type(spec.requires) == "table" or type(spec.requires) == "string",
|
|
"package requires must be a string or table"
|
|
)
|
|
assert(
|
|
spec.deps == nil or type(spec.deps) == "table" or type(spec.deps) == "string",
|
|
"package deps must be a string or table"
|
|
)
|
|
|
|
assert(spec.setup == nil or type(spec.setup) == "function", "package setup must be a function")
|
|
assert(spec.config == nil or type(spec.config) == "function", "package config must be a function")
|
|
assert(spec[2] == nil or type(spec[2]) == "function", "package loader must be a function")
|
|
end,
|
|
|
|
--- Creates or updates a package from the given spec table, and returns that package.
|
|
add_spec = function(self, spec, scope)
|
|
self:validate_spec(spec)
|
|
scope = scope or {}
|
|
|
|
local id = spec[1]
|
|
local pkg = self[id]
|
|
|
|
if not pkg then
|
|
pkg = Package(id)
|
|
self[id], self[#self + 1] = pkg, pkg
|
|
end
|
|
|
|
-- blend package spec with existing package info
|
|
pkg.name = spec.as or pkg.name
|
|
pkg.url = spec.url or pkg.url
|
|
pkg.branch = spec.branch or pkg.branch
|
|
pkg.pin = scope.pin or spec.pin or pkg.pin
|
|
pkg.enabled = not scope.disable and not spec.disable and pkg.enabled
|
|
|
|
pkg.on_setup[#pkg.on_setup + 1] = spec.setup
|
|
pkg.on_config[#pkg.on_config + 1] = spec.config
|
|
pkg.on_load[#pkg.on_load + 1] = spec[2]
|
|
|
|
local requires = type(spec.requires) == "table" and spec.requires or { spec.requires }
|
|
local deps = type(spec.deps) == "table" and spec.deps or { spec.deps }
|
|
|
|
-- recursively add specs for dependencies and dependents
|
|
for i = 1, #requires do
|
|
self:link_dependency(self:add_spec(requires[i], scope), pkg)
|
|
end
|
|
|
|
for i = 1, #deps do
|
|
self:link_dependency(pkg, self:add_spec(deps[i], scope))
|
|
end
|
|
end,
|
|
|
|
--- Adds the given list of specs.
|
|
add_specs = function(self, specs, scope)
|
|
assert(type(specs) == "table", "package list must be a table")
|
|
assert(specs.pin == nil or type(specs.pin) == "boolean", "package list pin must be a boolean")
|
|
assert(specs.disable == nil or type(specs.disable) == "boolean", "package list disable must be a boolean")
|
|
assert(specs.modules == nil or type(specs.modules) == "table", "package list module list must be a table")
|
|
|
|
scope = scope or {}
|
|
scope = {
|
|
-- outer scope takes precedence over inner list's overrides
|
|
pin = scope.pin or specs.pin,
|
|
disable = scope.disable or specs.disable,
|
|
}
|
|
|
|
-- add specs in spec list
|
|
for i = 1, #specs do
|
|
self:add_spec(specs[i], scope)
|
|
end
|
|
|
|
-- recursively add referenced spec list modules
|
|
if specs.modules then
|
|
local prefix = specs.modules.prefix or ""
|
|
for i = 1, #specs.modules do
|
|
local name = specs.modules[i]
|
|
assert(type(name) == "string", "package list inner module name must be a string")
|
|
name = prefix .. name
|
|
|
|
local module = require(name)
|
|
assert(type(module) == "table", "package list inner module did not return a spec list table")
|
|
self:add_specs(module, scope)
|
|
end
|
|
end
|
|
end,
|
|
|
|
--- Ensures there are no circular dependencies in this package store.
|
|
ensure_acyclic = function(self)
|
|
-- tarjan's strongly connected components algorithm
|
|
local idx, indices, lowlink, stack = 0, {}, {}, {}
|
|
|
|
local function connect(pkg)
|
|
indices[pkg.id], lowlink[pkg.id] = idx, idx
|
|
stack[#stack + 1], stack[pkg.id] = pkg, true
|
|
idx = idx + 1
|
|
|
|
for i = 1, #pkg.dependents do
|
|
local dependent = pkg.dependents[i]
|
|
|
|
if not indices[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], indices[dependent.id])
|
|
end
|
|
end
|
|
|
|
if lowlink[pkg.id] == indices[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 the package explicitly specified itself as a dependency (i.e. the user is being weird)
|
|
if #cycle > 2 or pkg.dependents[pkg.id] then
|
|
return cycle
|
|
end
|
|
end
|
|
end
|
|
|
|
for i = 1, #self do
|
|
local pkg = self[i]
|
|
|
|
if not indices[pkg.id] then
|
|
local cycle = connect(pkg)
|
|
if cycle then
|
|
-- found dependency cycle
|
|
local names = {}
|
|
for j = 1, #cycle do
|
|
names[j] = cycle[j].id
|
|
end
|
|
error("circular dependency detected in package dependency graph: " .. table.concat(names, " -> "))
|
|
end
|
|
end
|
|
end
|
|
end,
|
|
},
|
|
}, {
|
|
--- Constructs a new `PackageStore`.
|
|
__call = function(mt)
|
|
-- hash part of store maps package ids to packages
|
|
-- array part of store is a list of packages
|
|
-- all packages in a store are unique based on their id
|
|
return setmetatable({}, mt)
|
|
end,
|
|
})
|
|
|
|
return {
|
|
Package = Package,
|
|
PackageStore = PackageStore,
|
|
}
|