Skip to content

Instantly share code, notes, and snippets.

@jmealo
Last active September 1, 2026 03:39
Show Gist options
  • Select an option

  • Save jmealo/79d3cbda08e16694694b0088968d6cca to your computer and use it in GitHub Desktop.

Select an option

Save jmealo/79d3cbda08e16694694b0088968d6cca to your computer and use it in GitHub Desktop.
Run legacy 32-bit Adobe AIR macOS games (e.g. Detective Grimoire, 2014) on Apple Silicon / modern macOS. Bring your own legally purchased copy.

Reviving 32-bit Adobe AIR macOS games on Apple Silicon

A script that rebuilds a legacy Adobe AIR game you already own so it runs on modern macOS (Apple Silicon and Intel).

This ships no game content and no Adobe/Wipro code. It only transforms files already on your machine. You supply your own legally purchased copy.

Tested with Detective Grimoire (SFB Games, 2014). It may work with other Adobe AIR apps, but that is untested. Nothing is hardcoded, since the script reads the app's own descriptor for its identity and content, so other AIR desktop titles of the same era have a reasonable chance of working. Reports welcome.


Quick start (no coding experience needed)

You do not need to know anything about programming. This is copy and paste, and takes about five minutes. Nothing here modifies your Mac's system files, and your original game download is never changed.

Step 1: Get the game

Buy and download Detective Grimoire from the Humble Store, choosing the Mac download. You will get a file ending in .dmg, which normally lands in your Downloads folder.

The Steam version will not work for this, because Steam only offers the Windows build.

Step 2: Install the AIR SDK Manager

Download and install it from airsdk.dev, then drag it to your Applications folder like any other app. You do not need to open it or create an account. The script only borrows one file from it.

Step 3: Open Terminal

Press Command + Space, type Terminal, and press Return. A window with a text prompt appears. That is normal.

Step 4: Copy and paste these commands

Copy this whole block, paste it into Terminal, and press Return:

cd ~/Downloads
curl -O https://raw.githubusercontent.com/jmealo/air-game-apple-silicon/main/build.sh
chmod +x build.sh

Now run the build. If your download has a different filename, replace the part after --source with the correct one:

./build.sh --source ~/Downloads/DetectiveGrimoire.dmg --download-runtime

Tip: instead of typing the filename, you can type ./build.sh --source (with a space at the end), then drag your .dmg file from Finder into the Terminal window. It will fill in the path for you. Then type --download-runtime and press Return.

It may ask whether you want crisper Retina graphics. Either answer is fine, and off uses noticeably less battery.

Step 5: Play

When it finishes, it prints Done: and a path. Your game is now in your Applications folder. Open it like any other app.

If macOS says the app "cannot be opened" or is from an unidentified developer: right-click (or Control-click) the app and choose Open, then click Open in the dialog. You only need to do this once.

If you hear choppy or stuttering audio: check whether your Mac is playing sound through AirPlay to a TV or speaker. AirPlay breaks this game's audio. Option-click the volume icon in your menu bar and pick MacBook Pro Speakers (or any directly connected output).

If something goes wrong

Read the error message, since the script tries to explain what to do. The most common causes are a mistyped filename in Step 4, or skipping Step 2. If you are stuck, open an issue and paste what Terminal printed.


TL;DR (technical)

# 1. Install the free AIR SDK Manager (supplies the universal launcher stub)
#    https://airsdk.dev/download/airsdkmanager/?platform=macos

# 2. Build
./build.sh --source ~/Downloads/YourGame.dmg --download-runtime

Two things must both be right, or you get a black window with no error:

Component Requirement Why
Launcher stub Universal (arm64 + x86_64), from AIR SDK Manager The stub inside the AIR framework is x86_64-only and black-screens
AIR runtime 51.1.2.2 or older 51.3.3.1 black-screens legacy titles

That is the whole fix. Everything else in this README is diagnosis.

Where to buy the game

This script requires a copy you own. Please buy it. It is an excellent game by a small studio, and none of this works without their files.

Detective Grimoire (SFB Games):

  • Humble Store: DRM-free, cross-platform. This is the one you want, because it provides the macOS .dmg this script needs.
  • Official site, which links to all storefronts
  • Steam. Note the Steam build is Windows-only, so it will not give you a macOS installer.
  • iOS App Store
  • Google Play

Also worth your money: Tangle Tower, the studio's modern sequel. It is natively supported and needs none of this.

The problem

Open a 2014-era AIR game on a current Mac and nothing happens:

$ file "Install Detective Grimoire.app/Contents/MacOS/Install Detective Grimoire"
Mach-O executable i386

Both the installer and the game launcher are 32-bit i386. macOS dropped 32-bit execution in Catalina (10.15), so these cannot launch on any modern Mac. This is not Apple-Silicon-specific: an Intel Mac on a current OS fails identically, and installing Rosetta does not help, because Rosetta translates 64-bit x86_64 only.

Only the tiny native launcher is architecture-specific. The game itself is a SWF plus assets, which is portable bytecode. Replace the launcher, pair it with a working runtime, and it runs.

The AIR 51.3.3.1 regression

The most important finding here, and the one that cost the most time.

AIR runtime 51.3.3.1 renders legacy titles as a black window. The game launches, allocates memory, creates its save directory, runs ActionScript, and never draws. No error, no crash, no log entry.

Verified by swapping only the runtime inside an otherwise identical bundle:

Runtime Result CPU RSS
51.1.1.4 works ~93% 172 MB
51.1.2.2 works ~90% 98 MB
51.3.3.1 black screen ~0.8% 60 MB

All three are universal binaries (x86_64 arm64), so this is a genuine version regression and not an architecture mismatch.

(51.2.1.7 is referenced in places but is not actually published. That download returns a ~1 KB stub, so the regression lands somewhere in 51.2.x to 51.3.x.)

The CPU and memory figures are the practical tell: a working build sits near 100% of a core and 100 MB or more, while a black one idles near zero.

--download-runtime fetches a known-good runtime automatically. The script also checks the version you selected and warns before building something that will not render.

Worth noting that the AIR SDK Manager happens to bundle 51.1.1.4. That was luck, not design, and may change in future releases.

What the script does

  1. Finds the payload, by locating META-INF/AIR/application.xml inside the installer. That directory is a complete, unpacked AIR application.
  2. Reads the original identity, parsing <id>, <name>, <versionNumber> and the content SWF from the original descriptor. Reusing the original <id> matters, because AIR derives the save path (~/Library/Application Support/<id>) from it, so changing it orphans saves.
  3. Clones a donor shell for its universal launcher stub.
  4. Installs a known-good runtime into Contents/Frameworks/.
  5. Writes a modern descriptor (namespace 51.0) with explicit dimensions. This is a convenience, not a fix. See below.
  6. Rebrands Info.plist, resolves Retina, and ad-hoc code-signs.

Requirements

  • macOS 10.13+
  • AIR SDK Manager: download. Required for its universal launcher stub. Any AIR app with a universal stub and Contents/Frameworks/Adobe AIR.framework works via --runtime-donor.
  • Your own copy of the game.

Usage

chmod +x build.sh
./build.sh --source ~/Downloads/YourGame.dmg --download-runtime
--source <path>         .dmg, installer .app, or a directory      (required)
--runtime-donor <app>   default: /Applications/AIRSDKManager.app
--runtime <path>        an Adobe AIR.framework, an .app containing one,
                        or an AdobeAIR.dmg (mounted and extracted)
--download-runtime [v]  fetch a known-good runtime (default 51.1.2.2)
--output <path>         default: /Applications/<Game Name>.app
--width / --height      initial window size (default 1280x720)
--fullscreen            launch fullscreen
--render <mode>         direct | gpu | auto  (default: direct)
--hidpi <mode>          on | off | auto      (default: auto)
--force                 overwrite an existing bundle

Fullscreen, windowed mode, and save games were all verified working.

Retina / HiDPI

Legacy AIR titles were authored for 1x. Enabling HiDPI makes AIR rasterize at 2x, roughly 4x the pixels. Measured on an M2 Max, same build, only this flag changed:

HiDPI CPU
off ~95%
on ~240%

Roughly 2.5x. Crisper, but it can starve audio on a busy machine, which is why the original developers shipped NSHighResolutionCapable=false. auto detects a Retina display and asks, recommending on for Macs with 8 or more performance cores. Non-interactive runs default to off.


Things that are not the problem

Documented because each one cost real debugging time, and because guides elsewhere get some of these wrong.

The legacy descriptor is fine

A 2014 descriptor looks alarming:

<application xmlns="http://ns.adobe.com/air/application/14.0">
  <initialWindow>
    <fullScreen>true</fullScreen>
    <renderMode>direct</renderMode>
    <!-- no <width> / <height> -->

It is tempting to blame namespace 14.0, or fullScreen with no dimensions. It is not the cause. Tested directly: the untouched 2014 descriptor paired with a known-good runtime runs fine (~145% CPU, 99 MB). Namespace 14.0 is still accepted by current runtimes.

The script still writes a modern descriptor, but only so it can offer windowed mode and explicit window sizing. If you keep the original, it will work.

Render mode barely matters

direct, gpu, and auto all measured within noise of each other (~80 to 95% CPU). If you have a black window, changing render mode will not save you.

Rosetta does not help

The launcher is i386. Rosetta 2 translates x86_64 only. There is no path that runs a 32-bit binary on modern macOS.

Things that are the problem

The framework's Template stub is x86_64-only

The AIR framework ships Resources/Template.app/Contents/MacOS/Template, which looks like exactly the launcher you want. It is x86_64-only, and produces a black window even paired with a known-good runtime. The AIR SDK Manager's stub is universal and works. That is the entire reason a donor app is required.

If you pick a stub with no runtime to load, you get an honest dialog:

This application requires a version of Adobe AIR which cannot be found.

That message is a gift. A missing runtime is far easier to debug than a silent black window.

Repackaging as .air and using the official installer

This does work. The payload directory is a complete unpacked .air, and its self-signed certificate is typically valid for decades, so the signature still verifies. But the archive ordering is strict and absent from the error messages:

1. mimetype                        (STORED, not deflated)
2. META-INF/AIR/application.xml
3. META-INF/AIR/hash
4. META-INF/signatures.xml
5. ...content...

Also: no directory entries (zip -D) and no .DS_Store, or you get Found N files that were not listed in signatures.xml. And exclude Icon.icns, because the original Mac installer generates it at install time, so it is not in signatures.xml.

Every failure surfaces to the user as the same unhelpful dialog: "The application could not be installed because the installer file is damaged." The real error is in ~/Library/Application Support/Adobe/AIR/install.log.

Even after a clean install this route has a fatal flaw: an AIR-installed app ignores edits to its application.xml, because it uses a registered copy. You cannot iterate. A standalone bundle reads its descriptor directly, which is why the script builds one. The official installer also runs lipo -thin x86_64, stripping arm64 and forcing the game through Rosetta.

Bundle ID collisions

If you also own the iOS build (Apple Silicon Macs can run iPhone apps), it shares the game's bundle identifier. The AIR installer finds it, assumes a previous installation to upgrade, looks for AIR metadata inside an iOS bundle, and fails:

Info:  No app located in directory /Applications/<Game>.app
Error: Missing necessary file META-INF/AIR/application.xml

Remove or relocate the iOS build first.


Troubleshooting

Black window

  1. Check the runtime version. See the regression table above. This is the most likely cause, and --download-runtime fixes it.
  2. Check the stub is universal, with lipo -info "YourGame.app/Contents/MacOS/"*. It should list arm64.
  3. Verify AIR works at all by launching your donor app. If it renders, the runtime is fine and the problem is your bundle. Worth doing early, since it splits the problem cleanly in half.

Diagnosing without a screenshot

A black window looks identical to a hung process. Memory and CPU tell them apart:

ps -o pid,rss,%cpu -p $(pgrep -f "YourGame.app/Contents/MacOS" | head -1)

A game genuinely loading assets climbs to 100 MB or more and burns most of a core. One flat at ~30 to 60 MB and ~0% CPU never started.

Check whether it created its save directory, which proves ActionScript is running:

ls ~/Library/Application\ Support/<app.id>/Local\ Store/

Choppy or stuttering audio

Check your audio output device before blaming the game.

system_profiler SPAudioDataType | grep -A6 "Default Output Device: Yes"

If Transport: AirPlay appears, that is almost certainly the cause. AirPlay is network-streamed and buffered, so it stutters under CPU or network load. Streaming video tolerates that buffering; real-time game audio does not. Switch to built-in speakers by Option-clicking the volume icon.

The tell: if the game also stutters on your phone, it is not the game. A software bug cannot follow you across devices, but a shared speaker can.

Then check contention and memory pressure:

uptime                        # load average vs. core count
sysctl vm.swapusage           # near-full swap starves audio buffers
ps -Ao pid,%cpu,comm -r | head

Also try --hidpi off, which cuts CPU roughly 2.5x.

Gatekeeper

The bundle is ad-hoc signed, so moving it may trigger quarantine. Right-click and choose Open, or:

xattr -cr "/Applications/YourGame.app"
codesign --force --deep --sign - "/Applications/YourGame.app"

Legal

Contains no game assets, no Adobe code, and no Wipro/HARMAN code. It rebuilds software you already own to run on hardware you already own, and circumvents no copy protection, since legacy AIR packages are self-signed rather than encrypted. The optional --download-runtime flag fetches the AIR runtime from Wipro's own servers.

Adobe AIR is a trademark of Adobe. AIR is now maintained by Wipro Engineering Connected Services (formerly HARMAN DTS), which took over support after Adobe discontinued it in 2020. The SDK is commercially licensed per developer seat (free tier under $50k/yr revenue, which injects a splash screen; paid tiers $199 to $1,199/yr). Runtimes are free for end users. Check airsdk.harman.com and respect the licensing of whatever runtime you bundle before redistributing a built app.

#!/usr/bin/env bash
#
# build.sh: rebuild a legacy 32-bit Adobe AIR macOS game so it runs on
# modern macOS (Apple Silicon and Intel).
#
# You must own the game. This script redistributes no game, Adobe, or Wipro
# content -- it only transforms files you already have. The optional
# --download-runtime flag fetches an AIR runtime from Wipro's own servers.
#
# Tested with Detective Grimoire (SFB Games, 2014). May work with other Adobe
# AIR apps, but that is untested.
#
# See README.md for background on why each step is necessary.
set -euo pipefail
usage() {
cat <<'EOF'
Usage: ./build.sh --source <path> [options]
Required:
--source <path> The game you purchased. Accepts:
- a .dmg
- the installer .app bundle
- any directory containing the AIR payload
Options:
--runtime-donor <app> An installed AIR app that bundles a captive runtime in
Contents/Frameworks/Adobe AIR.framework. Supplies the
universal launcher stub.
Default: /Applications/AIRSDKManager.app
Get it free:
https://airsdk.dev/download/airsdkmanager/?platform=macos
--runtime <path> Use this runtime instead of the donor's. Accepts an
"Adobe AIR.framework" directory, an .app containing one,
or an AdobeAIR.dmg (it will be mounted and extracted).
--download-runtime [v] Download a known-good AIR runtime and use it.
Default version: 51.1.2.2 (see KNOWN ISSUES below)
--output <path> Where to write the .app.
Default: /Applications/<Game Name>.app
--width <px> Initial window width (default 1280)
--height <px> Initial window height (default 720)
--fullscreen Launch fullscreen (default: windowed). Verified
working; fullScreen is NOT a cause of black windows.
--render <mode> direct | gpu | auto (default: direct)
--hidpi <mode> on | off | auto (default: auto)
Retina rendering. Crisper, but measured ~2.5x the CPU
on a 2014-era title. 'auto' detects your display and
asks. Non-interactive runs default to off.
--force Overwrite an existing output bundle
-h, --help Show this help
Examples:
./build.sh --source ~/Downloads/DetectiveGrimoire.dmg
./build.sh --source ~/Downloads/DetectiveGrimoire.dmg --download-runtime
./build.sh --source ~/Downloads/Game.dmg --runtime ~/Downloads/AdobeAIR.dmg
KNOWN ISSUES
AIR runtime 51.3.3.1 renders legacy titles as a black window: the game loads
and allocates memory but never draws, with no error. Verified against
Detective Grimoire (2014), swapping only the runtime:
51.1.1.4 works (~93% CPU, 172MB)
51.1.2.2 works (~90% CPU, 98MB)
51.3.3.1 black screen (~0.8% CPU, 60MB)
This script warns if the runtime you selected is a known-bad version.
Use --download-runtime to fetch a known-good build automatically.
EOF
}
SOURCE=""
DONOR="/Applications/AIRSDKManager.app"
OUTPUT=""
WIDTH=1280
HEIGHT=720
FULLSCREEN=false
RENDER="direct"
HIDPI="auto"
FORCE=false
RUNTIME_SRC=""
DOWNLOAD_RUNTIME=""
# Runtime versions verified against Detective Grimoire (2014).
GOOD_RUNTIME_DEFAULT="51.1.2.2"
KNOWN_GOOD_RUNTIMES="51.1.1.4 51.1.2.2"
KNOWN_BAD_RUNTIMES="51.3.3.1"
RUNTIME_URL_TEMPLATE="https://airsdk.harman.com/assets/downloads/%s/AdobeAIR.dmg"
while [[ $# -gt 0 ]]; do
case "$1" in
--source) SOURCE="${2:-}"; shift 2 ;;
--runtime-donor) DONOR="${2:-}"; shift 2 ;;
--runtime) RUNTIME_SRC="${2:-}"; shift 2 ;;
--download-runtime)
# optional version argument
if [[ -n "${2:-}" && "${2:-}" != --* ]]; then
DOWNLOAD_RUNTIME="$2"; shift 2
else
DOWNLOAD_RUNTIME="$GOOD_RUNTIME_DEFAULT"; shift
fi ;;
--output) OUTPUT="${2:-}"; shift 2 ;;
--width) WIDTH="${2:-}"; shift 2 ;;
--height) HEIGHT="${2:-}"; shift 2 ;;
--render) RENDER="${2:-}"; shift 2 ;;
--hidpi) HIDPI="${2:-}"; shift 2 ;;
--fullscreen) FULLSCREEN=true; shift ;;
--force) FORCE=true; shift ;;
-h|--help) usage; exit 0 ;;
*) echo "error: unknown option '$1'" >&2; usage >&2; exit 2 ;;
esac
done
[[ -n "$SOURCE" ]] || { echo "error: --source is required" >&2; usage >&2; exit 2; }
[[ -e "$SOURCE" ]] || { echo "error: source not found: $SOURCE" >&2; exit 2; }
say() { printf '\033[1;34m==>\033[0m %s\n' "$*"; }
warn() { printf '\033[1;33mwarning:\033[0m %s\n' "$*" >&2; }
die() { printf '\033[1;31merror:\033[0m %s\n' "$*" >&2; exit 1; }
MOUNTPOINT=""
cleanup() {
if [[ -n "$MOUNTPOINT" && -d "$MOUNTPOINT" ]]; then
hdiutil detach "$MOUNTPOINT" -quiet 2>/dev/null || true
fi
}
trap cleanup EXIT
# ---------------------------------------------------------------------------
# 1. Resolve the source into a searchable directory
# ---------------------------------------------------------------------------
SEARCH_ROOT="$SOURCE"
if [[ "$SOURCE" == *.dmg ]]; then
say "Mounting $(basename "$SOURCE")"
MOUNTPOINT="$(mktemp -d /tmp/airgame.XXXXXX)"
hdiutil attach "$SOURCE" -nobrowse -readonly -mountpoint "$MOUNTPOINT" -quiet \
|| die "could not mount disk image"
SEARCH_ROOT="$MOUNTPOINT"
fi
# ---------------------------------------------------------------------------
# 2. Locate the AIR payload
#
# An AIR app's payload is the directory holding META-INF/AIR/application.xml
# alongside the content SWF. In a legacy installer this sits nested inside the
# installer bundle, so search rather than assume a fixed depth.
# ---------------------------------------------------------------------------
say "Locating AIR payload"
DESCRIPTOR=""
while IFS= read -r candidate; do
[[ -n "$candidate" ]] || continue
DESCRIPTOR="$candidate"
break
done < <(find "$SEARCH_ROOT" -type f -path '*/META-INF/AIR/application.xml' 2>/dev/null | sort)
[[ -n "$DESCRIPTOR" ]] || die "no META-INF/AIR/application.xml found under: $SOURCE
Is this actually an Adobe AIR application?"
PAYLOAD="$(cd "$(dirname "$DESCRIPTOR")/../.." && pwd)"
say "Payload: $PAYLOAD"
# ---------------------------------------------------------------------------
# 3. Read identity out of the ORIGINAL descriptor
#
# Reusing the original <id> matters: AIR derives the save-game location
# (~/Library/Application Support/<id>) from it, so changing it orphans saves.
# ---------------------------------------------------------------------------
read_descriptor() {
python3 - "$DESCRIPTOR" <<'PY'
import sys, xml.etree.ElementTree as ET
root = ET.parse(sys.argv[1]).getroot()
ns = {'a': root.tag.split('}')[0].strip('{')} if '}' in root.tag else {}
def get(path, default=''):
el = root.find(path, ns) if ns else root.find(path)
return (el.text or '').strip() if el is not None and el.text else default
win = 'a:initialWindow/' if ns else 'initialWindow/'
app_id = get('a:id' if ns else 'id')
name = get('a:name' if ns else 'name') or app_id.split('.')[-1]
content= get(win + ('a:content' if ns else 'content'))
ver = get('a:versionNumber' if ns else 'versionNumber') or \
get('a:version' if ns else 'version') or '1.0.0'
print('\t'.join([app_id, name, content, ver]))
PY
}
IFS=$'\t' read -r APP_ID APP_NAME CONTENT_SWF APP_VERSION < <(read_descriptor) \
|| die "could not parse the original application.xml"
[[ -n "$APP_ID" ]] || die "descriptor has no <id>"
[[ -n "$CONTENT_SWF" ]] || die "descriptor has no <initialWindow><content>"
[[ -f "$PAYLOAD/$CONTENT_SWF" ]] || die "content SWF missing from payload: $CONTENT_SWF"
say "Detected: $APP_NAME ($APP_ID) v$APP_VERSION -> $CONTENT_SWF"
[[ -n "$OUTPUT" ]] || OUTPUT="/Applications/$APP_NAME.app"
# ---------------------------------------------------------------------------
# 4. Validate the runtime donor
#
# This is the crux. The system-installed AIR framework may render legacy games
# as a black window. A *captive* runtime bundled inside the .app works. We copy
# one out of an AIR app you already have installed.
# ---------------------------------------------------------------------------
[[ -d "$DONOR" ]] || die "runtime donor not found: $DONOR
Install the free AIR SDK Manager from https://airsdk.harman.com/
or pass a different AIR app with --runtime-donor."
DONOR_FW="$DONOR/Contents/Frameworks/Adobe AIR.framework"
[[ -d "$DONOR_FW" ]] || die "donor has no captive runtime at:
$DONOR_FW
Pick an AIR app that bundles its own runtime (--runtime-donor)."
DONOR_PLIST="$DONOR/Contents/Info.plist"
DONOR_STUB_NAME="$(plutil -extract CFBundleExecutable raw "$DONOR_PLIST" 2>/dev/null || true)"
DONOR_STUB="$DONOR/Contents/MacOS/$DONOR_STUB_NAME"
[[ -f "$DONOR_STUB" ]] || die "donor launcher stub not found: $DONOR_STUB"
if ! lipo -info "$DONOR_STUB" 2>/dev/null | grep -q 'arm64\|x86_64'; then
warn "donor stub architecture looks unusual; continuing anyway"
fi
say "Runtime donor: $DONOR ($DONOR_STUB_NAME)"
# ---------------------------------------------------------------------------
# 4a. Resolve which AIR runtime to bundle
#
# The donor supplies the launcher stub AND a default runtime, but the runtime
# can be overridden -- which matters, because some runtime versions render
# legacy titles as a black window. See KNOWN ISSUES in --help.
# ---------------------------------------------------------------------------
RUNTIME_TMP=""
cleanup_runtime() {
[[ -n "$RUNTIME_TMP" && -d "$RUNTIME_TMP" ]] && rm -rf "$RUNTIME_TMP"
return 0
}
trap 'cleanup; cleanup_runtime' EXIT
fw_version() {
plutil -extract CFBundleVersion raw "$1/Versions/1.0/Resources/Info.plist" 2>/dev/null \
|| plutil -extract CFBundleVersion raw "$1/Resources/Info.plist" 2>/dev/null \
|| echo "unknown"
}
# Pull an "Adobe AIR.framework" out of a .dmg
extract_framework_from_dmg() {
local dmg="$1" dest="$2" mp fw
# A missing/redirected download yields a tiny HTML file; catch that early
# with a clearer message than hdiutil's.
if [[ $(stat -f%z "$dmg" 2>/dev/null || echo 0) -lt 1000000 ]]; then
die "not a usable disk image (only $(du -h "$dmg" | cut -f1)): $dmg
That version is probably not published. Try --download-runtime $GOOD_RUNTIME_DEFAULT"
fi
mp="$(mktemp -d /tmp/airrt.XXXXXX)"
hdiutil attach "$dmg" -nobrowse -readonly -mountpoint "$mp" -quiet 2>/dev/null \
|| { rm -rf "$mp"; die "could not mount $dmg"; }
fw="$(find "$mp" -maxdepth 6 -type d -name 'Adobe AIR.framework' 2>/dev/null | head -1)"
if [[ -z "$fw" ]]; then
hdiutil detach "$mp" -quiet 2>/dev/null || true; rm -rf "$mp"
die "no 'Adobe AIR.framework' inside $dmg"
fi
ditto "$fw" "$dest"
hdiutil detach "$mp" -quiet 2>/dev/null || true
rm -rf "$mp"
}
if [[ -n "$DOWNLOAD_RUNTIME" ]]; then
RUNTIME_TMP="$(mktemp -d /tmp/airdl.XXXXXX)"
URL="$(printf "$RUNTIME_URL_TEMPLATE" "$DOWNLOAD_RUNTIME")"
say "Downloading AIR runtime $DOWNLOAD_RUNTIME"
curl -fSL --progress-bar --max-time 600 -o "$RUNTIME_TMP/AdobeAIR.dmg" "$URL" \
|| die "download failed: $URL"
extract_framework_from_dmg "$RUNTIME_TMP/AdobeAIR.dmg" "$RUNTIME_TMP/Adobe AIR.framework"
RUNTIME_FW="$RUNTIME_TMP/Adobe AIR.framework"
elif [[ -n "$RUNTIME_SRC" ]]; then
[[ -e "$RUNTIME_SRC" ]] || die "--runtime not found: $RUNTIME_SRC"
case "$RUNTIME_SRC" in
*.dmg)
RUNTIME_TMP="$(mktemp -d /tmp/airrt.XXXXXX)"
extract_framework_from_dmg "$RUNTIME_SRC" "$RUNTIME_TMP/Adobe AIR.framework"
RUNTIME_FW="$RUNTIME_TMP/Adobe AIR.framework" ;;
*.framework|*.framework/)
RUNTIME_FW="${RUNTIME_SRC%/}" ;;
*.app|*.app/)
RUNTIME_FW="${RUNTIME_SRC%/}/Contents/Frameworks/Adobe AIR.framework"
[[ -d "$RUNTIME_FW" ]] || die "no captive runtime inside $RUNTIME_SRC" ;;
*)
# The path may itself be a framework under a different name, or a
# directory containing one.
if [[ -d "$RUNTIME_SRC/Versions" || -f "$RUNTIME_SRC/Resources/Info.plist" ]]; then
RUNTIME_FW="${RUNTIME_SRC%/}"
else
RUNTIME_FW="$(find "$RUNTIME_SRC" -maxdepth 4 -type d -name 'Adobe AIR.framework' 2>/dev/null | head -1)"
[[ -n "$RUNTIME_FW" ]] || die "no 'Adobe AIR.framework' found under $RUNTIME_SRC"
fi ;;
esac
else
RUNTIME_FW="$DONOR_FW"
fi
RUNTIME_VERSION="$(fw_version "$RUNTIME_FW")"
say "AIR runtime: $RUNTIME_VERSION"
for bad in $KNOWN_BAD_RUNTIMES; do
if [[ "$RUNTIME_VERSION" == "$bad" ]]; then
warn "AIR $RUNTIME_VERSION is known to render legacy titles as a BLACK WINDOW."
echo " Known-good versions: $KNOWN_GOOD_RUNTIMES" >&2
echo " Re-run with: --download-runtime $GOOD_RUNTIME_DEFAULT" >&2
if [[ -t 0 ]]; then
read -r -p " Continue anyway? [y/N]: " GO || GO=""
[[ "$GO" =~ ^([yY]|[yY][eE][sS])$ ]] || die "aborted at user request"
else
warn "continuing (non-interactive), but the result will probably not render"
fi
fi
done
# ---------------------------------------------------------------------------
# 4b. Decide Retina (HiDPI) rendering
#
# Legacy AIR titles were authored for 1x. Enabling HiDPI makes AIR rasterize at
# 2x, which is ~4x the pixels. Benchmarked on a 2014 title on an M2 Max:
# HiDPI off -> ~95% CPU HiDPI on -> ~240% CPU
# Crisper, but it can starve audio on a busy machine. The original developers
# shipped these games with NSHighResolutionCapable=false for this reason.
# ---------------------------------------------------------------------------
case "$HIDPI" in
on) HIDPI_BOOL=true ;;
off) HIDPI_BOOL=false ;;
auto)
IS_RETINA=false
if system_profiler SPDisplaysDataType 2>/dev/null \
| grep -qiE 'Retina|UI Looks like'; then
IS_RETINA=true
fi
# Performance-core count; falls back to total CPUs on Intel.
PCORES="$(sysctl -n hw.perflevel0.logicalcpu 2>/dev/null || true)"
[[ -n "$PCORES" ]] || PCORES="$(sysctl -n hw.ncpu 2>/dev/null || echo 4)"
if ! $IS_RETINA; then
HIDPI_BOOL=false
say "Display is not Retina - HiDPI off (no visual benefit)"
elif [[ ! -t 0 ]]; then
HIDPI_BOOL=false
say "Retina display detected; non-interactive run - defaulting HiDPI off"
echo " (re-run with --hidpi on for crisper visuals)"
else
if (( PCORES >= 8 )); then RECOMMEND=on; else RECOMMEND=off; fi
echo
echo " Retina display detected (${PCORES} performance cores)."
echo " on - crisper visuals, roughly 2.5x the CPU"
echo " off - softer upscaling, much lighter; how the game shipped"
echo " Recommended for this Mac: $RECOMMEND"
echo
read -r -p " Enable Retina/HiDPI rendering? [on/off] (default $RECOMMEND): " ANSWER || ANSWER=""
ANSWER="$(echo "${ANSWER:-$RECOMMEND}" | tr '[:upper:]' '[:lower:]')"
case "$ANSWER" in
on|y|yes|true) HIDPI_BOOL=true ;;
off|n|no|false) HIDPI_BOOL=false ;;
*) warn "unrecognized answer '$ANSWER'; using $RECOMMEND"
[[ "$RECOMMEND" == on ]] && HIDPI_BOOL=true || HIDPI_BOOL=false ;;
esac
fi
;;
*) die "--hidpi must be one of: on, off, auto (got '$HIDPI')" ;;
esac
say "HiDPI: $HIDPI_BOOL"
# ---------------------------------------------------------------------------
# 5. Assemble the bundle
# ---------------------------------------------------------------------------
if [[ -e "$OUTPUT" ]]; then
$FORCE || die "output already exists: $OUTPUT (use --force to replace)"
say "Removing existing $OUTPUT"
rm -rf "$OUTPUT"
fi
say "Cloning donor shell (stub + captive runtime)"
ditto "$DONOR" "$OUTPUT"
# Strip the donor's own payload, keep Contents/Frameworks and Contents/MacOS.
rm -rf "$OUTPUT/Contents/Resources" "$OUTPUT/Contents/_CodeSignature"
mkdir -p "$OUTPUT/Contents/Resources/META-INF/AIR"
# Swap in a different runtime if one was requested.
if [[ "$RUNTIME_FW" != "$DONOR_FW" ]]; then
say "Installing AIR runtime $RUNTIME_VERSION into the bundle"
rm -rf "$OUTPUT/Contents/Frameworks/Adobe AIR.framework"
ditto "$RUNTIME_FW" "$OUTPUT/Contents/Frameworks/Adobe AIR.framework"
fi
say "Copying game payload"
# Copy everything except AIR metadata, which we regenerate below.
( cd "$PAYLOAD" && find . -mindepth 1 -maxdepth 1 \
! -name 'META-INF' ! -name '.DS_Store' -print0 ) \
| while IFS= read -r -d '' item; do
ditto "$PAYLOAD/${item#./}" "$OUTPUT/Contents/Resources/${item#./}"
done
# ---------------------------------------------------------------------------
# 6. Write a modern descriptor
#
# NOTE: the legacy descriptor is NOT a cause of black windows. Tested directly,
# the untouched 2014 descriptor (namespace 14.0, fullScreen, no dimensions) runs
# fine on a known-good runtime. We rewrite it only to offer windowed mode and
# explicit window sizing.
# ---------------------------------------------------------------------------
say "Writing modern descriptor (namespace 51.0)"
{
printf '<?xml version="1.0" encoding="utf-8" ?>\n'
printf '<application xmlns="http://ns.adobe.com/air/application/51.0">\n'
printf ' <id>%s</id>\n' "$APP_ID"
printf ' <name>%s</name>\n' "$APP_NAME"
printf ' <versionNumber>%s</versionNumber>\n' "$APP_VERSION"
printf ' <filename>%s</filename>\n' "$APP_NAME"
printf ' <supportedProfiles>extendedDesktop desktop</supportedProfiles>\n'
printf ' <initialWindow>\n'
printf ' <title>%s</title>\n' "$APP_NAME"
printf ' <content>%s</content>\n' "$CONTENT_SWF"
printf ' <visible>true</visible>\n'
printf ' <fullScreen>%s</fullScreen>\n' "$FULLSCREEN"
printf ' <width>%s</width>\n' "$WIDTH"
printf ' <height>%s</height>\n' "$HEIGHT"
printf ' <resizable>true</resizable>\n'
printf ' <renderMode>%s</renderMode>\n' "$RENDER"
printf ' </initialWindow>\n'
printf '</application>\n'
} > "$OUTPUT/Contents/Resources/META-INF/AIR/application.xml"
# ---------------------------------------------------------------------------
# 7. Rebrand Info.plist (keep CFBundleExecutable: it must match the stub)
# ---------------------------------------------------------------------------
say "Rebranding Info.plist"
P="$OUTPUT/Contents/Info.plist"
plutil -replace CFBundleIdentifier -string "$APP_ID" "$P"
plutil -replace CFBundleName -string "$APP_NAME" "$P"
plutil -replace CFBundleDisplayName -string "$APP_NAME" "$P"
plutil -replace CFBundleShortVersionString -string "$APP_VERSION" "$P"
plutil -replace CFBundleVersion -string "$APP_VERSION" "$P"
plutil -replace NSHighResolutionCapable -bool "$HIDPI_BOOL" "$P"
if [[ -f "$OUTPUT/Contents/Resources/Icon.icns" ]]; then
plutil -replace CFBundleIconFile -string "Icon.icns" "$P"
else
warn "no Icon.icns in payload; the app will use a generic icon"
fi
printf 'APPL????' > "$OUTPUT/Contents/PkgInfo"
# ---------------------------------------------------------------------------
# 8. Sign
#
# Ad-hoc signature. Required: an unsigned bundle with a mismatched signature
# will be killed by Gatekeeper on launch.
# ---------------------------------------------------------------------------
say "Signing (ad-hoc)"
xattr -cr "$OUTPUT"
codesign --force --deep --sign - "$OUTPUT" >/dev/null 2>&1 \
|| die "codesign failed"
codesign --verify --deep "$OUTPUT" 2>/dev/null \
|| warn "signature verification reported problems; the app may still run"
say "Done: $OUTPUT"
echo
echo "Launch it with: open \"$OUTPUT\""
echo
echo "If you get a black window, see the Troubleshooting section in README.md."
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment