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/.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/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.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..23f0848b 100644 --- a/packages/host/src/node/prebuilds/apple.ts +++ b/packages/host/src/node/prebuilds/apple.ts @@ -14,10 +14,25 @@ type XCframeworkOptions = { autoLink: boolean; }; -export async function createAppleFramework( - libraryPath: string, +/** + * 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, "-"); +} + +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"); @@ -39,7 +54,9 @@ export async function createAppleFramework( plist.build({ CFBundleDevelopmentRegion: "en", CFBundleExecutable: libraryName, - CFBundleIdentifier: `com.callstackincubator.node-api.${libraryName}`, + CFBundleIdentifier: escapeBundleIdentifier( + bundleIdentifier ?? `com.callstackincubator.node-api.${libraryName}`, + ), CFBundleInfoDictionaryVersion: "6.0", CFBundleName: libraryName, CFBundlePackageType: "FMWK",