Skip to content

Commit 597c840

Browse files
committed
Share gradient stop calculations through Kotlin Multiplatform
1 parent 6945e2a commit 597c840

38 files changed

Lines changed: 3077 additions & 201 deletions

.github/workflows/test-all.yml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@ jobs:
4141
if: github.repository == 'react/react-native'
4242
outputs:
4343
any_code_change: ${{ steps.filter_exclusions.outputs.any_code_change == 'true' || github.event_name != 'pull_request' }}
44-
should_test_android: ${{ steps.filter_exclusions.outputs.should_test_android == 'true' || github.event_name != 'pull_request' }}
45-
should_test_ios: ${{ steps.filter_exclusions.outputs.should_test_ios == 'true' || github.event_name != 'pull_request' }}
44+
should_test_android: ${{ steps.filter_exclusions.outputs.should_test_android == 'true' || steps.filter_inclusions.outputs.react_shared == 'true' || github.event_name != 'pull_request' }}
45+
should_test_ios: ${{ steps.filter_exclusions.outputs.should_test_ios == 'true' || steps.filter_inclusions.outputs.react_shared == 'true' || github.event_name != 'pull_request' }}
4646
debugger_shell: ${{ steps.filter_inclusions.outputs.debugger_shell }}
4747
steps:
4848
- name: Checkout
@@ -85,6 +85,9 @@ jobs:
8585
id: filter_inclusions
8686
with:
8787
filters: |
88+
# Shared Kotlin and its build configuration affect both platforms.
89+
react_shared:
90+
- 'packages/react-native/ReactShared/**'
8891
debugger_shell:
8992
- 'packages/debugger-shell/**'
9093
- 'scripts/debugger-shell/**'

.github/workflows/test-kmp.yml

Lines changed: 282 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: Test Kotlin Multiplatform foundation
1+
name: Test Kotlin Multiplatform pilot
22

33
on:
44
workflow_dispatch:
@@ -7,7 +7,37 @@ on:
77
- '.github/workflows/test-kmp.yml'
88
- '.github/actions/setup-gradle/**'
99
- '.github/actions/setup-xcode/**'
10+
- '.github/actions/setup-node/**'
11+
- '.github/actions/yarn-install/**'
12+
- 'Gemfile'
13+
- 'Gemfile.lock'
14+
- 'build.gradle.kts'
15+
- 'gradle.properties'
16+
- 'gradle/wrapper/**'
17+
- 'settings.gradle.kts'
18+
- 'yarn.lock'
19+
- 'packages/rn-tester/Podfile'
20+
- 'packages/rn-tester/RCTTest/**'
21+
- 'packages/rn-tester/RNTesterPods.xcodeproj/**'
22+
- 'packages/rn-tester/RNTester/RNTester.xctestplan'
1023
- 'packages/react-native/ReactShared/**'
24+
- 'packages/react-native/ReactAndroid/build.gradle.kts'
25+
- 'packages/react-native/ReactAndroid/gradle.properties'
26+
- 'packages/react-native/gradle/libs.versions.toml'
27+
- 'packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/ColorStop.kt'
28+
- 'packages/react-native/ReactAndroid/src/test/java/com/facebook/react/uimanager/style/ColorStopTest.kt'
29+
- 'packages/react-native/React/Fabric/Utils/RCTGradientUtils.*'
30+
- 'packages/react-native/React/Fabric/RCTConversions.h'
31+
- 'packages/react-native/Libraries/NativeAnimation/RCTAnimationUtils.*'
32+
- 'packages/react-native/ReactCommon/react/renderer/graphics/**'
33+
- 'packages/react-native/ReactCommon/react/utils/FloatComparison.h'
34+
- 'packages/react-native/ReactCommon/react/utils/ManagedObjectWrapper.*'
35+
- 'packages/react-native/React/React-RCTFabric.podspec'
36+
- 'packages/react-native/scripts/react_native_pods.rb'
37+
- 'packages/react-native/scripts/cocoapods/kmp.rb'
38+
- 'packages/react-native/Package.swift'
39+
- 'packages/react-native/package.json'
40+
- 'packages/react-native/settings.gradle.kts'
1141
push:
1242
branches:
1343
- main
@@ -16,7 +46,37 @@ on:
1646
- '.github/workflows/test-kmp.yml'
1747
- '.github/actions/setup-gradle/**'
1848
- '.github/actions/setup-xcode/**'
49+
- '.github/actions/setup-node/**'
50+
- '.github/actions/yarn-install/**'
51+
- 'Gemfile'
52+
- 'Gemfile.lock'
53+
- 'build.gradle.kts'
54+
- 'gradle.properties'
55+
- 'gradle/wrapper/**'
56+
- 'settings.gradle.kts'
57+
- 'yarn.lock'
58+
- 'packages/rn-tester/Podfile'
59+
- 'packages/rn-tester/RCTTest/**'
60+
- 'packages/rn-tester/RNTesterPods.xcodeproj/**'
61+
- 'packages/rn-tester/RNTester/RNTester.xctestplan'
1962
- 'packages/react-native/ReactShared/**'
63+
- 'packages/react-native/ReactAndroid/build.gradle.kts'
64+
- 'packages/react-native/ReactAndroid/gradle.properties'
65+
- 'packages/react-native/gradle/libs.versions.toml'
66+
- 'packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/ColorStop.kt'
67+
- 'packages/react-native/ReactAndroid/src/test/java/com/facebook/react/uimanager/style/ColorStopTest.kt'
68+
- 'packages/react-native/React/Fabric/Utils/RCTGradientUtils.*'
69+
- 'packages/react-native/React/Fabric/RCTConversions.h'
70+
- 'packages/react-native/Libraries/NativeAnimation/RCTAnimationUtils.*'
71+
- 'packages/react-native/ReactCommon/react/renderer/graphics/**'
72+
- 'packages/react-native/ReactCommon/react/utils/FloatComparison.h'
73+
- 'packages/react-native/ReactCommon/react/utils/ManagedObjectWrapper.*'
74+
- 'packages/react-native/React/React-RCTFabric.podspec'
75+
- 'packages/react-native/scripts/react_native_pods.rb'
76+
- 'packages/react-native/scripts/cocoapods/kmp.rb'
77+
- 'packages/react-native/Package.swift'
78+
- 'packages/react-native/package.json'
79+
- 'packages/react-native/settings.gradle.kts'
2080

2181
permissions:
2282
contents: read
@@ -26,6 +86,227 @@ concurrency:
2686
cancel-in-progress: true
2787

2888
jobs:
89+
test-shared:
90+
runs-on: macos-26
91+
timeout-minutes: 45
92+
steps:
93+
- name: Checkout
94+
uses: actions/checkout@v6
95+
- name: Set up JDK 17
96+
uses: actions/setup-java@v5
97+
with:
98+
java-version: '17'
99+
distribution: 'zulu'
100+
- name: Set up Xcode for Kotlin Native
101+
uses: ./.github/actions/setup-xcode
102+
with:
103+
xcode-version: '26.4.1'
104+
- name: Set up Gradle
105+
uses: ./.github/actions/setup-gradle
106+
- name: Set up Ruby and CocoaPods
107+
uses: ruby/setup-ruby@v1
108+
with:
109+
ruby-version: '3.2.0'
110+
bundler-cache: true
111+
- name: Check CocoaPods linking configurations
112+
run: bundle exec ruby packages/react-native/ReactShared/scripts/test-cocoapods-linking.rb
113+
- name: Cache Kotlin Native toolchain
114+
uses: actions/cache@v5
115+
with:
116+
path: ~/.konan
117+
key: kmp-native-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('packages/react-native/ReactShared/*.gradle.kts', 'packages/react-native/ReactShared/gradle.properties', 'packages/react-native/ReactShared/gradle/wrapper/gradle-wrapper.properties') }}
118+
- name: Test shared code on JVM and iOS
119+
id: shared
120+
working-directory: packages/react-native/ReactShared
121+
run: >-
122+
./gradlew
123+
jvmTest iosSimulatorArm64Test
124+
linkDebugFrameworkIosArm64 linkReleaseFrameworkIosArm64
125+
linkDebugFrameworkIosSimulatorArm64 linkReleaseFrameworkIosSimulatorArm64
126+
linkDebugFrameworkIosX64 linkReleaseFrameworkIosX64
127+
--stacktrace --console=plain
128+
- name: Compare the Apple adapter with native gradients
129+
if: ${{ !cancelled() && steps.shared.outcome == 'success' }}
130+
working-directory: packages/react-native/ReactShared
131+
run: ./scripts/test-apple-gradient.sh
132+
- name: Compare optimized Apple gradients
133+
if: ${{ !cancelled() && steps.shared.outcome == 'success' }}
134+
working-directory: packages/react-native/ReactShared
135+
env:
136+
RCT_KMP_BUILD_TYPE: Release
137+
run: ./scripts/test-apple-gradient.sh
138+
- name: Test packaged XCFramework consumption
139+
if: ${{ !cancelled() && steps.shared.outcome == 'success' }}
140+
working-directory: packages/react-native/ReactShared
141+
run: ./scripts/test-apple-distribution.sh
142+
- name: Test independent Kotlin runtime coexistence
143+
if: ${{ !cancelled() && steps.shared.outcome == 'success' }}
144+
working-directory: packages/react-native/ReactShared
145+
run: python3 scripts/test-kotlin-coexistence.py
146+
- name: Measure native gradient costs
147+
if: ${{ !cancelled() && steps.shared.outcome == 'success' }}
148+
working-directory: packages/react-native/ReactShared
149+
run: python3 scripts/benchmark-apple-gradient.py
150+
- name: Measure clean and incremental shared builds
151+
if: ${{ !cancelled() && steps.shared.outcome == 'success' }}
152+
working-directory: packages/react-native/ReactShared
153+
run: python3 scripts/benchmark-kmp-build.py
154+
- name: Upload test reports
155+
if: always()
156+
uses: actions/upload-artifact@v6
157+
with:
158+
name: kotlin-multiplatform-test-results
159+
path: |
160+
packages/react-native/ReactShared/build/reports/tests
161+
packages/react-native/ReactShared/build/test-results
162+
packages/react-native/ReactShared/build/apple-distribution/**/*.json
163+
packages/react-native/ReactShared/build/apple-distribution/**/*.log
164+
packages/react-native/ReactShared/build/apple-distribution/**/consumer.xcresult
165+
packages/react-native/ReactShared/build/kotlin-coexistence/**/*.json
166+
packages/react-native/ReactShared/build/kotlin-coexistence/**/*.log
167+
packages/react-native/ReactShared/build/apple-gradient-benchmark/**/*.json
168+
packages/react-native/ReactShared/build/apple-gradient-benchmark/**/*.log
169+
packages/react-native/ReactShared/build/kmp-build-benchmark/**/*.json
170+
packages/react-native/ReactShared/build/kmp-build-benchmark/**/*.log
171+
if-no-files-found: warn
172+
173+
test-android-consumers:
174+
name: Android Maven and npm source consumers
175+
runs-on: 8-core-ubuntu
176+
timeout-minutes: 90
177+
steps:
178+
- name: Checkout
179+
uses: actions/checkout@v6
180+
- name: Set up Node
181+
uses: ./.github/actions/setup-node
182+
- name: Install JavaScript dependencies
183+
uses: ./.github/actions/yarn-install
184+
- name: Set up Python
185+
uses: actions/setup-python@v5
186+
with:
187+
python-version: '3.12'
188+
- name: Set up JDK 17
189+
uses: actions/setup-java@v5
190+
with:
191+
java-version: '17'
192+
distribution: 'zulu'
193+
- name: Set up Gradle
194+
uses: ./.github/actions/setup-gradle
195+
- name: Install Android build SDK
196+
id: android-sdk
197+
run: |
198+
python3 - <<'PY'
199+
import os, pathlib, re, shutil, subprocess, tomllib
200+
versions = tomllib.loads(pathlib.Path('packages/react-native/gradle/libs.versions.toml').read_text())['versions']
201+
sdkmanager = shutil.which('sdkmanager') or str(pathlib.Path(os.environ['ANDROID_HOME']) / 'cmdline-tools/latest/bin/sdkmanager')
202+
packages = subprocess.check_output([sdkmanager, '--list'], text=True)
203+
base = 'platforms;android-' + versions['compileSdk']
204+
# Recent SDKs use a minor version in their package ID (for example android-37.0).
205+
platform = next((name for name in [base + '.0', base] if re.search(r'^\s*' + re.escape(name) + r'\s*\|', packages, re.M)), None)
206+
if platform is None:
207+
raise SystemExit('No Android SDK package matches compileSdk ' + versions['compileSdk'])
208+
subprocess.run([sdkmanager, platform, 'build-tools;' + versions['buildTools']], check=True)
209+
with open(os.environ['GITHUB_OUTPUT'], 'a') as output:
210+
output.write('ndk=' + versions['ndkVersion'] + '\n')
211+
PY
212+
- name: Enable emulator hardware acceleration
213+
run: |
214+
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
215+
sudo udevadm control --reload-rules
216+
sudo udevadm trigger --name-match=kvm
217+
- name: Build and run packaged Android consumers
218+
uses: reactivecircus/android-emulator-runner@v2
219+
with:
220+
api-level: 35
221+
target: google_apis
222+
arch: x86_64
223+
ndk: ${{ steps.android-sdk.outputs.ndk }}
224+
cmake: 3.30.5
225+
cores: 4
226+
disable-animations: true
227+
script: python3 packages/react-native/ReactShared/scripts/test-android-consumers.py --output-dir "$RUNNER_TEMP/kmp-android-consumers" --abi x86_64 --device emulator-5554
228+
- name: Upload consumer reports
229+
if: always()
230+
uses: actions/upload-artifact@v6
231+
with:
232+
name: kmp-android-consumers
233+
path: |
234+
${{ runner.temp }}/kmp-android-consumers/*.log
235+
${{ runner.temp }}/kmp-android-consumers/*.json
236+
if-no-files-found: warn
237+
238+
test-apple-app:
239+
name: RNTester KMP ${{ matrix.platform }} (${{ matrix.linkage }})
240+
runs-on: macos-26
241+
timeout-minutes: 90
242+
strategy:
243+
fail-fast: false
244+
matrix:
245+
include:
246+
- platform: simulator
247+
linkage: libraries
248+
- platform: simulator
249+
linkage: static
250+
- platform: simulator
251+
linkage: dynamic
252+
- platform: device
253+
linkage: libraries
254+
- platform: catalyst
255+
linkage: libraries
256+
env:
257+
RCT_KMP_APP_OUTPUT_DIR: ${{ github.workspace }}/packages/react-native/ReactShared/build/apple-app-results
258+
steps:
259+
- name: Checkout
260+
uses: actions/checkout@v6
261+
- name: Set up Node
262+
uses: ./.github/actions/setup-node
263+
- name: Install JavaScript dependencies
264+
uses: ./.github/actions/yarn-install
265+
- name: Select the Hermes compiler package
266+
run: node scripts/releases/use-hermes-prebuilt.js
267+
- name: Install the selected Hermes compiler
268+
uses: ./.github/actions/yarn-install
269+
- name: Set up JDK 17
270+
uses: actions/setup-java@v5
271+
with:
272+
java-version: '17'
273+
distribution: 'zulu'
274+
- name: Set up Xcode for Kotlin Native
275+
uses: ./.github/actions/setup-xcode
276+
with:
277+
xcode-version: '26.4.1'
278+
- name: Set up Gradle
279+
uses: ./.github/actions/setup-gradle
280+
- name: Set up Ruby and CocoaPods
281+
uses: ruby/setup-ruby@v1
282+
with:
283+
ruby-version: '3.2.0'
284+
bundler-cache: true
285+
- name: Cache Kotlin Native toolchain
286+
uses: actions/cache@v5
287+
with:
288+
path: ~/.konan
289+
key: kmp-native-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('packages/react-native/ReactShared/*.gradle.kts', 'packages/react-native/ReactShared/gradle.properties', 'packages/react-native/ReactShared/gradle/wrapper/gradle-wrapper.properties') }}
290+
- name: Build and validate the KMP source application
291+
run: |
292+
if [[ '${{ matrix.linkage }}' != libraries ]]; then
293+
export USE_FRAMEWORKS='${{ matrix.linkage }}'
294+
fi
295+
./packages/react-native/ReactShared/scripts/test-apple-app.sh '${{ matrix.platform }}'
296+
- name: Upload application test reports
297+
if: always()
298+
uses: actions/upload-artifact@v6
299+
with:
300+
name: kmp-app-${{ matrix.platform }}-${{ matrix.linkage }}
301+
path: |
302+
${{ env.RCT_KMP_APP_OUTPUT_DIR }}/*.log
303+
${{ env.RCT_KMP_APP_OUTPUT_DIR }}/*.json
304+
${{ env.RCT_KMP_APP_OUTPUT_DIR }}/*.txt
305+
${{ env.RCT_KMP_APP_OUTPUT_DIR }}/*.xcresult
306+
${{ env.RCT_KMP_APP_OUTPUT_DIR }}/Podfile*
307+
${{ env.RCT_KMP_APP_OUTPUT_DIR }}/project.pbxproj
308+
if-no-files-found: warn
309+
29310
smoke:
30311
runs-on: macos-26
31312
timeout-minutes: 45

build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ nexusPublishing {
7272
tasks.register("clean", Delete::class.java) {
7373
description = "Remove all the build files and intermediate build outputs"
7474
dependsOn(gradle.includedBuild("gradle-plugin").task(":clean"))
75+
dependsOn(gradle.includedBuild("react-native-shared").task(":clean"))
7576
subprojects.forEach {
7677
if (
7778
it.project.plugins.hasPlugin("com.android.library") ||
@@ -103,6 +104,7 @@ tasks.register("clean", Delete::class.java) {
103104
tasks.register("build") {
104105
description = "Build and test all the React Native relevant projects."
105106
dependsOn(gradle.includedBuild("gradle-plugin").task(":build"))
107+
dependsOn(gradle.includedBuild("react-native-shared").task(":jvmTest"))
106108
}
107109

108110
tasks.register("publishAllToMavenTempLocal") {

packages/react-native/Package.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@
99
import Foundation
1010
import PackageDescription
1111

12+
if ProcessInfo.processInfo.environment["RCT_USE_KMP"] == "1" {
13+
fatalError("RCT_USE_KMP=1 currently supports CocoaPods source builds only. Run RCT_USE_KMP=1 pod install in the iOS app; the SwiftPM prebuild does not include ReactNativeShared.")
14+
}
15+
1216
let BUILD_FROM_SOURCE = false
1317

1418
// Removing the legacy TurboModule and component interop layers is opt-in while those

0 commit comments

Comments
 (0)