Skip to content

Commit b49d0eb

Browse files
committed
Convert binary plist files to xml and use a proper parser when linking
1 parent 61b3825 commit b49d0eb

File tree

5 files changed

+131
-56
lines changed

5 files changed

+131
-56
lines changed

package-lock.json

Lines changed: 30 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/host/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@
8080
],
8181
"license": "MIT",
8282
"dependencies": {
83+
"@expo/plist": "^0.4.7",
8384
"@react-native-node-api/cli-utils": "0.1.0",
8485
"pkg-dir": "^8.0.0",
8586
"read-pkg": "^9.0.1"

packages/host/src/node/cli/apple.test.ts

Lines changed: 79 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,113 @@
11
import assert from "node:assert/strict";
22
import { describe, it } from "node:test";
33
import path from "node:path";
4-
import { readInfoPlist } from "./apple";
4+
import fs from "node:fs";
5+
6+
import {
7+
determineInfoPlistPath,
8+
readInfoPlist,
9+
updateInfoPlist,
10+
} from "./apple";
511
import { setupTempDirectory } from "../test-utils";
612

713
describe("apple", () => {
8-
describe("Info.plist lookup", () => {
9-
it("should find Info.plist files in unversioned frameworks", async (context) => {
14+
describe("determineInfoPlistPath", () => {
15+
it("should find Info.plist files in unversioned frameworks", (context) => {
1016
const infoPlistContents = `<?xml version="1.0" encoding="UTF-8"?>...`;
1117
const infoPlistSubPath = "Info.plist";
1218
const tempDirectoryPath = setupTempDirectory(context, {
1319
[infoPlistSubPath]: infoPlistContents,
1420
});
1521

16-
const result = await readInfoPlist(tempDirectoryPath);
17-
18-
assert.strictEqual(result.contents, infoPlistContents);
1922
assert.strictEqual(
20-
result.infoPlistPath,
23+
determineInfoPlistPath(tempDirectoryPath),
2124
path.join(tempDirectoryPath, infoPlistSubPath),
2225
);
2326
});
2427

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

32-
const result = await readInfoPlist(tempDirectoryPath);
33-
34-
assert.strictEqual(result.contents, infoPlistContents);
3535
assert.strictEqual(
36-
result.infoPlistPath,
36+
determineInfoPlistPath(tempDirectoryPath),
3737
path.join(tempDirectoryPath, infoPlistSubPath),
3838
);
3939
});
4040

41-
it("should throw if Info.plist is missing from framework", async (context) => {
41+
it("should throw if Info.plist is missing from framework", (context) => {
42+
const tempDirectoryPath = setupTempDirectory(context, {});
43+
44+
assert.throws(
45+
() => determineInfoPlistPath(tempDirectoryPath),
46+
/Unable to locate an Info.plist file within framework./,
47+
);
48+
});
49+
});
50+
51+
describe("readInfoPlist", () => {
52+
it("should read Info.plist contents", async (context) => {
53+
const infoPlistContents = `
54+
<?xml version="1.0" encoding="UTF-8"?>
55+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
56+
<plist version="1.0">
57+
<dict>
58+
<key>CFBundleExecutable</key>
59+
<string>ExecutableFileName</string>
60+
<key>CFBundleIconFile</key>
61+
<string>AppIcon</string>
62+
</dict>
63+
</plist>
64+
`;
65+
const infoPlistSubPath = "Info.plist";
66+
const tempDirectoryPath = setupTempDirectory(context, {
67+
[infoPlistSubPath]: infoPlistContents,
68+
});
69+
const infoPlistPath = path.join(tempDirectoryPath, infoPlistSubPath);
70+
71+
const contents = await readInfoPlist(infoPlistPath);
72+
assert.deepEqual(contents, {
73+
CFBundleExecutable: "ExecutableFileName",
74+
CFBundleIconFile: "AppIcon",
75+
});
76+
});
77+
78+
it("should throw if Info.plist doesn't exist", async (context) => {
4279
const tempDirectoryPath = setupTempDirectory(context, {});
80+
const infoPlistPath = path.join(tempDirectoryPath, "Info.plist");
4381

4482
await assert.rejects(
45-
async () => readInfoPlist(tempDirectoryPath),
46-
/Unable to read Info.plist for framework at path ".*?", as an Info.plist file couldn't be found./,
83+
() => readInfoPlist(infoPlistPath),
84+
/Unable to read Info.plist at path/,
85+
);
86+
});
87+
});
88+
89+
describe("updateInfoPlist", () => {
90+
it("converts a binary plist to xml", async (context) => {
91+
const tempDirectoryPath = setupTempDirectory(context, {});
92+
// Write a binary plist file
93+
const binaryPlistContents = Buffer.from(
94+
"YnBsaXN0MDDfEBUBAgMEBQYHCAkKCwwNDg8QERITFBUWFxgZGhscHR4cICEiIyQiJSYnJChfEBNCdWlsZE1hY2hpbmVPU0J1aWxkXxAZQ0ZCdW5kbGVEZXZlbG9wbWVudFJlZ2lvbl8QEkNGQnVuZGxlRXhlY3V0YWJsZV8QEkNGQnVuZGxlSWRlbnRpZmllcl8QHUNGQnVuZGxlSW5mb0RpY3Rpb25hcnlWZXJzaW9uXxATQ0ZCdW5kbGVQYWNrYWdlVHlwZV8QGkNGQnVuZGxlU2hvcnRWZXJzaW9uU3RyaW5nXxARQ0ZCdW5kbGVTaWduYXR1cmVfEBpDRkJ1bmRsZVN1cHBvcnRlZFBsYXRmb3Jtc18QD0NGQnVuZGxlVmVyc2lvbl8QFUNTUmVzb3VyY2VzRmlsZU1hcHBlZFpEVENvbXBpbGVyXxAPRFRQbGF0Zm9ybUJ1aWxkXkRUUGxhdGZvcm1OYW1lXxARRFRQbGF0Zm9ybVZlcnNpb25aRFRTREtCdWlsZFlEVFNES05hbWVXRFRYY29kZVxEVFhjb2RlQnVpbGRfEBBNaW5pbXVtT1NWZXJzaW9uXlVJRGV2aWNlRmFtaWx5VjI0RzIzMVdFbmdsaXNoVWFkZG9uXxAPZXhhbXBsZV82LmFkZG9uUzYuMFRGTVdLUzEuMFQ/Pz8/oR9fEA9pUGhvbmVTaW11bGF0b3IJXxAiY29tLmFwcGxlLmNvbXBpbGVycy5sbHZtLmNsYW5nLjFfMFYyMkMxNDZfEA9pcGhvbmVzaW11bGF0b3JUMTguMl8QE2lwaG9uZXNpbXVsYXRvcjE4LjJUMTYyMFgxNkM1MDMyYaEpEAEACAA1AEsAZwB8AJEAsQDHAOQA+AEVAScBPwFKAVwBawF/AYoBlAGcAakBvAHLAdIB2gHgAfIB9gH7Af8CBAIGAhgCGQI+AkUCVwJcAnICdwKAAoIAAAAAAAACAQAAAAAAAAAqAAAAAAAAAAAAAAAAAAAChA==",
95+
"base64",
96+
);
97+
const binaryPlistPath = path.join(tempDirectoryPath, "Info.plist");
98+
await fs.promises.writeFile(binaryPlistPath, binaryPlistContents);
99+
100+
await updateInfoPlist({
101+
frameworkPath: tempDirectoryPath,
102+
oldLibraryName: "addon",
103+
newLibraryName: "new-addon-name",
104+
});
105+
106+
const contents = await fs.promises.readFile(binaryPlistPath, "utf-8");
107+
assert.match(contents, /<\?xml version="1.0" encoding="UTF-8"\?>/);
108+
assert.match(
109+
contents,
110+
/<key>CFBundleExecutable<\/key>\s*<string>new-addon-name<\/string>/,
47111
);
48112
});
49113
});

packages/host/src/node/cli/apple.ts

Lines changed: 19 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import path from "node:path";
33
import fs from "node:fs";
44
import os from "node:os";
55

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

89
import { getLatestMtime, getLibraryName } from "../path-utils.js";
@@ -12,7 +13,7 @@ import {
1213
LinkModuleResult,
1314
} from "./link-modules.js";
1415

15-
function determineInfoPlistPath(frameworkPath: string) {
16+
export function determineInfoPlistPath(frameworkPath: string) {
1617
const checkedPaths = new Array<string>();
1718

1819
// First, assume it is an "unversioned" framework that keeps its Info.plist in
@@ -47,28 +48,15 @@ function determineInfoPlistPath(frameworkPath: string) {
4748
/**
4849
* Resolves the Info.plist file within a framework and reads its contents.
4950
*/
50-
export async function readInfoPlist(frameworkPath: string) {
51-
let infoPlistPath: string;
51+
export async function readInfoPlist(infoPlistPath: string) {
5252
try {
53-
infoPlistPath = determineInfoPlistPath(frameworkPath);
53+
const contents = await fs.promises.readFile(infoPlistPath, "utf-8");
54+
return plist.parse(contents) as Record<string, unknown>;
5455
} catch (cause) {
55-
throw new Error(
56-
`Unable to read Info.plist for framework at path "${frameworkPath}", as an Info.plist file couldn't be found.`,
57-
{ cause },
58-
);
56+
throw new Error(`Unable to read Info.plist at path "${infoPlistPath}"`, {
57+
cause,
58+
});
5959
}
60-
61-
let contents: string;
62-
try {
63-
contents = await fs.promises.readFile(infoPlistPath, "utf-8");
64-
} catch (cause) {
65-
throw new Error(
66-
`Unable to read Info.plist for framework at path "${frameworkPath}", due to a file system error.`,
67-
{ cause },
68-
);
69-
}
70-
71-
return { infoPlistPath, contents };
7260
}
7361

7462
type UpdateInfoPlistOptions = {
@@ -85,11 +73,17 @@ export async function updateInfoPlist({
8573
oldLibraryName,
8674
newLibraryName,
8775
}: UpdateInfoPlistOptions) {
88-
const { infoPlistPath, contents } = await readInfoPlist(frameworkPath);
89-
90-
// TODO: Use a proper plist parser
91-
const updatedContents = contents.replaceAll(oldLibraryName, newLibraryName);
92-
await fs.promises.writeFile(infoPlistPath, updatedContents, "utf-8");
76+
const infoPlistPath = determineInfoPlistPath(frameworkPath);
77+
78+
// Convert to XML format if needed
79+
await spawn("plutil", ["-convert", "xml1", infoPlistPath], {
80+
outputMode: "inherit",
81+
});
82+
const contents = await readInfoPlist(infoPlistPath);
83+
if (contents.CFBundleExecutable === oldLibraryName) {
84+
contents.CFBundleExecutable = newLibraryName;
85+
}
86+
await fs.promises.writeFile(infoPlistPath, plist.build(contents), "utf-8");
9387
}
9488

9589
export async function linkXcframework({

packages/host/src/node/prebuilds/apple.ts

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import fs from "node:fs";
33
import path from "node:path";
44
import os from "node:os";
55

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

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

26-
export function createPlistContent(values: Record<string, string>) {
27-
return [
28-
'<?xml version="1.0" encoding="UTF-8"?>',
29-
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
30-
'<plist version="1.0">',
31-
" <dict>",
32-
...Object.entries(values).flatMap(([key, value]) => [
33-
` <key>${key}</key>`,
34-
` <string>${value}</string>`,
35-
]),
36-
" </dict>",
37-
"</plist>",
38-
].join("\n");
39-
}
40-
4127
type XCframeworkOptions = {
4228
frameworkPaths: string[];
4329
outputPath: string;
@@ -66,7 +52,7 @@ export async function createAppleFramework(
6652
// Create an empty Info.plist file
6753
await fs.promises.writeFile(
6854
path.join(frameworkPath, "Info.plist"),
69-
createPlistContent({
55+
plist.build({
7056
CFBundleDevelopmentRegion: "en",
7157
CFBundleExecutable: libraryName,
7258
CFBundleIdentifier: `com.callstackincubator.node-api.${libraryName}`,

0 commit comments

Comments
 (0)