Refactor package store out of main file

This commit is contained in:
luaneko
2022-12-20 23:26:30 +11:00
parent 6b11a1fee5
commit ff83edab8d

266
lua/dep/package.lua Normal file
View File

@ -0,0 +1,266 @@
--
-- 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://opensource.org/licenses/MIT
--
local require, type, setmetatable, error, assert, math, os, debug =
require, type, setmetatable, error, 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 = {
-- Constructs a new `Package` with the given identifier.
new = 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,
__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,
},
}
-- Manages a set of packages.
local PackageStore = {
-- Constructs a new `PackageStore`.
new = 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,
__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 given the following 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:new()
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,
},
}