From 6e488bfecee5aef8085a2056d365fcfba6e4d681 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Tue, 4 Nov 2025 18:51:37 +0100 Subject: [PATCH 1/2] Escapes library names to match a CFBundleIdentifier --- .changeset/long-regions-yawn.md | 5 +++++ packages/host/src/node/prebuilds/apple.test.ts | 17 +++++++++++++++++ packages/host/src/node/prebuilds/apple.ts | 10 +++++++++- 3 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 .changeset/long-regions-yawn.md create mode 100644 packages/host/src/node/prebuilds/apple.test.ts diff --git a/.changeset/long-regions-yawn.md b/.changeset/long-regions-yawn.md new file mode 100644 index 00000000..b0d515e7 --- /dev/null +++ b/.changeset/long-regions-yawn.md @@ -0,0 +1,5 @@ +--- +"react-native-node-api": minor +--- + +Ensure proper escaping when generating a bundle identifier while creating an Apple framework diff --git a/packages/host/src/node/prebuilds/apple.test.ts b/packages/host/src/node/prebuilds/apple.test.ts new file mode 100644 index 00000000..4139831e --- /dev/null +++ b/packages/host/src/node/prebuilds/apple.test.ts @@ -0,0 +1,17 @@ +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; + +import { escapeBundleIdentifier } from "./apple"; + +describe("escapeBundleIdentifier", () => { + it("escapes and passes through values as expected", () => { + assert.equal( + escapeBundleIdentifier("abc-def-123-789.-"), + "abc-def-123-789.-", + ); + assert.equal(escapeBundleIdentifier("abc_def"), "abc-def"); + assert.equal(escapeBundleIdentifier("abc\ndef"), "abc-def"); + assert.equal(escapeBundleIdentifier("\0abc"), "-abc"); + assert.equal(escapeBundleIdentifier("🤷"), "--"); // An emoji takes up two chars + }); +}); diff --git a/packages/host/src/node/prebuilds/apple.ts b/packages/host/src/node/prebuilds/apple.ts index 1aeb9658..0b8e6503 100644 --- a/packages/host/src/node/prebuilds/apple.ts +++ b/packages/host/src/node/prebuilds/apple.ts @@ -14,6 +14,14 @@ type XCframeworkOptions = { autoLink: boolean; }; +/** + * Escapes any input to match a CFBundleIdentifier + * See https://developer.apple.com/documentation/bundleresources/information-property-list/cfbundleidentifier + */ +export function escapeBundleIdentifier(input: string) { + return input.replace(/[^A-Za-z0-9-.]/g, "-"); +} + export async function createAppleFramework( libraryPath: string, versioned = false, @@ -39,7 +47,7 @@ export async function createAppleFramework( plist.build({ CFBundleDevelopmentRegion: "en", CFBundleExecutable: libraryName, - CFBundleIdentifier: `com.callstackincubator.node-api.${libraryName}`, + CFBundleIdentifier: `com.callstackincubator.node-api.${escapeBundleIdentifier(libraryName)}`, CFBundleInfoDictionaryVersion: "6.0", CFBundleName: libraryName, CFBundlePackageType: "FMWK", From 8b1601fa01e1919eb1032651d9e2527c538ad610 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Tue, 4 Nov 2025 18:52:58 +0100 Subject: [PATCH 2/2] Allow passing --apple-bundle-identifier --- .changeset/gold-beans-jump.md | 7 +++++++ packages/cmake-rn/src/platforms/apple.ts | 24 +++++++++++++++++------ packages/ferric/src/build.ts | 12 +++++++++++- packages/host/src/node/prebuilds/apple.ts | 17 ++++++++++++---- 4 files changed, 49 insertions(+), 11 deletions(-) create mode 100644 .changeset/gold-beans-jump.md diff --git a/.changeset/gold-beans-jump.md b/.changeset/gold-beans-jump.md new file mode 100644 index 00000000..ecc9f902 --- /dev/null +++ b/.changeset/gold-beans-jump.md @@ -0,0 +1,7 @@ +--- +"cmake-rn": patch +"ferric-cli": patch +"react-native-node-api": patch +--- + +Allow passing --apple-bundle-identifier to specify the bundle identifiers used when creating Apple frameworks. diff --git a/packages/cmake-rn/src/platforms/apple.ts b/packages/cmake-rn/src/platforms/apple.ts index 8f3de38a..c61c1e81 100644 --- a/packages/cmake-rn/src/platforms/apple.ts +++ b/packages/cmake-rn/src/platforms/apple.ts @@ -155,8 +155,14 @@ const xcframeworkExtensionOption = new Option( "Don't rename the xcframework to .apple.node", ).default(false); +const appleBundleIdentifierOption = new Option( + "--apple-bundle-identifier ", + "Unique CFBundleIdentifier used for Apple framework artifacts", +).default(undefined, "com.callstackincubator.node-api.{libraryName}"); + type AppleOpts = { xcframeworkExtension: boolean; + appleBundleIdentifier?: string; }; function getBuildPath(baseBuildPath: string, triplet: Triplet) { @@ -233,7 +239,9 @@ export const platform: Platform = { } }, amendCommand(command) { - return command.addOption(xcframeworkExtensionOption); + return command + .addOption(xcframeworkExtensionOption) + .addOption(appleBundleIdentifierOption); }, async configure( triplets, @@ -284,7 +292,10 @@ export const platform: Platform = { }), ); }, - async build({ spawn, triplet }, { build, target, configuration }) { + async build( + { spawn, triplet }, + { build, target, configuration, appleBundleIdentifier }, + ) { // We expect the final application to sign these binaries if (target.length > 1) { throw new Error("Building for multiple targets is not supported yet"); @@ -368,10 +379,11 @@ export const platform: Platform = { "Expected exactly one artifact", ); const [artifact] = artifacts; - await createAppleFramework( - path.join(buildPath, artifact.path), - triplet.endsWith("-darwin"), - ); + await createAppleFramework({ + libraryPath: path.join(buildPath, artifact.path), + versioned: triplet.endsWith("-darwin"), + bundleIdentifier: appleBundleIdentifier, + }); } }, isSupportedByHost: function (): boolean | Promise { diff --git a/packages/ferric/src/build.ts b/packages/ferric/src/build.ts index 6587347e..57f9ec59 100644 --- a/packages/ferric/src/build.ts +++ b/packages/ferric/src/build.ts @@ -107,6 +107,11 @@ const configurationOption = new Option( .choices(["debug", "release"]) .default("debug"); +const appleBundleIdentifierOption = new Option( + "--apple-bundle-identifier ", + "Unique CFBundleIdentifier used for Apple framework artifacts", +).default(undefined, "com.callstackincubator.node-api.{libraryName}"); + export const buildCommand = new Command("build") .description("Build Rust Node-API module") .addOption(targetOption) @@ -116,6 +121,7 @@ export const buildCommand = new Command("build") .addOption(outputPathOption) .addOption(configurationOption) .addOption(xcframeworkExtensionOption) + .addOption(appleBundleIdentifierOption) .action( wrapAction( async ({ @@ -126,6 +132,7 @@ export const buildCommand = new Command("build") output: outputPath, configuration, xcframeworkExtension, + appleBundleIdentifier, }) => { const targets = new Set([...targetArg]); if (apple) { @@ -239,7 +246,10 @@ export const buildCommand = new Command("build") const frameworkPaths = await Promise.all( libraryPaths.map((libraryPath) => // TODO: Pass true as `versioned` argument for -darwin targets - createAppleFramework(libraryPath), + createAppleFramework({ + libraryPath, + bundleIdentifier: appleBundleIdentifier, + }), ), ); const xcframeworkFilename = determineXCFrameworkFilename( diff --git a/packages/host/src/node/prebuilds/apple.ts b/packages/host/src/node/prebuilds/apple.ts index 0b8e6503..23f0848b 100644 --- a/packages/host/src/node/prebuilds/apple.ts +++ b/packages/host/src/node/prebuilds/apple.ts @@ -22,10 +22,17 @@ export function escapeBundleIdentifier(input: string) { return input.replace(/[^A-Za-z0-9-.]/g, "-"); } -export async function createAppleFramework( - libraryPath: string, +type CreateAppleFrameworkOptions = { + libraryPath: string; + versioned?: boolean; + bundleIdentifier?: string; +}; + +export async function createAppleFramework({ + libraryPath, versioned = false, -) { + bundleIdentifier, +}: CreateAppleFrameworkOptions) { if (versioned) { // TODO: Add support for generating a Versions/Current/Resources/Info.plist convention framework throw new Error("Creating versioned frameworks is not supported yet"); @@ -47,7 +54,9 @@ export async function createAppleFramework( plist.build({ CFBundleDevelopmentRegion: "en", CFBundleExecutable: libraryName, - CFBundleIdentifier: `com.callstackincubator.node-api.${escapeBundleIdentifier(libraryName)}`, + CFBundleIdentifier: escapeBundleIdentifier( + bundleIdentifier ?? `com.callstackincubator.node-api.${libraryName}`, + ), CFBundleInfoDictionaryVersion: "6.0", CFBundleName: libraryName, CFBundlePackageType: "FMWK",