Skip to content

Commit b3f65d2

Browse files
committed
Add standalone Kotlin Multiplatform foundation
1 parent f109c0a commit b3f65d2

14 files changed

Lines changed: 689 additions & 0 deletions

File tree

.github/workflows/test-kmp.yml

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
name: Test Kotlin Multiplatform foundation
2+
3+
on:
4+
workflow_dispatch:
5+
pull_request:
6+
paths:
7+
- '.github/workflows/test-kmp.yml'
8+
- '.github/actions/setup-gradle/**'
9+
- '.github/actions/setup-xcode/**'
10+
- 'packages/react-native/ReactShared/**'
11+
push:
12+
branches:
13+
- main
14+
- '*-stable'
15+
paths:
16+
- '.github/workflows/test-kmp.yml'
17+
- '.github/actions/setup-gradle/**'
18+
- '.github/actions/setup-xcode/**'
19+
- 'packages/react-native/ReactShared/**'
20+
21+
permissions:
22+
contents: read
23+
24+
concurrency:
25+
group: kmp-${{ github.workflow }}-${{ github.ref }}
26+
cancel-in-progress: true
27+
28+
jobs:
29+
smoke:
30+
runs-on: macos-26
31+
timeout-minutes: 45
32+
steps:
33+
- name: Checkout
34+
uses: actions/checkout@v6
35+
- name: Set up JDK 17
36+
uses: actions/setup-java@v5
37+
with:
38+
java-version: '17'
39+
distribution: 'zulu'
40+
- name: Set up Xcode
41+
uses: ./.github/actions/setup-xcode
42+
with:
43+
xcode-version: '26.4.1'
44+
- name: Set up Gradle
45+
uses: ./.github/actions/setup-gradle
46+
- name: Cache Kotlin Native toolchain
47+
uses: actions/cache@v5
48+
with:
49+
path: ~/.konan
50+
key: kmp-foundation-${{ 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') }}
51+
- name: Test common code and build Apple frameworks
52+
working-directory: packages/react-native/ReactShared
53+
run: >-
54+
./gradlew -PreactNativeSharedSmoke=true
55+
jvmTest iosSimulatorArm64Test
56+
linkDebugFrameworkIosArm64 linkReleaseFrameworkIosArm64
57+
linkDebugFrameworkIosSimulatorArm64 linkReleaseFrameworkIosSimulatorArm64
58+
linkDebugFrameworkIosX64 linkReleaseFrameworkIosX64
59+
--max-workers=2 --stacktrace --console=plain
60+
- name: Test Objective-C consumption
61+
working-directory: packages/react-native/ReactShared
62+
run: ./scripts/test-apple-smoke.sh
63+
- name: Verify nonempty tests and fixture isolation
64+
working-directory: packages/react-native/ReactShared
65+
run: |
66+
./gradlew jvmJar --max-workers=2 --console=plain
67+
python3 - <<'PY'
68+
from pathlib import Path
69+
import xml.etree.ElementTree as ET
70+
import zipfile
71+
for target in ('jvmTest', 'iosSimulatorArm64Test'):
72+
suites = [ET.parse(p).getroot() for p in Path('build/smoke/test-results', target).glob('TEST-*.xml')]
73+
assert suites and sum(int(s.get('tests', 0)) for s in suites) > 0, target
74+
assert all(not int(s.get(k, 0)) for s in suites for k in ('failures', 'errors', 'skipped')), target
75+
jars = list(Path('build/libs').glob('*.jar'))
76+
assert len(jars) == 1, jars
77+
with zipfile.ZipFile(jars[0]) as jar:
78+
assert not any('/smoke/' in name for name in jar.namelist()), 'Test fixture leaked into default output'
79+
PY
80+
- name: Upload smoke reports
81+
if: always()
82+
uses: actions/upload-artifact@v6
83+
with:
84+
name: kotlin-multiplatform-smoke-results
85+
path: |
86+
packages/react-native/ReactShared/build/smoke/reports/tests
87+
packages/react-native/ReactShared/build/smoke/test-results
88+
packages/react-native/ReactShared/build/smoke/apple-interop/*.log
89+
if-no-files-found: warn
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
/build/
2+
/.gradle/
3+
/.kotlin/
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Kotlin Multiplatform foundation
2+
3+
This standalone build provides a place to assess sharing library code between
4+
the JVM and iOS with Kotlin Multiplatform. It has no Compose or other UI dependency.
5+
No React Native application, Android artifact, CocoaPods target, or npm package
6+
consumes this module. Normal React Native builds and runtime behavior are unchanged.
7+
8+
The build has no production sources yet. Its small arithmetic fixture is enabled
9+
only by `-PreactNativeSharedSmoke=true`, and its outputs go under `build/smoke`.
10+
The fixture checks the compiler, common tests and Objective-C integer interop;
11+
it is not a proposed React Native API.
12+
13+
## Validation
14+
15+
Use JDK 17. Apple validation also requires macOS, Xcode and an installed iOS
16+
simulator. The separate Gradle wrapper isolates the Kotlin/Native plugin from the
17+
main Android build. CI selects Xcode 26.4.1 and uses an Apple Silicon runner.
18+
19+
From this directory:
20+
21+
```sh
22+
./gradlew -PreactNativeSharedSmoke=true jvmTest iosSimulatorArm64Test \
23+
linkDebugFrameworkIosArm64 linkReleaseFrameworkIosArm64 \
24+
linkDebugFrameworkIosSimulatorArm64 linkReleaseFrameworkIosSimulatorArm64 \
25+
linkDebugFrameworkIosX64 linkReleaseFrameworkIosX64 \
26+
--max-workers=2 --console=plain
27+
./scripts/test-apple-smoke.sh
28+
```
29+
30+
The native runner checks Debug and Release frameworks through Objective-C on the
31+
host simulator. Set `RCT_KMP_SIMULATOR_UDID` to select an existing simulator.
32+
Intel simulator frameworks are built; Intel execution and physical-device tests
33+
require their respective hosts and are not part of this CI job.
34+
35+
## Adoption is separate
36+
37+
Each use case needs its own behavior, dependency, packaging and performance
38+
review before any application consumes shared code. Earlier gradient experiments
39+
measured additional adapter latency, allocation and Kotlin/Native runtime cost;
40+
this foundation does not establish that those costs are acceptable. Android AAR
41+
embedding, Apple runtime ownership, CocoaPods, SwiftPM and prebuilt distribution
42+
belong to those follow-ups. No Apple consumer or framework is designated as the
43+
runtime owner here.
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
9+
import org.jetbrains.kotlin.gradle.dsl.KotlinVersion
10+
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
11+
12+
plugins { kotlin("multiplatform") version "2.4.20" }
13+
14+
group = "com.facebook.react"
15+
16+
version = "0.0.0-local"
17+
18+
val smokeEnabled =
19+
providers.gradleProperty("reactNativeSharedSmoke").map { it.toBooleanStrict() }.getOrElse(false)
20+
21+
if (smokeEnabled) {
22+
// Keep test-only exports separate from any future production artifacts.
23+
layout.buildDirectory.set(layout.projectDirectory.dir("build/smoke"))
24+
}
25+
26+
kotlin {
27+
explicitApi()
28+
jvmToolchain(17)
29+
compilerOptions {
30+
// Preserve compatibility with the repository's existing Kotlin consumers.
31+
languageVersion.set(KotlinVersion.KOTLIN_2_2)
32+
apiVersion.set(KotlinVersion.KOTLIN_2_2)
33+
}
34+
jvm { compilerOptions { jvmTarget.set(JvmTarget.JVM_17) } }
35+
iosArm64()
36+
iosSimulatorArm64()
37+
iosX64()
38+
39+
targets.withType<KotlinNativeTarget>().configureEach {
40+
binaries.framework {
41+
baseName = "ReactNativeShared"
42+
isStatic = true
43+
binaryOption("bundleId", "com.facebook.react.shared")
44+
}
45+
}
46+
47+
sourceSets {
48+
commonMain.dependencies { implementation(kotlin("stdlib", "2.2.0")) }
49+
commonTest.dependencies { implementation(kotlin("test")) }
50+
if (smokeEnabled) {
51+
commonMain { kotlin.srcDir("tests/smoke/kotlin") }
52+
commonTest { kotlin.srcDir("tests/smoke/kotlinTest") }
53+
}
54+
}
55+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=768m -Dfile.encoding=UTF-8
2+
org.gradle.caching=true
3+
kotlin.stdlib.default.dependency=false
Binary file not shown.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
distributionBase=GRADLE_USER_HOME
2+
distributionPath=wrapper/dists
3+
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip
4+
distributionSha256Sum=2ab2958f2a1e51120c326cad6f385153bb11ee93b3c216c5fccebfdfbb7ec6cb
5+
networkTimeout=10000
6+
validateDistributionUrl=true
7+
zipStoreBase=GRADLE_USER_HOME
8+
zipStorePath=wrapper/dists

0 commit comments

Comments
 (0)