Skip to content
Merged
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
51 changes: 50 additions & 1 deletion .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ jobs:
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- name: Build weak-node-api for all architectures
run: npm run build-weak-node-api -- --android
run: npm run build-weak-node-api:android
working-directory: packages/host
- name: Build ferric-example for all architectures
run: npm run build -- --android
Expand Down Expand Up @@ -188,3 +188,52 @@ jobs:
with:
name: emulator-logcat
path: apps/test-app/emulator-logcat.txt
test-ferric-apple-triplets:
if: github.ref == 'refs/heads/main' || contains(github.event.pull_request.labels.*.name, 'Ferric 🦀')
name: Test ferric Apple triplets
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: lts/jod
Copy link

Copilot AI Oct 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Corrected spelling of 'jod' to 'jod'. This should be 'lts/iron' or another valid LTS codename.

Suggested change
node-version: lts/jod
node-version: lts/iron

Copilot uses AI. Check for mistakes.
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: "17"
distribution: "temurin"
- run: rustup target add x86_64-apple-darwin x86_64-apple-ios aarch64-apple-ios aarch64-apple-ios-sim
- run: rustup toolchain install nightly --component rust-src
- run: npm ci
- run: npm run build
# Build weak-node-api for all Apple architectures
- run: |
npm run prepare-weak-node-api
npm run build-weak-node-api:apple
working-directory: packages/host
# Build Ferric example for all Apple architectures
- run: npx ferric --apple
working-directory: packages/ferric-example
- name: Inspect the structure of the prebuilt binary
run: lipo -info ferric_example.apple.node/*/libferric_example.framework/libferric_example > lipo-info.txt
working-directory: packages/ferric-example
- name: Upload lipo info
uses: actions/upload-artifact@v4
with:
name: lipo-info
path: packages/ferric-example/lipo-info.txt
- name: Verify Apple triplet builds
run: |
# Create expected fixture content
cat > expected-lipo-info.txt << 'EOF'
Architectures in the fat file: ferric_example.apple.node/ios-arm64_x86_64-simulator/libferric_example.framework/libferric_example are: x86_64 arm64
Architectures in the fat file: ferric_example.apple.node/macos-arm64_x86_64/libferric_example.framework/libferric_example are: x86_64 arm64
Architectures in the fat file: ferric_example.apple.node/tvos-arm64_x86_64-simulator/libferric_example.framework/libferric_example are: x86_64 arm64
Non-fat file: ferric_example.apple.node/ios-arm64/libferric_example.framework/libferric_example is architecture: arm64
Non-fat file: ferric_example.apple.node/tvos-arm64/libferric_example.framework/libferric_example is architecture: arm64
Non-fat file: ferric_example.apple.node/xros-arm64-simulator/libferric_example.framework/libferric_example is architecture: arm64
Non-fat file: ferric_example.apple.node/xros-arm64/libferric_example.framework/libferric_example is architecture: arm64
EOF
# Compare with expected fixture (will fail if files differ)
diff expected-lipo-info.txt lipo-info.txt
working-directory: packages/ferric-example
11 changes: 9 additions & 2 deletions packages/ferric/src/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import {
AndroidTargetName,
APPLE_TARGETS,
AppleTargetName,
ensureInstalledTargets,
ensureAvailableTargets,
filterTargetsByPlatform,
} from "./targets.js";
import { generateTypeScriptDeclarations } from "./napi-rs.js";
Expand Down Expand Up @@ -164,7 +164,7 @@ export const buildCommand = new Command("build")
);
}
ensureCargo();
ensureInstalledTargets(targets);
ensureAvailableTargets(targets);

const appleTargets = filterTargetsByPlatform(targets, "apple");
const androidTargets = filterTargetsByPlatform(targets, "android");
Expand Down Expand Up @@ -340,6 +340,7 @@ async function combineLibraries(
const result = [];
const darwinLibraries = [];
const iosSimulatorLibraries = [];
const tvosSimulatorLibraries = [];
for (const [target, libraryPath] of libraries) {
if (target.endsWith("-darwin")) {
darwinLibraries.push(libraryPath);
Expand All @@ -348,6 +349,11 @@ async function combineLibraries(
target === "x86_64-apple-ios" // Simulator despite name missing -sim suffix
) {
iosSimulatorLibraries.push(libraryPath);
} else if (
target === "aarch64-apple-tvos-sim" ||
target === "x86_64-apple-tvos" // Simulator despite name missing -sim suffix
) {
tvosSimulatorLibraries.push(libraryPath);
} else {
result.push(libraryPath);
}
Expand All @@ -356,6 +362,7 @@ async function combineLibraries(
const combinedLibraryPaths = await createUniversalAppleLibraries([
darwinLibraries,
iosSimulatorLibraries,
tvosSimulatorLibraries,
]);

return [...result, ...combinedLibraryPaths];
Expand Down
22 changes: 18 additions & 4 deletions packages/ferric/src/cargo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
AppleTargetName,
isAndroidTarget,
isAppleTarget,
isThirdTierTarget,
} from "./targets.js";

const APPLE_XCFRAMEWORK_CHILDS_PER_TARGET: Record<AppleTargetName, string> = {
Expand All @@ -26,12 +27,17 @@ const APPLE_XCFRAMEWORK_CHILDS_PER_TARGET: Record<AppleTargetName, string> = {
"aarch64-apple-ios-sim": "ios-arm64_x86_64-simulator", // Universal
"x86_64-apple-ios": "ios-arm64_x86_64-simulator", // Universal

"aarch64-apple-visionos": "xros-arm64",
"aarch64-apple-visionos-sim": "xros-arm64_x86_64-simulator", // Universal
// The x86_64 target for vision simulator isn't supported
// see https://doc.rust-lang.org/rustc/platform-support.html

"aarch64-apple-tvos": "tvos-arm64",
"aarch64-apple-tvos-sim": "tvos-arm64_x86_64-simulator",
"x86_64-apple-tvos": "tvos-arm64_x86_64-simulator",

// "aarch64-apple-ios-macabi": "", // Catalyst
// "x86_64-apple-ios-macabi": "ios-x86_64-simulator",
// "aarch64-apple-tvos": "tvos-arm64",
// "aarch64-apple-tvos-sim": "tvos-arm64-simulator",
// "aarch64-apple-visionos": "xros-arm64",
// "aarch64-apple-visionos-sim": "xros-arm64-simulator",
};

const ANDROID_ARCH_PR_TARGET: Record<AndroidTargetName, string> = {
Expand Down Expand Up @@ -84,6 +90,14 @@ export async function build(options: BuildOptions) {
if (configuration.toLowerCase() === "release") {
args.push("--release");
}
if (isThirdTierTarget(target)) {
// Use the nightly toolchain for third tier targets
args.splice(0, 0, "+nightly");
// Passing the nightly "build-std" to
// > Enable Cargo to compile the standard library itself as part of a crate graph compilation
// See https://doc.rust-lang.org/rustc/platform-support/apple-visionos.html#building-the-target
args.push("-Z", "build-std=std,panic_abort");
}
await spawn("cargo", args, {
outputMode: "buffered",
env: {
Expand Down
96 changes: 74 additions & 22 deletions packages/ferric/src/targets.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { chalk, UsageError } from "@react-native-node-api/cli-utils";
import cp from "node:child_process";

import { assertFixable } from "@react-native-node-api/cli-utils";
import { getInstalledTargets } from "./rustup.js";

export const ANDROID_TARGETS = [
Expand All @@ -24,25 +26,23 @@ export const APPLE_TARGETS = [
// "aarch64-apple-ios-macabi", // Catalyst
// "x86_64-apple-ios-macabi", // Catalyst

// TODO: Re-enabled these when we know how to install them 🙈
/*
"aarch64-apple-tvos",
"aarch64-apple-tvos-sim",
"aarch64-apple-visionos",
"aarch64-apple-visionos-sim",
*/

"aarch64-apple-tvos",
// "arm64e-apple-tvos",
"aarch64-apple-tvos-sim",
"x86_64-apple-tvos", // Simulator (despite the missing -sim suffix)

// "aarch64-apple-watchos",
// "aarch64-apple-watchos-sim",
// "arm64_32-apple-watchos",
// "arm64e-apple-darwin",
// "arm64e-apple-ios",
// "arm64e-apple-tvos",
// "armv7k-apple-watchos",
// "armv7s-apple-ios",
// "i386-apple-ios",
// "i686-apple-darwin",
// "x86_64-apple-tvos",
// "x86_64-apple-watchos-sim",
// "x86_64h-apple-darwin",
] as const;
Expand All @@ -51,24 +51,72 @@ export type AppleTargetName = (typeof APPLE_TARGETS)[number];
export const ALL_TARGETS = [...ANDROID_TARGETS, ...APPLE_TARGETS] as const;
export type TargetName = (typeof ALL_TARGETS)[number];

const THIRD_TIER_TARGETS: Set<TargetName> = new Set([
"aarch64-apple-visionos",
"aarch64-apple-visionos-sim",

"aarch64-apple-tvos",
"aarch64-apple-tvos-sim",
"x86_64-apple-tvos",
]);

export function assertNightlyToolchain() {
const toolchainLines = cp
.execFileSync("rustup", ["toolchain", "list"], {
encoding: "utf-8",
})
.split("\n");

const nightlyLines = toolchainLines.filter((line) =>
line.startsWith("nightly-"),
);
assertFixable(
nightlyLines.length > 0,
"You need to use a nightly Rust toolchain",
{
command: "rustup toolchain install nightly --component rust-src",
},
);

const componentLines = cp
.execFileSync("rustup", ["component", "list", "--toolchain", "nightly"], {
encoding: "utf-8",
})
.split("\n");
assertFixable(
componentLines.some((line) => line === "rust-src (installed)"),
"You need to install the rust-src component for the nightly Rust toolchain",
{
command: "rustup toolchain install nightly --component rust-src",
},
);
}

/**
* Ensure the targets are installed into the Rust toolchain
* Ensure the targets are either installed into the Rust toolchain or available via nightly Rust toolchain.
* We do this up-front because the error message and fix is very unclear from the failure when missing.
*/
export function ensureInstalledTargets(expectedTargets: Set<TargetName>) {
export function ensureAvailableTargets(expectedTargets: Set<TargetName>) {
const installedTargets = getInstalledTargets();
const missingTargets = new Set([
...[...expectedTargets].filter((target) => !installedTargets.has(target)),
]);
if (missingTargets.size > 0) {
// TODO: Ask the user if they want to run this
throw new UsageError(
`You're missing ${
missingTargets.size
} targets - to fix this, run:\n\n${chalk.italic(
`rustup target add ${[...missingTargets].join(" ")}`,
)}`,
);

const missingInstallableTargets = expectedTargets
.difference(installedTargets)
.difference(THIRD_TIER_TARGETS);

assertFixable(
missingInstallableTargets.size === 0,
`You need to add these targets to your toolchain: ${[
...missingInstallableTargets,
].join(", ")}`,
{
command: `rustup target add ${[...missingInstallableTargets].join(" ")}`,
},
);

const expectedThirdTierTargets =
expectedTargets.intersection(THIRD_TIER_TARGETS);
if (expectedThirdTierTargets.size > 0) {
assertNightlyToolchain();
}
}

Expand All @@ -82,6 +130,10 @@ export function isAppleTarget(target: TargetName): target is AppleTargetName {
return APPLE_TARGETS.includes(target as (typeof APPLE_TARGETS)[number]);
}

export function isThirdTierTarget(target: TargetName): boolean {
return THIRD_TIER_TARGETS.has(target);
}

export function filterTargetsByPlatform(
targets: Set<TargetName>,
platform: "android",
Expand Down
9 changes: 6 additions & 3 deletions packages/host/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,15 @@
"copy-node-api-headers": "tsx scripts/copy-node-api-headers.ts",
"generate-weak-node-api": "tsx scripts/generate-weak-node-api.ts",
"generate-weak-node-api-injector": "tsx scripts/generate-weak-node-api-injector.ts",
"prepare-weak-node-api": "node --run copy-node-api-headers && node --run generate-weak-node-api-injector && node --run generate-weak-node-api",
"build-weak-node-api": "cmake-rn --no-auto-link --no-weak-node-api-linkage --xcframework-extension --source ./weak-node-api --out ./weak-node-api",
"build-weak-node-api:all-triplets": "cmake-rn --android --apple --no-auto-link --no-weak-node-api-linkage --xcframework-extension --source ./weak-node-api --out ./weak-node-api",
"build-weak-node-api:android": "node --run build-weak-node-api -- --android",
"build-weak-node-api:apple": "node --run build-weak-node-api -- --apple",
"build-weak-node-api:all": "node --run build-weak-node-api -- --android --apple",
"test": "tsx --test --test-reporter=@reporters/github --test-reporter-destination=stdout --test-reporter=spec --test-reporter-destination=stdout src/node/**/*.test.ts src/node/*.test.ts",
"test:gradle": "ENABLE_GRADLE_TESTS=true node --run test",
"bootstrap": "node --run copy-node-api-headers && node --run generate-weak-node-api-injector && node --run generate-weak-node-api && node --run build-weak-node-api",
"prerelease": "node --run copy-node-api-headers && node --run generate-weak-node-api-injector && node --run generate-weak-node-api && node --run build-weak-node-api:all-triplets"
"bootstrap": "node --run prepare-weak-node-api && node --run build-weak-node-api",
"prerelease": "node --run prepare-weak-node-api && node --run build-weak-node-api:all"
},
"keywords": [
"react-native",
Expand Down
Loading