Skip to content

Commit ac3ee34

Browse files
authored
Convert binary plist files to xml and use a proper parser when linking (#275)
* Convert binary plist files to xml and use a proper parser when linking * Add assert for platform in updateInfoPlist * Changed updateInfoPlist to assert old library name
1 parent 5016ed2 commit ac3ee34

File tree

5 files changed

+215
-55
lines changed

5 files changed

+215
-55
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"
Lines changed: 149 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,184 @@
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/,
4785
);
4886
});
4987
});
88+
89+
describe("updateInfoPlist", () => {
90+
it(
91+
"updates an xml plist",
92+
{ skip: process.platform !== "darwin" },
93+
async (context) => {
94+
const infoPlistSubPath = "Info.plist";
95+
const tempDirectoryPath = setupTempDirectory(context, {
96+
[infoPlistSubPath]: `
97+
<?xml version="1.0" encoding="UTF-8"?>
98+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
99+
<plist version="1.0">
100+
<dict>
101+
<key>CFBundleExecutable</key>
102+
<string>addon</string>
103+
</dict>
104+
</plist>
105+
`,
106+
});
107+
108+
await updateInfoPlist({
109+
frameworkPath: tempDirectoryPath,
110+
oldLibraryName: "addon",
111+
newLibraryName: "new-addon-name",
112+
});
113+
114+
const contents = await fs.promises.readFile(
115+
path.join(tempDirectoryPath, infoPlistSubPath),
116+
"utf-8",
117+
);
118+
assert.match(contents, /<\?xml version="1.0" encoding="UTF-8"\?>/);
119+
assert.match(
120+
contents,
121+
/<key>CFBundleExecutable<\/key>\s*<string>new-addon-name<\/string>/,
122+
);
123+
},
124+
);
125+
126+
it(
127+
"converts a binary plist to xml",
128+
{ skip: process.platform !== "darwin" },
129+
async (context) => {
130+
const tempDirectoryPath = setupTempDirectory(context, {});
131+
// Write a binary plist file
132+
const binaryPlistContents = Buffer.from(
133+
// Generated running "base64 -i <path-to-binary-plist>" on a plist file from a framework in the node-examples package
134+
"YnBsaXN0MDDfEBUBAgMEBQYHCAkKCwwNDg8QERITFBUWFxgZGhscHR4cICEiIyQiJSYnJChfEBNCdWlsZE1hY2hpbmVPU0J1aWxkXxAZQ0ZCdW5kbGVEZXZlbG9wbWVudFJlZ2lvbl8QEkNGQnVuZGxlRXhlY3V0YWJsZV8QEkNGQnVuZGxlSWRlbnRpZmllcl8QHUNGQnVuZGxlSW5mb0RpY3Rpb25hcnlWZXJzaW9uXxATQ0ZCdW5kbGVQYWNrYWdlVHlwZV8QGkNGQnVuZGxlU2hvcnRWZXJzaW9uU3RyaW5nXxARQ0ZCdW5kbGVTaWduYXR1cmVfEBpDRkJ1bmRsZVN1cHBvcnRlZFBsYXRmb3Jtc18QD0NGQnVuZGxlVmVyc2lvbl8QFUNTUmVzb3VyY2VzRmlsZU1hcHBlZFpEVENvbXBpbGVyXxAPRFRQbGF0Zm9ybUJ1aWxkXkRUUGxhdGZvcm1OYW1lXxARRFRQbGF0Zm9ybVZlcnNpb25aRFRTREtCdWlsZFlEVFNES05hbWVXRFRYY29kZVxEVFhjb2RlQnVpbGRfEBBNaW5pbXVtT1NWZXJzaW9uXlVJRGV2aWNlRmFtaWx5VjI0RzIzMVdFbmdsaXNoVWFkZG9uXxAPZXhhbXBsZV82LmFkZG9uUzYuMFRGTVdLUzEuMFQ/Pz8/oR9fEA9pUGhvbmVTaW11bGF0b3IJXxAiY29tLmFwcGxlLmNvbXBpbGVycy5sbHZtLmNsYW5nLjFfMFYyMkMxNDZfEA9pcGhvbmVzaW11bGF0b3JUMTguMl8QE2lwaG9uZXNpbXVsYXRvcjE4LjJUMTYyMFgxNkM1MDMyYaEpEAEACAA1AEsAZwB8AJEAsQDHAOQA+AEVAScBPwFKAVwBawF/AYoBlAGcAakBvAHLAdIB2gHgAfIB9gH7Af8CBAIGAhgCGQI+AkUCVwJcAnICdwKAAoIAAAAAAAACAQAAAAAAAAAqAAAAAAAAAAAAAAAAAAAChA==",
135+
"base64",
136+
);
137+
const binaryPlistPath = path.join(tempDirectoryPath, "Info.plist");
138+
await fs.promises.writeFile(binaryPlistPath, binaryPlistContents);
139+
140+
await updateInfoPlist({
141+
frameworkPath: tempDirectoryPath,
142+
oldLibraryName: "addon",
143+
newLibraryName: "new-addon-name",
144+
});
145+
146+
const contents = await fs.promises.readFile(binaryPlistPath, "utf-8");
147+
assert.match(contents, /<\?xml version="1.0" encoding="UTF-8"\?>/);
148+
assert.match(
149+
contents,
150+
/<key>CFBundleExecutable<\/key>\s*<string>new-addon-name<\/string>/,
151+
);
152+
},
153+
);
154+
155+
it(
156+
"throws when not on darwin",
157+
{ skip: process.platform === "darwin" },
158+
async (context) => {
159+
const tempDirectoryPath = setupTempDirectory(context, {
160+
["Info.plist"]: '<?xml version="1.0" encoding="UTF-8"?>',
161+
});
162+
163+
await assert.rejects(
164+
() =>
165+
updateInfoPlist({
166+
frameworkPath: tempDirectoryPath,
167+
oldLibraryName: "addon",
168+
newLibraryName: "new-addon-name",
169+
}),
170+
(err) => {
171+
assert(err instanceof Error);
172+
assert.match(err.message, /Failed to convert Info.plist at path/);
173+
assert(err.cause instanceof Error);
174+
assert.match(
175+
err.cause.message,
176+
/Updating Info.plist files are not supported on this platform/,
177+
);
178+
return true;
179+
},
180+
);
181+
},
182+
);
183+
});
50184
});

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

Lines changed: 33 additions & 24 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-
);
59-
}
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-
);
56+
throw new Error(`Unable to read Info.plist at path "${infoPlistPath}"`, {
57+
cause,
58+
});
6959
}
70-
71-
return { infoPlistPath, contents };
7260
}
7361

7462
type UpdateInfoPlistOptions = {
@@ -85,11 +73,32 @@ export async function updateInfoPlist({
8573
oldLibraryName,
8674
newLibraryName,
8775
}: UpdateInfoPlistOptions) {
88-
const { infoPlistPath, contents } = await readInfoPlist(frameworkPath);
76+
const infoPlistPath = determineInfoPlistPath(frameworkPath);
77+
78+
// Convert to XML format if needed
79+
try {
80+
assert(
81+
process.platform === "darwin",
82+
"Updating Info.plist files are not supported on this platform",
83+
);
84+
await spawn("plutil", ["-convert", "xml1", infoPlistPath], {
85+
outputMode: "inherit",
86+
});
87+
} catch (error) {
88+
throw new Error(
89+
`Failed to convert Info.plist at path "${infoPlistPath}" to XML format`,
90+
{ cause: error },
91+
);
92+
}
8993

90-
// TODO: Use a proper plist parser
91-
const updatedContents = contents.replaceAll(oldLibraryName, newLibraryName);
92-
await fs.promises.writeFile(infoPlistPath, updatedContents, "utf-8");
94+
const contents = await readInfoPlist(infoPlistPath);
95+
assert.equal(
96+
contents.CFBundleExecutable,
97+
oldLibraryName,
98+
"Unexpected CFBundleExecutable value in Info.plist",
99+
);
100+
contents.CFBundleExecutable = newLibraryName;
101+
await fs.promises.writeFile(infoPlistPath, plist.build(contents), "utf-8");
93102
}
94103

95104
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;
@@ -59,7 +45,7 @@ export async function createAppleFramework(libraryPath: string) {
5945
// Create an empty Info.plist file
6046
await fs.promises.writeFile(
6147
path.join(frameworkPath, "Info.plist"),
62-
createPlistContent({
48+
plist.build({
6349
CFBundleDevelopmentRegion: "en",
6450
CFBundleExecutable: libraryName,
6551
CFBundleIdentifier: `com.callstackincubator.node-api.${libraryName}`,

0 commit comments

Comments
 (0)