From c98c46b72161c609121ea6da4ddf4cb5a0bd148c Mon Sep 17 00:00:00 2001 From: vulcandth Date: Sat, 21 Mar 2026 06:07:09 -0700 Subject: [PATCH 1/3] Move pages build inputs to main branch Add a main-branch copy of build.py based on the gh-pages version with only the CLI/path plumbing needed for CI. Update deploy-pages.yml so deploy-pages checks out main, installs the core Python dependencies, dumps emulators.json and tests.json from main.py on each run, copies those manifests into gh-pages, builds pages/index.html from the gh-pages copies plus persisted emulator result JSON, and removes only pages/build.py before committing. This keeps gh-pages as published artifacts while allowing new tests, emulators, and page-build changes to be made from main. --- .github/workflows/deploy-pages.yml | 13 ++++-- build.py | 67 ++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 3 deletions(-) create mode 100644 build.py diff --git a/.github/workflows/deploy-pages.yml b/.github/workflows/deploy-pages.yml index f843e68..2ae524d 100644 --- a/.github/workflows/deploy-pages.yml +++ b/.github/workflows/deploy-pages.yml @@ -15,10 +15,15 @@ jobs: deploy: runs-on: windows-latest steps: + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 with: python-version: '3.x' + - name: Install build dependencies + run: python -m pip install -r requirements-core.txt + - name: Download emulator results uses: actions/download-artifact@v5 with: @@ -37,10 +42,12 @@ jobs: } else { Write-Host "No emulator JSON artifacts found to publish." } + python main.py --dump-emulators-json --dump-tests-json + Copy-Item emulators.json, tests.json -Destination pages -Force + python build.py --results-dir pages --emulators pages/emulators.json --tests pages/tests.json --output pages/index.html + Remove-Item pages\build.py -ErrorAction SilentlyContinue Set-Location pages - python build.py - git add *.json - git add index.html + git add -A if (git diff --cached --quiet) { Write-Host "No gh-pages changes to commit." exit 0 diff --git a/build.py b/build.py new file mode 100644 index 0000000..bcdf7c9 --- /dev/null +++ b/build.py @@ -0,0 +1,67 @@ +import argparse +import json +import os +from time import gmtime, strftime + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('--emulators', default='emulators.json') + parser.add_argument('--tests', default='tests.json') + parser.add_argument('--results-dir', default='.') + parser.add_argument('--output', default='index.html') + args = parser.parse_args() + + emulators = json.load(open(args.emulators, 'rt', encoding='utf-8')) + tests = json.load(open(args.tests, 'rt', encoding='utf-8')) + + for name in emulators: + result_file = os.path.join(args.results_dir, emulators[name]['file']) + if os.path.exists(result_file): + data = json.load(open(result_file, 'rt', encoding='utf-8')) + emulators[name].update(data) + emulators[name]['passed'] = len([result for result in data['tests'].values() if result['result'] != 'FAIL']) + else: + emulators[name].update({'passed': 0, 'tests': {}}) + + f = open(args.output, 'wt', encoding='utf-8') + f.write(""" + \n""") + f.write("\n") + for name, emulator in sorted(emulators.items(), key=lambda n: -n[1]['passed']): + f.write(" \n" % (emulator['url'], name, emulator['passed'], len(emulator['tests']))) + f.write("\n") + for test in tests: + name = test['name'].replace("/", "/​") + if test['url']: + name = "%s" % (test['url'], name) + if test['description']: + name += "%s" % (test['description']) + f.write("\n" % (name)) + for name, emulator in sorted(emulators.items(), key=lambda n: -n[1]['passed']): + result = emulator['tests'].get(test['name']) + if result: + f.write(" \n" % (result['result'], result['result'], result['screenshot'])) + else: + f.write(" \n") + f.write("\n") + f.write("
Updated On
" + strftime("%a, %d %b %Y %H:%M:%S +0000", gmtime()) + "
%s (%d/%d)
%s%s
No result
") + + +if __name__ == '__main__': + main() \ No newline at end of file From 4d0b6496b76d5635050542a550d04c070457224f Mon Sep 17 00:00:00 2001 From: vulcandth Date: Sat, 21 Mar 2026 06:53:37 -0700 Subject: [PATCH 2/3] Fix deploy-pages manifest generation Refactor main.py so --dump-emulators-json and --dump-tests-json use static emulator metadata instead of importing emulator backends. This avoids runtime-only dependencies such as selenium when deploy-pages regenerates emulators.json and tests.json. Add a direct workflow_dispatch entrypoint to deploy-pages.yml and skip the artifact download step for manual runs. That allows the Deploy Pages workflow to be triggered by hand to rebuild gh-pages from the current manifests and persisted results without first running an emulator workflow. --- .github/workflows/deploy-pages.yml | 2 + main.py | 169 +++++++++++++++++++++++------ 2 files changed, 135 insertions(+), 36 deletions(-) diff --git a/.github/workflows/deploy-pages.yml b/.github/workflows/deploy-pages.yml index 2ae524d..0ce8ee4 100644 --- a/.github/workflows/deploy-pages.yml +++ b/.github/workflows/deploy-pages.yml @@ -1,6 +1,7 @@ name: Deploy Pages on: + workflow_dispatch: workflow_call: inputs: artifact_pattern: @@ -25,6 +26,7 @@ jobs: run: python -m pip install -r requirements-core.txt - name: Download emulator results + if: ${{ github.event_name == 'workflow_call' }} uses: actions/download-artifact@v5 with: pattern: ${{ inputs.artifact_pattern }} diff --git a/main.py b/main.py index 7ab4ec2..2b06ab7 100644 --- a/main.py +++ b/main.py @@ -53,27 +53,119 @@ def _new_instance(module_name, class_name): return getattr(module, class_name)() -def load_emulators(filter_data): - emulator_factories = [ - (lambda: _new_instance("emulators.bdm", "BDM"), ["Beaten Dying Moon", "bdm", "beaten"]), - (lambda: _new_instance("emulators.mgba", "MGBA"), ["mGBA", "mgba"]), - (lambda: _new_instance("emulators.kigb", "KiGB"), ["KiGB", "kigb"]), - (lambda: _new_instance("emulators.sameboy", "SameBoy"), ["SameBoy", "sameboy"]), - (lambda: _new_instance("emulators.bgb", "BGB"), ["bgb"]), - (lambda: _new_instance("emulators.vba", "VBA"), ["VisualBoyAdvance", "vba"]), - (lambda: _new_instance("emulators.vba", "VBAM"), ["VisualBoyAdvance-M", "vba-m", "vbam"]), - (lambda: _new_instance("emulators.nocash", "NoCash"), ["No$gmb", "nocash", "no$gmb"]), - (lambda: _new_instance("emulators.gambatte", "GambatteSpeedrun"), ["GambatteSpeedrun", "gambatte"]), - (lambda: _new_instance("emulators.emulicious", "Emulicious"), ["Emulicious", "emulicious"]), - (lambda: _new_instance("emulators.goomba", "Goomba"), ["Goomba", "goomba"]), - (lambda: _new_instance("emulators.binjgb", "Binjgb"), ["binjgb"]), - (lambda: _new_instance("emulators.pyboy", "PyBoy"), ["PyBoy", "pyboy"]), - (lambda: _new_instance("emulators.ares", "Ares"), ["ares"]), - (lambda: _new_instance("emulators.emmy", "Emmy"), ["Emmy", "emmy"]), - (lambda: _new_instance("emulators.gameroy", "GameRoy"), ["gameroy", "GameRoy"]), +EMULATOR_SPECS = [ + { + 'factory': lambda: _new_instance("emulators.bdm", "BDM"), + 'keywords': ["Beaten Dying Moon", "bdm", "beaten"], + 'name': "Beaten Dying Moon", + 'url': "https://mattcurrie.com/bdm-demo/", + }, + { + 'factory': lambda: _new_instance("emulators.mgba", "MGBA"), + 'keywords': ["mGBA", "mgba"], + 'name': "mGBA", + 'url': "https://mgba.io/", + }, + { + 'factory': lambda: _new_instance("emulators.kigb", "KiGB"), + 'keywords': ["KiGB", "kigb"], + 'name': "KiGB", + 'url': "http://kigb.emuunlim.com/", + }, + { + 'factory': lambda: _new_instance("emulators.sameboy", "SameBoy"), + 'keywords': ["SameBoy", "sameboy"], + 'name': "SameBoy", + 'url': "https://sameboy.github.io/", + }, + { + 'factory': lambda: _new_instance("emulators.bgb", "BGB"), + 'keywords': ["bgb"], + 'name': "bgb", + 'url': "https://bgb.bircd.org/", + }, + { + 'factory': lambda: _new_instance("emulators.vba", "VBA"), + 'keywords': ["VisualBoyAdvance", "vba"], + 'name': "VisualBoyAdvance", + 'url': "https://sourceforge.net/projects/vba", + }, + { + 'factory': lambda: _new_instance("emulators.vba", "VBAM"), + 'keywords': ["VisualBoyAdvance-M", "vba-m", "vbam"], + 'name': "VisualBoyAdvance-M", + 'url': "https://vba-m.com/", + }, + { + 'factory': lambda: _new_instance("emulators.nocash", "NoCash"), + 'keywords': ["No$gmb", "nocash", "no$gmb"], + 'name': "No$gmb", + 'url': "https://problemkaputt.de/gmb.htm", + }, + { + 'factory': lambda: _new_instance("emulators.gambatte", "GambatteSpeedrun"), + 'keywords': ["GambatteSpeedrun", "gambatte"], + 'name': "GambatteSpeedrun", + 'url': "https://github.com/pokemon-speedrunning/gambatte-speedrun", + }, + { + 'factory': lambda: _new_instance("emulators.emulicious", "Emulicious"), + 'keywords': ["Emulicious", "emulicious"], + 'name': "Emulicious", + 'url': "https://emulicious.net/", + }, + { + 'factory': lambda: _new_instance("emulators.goomba", "Goomba"), + 'keywords': ["Goomba", "goomba"], + 'name': "Goomba", + 'url': "https://www.dwedit.org/gba/goombacolor.php", + }, + { + 'factory': lambda: _new_instance("emulators.binjgb", "Binjgb"), + 'keywords': ["binjgb"], + 'name': "binjgb", + 'url': "https://github.com/binji/binjgb/releases", + }, + { + 'factory': lambda: _new_instance("emulators.pyboy", "PyBoy"), + 'keywords': ["PyBoy", "pyboy"], + 'name': "PyBoy", + 'url': "https://github.com/Baekalfen/PyBoy", + }, + { + 'factory': lambda: _new_instance("emulators.ares", "Ares"), + 'keywords': ["ares"], + 'name': "ares", + 'url': "https://ares-emu.net/", + }, + { + 'factory': lambda: _new_instance("emulators.emmy", "Emmy"), + 'keywords': ["Emmy", "emmy"], + 'name': "Emmy", + 'url': "https://emmy.n1ark.com/", + }, + { + 'factory': lambda: _new_instance("emulators.gameroy", "GameRoy"), + 'keywords': ["gameroy", "GameRoy"], + 'name': "gameroy", + 'url': "https://github.com/Rodrigodd/gameroy", + }, +] + + +def get_emulator_specs(filter_data): + return [ + spec for spec in EMULATOR_SPECS + if _matches_emulator_filter(filter_data, spec['keywords']) ] - return [factory() for factory, keywords in emulator_factories if _matches_emulator_filter(filter_data, keywords)] + +def get_emulator_json_filename(name): + return "%s.json" % (name.replace(" ", "_").lower()) + + +def load_emulators(filter_data): + return [spec['factory']() for spec in get_emulator_specs(filter_data)] tests = testroms.acid.all + testroms.blargg.all + testroms.daid.all + testroms.ax6.all + testroms.mooneye.all + testroms.samesuite.all + testroms.hacktix.all + testroms.cpp.all + testroms.mealybug.all @@ -124,26 +216,14 @@ def checkFilter(input, filter_data): for test in tests if checkFilter(test, args.test) and checkFilter(test.model, args.model) ] - emulators = load_emulators(args.emulator) - - print("%d emulators" % (len(emulators))) - print("%d tests" % (len(tests))) + emulator_specs = get_emulator_specs(args.emulator) - if args.get_runtime: - for emulator in emulators: - emulator.setup() - for test in tests: - if not checkFilter(test, args.test): - continue - print("%s: %s: %g seconds" % (emulator, test, emulator.getRunTimeFor(test))) - emulator.undoSetup() - sys.exit() if args.dump_emulators_json: json.dump({ - str(emulator): { - "file": emulator.getJsonFilename(), - "url": emulator.url, - } for emulator in emulators + spec['name']: { + 'file': get_emulator_json_filename(spec['name']), + 'url': spec['url'], + } for spec in emulator_specs }, open("emulators.json", "wt"), indent=" ") if args.dump_tests_json: json.dump([ @@ -154,6 +234,23 @@ def checkFilter(input, filter_data): } for test in tests ], open("tests.json", "wt"), indent=" ") if args.dump_tests_json or args.dump_emulators_json: + print("%d emulators" % (len(emulator_specs))) + print("%d tests" % (len(tests))) + sys.exit() + + emulators = load_emulators(args.emulator) + + print("%d emulators" % (len(emulators))) + print("%d tests" % (len(tests))) + + if args.get_runtime: + for emulator in emulators: + emulator.setup() + for test in tests: + if not checkFilter(test, args.test): + continue + print("%s: %s: %g seconds" % (emulator, test, emulator.getRunTimeFor(test))) + emulator.undoSetup() sys.exit() if args.get_startuptime: From 65eae6664433330fd9bf7c2cb8349636d5be71bd Mon Sep 17 00:00:00 2001 From: vulcandth Date: Sat, 21 Mar 2026 07:49:39 -0700 Subject: [PATCH 3/3] Fix deploy-pages artifact download guard Use the reusable-workflow input instead of github.event_name to decide whether to download emulator result artifacts. This keeps artifact downloads enabled when ci-all or emulator-specific workflows call deploy-pages from a manual dispatch, while still skipping the download step for direct manual Deploy Pages runs with no artifacts. --- .github/workflows/deploy-pages.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy-pages.yml b/.github/workflows/deploy-pages.yml index 0ce8ee4..63b5c08 100644 --- a/.github/workflows/deploy-pages.yml +++ b/.github/workflows/deploy-pages.yml @@ -26,7 +26,7 @@ jobs: run: python -m pip install -r requirements-core.txt - name: Download emulator results - if: ${{ github.event_name == 'workflow_call' }} + if: ${{ inputs.artifact_pattern != '' }} uses: actions/download-artifact@v5 with: pattern: ${{ inputs.artifact_pattern }}