Skip to content

Commit d18b97f

Browse files
committed
iOS: bspatch integration
1 parent 60fec5b commit d18b97f

41 files changed

Lines changed: 557 additions & 45 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
variant="$1"
5+
test_command="$2"
6+
7+
if [ "$variant" = "bare" ]; then
8+
# These tests are independent of the bare/expo distinction. Bare tests are slightly faster.
9+
echo "::group::Instrumented tests"
10+
(cd android && ./gradlew :app:connectedAndroidTest)
11+
echo "::endgroup::"
12+
fi
13+
14+
echo "::group::E2E tests"
15+
npm run "$test_command"
16+
echo "::endgroup::"

.github/workflows/ci-test.yml

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -104,17 +104,12 @@ jobs:
104104
arch: x86
105105
disable-animations: true
106106
# connectedAndroidTest is included here (rather than a separate job or step) to reuse
107-
# this job's already-booted emulator, even though it means it runs once per matrix
108-
# variant. The android-emulator-runner action has no post-cleanup step, so a second
109-
# step would boot and tear down a second emulator; ::group:: markers keep the two
110-
# test runs visually separated in the Actions log instead.
111-
script: |
112-
echo "::group::Instrumented tests"
113-
(cd android && ./gradlew :app:connectedAndroidTest)
114-
echo "::endgroup::"
115-
echo "::group::E2E tests"
116-
npm run ${{ matrix.test-command }}
117-
echo "::endgroup::"
107+
# this job's already-booted emulator. The emulator only lives for the duration of this
108+
# action (it's killed at the end of the same invocation, not in a post/cleanup step),
109+
# so the logic can't be split into a later workflow step - it has to run here.
110+
# android-emulator-runner also runs each line of `script` as its own separate `sh -c`
111+
# invocation, so multi-line shell logic is delegated to a script file instead of inlining here.
112+
script: .github/scripts/run-android-tests.sh ${{ matrix.variant }} ${{ matrix.test-command }}
118113

119114
ios-test:
120115
needs: lint
@@ -141,10 +136,16 @@ jobs:
141136
restore-keys: |
142137
${{ runner.os }}-npm-
143138
144-
- name: Run iOS Tests
145-
run: |
146-
npm install
147-
npm run ${{ matrix.test-command }}
139+
- name: Install dependencies
140+
run: npm install
141+
142+
- name: Run iOS unit tests
143+
# These tests are independent of the bare/expo distinction. Bare tests are slightly faster.
144+
if: matrix.variant == 'bare'
145+
run: npm run test:unit:ios
146+
147+
- name: Run iOS E2E Tests
148+
run: npm run ${{ matrix.test-command }}
148149

149150
- name: Upload iOS Simulator crash reports
150151
if: failure()

.npmignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ android/.gradle
5252
android/**/*.iml
5353
android/.idea
5454

55+
ios/CodePushDiffPatchTests/
56+
5557

5658
# Windows
5759
windows/.vs/

CLAUDE.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@ React Native CodePush is a native module that enables over-the-air updates for R
1010

1111
#### Unit tests
1212

13-
- `cd android && ./gradlew :app:test`
14-
- iOS: no unit tests yet.
13+
- `npm run test:unit:android`
14+
- `npm run test:unit:ios`
15+
16+
Prefer unit testing what's possible (even though, on iOS, this involves a simulator). Legacy code used E2E tests for everything, which is complex, error-prone, and slow. The existing E2E tests are still useful, but this is not a pattern to follow.
1517

1618
#### E2E Tests
1719
- `npm run test:android` - Run Android-specific tests
@@ -54,7 +56,7 @@ React Native CodePush is a native module that enables over-the-air updates for R
5456
- **Custom Test Runner**: TypeScript-based test framework in `test/`
5557
- **Real App Testing**: Creates actual React Native apps for integration testing
5658
- **Scenario Testing**: Update, rollback, and error scenarios
57-
- **No unit test infra for JS/iOS yet**: JS/iOS only have the mocha-based integration suite above. `src/acquisition-sdk/__tests__/` contains tests ported from upstream `microsoft/code-push`, kept for future reference - they are deliberately not wired into `npm test` or any runner. Don't assume they're dead/forgotten code, and don't wire them in without setting up real unit test infra first.
59+
- **No unit test infra for JS yet**: JS only has the mocha-based integration suite above. `src/acquisition-sdk/__tests__/` contains tests ported from upstream `microsoft/code-push`, kept for future reference - they are deliberately not wired into `npm test` or any runner. Don't assume they're dead/forgotten code, and don't wire them in without setting up real unit test infra first.
5860
- **Templates**: `test/template/` holds native files (Podfile, AppDelegate, Android app files) and JS scenarios copied over top of a freshly generated RN/Expo app during test setup, overwriting its defaults — edit files here, not the generated project, for changes to persist
5961
- **`test:ios` vs `test:setup:ios` vs `test:fast:ios`**: `test:ios` is just `test:setup:ios` followed by `test:fast:ios` — the two are meant to be split apart for local iteration.
6062
- `test:setup:ios` (mocha `--ios --setup`) boots the simulator and provisions the test app once: copies templates, runs `pod install`, patches Info.plist/AppDelegate. It never builds or runs any test scenario.

CodePush.podspec

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,23 @@ Pod::Spec.new do |s|
1313
s.ios.deployment_target = '15.5'
1414
s.tvos.deployment_target = '15.5'
1515
s.preserve_paths = '*.js'
16-
s.library = 'z'
17-
s.source_files = 'ios/CodePush/*.{h,m}'
16+
s.libraries = 'z', 'bz2'
17+
s.source_files = [
18+
'ios/CodePush/*.{h,m}',
19+
'shared/diffpatch/*.{c,h}',
20+
'shared/third_party/hdiffpatch/libHDiffPatch/HPatch/patch.{c,h}',
21+
'shared/third_party/hdiffpatch/bsdiff_wrapper/bspatch_wrapper.{c,h}',
22+
'shared/third_party/hdiffpatch/file_for_patch.{c,h}',
23+
]
1824
s.public_header_files = ['ios/CodePush/CodePush.h']
19-
s.pod_target_xcconfig = { "DEFINES_MODULE" => "YES" }
25+
s.pod_target_xcconfig = {
26+
"DEFINES_MODULE" => "YES",
27+
# HDiffPatch's bspatch-only usage: no multithreading, no directory diff/patch, and no raw
28+
# block device support (which would otherwise probe Linux-only <linux/fs.h> ioctls).
29+
# Keep in sync with android/app/src/main/cpp/CMakeLists.txt.
30+
"GCC_PREPROCESSOR_DEFINITIONS" => "$(inherited) _IS_NEED_BLOCK_DEV=0 _IS_USED_MULTITHREAD=0 _IS_NEED_DIR_DIFF_PATCH=0",
31+
"HEADER_SEARCH_PATHS" => "$(inherited) $(PODS_TARGET_SRCROOT)/shared $(PODS_TARGET_SRCROOT)/shared/diffpatch $(PODS_TARGET_SRCROOT)/shared/third_party/hdiffpatch $(PODS_TARGET_SRCROOT)/shared/third_party/hdiffpatch/libHDiffPatch/HPatch",
32+
}
2033

2134
# Note: Even though there are copy/pasted versions of some of these dependencies in the repo,
2235
# we explicitly let CocoaPods pull in the versions below so all dependencies are resolved and

android/app/src/main/cpp/CMakeLists.txt

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,12 @@ project(codepush_diffpatch C)
1212
# See third_party/README.md for what we vendor and why.
1313
add_library(codepush_diffpatch SHARED
1414
diffpatch_jni.c
15-
bspatch_bridge.c
15+
../../../../../shared/diffpatch/bspatch_bridge.c
16+
17+
../../../../../shared/third_party/hdiffpatch/libHDiffPatch/HPatch/patch.c
18+
../../../../../shared/third_party/hdiffpatch/bsdiff_wrapper/bspatch_wrapper.c
19+
../../../../../shared/third_party/hdiffpatch/file_for_patch.c
1620

17-
third_party/hdiffpatch/libHDiffPatch/HPatch/patch.c
18-
third_party/hdiffpatch/bsdiff_wrapper/bspatch_wrapper.c
19-
third_party/hdiffpatch/file_for_patch.c
20-
2121
bzip2_error_stub.c
2222
third_party/bzip2/bzlib.c
2323
third_party/bzip2/decompress.c
@@ -28,6 +28,8 @@ add_library(codepush_diffpatch SHARED
2828

2929
target_include_directories(codepush_diffpatch PRIVATE
3030
.
31+
../../../../../shared/diffpatch
32+
../../../../../shared
3133
third_party/bzip2
3234
)
3335

android/app/src/main/cpp/third_party/README.md

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,6 @@
11
# Vendored sources
22

3-
These directories contain trimmed copies of two upstream libraries, pinned
4-
to a specific commit. Only the files needed to *apply* a BSDIFF40-style
5-
patch (the `hdiffz -BSD` producer output) are vendored. `hdiffpatch/` is an
6-
unmodified-source copy; `bzip2/bzlib.c` carries one small, documented patch
7-
(see below).
8-
9-
## hdiffpatch/
10-
11-
Source: https://github.com/sisong/HDiffPatch
12-
Pinned commit: `3b9dca715ca492873bf2c49e22e5d5b7d2a78620` (2026-07-31)
13-
License: MIT.
14-
15-
Files were chosen by tracing the actual dependency graph of
16-
`bsdiff_wrapper/bspatch_wrapper.c` (the BSDIFF40-compatible patch applier),
17-
not by directory boundaries. In particular `libHDiffPatch/HPatch/patch.c`
18-
(~157KB) is HDiffPatch's own diff-format decoder, but it's still required
19-
here because `bspatch_wrapper.c` shares its low-level stream-cache helpers
20-
(`_TOutStreamCache_*`, `getStreamClip`, `_patch_cache_all_old`, etc.).
3+
Note: `shared/third_party/` also contains vendored sources, shared between Android and iOS.
214

225
## bzip2/
236

android/app/src/main/java/com/microsoft/codepush/react/diffpatch/DiffPatch.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ object DiffPatch {
3737
UNKNOWN;
3838

3939
companion object {
40-
// Keep in sync with cpp/bspatch_bridge.h
40+
// Keep in sync with shared/diffpatch/bspatch_bridge.h
4141
fun fromNativeCode(code: Int): PatchResult = when (code) {
4242
0 -> OK
4343
1 -> BAD_DIFF_HEADER

0 commit comments

Comments
 (0)