Skip to content
Open
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
7 changes: 7 additions & 0 deletions .changeset/rich-weeks-cry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"cmake-rn": minor
"ferric-cli": minor
"react-native-node-api": minor
---

Add support for building versioned frameworks for Apple Darwin / macOS
2 changes: 1 addition & 1 deletion .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ env:
# Version here should match the one in React Native template and packages/cmake-rn/src/cli.ts
NDK_VERSION: 27.1.12297006
# Building Hermes from source doesn't support CMake v4
CMAKE_VERSION: 3.31.6
CMAKE_VERSION: 4.2.2
# Enabling the Gradle test on CI (disabled by default because it downloads a lot)
ENABLE_GRADLE_TESTS: true

Expand Down
2 changes: 1 addition & 1 deletion packages/cmake-rn/src/platforms/apple.ts
Original file line number Diff line number Diff line change
Expand Up @@ -383,7 +383,7 @@ export const platform: Platform<Triplet[], AppleOpts> = {
const [artifact] = artifacts;
await createAppleFramework({
libraryPath: path.join(buildPath, artifact.path),
versioned: triplet.endsWith("-darwin"),
kind: triplet.endsWith("-darwin") ? "versioned" : "flat",
bundleIdentifier: appleBundleIdentifier,
});
}
Expand Down
49 changes: 32 additions & 17 deletions packages/ferric/src/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,11 @@ import {
ANDROID_TARGETS,
AndroidTargetName,
APPLE_TARGETS,
AppleOperatingSystem,
AppleTargetName,
ensureAvailableTargets,
filterTargetsByPlatform,
parseAppleTargetName,
} from "./targets.js";
import { generateTypeScriptDeclarations } from "./napi-rs.js";
import { getBlockComment } from "./banner.js";
Expand Down Expand Up @@ -299,15 +301,15 @@ export const buildCommand = new Command("build")
}

if (appleLibraries.length > 0) {
const libraryPaths = await combineLibraries(appleLibraries);
const libraries = await combineAppleLibraries(appleLibraries);

const frameworkPaths = await oraPromise(
Promise.all(
libraryPaths.map((libraryPath) =>
libraries.map((library) =>
limit(() =>
// TODO: Pass true as `versioned` argument for -darwin targets
createAppleFramework({
libraryPath,
libraryPath: library.path,
kind: library.os === "darwin" ? "versioned" : "flat",
bundleIdentifier: appleBundleIdentifier,
}),
),
Expand Down Expand Up @@ -389,16 +391,23 @@ export const buildCommand = new Command("build")
),
);

async function createUniversalAppleLibraries(libraryPathGroups: string[][]) {
async function createUniversalAppleLibraries(
groups: { os: AppleOperatingSystem; paths: string[] }[],
): Promise<{ os: AppleOperatingSystem; path: string }[]> {
const result = await oraPromise(
Promise.all(
libraryPathGroups.map(async (libraryPaths) => {
if (libraryPaths.length === 0) {
groups.map(async ({ os, paths }) => {
if (paths.length === 0) {
return [];
} else if (libraryPaths.length === 1) {
return libraryPaths;
} else if (paths.length == 1) {
return [{ os, path: paths[0] }];
} else {
return [await createUniversalAppleLibrary(libraryPaths)];
return [
{
os,
path: await createUniversalAppleLibrary(paths),
},
];
}
}),
),
Expand All @@ -412,15 +421,21 @@ async function createUniversalAppleLibraries(libraryPathGroups: string[][]) {
return result.flat();
}

async function combineLibraries(
type CombinedAppleLibrary = {
path: string;
os: AppleOperatingSystem;
};

async function combineAppleLibraries(
libraries: Readonly<[AppleTargetName, string]>[],
): Promise<string[]> {
): Promise<CombinedAppleLibrary[]> {
const result = [];
const darwinLibraries = [];
const iosSimulatorLibraries = [];
const tvosSimulatorLibraries = [];
for (const [target, libraryPath] of libraries) {
if (target.endsWith("-darwin")) {
const { os } = parseAppleTargetName(target);
if (os === "darwin") {
darwinLibraries.push(libraryPath);
} else if (
target === "aarch64-apple-ios-sim" ||
Expand All @@ -433,14 +448,14 @@ async function combineLibraries(
) {
tvosSimulatorLibraries.push(libraryPath);
} else {
result.push(libraryPath);
result.push({ os, path: libraryPath });
}
}

const combinedLibraryPaths = await createUniversalAppleLibraries([
darwinLibraries,
iosSimulatorLibraries,
tvosSimulatorLibraries,
{ os: "darwin", paths: darwinLibraries },
{ os: "ios", paths: iosSimulatorLibraries },
{ os: "tvos", paths: tvosSimulatorLibraries },
]);

return [...result, ...combinedLibraryPaths];
Expand Down
60 changes: 60 additions & 0 deletions packages/ferric/src/targets.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import assert from "node:assert";
import cp from "node:child_process";

import { assertFixable } from "@react-native-node-api/cli-utils";
Expand Down Expand Up @@ -48,6 +49,65 @@ export const APPLE_TARGETS = [
] as const;
export type AppleTargetName = (typeof APPLE_TARGETS)[number];

const APPLE_ARCHITECTURES = [
"aarch64",
"arm64_32",
"arm64e",
"armv7",
"armv7s",
"x86_64",
"x86_64h",
"i386",
"i686",
] as const;
export type AppleArchitecture = (typeof APPLE_ARCHITECTURES)[number];
export function isAppleArchitecture(
architecture: string,
): architecture is AppleArchitecture {
return (APPLE_ARCHITECTURES as readonly string[]).includes(architecture);
}

const APPLE_OPERATING_SYSTEMS = [
"darwin",
"ios",
"tvos",
"visionos",
"watchos",
] as const;
export type AppleOperatingSystem = (typeof APPLE_OPERATING_SYSTEMS)[number];
export function isAppleOperatingSystem(os: string): os is AppleOperatingSystem {
return (APPLE_OPERATING_SYSTEMS as readonly string[]).includes(os);
}

const APPLE_VARIANTS = ["sim", "macabi"] as const;
export type AppleVariant = (typeof APPLE_VARIANTS)[number];
export function isAppleVariant(variant: string): variant is AppleVariant {
return (APPLE_VARIANTS as readonly string[]).includes(variant);
}

export function parseAppleTargetName(target: AppleTargetName): {
architecture: AppleArchitecture;
os: AppleOperatingSystem;
variant?: AppleVariant;
} {
const [architecture, vendor, os, variant] = target.split("-");
assert(vendor === "apple", "Expected vendor to be apple");
assert(
isAppleArchitecture(architecture),
`Unexpected architecture: ${architecture}`,
);
assert(isAppleOperatingSystem(os), `Unexpected operating system: ${os}`);
assert(
typeof variant === "undefined" || isAppleVariant(variant),
`Unexpected variant: ${variant}`,
);
return {
architecture,
os,
variant,
};
}

export const ALL_TARGETS = [...ANDROID_TARGETS, ...APPLE_TARGETS] as const;
export type TargetName = (typeof ALL_TARGETS)[number];

Expand Down
Loading
Loading