Skip to content

Commit 696a2cc

Browse files
committed
Update existing xcframework instead of re-creating it when linking
1 parent 5156d35 commit 696a2cc

File tree

3 files changed

+167
-140
lines changed

3 files changed

+167
-140
lines changed

package-lock.json

Lines changed: 6 additions & 25 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: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,14 +83,14 @@
8383
"@expo/plist": "^0.4.7",
8484
"@react-native-node-api/cli-utils": "0.1.0",
8585
"pkg-dir": "^8.0.0",
86-
"read-pkg": "^9.0.1"
86+
"read-pkg": "^9.0.1",
87+
"zod": "^4.1.11"
8788
},
8889
"devDependencies": {
8990
"@babel/core": "^7.26.10",
9091
"@babel/types": "^7.27.0",
9192
"fswin": "^3.24.829",
92-
"node-api-headers": "^1.5.0",
93-
"zod": "^3.24.3"
93+
"node-api-headers": "^1.5.0"
9494
},
9595
"peerDependencies": {
9696
"@babel/core": "^7.26.10",
Lines changed: 158 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import assert from "node:assert/strict";
22
import path from "node:path";
33
import fs from "node:fs";
4-
import os from "node:os";
54

65
import plist from "@expo/plist";
6+
import * as zod from "zod";
7+
78
import { spawn } from "@react-native-node-api/cli-utils";
89

910
import { getLatestMtime, getLibraryName } from "../path-utils.js";
@@ -13,6 +14,86 @@ import {
1314
LinkModuleResult,
1415
} from "./link-modules.js";
1516

17+
/**
18+
* Reads and parses a plist file, converting it to XML format if needed.
19+
*/
20+
async function readAndParsePlist(plistPath: string): Promise<unknown> {
21+
try {
22+
// Convert to XML format if needed
23+
assert(
24+
process.platform === "darwin",
25+
"Updating Info.plist files are not supported on this platform",
26+
);
27+
// Try reading the file to see if it is already in XML format
28+
const contents = await fs.promises.readFile(plistPath, "utf-8");
29+
if (contents.startsWith("<?xml")) {
30+
return plist.parse(contents) as unknown;
31+
} else {
32+
await spawn("plutil", ["-convert", "xml1", plistPath], {
33+
outputMode: "inherit",
34+
});
35+
// Read it again
36+
return plist.parse(
37+
await fs.promises.readFile(plistPath, "utf-8"),
38+
) as unknown;
39+
}
40+
} catch (error) {
41+
throw new Error(
42+
`Failed to convert plist at path "${plistPath}" to XML format`,
43+
{ cause: error },
44+
);
45+
}
46+
}
47+
48+
// Using a looseObject to allow additional fields that we don't know about
49+
const XcframeworkInfoSchema = zod.looseObject({
50+
AvailableLibraries: zod.array(
51+
zod.object({
52+
BinaryPath: zod.string(),
53+
LibraryIdentifier: zod.string(),
54+
LibraryPath: zod.string(),
55+
}),
56+
),
57+
CFBundlePackageType: zod.literal("XFWK"),
58+
XCFrameworkFormatVersion: zod.literal("1.0"),
59+
});
60+
61+
export async function readXcframeworkInfo(xcframeworkPath: string) {
62+
const infoPlistPath = path.join(xcframeworkPath, "Info.plist");
63+
const infoPlist = await readAndParsePlist(infoPlistPath);
64+
return XcframeworkInfoSchema.parse(infoPlist);
65+
}
66+
67+
export async function writeXcframeworkInfo(
68+
xcframeworkPath: string,
69+
info: zod.infer<typeof XcframeworkInfoSchema>,
70+
) {
71+
const infoPlistPath = path.join(xcframeworkPath, "Info.plist");
72+
const infoPlistXml = plist.build(info);
73+
await fs.promises.writeFile(infoPlistPath, infoPlistXml, "utf-8");
74+
}
75+
76+
const FrameworkInfoSchema = zod.looseObject({
77+
CFBundlePackageType: zod.literal("FMWK"),
78+
CFBundleInfoDictionaryVersion: zod.literal("6.0"),
79+
CFBundleExecutable: zod.string(),
80+
});
81+
82+
export async function readFrameworkInfo(frameworkPath: string) {
83+
const infoPlistPath = path.join(frameworkPath, "Info.plist");
84+
const infoPlist = await readAndParsePlist(infoPlistPath);
85+
return FrameworkInfoSchema.parse(infoPlist);
86+
}
87+
88+
export async function writeFrameworkInfo(
89+
frameworkPath: string,
90+
info: zod.infer<typeof FrameworkInfoSchema>,
91+
) {
92+
const infoPlistPath = path.join(frameworkPath, "Info.plist");
93+
const infoPlistXml = plist.build(info);
94+
await fs.promises.writeFile(infoPlistPath, infoPlistXml, "utf-8");
95+
}
96+
1697
export function determineInfoPlistPath(frameworkPath: string) {
1798
const checkedPaths = new Array<string>();
1899

@@ -109,121 +190,86 @@ export async function linkXcframework({
109190
}: LinkModuleOptions): Promise<LinkModuleResult> {
110191
// Copy the xcframework to the output directory and rename the framework and binary
111192
const newLibraryName = getLibraryName(modulePath, naming);
193+
const newFrameworkRelativePath = `${newLibraryName}.framework`;
194+
const newBinaryRelativePath = `${newFrameworkRelativePath}/${newLibraryName}`;
112195
const outputPath = getLinkedModuleOutputPath(platform, modulePath, naming);
113-
const tempPath = await fs.promises.mkdtemp(
114-
path.join(os.tmpdir(), `react-native-node-api-${newLibraryName}-`),
115-
);
116-
try {
117-
if (incremental && fs.existsSync(outputPath)) {
118-
const moduleModified = getLatestMtime(modulePath);
119-
const outputModified = getLatestMtime(outputPath);
120-
if (moduleModified < outputModified) {
121-
return {
122-
originalPath: modulePath,
123-
libraryName: newLibraryName,
124-
outputPath,
125-
skipped: true,
126-
};
127-
}
128-
}
129-
// Delete any existing xcframework (or xcodebuild will try to amend it)
130-
await fs.promises.rm(outputPath, { recursive: true, force: true });
131-
await fs.promises.cp(modulePath, tempPath, { recursive: true });
132-
133-
// Following extracted function mimics `glob("*/*.framework/")`
134-
function globFrameworkDirs<T>(
135-
startPath: string,
136-
fn: (parentPath: string, name: string) => Promise<T>,
137-
) {
138-
return fs
139-
.readdirSync(startPath, { withFileTypes: true })
140-
.filter((tripletEntry) => tripletEntry.isDirectory())
141-
.flatMap((tripletEntry) => {
142-
const tripletPath = path.join(startPath, tripletEntry.name);
143-
return fs
144-
.readdirSync(tripletPath, { withFileTypes: true })
145-
.filter(
146-
(frameworkEntry) =>
147-
frameworkEntry.isDirectory() &&
148-
path.extname(frameworkEntry.name) === ".framework",
149-
)
150-
.flatMap(
151-
async (frameworkEntry) =>
152-
await fn(tripletPath, frameworkEntry.name),
153-
);
154-
});
196+
197+
if (incremental && fs.existsSync(outputPath)) {
198+
const moduleModified = getLatestMtime(modulePath);
199+
const outputModified = getLatestMtime(outputPath);
200+
if (moduleModified < outputModified) {
201+
return {
202+
originalPath: modulePath,
203+
libraryName: newLibraryName,
204+
outputPath,
205+
skipped: true,
206+
};
155207
}
208+
}
209+
// Delete any existing xcframework (or xcodebuild will try to amend it)
210+
await fs.promises.rm(outputPath, { recursive: true, force: true });
211+
// Copy the existing xcframework to the output path
212+
await fs.promises.cp(modulePath, outputPath, { recursive: true });
156213

157-
const frameworkPaths = await Promise.all(
158-
globFrameworkDirs(tempPath, async (tripletPath, frameworkEntryName) => {
159-
const frameworkPath = path.join(tripletPath, frameworkEntryName);
160-
const oldLibraryName = path.basename(frameworkEntryName, ".framework");
161-
const oldLibraryPath = path.join(frameworkPath, oldLibraryName);
162-
const newFrameworkPath = path.join(
163-
tripletPath,
164-
`${newLibraryName}.framework`,
165-
);
166-
const newLibraryPath = path.join(newFrameworkPath, newLibraryName);
167-
assert(
168-
fs.existsSync(oldLibraryPath),
169-
`Expected a library at '${oldLibraryPath}'`,
170-
);
171-
// Rename the library
172-
await fs.promises.rename(
173-
oldLibraryPath,
174-
// Cannot use newLibraryPath here, because the framework isn't renamed yet
175-
path.join(frameworkPath, newLibraryName),
176-
);
177-
// Rename the framework
178-
await fs.promises.rename(frameworkPath, newFrameworkPath);
179-
// Expect the library in the new location
180-
assert(fs.existsSync(newLibraryPath));
181-
// Update the binary
182-
await spawn(
183-
"install_name_tool",
184-
[
185-
"-id",
186-
`@rpath/${newLibraryName}.framework/${newLibraryName}`,
187-
newLibraryPath,
188-
],
189-
{
190-
outputMode: "buffered",
191-
},
192-
);
193-
// Update the Info.plist file for the framework
194-
await updateInfoPlist({
195-
frameworkPath: newFrameworkPath,
196-
oldLibraryName,
197-
newLibraryName,
198-
});
199-
return newFrameworkPath;
200-
}),
201-
);
214+
const info = await readXcframeworkInfo(outputPath);
202215

203-
// Create a new xcframework from the renamed frameworks
204-
await spawn(
205-
"xcodebuild",
206-
[
207-
"-create-xcframework",
208-
...frameworkPaths.flatMap((frameworkPath) => [
209-
"-framework",
210-
frameworkPath,
211-
]),
212-
"-output",
216+
await Promise.all(
217+
info.AvailableLibraries.map(async (framework) => {
218+
const frameworkPath = path.join(
213219
outputPath,
214-
],
215-
{
216-
outputMode: "buffered",
217-
},
218-
);
220+
framework.LibraryIdentifier,
221+
framework.LibraryPath,
222+
);
223+
assert(
224+
fs.existsSync(frameworkPath),
225+
`Expected framework at '${frameworkPath}'`,
226+
);
227+
const frameworkInfo = await readFrameworkInfo(frameworkPath);
228+
// Update install name
229+
await spawn(
230+
"install_name_tool",
231+
[
232+
"-id",
233+
`@rpath/${newBinaryRelativePath}`,
234+
frameworkInfo.CFBundleExecutable,
235+
],
236+
{
237+
outputMode: "buffered",
238+
cwd: frameworkPath,
239+
},
240+
);
241+
await writeFrameworkInfo(frameworkPath, {
242+
...frameworkInfo,
243+
CFBundleExecutable: newLibraryName,
244+
});
245+
// Rename the actual binary
246+
await fs.promises.rename(
247+
path.join(frameworkPath, frameworkInfo.CFBundleExecutable),
248+
path.join(frameworkPath, newLibraryName),
249+
);
250+
// Rename the framework directory
251+
await fs.promises.rename(
252+
frameworkPath,
253+
path.join(path.dirname(frameworkPath), newFrameworkRelativePath),
254+
);
255+
}),
256+
);
219257

220-
return {
221-
originalPath: modulePath,
222-
libraryName: newLibraryName,
223-
outputPath,
224-
skipped: false,
225-
};
226-
} finally {
227-
await fs.promises.rm(tempPath, { recursive: true, force: true });
228-
}
258+
await writeXcframeworkInfo(outputPath, {
259+
...info,
260+
AvailableLibraries: info.AvailableLibraries.map((library) => {
261+
return {
262+
...library,
263+
BinaryPath: newBinaryRelativePath,
264+
LibraryPath: newFrameworkRelativePath,
265+
};
266+
}),
267+
});
268+
269+
return {
270+
originalPath: modulePath,
271+
libraryName: newLibraryName,
272+
outputPath,
273+
skipped: false,
274+
};
229275
}

0 commit comments

Comments
 (0)