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
30 changes: 30 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/host/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
],
"license": "MIT",
"dependencies": {
"@expo/plist": "^0.4.7",
"@react-native-node-api/cli-utils": "0.1.0",
"pkg-dir": "^8.0.0",
"read-pkg": "^9.0.1"
Expand Down
164 changes: 149 additions & 15 deletions packages/host/src/node/cli/apple.test.ts
Original file line number Diff line number Diff line change
@@ -1,50 +1,184 @@
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import path from "node:path";
import { readInfoPlist } from "./apple";
import fs from "node:fs";

import {
determineInfoPlistPath,
readInfoPlist,
updateInfoPlist,
} from "./apple";
import { setupTempDirectory } from "../test-utils";

describe("apple", () => {
describe("Info.plist lookup", () => {
it("should find Info.plist files in unversioned frameworks", async (context) => {
describe("determineInfoPlistPath", () => {
it("should find Info.plist files in unversioned frameworks", (context) => {
const infoPlistContents = `<?xml version="1.0" encoding="UTF-8"?>...`;
const infoPlistSubPath = "Info.plist";
const tempDirectoryPath = setupTempDirectory(context, {
[infoPlistSubPath]: infoPlistContents,
});

const result = await readInfoPlist(tempDirectoryPath);

assert.strictEqual(result.contents, infoPlistContents);
assert.strictEqual(
result.infoPlistPath,
determineInfoPlistPath(tempDirectoryPath),
path.join(tempDirectoryPath, infoPlistSubPath),
);
});

it("should find Info.plist files in versioned frameworks", async (context) => {
it("should find Info.plist files in versioned frameworks", (context) => {
const infoPlistContents = `<?xml version="1.0" encoding="UTF-8"?>...`;
const infoPlistSubPath = "Versions/Current/Resources/Info.plist";
const tempDirectoryPath = setupTempDirectory(context, {
[infoPlistSubPath]: infoPlistContents,
});

const result = await readInfoPlist(tempDirectoryPath);

assert.strictEqual(result.contents, infoPlistContents);
assert.strictEqual(
result.infoPlistPath,
determineInfoPlistPath(tempDirectoryPath),
path.join(tempDirectoryPath, infoPlistSubPath),
);
});

it("should throw if Info.plist is missing from framework", async (context) => {
it("should throw if Info.plist is missing from framework", (context) => {
const tempDirectoryPath = setupTempDirectory(context, {});

assert.throws(
() => determineInfoPlistPath(tempDirectoryPath),
/Unable to locate an Info.plist file within framework./,
);
});
});

describe("readInfoPlist", () => {
it("should read Info.plist contents", async (context) => {
const infoPlistContents = `
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleExecutable</key>
<string>ExecutableFileName</string>
<key>CFBundleIconFile</key>
<string>AppIcon</string>
</dict>
</plist>
`;
const infoPlistSubPath = "Info.plist";
const tempDirectoryPath = setupTempDirectory(context, {
[infoPlistSubPath]: infoPlistContents,
});
const infoPlistPath = path.join(tempDirectoryPath, infoPlistSubPath);

const contents = await readInfoPlist(infoPlistPath);
assert.deepEqual(contents, {
CFBundleExecutable: "ExecutableFileName",
CFBundleIconFile: "AppIcon",
});
});

it("should throw if Info.plist doesn't exist", async (context) => {
const tempDirectoryPath = setupTempDirectory(context, {});
const infoPlistPath = path.join(tempDirectoryPath, "Info.plist");

await assert.rejects(
async () => readInfoPlist(tempDirectoryPath),
/Unable to read Info.plist for framework at path ".*?", as an Info.plist file couldn't be found./,
() => readInfoPlist(infoPlistPath),
/Unable to read Info.plist at path/,
);
});
});

describe("updateInfoPlist", () => {
it(
"updates an xml plist",
{ skip: process.platform !== "darwin" },
async (context) => {
const infoPlistSubPath = "Info.plist";
const tempDirectoryPath = setupTempDirectory(context, {
[infoPlistSubPath]: `
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleExecutable</key>
<string>addon</string>
</dict>
</plist>
`,
});

await updateInfoPlist({
frameworkPath: tempDirectoryPath,
oldLibraryName: "addon",
newLibraryName: "new-addon-name",
});

const contents = await fs.promises.readFile(
path.join(tempDirectoryPath, infoPlistSubPath),
"utf-8",
);
assert.match(contents, /<\?xml version="1.0" encoding="UTF-8"\?>/);
assert.match(
contents,
/<key>CFBundleExecutable<\/key>\s*<string>new-addon-name<\/string>/,
);
},
);

it(
"converts a binary plist to xml",
{ skip: process.platform !== "darwin" },
async (context) => {
const tempDirectoryPath = setupTempDirectory(context, {});
// Write a binary plist file
const binaryPlistContents = Buffer.from(
// Generated running "base64 -i <path-to-binary-plist>" on a plist file from a framework in the node-examples package
"YnBsaXN0MDDfEBUBAgMEBQYHCAkKCwwNDg8QERITFBUWFxgZGhscHR4cICEiIyQiJSYnJChfEBNCdWlsZE1hY2hpbmVPU0J1aWxkXxAZQ0ZCdW5kbGVEZXZlbG9wbWVudFJlZ2lvbl8QEkNGQnVuZGxlRXhlY3V0YWJsZV8QEkNGQnVuZGxlSWRlbnRpZmllcl8QHUNGQnVuZGxlSW5mb0RpY3Rpb25hcnlWZXJzaW9uXxATQ0ZCdW5kbGVQYWNrYWdlVHlwZV8QGkNGQnVuZGxlU2hvcnRWZXJzaW9uU3RyaW5nXxARQ0ZCdW5kbGVTaWduYXR1cmVfEBpDRkJ1bmRsZVN1cHBvcnRlZFBsYXRmb3Jtc18QD0NGQnVuZGxlVmVyc2lvbl8QFUNTUmVzb3VyY2VzRmlsZU1hcHBlZFpEVENvbXBpbGVyXxAPRFRQbGF0Zm9ybUJ1aWxkXkRUUGxhdGZvcm1OYW1lXxARRFRQbGF0Zm9ybVZlcnNpb25aRFRTREtCdWlsZFlEVFNES05hbWVXRFRYY29kZVxEVFhjb2RlQnVpbGRfEBBNaW5pbXVtT1NWZXJzaW9uXlVJRGV2aWNlRmFtaWx5VjI0RzIzMVdFbmdsaXNoVWFkZG9uXxAPZXhhbXBsZV82LmFkZG9uUzYuMFRGTVdLUzEuMFQ/Pz8/oR9fEA9pUGhvbmVTaW11bGF0b3IJXxAiY29tLmFwcGxlLmNvbXBpbGVycy5sbHZtLmNsYW5nLjFfMFYyMkMxNDZfEA9pcGhvbmVzaW11bGF0b3JUMTguMl8QE2lwaG9uZXNpbXVsYXRvcjE4LjJUMTYyMFgxNkM1MDMyYaEpEAEACAA1AEsAZwB8AJEAsQDHAOQA+AEVAScBPwFKAVwBawF/AYoBlAGcAakBvAHLAdIB2gHgAfIB9gH7Af8CBAIGAhgCGQI+AkUCVwJcAnICdwKAAoIAAAAAAAACAQAAAAAAAAAqAAAAAAAAAAAAAAAAAAAChA==",
"base64",
);
const binaryPlistPath = path.join(tempDirectoryPath, "Info.plist");
await fs.promises.writeFile(binaryPlistPath, binaryPlistContents);

await updateInfoPlist({
frameworkPath: tempDirectoryPath,
oldLibraryName: "addon",
newLibraryName: "new-addon-name",
});

const contents = await fs.promises.readFile(binaryPlistPath, "utf-8");
assert.match(contents, /<\?xml version="1.0" encoding="UTF-8"\?>/);
assert.match(
contents,
/<key>CFBundleExecutable<\/key>\s*<string>new-addon-name<\/string>/,
);
},
);

it(
"throws when not on darwin",
{ skip: process.platform === "darwin" },
async (context) => {
const tempDirectoryPath = setupTempDirectory(context, {
["Info.plist"]: '<?xml version="1.0" encoding="UTF-8"?>',
});

await assert.rejects(
() =>
updateInfoPlist({
frameworkPath: tempDirectoryPath,
oldLibraryName: "addon",
newLibraryName: "new-addon-name",
}),
(err) => {
assert(err instanceof Error);
assert.match(err.message, /Failed to convert Info.plist at path/);
assert(err.cause instanceof Error);
assert.match(
err.cause.message,
/Updating Info.plist files are not supported on this platform/,
);
return true;
},
);
},
);
});
});
57 changes: 33 additions & 24 deletions packages/host/src/node/cli/apple.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import path from "node:path";
import fs from "node:fs";
import os from "node:os";

import plist from "@expo/plist";
import { spawn } from "@react-native-node-api/cli-utils";

import { getLatestMtime, getLibraryName } from "../path-utils.js";
Expand All @@ -12,7 +13,7 @@ import {
LinkModuleResult,
} from "./link-modules.js";

function determineInfoPlistPath(frameworkPath: string) {
export function determineInfoPlistPath(frameworkPath: string) {
const checkedPaths = new Array<string>();

// First, assume it is an "unversioned" framework that keeps its Info.plist in
Expand Down Expand Up @@ -47,28 +48,15 @@ function determineInfoPlistPath(frameworkPath: string) {
/**
* Resolves the Info.plist file within a framework and reads its contents.
*/
export async function readInfoPlist(frameworkPath: string) {
let infoPlistPath: string;
export async function readInfoPlist(infoPlistPath: string) {
try {
infoPlistPath = determineInfoPlistPath(frameworkPath);
const contents = await fs.promises.readFile(infoPlistPath, "utf-8");
return plist.parse(contents) as Record<string, unknown>;
} catch (cause) {
throw new Error(
`Unable to read Info.plist for framework at path "${frameworkPath}", as an Info.plist file couldn't be found.`,
{ cause },
);
}

let contents: string;
try {
contents = await fs.promises.readFile(infoPlistPath, "utf-8");
} catch (cause) {
throw new Error(
`Unable to read Info.plist for framework at path "${frameworkPath}", due to a file system error.`,
{ cause },
);
throw new Error(`Unable to read Info.plist at path "${infoPlistPath}"`, {
cause,
});
}

return { infoPlistPath, contents };
}

type UpdateInfoPlistOptions = {
Expand All @@ -85,11 +73,32 @@ export async function updateInfoPlist({
oldLibraryName,
newLibraryName,
}: UpdateInfoPlistOptions) {
const { infoPlistPath, contents } = await readInfoPlist(frameworkPath);
const infoPlistPath = determineInfoPlistPath(frameworkPath);

// Convert to XML format if needed
try {
assert(
process.platform === "darwin",
"Updating Info.plist files are not supported on this platform",
);
await spawn("plutil", ["-convert", "xml1", infoPlistPath], {
outputMode: "inherit",
});
} catch (error) {
throw new Error(
`Failed to convert Info.plist at path "${infoPlistPath}" to XML format`,
{ cause: error },
);
}

// TODO: Use a proper plist parser
const updatedContents = contents.replaceAll(oldLibraryName, newLibraryName);
await fs.promises.writeFile(infoPlistPath, updatedContents, "utf-8");
const contents = await readInfoPlist(infoPlistPath);
assert.equal(
contents.CFBundleExecutable,
oldLibraryName,
"Unexpected CFBundleExecutable value in Info.plist",
);
contents.CFBundleExecutable = newLibraryName;
await fs.promises.writeFile(infoPlistPath, plist.build(contents), "utf-8");
Copy link

Choose a reason for hiding this comment

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

Bug: Info.plist Update Incomplete

The updateInfoPlist function now only updates CFBundleExecutable. Unlike the previous global string replacement, this misses other fields like CFBundleIdentifier and CFBundleName, leading to inconsistencies in the Info.plist file and potential framework linking issues.

Fix in Cursor Fix in Web

}

export async function linkXcframework({
Expand Down
18 changes: 2 additions & 16 deletions packages/host/src/node/prebuilds/apple.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import fs from "node:fs";
import path from "node:path";
import os from "node:os";

import plist from "@expo/plist";
import { spawn } from "@react-native-node-api/cli-utils";

import { AppleTriplet } from "./triplets.js";
Expand All @@ -23,21 +24,6 @@ export const APPLE_ARCHITECTURES = {
"arm64-apple-visionos-sim": "arm64",
} satisfies Record<AppleTriplet, AppleArchitecture>;

export function createPlistContent(values: Record<string, string>) {
return [
'<?xml version="1.0" encoding="UTF-8"?>',
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
'<plist version="1.0">',
" <dict>",
...Object.entries(values).flatMap(([key, value]) => [
` <key>${key}</key>`,
` <string>${value}</string>`,
]),
" </dict>",
"</plist>",
].join("\n");
}

type XCframeworkOptions = {
frameworkPaths: string[];
outputPath: string;
Expand All @@ -59,7 +45,7 @@ export async function createAppleFramework(libraryPath: string) {
// Create an empty Info.plist file
await fs.promises.writeFile(
path.join(frameworkPath, "Info.plist"),
createPlistContent({
plist.build({
CFBundleDevelopmentRegion: "en",
CFBundleExecutable: libraryName,
CFBundleIdentifier: `com.callstackincubator.node-api.${libraryName}`,
Expand Down
Loading