Skip to content

Commit e9ca2aa

Browse files
authored
Restore Apple framework symlinks (#302)
* Set up files in private packages * Reconstruct missing symbolic links if needed * Restore weak-node-api symlinks * Improve robustness of restoreFrameworkLinks By not expecting the current version name to be "A" * Add tests for restoreFrameworkLinks
1 parent c698698 commit e9ca2aa

File tree

9 files changed

+217
-3
lines changed

9 files changed

+217
-3
lines changed

package-lock.json

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

packages/ferric-example/package.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
{
22
"name": "@react-native-node-api/ferric-example",
3+
"version": "0.1.1",
34
"private": true,
45
"type": "commonjs",
5-
"version": "0.1.1",
66
"homepage": "https://github.com/callstackincubator/react-native-node-api",
77
"repository": {
88
"type": "git",
@@ -11,6 +11,12 @@
1111
},
1212
"main": "ferric_example.js",
1313
"types": "ferric_example.d.ts",
14+
"files": [
15+
"ferric_example.js",
16+
"ferric_example.d.ts",
17+
"ferric_example.apple.node",
18+
"ferric_example.android.node"
19+
],
1420
"scripts": {
1521
"build": "ferric build",
1622
"bootstrap": "node --run build"

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

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
readAndParsePlist,
1010
readFrameworkInfo,
1111
readXcframeworkInfo,
12+
restoreFrameworkLinks,
1213
} from "./apple";
1314
import { setupTempDirectory } from "../test-utils";
1415

@@ -267,6 +268,109 @@ describe("apple", { skip: process.platform !== "darwin" }, () => {
267268
);
268269
});
269270
});
271+
272+
describe("restoreFrameworkLinks", () => {
273+
it("restores a versioned framework", async (context) => {
274+
const infoPlistContents = `
275+
<?xml version="1.0" encoding="UTF-8"?>
276+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
277+
<plist version="1.0">
278+
<dict>
279+
<key>CFBundlePackageType</key>
280+
<string>FMWK</string>
281+
<key>CFBundleInfoDictionaryVersion</key>
282+
<string>6.0</string>
283+
<key>CFBundleExecutable</key>
284+
<string>example-addon</string>
285+
</dict>
286+
</plist>
287+
`;
288+
289+
const tempDirectoryPath = setupTempDirectory(context, {
290+
"foo.framework": {
291+
Versions: {
292+
A: {
293+
Resources: {
294+
"Info.plist": infoPlistContents,
295+
},
296+
"example-addon": "",
297+
},
298+
},
299+
},
300+
});
301+
302+
const frameworkPath = path.join(tempDirectoryPath, "foo.framework");
303+
const currentVersionPath = path.join(
304+
frameworkPath,
305+
"Versions",
306+
"Current",
307+
);
308+
const binaryLinkPath = path.join(frameworkPath, "example-addon");
309+
const realBinaryPath = path.join(
310+
frameworkPath,
311+
"Versions",
312+
"A",
313+
"example-addon",
314+
);
315+
316+
async function assertVersionedFramework() {
317+
const currentStat = await fs.promises.lstat(currentVersionPath);
318+
assert(
319+
currentStat.isSymbolicLink(),
320+
"Expected Current symlink to be restored",
321+
);
322+
assert.equal(
323+
await fs.promises.realpath(currentVersionPath),
324+
path.join(frameworkPath, "Versions", "A"),
325+
);
326+
327+
const binaryStat = await fs.promises.lstat(binaryLinkPath);
328+
assert(
329+
binaryStat.isSymbolicLink(),
330+
"Expected binary symlink to be restored",
331+
);
332+
assert.equal(
333+
await fs.promises.realpath(binaryLinkPath),
334+
realBinaryPath,
335+
);
336+
}
337+
338+
await restoreFrameworkLinks(frameworkPath);
339+
await assertVersionedFramework();
340+
341+
// Calling again to expect a no-op
342+
await restoreFrameworkLinks(frameworkPath);
343+
await assertVersionedFramework();
344+
});
345+
346+
it("throws on a flat framework", async (context) => {
347+
const tempDirectoryPath = setupTempDirectory(context, {
348+
"foo.framework": {
349+
"Info.plist": `
350+
<?xml version="1.0" encoding="UTF-8"?>
351+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
352+
<plist version="1.0">
353+
<dict>
354+
<key>CFBundlePackageType</key>
355+
<string>FMWK</string>
356+
<key>CFBundleInfoDictionaryVersion</key>
357+
<string>6.0</string>
358+
<key>CFBundleExecutable</key>
359+
<string>example-addon</string>
360+
</dict>
361+
</plist>
362+
`,
363+
},
364+
});
365+
366+
const frameworkPath = path.join(tempDirectoryPath, "foo.framework");
367+
368+
await assert.rejects(
369+
() => restoreFrameworkLinks(frameworkPath),
370+
/Expected "Versions" directory inside versioned framework/,
371+
);
372+
});
373+
});
270374
});
271375

272376
describe("apple on non-darwin", { skip: process.platform === "darwin" }, () => {

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

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,54 @@ export async function linkFlatFramework({
192192
}
193193
}
194194

195+
/**
196+
* NPM packages aren't preserving internal symlinks inside versioned frameworks.
197+
* This function attempts to restore those.
198+
*/
199+
export async function restoreFrameworkLinks(frameworkPath: string) {
200+
// Reconstruct missing symbolic links if needed
201+
const versionsPath = path.join(frameworkPath, "Versions");
202+
const versionCurrentPath = path.join(versionsPath, "Current");
203+
204+
assert(
205+
fs.existsSync(versionsPath),
206+
`Expected "Versions" directory inside versioned framework '${frameworkPath}'`,
207+
);
208+
209+
if (!fs.existsSync(versionCurrentPath)) {
210+
const versionDirectoryEntries = await fs.promises.readdir(versionsPath, {
211+
withFileTypes: true,
212+
});
213+
const versionDirectoryPaths = versionDirectoryEntries
214+
.filter((dirent) => dirent.isDirectory())
215+
.map((dirent) => path.join(dirent.parentPath, dirent.name));
216+
assert.equal(
217+
versionDirectoryPaths.length,
218+
1,
219+
`Expected a single directory in ${versionsPath}, found ${JSON.stringify(versionDirectoryPaths)}`,
220+
);
221+
const [versionDirectoryPath] = versionDirectoryPaths;
222+
await fs.promises.symlink(
223+
path.relative(path.dirname(versionCurrentPath), versionDirectoryPath),
224+
versionCurrentPath,
225+
);
226+
}
227+
228+
const { CFBundleExecutable } = await readFrameworkInfo(
229+
path.join(versionCurrentPath, "Resources", "Info.plist"),
230+
);
231+
232+
const libraryRealPath = path.join(versionCurrentPath, CFBundleExecutable);
233+
const libraryLinkPath = path.join(frameworkPath, CFBundleExecutable);
234+
// Reconstruct missing symbolic links if needed
235+
if (fs.existsSync(libraryRealPath) && !fs.existsSync(libraryLinkPath)) {
236+
await fs.promises.symlink(
237+
path.relative(path.dirname(libraryLinkPath), libraryRealPath),
238+
libraryLinkPath,
239+
);
240+
}
241+
}
242+
195243
export async function linkVersionedFramework({
196244
frameworkPath,
197245
newLibraryName,
@@ -201,6 +249,9 @@ export async function linkVersionedFramework({
201249
"darwin",
202250
"Linking Apple addons are only supported on macOS",
203251
);
252+
253+
await restoreFrameworkLinks(frameworkPath);
254+
204255
const frameworkInfoPath = path.join(
205256
frameworkPath,
206257
"Versions",

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

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import assert from "node:assert/strict";
22
import path from "node:path";
33
import { EventEmitter } from "node:stream";
4+
import fs from "node:fs";
45

56
import {
67
Command,
@@ -26,8 +27,9 @@ import {
2627
import { command as vendorHermes } from "./hermes";
2728
import { packageNameOption, pathSuffixOption } from "./options";
2829
import { linkModules, pruneLinkedModules, ModuleLinker } from "./link-modules";
29-
import { linkXcframework } from "./apple";
30+
import { linkXcframework, restoreFrameworkLinks } from "./apple";
3031
import { linkAndroidDir } from "./android";
32+
import { weakNodeApiPath } from "../weak-node-api";
3133

3234
// We're attaching a lot of listeners when spawning in parallel
3335
EventEmitter.defaultMaxListeners = 100;
@@ -169,6 +171,38 @@ program
169171
await pruneLinkedModules(platform, modules);
170172
}
171173
}
174+
175+
if (apple) {
176+
await oraPromise(
177+
async () => {
178+
const xcframeworkPath = path.join(
179+
weakNodeApiPath,
180+
"weak-node-api.xcframework",
181+
);
182+
await Promise.all(
183+
[
184+
path.join(xcframeworkPath, "macos-x86_64"),
185+
path.join(xcframeworkPath, "macos-arm64"),
186+
path.join(xcframeworkPath, "macos-arm64_x86_64"),
187+
].map(async (slicePath) => {
188+
const frameworkPath = path.join(
189+
slicePath,
190+
"weak-node-api.framework",
191+
);
192+
if (fs.existsSync(frameworkPath)) {
193+
await restoreFrameworkLinks(frameworkPath);
194+
}
195+
}),
196+
);
197+
},
198+
{
199+
text: "Restoring weak-node-api symlinks",
200+
successText: "Restored weak-node-api symlinks",
201+
failText: (error) =>
202+
`Failed to restore weak-node-api symlinks: ${error.message}`,
203+
},
204+
);
205+
}
172206
},
173207
),
174208
);
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
examples/
2+
build/

packages/node-addon-examples/package.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,17 @@
11
{
22
"name": "@react-native-node-api/node-addon-examples",
3+
"version": "0.1.0",
34
"type": "commonjs",
45
"main": "dist/index.js",
6+
"files": [
7+
"dist",
8+
"examples/**/package.json",
9+
"examples/**/*.js",
10+
"tests/**/package.json",
11+
"tests/**/*.js",
12+
"**/*.apple.node/**",
13+
"**/*.android.node/**"
14+
],
515
"private": true,
616
"homepage": "https://github.com/callstackincubator/react-native-node-api",
717
"repository": {

packages/node-addon-examples/tests/.gitignore

Lines changed: 0 additions & 1 deletion
This file was deleted.

packages/node-tests/package.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
{
22
"name": "@react-native-node-api/node-tests",
3+
"version": "0.1.0",
34
"description": "Harness for running the Node.js tests from https://github.com/nodejs/node/tree/main/test",
45
"type": "commonjs",
56
"main": "tests.generated.js",
7+
"files": [
8+
"dist",
9+
"tests/**/*.js",
10+
"**/*.apple.node/**",
11+
"**/*.android.node/**"
12+
],
613
"private": true,
714
"homepage": "https://github.com/callstackincubator/react-native-node-api",
815
"repository": {

0 commit comments

Comments
 (0)