Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 89 additions & 0 deletions .github/workflows/test-kmp.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
name: Test Kotlin Multiplatform foundation

on:
workflow_dispatch:
pull_request:
paths:
- '.github/workflows/test-kmp.yml'
- '.github/actions/setup-gradle/**'
- '.github/actions/setup-xcode/**'
- 'packages/react-native/ReactShared/**'
push:
branches:
- main
- '*-stable'
paths:
- '.github/workflows/test-kmp.yml'
- '.github/actions/setup-gradle/**'
- '.github/actions/setup-xcode/**'
- 'packages/react-native/ReactShared/**'

permissions:
contents: read

concurrency:
group: kmp-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
smoke:
runs-on: macos-26
timeout-minutes: 45
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Set up JDK 17
uses: actions/setup-java@v5
with:
java-version: '17'
distribution: 'zulu'
- name: Set up Xcode
uses: ./.github/actions/setup-xcode
with:
xcode-version: '26.4.1'
- name: Set up Gradle
uses: ./.github/actions/setup-gradle
- name: Cache Kotlin Native toolchain
uses: actions/cache@v5
with:
path: ~/.konan
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') }}
- name: Test common code and build Apple frameworks
working-directory: packages/react-native/ReactShared
run: >-
./gradlew -PreactNativeSharedSmoke=true
jvmTest iosSimulatorArm64Test
linkDebugFrameworkIosArm64 linkReleaseFrameworkIosArm64
linkDebugFrameworkIosSimulatorArm64 linkReleaseFrameworkIosSimulatorArm64
linkDebugFrameworkIosX64 linkReleaseFrameworkIosX64
--max-workers=2 --stacktrace --console=plain
- name: Test Objective-C consumption
working-directory: packages/react-native/ReactShared
run: ./scripts/test-apple-smoke.sh
- name: Verify nonempty tests and fixture isolation
working-directory: packages/react-native/ReactShared
run: |
./gradlew jvmJar --max-workers=2 --console=plain
python3 - <<'PY'
from pathlib import Path
import xml.etree.ElementTree as ET
import zipfile
for target in ('jvmTest', 'iosSimulatorArm64Test'):
suites = [ET.parse(p).getroot() for p in Path('build/smoke/test-results', target).glob('TEST-*.xml')]
assert suites and sum(int(s.get('tests', 0)) for s in suites) > 0, target
assert all(not int(s.get(k, 0)) for s in suites for k in ('failures', 'errors', 'skipped')), target
jars = list(Path('build/libs').glob('*.jar'))
assert len(jars) == 1, jars
with zipfile.ZipFile(jars[0]) as jar:
assert not any('/smoke/' in name for name in jar.namelist()), 'Test fixture leaked into default output'
PY
- name: Upload smoke reports
if: always()
uses: actions/upload-artifact@v6
with:
name: kotlin-multiplatform-smoke-results
path: |
packages/react-native/ReactShared/build/smoke/reports/tests
packages/react-native/ReactShared/build/smoke/test-results
packages/react-native/ReactShared/build/smoke/apple-interop/*.log
if-no-files-found: warn
3 changes: 3 additions & 0 deletions packages/react-native/ReactShared/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/build/
/.gradle/
/.kotlin/
43 changes: 43 additions & 0 deletions packages/react-native/ReactShared/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Kotlin Multiplatform foundation

This standalone build provides a place to assess sharing library code between
the JVM and iOS with Kotlin Multiplatform. It has no Compose or other UI dependency.
No React Native application, Android artifact, CocoaPods target, or npm package
consumes this module. Normal React Native builds and runtime behavior are unchanged.

The build has no production sources yet. Its small arithmetic fixture is enabled
only by `-PreactNativeSharedSmoke=true`, and its outputs go under `build/smoke`.
The fixture checks the compiler, common tests and Objective-C integer interop;
it is not a proposed React Native API.

## Validation

Use JDK 17. Apple validation also requires macOS, Xcode and an installed iOS
simulator. The separate Gradle wrapper isolates the Kotlin/Native plugin from the
main Android build. CI selects Xcode 26.4.1 and uses an Apple Silicon runner.

From this directory:

```sh
./gradlew -PreactNativeSharedSmoke=true jvmTest iosSimulatorArm64Test \
linkDebugFrameworkIosArm64 linkReleaseFrameworkIosArm64 \
linkDebugFrameworkIosSimulatorArm64 linkReleaseFrameworkIosSimulatorArm64 \
linkDebugFrameworkIosX64 linkReleaseFrameworkIosX64 \
--max-workers=2 --console=plain
./scripts/test-apple-smoke.sh
```

The native runner checks Debug and Release frameworks through Objective-C on the
host simulator. Set `RCT_KMP_SIMULATOR_UDID` to select an existing simulator.
Intel simulator frameworks are built; Intel execution and physical-device tests
require their respective hosts and are not part of this CI job.

## Adoption is separate

Each use case needs its own behavior, dependency, packaging and performance
review before any application consumes shared code. Earlier gradient experiments
measured additional adapter latency, allocation and Kotlin/Native runtime cost;
this foundation does not establish that those costs are acceptable. Android AAR
embedding, Apple runtime ownership, CocoaPods, SwiftPM and prebuilt distribution
belong to those follow-ups. No Apple consumer or framework is designated as the
runtime owner here.
55 changes: 55 additions & 0 deletions packages/react-native/ReactShared/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.dsl.KotlinVersion
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget

plugins { kotlin("multiplatform") version "2.4.20" }

group = "com.facebook.react"

version = "0.0.0-local"

val smokeEnabled =
providers.gradleProperty("reactNativeSharedSmoke").map { it.toBooleanStrict() }.getOrElse(false)

if (smokeEnabled) {
// Keep test-only exports separate from any future production artifacts.
layout.buildDirectory.set(layout.projectDirectory.dir("build/smoke"))
}

kotlin {
explicitApi()
jvmToolchain(17)
compilerOptions {
// Preserve compatibility with the repository's existing Kotlin consumers.
languageVersion.set(KotlinVersion.KOTLIN_2_2)
apiVersion.set(KotlinVersion.KOTLIN_2_2)
}
jvm { compilerOptions { jvmTarget.set(JvmTarget.JVM_17) } }
iosArm64()
iosSimulatorArm64()
iosX64()

targets.withType<KotlinNativeTarget>().configureEach {
binaries.framework {
baseName = "ReactNativeShared"
isStatic = true
binaryOption("bundleId", "com.facebook.react.shared")
}
}

sourceSets {
commonMain.dependencies { implementation(kotlin("stdlib", "2.2.0")) }
commonTest.dependencies { implementation(kotlin("test")) }
if (smokeEnabled) {
commonMain { kotlin.srcDir("tests/smoke/kotlin") }
commonTest { kotlin.srcDir("tests/smoke/kotlinTest") }
}
}
}
3 changes: 3 additions & 0 deletions packages/react-native/ReactShared/gradle.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=768m -Dfile.encoding=UTF-8
org.gradle.caching=true
kotlin.stdlib.default.dependency=false
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip
distributionSha256Sum=2ab2958f2a1e51120c326cad6f385153bb11ee93b3c216c5fccebfdfbb7ec6cb
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Loading
Loading