aboutsummaryrefslogtreecommitdiffstats
path: root/lua
diff options
context:
space:
mode:
Diffstat (limited to 'lua')
-rw-r--r--lua/dep/package.lua266
1 files changed, 266 insertions, 0 deletions
diff --git a/lua/dep/package.lua b/lua/dep/package.lua
new file mode 100644
index 0000000..c4f7714
--- /dev/null
+++ b/lua/dep/package.lua
@@ -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,
+ },
+}