diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 1955f9d2..ec9c7cee 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -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 @@ -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 + - 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 diff --git a/packages/ferric/src/build.ts b/packages/ferric/src/build.ts index 4dd26d87..6587347e 100644 --- a/packages/ferric/src/build.ts +++ b/packages/ferric/src/build.ts @@ -29,7 +29,7 @@ import { AndroidTargetName, APPLE_TARGETS, AppleTargetName, - ensureInstalledTargets, + ensureAvailableTargets, filterTargetsByPlatform, } from "./targets.js"; import { generateTypeScriptDeclarations } from "./napi-rs.js"; @@ -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"); @@ -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); @@ -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); } @@ -356,6 +362,7 @@ async function combineLibraries( const combinedLibraryPaths = await createUniversalAppleLibraries([ darwinLibraries, iosSimulatorLibraries, + tvosSimulatorLibraries, ]); return [...result, ...combinedLibraryPaths]; diff --git a/packages/ferric/src/cargo.ts b/packages/ferric/src/cargo.ts index 7b665b98..9afa0770 100644 --- a/packages/ferric/src/cargo.ts +++ b/packages/ferric/src/cargo.ts @@ -16,6 +16,7 @@ import { AppleTargetName, isAndroidTarget, isAppleTarget, + isThirdTierTarget, } from "./targets.js"; const APPLE_XCFRAMEWORK_CHILDS_PER_TARGET: Record = { @@ -26,12 +27,17 @@ const APPLE_XCFRAMEWORK_CHILDS_PER_TARGET: Record = { "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 = { @@ -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: { diff --git a/packages/ferric/src/targets.ts b/packages/ferric/src/targets.ts index 0e548de6..513c27e1 100644 --- a/packages/ferric/src/targets.ts +++ b/packages/ferric/src/targets.ts @@ -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 = [ @@ -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; @@ -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 = 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) { +export function ensureAvailableTargets(expectedTargets: Set) { 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(); } } @@ -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, platform: "android", diff --git a/packages/host/package.json b/packages/host/package.json index 87118caf..c12e5963 100644 --- a/packages/host/package.json +++ b/packages/host/package.json @@ -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",