Skip to content

Commit d789e82

Browse files
authored
Store Apple prebuilds as .apple.node instead of .xcframework (#52)
* Use ".apple.node" instead of ".xcframeworks" * Delete unused replaceWithNodeExtension * Fix tests * Update and add documentation * Fix verify script
1 parent 0a638a9 commit d789e82

File tree

12 files changed

+149
-83
lines changed

12 files changed

+149
-83
lines changed

docs/ANDROID.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
# Building Hermes from source
1+
# Android support
2+
3+
## Building Hermes from source
24

35
Because we're using a version of Hermes patched with Node-API support, we need to build React Native from source.
46

@@ -8,6 +10,8 @@ export REACT_NATIVE_OVERRIDE_HERMES_DIR=`npx react-native-node-api-modules vendo
810

911
## Cleaning your React Native build folders
1012

13+
If you've accidentally built your app without Hermes patched, you can clean things up by deleting the `ReactAndroid` build folder.
14+
1115
```
1216
rm -rf node_modules/react-native/ReactAndroid/build
1317
```

docs/AUTO-LINKING.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Auto-linking
2+
3+
The `react-native-node-api-modules` package (sometimes referred to as "the host package") has mechanisms to automatically find and link prebuilt binaries with Node-API modules.
4+
5+
When auto-linking, prebuilt binaries are copied (sometimes referred to as vendored) from dependencies of the app into the host package. As they're copied, they get renamed to avoid conflicts in naming as the library files across multiple dependency packages will be sharing a namespace when building the app.
6+
7+
## Naming scheme of libraries when linked into the host
8+
9+
The name of the library when linked / copied into the host is based on two things:
10+
11+
- The package name of the encapsulating package: The directory tree is walked from the original library path to the nearest `package.json` (this is the Node-API module's package root).
12+
- The relative path of the library to the package root:
13+
- Normalized (any "lib" prefix or file extension is stripped from the filename).
14+
- Escaped (any non-alphanumeric character is replaced with "-").
15+
16+
## How do I link Node-API module libraries into my app?
17+
18+
Linking will run when you `pod install` and as part of building your app with Gradle as long as your app has a dependency on the `react-native-node-api-modules` package.
19+
20+
You can also manually link by running the following in your app directory:
21+
22+
```bash
23+
npx react-native-node-api-modules link --android --apple
24+
```
25+
26+
> [!NOTE]
27+
> Because vendored frameworks must be present when running `pod install`, you have to run `pod install` if you add or remove a dependency with a Node-API module (or after creation if you're doing active development on it).

docs/PREBUILDS.md

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,38 @@
11
# Prebuilds
22

3-
This document will codify the naming and directory structure of prebuilt binaries, expected by the `react-native-node-api-modules` package and tools.
3+
This document codifies the naming and directory structure of prebuilt binaries, expected by the auto-linking mechanism.
44

5-
To align with prior art and established patterns around the distribution of Node-API modules for Node.js (and other engines supporting this),
5+
At the time of writing, our auto-linking host package (`react-native-node-api-modules`) support two kinds of prebuilds:
66

7-
## Apple: XCFrameworks of dynamic libraries in frameworks
7+
## `*.android.node` (for Android)
8+
9+
A jniLibs-like directory structure of CPU-architecture specific directories containing a single `.so` library file.
10+
11+
The name of all the `.so` library files:
12+
13+
- must be the same across all CPU-architectures
14+
- can have a "lib" prefix, but doesn't have to
15+
- must have an `.so` or `.node` file extension
16+
17+
> [!NOTE]
18+
> The `SONAME` doesn't have to match and is not updated as the .so is copied into the host package.
19+
> This might cause trouble if you're trying to link with the library from other native code.
20+
> We're tracking [#14](https://github.com/callstackincubator/react-native-node-api-modules/issues/14) to fix this 🤞
21+
22+
The directory must have a `react-native-node-api-module` file (the content doesn't matter), to signal that the directory is intended for auto-linking by the `react-native-node-api-module` package.
23+
24+
## `*.apple.node` (for Apple)
25+
26+
An XCFramework of dynamic libraries wrapped in `.framework` bundles, renamed from `.xcframework` to `.apple.node` to ease discoverability.
827

928
The Apple Developer documentation on ["Creating a multiplatform binary framework bundle"](https://developer.apple.com/documentation/xcode/creating-a-multi-platform-binary-framework-bundle#Avoid-issues-when-using-alternate-build-systems) mentions:
1029

1130
> An XCFramework can include dynamic library files, but only macOS supports these libraries for dynamic linking. Dynamic linking on iOS, watchOS, and tvOS requires the XCFramework to contain .framework bundles.
1231
13-
<!-- TODO: Write this -->
32+
The directory must have a `react-native-node-api-module` file (the content doesn't matter), to signal that the directory is intended for auto-linking by the `react-native-node-api-module` package.
33+
34+
## Why did we choose this naming scheme?
1435

15-
## Android: Directory of architecture specific directories of shared object library files.
36+
To align with prior art and established patterns around the distribution of Node-API modules for Node.js, we've chosen to use the ".node" filename extension for prebuilds of Node-API modules, targeting React Native.
1637

17-
<!-- TODO: Write this -->
38+
To enable distribution of packages with multiple co-existing platform-specific prebuilts, we've chosen to lean into the pattern of platform-specific filename extensions, used by the Metro bundler.

packages/ferric-example/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
Cargo.lock
33

44
/*.xcframework/
5+
/*.apple.node/
56
/*.android.node/
67

78
# Generated files

packages/ferric/src/build.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,11 @@ const ndkVersionOption = new Option(
6666
"--ndk-version <version>",
6767
"The NDK version to use for Android builds"
6868
).default(DEFAULT_NDK_VERSION);
69+
const xcframeworkExtensionOption = new Option(
70+
"--xcframework-extension",
71+
"Don't rename the xcframework to .apple.node"
72+
).default(false);
73+
6974
const outputPathOption = new Option(
7075
"--output <path>",
7176
"Writing outputs to this directory"
@@ -85,6 +90,7 @@ export const buildCommand = new Command("build")
8590
.addOption(ndkVersionOption)
8691
.addOption(outputPathOption)
8792
.addOption(configurationOption)
93+
.addOption(xcframeworkExtensionOption)
8894
.action(
8995
async ({
9096
target: targetArg,
@@ -93,6 +99,7 @@ export const buildCommand = new Command("build")
9399
ndkVersion,
94100
output: outputPath,
95101
configuration,
102+
xcframeworkExtension,
96103
}) => {
97104
try {
98105
const targets = new Set([...targetArg]);
@@ -221,8 +228,10 @@ export const buildCommand = new Command("build")
221228
if (appleLibraries.length > 0) {
222229
const libraryPaths = await combineLibraries(appleLibraries);
223230
const frameworkPaths = libraryPaths.map(createAppleFramework);
224-
const xcframeworkFilename =
225-
determineXCFrameworkFilename(frameworkPaths);
231+
const xcframeworkFilename = determineXCFrameworkFilename(
232+
frameworkPaths,
233+
xcframeworkExtension ? ".xcframework" : ".apple.node"
234+
);
226235

227236
// Create the xcframework
228237
const xcframeworkOutputPath = path.resolve(

packages/node-addon-examples/scripts/verify-prebuilds.mts

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,13 @@ const EXPECTED_XCFRAMEWORK_PLATFORMS = [
1616
"xros-arm64-simulator",
1717
];
1818

19-
async function verifyAndroidDirectory(dirent: fs.Dirent) {
19+
async function verifyAndroidPrebuild(dirent: fs.Dirent) {
20+
console.log(
21+
"Verifying Android prebuild",
22+
dirent.name,
23+
"in",
24+
dirent.parentPath
25+
);
2026
for (const arch of EXPECTED_ANDROID_ARCHS) {
2127
const archDir = path.join(dirent.parentPath, dirent.name, arch);
2228
for (const file of await fs.promises.readdir(archDir, {
@@ -31,7 +37,8 @@ async function verifyAndroidDirectory(dirent: fs.Dirent) {
3137
}
3238
}
3339

34-
async function verifyXcframework(dirent: fs.Dirent) {
40+
async function verifyApplePrebuild(dirent: fs.Dirent) {
41+
console.log("Verifying Apple prebuild", dirent.name, "in", dirent.parentPath);
3542
for (const arch of EXPECTED_XCFRAMEWORK_PLATFORMS) {
3643
const archDir = path.join(dirent.parentPath, dirent.name, arch);
3744
for (const file of await fs.promises.readdir(archDir, {
@@ -75,16 +82,17 @@ async function verifyXcframework(dirent: fs.Dirent) {
7582
}
7683
}
7784

78-
for (const dirent of await fs.promises.readdir(EXAMPLES_DIR, {
85+
for await (const dirent of fs.promises.glob("**/*.*.node", {
86+
cwd: EXAMPLES_DIR,
7987
withFileTypes: true,
80-
recursive: true,
8188
})) {
82-
if (!dirent.isDirectory()) {
83-
continue;
84-
}
8589
if (dirent.name.endsWith(".android.node")) {
86-
await verifyAndroidDirectory(dirent);
87-
} else if (dirent.name.endsWith(".xcframework")) {
88-
await verifyXcframework(dirent);
90+
await verifyAndroidPrebuild(dirent);
91+
} else if (dirent.name.endsWith(".apple.node")) {
92+
await verifyApplePrebuild(dirent);
93+
} else {
94+
throw new Error(
95+
`Unexpected prebuild file: ${dirent.name} in ${dirent.parentPath}`
96+
);
8997
}
9098
}

packages/react-native-node-api-cmake/src/cli.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,11 @@ const noWeakNodeApiLinkageOption = new Option(
9191
"Don't pass the path of the weak-node-api library from react-native-node-api-modules"
9292
);
9393

94+
const xcframeworkExtensionOption = new Option(
95+
"--xcframework-extension",
96+
"Don't rename the xcframework to .apple.node"
97+
).default(false);
98+
9499
export const program = new Command("react-native-node-api-cmake")
95100
.description("Build React Native Node API modules with CMake")
96101
.addOption(sourcePathOption)
@@ -104,6 +109,7 @@ export const program = new Command("react-native-node-api-cmake")
104109
.addOption(ndkVersionOption)
105110
.addOption(noAutoLinkOption)
106111
.addOption(noWeakNodeApiLinkageOption)
112+
.addOption(xcframeworkExtensionOption)
107113
.action(async ({ triplet: tripletValues, ...globalContext }) => {
108114
try {
109115
const buildPath = getBuildPath(globalContext);
@@ -212,8 +218,10 @@ export const program = new Command("react-native-node-api-cmake")
212218
})
213219
);
214220
const frameworkPaths = libraryPaths.map(createAppleFramework);
215-
const xcframeworkFilename =
216-
determineXCFrameworkFilename(frameworkPaths);
221+
const xcframeworkFilename = determineXCFrameworkFilename(
222+
frameworkPaths,
223+
globalContext.xcframeworkExtension ? ".xcframework" : ".apple.node"
224+
);
217225

218226
// Create the xcframework
219227
const xcframeworkOutputPath = path.resolve(

packages/react-native-node-api-modules/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
"copy-node-api-headers": "tsx scripts/copy-node-api-headers.ts",
4343
"generate-weak-node-api": "tsx scripts/generate-weak-node-api.ts",
4444
"generate-weak-node-api-injector": "tsx scripts/generate-weak-node-api-injector.ts",
45-
"build-weak-node-api": "npm run generate-weak-node-api && react-native-node-api-cmake --android --apple --no-auto-link --no-weak-node-api-linkage --source ./weak-node-api",
45+
"build-weak-node-api": "npm run generate-weak-node-api && react-native-node-api-cmake --android --apple --no-auto-link --no-weak-node-api-linkage --xcframework-extension --source ./weak-node-api",
4646
"test": "tsx --test src/node/**/*.test.ts src/node/*.test.ts"
4747
},
4848
"keywords": [

packages/react-native-node-api-modules/src/node/babel-plugin/plugin.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ describe("plugin", () => {
1212
it("transforms require calls, regardless", (context) => {
1313
const tempDirectoryPath = setupTempDirectory(context, {
1414
"package.json": `{ "name": "my-package" }`,
15-
"addon-1.xcframework/addon-1.node":
15+
"addon-1.apple.node/addon-1.node":
1616
"// This is supposed to be a binary file",
17-
"addon-2.xcframework/addon-2.node":
17+
"addon-2.apple.node/addon-2.node":
1818
"// This is supposed to be a binary file",
1919
"addon-1.js": `
2020
const addon = require('./addon-1.node');

0 commit comments

Comments
 (0)