From 3c86e2f0fb08ee8ed01b0f8001b6014a41c647e0 Mon Sep 17 00:00:00 2001 From: Ryan Lucia Date: Fri, 11 Sep 2026 17:29:29 -0700 Subject: [PATCH 1/4] Load SubInspector directly through LuaJIT FFI --- .github/workflows/ci.yml | 6 +- examples/Aegisub/Inspector.moon | 32 +++++++-- tests/meson.build | 16 +++++ tests/wrapper.lua | 122 ++++++++++++++++++++++++++++++++ tests/wrapper.py | 16 +++++ 5 files changed, 185 insertions(+), 7 deletions(-) create mode 100644 tests/wrapper.lua create mode 100644 tests/wrapper.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 84391c1..14e7e61 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -81,12 +81,16 @@ jobs: if: runner.os == 'Linux' run: | sudo apt-get update - sudo apt-get install -y build-essential pkg-config nasm libass-dev fonts-dejavu-core + sudo apt-get install -y build-essential pkg-config nasm libass-dev fonts-dejavu-core luajit luarocks liblua5.1-0-dev + sudo luarocks --lua-version=5.1 install moonscript 0.6.0 - name: Configure run: meson setup build ${{ matrix.args }} - name: Build run: meson compile -C build + - name: Check Lua wrapper tests are enabled + if: runner.os == 'Linux' + run: meson test -C build --list | grep -q aegisub-wrapper - name: Test run: meson test -C build --print-errorlogs - name: Check binary architecture and runtime dependencies diff --git a/examples/Aegisub/Inspector.moon b/examples/Aegisub/Inspector.moon index a362fef..6e26223 100644 --- a/examples/Aegisub/Inspector.moon +++ b/examples/Aegisub/Inspector.moon @@ -1,5 +1,5 @@ -- This library is unlicensed under CC0 -local requireffi, ffi, looseVersionCompare +local ffi, looseVersionCompare versionRecord = '0.7.2' haveDepCtrl, DependencyControl = pcall require, 'l0.DependencyControl' @@ -15,7 +15,6 @@ if haveDepCtrl feed: "https://raw.githubusercontent.com/TypesettingTools/SubInspector/master/DependencyControl.json", { { "ffi" } - { "requireffi.requireffi", version: "0.1.1" } } } ) @@ -34,11 +33,10 @@ if haveDepCtrl return true - ffi, requireffi = versionRecord\requireModules! + ffi = versionRecord\requireModules! else ffi = require 'ffi' - requireffi = require 'requireffi.requireffi' SIVersionCompat = 0x000501 versionComponents = ( version ) -> @@ -73,7 +71,29 @@ int si_calculateBounds( void*, SI_Rect*, const int32_t*, const uint32_t void si_cleanup( void* ); ]] ) -SubInspector, libraryPath = requireffi( 'SubInspector.Inspector.SubInspector' ) +loadLibrary = -> + filename = switch ffi.os + when 'Windows' then 'SubInspector.dll' + when 'OSX' then 'libSubInspector.dylib' + else 'libSubInspector.so' + + paths = { } + for template in package.path\gmatch '[^;]+' + template = template\gsub '\\', '/' + root = template\match('^(.-)%?%.lua$') or template\match('^(.-)%?%.moon$') + if root + table.insert paths, root .. 'SubInspector/Inspector/' .. filename + -- Package-manager installs can use the system's native library search path. + table.insert paths, 'SubInspector' + + errors = { 'Could not load the SubInspector library:' } + for path in *paths + success, library = pcall ffi.load, path + return library if success + table.insert errors, ' ' .. path .. ': ' .. tostring library + error table.concat errors, '\n' + +SubInspector = loadLibrary! log = ( message, ... ) -> aegisub.log 2, message .. '\n', ... @@ -198,7 +218,7 @@ addStyles = ( line, scriptText, seenStyles ) => class Inspector @version = versionRecord - new: ( subtitles = error( "You must provide the subtitles object." ), fcConfig = libraryPath .. "fonts.conf", fontDir = aegisub.decode_path( '?script/fonts' ), logFunc = log ) => + new: ( subtitles = error( "You must provide the subtitles object." ), fcConfig = nil, fontDir = aegisub.decode_path( '?script/fonts' ), logFunc = log ) => success, message = looseVersionCompare SubInspector.si_getVersion! assert success, message diff --git a/tests/meson.build b/tests/meson.build index 961a04d..d9e6db3 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -2,3 +2,19 @@ render_test = executable('render-test', 'render.c', include_directories: include_directories('../src'), link_with: subinspector) test('render', render_test) + +if not meson.is_cross_build() + luajit = find_program('luajit', required: false) + moonc = find_program('moonc', required: false) + if luajit.found() and moonc.found() + wrapper = custom_target('Inspector.lua', + input: '../examples/Aegisub/Inspector.moon', + output: 'Inspector.lua', + command: [moonc, '-o', '@OUTPUT@', '@INPUT@']) + python = import('python').find_installation() + test('aegisub-wrapper', python, + args: [files('wrapper.py'), luajit.full_path(), wrapper, + subinspector.full_path(), files('wrapper.lua')], + depends: subinspector) + endif +endif diff --git a/tests/wrapper.lua b/tests/wrapper.lua new file mode 100644 index 0000000..61be1b8 --- /dev/null +++ b/tests/wrapper.lua @@ -0,0 +1,122 @@ +local wrapper, include = arg[1], arg[2] +local ffi = require('ffi') + +local function load_wrapper(test_ffi, search_path, depctrl) + local registered + local env = setmetatable({ + package = {path = search_path}, + aegisub = { + decode_path = function() return include .. '/fonts' end, + video_size = function() return nil end, + log = function() end, + }, + require = function(name) + if name == 'ffi' then return test_ffi end + if name == 'l0.DependencyControl' and depctrl then + return function(record) + record.requireModules = function(self) + assert(#self[1] == 1 and self[1][1][1] == 'ffi') + return test_ffi + end + record.checkVersion = function() return true end + record.register = function(_, module) + registered = module + return module + end + return record + end + end + error('Unexpected dependency: ' .. name) + end, + }, {__index = _G}) + local module = setfenv(assert(loadfile(wrapper)), env)() + if depctrl then assert(registered == module) end + return module +end + +-- Exercise all platform names and path conventions without loading foreign +-- binaries. These cases also run without DependencyControl or requireffi. +local cases = { + {'Windows', 'C:\\Users\\Test Person\\include\\?.lua', + 'C:/Users/Test Person/include/SubInspector/Inspector/SubInspector.dll'}, + {'OSX', '/missing/?.lua;/Application Support/include/?.moon', + '/Application Support/include/SubInspector/Inspector/libSubInspector.dylib'}, + {'Linux', '/usr/share/aegisub/automation/include/?.lua', + '/usr/share/aegisub/automation/include/SubInspector/Inspector/libSubInspector.so'}, + {'Linux', '/missing/?.lua;/missing/?/init.lua', 'SubInspector'}, +} +for _, case in ipairs(cases) do + for _, depctrl in ipairs({false, true}) do + local calls = {} + load_wrapper({ + os = case[1], + cdef = function() end, + load = function(path) + calls[#calls + 1] = path + if path == case[3] then return {} end + error('library missing or incompatible') + end, + }, case[2], depctrl) + assert(calls[#calls] == case[3]) + for _, path in ipairs(calls) do + assert(not path:find('init.lua', 1, true)) + end + end +end + +local ok, message = pcall(load_wrapper, { + os = 'OSX', + cdef = function() end, + load = function() error('wrong architecture') end, +}, '/missing/?.lua', false) +assert(not ok) +assert(message:find('/missing/SubInspector/Inspector/libSubInspector.dylib', 1, true)) +assert(message:find('SubInspector: ', 1, true)) +assert(message:find('wrong architecture', 1, true)) + +-- Load the real staged library and exercise the wrapper-to-C interface, both +-- with and without DependencyControl registration. +local declared = false +for _, depctrl in ipairs({false, true}) do + local loaded_path, config + local test_ffi = setmetatable({ + cdef = function(definitions) + if not declared then ffi.cdef(definitions); declared = true end + end, + load = function(path) + local library = ffi.load(path) + loaded_path = path + return setmetatable({ + si_init = function(width, height, font_config, fonts) + config = font_config + return library.si_init(width, height, font_config, fonts) + end, + }, {__index = function(_, key) return library[key] end}) + end, + }, {__index = ffi}) + local Inspector = load_wrapper(test_ffi, include .. '/?.lua', depctrl) + assert(loaded_path:find(include .. '/SubInspector/Inspector/', 1, true) == 1) + local subtitles = { + {class = 'info', key = 'ScriptType', value = 'v4.00+', raw = 'ScriptType: v4.00+'}, + {class = 'info', key = 'PlayResX', value = '640', raw = 'PlayResX: 640'}, + {class = 'info', key = 'PlayResY', value = '480', raw = 'PlayResY: 480'}, + {class = 'style', name = 'Default', raw = + 'Style: Default,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,' .. + '0,0,0,0,100,100,0,0,1,0,0,7,0,0,0,1'}, + } + local inspector = Inspector(subtitles) + assert(config == nil and inspector.fcConfig == nil) + local shape = '{\\an7\\pos(100,100)\\p1}m 0 0 l 20 0 20 20 0 20' + local rects = assert(inspector:getBounds({{ + style = 'Default', text = shape, + raw = 'Dialogue: 0,0:00:00.00,0:00:02.00,Default,,0,0,0,,' .. shape, + }}, {0, 500})) + assert(#rects == 2) + assert(rects[1].x == 100 and rects[1].y == 100) + assert(rects[1].w == 20 and rects[1].h == 20 and rects[1].solid) + assert(rects[1].hash == rects[2].hash) + local configured = Inspector(subtitles, 'custom-fonts.conf') + assert(config == 'custom-fonts.conf' and configured.fcConfig == config) +end + +print('Library discovery, errors, DependencyControl registration, and wrapper rendering passed.') diff --git a/tests/wrapper.py b/tests/wrapper.py new file mode 100644 index 0000000..a48a388 --- /dev/null +++ b/tests/wrapper.py @@ -0,0 +1,16 @@ +"""Stage a normal Aegisub include directory and run the LuaJIT wrapper tests.""" + +import shutil +import subprocess +import sys +import tempfile +from pathlib import Path + +luajit, wrapper, binary, tests = sys.argv[1:] +with tempfile.TemporaryDirectory(prefix='SubInspector wrapper ') as directory: + include = Path(directory) / 'include' + libraries = include / 'SubInspector' / 'Inspector' + libraries.mkdir(parents=True) + (include / 'fonts').mkdir() + shutil.copy2(binary, libraries / Path(binary).name) + subprocess.run([luajit, tests, wrapper, str(include)], check=True) From 36f234b4a2f96b8605a8368928f244f9f1448504 Mon Sep 17 00:00:00 2001 From: Ryan Lucia Date: Fri, 11 Sep 2026 18:04:55 -0700 Subject: [PATCH 2/4] Test the LuaJIT wrapper on native macOS and Windows Address review feedback by requiring wrapper tests on all native CI jobs and normalizing the expected Windows library path. Remove the redundant nil constructor default. --- .github/workflows/ci.yml | 27 ++++++++++++++++++++++++++- examples/Aegisub/Inspector.moon | 2 +- tests/wrapper.lua | 3 ++- 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 14e7e61..b688e40 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -74,6 +74,30 @@ jobs: - name: Install NASM (macOS) if: runner.os == 'macOS' run: brew install nasm + - name: Install Lua wrapper test tools (macOS) + if: runner.os == 'macOS' && matrix.arch == 'arm64' + run: | + brew install luajit luarocks + luarocks --lua-version=5.1 --lua-dir="$(brew --prefix luajit)" --tree="$RUNNER_TEMP/luarocks" install moonscript 0.6.0 + echo "$RUNNER_TEMP/luarocks/bin" >> "$GITHUB_PATH" + - name: Install Lua wrapper test tools (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + git clone --depth 1 --branch v2.1 https://github.com/LuaJIT/LuaJIT.git "$env:RUNNER_TEMP\luajit" + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + Push-Location "$env:RUNNER_TEMP\luajit\src" + cmd /c msvcbuild.bat + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + Pop-Location + "$env:RUNNER_TEMP\luajit\src" >> $env:GITHUB_PATH + $archive = "$env:RUNNER_TEMP\moonscript.zip" + Invoke-WebRequest https://github.com/leafo/moonscript/releases/download/v0.6.0/moonscript-v0.6.0-windows-x86_64.zip -OutFile $archive + if ((Get-FileHash $archive -Algorithm SHA256).Hash -ne 'd91b2aec25caf71a551268c1d50d635708e556641ba0e19dd7f24d4a68d93e7c') { + throw 'MoonScript archive checksum mismatch' + } + Expand-Archive $archive "$env:RUNNER_TEMP\moonscript" + "$env:RUNNER_TEMP\moonscript" >> $env:GITHUB_PATH - name: Enable Intel tests through Rosetta if: runner.os == 'macOS' && matrix.arch == 'x86_64' run: sudo softwareupdate --install-rosetta --agree-to-license @@ -89,7 +113,8 @@ jobs: - name: Build run: meson compile -C build - name: Check Lua wrapper tests are enabled - if: runner.os == 'Linux' + if: runner.os != 'macOS' || matrix.arch != 'x86_64' + shell: bash run: meson test -C build --list | grep -q aegisub-wrapper - name: Test run: meson test -C build --print-errorlogs diff --git a/examples/Aegisub/Inspector.moon b/examples/Aegisub/Inspector.moon index 6e26223..acdd116 100644 --- a/examples/Aegisub/Inspector.moon +++ b/examples/Aegisub/Inspector.moon @@ -218,7 +218,7 @@ addStyles = ( line, scriptText, seenStyles ) => class Inspector @version = versionRecord - new: ( subtitles = error( "You must provide the subtitles object." ), fcConfig = nil, fontDir = aegisub.decode_path( '?script/fonts' ), logFunc = log ) => + new: ( subtitles = error( "You must provide the subtitles object." ), fcConfig, fontDir = aegisub.decode_path( '?script/fonts' ), logFunc = log ) => success, message = looseVersionCompare SubInspector.si_getVersion! assert success, message diff --git a/tests/wrapper.lua b/tests/wrapper.lua index 61be1b8..2664a6d 100644 --- a/tests/wrapper.lua +++ b/tests/wrapper.lua @@ -95,7 +95,8 @@ for _, depctrl in ipairs({false, true}) do end, }, {__index = ffi}) local Inspector = load_wrapper(test_ffi, include .. '/?.lua', depctrl) - assert(loaded_path:find(include .. '/SubInspector/Inspector/', 1, true) == 1) + local expected_root = include:gsub('\\', '/') .. '/SubInspector/Inspector/' + assert(loaded_path:find(expected_root, 1, true) == 1) local subtitles = { {class = 'info', key = 'ScriptType', value = 'v4.00+', raw = 'ScriptType: v4.00+'}, {class = 'info', key = 'PlayResX', value = '640', raw = 'PlayResX: 640'}, From fbe0a993709e92cc6c95701a237e4380b5af22ca Mon Sep 17 00:00:00 2001 From: Ryan Lucia Date: Fri, 11 Sep 2026 18:08:27 -0700 Subject: [PATCH 3/4] Move Lua test tool setup out of the workflow --- .github/workflows/ci.yml | 30 ++++------------------- tools/ci/setup-lua.py | 51 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 26 deletions(-) create mode 100644 tools/ci/setup-lua.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b688e40..24168af 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -74,30 +74,6 @@ jobs: - name: Install NASM (macOS) if: runner.os == 'macOS' run: brew install nasm - - name: Install Lua wrapper test tools (macOS) - if: runner.os == 'macOS' && matrix.arch == 'arm64' - run: | - brew install luajit luarocks - luarocks --lua-version=5.1 --lua-dir="$(brew --prefix luajit)" --tree="$RUNNER_TEMP/luarocks" install moonscript 0.6.0 - echo "$RUNNER_TEMP/luarocks/bin" >> "$GITHUB_PATH" - - name: Install Lua wrapper test tools (Windows) - if: runner.os == 'Windows' - shell: pwsh - run: | - git clone --depth 1 --branch v2.1 https://github.com/LuaJIT/LuaJIT.git "$env:RUNNER_TEMP\luajit" - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - Push-Location "$env:RUNNER_TEMP\luajit\src" - cmd /c msvcbuild.bat - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - Pop-Location - "$env:RUNNER_TEMP\luajit\src" >> $env:GITHUB_PATH - $archive = "$env:RUNNER_TEMP\moonscript.zip" - Invoke-WebRequest https://github.com/leafo/moonscript/releases/download/v0.6.0/moonscript-v0.6.0-windows-x86_64.zip -OutFile $archive - if ((Get-FileHash $archive -Algorithm SHA256).Hash -ne 'd91b2aec25caf71a551268c1d50d635708e556641ba0e19dd7f24d4a68d93e7c') { - throw 'MoonScript archive checksum mismatch' - } - Expand-Archive $archive "$env:RUNNER_TEMP\moonscript" - "$env:RUNNER_TEMP\moonscript" >> $env:GITHUB_PATH - name: Enable Intel tests through Rosetta if: runner.os == 'macOS' && matrix.arch == 'x86_64' run: sudo softwareupdate --install-rosetta --agree-to-license @@ -105,8 +81,10 @@ jobs: if: runner.os == 'Linux' run: | sudo apt-get update - sudo apt-get install -y build-essential pkg-config nasm libass-dev fonts-dejavu-core luajit luarocks liblua5.1-0-dev - sudo luarocks --lua-version=5.1 install moonscript 0.6.0 + sudo apt-get install -y build-essential pkg-config nasm libass-dev fonts-dejavu-core + - name: Install Lua wrapper test tools + if: runner.os != 'macOS' || matrix.arch != 'x86_64' + run: python tools/ci/setup-lua.py - name: Configure run: meson setup build ${{ matrix.args }} diff --git a/tools/ci/setup-lua.py b/tools/ci/setup-lua.py new file mode 100644 index 0000000..b549a52 --- /dev/null +++ b/tools/ci/setup-lua.py @@ -0,0 +1,51 @@ +"""Install LuaJIT and MoonScript for the native Aegisub wrapper tests in CI.""" + +import hashlib +import os +import subprocess +import sys +import urllib.request +import zipfile +from pathlib import Path + + +def run(*args, **kwargs): + subprocess.run(args, check=True, **kwargs) + + +temporary = Path(os.environ['RUNNER_TEMP']) +paths = [] + +if sys.platform == 'linux': + run('sudo', 'apt-get', 'install', '-y', 'luajit', 'luarocks', 'liblua5.1-0-dev') + run('sudo', 'luarocks', '--lua-version=5.1', 'install', 'moonscript', '0.6.0') +elif sys.platform == 'darwin': + run('brew', 'install', 'luajit', 'luarocks') + luajit = subprocess.check_output(['brew', '--prefix', 'luajit'], text=True).strip() + tree = temporary / 'luarocks' + run('luarocks', '--lua-version=5.1', f'--lua-dir={luajit}', f'--tree={tree}', + 'install', 'moonscript', '0.6.0') + paths.append(tree / 'bin') +elif sys.platform == 'win32': + luajit = temporary / 'luajit' + run('git', 'clone', '--depth', '1', '--branch', 'v2.1', + 'https://github.com/LuaJIT/LuaJIT.git', luajit) + run('cmd', '/c', 'msvcbuild.bat', cwd=luajit / 'src') + paths.append(luajit / 'src') + + archive = temporary / 'moonscript.zip' + urllib.request.urlretrieve( + 'https://github.com/leafo/moonscript/releases/download/v0.6.0/' + 'moonscript-v0.6.0-windows-x86_64.zip', archive) + if hashlib.sha256(archive.read_bytes()).hexdigest() != ( + 'd91b2aec25caf71a551268c1d50d635708e556641ba0e19dd7f24d4a68d93e7c'): + sys.exit('MoonScript archive checksum mismatch') + with zipfile.ZipFile(archive) as compiler: + compiler.extractall(temporary / 'moonscript') + paths.append(temporary / 'moonscript') +else: + sys.exit(f'Unsupported CI platform: {sys.platform}') + +with open(os.environ['GITHUB_PATH'], 'a', encoding='utf-8') as github_path: + for path in paths: + github_path.write(f'{path}\n') From 9e82546c2240f7956fad2748c44f2b3aafd692a7 Mon Sep 17 00:00:00 2001 From: Ryan Lucia Date: Fri, 11 Sep 2026 18:10:55 -0700 Subject: [PATCH 4/4] Use MoonScript 0.7.0 for wrapper tests --- tools/ci/setup-lua.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tools/ci/setup-lua.py b/tools/ci/setup-lua.py index b549a52..d17d457 100644 --- a/tools/ci/setup-lua.py +++ b/tools/ci/setup-lua.py @@ -18,13 +18,13 @@ def run(*args, **kwargs): if sys.platform == 'linux': run('sudo', 'apt-get', 'install', '-y', 'luajit', 'luarocks', 'liblua5.1-0-dev') - run('sudo', 'luarocks', '--lua-version=5.1', 'install', 'moonscript', '0.6.0') + run('sudo', 'luarocks', '--lua-version=5.1', 'install', 'moonscript', '0.7.0') elif sys.platform == 'darwin': run('brew', 'install', 'luajit', 'luarocks') luajit = subprocess.check_output(['brew', '--prefix', 'luajit'], text=True).strip() tree = temporary / 'luarocks' run('luarocks', '--lua-version=5.1', f'--lua-dir={luajit}', f'--tree={tree}', - 'install', 'moonscript', '0.6.0') + 'install', 'moonscript', '0.7.0') paths.append(tree / 'bin') elif sys.platform == 'win32': luajit = temporary / 'luajit' @@ -35,10 +35,10 @@ def run(*args, **kwargs): archive = temporary / 'moonscript.zip' urllib.request.urlretrieve( - 'https://github.com/leafo/moonscript/releases/download/v0.6.0/' - 'moonscript-v0.6.0-windows-x86_64.zip', archive) + 'https://github.com/leafo/moonscript/releases/download/v0.7.0/' + 'moonscript-v0.7.0-windows-x86_64.zip', archive) if hashlib.sha256(archive.read_bytes()).hexdigest() != ( - 'd91b2aec25caf71a551268c1d50d635708e556641ba0e19dd7f24d4a68d93e7c'): + '36c41ae1faaff1423e100df667a498df61607470b0588663adc14e9db536eb17'): sys.exit('MoonScript archive checksum mismatch') with zipfile.ZipFile(archive) as compiler: compiler.extractall(temporary / 'moonscript')