diff --git a/.changeset/many-candies-retire.md b/.changeset/many-candies-retire.md new file mode 100644 index 00000000..ec0192e5 --- /dev/null +++ b/.changeset/many-candies-retire.md @@ -0,0 +1,5 @@ +--- +"react-native-node-api": patch +--- + +Fix auto-linking from Gradle builds on Windows diff --git a/.changeset/silver-suits-double.md b/.changeset/silver-suits-double.md new file mode 100644 index 00000000..cbfd9bbb --- /dev/null +++ b/.changeset/silver-suits-double.md @@ -0,0 +1,5 @@ +--- +"react-native-node-api": minor +--- + +Assert that REACT_NATIVE_OVERRIDE_HERMES_DIR is set when Android / Gradle projects depend on the host package diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 52aa06c7..53c7f734 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -3,6 +3,8 @@ name: Check env: # Version here should match the one in React Native template and packages/cmake-rn/src/cli.ts NDK_VERSION: 27.1.12297006 + # Enabling the Gradle test on CI (disabled by default because it downloads a lot) + ENABLE_GRADLE_TESTS: true on: push: diff --git a/packages/host/android/build.gradle b/packages/host/android/build.gradle index ee47d91a..0eedddfb 100644 --- a/packages/host/android/build.gradle +++ b/packages/host/android/build.gradle @@ -1,5 +1,6 @@ import java.nio.file.Paths import groovy.json.JsonSlurper +import org.gradle.internal.os.OperatingSystem buildscript { ext.getExtOrDefault = {name -> @@ -134,12 +135,30 @@ dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" } +task checkHermesOverride { + doFirst { + if (!System.getenv("REACT_NATIVE_OVERRIDE_HERMES_DIR")) { + throw new GradleException([ + "React Native Node-API needs a custom version of Hermes with Node-API enabled.", + "Run the following in your terminal, to clone Hermes and instruct React Native to use it:", + "", + "export REACT_NATIVE_OVERRIDE_HERMES_DIR=`npx react-native-node-api vendor-hermes --silent --force`", + "", + "And follow this guide to build React Native from source:", + "https://reactnative.dev/contributing/how-to-build-from-source#update-your-project-to-build-from-source" + ].join('\n')) + } + } +} + +def commandLinePrefix = OperatingSystem.current().isWindows() ? ["cmd", "/c", "node"] : [] +def cliPath = file("../bin/react-native-node-api.mjs") + // Custom task to fetch jniLibs paths via CLI task linkNodeApiModules { doLast { exec { - // TODO: Support --strip-path-suffix - commandLine 'npx', 'react-native-node-api', 'link', '--android', rootProject.rootDir.absolutePath + commandLine commandLinePrefix + [cliPath, 'link', '--android', rootProject.rootDir.absolutePath] standardOutput = System.out errorOutput = System.err // Enable color output @@ -150,5 +169,5 @@ task linkNodeApiModules { } } -preBuild.dependsOn linkNodeApiModules +preBuild.dependsOn checkHermesOverride, linkNodeApiModules diff --git a/packages/host/package.json b/packages/host/package.json index f85eb021..45a096b6 100644 --- a/packages/host/package.json +++ b/packages/host/package.json @@ -47,6 +47,7 @@ "build-weak-node-api": "cmake-rn --no-auto-link --no-weak-node-api-linkage --xcframework-extension --source ./weak-node-api --out ./weak-node-api", "build-weak-node-api:all-triplets": "cmake-rn --android --apple --no-auto-link --no-weak-node-api-linkage --xcframework-extension --source ./weak-node-api --out ./weak-node-api", "test": "tsx --test --test-reporter=@reporters/github --test-reporter-destination=stdout --test-reporter=spec --test-reporter-destination=stdout src/node/**/*.test.ts src/node/*.test.ts", + "test:gradle": "ENABLE_GRADLE_TESTS=true node --run test", "bootstrap": "node --run copy-node-api-headers && node --run generate-weak-node-api-injector && node --run generate-weak-node-api && node --run build-weak-node-api", "prerelease": "node --run copy-node-api-headers && node --run generate-weak-node-api-injector && node --run generate-weak-node-api && node --run build-weak-node-api:all-triplets" }, diff --git a/packages/host/src/node/cli/bin.test.ts b/packages/host/src/node/cli/bin.test.ts new file mode 100644 index 00000000..f38dead1 --- /dev/null +++ b/packages/host/src/node/cli/bin.test.ts @@ -0,0 +1,57 @@ +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; +import cp from "node:child_process"; +import path from "node:path"; + +const PACKAGE_ROOT = path.join(__dirname, "../../.."); +const BIN_PATH = path.join(PACKAGE_ROOT, "bin/react-native-node-api.mjs"); + +describe("bin", () => { + describe("help command", () => { + it("should succeed with a mention of usage", () => { + const { status, stdout, stderr } = cp.spawnSync( + process.execPath, + [BIN_PATH, "help"], + { + cwd: PACKAGE_ROOT, + encoding: "utf8", + }, + ); + + assert.equal( + status, + 0, + `Expected success (got ${status}): ${stdout} ${stderr}`, + ); + assert.match( + stdout, + /Usage: react-native-node-api/, + `Failed to find expected output (stdout: ${stdout} stderr: ${stderr})`, + ); + }); + }); + + describe("link command", () => { + it("should succeed with a mention of Node-API modules", () => { + const { status, stdout, stderr } = cp.spawnSync( + process.execPath, + [BIN_PATH, "link", "--android", "--apple"], + { + cwd: PACKAGE_ROOT, + encoding: "utf8", + }, + ); + + assert.equal( + status, + 0, + `Expected success (got ${status}): ${stdout} ${stderr}`, + ); + assert.match( + stdout + stderr, + /Auto-linking Node-API modules/, + `Failed to find expected output (stdout: ${stdout} stderr: ${stderr})`, + ); + }); + }); +}); diff --git a/packages/host/src/node/gradle.test.ts b/packages/host/src/node/gradle.test.ts new file mode 100644 index 00000000..b1da4a10 --- /dev/null +++ b/packages/host/src/node/gradle.test.ts @@ -0,0 +1,66 @@ +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; +import cp from "node:child_process"; +import path from "node:path"; + +const PACKAGE_ROOT = path.join(__dirname, "../.."); +const MONOREPO_ROOT = path.join(PACKAGE_ROOT, "../.."); +const TEST_APP_ANDROID_PATH = path.join(MONOREPO_ROOT, "apps/test-app/android"); + +describe( + "Gradle tasks", + // Skipping these tests by default, as they download a lot and takes a long time + { skip: process.env.ENABLE_GRADLE_TESTS !== "true" }, + () => { + describe("checkHermesOverride task", () => { + it("should fail if REACT_NATIVE_OVERRIDE_HERMES_DIR is not set", () => { + const { status, stdout, stderr } = cp.spawnSync( + "sh", + ["gradlew", "react-native-node-api:checkHermesOverride"], + { + cwd: TEST_APP_ANDROID_PATH, + env: { + ...process.env, + REACT_NATIVE_OVERRIDE_HERMES_DIR: undefined, + }, + encoding: "utf-8", + }, + ); + + assert.notEqual(status, 0, `Expected failure: ${stdout} ${stderr}`); + assert.match( + stderr, + /React Native Node-API needs a custom version of Hermes with Node-API enabled/, + ); + assert.match( + stderr, + /Run the following in your terminal, to clone Hermes and instruct React Native to use it/, + ); + assert.match( + stderr, + /export REACT_NATIVE_OVERRIDE_HERMES_DIR=`npx react-native-node-api vendor-hermes --silent --force`/, + ); + assert.match( + stderr, + /And follow this guide to build React Native from source/, + ); + }); + }); + + describe("linkNodeApiModules task", () => { + it("should call the CLI to autolink", () => { + const { status, stdout, stderr } = cp.spawnSync( + "sh", + ["gradlew", "react-native-node-api:linkNodeApiModules"], + { + cwd: TEST_APP_ANDROID_PATH, + encoding: "utf-8", + }, + ); + + assert.equal(status, 0, `Expected success: ${stdout} ${stderr}`); + assert.match(stdout, /Auto-linking Node-API modules/); + }); + }); + }, +);