Left to itself, Mesa's d3d12 driver takes the Microsoft Basic Render Driver (WARP) when that is the only D3D12 adapter: on the hosted ARM64 runner the retry came up as "D3D12 (Microsoft Basic Render Driver)" and the app exited with status 87 before its first screenshot, where the same runner renders fine on llvmpipe. WARP is software D3D12 under a GL translation, so it is the slower software path anyway. The launcher now enumerates DXGI adapters (dxgi.dll and d3d12.dll loaded by hand, no link dependency), skips DXGI_ADAPTER_FLAG_SOFTWARE ones and asks D3D12CreateDevice at feature level 11_0. First hit: GALLIUM_DRIVER= d3d12 plus MESA_D3D12_DEFAULT_ADAPTER_NAME pinned to that adapter. No hit: GALLIUM_DRIVER=llvmpipe and LIBGL_ALWAYS_SOFTWARE=true as #1138 had. A GALLIUM_DRIVER already in the environment wins. Both outcomes are logged on stderr and the GL test workflow asserts one of the two lines. Co-Authored-By: Claude Fable 5.1 <[email protected]>
33 KiB
Packaging the Virtual Jaguar app (#839)
CI builds every installable package from one reusable workflow,
.github/workflows/app-package.yml. It never publishes:
| Caller | When | Result |
|---|---|---|
release.yml |
tag v*, push to develop, push to release/** |
artifacts are attached next to the core assets (release / rolling nightly / rolling prerelease) and listed in SHA256SUMS.txt |
app.yml |
pull requests | artifacts plus one sticky PR comment linking them; signed and notarized like a release when the secrets exist (same-repo PRs), ad-hoc signed for fork PRs |
| manual dispatch | on demand | artifacts only; the sign input exercises the signing path |
Microsoft Store (MSIX)
The separate Windows Store MSIX workflow prepares unsigned x64/ARM64 submission
packages for the reserved product, outside the website release artifact run.
See windows/msix/README.md for identity, build commands,
validation and remaining Windows install/upgrade qualification. Website ZIP/EXE
packages retain their existing delivery path.
Assets
X.Y.Z is the tag without v; nightlies use nightly, PRs prNNN, the release/** lane prerelease.
Same-repository release/** PRs share the Release push workflow's package builds when the PR's
synthetic merge tree is identical to its head tree. Ordinary PRs, forks, manual app builds, and
release PRs whose merge adds different content keep their own package builds. Native app test
matrices and ThreadSanitizer still run on the PR; Release still builds and tests every package.
The PR's Verify shared Release packages check requires all twelve package/runtime jobs and
all six nonempty, unexpired app-package-* artifact groups from the latest Release push run for
that exact head, branch and repository. Pending, failed, cancelled, skipped or missing package
checks cannot pass the shared gate. A changed PR head or merge tree also fails it.
Shared packages retain their prerelease filenames, artifact names and 14-day retention in the
Release run. The sticky PR comment links directly to those artifacts and reports the verifier's
result; it does not rename, rebuild or copy the archives. Tag/nightly publication and checksum
collection still consume the Release workflow's artifacts as before. The verification job uses
a hosted runner, not an allowlisted temporary runner that Release metadata jobs may need, so
waiting for those jobs cannot deadlock that runner. It times out red after 345 minutes rather
than bypassing unfinished work. If it fails, fix or rerun the failing Release package job, then
rerun the PR's shared-package verifier; no duplicate package dispatch is needed.
| File | Notes |
|---|---|
VirtualJaguar-X.Y.Z-macos-universal.zip |
Virtual Jaguar.app (the zip's only top-level entry), arm64 + x86_64, macOS 11+; also the Homebrew cask's download. There is no DMG. |
VirtualJaguar-X.Y.Z-windows-x64.zip |
portable folder; portable.txt keeps settings in .\data |
VirtualJaguar-X.Y.Z-windows-x64-setup.exe |
Inno Setup installer, per-user by default (no admin prompt) |
VirtualJaguar-X.Y.Z-windows-arm64.zip, ...-windows-arm64-setup.exe |
the same two packages, native ARM64 (clang); the arm64 installer refuses to run on x64 Windows |
VirtualJaguar-X.Y.Z-linux-x86_64.AppImage, ...-linux-aarch64.AppImage |
built on Ubuntu 22.04 (glibc 2.35) |
virtualjaguar_X.Y.Z_amd64.deb, virtualjaguar_X.Y.Z_arm64.deb |
installs to /usr/lib/virtualjaguar, symlink in /usr/bin |
VirtualJaguar-X.Y.Z-linux-x86_64.flatpak |
single-file Flatpak bundle, app id io.github.libretro.VirtualJaguar; see Flatpak notes |
The .deb's internal Version: is Debian-legal (3.9.1, 3.9.1~rc1, 3.9.1~nightly.<sha>; the version is the core's); the
file name keeps the contract name.
The version guard
The app reports project(VERSION ...) from app/CMakeLists.txt. A tag whose version (minus any
-suffix) differs from it builds no app packages and logs a warning, so a core-only release never
ships an app that says another number. To ship packages with a release, bump the app version in the
release commit. A commit without app/ is skipped the same way.
The install tree
cmake --install build --prefix P produces P/bin/virtualjaguar[.exe], everything the build staged
beside the executable (cheats/, libretro-db/, shaders/, librashader.*) in P/bin/,
and licences/notices in P/share/doc/virtualjaguar/; Linux adds desktop, appstream and icon files.
The sweep is in app/cmake/VJPackage.cmake: a feature that bundles
another directory adds its name to VJ_PACKAGE_DATA_DIRS once. Each platform script then relocates
bin/: Contents/Resources (+ Contents/Frameworks for dylibs) in the .app, /usr/lib/virtualjaguar
in the .deb, usr/bin in the AppImage. CI checks that each package holds every data directory the
build tree had (check-payload.sh).
Secrets the maintainer must add
Nothing is Developer ID signed until these exist; without them forks, PRs and the first runs still build (macOS ad-hoc signed), with a warning in the log. Repository secrets (Settings > Secrets and variables > Actions):
macOS (Developer ID + notarization, existing Apple Developer account):
| Secret | What |
|---|---|
APPLE_DEVELOPER_ID_P12_BASE64 |
base64 -i DeveloperID.p12 of the Developer ID Application certificate with its private key |
APPLE_DEVELOPER_ID_P12_PASSWORD |
the password the .p12 was exported with |
APPLE_NOTARY_KEY_ID |
App Store Connect API key id (Users and Access > Integrations > Keys) |
APPLE_NOTARY_ISSUER_ID |
the issuer UUID shown on that page |
APPLE_NOTARY_KEY_P8_BASE64 |
base64 -i AuthKey_XXXX.p8 |
Windows (SignPath Foundation, dormant until the OSS application is approved):
| Name | Kind | What |
|---|---|---|
SIGNPATH_API_TOKEN |
secret | submitter API token |
SIGNPATH_ORGANIZATION_ID |
variable | organization id |
SIGNPATH_PROJECT_SLUG, SIGNPATH_SIGNING_POLICY_SLUG |
variables, optional | default virtualjaguar / release-signing |
The SignPath steps are written to the action's documented interface but have never run; expect to
adjust them against the artifact configuration SignPath gives you (they sign virtualjaguar.exe before
packaging and the installer after).
Signing runs when the caller asks (release.yml and app.yml do; a manual dispatch needs the sign
input) and the secrets exist. GitHub passes no secrets to a fork pull request, so those builds stay
ad-hoc signed and the log warns.
Every macOS build is signed (#989)
All the macOS files people download are signed with the Developer ID Application certificate (hardened
runtime, secure timestamp) and notarized, by one composite action,
.github/actions/macos-sign, which wraps
macos/codesign.sh. It imports the certificate into a temporary keychain that is
always deleted again, signs, submits one zip per call to notarytool (API key auth), and staples bundles.
| What | Where | Stapled |
|---|---|---|
Virtual Jaguar.app in the macOS zip |
app-package.yml (PRs, nightlies, tags, dispatch with sign) |
yes |
virtualjaguar binary, artifact virtualjaguar-macOS arm64 |
app.yml |
no |
virtualjaguar_libretro-macos-{arm64,x86_64}.dylib |
release.yml (tags, nightly, prerelease) |
no |
cue2cdi-macos-universal |
release.yml |
no |
chdman, jagcd-chd-check in jagcd-tools-macos.zip |
chdman-tools.yml |
no |
A bare Mach-O file cannot hold a stapled ticket: it is notarized inside a zip and Gatekeeper looks the ticket up online the first time. iOS and tvOS dylibs are not signed here (a Developer ID does not apply to them; Provenance signs them when it embeds them). A notarization failure (an Apple outage, a timeout) fails the step on a tag and only warns anywhere else, so PR CI keeps the signed-but-unnotarized build. Without the secrets the action prints the warning and does nothing.
The secrets reach a same-repo pull request's workflow run, so anyone with write access to the repository can have CI sign a file. Keep that in mind when granting write access.
Signing locally
local-package.sh packages a normal build on your own machine and signs it with a certificate it
finds there; CI is unaffected. Build first, then:
cmake -S app -B build -DCMAKE_BUILD_TYPE=Release && cmake --build build -j4
bash app/packaging/local-package.sh --sign # dist/Virtual Jaguar.app (+ a .zip)
bash app/packaging/local-package.sh --notarize # also submit to Apple, staple, spctl
Without --sign it only assembles (macOS ad-hoc signs, as Apple Silicon requires). The local build is
host-architecture only; the universal build is CI's.
Settings come from the environment, then from a repo-root .env (gitignored; copy
env.example, which lists every key). Only VJ_* and APPLE_* keys are read, values
are literal (nothing is evaluated), and a variable already set in your shell wins. A value written as
op://vault/item/field is resolved with the 1Password CLI when op is installed, so a secret need not
sit in the file (for APPLE_NOTARY_KEY_PATH and VJ_SIGN_PFX the item is written to a private temp file for
the run). VJ_ENV_FILE points at another file. Values are never printed, only key names.
macOS identity, first match wins:
VJ_SIGN_IDENTITY: a name, a SHA-1 hash, or-for ad hoc- the newest-expiring valid Developer ID Application identity in your keychain (chosen and passed by SHA-1, so renewed duplicates with the same name are not ambiguous)
- Apple Development (runs on this Mac; cannot be notarized, and a warning says so)
- ad hoc
It prints the identity name and expiry it picked. List yours with
security find-identity -v -p codesigning. The first signature may raise a keychain prompt for the
private key: click Always Allow.
Notarization (--notarize, needs a Developer ID identity), first match wins:
VJ_NOTARY_PROFILE: a keychain profile. Recommended, since no secret leaves the keychain. Create it once:xcrun notarytool store-credentials vj-notary --key AuthKey_XXXX.p8 --key-id KEYID --issuer ISSUER-UUIDAPPLE_NOTARY_KEY_ID+APPLE_NOTARY_ISSUER_ID+APPLE_NOTARY_KEY_PATH(the.p8on disk)
Notarizing uploads the app to Apple and waits for the verdict, then staples the ticket and runs
spctl -a -t exec -vv. Hardened runtime and the entitlements are the same as CI's.
Windows (Git Bash / MSYS2; builds the portable zip, plus the installer when Inno Setup's ISCC.exe is
found), certificate, first match wins:
VJ_SIGN_PFX(+VJ_SIGN_PFX_PASSWORD): a.pfxfileVJ_SIGN_CERT_SHA1: the thumbprint of a certificate in the CurrentUser\My storesigntool sign /a, which picks an installed code-signing certificate; none usable means unsigned, with a message
signtool.exe is found on PATH or in the newest Windows SDK under
C:\Program Files (x86)\Windows Kits\10\bin\*\x64; VJ_SIGNTOOL overrides. It signs virtualjaguar.exe,
the DLLs, and the installer, timestamped with /tr VJ_SIGN_TIMESTAMP_URL (default
http://timestamp.digicert.com, SHA-256). This path has not been run on Windows yet. Note signtool /p shows
the password in the process list; prefer the store thumbprint on a shared machine.
Dev executable. -DVJ_APP_SIGN_DEV_BUILD (macOS; ON by default outside CI, =OFF to skip) signs build/virtualjaguar after
every build with the identity chosen above, so macOS stops re-prompting for network and keychain access
after each rebuild of an unsigned binary. It does not use hardened runtime and never fails the build.
macOS notes
- Layout.
SDL_GetBasePath()in a.appisContents/Resources/(SDL's defaultresourcebase-dir type), so cheats, the dat files and shader presets live there;librashader.dylibgoes inContents/Frameworks/, one of the places the shader code probes. The dev target is deliberately not aMACOSX_BUNDLE: its POST_BUILD copies would land inContents/MacOS. - Universal. Two single-architecture builds merged with
lipo(make-universal.sh), because each configure is handed one single-architecture librashader (-DVJ_LIBRASHADER_LIB). A gate (check-universal.sh) fails the build if any Mach-O,librashader.dylibincluded, lacks a slice, and checks every binary'sminosagainst the deployment target (11.0). Both dylibs are built from source in CI from the same pinned tag, see Shader runtime. - OpenGL 4.1. macOS stops at OpenGL 4.1 and has no
GL_ARB_arrays_of_arrays. librashader 0.12 never sets SPIRV-Cross'sflatten_multidimensional_arrays, so a shader with a 2-D array (crt-geom'svec3 slotmask[3][4]) compiles to GLSL that requires that extension and cannot build there. 15 bundled presets do this: crt-geom, -deluxe, -tate; crt-hyllian, -ntsc, -ntsc-rainbow, -sinc-composite; crt-nobody, crt-super-xbr, vt220, crt-fast-bilateral-super-xbr, img_mod, test-pattern-append/-prepend and res-independent-scanlines. Where the context has neither GL 4.3 nor the extension, the preset list greys them out ("needs OpenGL 4.3"), and a preset chosen anyway (config.ini, file dialog) gets that reason in the on-screen message instead of the GL compiler's text. The list ispresetsNeedingArraysOfArrays()(present_util.cpp), pinned to the bundled slang-shaders commit;shaders_bundledandshaders_glcheck it. Real fix: librashader settingflatten_multidimensional_arrayswhen it generates GLSL below 4.30. The chain asks librashader for the context's own GLSL version (4.10 here). It used to ask for 3.30, which cost 11 presets GLSL 4.00 features the context has (lottesRVM, nnedi3 x5, mmpx-ADV, vectorscale x4). Rendering all 498 bundled presets on an M4 Max (GL 4.1 Metal) at 4.10, 473 work. Besides the 15 above, fsr and smaa+fsr needpackHalf2x16(GLSL 4.20), and 8 fail for reasons that do not look macOS-specific (not checked elsewhere): the six nedi bilateral-variant presets (unknown semanticplay), crt-black_crush-koko (preset parse error) and ntsc-blastem (varying type mismatch at link). - Hardened runtime and entitlements. Signed with
--options runtime. Library validation (on with the hardened runtime) refuses any dylib not signed by Apple or by our team. The bundledlibrashader.dylibis re-signed with our identity, so it needs no exception. The shader code also probes/opt/homebrew/liband/usr/local/libfor a user's own librashader; that one is signed by someone else (or ad hoc), and was verified to be refused without an exception and accepted withcom.apple.security.cs.disable-library-validation. That is the only entitlement inentitlements.plist; no JIT, camera, microphone or sandbox is involved. Delete the entitlement to forbid user-supplied shader runtimes. - Order. sign the dylib then the app, notarize the app and staple the
.app, then zip it withditto -c -k --sequesterRsrc --keepParent(the stapled ticket and signature travel inside the app). CI unpacks the finished zip and re-verifies the signature (and, when signed, the staple and Gatekeeper's verdict) on the unpacked app. The zip itself is never notarized separately; Apple's ticket is on the app. - Builds without the secrets (fork pull requests, local builds) are ad-hoc signed (
codesign -s -), which Apple Silicon requires; Gatekeeper still refuses them until right-click > Open orxattr -dr com.apple.quarantine.
Shader runtime (librashader)
The app loads librashader with dlopen (#834); every
package ships it beside the executable (Contents/Frameworks/librashader.dylib, usr/bin/librashader.so in the
AppImage, /usr/lib/virtualjaguar/librashader.so in the .deb, librashader.dll next to the exe), and
THIRD-PARTY-NOTICES.txt carries its licence (MPL-2.0 OR GPL-3.0-only).
| Package | Where the library comes from |
|---|---|
| macOS universal (arm64 + x86_64) | built from source in CI, both slices, then lipo-merged |
AppImage / .deb, x86_64 and aarch64 |
built from source in CI on the packaging runner (Ubuntu 22.04) |
| Windows x64 / ARM64 | upstream's prebuilt librashader-v0.12.0 zip (SHA-256-pinned in app/cmake/VJShaders.cmake) |
| Flatpak | see Flatpak notes |
Why source: upstream's x86_64-macos v0.12.0 zip holds an arm64 dylib (lipo-verified), so Intel Macs had no
shader runtime, and upstream publishes no Linux binary. Building both macOS slices ourselves (instead of mixing
upstream's arm64 with a self-built x86_64) keeps one version, one deployment target and one install name in the
universal file.
- The recipe is
.github/workflows/librashader.yml, called byapp-package.yml(joblibrashader, which the macOS and Linux jobsneed). The pin islibrashader/pin.env: taglibrashader-v0.12.1, its commit and the SHA-256 of the GitHub archive for that commit, a pinned stable Rust, and the macOS deployment target. The build islibrashader/build-from-source.sh: fetch, verify the digest, thencargo xtask build --profile optimized --target <triple> -- --locked.VJShaders.cmakereads the same tag for the C headers, whose digests are pinned there (the 0.12.1 headers are byte-identical to 0.12.0's, ABI 2). - Targets:
x86_64-apple-darwin(cross-built on the Apple-silicon runner),aarch64-apple-darwin,x86_64-unknown-linux-gnuandaarch64-unknown-linux-gnu(native,ubuntu-22.04/ubuntu-22.04-arm, glibc 2.35 like the packages). macOS usesMACOSX_DEPLOYMENT_TARGET=11.0and the install name@rpath/librashader.dylib(cargo's default would bake the runner's absolute path in). - Cache and artifacts. Each target is cached under
librashader-<commit>-<triple>-rust<ver>-mac<min>-v1(a cold build is ~10 minutes with LTO, a hit is seconds; bump-v1in the workflow to force a rebuild after a recipe change) and uploaded as the artifactlibrashader-<triple>(library +BUILD-INFO.txt). These are intermediates:release.ymlleaveslibrashader-*out of the release assets. - Checks. In
librashader.yml: the architecture (lipo -archs/ ELF machine),minos, install name, the highestGLIBCsymbol version (must be <= 2.35), only C/C++-runtimeNEEDEDentries, and (same-architecture runners) a load that reads the ABI version. Inapp-package.yml:vj_shader_tests librarywithVJ_REQUIRE_LIBRASHADER=1(a missing runtime fails instead of skipping); the macOS bundle's universal dylib is loaded by an arm64 process and, under Rosetta, an x86_64 one; for the AppImage and the.deb,linux/check-librashader.shchecks the file, its CPU and dependencies, and runsvj_shader_tests probefrom the package's own folder, i.e. the app's real lookup (beside the executable,VJ_LIBRASHADERunset). - Bumping: change
LIBRASHADER_TAG,LIBRASHADER_COMMITandLIBRASHADER_SHA256together (and re-pin the header digests inVJShaders.cmakeifinclude/changed; its comment says how), updateTHIRD-PARTY-NOTICES.txt. - Without the bundled file the Shader section of the app says why (not bundled / failed to load: reason) and
links to the "Getting the shader runtime" section of
app/README.md.
Windows notes
- MinGW-w64 (the core's toolchain), statically linked C/C++ runtime; CI fails if the exe imports anything but Windows system DLLs.
- ARM64. Built natively on GitHub's
windows-11-armrunner in the MSYS2CLANGARM64environment (clang, libc++, lld), the ARM64 flavour of the same MinGW-w64 setup; MSVC was not used because the MSVC build of the core has never passed. The blitter's SIMD choice is autodetected, so ARM64 gets the NEON implementation; CI checks that exactly oneblitter_simd_opsis linked, and that the exe is an ARM64 image. The samepackage.shandvirtualjaguar.issserve both architectures (--arch arm64,/DArm64): the arm64 installer isArchitecturesAllowed=arm64, the x64 onex64compatible(so the x64 package also installs, emulated, on ARM64 Windows). librashader'saarch64-windowsruntime is bundled.tools/install.ps1picks the ARM64 package on ARM64 Windows and falls back to x64 for older releases. GUI subsystem (no console window);console_attach.cppre-attaches to the parent console so--help/--versionstill print. - Installer: Inno Setup 6.7.3 (pinned by SHA-256). Chosen over NSIS for a maintained modern wizard,
per-user install without a UAC prompt by default (
PrivilegesRequiredOverridesAllowed=dialog), clean uninstall registration, and winget'sinnoinstaller type with known silent switches. The installed copy is not portable (noportable.txt; Program Files is not writable); data lives in%APPDATA%\VirtualJaguar. - CI installs the setup silently, runs the installed exe, uninstalls, and checks it is gone.
Linux notes
- AppImage: linuxdeploy
1-alpha-20251107-1+ appimagetool1.9.1, both pinned by SHA-256, run withAPPIMAGE_EXTRACT_AND_RUN=1(no FUSE needed). SDL3 is static anddlopens the host's display, audio and GL. .deb: built bymake-deb.sh(not CPack) so the executable and its data stay together under/usr/lib/virtualjaguarwith a/usr/binsymlink (SDL_GetBasePath()resolves/proc/self/exe).Dependscomes fromdpkg-shlibdeps; the display and audio stacks SDL loads at run time areRecommends.- Built on Ubuntu 22.04 runners, the oldest available, for the lowest glibc floor.
librashader.sois built from source on the same runners (see Shader runtime above) and installed beside the executable bycmake --install; it is not a linked dependency (no rpath needed, the appdlopens it by absolute path fromSDL_GetBasePath()), so linuxdeploy anddpkg-shlibdepsleave it alone.
Build channel
-DVJ_BUILD_CHANNEL= (release | nightly | dev, default dev) decides what Settings > Interface > Emulation thread
starts as for a user who never chose (a stored choice always wins): nightly on, release and dev off. dev is everything
else: pull-request artifacts, prerelease, local builds. app-package.yml derives CHANNEL from release_tag (nightly ->
nightly, v* -> release, anything else -> dev) in its top-level env: beside TV_DEFAULT, and passes it to the macOS, Windows
and Linux configures. The Flatpak manifest commits -DVJ_BUILD_CHANNEL=release (so the Flathub render is a release build);
the flatpak job rewrites that line to $CHANNEL and fails if the rewrite matched nothing. About shows the channel and the start-up
log prints [emu] channel=....
Flatpak notes
- Manifest:
flatpak/io.github.libretro.VirtualJaguar.yml, runtimeorg.freedesktop.Platform25.08. It builds the checked-out tree (type: dir);manifests/render.shderives the Flathub form (git tag + commit) from the same file, so there is one manifest. - Offline. Flatpak builds have no network. SDL3, Dear ImGui, FreeType, rcheevos, the librashader headers and source archive (+ vendored crates), the
slang-shaders archive, the two libretro dats and the 41 cheat files + licence are
archive/filesources with SHA-256, wired in through the app's offline knobs (FETCHCONTENT_SOURCE_DIR_SDL3|IMGUI|FREETYPE|RCHEEVOS,VJ_LIBRASHADER_INCLUDE_DIR,VJ_SLANG_SHADERS_ARCHIVE,VJ_APP_LIBRETRO_DB_DIR,VJ_APP_CHEATS_SOURCE_DIR), andFETCHCONTENT_FULLY_DISCONNECTED=ONso a missed download fails the build. The cheat sources are generated fromapp/cmake/VJCheatManifest.cmake: when that file's pinned commit changes, regenerate the manifest's cheat entries (same names, URLs and hashes). Apost-installstep fails the build if the data directories or libcurl are missing. - Shaders (librashader, #951). Upstream publishes no Linux binary and the sandbox cannot load a host library,
so the manifest's
librashadermodule builds the C API from the pinned release tag (librashader-v0.12.1archive, SHA-256) withorg.freedesktop.Sdk.Extension.rust-stable(Rust 1.98 on the 25.08 branch; librashader needs >= 1.88), offline:build-optionsputs/usr/lib/sdk/rust-stable/binonPATH, setsCARGO_HOME=/run/build/librashader/cargoandCARGO_NET_OFFLINE=true, and every crate is a hash-pinned source inflatpak/cargo-sources.json(which also carries thecargo/configthat points cargo at the vendored sources). Only the OpenGL runtime is built (--no-default-features --features runtime-opengl, the one backend the app uses). The result is installed as/app/lib/librashader.so, which the app finds through the sandbox'sLD_LIBRARY_PATH, with the MPL-2.0 text in/app/share/licenses/io.github.libretro.VirtualJaguar/. The slang presets ship beside the executable as before. Bumping librashader (also keep the header pins andVJ_LIBRASHADER_TAGincmake/VJShaders.cmakein step when the C ABI changes):- change the tag in the module's archive
url, and itssha256(curl -L <url> | shasum -a 256); - regenerate the crate list from that tag's lock file with
flatpak-cargo-generator
(
pip install aiohttp tomlkit PyYAML; last run withcargo/flatpak-cargo-generator.pyat flatpak-builder-tools41c20aa):curl -L https://github.com/SnowflakePowered/librashader/archive/refs/tags/librashader-vX.Y.Z.tar.gz | tar xz python3 flatpak-cargo-generator.py librashader-librashader-vX.Y.Z/Cargo.lock -o app/packaging/flatpak/cargo-sources.json - commit both together. The file lists every crate in the workspace lock (a vendored build must be able to
resolve all of them, including the Windows/Metal/Vulkan backends it does not compile); that is why it is
large.
manifests/render.shcopies it next to the rendered Flathub manifest.
- change the tag in the module's archive
- Permissions: network (achievements, box art, netplay), Wayland + X11 fallback, PulseAudio,
--device=all(gamepads), IPC, and--filesystem=home:ro. The last is needed because the library scans folders and a.cueneeds its sibling.bintracks, neither of which the file-chooser portal grants; it is read-only and limited to the home directory. Settings, saves and states live in~/.var/app/io.github.libretro.VirtualJaguar/data/VirtualJaguar(SDL's pref path followsXDG_DATA_HOME, which Flatpak sets). - CI:
app-package.ymljobflatpakusesflatpak/flatpak-github-actions/flatpak-builderv6.8 (pinned by commit) in the Flathub imagefreedesktop-25.08, with the action's own artifact upload disabled (it would not be namedapp-package-*); the bundle is smoke-tested (install, headless frame hash) and uploaded asapp-package-flatpak. x86_64 only for now; a native aarch64 leg onubuntu-24.04-armwould be a matrix entry. - Install:
flatpak remote-add --user --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo(the runtime comes from Flathub), thenflatpak install --user VirtualJaguar-X.Y.Z-linux-x86_64.flatpak.
Package managers (templates; nothing is submitted by CI)
Templates live in manifests/: Homebrew cask (macOS zip) and formula (source build),
winget (3-file manifest for the Inno installer), Scoop (portable zip), Flatpak (offline-source manifest).
CI renders and parses them on every run with placeholder checksums. After a release is published:
curl -LO https://github.com/libretro/virtualjaguar-libretro/releases/download/vX.Y.Z/SHA256SUMS.txt
bash app/packaging/manifests/render.sh --version X.Y.Z --sums SHA256SUMS.txt \
--fetch-source --commit "$(git rev-parse vX.Y.Z)" --out /tmp/vj-manifests
then, by hand (these are outward actions that stay with the maintainer):
- Homebrew cask (
homebrew/virtualjaguar.rb): PR to your tap orHomebrew/homebrew-cask(brew audit --new --cask,brew install --cask ./virtualjaguar.rb). Homebrew requires casks to be signed and notarized, so wait for a notarized release. - Homebrew formula (
virtualjaguar-formula.rb): tap only; its configure step downloads data (not allowed in homebrew-core). - winget:
wingetcreateor a PR tomicrosoft/winget-pkgswith the threewinget/*.yamlfiles (winget validate). Unsigned installers trigger SmartScreen; reviewers may ask for signing. - Scoop: add
scoop/virtualjaguar.jsonto a bucket (scoop install ./virtualjaguar.json). - Flatpak / Flathub: new-app PR to
flathub/flathubwithflatpak/io.github.libretro.VirtualJaguar.yml. Theio.github.libretro.*app id assumes the libretro GitHub organization owns the app; change it everywhere (app/cmake/VJPackage.cmake,app/packaging/linux/*,macos/Info.plist.in) if a different id is wanted. The manifest is built in CI on every package run (bundle smoke test included). Flathub's own linter and review may ask for changes (for instance a librashader module).
File associations and native integration (#833)
Which extensions each package registers, and where the list lives. The set is .j64 .jag .rom .abs .cof
(cartridges, executables) and .cue .chd .cdi (CD images); .bin / .prg are deliberately left out (too generic).
One source of truth: VJ_ASSOC_LIST in src/platform/commands.h. The CTest platform_assoc_sync fails if any
of these drifts, and checks that the application id io.github.libretro.VirtualJaguar is the same in all of them:
| OS | Where | What it does |
|---|---|---|
| macOS | macos/Info.plist.in: CFBundleDocumentTypes (+ an exported UTI io.github.libretro.virtualjaguar.rom for .j64 / .jag) |
Finder shows "Atari Jaguar game", Open With lists the app, a game dropped on the Dock icon or opened from Finder arrives as the drop event. Rank: Default for games, Alternate for CD images (never takes over .cue / .chd). |
| Windows | windows/virtualjaguar.iss: [Registry] |
Always: Applications\virtualjaguar.exe (SupportedTypes: Open With, and what lets Windows show recent games in the Jump List). The associate task (unchecked, per user via HKA): ProgId VirtualJaguar.Game, an OpenWithProgids entry per extension, and the default handler for .j64 / .jag only. Shortcuts carry AppUserModelID = the id the program sets itself. Windows 10/11 keep the user's own default (UserChoice). |
| Linux | linux/io.github.libretro.VirtualJaguar.xml (shared-mime-info) and MimeType= in the .desktop file |
Installed to share/mime/packages by cmake --install, so the .deb (dpkg trigger), the Flatpak (export) and the AppImage's AppDir carry it. rom, abs, cof, cue, chd, cdi are generic: they have a lower weight (30) so a type another package already defines keeps winning. |
Other native pieces that live in packaging: the Windows manifest (windows/virtualjaguar.manifest, per-monitor-v2 DPI,
long paths, Windows 10/11; MSVC links it from the source list, MinGW's windres embeds it from the .rc), the .desktop
Actions (Open Game Library, Boot Without Cartridge) and StartupWMClass (= the application id, which SDL uses as the
Wayland app_id / X11 class), and the Flatpak's --talk-name=org.kde.StatusNotifierWatcher for the optional tray icon.
Placeholder icon
icons/virtualjaguar.svg is a placeholder (original artwork, no Atari marks). Replace it and run
icons/make-icons.sh (needs rsvg-convert, ImageMagick, macOS iconutil) to regenerate the committed
PNG, .ico and .icns files.
Clean-machine checks CI cannot do
Gatekeeper first-run on a freshly downloaded signed zip, SmartScreen on the installer, Intel Mac behavior (CI may not run the x86_64 slice), Wayland/X11/PipeWire sound on real Linux desktops, and the AppImage on distributions older than Ubuntu 22.04.
Windows packages also stage a bundled Mesa OpenGL through
windows/stage-software-gl.py (MSYS2 Mesa, Python and objdump required) in
the software-opengl/ folder (the name predates the Direct3D 12 path below).
The normal executable automatically retries once with the bundled app if
graphics startup fails, which on a stock Windows 10/11 install without a vendor
OpenGL driver (Windows on ARM, VMs, Remote Desktop, the basic display adapter)
means WGL handed us the GDI Generic OpenGL 1.1 driver. The retry preserves game
arguments, working directory and the original config/save root; users do not
need a separate launcher. The launcher picks Mesa's driver: d3d12 (OpenGL on
the machine's Direct3D 12 driver, hardware rendering, the same layer as
Microsoft's OpenGL Compatibility Pack) when a hardware D3D12 adapter exists,
pinned to that adapter; llvmpipe (CPU rendering, slower) otherwise, including
when the only D3D12 adapter is the Microsoft Basic Render Driver (WARP), which
is software too and slower than llvmpipe. A GALLIUM_DRIVER already in the
environment wins. The [gl] log lines name the choice and the renderer that
came up. Keep the dependency notices and matching architecture alongside the
bundled executable.