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
5 changes: 5 additions & 0 deletions .changeset/better-pets-help.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"ferric-cli": patch
---

Add x86_64 ios simulator target and output universal libraries for iOS simulators.
5 changes: 5 additions & 0 deletions .changeset/large-hornets-burn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"ferric-cli": patch
---

It's no longer required to pass "build" to ferric, as this is default now
52 changes: 36 additions & 16 deletions packages/ferric/src/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -311,34 +311,54 @@ export const buildCommand = new Command("build")
),
);

async function createUniversalAppleLibraries(libraryPathGroups: string[][]) {
const result = await oraPromise(
Promise.all(
libraryPathGroups.map(async (libraryPaths) => {
if (libraryPaths.length === 0) {
return [];
Comment on lines +318 to +319
Copy link

Copilot AI Oct 26, 2025

Choose a reason for hiding this comment

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

The empty array check is unnecessary because the function is called with pre-filtered groups (darwinLibraries and iosSimulatorLibraries). If either group is empty, it should not be included in the input array to avoid processing empty groups. Consider filtering out empty groups before calling this function.

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

@kraenhansen kraenhansen Oct 26, 2025

Choose a reason for hiding this comment

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

As we're also exporting this function, I don't want to make too many assumptions on how it's called. Also, it seems the call pattern is much simpler here than if we had to not pass the array argument (darwinLibraries or iosSimulatorLibraries) if it's empty.

} else if (libraryPaths.length === 1) {
return libraryPaths;
} else {
return [await createUniversalAppleLibrary(libraryPaths)];
}
}),
),
{
text: "Combining arch-specific libraries into universal libraries",
successText: "Combined arch-specific libraries into universal libraries",
failText: (error) =>
`Failed to combine arch-specific libraries: ${error.message}`,
},
);
return result.flat();
}

async function combineLibraries(
libraries: Readonly<[AppleTargetName, string]>[],
): Promise<string[]> {
const result = [];
const darwinLibraries = [];
const iosSimulatorLibraries = [];
for (const [target, libraryPath] of libraries) {
if (target.endsWith("-darwin")) {
darwinLibraries.push(libraryPath);
} else if (
target === "aarch64-apple-ios-sim" ||
target === "x86_64-apple-ios" // Simulator despite name missing -sim suffix
) {
iosSimulatorLibraries.push(libraryPath);
} else {
result.push(libraryPath);
}
}
if (darwinLibraries.length === 0) {
return result;
} else if (darwinLibraries.length === 1) {
return [...result, darwinLibraries[0]];
} else {
const universalPath = await oraPromise(
createUniversalAppleLibrary(darwinLibraries),
{
text: "Combining Darwin libraries into a universal library",
successText: "Combined Darwin libraries into a universal library",
failText: (error) =>
`Failed to combine Darwin libraries: ${error.message}`,
},
);
return [...result, universalPath];
}

const combinedLibraryPaths = await createUniversalAppleLibraries([
darwinLibraries,
iosSimulatorLibraries,
]);

return [...result, ...combinedLibraryPaths];
}

export function isAndroidSupported() {
Expand Down
8 changes: 7 additions & 1 deletion packages/ferric/src/cargo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,12 @@ import {
const APPLE_XCFRAMEWORK_CHILDS_PER_TARGET: Record<AppleTargetName, string> = {
"aarch64-apple-darwin": "macos-arm64_x86_64", // Universal
"x86_64-apple-darwin": "macos-arm64_x86_64", // Universal

"aarch64-apple-ios": "ios-arm64",
"aarch64-apple-ios-sim": "ios-arm64_x86_64-simulator", // Universal
"x86_64-apple-ios": "ios-arm64_x86_64-simulator", // Universal

// "aarch64-apple-ios-macabi": "", // Catalyst
// "x86_64-apple-ios": "ios-x86_64",
// "x86_64-apple-ios-macabi": "ios-x86_64-simulator",
// "aarch64-apple-tvos": "tvos-arm64",
// "aarch64-apple-tvos-sim": "tvos-arm64-simulator",
Expand Down Expand Up @@ -216,6 +218,10 @@ export function getTargetEnvironmentVariables({
};
} else if (isAppleTarget(target)) {
const weakNodeApiFrameworkPath = getWeakNodeApiFrameworkPath(target);
assert(
fs.existsSync(weakNodeApiFrameworkPath),
`Expected weak-node-api framework at ${weakNodeApiFrameworkPath}`,
);
return {
CARGO_ENCODED_RUSTFLAGS: [
"-L",
Expand Down
2 changes: 1 addition & 1 deletion packages/ferric/src/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ import { buildCommand } from "./build.js";
export const program = new Command("ferric")
.hook("preAction", () => printBanner())
.description("Rust Node-API Modules for React Native")
.addCommand(buildCommand);
.addCommand(buildCommand, { isDefault: true });
4 changes: 3 additions & 1 deletion packages/ferric/src/targets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@ export type AndroidTargetName = (typeof ANDROID_TARGETS)[number];
export const APPLE_TARGETS = [
"aarch64-apple-darwin",
"x86_64-apple-darwin",

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

// "aarch64-apple-ios-macabi", // Catalyst
// "x86_64-apple-ios",
// "x86_64-apple-ios-macabi", // Catalyst

// TODO: Re-enabled these when we know how to install them 🙈
Expand Down
8 changes: 7 additions & 1 deletion packages/host/src/node/prebuilds/apple.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,11 +131,17 @@ export function determineXCFrameworkFilename(
}

export async function createUniversalAppleLibrary(libraryPaths: string[]) {
assert(
libraryPaths.length > 0,
"Expected at least one library to create a universal library",
);
// Determine the output path
const filenames = new Set(libraryPaths.map((p) => path.basename(p)));
assert(
filenames.size === 1,
"Expected all darwin libraries to have the same name",
`Expected libraries to have the same name, but got: ${[...filenames].join(
", ",
)}`,
);
const [filename] = filenames;
const lipoParentPath = fs.realpathSync(
Expand Down
Loading