From cd0329020a13e6f748b5eaecd51a50f83899e3e7 Mon Sep 17 00:00:00 2001 From: line0 Date: Sat, 23 May 2026 15:16:34 +0200 Subject: [PATCH 01/15] fix: add missing SemanticVersioning module --- macros/l0.DependencyControl.Toolbox.moon | 4 +- modules/DependencyControl.moon | 1 + modules/DependencyControl/ModuleLoader.moon | 342 +++++----- modules/DependencyControl/Record.moon | 628 +++++++++--------- .../DependencyControl/SemanticVersioning.moon | 60 ++ modules/DependencyControl/UpdateFeed.moon | 11 +- modules/DependencyControl/Updater.moon | 25 +- 7 files changed, 568 insertions(+), 503 deletions(-) create mode 100644 modules/DependencyControl/SemanticVersioning.moon diff --git a/macros/l0.DependencyControl.Toolbox.moon b/macros/l0.DependencyControl.Toolbox.moon index d66c5f6..3511e62 100644 --- a/macros/l0.DependencyControl.Toolbox.moon +++ b/macros/l0.DependencyControl.Toolbox.moon @@ -37,7 +37,7 @@ buildInstalledDlgList = (scriptType, config, isUninstall) -> for namespace, script in pairs config.c[scriptType] continue if protectedModules[namespace] - item = "%s v%s%s"\format script.name, depRec\getVersionString(script.version), + item = "%s v%s%s"\format script.name, DepCtrl.SemanticVersioning\toString(script.version), script.activeChannel and " [#{script.activeChannel}]" or "" list[#list+1] = item table.sort list, (a, b) -> a\lower! < b\lower! @@ -96,7 +96,7 @@ install = -> tbl[namespace] or= {} for channel in *channels record = scriptData.data.channels[channel] - verNum = depRec\getVersionNumber record.version + verNum = DepCtrl.SemanticVersioning\toNumber record.version unless config.c[scriptType][namespace] or (tbl[namespace][channel] and verNum < tbl[namespace][channel].verNum) tbl[namespace][channel] = { name: scriptData.name, version: record.version, verNum: verNum, feed: feed.url, default: defaultChannel == channel, moduleName: scriptType == "modules" and namespace } diff --git a/modules/DependencyControl.moon b/modules/DependencyControl.moon index b7a95c4..8e0132c 100644 --- a/modules/DependencyControl.moon +++ b/modules/DependencyControl.moon @@ -24,6 +24,7 @@ class DependencyControl extends Record @Updater = Updater @UnitTestSuite = UnitTestSuite @FileOps = FileOps + @SemanticVersioning = SemanticVersioning rec = DependencyControl{ diff --git a/modules/DependencyControl/ModuleLoader.moon b/modules/DependencyControl/ModuleLoader.moon index e1f6738..2b4971e 100644 --- a/modules/DependencyControl/ModuleLoader.moon +++ b/modules/DependencyControl/ModuleLoader.moon @@ -1,172 +1,174 @@ --- Note: this is a private API intended to be exclusively for internal DependenyControl use --- Everyting in this class can and will change without any prior notice --- and calling any method is guaranteed to interfere with DepdencyControl operation - -class ModuleLoader - msgs = { - checkOptionalModules: { - downloadHint: "Please download the modules in question manually, put them in your %s folder and reload your automation scripts." - missing: "Error: a %s feature you're trying to use requires additional modules that were not found on your system:\n%s\n%s" - } - formatVersionErrorTemplate: { - missing: "— %s %s%s\n—— Reason: %s" - outdated: "— %s (Installed: v%s; Required: v%s)%s\n—— Reason: %s" - } - loadModules: { - missing: "Error: one or more of the modules required by %s could not be found on your system:\n%s\n%s" - missingRecord: "Error: module '%s' is missing a version record." - moduleError: "Error in required module %s:\n%s" - outdated: [[Error: one or more of the modules required by %s are outdated on your system: -%s\nPlease update the modules in question manually and reload your automation scripts.]] - } - } - - @formatVersionErrorTemplate = (name, reqVersion, url, reason, ref) => - url = url and ": #{url}" or "" - if ref - version = @@parseVersion ref.version - return msgs.formatVersionErrorTemplate.outdated\format name, version, reqVersion, url, reason - else - reqVersion = reqVersion and " (v#{reqVersion})" or "" - return msgs.formatVersionErrorTemplate.missing\format name, reqVersion, url, reason - - @createDummyRef = => - return nil if @scriptType != @@ScriptType.Module - -- global module registry allows for circular dependencies: - -- set a dummy reference to this module since this module is not ready - -- when the other one tries to load it (and vice versa) - export LOADED_MODULES = {} unless LOADED_MODULES - unless LOADED_MODULES[@namespace] - @ref = {} - LOADED_MODULES[@namespace] = setmetatable {__depCtrlDummy: true, version: @}, @ref - return true - return false - - @removeDummyRef = => - return nil if @scriptType != @@ScriptType.Module - if LOADED_MODULES[@namespace] and LOADED_MODULES[@namespace].__depCtrlDummy - LOADED_MODULES[@namespace] = nil - return true - return false - - @loadModule = (mdl, usePrivate, reload) => - runInitializer = (ref) -> - return unless type(ref) == "table" and ref.__depCtrlInit - -- Note to future self: don't change this to a class check! When DepCtrl self-updates - -- any managed module initialized before will still use the same instance +-- Note: this is a private API intended to be exclusively for internal DependenyControl use +-- Everyting in this class can and will change without any prior notice +-- and calling any method is guaranteed to interfere with DepdencyControl operation + +SemanticVersioning = require "l0.DependencyControl.SemanticVersioning" + +class ModuleLoader + msgs = { + checkOptionalModules: { + downloadHint: "Please download the modules in question manually, put them in your %s folder and reload your automation scripts." + missing: "Error: a %s feature you're trying to use requires additional modules that were not found on your system:\n%s\n%s" + } + formatVersionErrorTemplate: { + missing: "— %s %s%s\n—— Reason: %s" + outdated: "— %s (Installed: v%s; Required: v%s)%s\n—— Reason: %s" + } + loadModules: { + missing: "Error: one or more of the modules required by %s could not be found on your system:\n%s\n%s" + missingRecord: "Error: module '%s' is missing a version record." + moduleError: "Error in required module %s:\n%s" + outdated: [[Error: one or more of the modules required by %s are outdated on your system: +%s\nPlease update the modules in question manually and reload your automation scripts.]] + } + } + + @formatVersionErrorTemplate = (name, reqVersion, url, reason, ref) => + url = url and ": #{url}" or "" + if ref + version = SemanticVersioning\toNumber ref.version + return msgs.formatVersionErrorTemplate.outdated\format name, version, reqVersion, url, reason + else + reqVersion = reqVersion and " (v#{reqVersion})" or "" + return msgs.formatVersionErrorTemplate.missing\format name, reqVersion, url, reason + + @createDummyRef = => + return nil if @scriptType != @@ScriptType.Module + -- global module registry allows for circular dependencies: + -- set a dummy reference to this module since this module is not ready + -- when the other one tries to load it (and vice versa) + export LOADED_MODULES = {} unless LOADED_MODULES + unless LOADED_MODULES[@namespace] + @ref = {} + LOADED_MODULES[@namespace] = setmetatable {__depCtrlDummy: true, version: @}, @ref + return true + return false + + @removeDummyRef = => + return nil if @scriptType != @@ScriptType.Module + if LOADED_MODULES[@namespace] and LOADED_MODULES[@namespace].__depCtrlDummy + LOADED_MODULES[@namespace] = nil + return true + return false + + @loadModule = (mdl, usePrivate, reload) => + runInitializer = (ref) -> + return unless type(ref) == "table" and ref.__depCtrlInit + -- Note to future self: don't change this to a class check! When DepCtrl self-updates + -- any managed module initialized before will still use the same instance if type(ref.version) != "table" or ref.version.__name != @@__name - ref.__depCtrlInit @@ - - with mdl - ._missing, ._error = nil - - moduleName = usePrivate and "#{@namespace}.#{mdl.moduleName}" or .moduleName - name = "#{mdl.name or mdl.moduleName}#{usePrivate and ' (Private Copy)' or ''}" - - if .outdated or reload - -- clear old references - package.loaded[moduleName], LOADED_MODULES[moduleName] = nil - - elseif ._ref = LOADED_MODULES[moduleName] - -- module is already loaded, however it may or may not have been loaded by DepCtrl - -- so we have to call any DepCtrl initializer if it hasn't been called yet - runInitializer ._ref - return ._ref - - loaded, res = xpcall require, debug.traceback, moduleName - unless loaded - LOADED_MODULES[moduleName] = nil - res or= "unknown error" - ._missing = res\match "module '.+' not found:" - ._error = res unless ._missing - return nil - - -- set new references - if reload and ._ref and ._ref.__depCtrlDummy - setmetatable ._ref, res - ._ref, LOADED_MODULES[moduleName] = res, res - - -- run DepCtrl initializer if one was specified - runInitializer res - - return mdl._ref -- having this in the with block breaks moonscript - - @loadModules = (modules, addFeeds = {@feed}, skip = @moduleName and {[@moduleName]: true} or {}) => - for mdl in *modules - continue if skip[mdl] - with mdl - ._ref, ._updated, ._missing, ._outdated, ._reason, ._error = nil - - -- try to load private copies of required modules first - ModuleLoader.loadModule @, mdl, true - ModuleLoader.loadModule @, mdl unless ._ref - - -- try to fetch and load a missing module from the web - if ._missing - record = @@{moduleName:.moduleName, name:.name or .moduleName, - version:-1, url:.url, feed:.feed, virtual:true} - ._ref, code, extErr = @@updater\require record, .version, addFeeds, .optional - if ._ref or .optional - ._updated, ._missing = true, false - else - ._reason = @@updater\getUpdaterErrorMsg code, .name or .moduleName, true, true, extErr - -- nuke dummy reference for circular dependencies - LOADED_MODULES[.moduleName] = nil - - -- check if the version requirements are satisfied - -- which is guaranteed for modules updated with \require, so we don't need to check again - if .version and ._ref and not ._updated - record = ._ref.version - unless record - ._error = msgs.loadModules.missingRecord\format .moduleName - continue - - if type(record) != "table" or record.__class != @@ - record = @@ moduleName: .moduleName, version: record, recordType: @@RecordType.Unmanaged - - -- force an update for outdated modules - if not record\checkVersion .version - ref, code, extErr = @@updater\require record, .version, addFeeds - if ref - ._ref = ref - elseif not .optional - ._outdated = true - ._reason = @@updater\getUpdaterErrorMsg code, .name or .moduleName, true, false, extErr - else - -- perform regular update check if we can get a lock without waiting - -- right now we don't care about the result and don't reload the module - -- so the update will not be effective until the user restarts Aegisub - -- or reloads the script - @@updater\scheduleUpdate record - - missing, outdated, moduleError = {}, {}, {} - for mdl in *modules - with mdl - name = .name or .moduleName - if ._missing - missing[#missing+1] = ModuleLoader.formatVersionErrorTemplate @, name, .version, .url, ._reason - elseif ._outdated - outdated[#outdated+1] = ModuleLoader.formatVersionErrorTemplate @, name, .version, .url, ._reason, ._ref - elseif ._error - moduleError[#moduleError+1] = msgs.loadModules.moduleError\format name, ._error - - errorMsg = {} - if #moduleError > 0 - errorMsg[1] = table.concat moduleError, "\n" - if #outdated > 0 - errorMsg[#errorMsg+1] = msgs.loadModules.outdated\format @name, table.concat outdated, "\n" - if #missing > 0 - errorMsg[#errorMsg+1] = msgs.loadModules.missing\format @name, table.concat(missing, "\n"), downloadHint - - return #errorMsg == 0, table.concat(errorMsg, "\n\n") - - @checkOptionalModules = (modules) => - modules = type(modules)=="string" and {[modules]:true} or {mdl,true for mdl in *modules} - missing = [ModuleLoader.formatVersionErrorTemplate @, mdl.moduleName, mdl.version, msl.url, - mdl._reason for mdl in *@requiredModules when mdl.optional and mdl._missing and modules[mdl.name]] - - if #missing>0 - downloadHint = msgs.checkOptionalModules.downloadHint\format @@automationDir.modules - errorMsg = msgs.checkOptionalModules.missing\format @name, table.concat(missing, "\n"), downloadHint - return false, errorMsg + ref.__depCtrlInit @@ + + with mdl + ._missing, ._error = nil + + moduleName = usePrivate and "#{@namespace}.#{mdl.moduleName}" or .moduleName + name = "#{mdl.name or mdl.moduleName}#{usePrivate and ' (Private Copy)' or ''}" + + if .outdated or reload + -- clear old references + package.loaded[moduleName], LOADED_MODULES[moduleName] = nil + + elseif ._ref = LOADED_MODULES[moduleName] + -- module is already loaded, however it may or may not have been loaded by DepCtrl + -- so we have to call any DepCtrl initializer if it hasn't been called yet + runInitializer ._ref + return ._ref + + loaded, res = xpcall require, debug.traceback, moduleName + unless loaded + LOADED_MODULES[moduleName] = nil + res or= "unknown error" + ._missing = res\match "module '.+' not found:" + ._error = res unless ._missing + return nil + + -- set new references + if reload and ._ref and ._ref.__depCtrlDummy + setmetatable ._ref, res + ._ref, LOADED_MODULES[moduleName] = res, res + + -- run DepCtrl initializer if one was specified + runInitializer res + + return mdl._ref -- having this in the with block breaks moonscript + + @loadModules = (modules, addFeeds = {@feed}, skip = @moduleName and {[@moduleName]: true} or {}) => + for mdl in *modules + continue if skip[mdl] + with mdl + ._ref, ._updated, ._missing, ._outdated, ._reason, ._error = nil + + -- try to load private copies of required modules first + ModuleLoader.loadModule @, mdl, true + ModuleLoader.loadModule @, mdl unless ._ref + + -- try to fetch and load a missing module from the web + if ._missing + record = @@{moduleName:.moduleName, name:.name or .moduleName, + version:-1, url:.url, feed:.feed, virtual:true} + ._ref, code, extErr = @@updater\require record, .version, addFeeds, .optional + if ._ref or .optional + ._updated, ._missing = true, false + else + ._reason = @@updater\getUpdaterErrorMsg code, .name or .moduleName, true, true, extErr + -- nuke dummy reference for circular dependencies + LOADED_MODULES[.moduleName] = nil + + -- check if the version requirements are satisfied + -- which is guaranteed for modules updated with \require, so we don't need to check again + if .version and ._ref and not ._updated + record = ._ref.version + unless record + ._error = msgs.loadModules.missingRecord\format .moduleName + continue + + if type(record) != "table" or record.__class != @@ + record = @@ moduleName: .moduleName, version: record, recordType: @@RecordType.Unmanaged + + -- force an update for outdated modules + if not record\checkVersion .version + ref, code, extErr = @@updater\require record, .version, addFeeds + if ref + ._ref = ref + elseif not .optional + ._outdated = true + ._reason = @@updater\getUpdaterErrorMsg code, .name or .moduleName, true, false, extErr + else + -- perform regular update check if we can get a lock without waiting + -- right now we don't care about the result and don't reload the module + -- so the update will not be effective until the user restarts Aegisub + -- or reloads the script + @@updater\scheduleUpdate record + + missing, outdated, moduleError = {}, {}, {} + for mdl in *modules + with mdl + name = .name or .moduleName + if ._missing + missing[#missing+1] = ModuleLoader.formatVersionErrorTemplate @, name, .version, .url, ._reason + elseif ._outdated + outdated[#outdated+1] = ModuleLoader.formatVersionErrorTemplate @, name, .version, .url, ._reason, ._ref + elseif ._error + moduleError[#moduleError+1] = msgs.loadModules.moduleError\format name, ._error + + errorMsg = {} + if #moduleError > 0 + errorMsg[1] = table.concat moduleError, "\n" + if #outdated > 0 + errorMsg[#errorMsg+1] = msgs.loadModules.outdated\format @name, table.concat outdated, "\n" + if #missing > 0 + errorMsg[#errorMsg+1] = msgs.loadModules.missing\format @name, table.concat(missing, "\n"), downloadHint + + return #errorMsg == 0, table.concat(errorMsg, "\n\n") + + @checkOptionalModules = (modules) => + modules = type(modules)=="string" and {[modules]:true} or {mdl,true for mdl in *modules} + missing = [ModuleLoader.formatVersionErrorTemplate @, mdl.moduleName, mdl.version, msl.url, + mdl._reason for mdl in *@requiredModules when mdl.optional and mdl._missing and modules[mdl.name]] + + if #missing>0 + downloadHint = msgs.checkOptionalModules.downloadHint\format @@automationDir.modules + errorMsg = msgs.checkOptionalModules.missing\format @name, table.concat(missing, "\n"), downloadHint + return false, errorMsg return true \ No newline at end of file diff --git a/modules/DependencyControl/Record.moon b/modules/DependencyControl/Record.moon index 5293321..5aef6fb 100644 --- a/modules/DependencyControl/Record.moon +++ b/modules/DependencyControl/Record.moon @@ -1,315 +1,315 @@ -json = require "json" -lfs = require "lfs" -re = require "aegisub.re" - -Common = require "l0.DependencyControl.Common" -Logger = require "l0.DependencyControl.Logger" -ConfigHandler = require "l0.DependencyControl.ConfigHandler" -FileOps = require "l0.DependencyControl.FileOps" -Updater = require "l0.DependencyControl.Updater" -ModuleLoader = require "l0.DependencyControl.ModuleLoader" -SemanticVersioning = require "l0.DependencyControl.SemanticVersioning" - -class Record extends Common - namespaceValidation = re.compile "^(?:[-\\w]+\\.)+[-\\w]+$" - - msgs = { - new: { - badRecordError: "Error: Bad #{@@__name} record (%s)." - badRecord: { - noUnmanagedMacros: "Creating unmanaged version records for macros is not allowed" - missingNamespace: "No namespace defined" - badVersion: "Couldn't parse version number: %s" - badNamespace: "Namespace '%s' failed validation. Namespace rules: must contain 1+ single dots, but not start or end with a dot; all other characters must be in [A-Za-z0-9-_]." - badModuleTable: "Invalid required module table #%d (%s)." - } - } - uninstall: { - noVirtualOrUnmanaged: "Can't uninstall %s %s '%s'. (Only installed scripts managed by #{@@__name} can be uninstalled)." - } - writeConfig: { - error: "An error occured while writing the #{@@__name} config file: %s" - writing: "Writing updated %s data to config file..." - } - } - - @depConf = { - file: aegisub.decode_path "?user/config/l0.#{@@__name}.json", - scriptFields: {"author", "configFile", "feed", "moduleName", "name", "namespace", "url", -- REMOVE - "requiredModules", "version", "unmanaged"}, - globalDefaults: {updaterEnabled:true, updateInterval:302400, traceLevel:3, extraFeeds:{}, - tryAllFeeds:false, dumpFeeds:true, configDir:"?user/config", - logMaxFiles: 200, logMaxAge: 604800, logMaxSize:10*(10^6), - updateWaitTimeout: 60, updateOrphanTimeout: 600, - logDir: "?user/log", writeLogs: true} - } - - init = => - FileOps.mkdir @depConf.file, true - @loadConfig! - @logger = Logger { fileBaseName: "DepCtrl", fileSubName: script_namespace, prefix: "[#{@@__name}] ", - toFile: @config.c.writeLogs, defaultLevel: @config.c.traceLevel, - maxAge: @config.c.logMaxAge,maxSize: @config.c.logMaxSize, maxFiles: @config.c.logMaxFiles, - logDir: @config.c.logDir } - - @updater = Updater script_namespace, @config, @logger - @configDir = @config.c.configDir - - FileOps.mkdir aegisub.decode_path @configDir - logsHaveBeenTrimmed or= @logger\trimFiles! - FileOps.runScheduledRemoval @configDir - - - new: (args) => - init Record unless @@logger - - -- defaults - args[k] = v for k, v in pairs { - readGlobalScriptVars: true - saveRecordToConfig: true - } when args[k] == nil - - {@requiredModules, moduleName:@moduleName, configFile:configFile, virtual:@virtual, :name, - description:@description, url:@url, feed:@feed, recordType:@recordType, :namespace, - author:@author, :version, configFile:@configFile, - :readGlobalScriptVars, :saveRecordToConfig} = args - - @recordType or= @@RecordType.Managed - -- also support name key (as used in configuration) for required modules - @requiredModules or= args.requiredModules - - if @moduleName - @namespace = @moduleName - @name = name or @moduleName - @scriptType = @@ScriptType.Module - ModuleLoader.createDummyRef @ unless @virtual or @recordType == @@RecordType.Unmanaged - - else - if @virtual or not readGlobalScriptVars - @name = name or namespace - @namespace = namespace - version or= 0 - else - @name = name or script_name - @description or= script_description - @author or= script_author - version or= script_version - - @namespace = namespace or script_namespace - assert @recordType == @@RecordType.Managed, msgs.new.badRecordError\format msgs.new.badRecord.noUnmanagedMacros - assert @namespace, msgs.new.badRecordError\format msgs.new.badRecord.missingNamespace - @scriptType = @@ScriptType.Automation - - -- if the hosting macro doesn't have a namespace defined, define it for - -- the first DepCtrled module loaded by the macro or its required modules - unless script_namespace - export script_namespace = @namespace - - -- non-depctrl record don't need to conform to namespace rules - assert @virtual or @recordType == @@RecordType.Unmanaged or @validateNamespace!, - msgs.new.badRecord.badNamespace\format @namespace - - @configFile = configFile or "#{@namespace}.json" - @automationDir = @@automationDir[@scriptType] - @testDir = @@testDir[@scriptType] - @version, err = @@parseVersion version - assert @version, msgs.new.badRecordError\format msgs.new.badRecord.badVersion\format err - - @requiredModules or= {} - -- normalize short format module tables - for i, mdl in pairs @requiredModules - switch type mdl - when "table" - mdl.moduleName or= mdl[1] - mdl[1] = nil - when "string" - @requiredModules[i] = {moduleName: mdl} - else error msgs.new.badRecordError\format msgs.new.badRecord.badModuleTable\format i, tostring mdl - - shouldWriteConfig = @loadConfig! - - -- write config file if contents are missing or are out of sync with the script version record - -- ramp up the random wait time on first initialization (many scripts may want to write configuration data) - -- we can't really profit from write concerting here because we don't know which module loads last - @writeConfig if shouldWriteConfig and saveRecordToConfig - - checkOptionalModules: ModuleLoader.checkOptionalModules - - -- loads the DependencyControl global configuration - @loadConfig = => - if @config - @config\load! - else @config = ConfigHandler @depConf.file, @depConf.globalDefaults, {"config"}, nil, @logger - - -- loads the script configuration - loadConfig: (importRecord = false) => - -- virtual modules are not yet present on the user's system and have no persistent configuration - @config or= ConfigHandler not @virtual and @@depConf.file, {}, - { @@ScriptType.name.legacy[@scriptType], @namespace }, true, @@logger - - -- import and overwrites version record from the configuration - if importRecord - -- check if a module that was previously virtual was installed in the meantime - -- TODO: prevent issues caused by orphaned config entries - haveConfig = false - if @virtual - @config\setFile @@depConf.file - if @config\load! - haveConfig, @virtual = true, false - else @config\unsetFile! - else - haveConfig = @config\load! - - -- only need to refresh data if the record was changed by an update - if haveConfig - @[key] = @config.c[key] for key in *@@depConf.scriptFields - - elseif not @virtual - -- copy script information to the config - @config\load! - shouldWriteConfig = @config\import @, @@depConf.scriptFields, false, true - return shouldWriteConfig - - return false - - writeConfig: => - unless @virtual or @config.file - @config\setFile @@depConf.file - - @@logger\trace msgs.writeConfig.writing, @@terms.scriptType.singular[@scriptType] - @config\import @, @@depConf.scriptFields, false, true - success, errMsg = @config\write false - - assert success, msgs.writeConfig.error\format errMsg - - - @parseVersion = SemanticVersioning.parse - - - @getVersionString = SemanticVersioning.toString - - - getConfigFileName: () => - return aegisub.decode_path "#{@@configDir}/#{@configFile}" - - getConfigHandler: (defaults, section, noLoad) => - return ConfigHandler @getConfigFileName!, defaults, section, noLoad - - getLogger: (args = {}) => - args.fileBaseName or= @namespace - args.toFile = @config.c.logToFile if args.toFile == nil - args.defaultLevel or= @config.c.logLevel - args.prefix or= @moduleName and "[#{@name}]" - - return Logger args - - checkVersion: (value, precision = "patch") => - if type(value) == "table" and value.__class == @@ - value = value.version - return SemanticVersioning\check @version, value - - - getSubmodules: => - return nil if @virtual or @recordType == @@RecordType.Unmanaged or @scriptType != @@ScriptType.Module - mdlConfig = @@config\getSectionHandler @@ScriptType.name.legacy[@@ScriptType.Module] - pattern = "^#{@namespace}."\gsub "%.", "%%." - return [mdl for mdl, _ in pairs mdlConfig.c when mdl\match pattern], mdlConfig - - requireModules: (modules = @requiredModules, addFeeds = {@feed}) => - success, err = ModuleLoader.loadModules @, modules, addFeeds - @@updater\releaseLock! - unless success - -- if we failed loading our required modules - -- then that means we also failed to load - LOADED_MODULES[@namespace] = nil - @@logger\error err - return unpack [mdl._ref for mdl in *modules] - - registerTests: (...) => - -- load external tests - haveTests, tests = pcall require, "DepUnit.#{@@ScriptType.name.legacy[@scriptType]}.#{@namespace}" - - if haveTests and not @testsLoaded - @tests, tests.name = tests, @name - modules = table.pack @requireModules! - if @moduleName - @tests\import @ref, modules, ... - else @tests\import modules, ... - - @tests\registerMacros! - @testsLoaded = true - - register: (selfRef, ...) => - -- replace dummy refs with real refs to own module - @ref.__index, @ref, LOADED_MODULES[@moduleName] = selfRef, selfRef, selfRef - @registerTests selfRef, ... - return selfRef - - registerMacro: (name=@name, description=@description, process, validate, isActive, submenu) => - -- alternative signature takes name and description from script - if type(name)=="function" - process, validate, isActive, submenu = name, description, process, validate - name, description = @name, @description - - -- use automation script name for submenu by default - submenu = @name if submenu == true - - menuName = { @config.c.customMenu } - menuName[#menuName+1] = submenu if submenu - menuName[#menuName+1] = name - - -- check for updates before running a macro - processHooked = (sub, sel, act) -> - @@updater\scheduleUpdate @ - @@updater\releaseLock! - return process sub, sel, act - - aegisub.register_macro table.concat(menuName, "/"), description, processHooked, validate, isActive - - registerMacros: (macros = {}, submenuDefault = true) => - for macro in *macros - -- allow macro table to omit name and description - submenuIdx = type(macro[1])=="function" and 4 or 6 - macro[submenuIdx] = submenuDefault if macro[submenuIdx] == nil - @registerMacro unpack(macro, 1, 6) - - setVersion: (version) => - version, err = @@parseVersion version - if version - @version = version - return version - else return nil, err - - validateNamespace: (namespace = @namespace, isVirtual = @virtual) => - return isVirtual or namespaceValidation\match @namespace - - uninstall: (removeConfig = true) => - if @virtual or @recordType == @@RecordType.Unmanaged - return nil, msgs.uninstall.noVirtualOrUnmanaged\format @virtual and "virtual" or "unmanaged", - @@terms.scriptType.singular[@scriptType], - @name - @config\delete! - subModules, mdlConfig = @getSubmodules! - -- uninstalling a module also removes all submodules - if subModules and #subModules > 0 - mdlConfig.c[mdl] = nil for mdl in *subModules - mdlConfig\write! - - toRemove, pattern, dir = {} - if @moduleName - nsp, name = @namespace\match "(.+)%.(.+)" - pattern = "^#{name}" - dir = "#{@automationDir}/#{nsp\gsub '%.', '/'}" - else - pattern = "^#{@namespace}"\gsub "%.", "%%." - dir = @automationDir - - lfs.chdir dir - for file in lfs.dir dir - mode, path = FileOps.attributes file, "mode" - -- parent level module files must be .ext - currPattern = @moduleName and mode == "file" and pattern.."%." or pattern - -- automation scripts don't use any subdirectories - if (@moduleName or mode == "file") and file\match currPattern - toRemove[#toRemove+1] = path +json = require "json" +lfs = require "lfs" +re = require "aegisub.re" + +Common = require "l0.DependencyControl.Common" +Logger = require "l0.DependencyControl.Logger" +ConfigHandler = require "l0.DependencyControl.ConfigHandler" +FileOps = require "l0.DependencyControl.FileOps" +Updater = require "l0.DependencyControl.Updater" +ModuleLoader = require "l0.DependencyControl.ModuleLoader" +SemanticVersioning = require "l0.DependencyControl.SemanticVersioning" + +class Record extends Common + namespaceValidation = re.compile "^(?:[-\\w]+\\.)+[-\\w]+$" + + msgs = { + new: { + badRecordError: "Error: Bad #{@@__name} record (%s)." + badRecord: { + noUnmanagedMacros: "Creating unmanaged version records for macros is not allowed" + missingNamespace: "No namespace defined" + badVersion: "Couldn't parse version number: %s" + badNamespace: "Namespace '%s' failed validation. Namespace rules: must contain 1+ single dots, but not start or end with a dot; all other characters must be in [A-Za-z0-9-_]." + badModuleTable: "Invalid required module table #%d (%s)." + } + } + uninstall: { + noVirtualOrUnmanaged: "Can't uninstall %s %s '%s'. (Only installed scripts managed by #{@@__name} can be uninstalled)." + } + writeConfig: { + error: "An error occured while writing the #{@@__name} config file: %s" + writing: "Writing updated %s data to config file..." + } + } + + @depConf = { + file: aegisub.decode_path "?user/config/l0.#{@@__name}.json", + scriptFields: {"author", "configFile", "feed", "moduleName", "name", "namespace", "url", -- REMOVE + "requiredModules", "version", "unmanaged"}, + globalDefaults: {updaterEnabled:true, updateInterval:302400, traceLevel:3, extraFeeds:{}, + tryAllFeeds:false, dumpFeeds:true, configDir:"?user/config", + logMaxFiles: 200, logMaxAge: 604800, logMaxSize:10*(10^6), + updateWaitTimeout: 60, updateOrphanTimeout: 600, + logDir: "?user/log", writeLogs: true} + } + + init = => + FileOps.mkdir @depConf.file, true + @loadConfig! + @logger = Logger { fileBaseName: "DepCtrl", fileSubName: script_namespace, prefix: "[#{@@__name}] ", + toFile: @config.c.writeLogs, defaultLevel: @config.c.traceLevel, + maxAge: @config.c.logMaxAge,maxSize: @config.c.logMaxSize, maxFiles: @config.c.logMaxFiles, + logDir: @config.c.logDir } + + @updater = Updater script_namespace, @config, @logger + @configDir = @config.c.configDir + + FileOps.mkdir aegisub.decode_path @configDir + logsHaveBeenTrimmed or= @logger\trimFiles! + FileOps.runScheduledRemoval @configDir + + + new: (args) => + init Record unless @@logger + + -- defaults + args[k] = v for k, v in pairs { + readGlobalScriptVars: true + saveRecordToConfig: true + } when args[k] == nil + + {@requiredModules, moduleName:@moduleName, configFile:configFile, virtual:@virtual, :name, + description:@description, url:@url, feed:@feed, recordType:@recordType, :namespace, + author:@author, :version, configFile:@configFile, + :readGlobalScriptVars, :saveRecordToConfig} = args + + @recordType or= @@RecordType.Managed + -- also support name key (as used in configuration) for required modules + @requiredModules or= args.requiredModules + + if @moduleName + @namespace = @moduleName + @name = name or @moduleName + @scriptType = @@ScriptType.Module + ModuleLoader.createDummyRef @ unless @virtual or @recordType == @@RecordType.Unmanaged + + else + if @virtual or not readGlobalScriptVars + @name = name or namespace + @namespace = namespace + version or= 0 + else + @name = name or script_name + @description or= script_description + @author or= script_author + version or= script_version + + @namespace = namespace or script_namespace + assert @recordType == @@RecordType.Managed, msgs.new.badRecordError\format msgs.new.badRecord.noUnmanagedMacros + assert @namespace, msgs.new.badRecordError\format msgs.new.badRecord.missingNamespace + @scriptType = @@ScriptType.Automation + + -- if the hosting macro doesn't have a namespace defined, define it for + -- the first DepCtrled module loaded by the macro or its required modules + unless script_namespace + export script_namespace = @namespace + + -- non-depctrl record don't need to conform to namespace rules + assert @virtual or @recordType == @@RecordType.Unmanaged or @validateNamespace!, + msgs.new.badRecord.badNamespace\format @namespace + + @configFile = configFile or "#{@namespace}.json" + @automationDir = @@automationDir[@scriptType] + @testDir = @@testDir[@scriptType] + @version, err = SemanticVersioning\toNumber version + assert @version, msgs.new.badRecordError\format msgs.new.badRecord.badVersion\format err + + @requiredModules or= {} + -- normalize short format module tables + for i, mdl in pairs @requiredModules + switch type mdl + when "table" + mdl.moduleName or= mdl[1] + mdl[1] = nil + when "string" + @requiredModules[i] = {moduleName: mdl} + else error msgs.new.badRecordError\format msgs.new.badRecord.badModuleTable\format i, tostring mdl + + shouldWriteConfig = @loadConfig! + + -- write config file if contents are missing or are out of sync with the script version record + -- ramp up the random wait time on first initialization (many scripts may want to write configuration data) + -- we can't really profit from write concerting here because we don't know which module loads last + @writeConfig if shouldWriteConfig and saveRecordToConfig + + checkOptionalModules: ModuleLoader.checkOptionalModules + + -- loads the DependencyControl global configuration + @loadConfig = => + if @config + @config\load! + else @config = ConfigHandler @depConf.file, @depConf.globalDefaults, {"config"}, nil, @logger + + -- loads the script configuration + loadConfig: (importRecord = false) => + -- virtual modules are not yet present on the user's system and have no persistent configuration + @config or= ConfigHandler not @virtual and @@depConf.file, {}, + { @@ScriptType.name.legacy[@scriptType], @namespace }, true, @@logger + + -- import and overwrites version record from the configuration + if importRecord + -- check if a module that was previously virtual was installed in the meantime + -- TODO: prevent issues caused by orphaned config entries + haveConfig = false + if @virtual + @config\setFile @@depConf.file + if @config\load! + haveConfig, @virtual = true, false + else @config\unsetFile! + else + haveConfig = @config\load! + + -- only need to refresh data if the record was changed by an update + if haveConfig + @[key] = @config.c[key] for key in *@@depConf.scriptFields + + elseif not @virtual + -- copy script information to the config + @config\load! + shouldWriteConfig = @config\import @, @@depConf.scriptFields, false, true + return shouldWriteConfig + + return false + + writeConfig: => + unless @virtual or @config.file + @config\setFile @@depConf.file + + @@logger\trace msgs.writeConfig.writing, @@terms.scriptType.singular[@scriptType] + @config\import @, @@depConf.scriptFields, false, true + success, errMsg = @config\write false + + assert success, msgs.writeConfig.error\format errMsg + + + -- retained for compatibility with DepCtrl <= v0.6.3 + -- TODO: deprecate w/ v0.7.0 and remove in next major release + @getVersionNumber = SemanticVersioning.toNumber + @getVersionString = SemanticVersioning.toString + + + getConfigFileName: () => + return aegisub.decode_path "#{@@configDir}/#{@configFile}" + + getConfigHandler: (defaults, section, noLoad) => + return ConfigHandler @getConfigFileName!, defaults, section, noLoad + + getLogger: (args = {}) => + args.fileBaseName or= @namespace + args.toFile = @config.c.logToFile if args.toFile == nil + args.defaultLevel or= @config.c.logLevel + args.prefix or= @moduleName and "[#{@name}]" + + return Logger args + + checkVersion: (value, precision = "patch") => + if type(value) == "table" and value.__class == @@ + value = value.version + return SemanticVersioning\check @version, value + + + getSubmodules: => + return nil if @virtual or @recordType == @@RecordType.Unmanaged or @scriptType != @@ScriptType.Module + mdlConfig = @@config\getSectionHandler @@ScriptType.name.legacy[@@ScriptType.Module] + pattern = "^#{@namespace}."\gsub "%.", "%%." + return [mdl for mdl, _ in pairs mdlConfig.c when mdl\match pattern], mdlConfig + + requireModules: (modules = @requiredModules, addFeeds = {@feed}) => + success, err = ModuleLoader.loadModules @, modules, addFeeds + @@updater\releaseLock! + unless success + -- if we failed loading our required modules + -- then that means we also failed to load + LOADED_MODULES[@namespace] = nil + @@logger\error err + return unpack [mdl._ref for mdl in *modules] + + registerTests: (...) => + -- load external tests + haveTests, tests = pcall require, "DepUnit.#{@@ScriptType.name.legacy[@scriptType]}.#{@namespace}" + + if haveTests and not @testsLoaded + @tests, tests.name = tests, @name + modules = table.pack @requireModules! + if @moduleName + @tests\import @ref, modules, ... + else @tests\import modules, ... + + @tests\registerMacros! + @testsLoaded = true + + register: (selfRef, ...) => + -- replace dummy refs with real refs to own module + @ref.__index, @ref, LOADED_MODULES[@moduleName] = selfRef, selfRef, selfRef + @registerTests selfRef, ... + return selfRef + + registerMacro: (name=@name, description=@description, process, validate, isActive, submenu) => + -- alternative signature takes name and description from script + if type(name)=="function" + process, validate, isActive, submenu = name, description, process, validate + name, description = @name, @description + + -- use automation script name for submenu by default + submenu = @name if submenu == true + + menuName = { @config.c.customMenu } + menuName[#menuName+1] = submenu if submenu + menuName[#menuName+1] = name + + -- check for updates before running a macro + processHooked = (sub, sel, act) -> + @@updater\scheduleUpdate @ + @@updater\releaseLock! + return process sub, sel, act + + aegisub.register_macro table.concat(menuName, "/"), description, processHooked, validate, isActive + + registerMacros: (macros = {}, submenuDefault = true) => + for macro in *macros + -- allow macro table to omit name and description + submenuIdx = type(macro[1])=="function" and 4 or 6 + macro[submenuIdx] = submenuDefault if macro[submenuIdx] == nil + @registerMacro unpack(macro, 1, 6) + + setVersion: (version) => + version, err = SemanticVersioning\toNumber version + if version + @version = version + return version + else return nil, err + + validateNamespace: (namespace = @namespace, isVirtual = @virtual) => + return isVirtual or namespaceValidation\match @namespace + + uninstall: (removeConfig = true) => + if @virtual or @recordType == @@RecordType.Unmanaged + return nil, msgs.uninstall.noVirtualOrUnmanaged\format @virtual and "virtual" or "unmanaged", + @@terms.scriptType.singular[@scriptType], + @name + @config\delete! + subModules, mdlConfig = @getSubmodules! + -- uninstalling a module also removes all submodules + if subModules and #subModules > 0 + mdlConfig.c[mdl] = nil for mdl in *subModules + mdlConfig\write! + + toRemove, pattern, dir = {} + if @moduleName + nsp, name = @namespace\match "(.+)%.(.+)" + pattern = "^#{name}" + dir = "#{@automationDir}/#{nsp\gsub '%.', '/'}" + else + pattern = "^#{@namespace}"\gsub "%.", "%%." + dir = @automationDir + + lfs.chdir dir + for file in lfs.dir dir + mode, path = FileOps.attributes file, "mode" + -- parent level module files must be .ext + currPattern = @moduleName and mode == "file" and pattern.."%." or pattern + -- automation scripts don't use any subdirectories + if (@moduleName or mode == "file") and file\match currPattern + toRemove[#toRemove+1] = path return FileOps.remove toRemove, true, true \ No newline at end of file diff --git a/modules/DependencyControl/SemanticVersioning.moon b/modules/DependencyControl/SemanticVersioning.moon new file mode 100644 index 0000000..f8b9fb9 --- /dev/null +++ b/modules/DependencyControl/SemanticVersioning.moon @@ -0,0 +1,60 @@ +class SemanticVersioning + msgs = { + toNumber: { + badString: "Can't parse version string '%s'. Make sure it conforms to semantic versioning standards." + badType: "Argument had the wrong type: expected a string or number, got a %s." + overflow: "Error: %s version must be an integer < 255, got %s." + } + } + + semParts = {{"major", 16}, {"minor", 8}, {"patch", 0}} + + @toString = (version, precision = "patch") => + if type(version) == "string" + version = @toNumber version + + parts = {0, 0, 0} + for i, part in ipairs semParts + parts[i] = bit.rshift(version, part[2]) % 256 + break if precision == part[1] + + return "%d.%d.%d"\format unpack parts + + + @toNumber = (value) => + return switch type value + when "number" then math.max value, 0 + when "nil" then 0 + when "string" + matches = {value\match "^(%d+).(%d+).(%d+)$"} + if #matches != 3 + return false, msgs.toNumber.badString\format value + + version = 0 + for i, part in ipairs semParts + value = tonumber matches[i] + if type(value) != "number" or value > 256 + return false, msgs.toNumber.overflow\format part[1], tostring value + + version += bit.lshift value, part[2] + version + + else false, msgs.toNumber.badType\format type value + + + @check: (a, b, precision = "patch") => + if type(a) != "number" + a, err = @toNumber a + return nil, err unless a + + if type(b) != "number" + b, err = @toNumber b + return nil, err unless b + + mask = 0 + for part in *semParts + mask += 0xFF * 2^part[2] + break if precision == part[1] + + b = bit.band b, mask + return a >= b, b \ No newline at end of file diff --git a/modules/DependencyControl/UpdateFeed.moon b/modules/DependencyControl/UpdateFeed.moon index 7ec8035..60f539d 100644 --- a/modules/DependencyControl/UpdateFeed.moon +++ b/modules/DependencyControl/UpdateFeed.moon @@ -4,6 +4,7 @@ DownloadManager = require "DM.DownloadManager" DependencyControl = nil Logger = require "l0.DependencyControl.Logger" Common = require "l0.DependencyControl.Common" +SemanticVersioning = require "l0.DependencyControl.SemanticVersioning" defaultLogger = Logger fileBaseName: "DepCtrl.UpdateFeed" @@ -53,20 +54,20 @@ class ScriptUpdateRecord extends Common getChangelog: (versionRecord, minVer = 0) => return "" unless "table" == type @changelog - maxVer = DependencyControl\parseVersion @version - minVer = DependencyControl\parseVersion minVer + maxVer = SemanticVersioning\toNumber @version + minVer = SemanticVersioning\toNumber minVer changelog = {} for ver, entry in pairs @changelog - ver = DependencyControl\parseVersion ver - verStr = DependencyControl\getVersionString ver + ver = SemanticVersioning\toNumber ver + verStr = SemanticVersioning\toString ver if ver >= minVer and ver <= maxVer changelog[#changelog+1] = {ver, verStr, entry} return "" if #changelog == 0 table.sort changelog, (a,b) -> a[1]>b[1] - msg = {msgs.changelog.header\format @name, DependencyControl\getVersionString(@version), @released or ""} + msg = {msgs.changelog.header\format @name, SemanticVersioning\toString(@version), @released or ""} for chg in *changelog chg[3] = {chg[3]} if type(chg[3]) ~= "table" if #chg[3] > 0 diff --git a/modules/DependencyControl/Updater.moon b/modules/DependencyControl/Updater.moon index 888a105..2480ba4 100644 --- a/modules/DependencyControl/Updater.moon +++ b/modules/DependencyControl/Updater.moon @@ -7,6 +7,7 @@ fileOps = require "l0.DependencyControl.FileOps" Logger = require "l0.DependencyControl.Logger" Common = require "l0.DependencyControl.Common" ModuleLoader = require "l0.DependencyControl.ModuleLoader" +SemanticVersioning = require "l0.DependencyControl.SemanticVersioning" DependencyControl = nil class UpdaterBase extends Common @@ -97,7 +98,7 @@ class UpdateTask extends UpdaterBase @logger = @updater.logger @triedFeeds = {} @status = nil - @targetVersion = DependencyControl\parseVersion targetVersion + @targetVersion = SemanticVersioning\toNumber targetVersion -- set UpdateFeed settings @feedConfig = { @@ -109,7 +110,7 @@ class UpdateTask extends UpdaterBase return nil, -2 unless @record\validateNamespace! set: (targetVersion, @addFeeds, @exhaustive, @channel, @optional) => - @targetVersion = DependencyControl\parseVersion targetVersion + @targetVersion = SemanticVersioning\toNumber targetVersion return @ checkFeed: (feedUrl) => @@ -161,7 +162,7 @@ class UpdateTask extends UpdaterBase -- check if the script was already updated if @updated and not exhaustive and @record\checkVersion @targetVersion - @logger\log msgs.run.alreadyUpdated, @record.name, DependencyControl\getVersionString @record.version + @logger\log msgs.run.alreadyUpdated, @record.name, SemanticVersioning\toString @record.version return 2 -- build feed list @@ -229,12 +230,12 @@ class UpdateTask extends UpdaterBase -- and the version must at least be that returned by at least one feed if maxVer>0 and not @record.virtual and @targetVersion <= @record.version @logger\log msgs.run.upToDate, @@terms.scriptType.singular[@record.scriptType], - @record.name, DependencyControl\getVersionString @record.version + @record.name, SemanticVersioning\toString @record.version return 0 - res = msgs.run.noFeedAvailExt\format @targetVersion == 0 and "any" or DependencyControl\getVersionString(@targetVersion), - @record.virtual and "no" or DependencyControl\getVersionString(@record.version), - maxVer<1 and "none" or DependencyControl\getVersionString maxVer + res = msgs.run.noFeedAvailExt\format @targetVersion == 0 and "any" or SemanticVersioning\toString(@targetVersion), + @record.virtual and "no" or SemanticVersioning\toString(@record.version), + maxVer<1 and "none" or SemanticVersioning\toString maxVer if @optional @logger\log msgs.run.skippedOptional, @record.name, @@terms.isInstall[@record.virtual], @@ -379,20 +380,20 @@ class UpdateTask extends UpdaterBase @ref = ref else with @record - .name, .version, .virtual = @record.name, DependencyControl\parseVersion update.version + .name, .version, .virtual = @record.name, SemanticVersioning\toNumber update.version @record\writeConfig! @updated = true @logger\log msgs.performUpdate.updSuccess, @@terms.capitalize(@@terms.isInstall[wasVirtual]), @@terms.scriptType.singular[@record.scriptType], - @record.name, DependencyControl\getVersionString @record.version + @record.name, SemanticVersioning\toString @record.version -- Diplay changelog - @logger\log update\getChangelog @record, (DependencyControl\parseVersion oldVer) + 1 + @logger\log update\getChangelog @record, (SemanticVersioning\toNumber oldVer) + 1 @logger\log msgs.performUpdate.reloadNotice -- TODO: check handling of private module copies (need extra return value?) - return finish 1, DependencyControl\getVersionString @record.version + return finish 1, SemanticVersioning\toString @record.version refreshRecord: => @@ -406,7 +407,7 @@ class UpdateTask extends UpdaterBase @logger\log msgs.refreshRecord.unsetVirtual, @@terms.scriptType.singular[.scriptType], .name else @logger\log msgs.refreshRecord.otherUpdate, @@terms.scriptType.singular[.scriptType], .name, - DependencyControl\getVersionString @record.version + SemanticVersioning\toString @record.version class Updater extends UpdaterBase msgs = { From f147a45ef374c5a80bc5a1c4653f0a5e4072c41c Mon Sep 17 00:00:00 2001 From: line0 Date: Sat, 23 May 2026 16:54:40 +0200 Subject: [PATCH 02/15] fix: broken script type handling in update feeds --- macros/l0.DependencyControl.Toolbox.moon | 21 ++++++++++++++------- modules/DependencyControl/UpdateFeed.moon | 17 +++++++++++++---- modules/DependencyControl/Updater.moon | 3 ++- 3 files changed, 29 insertions(+), 12 deletions(-) diff --git a/macros/l0.DependencyControl.Toolbox.moon b/macros/l0.DependencyControl.Toolbox.moon index 3511e62..b757db5 100644 --- a/macros/l0.DependencyControl.Toolbox.moon +++ b/macros/l0.DependencyControl.Toolbox.moon @@ -11,7 +11,8 @@ logger.usePrefixWindow = false msgs = { install: { - scanning: "Scanning %d available feeds..." + scanning: "Scanning %d available feeds...", + createScriptUpdateRecordFailed: "Failed to create an update record for %s '%s' from feed %s: %s" } uninstall: { running: "Uninstalling %s '%s'..." @@ -90,16 +91,22 @@ install = -> config = getConfig! addAvailableToInstall = (tbl, feed, scriptType) -> - for namespace, data in pairs feed.data[scriptType] - scriptData = feed\getScript namespace, scriptType == "modules", nil, false + scriptTypeConfigAndFeedKeyName = DepCtrl.ScriptType.name.legacy[scriptType] + + for namespace, data in pairs feed.data[scriptTypeConfigAndFeedKeyName] + scriptData, err = feed\getScript namespace, scriptType, nil, false + if err + logger\warn msgs.install.createScriptUpdateRecordFailed\format DepCtrl.terms.scriptType.singular[scriptType], namespace, feed.url, err + continue + channels, defaultChannel = scriptData\getChannels! tbl[namespace] or= {} for channel in *channels record = scriptData.data.channels[channel] verNum = DepCtrl.SemanticVersioning\toNumber record.version - unless config.c[scriptType][namespace] or (tbl[namespace][channel] and verNum < tbl[namespace][channel].verNum) + unless config.c[scriptTypeConfigAndFeedKeyName][namespace] or (tbl[namespace][channel] and verNum < tbl[namespace][channel].verNum) tbl[namespace][channel] = { name: scriptData.name, version: record.version, verNum: verNum, feed: feed.url, - default: defaultChannel == channel, moduleName: scriptType == "modules" and namespace } + default: defaultChannel == channel, moduleName: scriptType == DepCtrl.ScriptType.Module and namespace } return tbl buildDlgList = (tbl) -> @@ -120,8 +127,8 @@ install = -> logger\log msgs.install.scanning, #feeds for feed in *feeds - macros = addAvailableToInstall macros, feed, "macros" - modules = addAvailableToInstall modules, feed, "modules" + macros = addAvailableToInstall macros, feed, DepCtrl.ScriptType.Automation + modules = addAvailableToInstall modules, feed, DepCtrl.ScriptType.Module -- build macro and module lists as well as reverse mappings moduleList, moduleMap = buildDlgList modules diff --git a/modules/DependencyControl/UpdateFeed.moon b/modules/DependencyControl/UpdateFeed.moon index 60f539d..5d228db 100644 --- a/modules/DependencyControl/UpdateFeed.moon +++ b/modules/DependencyControl/UpdateFeed.moon @@ -106,6 +106,7 @@ class UpdateFeed extends Common downloadFailed: "Download of feed %s to %s failed (%s)." cantOpen: "Can't open downloaded feed for reading (%s)." parse: "Error parsing feed." + invalidScriptType: "Invalid or unsupported script type: '%s'. Supported types: %s." } } @@ -141,14 +142,13 @@ class UpdateFeed extends Common feedsHaveBeenTrimmed or= Logger(fileMatchTemplate: fileMatchTemplate, logDir: @config.downloadPath, maxFiles: 20)\trimFiles! @fileName = fileName or table.concat {@config.downloadPath, fileBaseName, "%04X"\format(math.random 0, 16^4-1), ".json"} + @downloadManager = DownloadManager aegisub.decode_path @config.downloadPath if @@cache[@url] @logger\trace msgs.trace.usingCached @data = @@cache[@url] elseif autoFetch @fetch! - @downloadManager = DownloadManager aegisub.decode_path @config.downloadPath - getKnownFeeds: => return {} unless @data return [url for _, url in pairs @data.knownFeeds] @@ -239,13 +239,22 @@ class UpdateFeed extends Common return @data getScript: (namespace, scriptType, config, autoChannel) => + -- legacy compatibility for <= 0.6.3 + if scriptType == true then scriptType = @@ScriptType.Module + elseif scriptType == false then scriptType = @@ScriptType.Automation + section = @@ScriptType.name.legacy[scriptType] + unless section + err = msgs.errors.invalidScriptType\format scriptType, + table.concat ["#{v} (#{@@ScriptType.name.canonical[v]})" for k, v in pairs @@ScriptType when k != "name"], ", " + return nil, err + scriptData = @data[section][namespace] return false unless scriptData ScriptUpdateRecord namespace, scriptData, config, scriptType, autoChannel, @logger getMacro: (namespace, config, autoChannel) => - @getScript namespace, false, config, autoChannel + @getScript namespace, @@ScriptType.Automation, config, autoChannel getModule: (namespace, config, autoChannel) => - @getScript namespace, true, config, autoChannel \ No newline at end of file + @getScript namespace, @@ScriptType.Module, config, autoChannel \ No newline at end of file diff --git a/modules/DependencyControl/Updater.moon b/modules/DependencyControl/Updater.moon index 2480ba4..ccf5380 100644 --- a/modules/DependencyControl/Updater.moon +++ b/modules/DependencyControl/Updater.moon @@ -122,8 +122,9 @@ class UpdateTask extends UpdaterBase return nil, msgs.checkFeed.downloadFailed\format err -- select our script and update channel - updateRecord = feed\getScript @record.namespace, @record.scriptType, @record.config, false + updateRecord, err = feed\getScript @record.namespace, @record.scriptType, @record.config, false unless updateRecord + return nil, err if err return nil, msgs.checkFeed.noData\format @@terms.scriptType.singular[@record.scriptType], @record.name success, currentChannel = updateRecord\setChannel @channel From bf665954cda2873055607749cbf5b5ef34395e08 Mon Sep 17 00:00:00 2001 From: line0 Date: Sat, 23 May 2026 18:00:23 +0200 Subject: [PATCH 03/15] fix: module loader running depctrl initializer hooks on already initialized modules --- modules/DependencyControl/ModuleLoader.moon | 344 ++++++++++---------- 1 file changed, 172 insertions(+), 172 deletions(-) diff --git a/modules/DependencyControl/ModuleLoader.moon b/modules/DependencyControl/ModuleLoader.moon index 2b4971e..fb5e9e6 100644 --- a/modules/DependencyControl/ModuleLoader.moon +++ b/modules/DependencyControl/ModuleLoader.moon @@ -1,174 +1,174 @@ --- Note: this is a private API intended to be exclusively for internal DependenyControl use --- Everyting in this class can and will change without any prior notice --- and calling any method is guaranteed to interfere with DepdencyControl operation - -SemanticVersioning = require "l0.DependencyControl.SemanticVersioning" - -class ModuleLoader - msgs = { - checkOptionalModules: { - downloadHint: "Please download the modules in question manually, put them in your %s folder and reload your automation scripts." - missing: "Error: a %s feature you're trying to use requires additional modules that were not found on your system:\n%s\n%s" - } - formatVersionErrorTemplate: { - missing: "— %s %s%s\n—— Reason: %s" - outdated: "— %s (Installed: v%s; Required: v%s)%s\n—— Reason: %s" - } - loadModules: { - missing: "Error: one or more of the modules required by %s could not be found on your system:\n%s\n%s" - missingRecord: "Error: module '%s' is missing a version record." - moduleError: "Error in required module %s:\n%s" - outdated: [[Error: one or more of the modules required by %s are outdated on your system: -%s\nPlease update the modules in question manually and reload your automation scripts.]] - } - } - - @formatVersionErrorTemplate = (name, reqVersion, url, reason, ref) => - url = url and ": #{url}" or "" - if ref +-- Note: this is a private API intended to be exclusively for internal DependenyControl use +-- Everyting in this class can and will change without any prior notice +-- and calling any method is guaranteed to interfere with DepdencyControl operation + +SemanticVersioning = require "l0.DependencyControl.SemanticVersioning" + +class ModuleLoader + msgs = { + checkOptionalModules: { + downloadHint: "Please download the modules in question manually, put them in your %s folder and reload your automation scripts." + missing: "Error: a %s feature you're trying to use requires additional modules that were not found on your system:\n%s\n%s" + } + formatVersionErrorTemplate: { + missing: "— %s %s%s\n—— Reason: %s" + outdated: "— %s (Installed: v%s; Required: v%s)%s\n—— Reason: %s" + } + loadModules: { + missing: "Error: one or more of the modules required by %s could not be found on your system:\n%s\n%s" + missingRecord: "Error: module '%s' is missing a version record." + moduleError: "Error in required module %s:\n%s" + outdated: [[Error: one or more of the modules required by %s are outdated on your system: +%s\nPlease update the modules in question manually and reload your automation scripts.]] + } + } + + @formatVersionErrorTemplate = (name, reqVersion, url, reason, ref) => + url = url and ": #{url}" or "" + if ref version = SemanticVersioning\toNumber ref.version - return msgs.formatVersionErrorTemplate.outdated\format name, version, reqVersion, url, reason - else - reqVersion = reqVersion and " (v#{reqVersion})" or "" - return msgs.formatVersionErrorTemplate.missing\format name, reqVersion, url, reason - - @createDummyRef = => - return nil if @scriptType != @@ScriptType.Module - -- global module registry allows for circular dependencies: - -- set a dummy reference to this module since this module is not ready - -- when the other one tries to load it (and vice versa) - export LOADED_MODULES = {} unless LOADED_MODULES - unless LOADED_MODULES[@namespace] - @ref = {} - LOADED_MODULES[@namespace] = setmetatable {__depCtrlDummy: true, version: @}, @ref - return true - return false - - @removeDummyRef = => - return nil if @scriptType != @@ScriptType.Module - if LOADED_MODULES[@namespace] and LOADED_MODULES[@namespace].__depCtrlDummy - LOADED_MODULES[@namespace] = nil - return true - return false - - @loadModule = (mdl, usePrivate, reload) => - runInitializer = (ref) -> - return unless type(ref) == "table" and ref.__depCtrlInit - -- Note to future self: don't change this to a class check! When DepCtrl self-updates - -- any managed module initialized before will still use the same instance - if type(ref.version) != "table" or ref.version.__name != @@__name - ref.__depCtrlInit @@ - - with mdl - ._missing, ._error = nil - - moduleName = usePrivate and "#{@namespace}.#{mdl.moduleName}" or .moduleName - name = "#{mdl.name or mdl.moduleName}#{usePrivate and ' (Private Copy)' or ''}" - - if .outdated or reload - -- clear old references - package.loaded[moduleName], LOADED_MODULES[moduleName] = nil - - elseif ._ref = LOADED_MODULES[moduleName] - -- module is already loaded, however it may or may not have been loaded by DepCtrl - -- so we have to call any DepCtrl initializer if it hasn't been called yet - runInitializer ._ref - return ._ref - - loaded, res = xpcall require, debug.traceback, moduleName - unless loaded - LOADED_MODULES[moduleName] = nil - res or= "unknown error" - ._missing = res\match "module '.+' not found:" - ._error = res unless ._missing - return nil - - -- set new references - if reload and ._ref and ._ref.__depCtrlDummy - setmetatable ._ref, res - ._ref, LOADED_MODULES[moduleName] = res, res - - -- run DepCtrl initializer if one was specified - runInitializer res - - return mdl._ref -- having this in the with block breaks moonscript - - @loadModules = (modules, addFeeds = {@feed}, skip = @moduleName and {[@moduleName]: true} or {}) => - for mdl in *modules - continue if skip[mdl] - with mdl - ._ref, ._updated, ._missing, ._outdated, ._reason, ._error = nil - - -- try to load private copies of required modules first - ModuleLoader.loadModule @, mdl, true - ModuleLoader.loadModule @, mdl unless ._ref - - -- try to fetch and load a missing module from the web - if ._missing - record = @@{moduleName:.moduleName, name:.name or .moduleName, - version:-1, url:.url, feed:.feed, virtual:true} - ._ref, code, extErr = @@updater\require record, .version, addFeeds, .optional - if ._ref or .optional - ._updated, ._missing = true, false - else - ._reason = @@updater\getUpdaterErrorMsg code, .name or .moduleName, true, true, extErr - -- nuke dummy reference for circular dependencies - LOADED_MODULES[.moduleName] = nil - - -- check if the version requirements are satisfied - -- which is guaranteed for modules updated with \require, so we don't need to check again - if .version and ._ref and not ._updated - record = ._ref.version - unless record - ._error = msgs.loadModules.missingRecord\format .moduleName - continue - - if type(record) != "table" or record.__class != @@ - record = @@ moduleName: .moduleName, version: record, recordType: @@RecordType.Unmanaged - - -- force an update for outdated modules - if not record\checkVersion .version - ref, code, extErr = @@updater\require record, .version, addFeeds - if ref - ._ref = ref - elseif not .optional - ._outdated = true - ._reason = @@updater\getUpdaterErrorMsg code, .name or .moduleName, true, false, extErr - else - -- perform regular update check if we can get a lock without waiting - -- right now we don't care about the result and don't reload the module - -- so the update will not be effective until the user restarts Aegisub - -- or reloads the script - @@updater\scheduleUpdate record - - missing, outdated, moduleError = {}, {}, {} - for mdl in *modules - with mdl - name = .name or .moduleName - if ._missing - missing[#missing+1] = ModuleLoader.formatVersionErrorTemplate @, name, .version, .url, ._reason - elseif ._outdated - outdated[#outdated+1] = ModuleLoader.formatVersionErrorTemplate @, name, .version, .url, ._reason, ._ref - elseif ._error - moduleError[#moduleError+1] = msgs.loadModules.moduleError\format name, ._error - - errorMsg = {} - if #moduleError > 0 - errorMsg[1] = table.concat moduleError, "\n" - if #outdated > 0 - errorMsg[#errorMsg+1] = msgs.loadModules.outdated\format @name, table.concat outdated, "\n" - if #missing > 0 - errorMsg[#errorMsg+1] = msgs.loadModules.missing\format @name, table.concat(missing, "\n"), downloadHint - - return #errorMsg == 0, table.concat(errorMsg, "\n\n") - - @checkOptionalModules = (modules) => - modules = type(modules)=="string" and {[modules]:true} or {mdl,true for mdl in *modules} - missing = [ModuleLoader.formatVersionErrorTemplate @, mdl.moduleName, mdl.version, msl.url, - mdl._reason for mdl in *@requiredModules when mdl.optional and mdl._missing and modules[mdl.name]] - - if #missing>0 - downloadHint = msgs.checkOptionalModules.downloadHint\format @@automationDir.modules - errorMsg = msgs.checkOptionalModules.missing\format @name, table.concat(missing, "\n"), downloadHint - return false, errorMsg + return msgs.formatVersionErrorTemplate.outdated\format name, version, reqVersion, url, reason + else + reqVersion = reqVersion and " (v#{reqVersion})" or "" + return msgs.formatVersionErrorTemplate.missing\format name, reqVersion, url, reason + + @createDummyRef = => + return nil if @scriptType != @@ScriptType.Module + -- global module registry allows for circular dependencies: + -- set a dummy reference to this module since this module is not ready + -- when the other one tries to load it (and vice versa) + export LOADED_MODULES = {} unless LOADED_MODULES + unless LOADED_MODULES[@namespace] + @ref = {} + LOADED_MODULES[@namespace] = setmetatable {__depCtrlDummy: true, version: @}, @ref + return true + return false + + @removeDummyRef = => + return nil if @scriptType != @@ScriptType.Module + if LOADED_MODULES[@namespace] and LOADED_MODULES[@namespace].__depCtrlDummy + LOADED_MODULES[@namespace] = nil + return true + return false + + @loadModule = (mdl, usePrivate, reload) => + runInitializer = (ref) -> + return unless type(ref) == "table" and ref.__depCtrlInit + -- Note to future self: don't change this to a class check! When DepCtrl self-updates + -- any managed module initialized before will still use the same instance + if type(ref.version) != "table" or not (ref.version.__class and ref.version.__class.__name == @@__name) + ref.__depCtrlInit @@ + + with mdl + ._missing, ._error = nil + + moduleName = usePrivate and "#{@namespace}.#{mdl.moduleName}" or .moduleName + name = "#{mdl.name or mdl.moduleName}#{usePrivate and ' (Private Copy)' or ''}" + + if .outdated or reload + -- clear old references + package.loaded[moduleName], LOADED_MODULES[moduleName] = nil + + elseif ._ref = LOADED_MODULES[moduleName] + -- module is already loaded, however it may or may not have been loaded by DepCtrl + -- so we have to call any DepCtrl initializer if it hasn't been called yet + runInitializer ._ref + return ._ref + + loaded, res = xpcall require, debug.traceback, moduleName + unless loaded + LOADED_MODULES[moduleName] = nil + res or= "unknown error" + ._missing = res\match "module '.+' not found:" + ._error = res unless ._missing + return nil + + -- set new references + if reload and ._ref and ._ref.__depCtrlDummy + setmetatable ._ref, res + ._ref, LOADED_MODULES[moduleName] = res, res + + -- run DepCtrl initializer if one was specified + runInitializer res + + return mdl._ref -- having this in the with block breaks moonscript + + @loadModules = (modules, addFeeds = {@feed}, skip = @moduleName and {[@moduleName]: true} or {}) => + for mdl in *modules + continue if skip[mdl] + with mdl + ._ref, ._updated, ._missing, ._outdated, ._reason, ._error = nil + + -- try to load private copies of required modules first + ModuleLoader.loadModule @, mdl, true + ModuleLoader.loadModule @, mdl unless ._ref + + -- try to fetch and load a missing module from the web + if ._missing + record = @@{moduleName:.moduleName, name:.name or .moduleName, + version:-1, url:.url, feed:.feed, virtual:true} + ._ref, code, extErr = @@updater\require record, .version, addFeeds, .optional + if ._ref or .optional + ._updated, ._missing = true, false + else + ._reason = @@updater\getUpdaterErrorMsg code, .name or .moduleName, true, true, extErr + -- nuke dummy reference for circular dependencies + LOADED_MODULES[.moduleName] = nil + + -- check if the version requirements are satisfied + -- which is guaranteed for modules updated with \require, so we don't need to check again + if .version and ._ref and not ._updated + record = ._ref.version + unless record + ._error = msgs.loadModules.missingRecord\format .moduleName + continue + + if type(record) != "table" or record.__class != @@ + record = @@ moduleName: .moduleName, version: record, recordType: @@RecordType.Unmanaged + + -- force an update for outdated modules + if not record\checkVersion .version + ref, code, extErr = @@updater\require record, .version, addFeeds + if ref + ._ref = ref + elseif not .optional + ._outdated = true + ._reason = @@updater\getUpdaterErrorMsg code, .name or .moduleName, true, false, extErr + else + -- perform regular update check if we can get a lock without waiting + -- right now we don't care about the result and don't reload the module + -- so the update will not be effective until the user restarts Aegisub + -- or reloads the script + @@updater\scheduleUpdate record + + missing, outdated, moduleError = {}, {}, {} + for mdl in *modules + with mdl + name = .name or .moduleName + if ._missing + missing[#missing+1] = ModuleLoader.formatVersionErrorTemplate @, name, .version, .url, ._reason + elseif ._outdated + outdated[#outdated+1] = ModuleLoader.formatVersionErrorTemplate @, name, .version, .url, ._reason, ._ref + elseif ._error + moduleError[#moduleError+1] = msgs.loadModules.moduleError\format name, ._error + + errorMsg = {} + if #moduleError > 0 + errorMsg[1] = table.concat moduleError, "\n" + if #outdated > 0 + errorMsg[#errorMsg+1] = msgs.loadModules.outdated\format @name, table.concat outdated, "\n" + if #missing > 0 + errorMsg[#errorMsg+1] = msgs.loadModules.missing\format @name, table.concat(missing, "\n"), downloadHint + + return #errorMsg == 0, table.concat(errorMsg, "\n\n") + + @checkOptionalModules = (modules) => + modules = type(modules)=="string" and {[modules]:true} or {mdl,true for mdl in *modules} + missing = [ModuleLoader.formatVersionErrorTemplate @, mdl.moduleName, mdl.version, msl.url, + mdl._reason for mdl in *@requiredModules when mdl.optional and mdl._missing and modules[mdl.name]] + + if #missing>0 + downloadHint = msgs.checkOptionalModules.downloadHint\format @@automationDir.modules + errorMsg = msgs.checkOptionalModules.missing\format @name, table.concat(missing, "\n"), downloadHint + return false, errorMsg return true \ No newline at end of file From f2e1fa86b657889ecec5047e0061f6217efc2c4e Mon Sep 17 00:00:00 2001 From: line0 Date: Sat, 23 May 2026 18:31:52 +0200 Subject: [PATCH 04/15] fix: update failing due to unsupported string index in capitalization function --- modules/DependencyControl/Common.moon | 2 +- modules/DependencyControl/Updater.moon | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/modules/DependencyControl/Common.moon b/modules/DependencyControl/Common.moon index 8de8024..33ffcb7 100644 --- a/modules/DependencyControl/Common.moon +++ b/modules/DependencyControl/Common.moon @@ -15,7 +15,7 @@ class DependencyControlCommon [false]: "update" } - capitalize: (str) -> str[1]\upper! .. str\sub 2 + capitalize: (str) -> (str\sub 1, 1)\upper! .. str\sub 2 } -- Common enums diff --git a/modules/DependencyControl/Updater.moon b/modules/DependencyControl/Updater.moon index ccf5380..45233ff 100644 --- a/modules/DependencyControl/Updater.moon +++ b/modules/DependencyControl/Updater.moon @@ -381,15 +381,17 @@ class UpdateTask extends UpdaterBase @ref = ref else with @record - .name, .version, .virtual = @record.name, SemanticVersioning\toNumber update.version + .name = @record.name + .virtual = false + .version = SemanticVersioning\toNumber update.version @record\writeConfig! @updated = true - @logger\log msgs.performUpdate.updSuccess, @@terms.capitalize(@@terms.isInstall[wasVirtual]), + @logger\log msgs.performUpdate.updSuccess, @@terms.capitalize(@@terms.isInstall[wasVirtual or false]), @@terms.scriptType.singular[@record.scriptType], @record.name, SemanticVersioning\toString @record.version - -- Diplay changelog + -- Display changelog @logger\log update\getChangelog @record, (SemanticVersioning\toNumber oldVer) + 1 @logger\log msgs.performUpdate.reloadNotice From 977f37a0b15f0e8658a4155287e9b25e001fab30 Mon Sep 17 00:00:00 2001 From: line0 Date: Sat, 23 May 2026 18:47:21 +0200 Subject: [PATCH 05/15] test: add first test --- DependencyControl.json | 10 ++++++++-- macros/l0.DependencyControl.Toolbox.moon | 3 +++ modules/DependencyControl/Tests.moon | 11 +++++++++++ 3 files changed, 22 insertions(+), 2 deletions(-) create mode 100644 modules/DependencyControl/Tests.moon diff --git a/DependencyControl.json b/DependencyControl.json index 90690ce..5134a41 100644 --- a/DependencyControl.json +++ b/DependencyControl.json @@ -76,8 +76,8 @@ "fileBaseUrl": "@{fileBaseUrl}v@{version}-@{channel}/modules/@{scriptName}", "channels": { "alpha": { - "version": "0.6.3", - "released": "2016-02-06", + "version": "0.6.4", + "released": "2026-05-23", "default": true, "files": [ { @@ -114,6 +114,12 @@ "name": "/Updater.moon", "url": "@{fileBaseUrl}@{fileName}", "sha1": "A4AE061724E68B2EFBB7495A477263E1746E228A" + }, + { + "name": ".moon", + "type": "test", + "url": "@{fileBaseUrl}/Tests.moon", + "sha1": "1ED8961CAFCADA7E4C04778227EECDC18E509B8D" } ], "requiredModules": [ diff --git a/macros/l0.DependencyControl.Toolbox.moon b/macros/l0.DependencyControl.Toolbox.moon index b757db5..836734f 100644 --- a/macros/l0.DependencyControl.Toolbox.moon +++ b/macros/l0.DependencyControl.Toolbox.moon @@ -220,6 +220,9 @@ macroConfig = -> config\write! +-- required to register DepCtrl test suite +DepCtrl.__class.version\register DepCtrl + depRec\registerMacros{ {"Install Script", "Installs an automation script or module on your system.", install}, {"Update Script", "Manually check and perform updates to any installed script.", update}, diff --git a/modules/DependencyControl/Tests.moon b/modules/DependencyControl/Tests.moon new file mode 100644 index 0000000..af0ff3e --- /dev/null +++ b/modules/DependencyControl/Tests.moon @@ -0,0 +1,11 @@ +DependencyControl = require "l0.DependencyControl" + +DependencyControl.UnitTestSuite "l0.DependencyControl", (DepCtrl) -> + { + Common: { + _description: "Tests for the Common base class providing shared utilities and enums across DependencyControl components." + + capitalizeTerms: (ut) -> + ut\assertEquals DepCtrl.terms.capitalize("hello world"), "Hello world" + } + } From 75b6c43591e1826be7c75882d10a9e13fce9881b Mon Sep 17 00:00:00 2001 From: line0 Date: Sat, 23 May 2026 18:50:02 +0200 Subject: [PATCH 06/15] ci: enforce LF + EOF newline for consistent hash calculation --- .gitattributes | 1 + .vscode/settings.json | 3 + macros/l0.DependencyControl.Toolbox.moon | 2 +- modules/DependencyControl.moon | 2 +- modules/DependencyControl/Common.moon | 84 ++--- modules/DependencyControl/ConfigHandler.moon | 2 +- modules/DependencyControl/FileOps.moon | 2 +- modules/DependencyControl/ModuleLoader.moon | 346 +++++++++--------- modules/DependencyControl/Record.moon | 2 +- .../DependencyControl/SemanticVersioning.moon | 2 +- modules/DependencyControl/UnitTestSuite.moon | 2 +- modules/DependencyControl/UpdateFeed.moon | 2 +- modules/DependencyControl/Updater.moon | 2 +- 13 files changed, 228 insertions(+), 224 deletions(-) create mode 100644 .gitattributes create mode 100644 .vscode/settings.json diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..6313b56 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..d5d0018 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "files.insertFinalNewline": true +} diff --git a/macros/l0.DependencyControl.Toolbox.moon b/macros/l0.DependencyControl.Toolbox.moon index 836734f..0abd5a6 100644 --- a/macros/l0.DependencyControl.Toolbox.moon +++ b/macros/l0.DependencyControl.Toolbox.moon @@ -228,4 +228,4 @@ depRec\registerMacros{ {"Update Script", "Manually check and perform updates to any installed script.", update}, {"Uninstall Script", "Removes an automation script or module from your system.", uninstall}, {"Macro Configuration", "Lets you change per-automation script settings.", macroConfig}, -}, "DependencyControl" \ No newline at end of file +}, "DependencyControl" diff --git a/modules/DependencyControl.moon b/modules/DependencyControl.moon index 8e0132c..111848e 100644 --- a/modules/DependencyControl.moon +++ b/modules/DependencyControl.moon @@ -47,4 +47,4 @@ LOADED_MODULES[rec.moduleName], package.loaded[rec.moduleName] = DependencyContr DependencyControl.updater\scheduleUpdate rec rec\requireModules! -return DependencyControl \ No newline at end of file +return DependencyControl diff --git a/modules/DependencyControl/Common.moon b/modules/DependencyControl/Common.moon index 33ffcb7..00d77d2 100644 --- a/modules/DependencyControl/Common.moon +++ b/modules/DependencyControl/Common.moon @@ -1,42 +1,42 @@ -ffi = require "ffi" - -class DependencyControlCommon - -- Some terms are shared across components - @platform = "#{ffi.os}-#{ffi.arch}" - - @terms = { - scriptType: { - singular: { "automation script", "module" } - plural: { "automation scripts", "modules" } - } - - isInstall: { - [true]: "installation" - [false]: "update" - } - - capitalize: (str) -> (str\sub 1, 1)\upper! .. str\sub 2 - } - - -- Common enums - @RecordType = { - Managed: 1 - Unmanaged: 2 - } - - @ScriptType = { - Automation: 1 - Module: 2 - name: { - legacy: { "macros", "modules" } - canonical: {"automation", "modules"} - } - } - - automationDir: { - aegisub.decode_path("?user/automation/autoload"), - aegisub.decode_path("?user/automation/include") - } - - @testDir = {aegisub.decode_path("?user/automation/tests/DepUnit/macros"), - aegisub.decode_path("?user/automation/tests/DepUnit/modules")} \ No newline at end of file +ffi = require "ffi" + +class DependencyControlCommon + -- Some terms are shared across components + @platform = "#{ffi.os}-#{ffi.arch}" + + @terms = { + scriptType: { + singular: { "automation script", "module" } + plural: { "automation scripts", "modules" } + } + + isInstall: { + [true]: "installation" + [false]: "update" + } + + capitalize: (str) -> (str\sub 1, 1)\upper! .. str\sub 2 + } + + -- Common enums + @RecordType = { + Managed: 1 + Unmanaged: 2 + } + + @ScriptType = { + Automation: 1 + Module: 2 + name: { + legacy: { "macros", "modules" } + canonical: {"automation", "modules"} + } + } + + automationDir: { + aegisub.decode_path("?user/automation/autoload"), + aegisub.decode_path("?user/automation/include") + } + + @testDir = {aegisub.decode_path("?user/automation/tests/DepUnit/macros"), + aegisub.decode_path("?user/automation/tests/DepUnit/modules")} diff --git a/modules/DependencyControl/ConfigHandler.moon b/modules/DependencyControl/ConfigHandler.moon index 11cfacf..6bb9909 100644 --- a/modules/DependencyControl/ConfigHandler.moon +++ b/modules/DependencyControl/ConfigHandler.moon @@ -327,4 +327,4 @@ Reload your automation scripts to generate a new configuration file.]] @userConfig[k] = isTable and @deepCopy(v) or v changesMade = true - return changesMade \ No newline at end of file + return changesMade diff --git a/modules/DependencyControl/FileOps.moon b/modules/DependencyControl/FileOps.moon index 1369a60..75defed 100644 --- a/modules/DependencyControl/FileOps.moon +++ b/modules/DependencyControl/FileOps.moon @@ -304,4 +304,4 @@ class FileOps path = table.concat({dev, dir, file and pathMatch.sep, file}) - return path, dev, dir, file \ No newline at end of file + return path, dev, dir, file diff --git a/modules/DependencyControl/ModuleLoader.moon b/modules/DependencyControl/ModuleLoader.moon index fb5e9e6..de8a0e5 100644 --- a/modules/DependencyControl/ModuleLoader.moon +++ b/modules/DependencyControl/ModuleLoader.moon @@ -1,174 +1,174 @@ --- Note: this is a private API intended to be exclusively for internal DependenyControl use --- Everyting in this class can and will change without any prior notice --- and calling any method is guaranteed to interfere with DepdencyControl operation - -SemanticVersioning = require "l0.DependencyControl.SemanticVersioning" - -class ModuleLoader - msgs = { - checkOptionalModules: { - downloadHint: "Please download the modules in question manually, put them in your %s folder and reload your automation scripts." - missing: "Error: a %s feature you're trying to use requires additional modules that were not found on your system:\n%s\n%s" - } - formatVersionErrorTemplate: { - missing: "— %s %s%s\n—— Reason: %s" - outdated: "— %s (Installed: v%s; Required: v%s)%s\n—— Reason: %s" - } - loadModules: { - missing: "Error: one or more of the modules required by %s could not be found on your system:\n%s\n%s" - missingRecord: "Error: module '%s' is missing a version record." - moduleError: "Error in required module %s:\n%s" - outdated: [[Error: one or more of the modules required by %s are outdated on your system: -%s\nPlease update the modules in question manually and reload your automation scripts.]] - } - } - - @formatVersionErrorTemplate = (name, reqVersion, url, reason, ref) => - url = url and ": #{url}" or "" - if ref +-- Note: this is a private API intended to be exclusively for internal DependenyControl use +-- Everyting in this class can and will change without any prior notice +-- and calling any method is guaranteed to interfere with DepdencyControl operation + +SemanticVersioning = require "l0.DependencyControl.SemanticVersioning" + +class ModuleLoader + msgs = { + checkOptionalModules: { + downloadHint: "Please download the modules in question manually, put them in your %s folder and reload your automation scripts." + missing: "Error: a %s feature you're trying to use requires additional modules that were not found on your system:\n%s\n%s" + } + formatVersionErrorTemplate: { + missing: "— %s %s%s\n—— Reason: %s" + outdated: "— %s (Installed: v%s; Required: v%s)%s\n—— Reason: %s" + } + loadModules: { + missing: "Error: one or more of the modules required by %s could not be found on your system:\n%s\n%s" + missingRecord: "Error: module '%s' is missing a version record." + moduleError: "Error in required module %s:\n%s" + outdated: [[Error: one or more of the modules required by %s are outdated on your system: +%s\nPlease update the modules in question manually and reload your automation scripts.]] + } + } + + @formatVersionErrorTemplate = (name, reqVersion, url, reason, ref) => + url = url and ": #{url}" or "" + if ref version = SemanticVersioning\toNumber ref.version - return msgs.formatVersionErrorTemplate.outdated\format name, version, reqVersion, url, reason - else - reqVersion = reqVersion and " (v#{reqVersion})" or "" - return msgs.formatVersionErrorTemplate.missing\format name, reqVersion, url, reason - - @createDummyRef = => - return nil if @scriptType != @@ScriptType.Module - -- global module registry allows for circular dependencies: - -- set a dummy reference to this module since this module is not ready - -- when the other one tries to load it (and vice versa) - export LOADED_MODULES = {} unless LOADED_MODULES - unless LOADED_MODULES[@namespace] - @ref = {} - LOADED_MODULES[@namespace] = setmetatable {__depCtrlDummy: true, version: @}, @ref - return true - return false - - @removeDummyRef = => - return nil if @scriptType != @@ScriptType.Module - if LOADED_MODULES[@namespace] and LOADED_MODULES[@namespace].__depCtrlDummy - LOADED_MODULES[@namespace] = nil - return true - return false - - @loadModule = (mdl, usePrivate, reload) => - runInitializer = (ref) -> - return unless type(ref) == "table" and ref.__depCtrlInit - -- Note to future self: don't change this to a class check! When DepCtrl self-updates - -- any managed module initialized before will still use the same instance - if type(ref.version) != "table" or not (ref.version.__class and ref.version.__class.__name == @@__name) - ref.__depCtrlInit @@ - - with mdl - ._missing, ._error = nil - - moduleName = usePrivate and "#{@namespace}.#{mdl.moduleName}" or .moduleName - name = "#{mdl.name or mdl.moduleName}#{usePrivate and ' (Private Copy)' or ''}" - - if .outdated or reload - -- clear old references - package.loaded[moduleName], LOADED_MODULES[moduleName] = nil - - elseif ._ref = LOADED_MODULES[moduleName] - -- module is already loaded, however it may or may not have been loaded by DepCtrl - -- so we have to call any DepCtrl initializer if it hasn't been called yet - runInitializer ._ref - return ._ref - - loaded, res = xpcall require, debug.traceback, moduleName - unless loaded - LOADED_MODULES[moduleName] = nil - res or= "unknown error" - ._missing = res\match "module '.+' not found:" - ._error = res unless ._missing - return nil - - -- set new references - if reload and ._ref and ._ref.__depCtrlDummy - setmetatable ._ref, res - ._ref, LOADED_MODULES[moduleName] = res, res - - -- run DepCtrl initializer if one was specified - runInitializer res - - return mdl._ref -- having this in the with block breaks moonscript - - @loadModules = (modules, addFeeds = {@feed}, skip = @moduleName and {[@moduleName]: true} or {}) => - for mdl in *modules - continue if skip[mdl] - with mdl - ._ref, ._updated, ._missing, ._outdated, ._reason, ._error = nil - - -- try to load private copies of required modules first - ModuleLoader.loadModule @, mdl, true - ModuleLoader.loadModule @, mdl unless ._ref - - -- try to fetch and load a missing module from the web - if ._missing - record = @@{moduleName:.moduleName, name:.name or .moduleName, - version:-1, url:.url, feed:.feed, virtual:true} - ._ref, code, extErr = @@updater\require record, .version, addFeeds, .optional - if ._ref or .optional - ._updated, ._missing = true, false - else - ._reason = @@updater\getUpdaterErrorMsg code, .name or .moduleName, true, true, extErr - -- nuke dummy reference for circular dependencies - LOADED_MODULES[.moduleName] = nil - - -- check if the version requirements are satisfied - -- which is guaranteed for modules updated with \require, so we don't need to check again - if .version and ._ref and not ._updated - record = ._ref.version - unless record - ._error = msgs.loadModules.missingRecord\format .moduleName - continue - - if type(record) != "table" or record.__class != @@ - record = @@ moduleName: .moduleName, version: record, recordType: @@RecordType.Unmanaged - - -- force an update for outdated modules - if not record\checkVersion .version - ref, code, extErr = @@updater\require record, .version, addFeeds - if ref - ._ref = ref - elseif not .optional - ._outdated = true - ._reason = @@updater\getUpdaterErrorMsg code, .name or .moduleName, true, false, extErr - else - -- perform regular update check if we can get a lock without waiting - -- right now we don't care about the result and don't reload the module - -- so the update will not be effective until the user restarts Aegisub - -- or reloads the script - @@updater\scheduleUpdate record - - missing, outdated, moduleError = {}, {}, {} - for mdl in *modules - with mdl - name = .name or .moduleName - if ._missing - missing[#missing+1] = ModuleLoader.formatVersionErrorTemplate @, name, .version, .url, ._reason - elseif ._outdated - outdated[#outdated+1] = ModuleLoader.formatVersionErrorTemplate @, name, .version, .url, ._reason, ._ref - elseif ._error - moduleError[#moduleError+1] = msgs.loadModules.moduleError\format name, ._error - - errorMsg = {} - if #moduleError > 0 - errorMsg[1] = table.concat moduleError, "\n" - if #outdated > 0 - errorMsg[#errorMsg+1] = msgs.loadModules.outdated\format @name, table.concat outdated, "\n" - if #missing > 0 - errorMsg[#errorMsg+1] = msgs.loadModules.missing\format @name, table.concat(missing, "\n"), downloadHint - - return #errorMsg == 0, table.concat(errorMsg, "\n\n") - - @checkOptionalModules = (modules) => - modules = type(modules)=="string" and {[modules]:true} or {mdl,true for mdl in *modules} - missing = [ModuleLoader.formatVersionErrorTemplate @, mdl.moduleName, mdl.version, msl.url, - mdl._reason for mdl in *@requiredModules when mdl.optional and mdl._missing and modules[mdl.name]] - - if #missing>0 - downloadHint = msgs.checkOptionalModules.downloadHint\format @@automationDir.modules - errorMsg = msgs.checkOptionalModules.missing\format @name, table.concat(missing, "\n"), downloadHint - return false, errorMsg - return true \ No newline at end of file + return msgs.formatVersionErrorTemplate.outdated\format name, version, reqVersion, url, reason + else + reqVersion = reqVersion and " (v#{reqVersion})" or "" + return msgs.formatVersionErrorTemplate.missing\format name, reqVersion, url, reason + + @createDummyRef = => + return nil if @scriptType != @@ScriptType.Module + -- global module registry allows for circular dependencies: + -- set a dummy reference to this module since this module is not ready + -- when the other one tries to load it (and vice versa) + export LOADED_MODULES = {} unless LOADED_MODULES + unless LOADED_MODULES[@namespace] + @ref = {} + LOADED_MODULES[@namespace] = setmetatable {__depCtrlDummy: true, version: @}, @ref + return true + return false + + @removeDummyRef = => + return nil if @scriptType != @@ScriptType.Module + if LOADED_MODULES[@namespace] and LOADED_MODULES[@namespace].__depCtrlDummy + LOADED_MODULES[@namespace] = nil + return true + return false + + @loadModule = (mdl, usePrivate, reload) => + runInitializer = (ref) -> + return unless type(ref) == "table" and ref.__depCtrlInit + -- Note to future self: don't change this to a class check! When DepCtrl self-updates + -- any managed module initialized before will still use the same instance + if type(ref.version) != "table" or not (ref.version.__class and ref.version.__class.__name == @@__name) + ref.__depCtrlInit @@ + + with mdl + ._missing, ._error = nil + + moduleName = usePrivate and "#{@namespace}.#{mdl.moduleName}" or .moduleName + name = "#{mdl.name or mdl.moduleName}#{usePrivate and ' (Private Copy)' or ''}" + + if .outdated or reload + -- clear old references + package.loaded[moduleName], LOADED_MODULES[moduleName] = nil + + elseif ._ref = LOADED_MODULES[moduleName] + -- module is already loaded, however it may or may not have been loaded by DepCtrl + -- so we have to call any DepCtrl initializer if it hasn't been called yet + runInitializer ._ref + return ._ref + + loaded, res = xpcall require, debug.traceback, moduleName + unless loaded + LOADED_MODULES[moduleName] = nil + res or= "unknown error" + ._missing = res\match "module '.+' not found:" + ._error = res unless ._missing + return nil + + -- set new references + if reload and ._ref and ._ref.__depCtrlDummy + setmetatable ._ref, res + ._ref, LOADED_MODULES[moduleName] = res, res + + -- run DepCtrl initializer if one was specified + runInitializer res + + return mdl._ref -- having this in the with block breaks moonscript + + @loadModules = (modules, addFeeds = {@feed}, skip = @moduleName and {[@moduleName]: true} or {}) => + for mdl in *modules + continue if skip[mdl] + with mdl + ._ref, ._updated, ._missing, ._outdated, ._reason, ._error = nil + + -- try to load private copies of required modules first + ModuleLoader.loadModule @, mdl, true + ModuleLoader.loadModule @, mdl unless ._ref + + -- try to fetch and load a missing module from the web + if ._missing + record = @@{moduleName:.moduleName, name:.name or .moduleName, + version:-1, url:.url, feed:.feed, virtual:true} + ._ref, code, extErr = @@updater\require record, .version, addFeeds, .optional + if ._ref or .optional + ._updated, ._missing = true, false + else + ._reason = @@updater\getUpdaterErrorMsg code, .name or .moduleName, true, true, extErr + -- nuke dummy reference for circular dependencies + LOADED_MODULES[.moduleName] = nil + + -- check if the version requirements are satisfied + -- which is guaranteed for modules updated with \require, so we don't need to check again + if .version and ._ref and not ._updated + record = ._ref.version + unless record + ._error = msgs.loadModules.missingRecord\format .moduleName + continue + + if type(record) != "table" or record.__class != @@ + record = @@ moduleName: .moduleName, version: record, recordType: @@RecordType.Unmanaged + + -- force an update for outdated modules + if not record\checkVersion .version + ref, code, extErr = @@updater\require record, .version, addFeeds + if ref + ._ref = ref + elseif not .optional + ._outdated = true + ._reason = @@updater\getUpdaterErrorMsg code, .name or .moduleName, true, false, extErr + else + -- perform regular update check if we can get a lock without waiting + -- right now we don't care about the result and don't reload the module + -- so the update will not be effective until the user restarts Aegisub + -- or reloads the script + @@updater\scheduleUpdate record + + missing, outdated, moduleError = {}, {}, {} + for mdl in *modules + with mdl + name = .name or .moduleName + if ._missing + missing[#missing+1] = ModuleLoader.formatVersionErrorTemplate @, name, .version, .url, ._reason + elseif ._outdated + outdated[#outdated+1] = ModuleLoader.formatVersionErrorTemplate @, name, .version, .url, ._reason, ._ref + elseif ._error + moduleError[#moduleError+1] = msgs.loadModules.moduleError\format name, ._error + + errorMsg = {} + if #moduleError > 0 + errorMsg[1] = table.concat moduleError, "\n" + if #outdated > 0 + errorMsg[#errorMsg+1] = msgs.loadModules.outdated\format @name, table.concat outdated, "\n" + if #missing > 0 + errorMsg[#errorMsg+1] = msgs.loadModules.missing\format @name, table.concat(missing, "\n"), downloadHint + + return #errorMsg == 0, table.concat(errorMsg, "\n\n") + + @checkOptionalModules = (modules) => + modules = type(modules)=="string" and {[modules]:true} or {mdl,true for mdl in *modules} + missing = [ModuleLoader.formatVersionErrorTemplate @, mdl.moduleName, mdl.version, msl.url, + mdl._reason for mdl in *@requiredModules when mdl.optional and mdl._missing and modules[mdl.name]] + + if #missing>0 + downloadHint = msgs.checkOptionalModules.downloadHint\format @@automationDir.modules + errorMsg = msgs.checkOptionalModules.missing\format @name, table.concat(missing, "\n"), downloadHint + return false, errorMsg + return true diff --git a/modules/DependencyControl/Record.moon b/modules/DependencyControl/Record.moon index 5aef6fb..fc8740b 100644 --- a/modules/DependencyControl/Record.moon +++ b/modules/DependencyControl/Record.moon @@ -312,4 +312,4 @@ class Record extends Common -- automation scripts don't use any subdirectories if (@moduleName or mode == "file") and file\match currPattern toRemove[#toRemove+1] = path - return FileOps.remove toRemove, true, true \ No newline at end of file + return FileOps.remove toRemove, true, true diff --git a/modules/DependencyControl/SemanticVersioning.moon b/modules/DependencyControl/SemanticVersioning.moon index f8b9fb9..09c50f1 100644 --- a/modules/DependencyControl/SemanticVersioning.moon +++ b/modules/DependencyControl/SemanticVersioning.moon @@ -57,4 +57,4 @@ class SemanticVersioning break if precision == part[1] b = bit.band b, mask - return a >= b, b \ No newline at end of file + return a >= b, b diff --git a/modules/DependencyControl/UnitTestSuite.moon b/modules/DependencyControl/UnitTestSuite.moon index 64b9301..7157fdb 100644 --- a/modules/DependencyControl/UnitTestSuite.moon +++ b/modules/DependencyControl/UnitTestSuite.moon @@ -840,4 +840,4 @@ class UnitTestSuite @logger\log msgs.run.success else @logger\log msgs.run.classesFailed, failedCnt, classCnt - return @success, failedCnt > 0 and allFailed or nil \ No newline at end of file + return @success, failedCnt > 0 and allFailed or nil diff --git a/modules/DependencyControl/UpdateFeed.moon b/modules/DependencyControl/UpdateFeed.moon index 5d228db..a196e69 100644 --- a/modules/DependencyControl/UpdateFeed.moon +++ b/modules/DependencyControl/UpdateFeed.moon @@ -257,4 +257,4 @@ class UpdateFeed extends Common @getScript namespace, @@ScriptType.Automation, config, autoChannel getModule: (namespace, config, autoChannel) => - @getScript namespace, @@ScriptType.Module, config, autoChannel \ No newline at end of file + @getScript namespace, @@ScriptType.Module, config, autoChannel diff --git a/modules/DependencyControl/Updater.moon b/modules/DependencyControl/Updater.moon index 45233ff..f9ee8e1 100644 --- a/modules/DependencyControl/Updater.moon +++ b/modules/DependencyControl/Updater.moon @@ -525,4 +525,4 @@ class Updater extends UpdaterBase return false unless @hasLock @hasLock = false @config.c.updaterRunning = false - @config\write! \ No newline at end of file + @config\write! From 7f2530afbc9ac2fde48cd47246af4a9e9050c91c Mon Sep 17 00:00:00 2001 From: line0 Date: Sat, 23 May 2026 20:55:01 +0200 Subject: [PATCH 07/15] build: bump version; update feed --- DependencyControl.json | 66 ++++++++++++++++++------ macros/l0.DependencyControl.Toolbox.moon | 2 +- modules/DependencyControl.moon | 2 +- 3 files changed, 53 insertions(+), 17 deletions(-) diff --git a/DependencyControl.json b/DependencyControl.json index 5134a41..6fe2d04 100644 --- a/DependencyControl.json +++ b/DependencyControl.json @@ -32,28 +32,26 @@ "fileBaseUrl": "@{fileBaseUrl}macros-v@{version}-@{channel}/macros/@{namespace}", "channels": { "alpha": { - "version": "0.1.3", - "released": "2016-01-27", + "version": "0.2.0", + "released": "2026-05-23", "default": true, "files": [ { "name": ".moon", "url": "@{fileBaseUrl}@{fileName}", - "sha1": "3677B2817C3D1FFE86981C8ABCC092B3D2CCEE7B" + "sha1": "5F7E0EEFC89E71F427819EEF69630455C0CC2304" } ], "requiredModules": [ { "moduleName": "l0.DependencyControl", - "version": "0.6.1" + "version": "0.7.0" } ] } }, "changelog": { - "0.1.0": [ - "initial release" - ], + "0.1.0": ["initial release"], "0.1.1": [ "The Install/Uninstall/Update dialogs now sort scripts by name.", "DependencyControl and its requirements no longer appear in the uninstall menu." @@ -63,6 +61,9 @@ ], "0.1.3": [ "Fixed an issue where trying to uninstall an unmanaged script resulted in an error unrelated to the intended error message." + ], + "0.2.0": [ + "Now registers the DepCtrl-internal test suite as a macro." ] } } @@ -76,44 +77,64 @@ "fileBaseUrl": "@{fileBaseUrl}v@{version}-@{channel}/modules/@{scriptName}", "channels": { "alpha": { - "version": "0.6.4", + "version": "0.7.0", "released": "2026-05-23", "default": true, "files": [ { "name": ".moon", "url": "@{fileBaseUrl}@{fileName}", - "sha1": "76C22149258CB1189265A367C1B28046F54F8FB3" + "sha1": "36104C47B776412EBF36AAA00D583180BF4507D5" + }, + { + "name": "/Common.moon", + "url": "@{fileBaseUrl}@{fileName}", + "sha1": "7262886AEB9F106E95697E86FF0D44738415DBA6" }, { "name": "/ConfigHandler.moon", "url": "@{fileBaseUrl}@{fileName}", - "sha1": "97BCD3207FE8158261FA7851057464535FCEFBC6" + "sha1": "1FEC3583C37E4A997E806D5B17A338390657BA53" }, { "name": "/FileOps.moon", "url": "@{fileBaseUrl}@{fileName}", - "sha1": "D999D34DB93BA76EF0E991CEB1CD63F5CC5F8E68" + "sha1": "5A54D4B942F34C005ABC977B7655C2B849EC8889" }, { "name": "/Logger.moon", "url": "@{fileBaseUrl}@{fileName}", - "sha1": "1E479FE95F0DFBEE8B098302AB589F32D0C40A00" + "sha1": "C4980A42A5AE9C8E24BE04DD12006D118606DBA1" + }, + { + "name": "/ModuleLoader.moon", + "url": "@{fileBaseUrl}@{fileName}", + "sha1": "F35D88A9902FF9BC912D34299733D37FC15A36DF" + }, + { + "name": "/Record.moon", + "url": "@{fileBaseUrl}@{fileName}", + "sha1": "796A430D14CACA3E2E15DBDD23F01DC4DC9E4B19" + }, + { + "name": "/SemanticVersioning.moon", + "url": "@{fileBaseUrl}@{fileName}", + "sha1": "C8DE63A2BE75B1135CEED3ED4ADF7025C927706C" }, { "name": "/UnitTestSuite.moon", "url": "@{fileBaseUrl}@{fileName}", - "sha1": "ADAB6EFB05E08A7828DCA01BC1FC43D6482979A1" + "sha1": "BF316812E9ACF6C73570337C2FCA89FD33189A2B" }, { "name": "/UpdateFeed.moon", "url": "@{fileBaseUrl}@{fileName}", - "sha1": "1EE16D9D551FF82C2D7E448F2CD980E528874108" + "sha1": "7B64A01259AAA32E963708AE26BCF090AFC1E0DD" }, { "name": "/Updater.moon", "url": "@{fileBaseUrl}@{fileName}", - "sha1": "A4AE061724E68B2EFBB7495A477263E1746E228A" + "sha1": "6647D7CAB70637E2B961EF334153718B06EA1027" }, { "name": ".moon", @@ -147,6 +168,21 @@ } }, "changelog": { + "0.7.0": [ + "The previously monolithic `DependencyControl.moon` has been broken up into focused sub-modules as groundwork for a future SQLite-based script registry backend: `Record` (version record management), `ModuleLoader` (module loading and dependency resolution), `SemanticVersioning` (version number handling), and `Common` (shared enums and utilities).", + "Script types (automation macros vs. modules) and record types are now represented by proper enums (`ScriptType`, `RecordType`) instead of bare booleans, making the API more explicit and extensible.", + "UpdateFeed: Fixed two regressions caused by the refactoring, both of which caused the update process to fail.", + "Global initialization has been moved into a dedicated setup method, reducing implicit global state for loggers and configuration.", + "DepCtrl now refuses to load if the installed Moonscript is below the minimum required version with a helpful error message directing users to update their Aegisub build.", + "ModuleLoader: Fixed a regression where DepCtrl init hooks were called again on already-initialized modules, causing errors in modules that mutate their exported state on first call (e.g. BadMutex).", + "Common: Fixed a long-standing bug that guaranteed the `capitalize()` function to fail, that was never caught because it was unused until the refactoring.", + "Updater: Fixed a potential issue where a multi-assignment statement could corrupt record fields after an unsuccessful update." + ], + "0.6.4": [ + "Logger: Fixed a crash when `logEx()` is called without format arguments — `msg:format(...)` is now skipped when no varargs are supplied.", + "Logger: `fileBaseName` now falls back to `\"UNKNOWN\"` when `script_namespace` is nil, preventing errors during Logger initialization in contexts where no namespace is available.", + "Logger/UpdateFeed: Fixed chained method calls on file handles (`handle:write():flush()` and `handle:write():close()`) that could silently swallow errors" + ], "0.6.3": [ "Fixed a v0.6.2 regression that caused DependencyControl to fail loading the first time after a scheduled self-update." ], diff --git a/macros/l0.DependencyControl.Toolbox.moon b/macros/l0.DependencyControl.Toolbox.moon index 0abd5a6..ce94b73 100644 --- a/macros/l0.DependencyControl.Toolbox.moon +++ b/macros/l0.DependencyControl.Toolbox.moon @@ -1,6 +1,6 @@ export script_name = "DependencyControl Toolbox" export script_description = "Provides DependencyControl maintenance and configuration tools." -export script_version = "0.1.3" +export script_version = "0.2.0" export script_author = "line0" export script_namespace = "l0.DependencyControl.Toolbox" diff --git a/modules/DependencyControl.moon b/modules/DependencyControl.moon index 111848e..09f6696 100644 --- a/modules/DependencyControl.moon +++ b/modules/DependencyControl.moon @@ -29,7 +29,7 @@ class DependencyControl extends Record rec = DependencyControl{ name: "DependencyControl", - version: "0.6.3", + version: "0.7.0", description: "Provides script management and auto-updating for Aegisub macros and modules.", author: "line0", url: "http://github.com/TypesettingTools/DependencyControl", From ff8495a5d1bb70d3c1f03af51d2b5b1523e77dac Mon Sep 17 00:00:00 2001 From: line0 Date: Sat, 23 May 2026 21:18:53 +0200 Subject: [PATCH 08/15] fix: typo in ModuleLoader breaking optional module presence checks --- modules/DependencyControl/ModuleLoader.moon | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/DependencyControl/ModuleLoader.moon b/modules/DependencyControl/ModuleLoader.moon index de8a0e5..e9ccba1 100644 --- a/modules/DependencyControl/ModuleLoader.moon +++ b/modules/DependencyControl/ModuleLoader.moon @@ -164,7 +164,7 @@ class ModuleLoader @checkOptionalModules = (modules) => modules = type(modules)=="string" and {[modules]:true} or {mdl,true for mdl in *modules} - missing = [ModuleLoader.formatVersionErrorTemplate @, mdl.moduleName, mdl.version, msl.url, + missing = [ModuleLoader.formatVersionErrorTemplate @, mdl.moduleName, mdl.version, mdl.url, mdl._reason for mdl in *@requiredModules when mdl.optional and mdl._missing and modules[mdl.name]] if #missing>0 From d0cbeee4bdc77719c41e679b6924851872f83a08 Mon Sep 17 00:00:00 2001 From: line0 Date: Sat, 23 May 2026 22:50:08 +0200 Subject: [PATCH 09/15] refactor: tighten version number validation in updater --- macros/l0.DependencyControl.Toolbox.moon | 20 ++++++++++-------- modules/DependencyControl/Updater.moon | 26 +++++++++++++----------- 2 files changed, 25 insertions(+), 21 deletions(-) diff --git a/macros/l0.DependencyControl.Toolbox.moon b/macros/l0.DependencyControl.Toolbox.moon index ce94b73..0fe60ea 100644 --- a/macros/l0.DependencyControl.Toolbox.moon +++ b/macros/l0.DependencyControl.Toolbox.moon @@ -78,12 +78,14 @@ getScriptListDlg = (macros, modules) -> {name: "module", class: "dropdown", x: 1, y: 1, width: 1, height: 1, items: modules, value: "" } } -runUpdaterTask = (scriptData, exhaustive) -> +runUpdaterTask = (scriptData, exhaustive, isInstall) -> return unless scriptData - task, err = DepCtrl.updater\addTask scriptData, nil, nil, exhaustive, scriptData.channel - if task then task\run! - else logger\log err - + + task, code, extErr = DepCtrl.updater\addTask scriptData, nil, nil, exhaustive, scriptData.channel + return task\run! if task + with scriptData + logger\log DepCtrl.updater\getUpdaterErrorMsg code, .moduleName or .name, + .moduleName and DepCtrl.ScriptType.Module or DepCtrl.ScriptType.Automation, isInstall, extErr -- Macros @@ -139,8 +141,8 @@ install = -> -- create and run the update tasks macro, mdl = macroMap[res.macro], moduleMap[res.module] - runUpdaterTask mdl, false - runUpdaterTask macro, false + runUpdaterTask mdl, false, true + runUpdaterTask macro, false, true uninstall = -> doUninstall = (script) -> @@ -190,8 +192,8 @@ update = -> -- create and run the update tasks macro, mdl = macroMap[res.macro], moduleMap[res.module] - runUpdaterTask mdl, res.exhaustive - runUpdaterTask macro, res.exhaustive + runUpdaterTask mdl, res.exhaustive, false + runUpdaterTask macro, res.exhaustive, false macroConfig = -> config = getConfig "macros" diff --git a/modules/DependencyControl/Updater.moon b/modules/DependencyControl/Updater.moon index f9ee8e1..c3c4c39 100644 --- a/modules/DependencyControl/Updater.moon +++ b/modules/DependencyControl/Updater.moon @@ -22,6 +22,7 @@ class UpdaterBase extends Common [6]: "The %s of %s '%s' failed because no suitable package could be found %s." [5]: "Skipped %s of %s '%s': Another update initiated by %s is already running." [7]: "Skipped %s of %s '%s': An internet connection is currently not available." + [8]: "Couldn't %s %s '%s' because the requested version is invalid: %s" [10]: "Skipped %s of %s '%s': the update task is already running." [15]: "Couldn't %s %s '%s' because its requirements could not be satisfied:" [30]: "Couldn't %s %s '%s': failed to create temporary download directory %s" @@ -91,14 +92,15 @@ class UpdateTask extends UpdaterBase } } - new: (@record, targetVersion = 0, @addFeeds, @exhaustive, @channel, @optional, @updater) => + new: (@record, targetVersionNumber = 0, @addFeeds, @exhaustive, @channel, @optional, @updater) => DependencyControl or= require "l0.DependencyControl" assert @record.__class == DependencyControl, "First parameter must be a #{DependencyControl.__name} object." + assert type(targetVersionNumber) == "number", "Second parameter must be a semantic version number in integer format." @logger = @updater.logger @triedFeeds = {} @status = nil - @targetVersion = SemanticVersioning\toNumber targetVersion + @targetVersion = targetVersionNumber -- set UpdateFeed settings @feedConfig = { @@ -109,10 +111,6 @@ class UpdateTask extends UpdaterBase return nil, -1 unless @updater.config.c.updaterEnabled -- TODO: check if this even works return nil, -2 unless @record\validateNamespace! - set: (targetVersion, @addFeeds, @exhaustive, @channel, @optional) => - @targetVersion = SemanticVersioning\toNumber targetVersion - return @ - checkFeed: (feedUrl) => -- get feed contents feed = UpdateFeed feedUrl, false, nil, @feedConfig, @logger @@ -439,18 +437,22 @@ class Updater extends UpdaterBase depRec[k] = v for k, v in pairs record record = DependencyControl depRec + targetVersionNumber, err = SemanticVersioning\toNumber targetVersion + if (err) then return nil, -8, err + task = @tasks[record.scriptType][record.namespace] - if task - return task\set targetVersion, addFeeds, exhaustive, channel, optional - else - task, err = UpdateTask record, targetVersion, addFeeds, exhaustive, channel, optional, @ + return if task then with task + .targetVersion = targetVersionNumber + .addFeeds, .exhaustive, .channel, .optional = addFeeds, exhaustive, channel, optional + + task, code = UpdateTask record, targetVersionNumber, addFeeds, exhaustive, channel, optional, @ @tasks[record.scriptType][record.namespace] = task - return task, err + return task, code require: (record, ...) => @logger\assert record.scriptType == @@ScriptType.Module, msgs.require, record.name or record.namespace @logger\log "%s module '%s'...", record.virtual and "Installing required" or "Updating outdated", record.name - task, code = @addTask record, ... + task, code, res = @addTask record, ... code, res = task\run true if task if code == 0 and not task.updated From d16fd6416f55401de6d029a63342b8bb53770de8 Mon Sep 17 00:00:00 2001 From: line0 Date: Sat, 23 May 2026 23:34:20 +0200 Subject: [PATCH 10/15] fix: broken version number display in version errors --- modules/DependencyControl/ModuleLoader.moon | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/DependencyControl/ModuleLoader.moon b/modules/DependencyControl/ModuleLoader.moon index e9ccba1..be57a8c 100644 --- a/modules/DependencyControl/ModuleLoader.moon +++ b/modules/DependencyControl/ModuleLoader.moon @@ -26,7 +26,8 @@ class ModuleLoader @formatVersionErrorTemplate = (name, reqVersion, url, reason, ref) => url = url and ": #{url}" or "" if ref - version = SemanticVersioning\toNumber ref.version + -- unmanaged records have refs whose .version is a string instead of a DepCtrl record + version = SemanticVersioning\toString type(ref.version) == "table" and ref.version.version or ref.version return msgs.formatVersionErrorTemplate.outdated\format name, version, reqVersion, url, reason else reqVersion = reqVersion and " (v#{reqVersion})" or "" From 167d427215d9cc4a668edb047dcd055b7c086593 Mon Sep 17 00:00:00 2001 From: line0 Date: Sat, 23 May 2026 23:34:51 +0200 Subject: [PATCH 11/15] fix: typos --- macros/l0.DependencyControl.Toolbox.moon | 2 +- modules/DependencyControl/ModuleLoader.moon | 4 ++-- modules/DependencyControl/Updater.moon | 10 +++++----- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/macros/l0.DependencyControl.Toolbox.moon b/macros/l0.DependencyControl.Toolbox.moon index 0fe60ea..7e674ef 100644 --- a/macros/l0.DependencyControl.Toolbox.moon +++ b/macros/l0.DependencyControl.Toolbox.moon @@ -16,7 +16,7 @@ msgs = { } uninstall: { running: "Uninstalling %s '%s'..." - success: "%s '%s' was removed sucessfully. Reload your automation scripts or restart Aegisub for the changes to take effect." + success: "%s '%s' was removed successfully. Reload your automation scripts or restart Aegisub for the changes to take effect." lockedFiles: "%s Some script files are still in use and will be deleted during the next restart/reload:\n%s" error: "Error: %s" } diff --git a/modules/DependencyControl/ModuleLoader.moon b/modules/DependencyControl/ModuleLoader.moon index be57a8c..d2db654 100644 --- a/modules/DependencyControl/ModuleLoader.moon +++ b/modules/DependencyControl/ModuleLoader.moon @@ -1,6 +1,6 @@ -- Note: this is a private API intended to be exclusively for internal DependenyControl use --- Everyting in this class can and will change without any prior notice --- and calling any method is guaranteed to interfere with DepdencyControl operation +-- Everything in this class can and will change without any prior notice +-- and calling any method is guaranteed to interfere with DependencyControl operation SemanticVersioning = require "l0.DependencyControl.SemanticVersioning" diff --git a/modules/DependencyControl/Updater.moon b/modules/DependencyControl/Updater.moon index c3c4c39..c336ee7 100644 --- a/modules/DependencyControl/Updater.moon +++ b/modules/DependencyControl/Updater.moon @@ -29,9 +29,9 @@ class UpdaterBase extends Common [35]: "Aborted %s of %s '%s' because the feed contained a missing or malformed SHA-1 hash for file %s." [50]: "Couldn't finish %s of %s '%s' because some files couldn't be moved to their target location:\n" [55]: "%s of %s '%s' succeeded, couldn't be located by the module loader." - [56]: "%s of %s '%s' succeeded, but an error occured while loading the module:\n%s" + [56]: "%s of %s '%s' succeeded, but an error occurred while loading the module:\n%s" [57]: "%s of %s '%s' succeeded, but it's missing a version record." - [58]: "%s of unmanaged %s '%s' succeeded, but an error occured while creating a DependencyControl record: %s" + [58]: "%s of unmanaged %s '%s' succeeded, but an error occurred while creating a DependencyControl record: %s", [100]: "Error (%d) in component %s during %s of %s '%s':\n— %s" } updaterErrorComponent: {"DownloadManager (adding download)", "DownloadManager"} @@ -87,8 +87,8 @@ class UpdateTask extends UpdaterBase unknownType: "Skipping file '%s': unknown type '%s'." } refreshRecord: { - unsetVirtual: "Update initated by another macro already fetched %s '%s', switching to update mode." - otherUpdate: "Update initated by another macro already updated %s '%s' to v%s." + unsetVirtual: "Update initiated by another macro already fetched %s '%s', switching to update mode." + otherUpdate: "Update initiated by another macro already updated %s '%s' to v%s." } } @@ -446,7 +446,7 @@ class Updater extends UpdaterBase .addFeeds, .exhaustive, .channel, .optional = addFeeds, exhaustive, channel, optional task, code = UpdateTask record, targetVersionNumber, addFeeds, exhaustive, channel, optional, @ - @tasks[record.scriptType][record.namespace] = task + @tasks[record.scriptType][record.namespace] = task return task, code require: (record, ...) => From a1fc3f89fe300f5577e9b134f6aadccc7da3269d Mon Sep 17 00:00:00 2001 From: line0 Date: Sun, 24 May 2026 00:03:37 +0200 Subject: [PATCH 12/15] fix: semver parsers allowing invalid values number segments over 255 and non-dot separate values --- modules/DependencyControl/SemanticVersioning.moon | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/DependencyControl/SemanticVersioning.moon b/modules/DependencyControl/SemanticVersioning.moon index 09c50f1..88f4bd8 100644 --- a/modules/DependencyControl/SemanticVersioning.moon +++ b/modules/DependencyControl/SemanticVersioning.moon @@ -3,7 +3,7 @@ class SemanticVersioning toNumber: { badString: "Can't parse version string '%s'. Make sure it conforms to semantic versioning standards." badType: "Argument had the wrong type: expected a string or number, got a %s." - overflow: "Error: %s version must be an integer < 255, got %s." + overflow: "Error: %s version must be an integer <= 255, got %s." } } @@ -26,14 +26,14 @@ class SemanticVersioning when "number" then math.max value, 0 when "nil" then 0 when "string" - matches = {value\match "^(%d+).(%d+).(%d+)$"} + matches = {value\match "^(%d+)%.(%d+)%.(%d+)$"} if #matches != 3 return false, msgs.toNumber.badString\format value version = 0 for i, part in ipairs semParts value = tonumber matches[i] - if type(value) != "number" or value > 256 + if type(value) != "number" or value > 255 return false, msgs.toNumber.overflow\format part[1], tostring value version += bit.lshift value, part[2] From 5bc3f451109632d98108b34e71c66e9d71b957c0 Mon Sep 17 00:00:00 2001 From: line0 Date: Sun, 24 May 2026 00:08:00 +0200 Subject: [PATCH 13/15] fix: semantic version toString roundtrip exception when invalid semver string is passed --- modules/DependencyControl/SemanticVersioning.moon | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/DependencyControl/SemanticVersioning.moon b/modules/DependencyControl/SemanticVersioning.moon index 88f4bd8..fd410bc 100644 --- a/modules/DependencyControl/SemanticVersioning.moon +++ b/modules/DependencyControl/SemanticVersioning.moon @@ -11,7 +11,8 @@ class SemanticVersioning @toString = (version, precision = "patch") => if type(version) == "string" - version = @toNumber version + version, err = @toNumber version + return nil, err unless version parts = {0, 0, 0} for i, part in ipairs semParts From c08a79cc4bcd2aaa78becefd7516b7230cbb82e7 Mon Sep 17 00:00:00 2001 From: line0 Date: Sun, 24 May 2026 08:14:46 +0200 Subject: [PATCH 14/15] fix: update errors missing information about whether or not a record is virtual --- modules/DependencyControl/Updater.moon | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/DependencyControl/Updater.moon b/modules/DependencyControl/Updater.moon index c336ee7..c2a8248 100644 --- a/modules/DependencyControl/Updater.moon +++ b/modules/DependencyControl/Updater.moon @@ -148,7 +148,7 @@ class UpdateTask extends UpdaterBase run: (waitLock, exhaustive = @updater.config.c.tryAllFeeds or @@exhaustive) => - logUpdateError = (code, extErr, virtual = @virtual) -> + logUpdateError = (code, extErr, virtual = @record.virtual) -> if code < 0 @logger\log @getUpdaterErrorMsg code, @record.name, @record.scriptType, virtual, extErr return code, extErr From 80ead7c18abb0646bbb099ee9f84799f8e3c6db8 Mon Sep 17 00:00:00 2001 From: line0 Date: Sun, 24 May 2026 08:25:50 +0200 Subject: [PATCH 15/15] fix: broken skip lists in module loader nothing inside DepCtrl is supplying this parameter, so the only effect this bug would have had, is removing the default guard against modules trying to load themselves --- modules/DependencyControl/ModuleLoader.moon | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/DependencyControl/ModuleLoader.moon b/modules/DependencyControl/ModuleLoader.moon index d2db654..689eee6 100644 --- a/modules/DependencyControl/ModuleLoader.moon +++ b/modules/DependencyControl/ModuleLoader.moon @@ -96,7 +96,7 @@ class ModuleLoader @loadModules = (modules, addFeeds = {@feed}, skip = @moduleName and {[@moduleName]: true} or {}) => for mdl in *modules - continue if skip[mdl] + continue if skip[mdl.moduleName] with mdl ._ref, ._updated, ._missing, ._outdated, ._reason, ._error = nil