feat(ios): support building for both physical devices and simulators

- Replace hardcoded simulator framework path with SDK-conditional paths
  so Xcode automatically resolves iosSimulatorArm64 or iosArm64 based
  on the selected build target
- Fix Compose Resources build script to use \$PLATFORM_NAME instead of
  \$CONFIGURATION for platform detection
- Add CODE_SIGN_IDENTITY override for iphoneos SDK to enable device signing
- Document simulator and physical device build workflows in README
This commit is contained in:
txa5451 2026-04-25 00:41:35 -04:00
parent dad37f510e
commit 86d84a350f
4 changed files with 89 additions and 10 deletions

View file

@ -173,9 +173,12 @@ jobs:
key: ${{ runner.os }}-konan-${{ hashFiles('gradle/libs.versions.toml') }}
restore-keys: ${{ runner.os }}-konan-
- name: Build shared KMP framework
- name: Build shared KMP framework (simulator)
run: ./gradlew :shared:linkDebugFrameworkIosSimulatorArm64
- name: Build shared KMP framework (device)
run: ./gradlew :shared:linkDebugFrameworkIosArm64
- name: Build iOS app
run: |
xcodebuild -project iosApp/iosApp.xcodeproj \

View file

@ -55,6 +55,30 @@ NON_BROWSER_VIOLATION_SCORE=0
Without this setting, the server's violation system may accumulate ban points against the mobile client if the User-Agent check fails, eventually locking the account out.
### Self-Signed Certificates (iOS)
The iOS app can connect to a LibreChat server using a self-signed certificate, but iOS enforces strict requirements on all TLS certificates regardless of whether they are self-signed or CA-issued. The certificate must meet **all** of the following or iOS will reject the connection:
| Requirement | Details |
|---|---|
| **Validity period** | ≤ 398 days (for certificates issued after September 1, 2020) |
| **Subject Alternative Name** | DNS name must be present in the SAN extension — CommonName alone is not accepted |
| **Key algorithm** | RSA ≥ 2048 bits |
| **Signature algorithm** | SHA-256 or better (SHA-1 is not accepted) |
| **Extended Key Usage** | Must include `id-kp-serverAuth` (TLS Web Server Authentication) |
The 398-day validity limit is the most common cause of failures — a certificate that works in every browser but was generated with a multi-year validity will be rejected by iOS.
**Setup steps:**
1. Generate a compliant certificate signed by your own CA (tools like [`mkcert`](https://github.com/FiloSottile/mkcert) handle all requirements automatically)
2. On your iPhone: **Settings → General → VPN & Device Management** — install your CA certificate
3. Go to **Settings → General → About → Certificate Trust Settings** — enable full trust for your CA
Once the CA is trusted on the device, you only need to re-install it if you create a new CA. The server certificate itself (which you replace when it expires) does not need to be installed separately.
> **Android** has no equivalent restrictions — self-signed certificates with any validity period work as long as the CA is trusted on the device.
### Notes
- **Registration** — The app respects your server's registration settings. If registration is disabled server-side, only the login form is shown.
@ -88,16 +112,26 @@ For a release build:
### iOS
Build and run on the iOS Simulator:
**Simulator:**
```bash
./gradlew :shared:linkDebugFrameworkIosSimulatorArm64
xcodebuild -project iosApp/iosApp.xcodeproj -scheme iosApp \
-sdk iphonesimulator \
-destination 'platform=iOS Simulator,name=iPhone 16' \
-derivedDataPath iosApp/build build
```
See [CONTRIBUTING.md](CONTRIBUTING.md) for full simulator install and launch commands.
**Physical device:**
```bash
./gradlew :shared:linkDebugFrameworkIosArm64
open iosApp/iosApp.xcodeproj
```
Then in Xcode: select your device, set your Team under Signing & Capabilities, and press ⌘R.
See [iosApp/README.md](iosApp/README.md) for full build and launch instructions.
### Tech Stack

View file

@ -35,25 +35,51 @@ The `:shared` Gradle module exports `core:common`, `core:model`, `core:network`,
### Build and Run
#### Simulator
```bash
# Build the app (Xcode build phases handle the shared framework automatically)
# 1. Build the shared KMP framework
./gradlew :shared:linkDebugFrameworkIosSimulatorArm64
# 2. Build the Xcode project
xcodebuild -project iosApp/iosApp.xcodeproj -scheme iosApp \
-sdk iphonesimulator \
-destination 'platform=iOS Simulator,name=iPhone 16' \
-derivedDataPath iosApp/build build
# Boot simulator, install, and launch
# 3. Boot simulator, install, and launch
xcrun simctl boot "iPhone 16"
xcrun simctl install booted iosApp/build/Build/Products/Debug-iphonesimulator/iosApp.app
xcrun simctl launch booted com.garfiec.librechat.ios
```
> **Note:** The first build takes several minutes while the Kotlin/Native toolchain downloads.
To rebuild only the shared KMP framework (e.g., after changing shared Kotlin code):
#### Physical Device
```bash
./gradlew :shared:linkDebugFrameworkIosSimulatorArm64
# 1. Build the shared KMP framework for device
./gradlew :shared:linkDebugFrameworkIosArm64
```
Then open the project in Xcode:
```bash
open iosApp/iosApp.xcodeproj
```
In Xcode:
1. Select your iPhone from the device picker (top toolbar)
2. Go to **Signing & Capabilities** → set your **Team** (required for device signing)
3. Press **⌘R** to build and install
> **Note:** The first build takes several minutes while the Kotlin/Native toolchain downloads. You need an Apple Developer account (free tier works) for device signing.
### Known Xcode Behaviour
**Red dot on `Shared.framework` in the navigator** — You may see a red indicator on `Shared.framework` in the Xcode file navigator (left sidebar). This is cosmetic and does not affect builds. It happens because the Xcode project file keeps a static reference path to the framework for display purposes, and that path may not exist if you haven't built that particular target yet. The linker always resolves the framework via `FRAMEWORK_SEARCH_PATHS`, which is set correctly per target — the red dot can be safely ignored. Building the Gradle framework for your active target clears it:
```bash
./gradlew :shared:linkDebugFrameworkIosSimulatorArm64 # clears it for simulator
./gradlew :shared:linkDebugFrameworkIosArm64 # clears it for device
```
## Info.plist Permissions

View file

@ -43,7 +43,7 @@
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "# Aggregate and copy Compose Multiplatform resources into the app bundle.\n# Runs the Gradle aggregation task to ensure resources from all feature\n# modules are collected, then copies them into the .app bundle where\n# the Compose resource reader expects them at runtime.\n\nif [ \"$CONFIGURATION\" = \"Debug\" ]; then\n ARCH_DIR=\"iosSimulatorArm64\"\n GRADLE_TASK=\":shared:iosSimulatorArm64AggregateResources\"\nelse\n ARCH_DIR=\"iosArm64\"\n GRADLE_TASK=\":shared:iosArm64AggregateResources\"\nfi\n\nPROJECT_ROOT=\"$SRCROOT/..\"\nSRC=\"$PROJECT_ROOT/shared/build/kotlin-multiplatform-resources/aggregated-resources/$ARCH_DIR/composeResources\"\nDST=\"$BUILT_PRODUCTS_DIR/$PRODUCT_NAME.app/compose-resources/composeResources\"\n\n# Run Gradle aggregation to ensure resources are up to date\necho \"Running $GRADLE_TASK...\"\ncd \"$PROJECT_ROOT\" && ./gradlew \"$GRADLE_TASK\" 2>&1\nGRADLE_EXIT=$?\nif [ $GRADLE_EXIT -ne 0 ]; then\n echo \"error: Gradle task $GRADLE_TASK failed with exit code $GRADLE_EXIT\"\n exit $GRADLE_EXIT\nfi\n\n# Clean stale resources before copying fresh ones\nif [ -d \"$DST\" ]; then\n rm -rf \"$DST\"\nfi\n\nif [ -d \"$SRC\" ]; then\n mkdir -p \"$DST\"\n cp -R \"$SRC/\" \"$DST/\"\n echo \"Copied Compose resources from $SRC to $DST\"\nelse\n echo \"error: Compose resources not found at $SRC after Gradle aggregation\"\n exit 1\nfi\n";
shellScript = "# Aggregate and copy Compose Multiplatform resources into the app bundle.\n# Runs the Gradle aggregation task to ensure resources from all feature\n# modules are collected, then copies them into the .app bundle where\n# the Compose resource reader expects them at runtime.\n\nif [ \"$PLATFORM_NAME\" = \"iphonesimulator\" ]; then\n ARCH_DIR=\"iosSimulatorArm64\"\n GRADLE_TASK=\":shared:iosSimulatorArm64AggregateResources\"\nelif [ \"$PLATFORM_NAME\" = \"iphoneos\" ]; then\n ARCH_DIR=\"iosArm64\"\n GRADLE_TASK=\":shared:iosArm64AggregateResources\"\nelse\n echo \"error: Unsupported PLATFORM_NAME: $PLATFORM_NAME\"\n exit 1\nfi\n\nPROJECT_ROOT=\"$SRCROOT/..\"\nSRC=\"$PROJECT_ROOT/shared/build/kotlin-multiplatform-resources/aggregated-resources/$ARCH_DIR/composeResources\"\nDST=\"$BUILT_PRODUCTS_DIR/$PRODUCT_NAME.app/compose-resources/composeResources\"\n\n# Run Gradle aggregation to ensure resources are up to date\necho \"Running $GRADLE_TASK...\"\ncd \"$PROJECT_ROOT\" && ./gradlew \"$GRADLE_TASK\" 2>&1\nGRADLE_EXIT=$?\nif [ $GRADLE_EXIT -ne 0 ]; then\n echo \"error: Gradle task $GRADLE_TASK failed with exit code $GRADLE_EXIT\"\n exit $GRADLE_EXIT\nfi\n\n# Clean stale resources before copying fresh ones\nif [ -d \"$DST\" ]; then\n rm -rf \"$DST\"\nfi\n\nif [ -d \"$SRC\" ]; then\n mkdir -p \"$DST\"\n cp -R \"$SRC/\" \"$DST/\"\n echo \"Copied Compose resources from $SRC to $DST\"\nelse\n echo \"error: Compose resources not found at $SRC after Gradle aggregation\"\n exit 1\nfi\n";
};
/* End PBXShellScriptBuildPhase section */
@ -237,14 +237,22 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_IDENTITY = "-";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = "";
ENABLE_PREVIEWS = YES;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
);
"FRAMEWORK_SEARCH_PATHS[sdk=iphonesimulator*]" = (
"$(inherited)",
"$(PROJECT_DIR)/../shared/build/bin/iosSimulatorArm64/debugFramework",
);
"FRAMEWORK_SEARCH_PATHS[sdk=iphoneos*]" = (
"$(inherited)",
"$(PROJECT_DIR)/../shared/build/bin/iosArm64/debugFramework",
);
GENERATE_INFOPLIST_FILE = NO;
INFOPLIST_FILE = iosApp/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
@ -268,12 +276,20 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_IDENTITY = "-";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = "";
ENABLE_PREVIEWS = YES;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
);
"FRAMEWORK_SEARCH_PATHS[sdk=iphonesimulator*]" = (
"$(inherited)",
"$(PROJECT_DIR)/../shared/build/bin/iosSimulatorArm64/releaseFramework",
);
"FRAMEWORK_SEARCH_PATHS[sdk=iphoneos*]" = (
"$(inherited)",
"$(PROJECT_DIR)/../shared/build/bin/iosArm64/releaseFramework",
);
GENERATE_INFOPLIST_FILE = NO;