diff --git a/dm/ios/tests/test_ipa_unsigned.py b/dm/ios/tests/test_ipa_unsigned.py
index 028d506706..5110e3ad50 100644
--- a/dm/ios/tests/test_ipa_unsigned.py
+++ b/dm/ios/tests/test_ipa_unsigned.py
@@ -1,7 +1,8 @@
-"""The unsigned IPA of build_ipa.sh: the game files stay out with DM_COPY_ROMFS=NO, and the executable carries the entitlements in an ad-hoc signature.
-Only the archive step and the signing step run (a stand-in app, no Xcode or game files)."""
+"""The unsigned IPA of build_ipa.sh: the game files stay out with DM_COPY_ROMFS=NO, the executable carries the entitlements in an ad-hoc signature, and the Info.plist names the shared and the game's own URL scheme.
+Only the archive step, the signing step and the Info.plist block run (a stand-in app, no Xcode or game files)."""
import os
from pathlib import Path
+import plistlib
import shutil
import subprocess
import sys
@@ -19,13 +20,18 @@
SCRIPT = (REPO / 'dm/ios/tools/build_ipa.sh').read_text()
SIGNING = SCRIPT[SCRIPT.index('# The entitlements of an unsigned IPA'):SCRIPT.index('\nP=Payload/DmIOS.app\n')]
PACKING = SCRIPT[SCRIPT.index('\nP=Payload/DmIOS.app\n'):]
+SCHEMES = SCRIPT[SCRIPT.index('# Game handoff: register the shared scheme'):SCRIPT.index('# DM_METAL_CAPTURE=1')]
+PLIST = ('\n'
+ '\n'
+ 'CFBundleIdentifierdev.nxnative.fixture\n')
PREAMBLE = 'set -u\nt() { :; }; die() { echo "$*" >&2; exit 1; }\ngp() { python3 "$PROFILE_TOOL" "$GAME" "$1"; }\n'
CAN_SIGN = sys.platform == 'darwin' and shutil.which('codesign') and shutil.which('cc')
def run_step(script, root, **extra):
env = {k: v for k, v in os.environ.items() if not k.startswith('DM_') and k not in ('IOS_PULL', 'AOC', 'PSO')}
- env.update(GAME=GAME, DM=str(root / 'dm'), PROFILE_TOOL=str(REPO / 'dm/games/game.py'), **extra)
+ env.update({'GAME': GAME, 'DM': str(root / 'dm'), 'PROFILE_TOOL': str(REPO / 'dm/games/game.py')})
+ env.update(extra)
return subprocess.run(['/bin/bash', '-c', PREAMBLE + script], cwd=root, env=env, capture_output=True, text=True)
@@ -97,5 +103,37 @@ def test_executable_carries_the_entitlements(self):
self.assertEqual('com.apple.developer.kernel.increased-memory-limit' in shown.stdout + shown.stderr, expected)
+class UrlSchemeTests(unittest.TestCase):
+ """The Info.plist block build_ipa.sh runs after xcodegen: the shared scheme and this game's own."""
+
+ def plist(self, root, game_id=GAME):
+ (root / 'Gen').mkdir()
+ path = root / 'Gen/Info.plist'
+ path.write_text(PLIST)
+ return path, run_step(SCHEMES, root, GAME=game_id)
+
+ def test_one_entry_names_the_shared_and_the_game_scheme(self):
+ for game_id in (GAME, '0100A6301214E000'):
+ with self.subTest(game=game_id), tempfile.TemporaryDirectory() as temporary:
+ path, result = self.plist(Path(temporary), game_id)
+ self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
+ plist = plistlib.loads(path.read_bytes())
+ self.assertEqual(plist['CFBundleURLTypes'], [{
+ 'CFBundleURLName': 'dev.nxnative',
+ 'CFBundleURLSchemes': ['nxnative', 'nxnative-' + game_id.lower()]}])
+ self.assertEqual(plist['CFBundleIdentifier'], 'dev.nxnative.fixture') # nothing else is touched
+
+ def test_a_plist_that_already_names_schemes_stops_the_build(self):
+ with tempfile.TemporaryDirectory() as temporary:
+ root = Path(temporary)
+ path, result = self.plist(root)
+ self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
+ before = path.read_bytes()
+ result = run_step(SCHEMES, root)
+ self.assertNotEqual(result.returncode, 0) # die "URL schemes": no silent second entry for one bundle
+ self.assertIn('URL schemes', result.stderr)
+ self.assertEqual(path.read_bytes(), before)
+
+
if __name__ == '__main__':
unittest.main()
diff --git a/dm/ios/tools/build_ipa.sh b/dm/ios/tools/build_ipa.sh
index 57a16dbc23..3c27b24b7a 100755
--- a/dm/ios/tools/build_ipa.sh
+++ b/dm/ios/tools/build_ipa.sh
@@ -191,6 +191,14 @@ xcodegen generate -q > /dev/null || die "xcodegen"
COMMIT=$(git -C "$DM" rev-parse --short=12 HEAD 2> /dev/null) && { [ -z "$(git -C "$DM" status --porcelain --untracked-files=no 2> /dev/null)" ] || COMMIT=$COMMIT+dirty
/usr/libexec/PlistBuddy -c "Add :DMCommit string $COMMIT" Gen/Info.plist > /dev/null || die "DMCommit"; t "source: $COMMIT"; }
case "$MSLDIR" in b-*) /usr/libexec/PlistBuddy -c "Add :DMChain string ${MSLDIR#b-}" Gen/Info.plist > /dev/null || die "DMChain";; esac
+# Game handoff: register the shared scheme and this game's own scheme
+# (nxnative-
) so a launcher can detect and open exactly this game.
+/usr/libexec/PlistBuddy -c "Add :CFBundleURLTypes array" Gen/Info.plist > /dev/null || die "URL schemes"
+/usr/libexec/PlistBuddy -c "Add :CFBundleURLTypes:0 dict" Gen/Info.plist > /dev/null || die "URL schemes"
+/usr/libexec/PlistBuddy -c "Add :CFBundleURLTypes:0:CFBundleURLName string dev.nxnative" Gen/Info.plist > /dev/null || die "URL schemes"
+/usr/libexec/PlistBuddy -c "Add :CFBundleURLTypes:0:CFBundleURLSchemes array" Gen/Info.plist > /dev/null || die "URL schemes"
+/usr/libexec/PlistBuddy -c "Add :CFBundleURLTypes:0:CFBundleURLSchemes:0 string nxnative" Gen/Info.plist > /dev/null || die "URL schemes"
+/usr/libexec/PlistBuddy -c "Add :CFBundleURLTypes:0:CFBundleURLSchemes:1 string nxnative-$(echo "$GAME" | tr 'A-Z' 'a-z')" Gen/Info.plist > /dev/null || die "URL schemes"
# DM_METAL_CAPTURE=1: a development build that may write GPU frame captures (MetalCaptureEnabled; the host's DM_GPU_CAPTURE switch writes them to Documents/gpu_captures)
if [ "${DM_METAL_CAPTURE:-0}" = 1 ]; then /usr/libexec/PlistBuddy -c "Add :MetalCaptureEnabled bool true" Gen/Info.plist > /dev/null || die "MetalCaptureEnabled"; t "Metal frame capture: enabled (DM_METAL_CAPTURE=1)"; fi
RP=; if [ -n "${PROGRAMS_TSV:-}" ]; then # a game of several programs: each program's files, which the Xcode build phase copies under p as the IPA has them (stage_resources.sh)