mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-12 00:10:56 +02:00
Merge pull request #1058 from castdrian/ios-documents
feat(ios): expose app data in Documents dir
This commit is contained in:
+87
-104
@@ -1,130 +1,113 @@
|
||||
# iOS build (LÖVE 12.0)
|
||||
# iOS build
|
||||
|
||||
> **Native ROM/mod/save import.** The iOS build ships a Swift
|
||||
> document-picker bridge (`native/GRPickerBridge.swift` + `GRBootstrap.m`)
|
||||
> that `patch_love_src.py` wires into the LÖVE tree on every build:
|
||||
>
|
||||
> - `love.system.pickFile("rom"|"mod"|"sav")` and `love.system.createFile`
|
||||
> are exposed to Lua on iOS (same contract as love-android's SAF picker:
|
||||
> picks land in the save dir as `picked_rom.gb` / `picked_mod.zip` /
|
||||
> `picked_save.sav`; exports signal via `export_done.flag`).
|
||||
> - The Info.plist overlay enables `UIFileSharingEnabled` +
|
||||
> `LSSupportsOpeningDocumentsInPlace`, and `GRBootstrap.m` sweeps
|
||||
> `.gb/.gbc/.zip/.sav` files dropped in Documents (Files app / Finder)
|
||||
> into the LÖVE save dir on every activation — drop a ROM, open the app,
|
||||
> and it imports with no taps.
|
||||
> - `src/import/RomImporter.lua` treats iOS as a mobile platform and polls
|
||||
> for picker results (iOS pickers are in-process modals, so Android's
|
||||
> refocus rescan never fires).
|
||||
>
|
||||
> The note below about a missing "UIDocumentPicker handoff" is
|
||||
> resolved by this bridge.
|
||||
This directory contains the macOS/Xcode build used to package Gen1 Recomp as
|
||||
an iOS app with LÖVE 12.0.
|
||||
|
||||
macOS + Xcode only. Fetches the **LÖVE 12.0** source tree and matching Apple
|
||||
dependencies from the official [LÖVE source](https://github.com/love2d/love)
|
||||
and [Apple dependencies](https://github.com/love2d/love-apple-dependencies)
|
||||
repositories. `conf.lua` declares LÖVE 12.0 on iOS and 11.5 elsewhere.
|
||||
## User data location
|
||||
|
||||
Pin file: [`LOVE_VERSION`](./LOVE_VERSION) → `12.0`.
|
||||
The app uses the public iOS Documents directory as its LÖVE save directory.
|
||||
There is no `pokemon-love2d` subdirectory and the app does not create a
|
||||
README file there. When browsing `On My iPhone > gen1recomp` in Files, the
|
||||
directory contains the app's runtime data directly, including:
|
||||
|
||||
## Quick start (simulator)
|
||||
- installed mods and downloaded ROMs
|
||||
- save files and save-state data
|
||||
- options, caches, logs, and other files created by the game
|
||||
|
||||
The build enables `UIFileSharingEnabled` and
|
||||
`LSSupportsOpeningDocumentsInPlace`, so the same directory is available in
|
||||
Files and Finder. Files copied into the app's Documents directory are used by
|
||||
the game on its next activation.
|
||||
|
||||
Existing installations are migrated automatically. Files from the old
|
||||
private `Application Support/pokemon-love2d` directory are merged into
|
||||
Documents on launch; conflicts are retained with a `.legacy` suffix.
|
||||
|
||||
## Build
|
||||
|
||||
Run these commands from the repository root:
|
||||
|
||||
```bash
|
||||
# Fetch LÖVE 12.0 iOS sources and dependencies (once) + build for Simulator
|
||||
scripts/build_ios.sh --fetch
|
||||
scripts/build_ios.sh
|
||||
```
|
||||
|
||||
The embedded `game.love` contains no ROM or generated game data. The current
|
||||
first-boot importer has desktop file pickers only, so a production iOS release
|
||||
still needs a UIDocumentPicker handoff that passes the selected ROM to LÖVE.
|
||||
`--fetch` downloads the pinned LÖVE source and matching Apple dependencies
|
||||
into the gitignored `love-src/` directory. It is only needed when that tree is
|
||||
missing. The default build targets the iOS Simulator in Debug configuration.
|
||||
|
||||
Default output: an unsigned Simulator `.app` under `mobile/ios/build/`
|
||||
(no Apple Developer account required). A convenience copy also lands under
|
||||
`dist/ios/<Config>-<sdk>/`.
|
||||
|
||||
Install on a booted simulator (example):
|
||||
For a physical device or a release build:
|
||||
|
||||
```bash
|
||||
xcrun simctl install booted mobile/ios/build/Build/Products/Debug-iphonesimulator/PokemonRed.app
|
||||
xcrun simctl launch booted com.theboisclub.pokemonred
|
||||
scripts/build_ios.sh --device --install
|
||||
scripts/build_ios.sh --device --release --install
|
||||
```
|
||||
|
||||
Or open `mobile/ios/love-src/platform/xcode/love.xcodeproj` in Xcode,
|
||||
select the `love-ios` target, and Run on a Simulator after
|
||||
`scripts/build_ios.sh --package-only` (or a full build) has placed `game.love`.
|
||||
Device builds require a paired, unlocked device and a valid Apple signing
|
||||
identity. Set `DEVELOPMENT_TEAM` or `CODE_SIGN_IDENTITY` when automatic
|
||||
signing cannot select the intended account. Add `--ipa` to create
|
||||
`dist/ios/gen1recomp.ipa`.
|
||||
|
||||
## Device / Release
|
||||
The script verifies the final app before packaging it:
|
||||
|
||||
```bash
|
||||
scripts/build_ios.sh --device # Debug, physical device SDK
|
||||
scripts/build_ios.sh --device --release # Release configuration
|
||||
- the public Documents plist settings are present
|
||||
- the native picker bridge is present
|
||||
- `game.love` exists and is non-empty
|
||||
|
||||
If the payload is missing, the build fails instead of producing a blank app.
|
||||
|
||||
## Useful options
|
||||
|
||||
| Option | Purpose |
|
||||
| --- | --- |
|
||||
| `--fetch` | Fetch LÖVE 12.0 and Apple dependencies when `love-src/` is missing |
|
||||
| `--device` | Build for `iphoneos` instead of the Simulator |
|
||||
| `--release` | Use the Release configuration |
|
||||
| `--install` | Install a device build on the first connected device |
|
||||
| `--ipa` | Create an IPA after a device build |
|
||||
| `--version X.Y.Z` | Stamp the engine and app version |
|
||||
| `--package-only` | Package `game.love` and apply the iOS plist overlay without Xcode |
|
||||
|
||||
`scripts/build.sh ios` delegates to this script and forwards the iOS release
|
||||
option.
|
||||
|
||||
## Output
|
||||
|
||||
Simulator and device app bundles are copied to:
|
||||
|
||||
```text
|
||||
dist/ios/Debug-iphonesimulator/gen1recomp.app
|
||||
dist/ios/Release-iphonesimulator/gen1recomp.app
|
||||
dist/ios/Debug-iphoneos/gen1recomp.app
|
||||
dist/ios/Release-iphoneos/gen1recomp.app
|
||||
```
|
||||
|
||||
Device builds need a signing identity and provisioning profile configured in
|
||||
Xcode (or via `DEVELOPMENT_TEAM` / `CODE_SIGN_IDENTITY` env vars). This repo
|
||||
does **not** store certificates, profiles, or App Store Connect secrets.
|
||||
The intermediate Xcode products are under `mobile/ios/build/`. Both locations
|
||||
are gitignored.
|
||||
|
||||
Manual out-of-band steps:
|
||||
|
||||
1. Apple Developer account + App ID for `com.theboisclub.pokemonred`
|
||||
2. Development or Distribution certificate + provisioning profile
|
||||
3. In Xcode: open `love.xcodeproj` → target `love-ios` → Signing & Capabilities
|
||||
→ select your Team (or set `DEVELOPMENT_TEAM=XXXXXXXXXX` when invoking
|
||||
`scripts/build_ios.sh --device`)
|
||||
4. Archive / export an `.ipa` from Xcode Organizer for TestFlight / Ad Hoc
|
||||
|
||||
## Layout
|
||||
|
||||
| Path | Role |
|
||||
|------|------|
|
||||
| `LOVE_VERSION` | Engine pin (`12.0`) |
|
||||
| `overlays/love-ios.plist` | Portrait-only Info.plist + display name **Pokemon Red** (copied over the upstream plist every build) |
|
||||
| `love-src/` | Downloaded LÖVE 12.0 source tree (**gitignored**, do not commit) |
|
||||
| `cache/` | Temporary source and dependency checkout data (**gitignored**) |
|
||||
| `build/` | `xcodebuild` derived data (**gitignored**) |
|
||||
|
||||
Game payload lands at:
|
||||
|
||||
`love-src/platform/xcode/ios/resources/game.love`
|
||||
|
||||
and is fused into the built `.app` (LÖVE auto-runs any bundled `*.love`).
|
||||
|
||||
## Apple libraries dependency
|
||||
|
||||
`scripts/build_ios.sh --fetch` retrieves the matching iOS libraries and the
|
||||
SDL3 framework from
|
||||
[love-apple-dependencies](https://github.com/love2d/love-apple-dependencies).
|
||||
Re-run it if either dependency directory is absent.
|
||||
The bundled game payload is staged at
|
||||
`love-src/platform/xcode/ios/resources/game.love` and copied into the final
|
||||
app bundle. The payload contains the game, not user-generated ROMs, mods, or
|
||||
saves; those are created at runtime in Documents.
|
||||
|
||||
## App identity
|
||||
|
||||
| Field | Value |
|
||||
|-------|--------|
|
||||
| Display name | Pokemon Red |
|
||||
| `PRODUCT_NAME` | PokemonRed |
|
||||
| Bundle ID | `com.theboisclub.pokemonred` |
|
||||
| Orientations | Portrait only (`UIInterfaceOrientationPortrait`) |
|
||||
| Field | Default |
|
||||
| --- | --- |
|
||||
| Display name | `gen1recomp` |
|
||||
| Product name | `gen1recomp` |
|
||||
| Bundle identifier | `com.theboisclub.gen1recomp` |
|
||||
| Save directory | Public `Documents` root |
|
||||
| Orientation | Portrait |
|
||||
|
||||
Overrides are applied by the build script (`xcodebuild` settings + plist overlay)
|
||||
so refreshing `love-src/` does not lose branding.
|
||||
Set `GEN1_BUNDLE_ID` to use a different bundle identifier for local device
|
||||
builds.
|
||||
|
||||
## Flags (`scripts/build_ios.sh`)
|
||||
## Prerequisites
|
||||
|
||||
| Flag | Meaning |
|
||||
|------|---------|
|
||||
| *(default)* | Simulator, Debug, no signing |
|
||||
| `--fetch` | Fetch the LÖVE 12.0 source tree and Apple dependencies if `love-src/` is missing |
|
||||
| `--device` | Build against `iphoneos` instead of `iphonesimulator` |
|
||||
| `--release` | `Release` configuration instead of `Debug` |
|
||||
| `--package-only` | Zip `game.love` + apply plist overlay; skip `xcodebuild` |
|
||||
- macOS with Xcode and `xcodebuild`
|
||||
- the iOS and iOS Simulator platforms installed in Xcode
|
||||
- a fetched `love-src/` tree, or the `--fetch` option
|
||||
- the matching iOS libraries and SDL3 framework under `love-src/`
|
||||
|
||||
Also: `scripts/build.sh ios` delegates here (`--release` is forwarded).
|
||||
|
||||
## Preconditions
|
||||
|
||||
- macOS (Darwin) with Xcode + `xcodebuild` on `PATH`
|
||||
- iOS platform installed in Xcode (Settings → Platforms). `xcodebuild -showsdks`
|
||||
should list `iphonesimulator` / `iphoneos`. A partial install can fail IB/xib
|
||||
compiles with `iOS … Platform Not Installed` even when the SDK name appears.
|
||||
- `love-src/` present (`--fetch`)
|
||||
- iOS libraries under `love-src/platform/xcode/ios/libraries/` and SDL3 under `love-src/platform/xcode/shared/Frameworks/`
|
||||
Use `xcodebuild -showsdks` to confirm that the required SDKs are installed.
|
||||
|
||||
@@ -16,6 +16,9 @@ static void GRBootstrapInstall(void)
|
||||
queue:[NSOperationQueue mainQueue]
|
||||
usingBlock:^(NSNotification *note) {
|
||||
Class bridge = NSClassFromString(@"GRPickerBridge");
|
||||
if ([bridge respondsToSelector:@selector(preparePublicDocuments)]) {
|
||||
[bridge performSelector:@selector(preparePublicDocuments)];
|
||||
}
|
||||
if ([bridge respondsToSelector:@selector(sweepInbox)]) {
|
||||
[bridge performSelector:@selector(sweepInbox)];
|
||||
}
|
||||
|
||||
@@ -26,8 +26,6 @@ public final class GRPickerBridge: NSObject {
|
||||
// silently doing nothing.
|
||||
private static var liveDelegates: [PickerDelegate] = []
|
||||
|
||||
// conf.lua t.identity — where LÖVE puts the fused save directory on iOS
|
||||
// (<sandbox>/Library/Application Support/<identity>).
|
||||
private static let loveIdentity = "pokemon-love2d"
|
||||
|
||||
@objc(httpDownloadWithUrl:destination:userAgent:accept:)
|
||||
@@ -177,10 +175,10 @@ public final class GRPickerBridge: NSObject {
|
||||
// UIApplicationDidBecomeActive (see GRBootstrap.m).
|
||||
@objc public static func sweepInbox() {
|
||||
let fm = FileManager.default
|
||||
guard let docs = fm.urls(for: .documentDirectory, in: .userDomainMask).first,
|
||||
let appSupport = fm.urls(for: .applicationSupportDirectory,
|
||||
in: .userDomainMask).first else { return }
|
||||
let saveDir = appSupport.appendingPathComponent(loveIdentity, isDirectory: true)
|
||||
migrateLegacySaveDirectory()
|
||||
guard let docs = documentsDirectory(),
|
||||
let saveDir = publicSaveDirectory() else { return }
|
||||
guard docs.standardizedFileURL != saveDir.standardizedFileURL else { return }
|
||||
let wanted: Set<String> = ["gb", "gbc", "zip", "sav"]
|
||||
guard let items = try? fm.contentsOfDirectory(at: docs,
|
||||
includingPropertiesForKeys: nil) else { return }
|
||||
@@ -197,15 +195,21 @@ public final class GRPickerBridge: NSObject {
|
||||
}
|
||||
}
|
||||
|
||||
@objc public static func preparePublicDocuments() {
|
||||
migrateLegacySaveDirectory()
|
||||
if let saveDir = publicSaveDirectory() {
|
||||
ensureDirectory(saveDir)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private static func resolvedSaveDir(_ cstr: UnsafePointer<CChar>?) -> URL? {
|
||||
var dir = cstr.map { String(cString: $0) } ?? ""
|
||||
if dir.isEmpty {
|
||||
guard let appSupport = FileManager.default
|
||||
.urls(for: .applicationSupportDirectory, in: .userDomainMask).first
|
||||
else { return nil }
|
||||
dir = appSupport.appendingPathComponent(loveIdentity).path
|
||||
migrateLegacySaveDirectory()
|
||||
guard let saveDir = publicSaveDirectory() else { return nil }
|
||||
dir = saveDir.path
|
||||
}
|
||||
let url = URL(fileURLWithPath: dir, isDirectory: true)
|
||||
ensureDirectory(url)
|
||||
@@ -217,6 +221,75 @@ public final class GRPickerBridge: NSObject {
|
||||
withIntermediateDirectories: true)
|
||||
}
|
||||
|
||||
private static func documentsDirectory() -> URL? {
|
||||
FileManager.default.urls(for: .documentDirectory,
|
||||
in: .userDomainMask).first
|
||||
}
|
||||
|
||||
private static func publicSaveDirectory() -> URL? {
|
||||
documentsDirectory()
|
||||
}
|
||||
|
||||
private static func legacySaveDirectory() -> URL? {
|
||||
FileManager.default.urls(for: .applicationSupportDirectory,
|
||||
in: .userDomainMask).first?
|
||||
.appendingPathComponent(loveIdentity, isDirectory: true)
|
||||
}
|
||||
|
||||
private static func migrateLegacySaveDirectory() {
|
||||
let fm = FileManager.default
|
||||
guard let destination = publicSaveDirectory(),
|
||||
let legacy = legacySaveDirectory(),
|
||||
fm.fileExists(atPath: legacy.path) else {
|
||||
return
|
||||
}
|
||||
ensureDirectory(destination)
|
||||
mergeDirectory(from: legacy, to: destination)
|
||||
try? fm.removeItem(at: legacy)
|
||||
}
|
||||
|
||||
private static func mergeDirectory(from source: URL, to destination: URL) {
|
||||
let fm = FileManager.default
|
||||
ensureDirectory(destination)
|
||||
guard let items = try? fm.contentsOfDirectory(at: source,
|
||||
includingPropertiesForKeys: nil)
|
||||
else { return }
|
||||
for item in items {
|
||||
let target = destination.appendingPathComponent(item.lastPathComponent)
|
||||
var sourceIsDirectory = ObjCBool(false)
|
||||
fm.fileExists(atPath: item.path, isDirectory: &sourceIsDirectory)
|
||||
var targetIsDirectory = ObjCBool(false)
|
||||
let targetExists = fm.fileExists(atPath: target.path,
|
||||
isDirectory: &targetIsDirectory)
|
||||
if sourceIsDirectory.boolValue && targetExists && targetIsDirectory.boolValue {
|
||||
mergeDirectory(from: item, to: target)
|
||||
continue
|
||||
}
|
||||
if targetExists {
|
||||
if !sourceIsDirectory.boolValue && !targetIsDirectory.boolValue &&
|
||||
fm.contentsEqual(atPath: item.path, andPath: target.path) {
|
||||
try? fm.removeItem(at: item)
|
||||
} else {
|
||||
moveToLegacyName(item, in: destination)
|
||||
}
|
||||
continue
|
||||
}
|
||||
try? fm.moveItem(at: item, to: target)
|
||||
}
|
||||
}
|
||||
|
||||
private static func moveToLegacyName(_ item: URL, in destination: URL) {
|
||||
let fm = FileManager.default
|
||||
let base = item.lastPathComponent + ".legacy"
|
||||
var target = destination.appendingPathComponent(base)
|
||||
var suffix = 2
|
||||
while fm.fileExists(atPath: target.path) {
|
||||
target = destination.appendingPathComponent("\(base).\(suffix)")
|
||||
suffix += 1
|
||||
}
|
||||
try? fm.moveItem(at: item, to: target)
|
||||
}
|
||||
|
||||
private static func copyItem(at src: URL, into dir: URL, named name: String) {
|
||||
let scoped = src.startAccessingSecurityScopedResource()
|
||||
defer { if scoped { src.stopAccessingSecurityScopedResource() } }
|
||||
|
||||
@@ -27,6 +27,8 @@ NATIVE_SRC = IOS_DIR / "native"
|
||||
NATIVE_DST = LOVE_SRC / "platform" / "xcode" / "ios" / "native"
|
||||
WRAP_SYSTEM = LOVE_SRC / "src" / "modules" / "system" / "wrap_System.cpp"
|
||||
PBXPROJ = LOVE_SRC / "platform" / "xcode" / "love.xcodeproj" / "project.pbxproj"
|
||||
APPLE_MM = LOVE_SRC / "src" / "common" / "apple.mm"
|
||||
FILESYSTEM_CPP = LOVE_SRC / "src" / "modules" / "filesystem" / "physfs" / "Filesystem.cpp"
|
||||
ENTITLEMENTS_SRC = IOS_DIR / "overlays" / "love-ios.entitlements"
|
||||
|
||||
NATIVE_FILES = ("GRPickerBridge.swift", "GRHealthBridge.swift", "GRBootstrap.m")
|
||||
@@ -245,19 +247,20 @@ def fail(msg):
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def pristine(path: Path) -> str:
|
||||
def pristine(path: Path, patched_markers=None) -> str:
|
||||
"""Text of `path` before any of our patching: backed by a `.orig` stash.
|
||||
|
||||
The stash is only trusted if it is itself unpatched; that protects
|
||||
against a stash accidentally taken after an earlier patch run.
|
||||
"""
|
||||
patched_markers = tuple(patched_markers or (MARKER, ID_FILE_PICKER))
|
||||
orig = path.with_suffix(path.suffix + ".orig")
|
||||
if orig.is_file():
|
||||
text = orig.read_text()
|
||||
if MARKER not in text and ID_FILE_PICKER not in text:
|
||||
if not any(marker in text for marker in patched_markers):
|
||||
return text
|
||||
text = path.read_text()
|
||||
if MARKER in text or ID_FILE_PICKER in text:
|
||||
if any(marker in text for marker in patched_markers):
|
||||
fail(f"{path} is already patched and no pristine .orig stash exists;\n"
|
||||
f" delete {LOVE_SRC} and re-run scripts/build_ios.sh --fetch")
|
||||
orig.write_text(text)
|
||||
@@ -299,6 +302,62 @@ def patch_wrap_system():
|
||||
"(pickFile/createFile/syncHealthSteps/httpDownload)")
|
||||
|
||||
|
||||
def patch_public_documents():
|
||||
text = pristine(
|
||||
APPLE_MM,
|
||||
("#ifdef LOVE_IOS\n"
|
||||
"\t\t\tnsdir = NSDocumentDirectory;\n"
|
||||
"#else\n",),
|
||||
)
|
||||
original = (
|
||||
"\t\tcase USER_DIRECTORY_APPSUPPORT:\n"
|
||||
"\t\t\tnsdir = NSApplicationSupportDirectory;\n"
|
||||
"\t\t\tbreak;"
|
||||
)
|
||||
replacement = (
|
||||
"\t\tcase USER_DIRECTORY_APPSUPPORT:\n"
|
||||
"#ifdef LOVE_IOS\n"
|
||||
"\t\t\tnsdir = NSDocumentDirectory;\n"
|
||||
"#else\n"
|
||||
"\t\t\tnsdir = NSApplicationSupportDirectory;\n"
|
||||
"#endif\n"
|
||||
"\t\t\tbreak;"
|
||||
)
|
||||
if original not in text:
|
||||
fail(f"iOS app-support path anchor not found in {APPLE_MM}")
|
||||
APPLE_MM.write_text(text.replace(original, replacement, 1))
|
||||
|
||||
filesystem_text = pristine(
|
||||
FILESYSTEM_CPP,
|
||||
("#ifdef LOVE_IOS\n"
|
||||
"\t\t\tsuffix.clear();\n"
|
||||
"#else\n",),
|
||||
)
|
||||
filesystem_original = (
|
||||
"\t\tstd::string suffix;\n"
|
||||
"\t\tif (isFused())\n"
|
||||
"\t\t\tsuffix = std::string(LOVE_PATH_SEPARATOR) + saveIdentity;\n"
|
||||
"\t\telse\n"
|
||||
"\t\t\tsuffix = std::string(LOVE_PATH_SEPARATOR LOVE_APPDATA_FOLDER LOVE_PATH_SEPARATOR) + saveIdentity;"
|
||||
)
|
||||
filesystem_replacement = (
|
||||
"\t\tstd::string suffix;\n"
|
||||
"#ifdef LOVE_IOS\n"
|
||||
"\t\t\tsuffix.clear();\n"
|
||||
"#else\n"
|
||||
"\t\tif (isFused())\n"
|
||||
"\t\t\tsuffix = std::string(LOVE_PATH_SEPARATOR) + saveIdentity;\n"
|
||||
"\t\telse\n"
|
||||
"\t\t\tsuffix = std::string(LOVE_PATH_SEPARATOR LOVE_APPDATA_FOLDER LOVE_PATH_SEPARATOR) + saveIdentity;\n"
|
||||
"#endif"
|
||||
)
|
||||
if filesystem_original not in filesystem_text:
|
||||
fail(f"iOS save directory suffix anchor not found in {FILESYSTEM_CPP}")
|
||||
FILESYSTEM_CPP.write_text(filesystem_text.replace(filesystem_original,
|
||||
filesystem_replacement, 1))
|
||||
print("patch_love_src: iOS save directory routed to Documents root")
|
||||
|
||||
|
||||
def patch_pbxproj():
|
||||
text = pristine(PBXPROJ)
|
||||
|
||||
@@ -362,6 +421,7 @@ def main():
|
||||
if not LOVE_SRC.is_dir():
|
||||
fail("love-src/ missing; run scripts/build_ios.sh --fetch first")
|
||||
copy_native_files()
|
||||
patch_public_documents()
|
||||
patch_wrap_system()
|
||||
patch_pbxproj()
|
||||
|
||||
|
||||
@@ -217,6 +217,14 @@ apply_ios_branding() {
|
||||
cp "$OVERLAY_PLIST" "$dest"
|
||||
}
|
||||
|
||||
verify_documents_overlay() {
|
||||
local sharing in_place
|
||||
sharing="$(/usr/libexec/PlistBuddy -c 'Print :UIFileSharingEnabled' "$OVERLAY_PLIST" 2>/dev/null || true)"
|
||||
in_place="$(/usr/libexec/PlistBuddy -c 'Print :LSSupportsOpeningDocumentsInPlace' "$OVERLAY_PLIST" 2>/dev/null || true)"
|
||||
[ "$sharing" = "true" ] && [ "$in_place" = "true" ] \
|
||||
|| fail "iOS plist overlay must enable UIFileSharingEnabled and LSSupportsOpeningDocumentsInPlace"
|
||||
}
|
||||
|
||||
apply_ios_icon() {
|
||||
local source="$ROOT/assets/logo/gen1recomp_cover.png"
|
||||
local target="$XCODE_DIR/Images.xcassets/iOS AppIcon.appiconset"
|
||||
@@ -578,6 +586,25 @@ verify_native_bridge() {
|
||||
say "native bridge present (pickFile, createFile)"
|
||||
}
|
||||
|
||||
verify_documents_configuration() {
|
||||
local app="$1"
|
||||
local plist="$app/Info.plist"
|
||||
local sharing in_place
|
||||
[ -f "$plist" ] || fail "built iOS app is missing Info.plist: $plist"
|
||||
sharing="$(/usr/libexec/PlistBuddy -c 'Print :UIFileSharingEnabled' "$plist" 2>/dev/null || true)"
|
||||
in_place="$(/usr/libexec/PlistBuddy -c 'Print :LSSupportsOpeningDocumentsInPlace' "$plist" 2>/dev/null || true)"
|
||||
[ "$sharing" = "true" ] && [ "$in_place" = "true" ] \
|
||||
|| fail "built iOS app does not expose its Documents folder in $(basename "$app")"
|
||||
say "public Documents exposure present (file sharing + in-place access)"
|
||||
}
|
||||
|
||||
verify_game_payload() {
|
||||
local app="$1"
|
||||
[ -s "$app/game.love" ] \
|
||||
|| fail "built iOS app is missing game.love: $app"
|
||||
say "game.love present ($(du -h "$app/game.love" | cut -f1))"
|
||||
}
|
||||
|
||||
run_xcodebuild() {
|
||||
local config sdk destination
|
||||
if $RELEASE; then
|
||||
@@ -618,6 +645,8 @@ run_xcodebuild() {
|
||||
PRODUCT_BUNDLE_IDENTIFIER="$BUNDLE_ID"
|
||||
MARKETING_VERSION="$marketing_version"
|
||||
CURRENT_PROJECT_VERSION="$project_version"
|
||||
INFOPLIST_KEY_UIFileSharingEnabled=YES
|
||||
INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace=YES
|
||||
ONLY_ACTIVE_ARCH=NO
|
||||
DISABLE_MANUAL_TARGET_ORDER_BUILD_WARNING=YES
|
||||
)
|
||||
@@ -693,6 +722,8 @@ run_xcodebuild() {
|
||||
fi
|
||||
fi
|
||||
|
||||
verify_documents_configuration "$app"
|
||||
|
||||
# Fuse even if the pbxproj wire-up failed, LÖVE runs any bundled *.love.
|
||||
# Byte-compare, never just existence: xcodebuild's incremental Copy Bundle
|
||||
# Resources can leave a previous build's game.love in a surviving .app, and
|
||||
@@ -703,6 +734,7 @@ run_xcodebuild() {
|
||||
cp "$LOVE_FILE" "$app/game.love"
|
||||
fi
|
||||
|
||||
verify_game_payload "$app"
|
||||
verify_native_bridge "$app"
|
||||
|
||||
local dist_dir="$DIST/${config}-${sdk}"
|
||||
@@ -774,6 +806,7 @@ install_to_device() {
|
||||
|
||||
# --------------------------------------------------------------- main
|
||||
apply_ios_branding
|
||||
verify_documents_overlay
|
||||
apply_ios_icon
|
||||
say "applying iOS native bridge patches (picker/Files support)"
|
||||
python3 "$IOS_DIR/patch_love_src.py" || fail "patch_love_src.py failed"
|
||||
|
||||
Reference in New Issue
Block a user