ios simulator patching

This commit is contained in:
EthanShoeDev
2025-09-12 16:13:11 -04:00
parent 7eda9b1295
commit f9a2a1bfca
2 changed files with 312 additions and 8 deletions

View File

@@ -1,4 +1,4 @@
import 'tsx/cjs'; // Add this to import TypeScript files import 'tsx/cjs';
import { type ExpoConfig } from 'expo/config'; import { type ExpoConfig } from 'expo/config';
import packageJson from './package.json'; import packageJson from './package.json';
@@ -13,9 +13,7 @@ const config: ExpoConfig = {
newArchEnabled: true, newArchEnabled: true,
ios: { ios: {
supportsTablet: true, supportsTablet: true,
config: { config: { usesNonExemptEncryption: false },
usesNonExemptEncryption: false,
},
bundleIdentifier: 'dev.fressh.app', bundleIdentifier: 'dev.fressh.app',
}, },
android: { android: {
@@ -53,17 +51,23 @@ const config: ExpoConfig = {
'expo-build-properties', 'expo-build-properties',
{ {
android: { android: {
// dylankenneally/react-native-ssh-sftp fails to build without this
packagingOptions: { packagingOptions: {
pickFirst: ['META-INF/versions/9/OSGI-INF/MANIFEST.MF'], pickFirst: ['META-INF/versions/9/OSGI-INF/MANIFEST.MF'],
}, },
}, },
ios: {
// https://github.com/dylankenneally/react-native-ssh-sftp/issues/20#issuecomment-3286693445
// ../../docs/ios-sim-not-working.md (Update 1)
extraPods: [
{ name: 'CSSH-Binary', podspec: 'https://gist.githubusercontent.com/EthanShoeDev/1ab212949007d7aeabfeb199b7b9e951/raw/8602ec55efdf8c620dbbae93cd54023e2a36a8b9/CSSH-Binary.podspec' },
{ name: 'NMSSH', git: 'https://github.com/EthanShoeDev/NMSSH.git', branch: 'master' },
],
},
}, },
], ],
], ],
experiments: { experiments: { typedRoutes: true, reactCompiler: true },
typedRoutes: true,
reactCompiler: true,
},
}; };
export default config; export default config;

300
docs/ios-sim-not-working.md Normal file
View File

@@ -0,0 +1,300 @@
# React Native SSH — iOS simulator limitations & paths forward
*Last updated: 2025-09-12*
## TL;DR
* Our current stack (`react-native-ssh-sftp``NMSSH (aanah0 fork)` → prebuilt OpenSSL/libssh2) **fails on the iOS Simulator** because the vendored native libraries are compiled for **iOS device** only (platform: `iphoneos`), not **iOS Simulator** (platform: `iphonesimulator`).
* The proper modern fix is to use **XCFrameworks** that include **both** device and simulator slices, and have NMSSH link against those instead of raw static `.a` libraries.
* We have two main paths:
1. Keep using the existing RN package, but swap NMSSHs underlying crypto/SSH libs to an **XCFramework** (e.g., from `DimaRU/Libssh2Prebuild`), via a tiny CocoaPods change.
2. Build our **own RN native module** (TurboModule/JSI) that links **libssh2** and **OpenSSL** directly (using or adapting the Libssh2Prebuild scripts), fixing iOS Simulator support and also addressing Android reliability issues (like double “disconnect” callbacks).
---
## Current stack & limitation
### Stack
* App → `react-native-ssh-sftp`
* iOS native → `NMSSH` (via the `aanah0/NMSSH` fork for a newer libssh2 baseline)
* `NMSSH` bundles **precompiled OpenSSL + libssh2** as static libraries.
### Why iOS Simulator builds fail
* Xcode treats **device** and **simulator** as **different platforms**. Since Apple Silicon, both may be `arm64`, but they are **not interchangeable**:
* Device slice: `ios-arm64` (platform: `iphoneos`)
* Simulator slices: `ios-arm64` and/or `ios-x86_64` (platform: `iphonesimulator`)
* NMSSHs vendored libs are device-only (`iphoneos`). When the Simulator build links those, the linker throws:
```
ld: building for 'iOS-simulator', but linking in object file .../libcrypto.a[arm64] built for 'iOS'
```
* **XCFrameworks** solve this: they are a bundle containing the correct slices for both platforms. Xcode picks the proper slice automatically.
> Temporary hack (not recommended): exclude `arm64` for the simulator (`EXCLUDED_ARCHS[sdk=iphonesimulator*]=arm64`) to force an x86\_64 sim under Rosetta. This “works” but loses native-sim performance and is brittle.
---
## Path 1 — Keep current RN package; replace NMSSHs vendored libs with an XCFramework
**Goal:** Do *not* change `react-native-ssh-sftp` or the NMSSH **API**. Only change how NMSSH links to OpenSSL/libssh2.
### What well use
* **`DimaRU/Libssh2Prebuild`**: ships a ready-made **XCFramework** (“CSSH”) that bundles **libssh2 + OpenSSL** with both **device** and **simulator** slices.
* CocoaPods can consume XCFrameworks via a **binary pod** (`vendored_frameworks`), but Libssh2Prebuild is published for **SwiftPM**. So we add a tiny **`CSSH-Binary` podspec** that points to its zipped XCFramework.
### Minimal changes
1. **Fork NMSSH** (from `aanah0/NMSSH`) and edit **`NMSSH.podspec`**
* **Remove** the vendored static libs (the `.a` files).
* **Add** a dependency on `CSSH-Binary` (our binary pod that vends `CSSH.xcframework`).
**Podspec diff (conceptual):**
```diff
@@
Pod::Spec.new do |s|
s.name = 'NMSSH'
# ... (other metadata unchanged)
- s.vendored_libraries = [
- 'NMSSH-iOS/Libraries/lib/libssh2.a',
- 'NMSSH-iOS/Libraries/lib/libssl.a',
- 'NMSSH-iOS/Libraries/lib/libcrypto.a'
- ]
+ s.dependency 'CSSH-Binary', '~> 1.11'
end
```
2. **Create a tiny binary pod for CSSH** (one-file repo): `CSSH-Binary.podspec`
```ruby
Pod::Spec.new do |s|
s.name = 'CSSH-Binary'
s.version = '1.11.0' # match the Libssh2/OpenSSL combo you choose
s.summary = 'libssh2 + OpenSSL as XCFramework (from DimaRU/Libssh2Prebuild)'
s.license = { :type => 'MIT' }
s.homepage = 'https://github.com/your-org/cssh-binary'
s.authors = { 'Your Name' => 'you@example.com' }
s.platform = :ios, '12.0'
s.source = {
:http => 'https://github.com/DimaRU/Libssh2Prebuild/releases/download/1.11.0-OpenSSL-1-1-1w/CSSH.xcframework.zip'
}
s.vendored_frameworks = 'CSSH.xcframework'
end
```
* You can host the zip yourself if you want deterministic supply (recommended for CI).
* Ensure license attributions (OpenSSL 3.x: Apache-2.0; libssh2: BSD-3-Clause) are included in our OSS notices.
3. **Wire via Expo** (`expo-build-properties`) in `app.config.ts`
```ts
[
'expo-build-properties',
{
ios: {
extraPods: [
// Binary pod that vends the XCFramework
{ name: 'CSSH-Binary', podspec: 'https://raw.githubusercontent.com/your-org/cssh-binary/main/CSSH-Binary.podspec' },
// Our NMSSH fork that depends on CSSH-Binary
{ name: 'NMSSH', git: 'https://github.com/your-org/NMSSH.git', branch: 'cssh-xcframework' },
],
},
android: {
packagingOptions: {
pickFirst: ['META-INF/versions/9/OSGI-INF/MANIFEST.MF'],
},
},
},
]
```
4. **Rebuild**
```bash
npx expo prebuild --platform ios --clean
npx pod-install
npx expo run:ios # Simulator should now link correctly
```
**Pros**
* Small surface area; we keep the RN package API intact.
* Correct, future-proof device/simulator packaging via XCFrameworks.
* Easy to maintain once the podspecs are set.
**Cons/Risks**
* We maintain a **fork of NMSSH** (podspec only).
* Potential conflicts with tools like **Flipper** if they also touch OpenSSL (rare; we can disable Flipper if needed).
* We rely on Libssh2Prebuild versions/tags (or build our own artifacts using their scripts).
---
## Path 2 — Build our own RN module (TurboModule/JSI) with libssh2/OpenSSL
**Goal:** Replace `react-native-ssh-sftp` + NMSSH with a **modern RN Native Module** that links libssh2 directly and ships correct device/simulator binaries out of the box.
### Why consider this
* Full control over the native surface (events, error handling, cancellation, reconnects).
* Fix Android reliability issues weve observed (e.g., “disconnect” callback firing multiple times, causing dev crashes).
* Avoid legacy Objective-C wrapper constraints; use **TurboModules/JSI**.
### High-level plan
**iOS**
* Use (or adapt) **Libssh2Prebuild build scripts** to produce our own **`libssh2.xcframework`** and **`OpenSSL.xcframework`** (or one combined framework).
* Publish them as a **binary CocoaPod** (like `CSSH-Binary`) *or* vendor the XCFrameworks in our module pod.
* Write a thin Obj-C++/C++ wrapper exposing the SSH API needed by JS (connect/auth/exec/sftp/streaming).
* Export via **TurboModule** (codegen) or **JSI** (C++), and provide a typed TS API in the RN package.
**Android**
* Build **libssh2** against **OpenSSL** (or MbedTLS) with the **NDK**.
* Package `.so` libs per ABI (`arm64-v8a`, `x86_64`, etc.).
* Implement JNI layer with strict **once-only** callbacks (disconnect must be idempotent), and make the public JS API promise/observable based.
* Add tests that assert no multiple “disconnect” emissions.
**Common**
* Define a **stable JS API**:
* Connection lifecycle (`connect`, `ready`, `disconnect`),
* Auth variants (password, key, agent if supported),
* Exec/PTY streaming,
* SFTP (get/put/mkdir/list/stat),
* Structured errors (error codes, host key checks, timeouts),
* Events (`onData`, `onExit`, `onError`, `onDisconnect`).
* **Testing/CI**: Simulated hosts (Docker) for integration tests; E2E with detox where feasible; CI matrix includes iOS Simulator and Android emulators.
**Pros**
* Clean slate, better DX, fewer legacy constraints.
* We can ensure **simulator** support is first-class.
* Fixes Android issues definitively.
**Cons**
* More initial engineering time.
* We own native maintenance across platforms.
---
## Known Android issue in current package
Weve observed that **disconnect** can trigger the callback **more than once**, which crashes the app in dev (double resolve/reject or repeated event emission). If we keep the current package for now:
* Add a JS-side **guard** to make `disconnect()` idempotent (ignore subsequent calls/notifications after the first).
* If we fork, fix the native layer so it emits exactly once and cleans up listeners predictably.
(If we pursue **Path 2**, well design the native layer so all callbacks/promises are strictly single-shot and lifecycle-scoped.)
---
## Recommendation
* **Short term:** Implement **Path 1**. Its the smallest change to unblock Simulator builds:
* Add a `CSSH-Binary` binary pod (vend `CSSH.xcframework`).
* Fork NMSSH podspec to depend on it.
* Wire both via `expo-build-properties` `ios.extraPods`.
* **Medium term:** Plan **Path 2** to remove legacy NMSSH constraints and resolve Android issues thoroughly using a Nitro/TurboModule with direct libssh2 bindings.
---
## Appendix
### A. `CSSH-Binary.podspec` (example)
```ruby
Pod::Spec.new do |s|
s.name = 'CSSH-Binary'
s.version = '1.11.0'
s.summary = 'libssh2 + OpenSSL as XCFramework (from DimaRU/Libssh2Prebuild)'
s.license = { :type => 'MIT' }
s.homepage = 'https://github.com/your-org/cssh-binary'
s.authors = { 'Your Name' => 'you@example.com' }
s.platform = :ios, '12.0'
s.source = {
:http => 'https://github.com/DimaRU/Libssh2Prebuild/releases/download/1.11.0-OpenSSL-1-1-1w/CSSH.xcframework.zip'
}
s.vendored_frameworks = 'CSSH.xcframework'
end
```
### B. `NMSSH.podspec` diff (replace vendored `.a` libs)
```diff
- s.vendored_libraries = [
- 'NMSSH-iOS/Libraries/lib/libssh2.a',
- 'NMSSH-iOS/Libraries/lib/libssl.a',
- 'NMSSH-iOS/Libraries/lib/libcrypto.a'
- ]
+ s.dependency 'CSSH-Binary', '~> 1.11'
```
### C. `app.config.ts` (Expo)
```ts
plugins: [
// ...
[
'expo-build-properties',
{
ios: {
extraPods: [
{ name: 'CSSH-Binary', podspec: 'https://raw.githubusercontent.com/your-org/cssh-binary/main/CSSH-Binary.podspec' },
{ name: 'NMSSH', git: 'https://github.com/your-org/NMSSH.git', branch: 'cssh-xcframework' },
],
},
android: {
packagingOptions: {
pickFirst: ['META-INF/versions/9/OSGI-INF/MANIFEST.MF'],
},
},
},
],
]
```
### D. Build/Release notes
* **Rebuild steps**
```bash
npx expo prebuild --platform ios --clean
npx pod-install
npx expo run:ios
```
* **Licenses**: Include OpenSSL and libssh2 license texts in our OSS notice.
* **Flipper**: If linking conflicts appear, disable Flipper in iOS debug builds.
### E. Temporary simulator workaround (not recommended long-term)
In both the app target and Pods project:
```
EXCLUDED_ARCHS[sdk=iphonesimulator*] = arm64
```
This forces an x86\_64 Simulator under Rosetta, avoiding the immediate link error but losing native-sim performance.
---
**Decision log**
* ✅ Adopt Path 1 now (XCFramework via CSSH + NMSSH podspec tweak).
* 🕒 Plan Path 2 (custom RN module) to address Android bugs and own the full stack.
# Updates
- 1. Implemented option 1. Seems to be working in simulator