Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 17 additions & 10 deletions DependencyControl.json
Original file line number Diff line number Diff line change
Expand Up @@ -121,14 +121,14 @@
"url": "@{baseUrl}#@{namespace}",
"channels": {
"main": {
"version": "0.8.1",
"released": "2026-07-24",
"version": "0.8.2",
"released": null,
"default": true,
"files": [
{
"name": ".moon",
"url": "@{fileBaseUrl}",
"sha1": "87CDA007933A971BAEFC91A044CF7FC941535D0D"
"sha1": "29CCC772940BFB3F7A92FD93EA0306905A05897F"
},
{
"name": "/Constants.moon",
Expand Down Expand Up @@ -229,7 +229,7 @@
{
"name": "/ModuleLoader.moon",
"url": "@{fileBaseUrl}",
"sha1": "F1ACB8570A361F9006A6E6BA74FFF84D1F058F3A"
"sha1": "059526819899F46484C78AF6C1D6A92F2C354EB2"
},
{
"name": "/ModuleProvider.moon",
Expand Down Expand Up @@ -284,7 +284,7 @@
{
"name": "/UpdateTask.moon",
"url": "@{fileBaseUrl}",
"sha1": "D7296244D4B9DAEAD0463CF6FE699AAF5CDF39A8"
"sha1": "4C1B8BFBE29120014A182F37F1D9E7AA0E456C07"
},
{
"name": "/Updater.moon",
Expand Down Expand Up @@ -369,7 +369,7 @@
{
"name": "/utils.moon",
"url": "@{fileBaseUrl}",
"sha1": "FA8307BC15AD2FEC1535F9B862E1D92C2A3A1AB9"
"sha1": "35B197A1947BA389AB1F522F1EE6F351EEC54949"
},
{
"name": ".moon",
Expand Down Expand Up @@ -488,7 +488,7 @@
{
"name": "/ModuleLoader.moon",
"url": "@{fileBaseUrl}",
"sha1": "8FE0E4ED90D3ED719F7DE80877591EF2F30014AA",
"sha1": "0B1EF3A8E4C79310DD20511F7F6BBEAF8F219CB0",
"type": "test"
},
{
Expand Down Expand Up @@ -554,13 +554,13 @@
{
"name": "/UpdateTask.moon",
"url": "@{fileBaseUrl}",
"sha1": "5CA6FC899341677F702788F543373C8F3EDCFAA0",
"sha1": "D1C04F53DE0609804FE0E9B63AF5DD1EEA2D1D5B",
"type": "test"
},
{
"name": "/Updater.moon",
"url": "@{fileBaseUrl}",
"sha1": "7945FCE5A2DC11257301DE5A2B2423E56A0AC4CB",
"sha1": "46B1E9BD099B831E194F514971BB26667DA470EC",
"type": "test"
},
{
Expand Down Expand Up @@ -638,7 +638,7 @@
{
"name": "/utils.moon",
"url": "@{fileBaseUrl}",
"sha1": "F78238492CB05F9F1568C14DE363A83CE1CAB6D8",
"sha1": "431E1940BB43BB2FE1C3C38A55F57574D278A3DF",
"type": "test"
},
{
Expand Down Expand Up @@ -779,6 +779,13 @@
],
"0.8.1": [
"fix: Reading the channel list of a package that declares no channels returns an empty list instead of throwing an error."
],
"0.8.2": [
"fix: A required module that can't be installed or updated once again reports why. The error path that turns an updater status into a readable reason was itself throwing, so every such failure surfaced as an unrelated \"attempt to call field 'getUpdaterErrorMsg'\" error instead of naming the module and the cause.",
"fix(Utils): `deepCopy()` no longer recurses forever on a table that references itself. Circular references are reproduced as cycles in the copy, and a table reached through several keys is copied once.",
"fix(CLI): `generate-types` and `generate-docs` skip files a feed marks deleted instead of reporting each one as an unreadable source",
"fix(CLI): a deleted file's entry can no longer shadow the source that replaced it when a test suite resolves its modules from the feed.",
"fix(Updater): Updates no longer fail with 'the update task is already running' for modules that require one another (such as Aegisub-Motion's 'Tags' and 'Transform')."
]
}
},
Expand Down
8 changes: 4 additions & 4 deletions depctrl.lua
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ local function registerFeedSearcher(feed)
local sourceById = {}
for file, _, pkg in feed:walkFiles() do
local src = file.localFilePath
if src then
if src and not file.delete then
local base = file.type == "test" and (pkg.namespace .. ".test") or pkg.namespace
local id = base .. leafSuffix(file.name)
sourceById[id] = sourceById[id] or src -- first channel wins; sources are channel-agnostic
Expand All @@ -281,8 +281,8 @@ local function registerFeedSearcher(feed)
end

-- Collects the selected module packages' non-test .moon sources from a feed, keyed by require
-- id, for annotation extraction. Vendored .lua files have no annotations and are skipped; a
-- warning is printed for any unreadable source.
-- id, for annotation extraction. Vendored .lua files have no annotations and are skipped, as are
-- files the feed marks deleted; a warning is printed for any unreadable source.
local function collectModuleSources(feed, filter)
local domain = require "l0.DependencyControl.domain"
local FileOps = require "l0.DependencyControl.file-ops"
Expand All @@ -299,7 +299,7 @@ local function collectModuleSources(feed, filter)
local sources, seen = {}, {}
for file, _, pkg in feed:walkFiles() do
local src = file.localFilePath
if selected[pkg.namespace] and src and file.type ~= "test" and file.name:match("%.moon$") then
if selected[pkg.namespace] and src and not file.delete and file.type ~= "test" and file.name:match("%.moon$") then
local requireId = pkg.namespace .. leafSuffix(file.name)
if not seen[requireId] then
seen[requireId] = true
Expand Down
2 changes: 1 addition & 1 deletion modules/l0/DependencyControl.moon
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ Accessors.install DependencyControl

rec = DependencyControl{
name: "DependencyControl",
version: "0.8.1", -- @{l0.DependencyControl:version}
version: "0.8.2", -- @{l0.DependencyControl:version}
description: "Provides script management and auto-updating for Aegisub macros and modules.",
author: "line0",
url: "http://github.com/TypesettingTools/DependencyControl",
Expand Down
8 changes: 4 additions & 4 deletions modules/l0/DependencyControl/ModuleLoader.moon
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ class ModuleLoader
---@return boolean success
---@return string err Combined error message (empty on success).
@loadModules = (modules, addFeeds = {@feed}, skip = @moduleName and {[@moduleName]: true} or {}) =>
UpdateTask or= require "l0.DependencyControl.UpdateTask"
for mdl in *modules
continue if skip[mdl.moduleName]
with mdl
Expand All @@ -147,9 +148,8 @@ class ModuleLoader
if ._ref
._updated, ._missing = true, false
else
UpdateTask or= require "l0.DependencyControl.UpdateTask"
unless code == UpdateTask.UpdateStatus.SkippedOptional
._reason = @@updater.__class.getUpdaterErrorMsg code, .name or .moduleName, domain.ScriptType.Module, true, extErr
._reason = UpdateTask.getUpdaterErrorMsg code, .name or .moduleName, domain.ScriptType.Module, true, extErr
-- nuke dummy reference for circular dependencies
LOADED_MODULES[.moduleName] = nil

Expand All @@ -169,9 +169,9 @@ class ModuleLoader
ref, code, extErr = @@updater\require record, .version, addFeeds
if ref
._ref = ref
elseif not .optional
elseif code != UpdateTask.UpdateStatus.UpdateInProgress and not .optional
._outdated = true
._reason = @@updater.__class.getUpdaterErrorMsg code, .name or .moduleName, domain.ScriptType.Module, false, extErr
._reason = UpdateTask.getUpdaterErrorMsg code, .name or .moduleName, domain.ScriptType.Module, false, extErr

missing, outdated, moduleError = {}, {}, {}
for mdl in *modules
Expand Down
20 changes: 19 additions & 1 deletion modules/l0/DependencyControl/UpdateTask.moon
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ SourceFeedKind = Enum "SourceFeedKind", {
---| 1 # Installed: the install or update succeeded
---| 2 # AlreadyUpdated: another in-flight update already brought the package to the target version
---| 3 # SkippedOptional: an optional dependency couldn't be satisfied and was skipped
---| 4 # UpdateInProgress: this package's update is already under way and will install a satisfying version
---| -1 # UpdaterDisabled: the updater is disabled in the config
---| -2 # InvalidNamespace: the record's namespace doesn't conform to the rules
---| -3 # Unmanaged: the record is virtual or unmanaged, so it isn't updated
Expand Down Expand Up @@ -168,6 +169,7 @@ UpdateStatus = Enum "UpdateStatus", {
Installed: 1
AlreadyUpdated: 2
SkippedOptional: 3
UpdateInProgress: 4
UpdaterDisabled: -1
InvalidNamespace: -2
Unmanaged: -3
Expand Down Expand Up @@ -297,6 +299,7 @@ msgs = {

---Mutable execution state for one install/update operation.
---@class UpdateTask
---@field private __installingVersion? string The version this task's running update will install. Set only while it resolves its requirements and used to break module dependency cycles.
class UpdateTask
---@private
@__downloader = Downloader!
Expand Down Expand Up @@ -623,6 +626,14 @@ class UpdateTask
with @record do @logger\log msgs.run.starting, getInstallTerm(@record),
domain.terms.scriptType.singular[.scriptType], .name

-- The field is only set while this task's own update resolves its requirements, so re-entering here
-- means we must be dealing with a dependency cycle. This short-circuits the requirement check if the
-- requested version is satisfied by the version being installed, so the cycle can be broken.
if @__installingVersion
satisfied = not @targetVersion or SemanticVersion\check @__installingVersion, @targetVersion
return UpdateStatus.UpdateInProgress, @__installingVersion if satisfied
return @__logUpdateError UpdateStatus.TaskAlreadyRunning

-- don't perform update of a script when another one is already running for the same script
return @__logUpdateError UpdateStatus.TaskAlreadyRunning if @running

Expand Down Expand Up @@ -908,8 +919,15 @@ class UpdateTask
if reqs and #reqs > 0
@logger\log msgs.performUpdate.updateReqs
@logger.indent += 1
success, err = ModuleLoader.loadModules @record, reqs, {@record.feed}
-- Remember the version being installed to break dependency cycles, but prevent it from becoming stale, e.g.,
-- in case of an update error, as it could otherwise corrupt subsequent update runs in an unpredictable way.
@__installingVersion = update.version
loaded, success, err = pcall ModuleLoader.loadModules, @record, reqs, {@record.feed}
@__installingVersion = nil
@logger.indent -= 1
unless loaded
@running = false
error success, 0
unless success
@logger.indent += 1
@logger\log err
Expand Down
36 changes: 27 additions & 9 deletions modules/l0/DependencyControl/test/ModuleLoader.moon
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
ModuleLoader = require "l0.DependencyControl.ModuleLoader"
ModuleProvider = require "l0.DependencyControl.ModuleProvider"
SemanticVersion = require "l0.DependencyControl.SemanticVersion"
UpdateTask = require "l0.DependencyControl.UpdateTask"

DEPCTRL_DUMMY_MODULE_MARKER = "#{constants.DEPCTRL_PRIVATE_GLOBAL_VAR_PREFIX}Dummy"

Expand Down Expand Up @@ -211,8 +212,7 @@
-- dummy ref is cleared.
loadModules_missingRequiredFails: (ut) ->
ns = "test.ModuleLoader.missingFail"
updaterClass = {getUpdaterErrorMsg: (code, name) -> "fetch failed: #{name}"}
updater = {require: ((...) => return nil, -6, "no feed"), __class: updaterClass}
updater = {require: ((...) => return nil, UpdateTask.UpdateStatus.NoSuitablePackage, "no feed")}
recClass = setmetatable {ScriptType: domain.ScriptType, __name: "DependencyControl", :updater},
{__call: (cls, args) -> {}}
rec = {feed: nil, moduleName: "host.Module", name: "host", __class: recClass}
Expand All @@ -223,13 +223,13 @@
success, err = ModuleLoader.loadModules rec, {mdl}
ut\assertFalse success
ut\assertContains err, ns
ut\assertContains err, "no feed" -- the updater's detail reaches the user through the real formatter
ut\assertNil LOADED_MODULES[ns] -- dummy ref nuked

-- loadModules: a missing *optional* module the updater skips is left missing without an error
-- reason and doesn't fail the overall load; the circular-dependency dummy ref is still cleared.
loadModules_missingOptionalSkipped: (ut) ->
ns = "test.ModuleLoader.missingOptionalSkip"
UpdateTask = require "l0.DependencyControl.UpdateTask"
updater = {require: ((...) => return nil, UpdateTask.UpdateStatus.SkippedOptional)}
recClass = setmetatable {ScriptType: domain.ScriptType, __name: "DependencyControl", :updater},
{__call: (cls, args) -> {}}
Expand All @@ -245,13 +245,12 @@
ut\assertNil LOADED_MODULES[ns] -- dummy ref nuked

-- loadModules: a required module that fails because one of ITS OWN requirements couldn't be satisfied
-- surfaces the nested reason (which sub-requirement failed, and why) in the error — using the real
-- getUpdaterErrorMsg so the RequirementsUnmet template's detail isn't dropped on the way to the UI.
-- surfaces the nested reason (which sub-requirement failed, and why) in the error, so the
-- RequirementsUnmet template's detail isn't dropped on the way to the UI.
loadModules_requirementsUnmetSurfacesNestedReason: (ut) ->
ns = "l0.ASSFoundation"
UpdateTask = require "l0.DependencyControl.UpdateTask"
innerReason = "— SubInspector.Inspector (v0.7.2)\n—— Reason: no build for your platform (Linux-x64)"
updater = {require: ((...) => return nil, UpdateTask.UpdateStatus.RequirementsUnmet, innerReason), __class: UpdateTask}
updater = {require: ((...) => return nil, UpdateTask.UpdateStatus.RequirementsUnmet, innerReason)}
recClass = setmetatable {ScriptType: domain.ScriptType, __name: "DependencyControl", :updater},
{__call: (cls, args) -> {}}
rec = {feed: nil, moduleName: "host.Module", name: "Vector Gradient", __class: recClass}
Expand Down Expand Up @@ -286,8 +285,7 @@
loadModules_outdatedRequiredFails: (ut) ->
ns = "test.ModuleLoader.outdatedFail"
loadedRef = {version: {version: 65793, checkVersion: ((target) => false)}}
updaterClass = {getUpdaterErrorMsg: (code, name) -> "too old: #{name}"}
updater = {require: ((...) => return nil, -6, "no newer version"), __class: updaterClass}
updater = {require: ((...) => return nil, UpdateTask.UpdateStatus.NoSuitablePackage, "no newer version")}
recClass = setmetatable {ScriptType: domain.ScriptType, __name: "DependencyControl", :updater},
{__call: (cls, args) -> {}}
rec = {feed: nil, moduleName: "host.Module", name: "host", __class: recClass}
Expand All @@ -297,6 +295,25 @@
success, err = ModuleLoader.loadModules rec, {mdl}
ut\assertFalse success
ut\assertContains err, ns
ut\assertContains err, "no newer version" -- the updater's detail reaches the user through the real formatter

-- loadModules: a requirement caught in a dependency cycle doesn't fail the load — the module already
-- loaded keeps its place and isn't reported as outdated
loadModules_updateInProgressAcceptsLoadedRef: (ut) ->
ns = "test.ModuleLoader.inFlight"
loadedRef = {version: {version: 65793, checkVersion: ((target) => false)}}
updater = {require: ((...) => return nil, UpdateTask.UpdateStatus.UpdateInProgress, "2.0.0")}
recClass = setmetatable {ScriptType: domain.ScriptType, __name: "DependencyControl", :updater},
{__call: (cls, args) -> {}}
rec = {feed: nil, moduleName: "host.Module", name: "host", __class: recClass}
mdl = {moduleName: ns, name: ns, version: SemanticVersion\toPacked "2.0.0", optional: false}
(ut\stub ModuleLoader, "loadModule")\calls (self, m, usePrivate) -> m._ref = loadedRef unless usePrivate
ut\stub(ModuleProvider, "isDepCtrlVersionRecord")\returns true
success, err = ModuleLoader.loadModules rec, {mdl}
ut\assertTrue success
ut\assertEquals err, ""
ut\assertEquals mdl._ref, loadedRef
ut\assertNil mdl._outdated

-- checkOptionalModules: mock self with requiredModules

Expand Down Expand Up @@ -336,6 +353,7 @@
"loadModules_missingFetchedViaUpdater", "loadModules_missingRequiredFails",
"loadModules_missingOptionalSkipped", "loadModules_requirementsUnmetSurfacesNestedReason",
"loadModules_outdatedForcesUpdate", "loadModules_outdatedRequiredFails",
"loadModules_updateInProgressAcceptsLoadedRef",
"checkOptionalModules_noneOptional", "checkOptionalModules_missingOptional"
}
}
Loading
Loading