diff --git a/src/commands/codepush/lib/react-native-utils.ts b/src/commands/codepush/lib/react-native-utils.ts index b8592c88d..a0704bbed 100644 --- a/src/commands/codepush/lib/react-native-utils.ts +++ b/src/commands/codepush/lib/react-native-utils.ts @@ -348,11 +348,12 @@ export function runReactNativeBundleCommand( }); } -export function runHermesEmitBinaryCommand( +export async function runHermesEmitBinaryCommand( bundleName: string, outputFolder: string, sourcemapOutput: string, - extraHermesFlags: string[] + extraHermesFlags: string[], + gradleFile: string ): Promise { const hermesArgs: string[] = []; const envNodeArgs: string = process.env.CODE_PUSH_NODE_ARGS; @@ -378,7 +379,7 @@ export function runHermesEmitBinaryCommand( } out.text(chalk.cyan("Converting JS bundle to byte code via Hermes, running command:\n")); - const hermesCommand = getHermesCommand(); + const hermesCommand = await getHermesCommand(gradleFile); const hermesProcess = childProcess.spawn(hermesCommand, hermesArgs); out.text(`${hermesCommand} ${hermesArgs.join(" ")}`); @@ -464,7 +465,7 @@ export function runHermesEmitBinaryCommand( }); } -export function getAndroidHermesEnabled(gradleFile: string): boolean { +function parseBuildGradleFile(gradleFile: string) { let buildGradlePath: string = path.join("android", "app"); if (gradleFile) { buildGradlePath = gradleFile; @@ -477,14 +478,28 @@ export function getAndroidHermesEnabled(gradleFile: string): boolean { throw new Error(`Unable to find gradle file "${buildGradlePath}".`); } - return g2js - .parseFile(buildGradlePath) - .catch(() => { - throw new Error(`Unable to parse the "${buildGradlePath}" file. Please ensure it is a well-formed Gradle file.`); - }) - .then((buildGradle: any) => { - return Array.from(buildGradle["project.ext.react"] || []).some((line: string) => /^enableHermes\s{0,}:\s{0,}true/.test(line)); - }); + return g2js.parseFile(buildGradlePath).catch(() => { + throw new Error(`Unable to parse the "${buildGradlePath}" file. Please ensure it is a well-formed Gradle file.`); + }); +} + +async function getHermesCommandFromGradle(gradleFile: string): Promise { + const buildGradle: any = await parseBuildGradleFile(gradleFile); + + const hermesCommandProperty: any = Array.from(buildGradle["project.ext.react"] || []).find((prop: string) => + prop.trim().startsWith("hermesCommand:") + ); + if (hermesCommandProperty) { + return hermesCommandProperty.replace("hermesCommand:", "").trim().slice(1, -1); + } else { + return ""; + } +} + +export function getAndroidHermesEnabled(gradleFile: string): boolean { + return parseBuildGradleFile(gradleFile).then((buildGradle: any) => { + return Array.from(buildGradle["project.ext.react"] || []).some((line: string) => /^enableHermes\s{0,}:\s{0,}true/.test(line)); + }); } export function getiOSHermesEnabled(podFile: string): boolean { @@ -529,7 +544,7 @@ function getHermesOSExe(): string { } } -function getHermesCommand(): string { +async function getHermesCommand(gradleFile: string): Promise { const fileExists = (file: string): boolean => { try { return fs.statSync(file).isFile(); @@ -537,12 +552,18 @@ function getHermesCommand(): string { return false; } }; - // assume if hermes-engine exists it should be used instead of hermesvm - const hermesEngine = path.join("node_modules", "hermes-engine", getHermesOSBin(), getHermesOSExe()); - if (fileExists(hermesEngine)) { - return hermesEngine; + + const gradleHermesCommand = await getHermesCommandFromGradle(gradleFile); + if (gradleHermesCommand) { + return path.join("android", "app", gradleHermesCommand.replace("%OS-BIN%", getHermesOSBin())); + } else { + // assume if hermes-engine exists it should be used instead of hermesvm + const hermesEngine = path.join("node_modules", "hermes-engine", getHermesOSBin(), getHermesOSExe()); + if (fileExists(hermesEngine)) { + return hermesEngine; + } + return path.join("node_modules", "hermesvm", getHermesOSBin(), "hermes"); } - return path.join("node_modules", "hermesvm", getHermesOSBin(), "hermes"); } function getComposeSourceMapsPath(): string { diff --git a/src/commands/codepush/release-react.ts b/src/commands/codepush/release-react.ts index 41f7f2f00..ce787bf40 100644 --- a/src/commands/codepush/release-react.ts +++ b/src/commands/codepush/release-react.ts @@ -224,13 +224,25 @@ export default class CodePushReleaseReactCommand extends CodePushReleaseCommandB if (this.os === "android") { const isHermesEnabled = await getAndroidHermesEnabled(this.gradleFile); if (isHermesEnabled) { - await runHermesEmitBinaryCommand(this.bundleName, this.updateContentsPath, this.sourcemapOutput, this.extraHermesFlags); + await runHermesEmitBinaryCommand( + this.bundleName, + this.updateContentsPath, + this.sourcemapOutput, + this.extraHermesFlags, + this.gradleFile + ); } } else if (this.os === "ios") { // Check if we have to run hermes to compile JS to Byte Code if Hermes is enabled in Podfile and we're releasing an iOS build const isHermesEnabled = await getiOSHermesEnabled(this.podFile); if (isHermesEnabled) { - await runHermesEmitBinaryCommand(this.bundleName, this.updateContentsPath, this.sourcemapOutput, this.extraHermesFlags); + await runHermesEmitBinaryCommand( + this.bundleName, + this.updateContentsPath, + this.sourcemapOutput, + this.extraHermesFlags, + this.gradleFile + ); } } out.text(chalk.cyan("\nReleasing update contents to CodePush:\n")); diff --git a/test/commands/codepush/release-react-test.ts b/test/commands/codepush/release-react-test.ts index ff28dabc5..8072adffd 100644 --- a/test/commands/codepush/release-react-test.ts +++ b/test/commands/codepush/release-react-test.ts @@ -939,6 +939,76 @@ describe("codepush release-react command", function () { // Assert expect(runHermesEmitBinaryCommandStub.calledOnce).is.true; }); + + it("uses hermesCommand path if set in gradle file", async function () { + const os = "Android"; + // Arrange + const args = { + ...goldenPathArgs, + // prettier-ignore + args: [ + "--target-binary-version", "1.0.0", + "--deployment-name", deployment, + "--app", app, + "--token", "c1o3d3e7", + ], + }; + const command = new CodePushReleaseReactCommand(args); + sandbox.stub(fs, "readFileSync").returns(` + { + "name": "RnCodepushAndroid", + "version": "0.0.1", + "dependencies": { + "react": "16.13.1", + "react-native": "0.63.3", + "react-native-code-push": "6.3.0" + } + } + `); + + Nock("https://api.appcenter.ms/").get(`/v0.1/apps/${app}/deployments/${deployment}`).reply(200, {}); + Nock("https://api.appcenter.ms/").get(`/v0.1/apps/${app}`).reply(200, { + os, + platform: "react-native", + }); + sandbox.stub(mkdirp, "sync"); + sandbox.stub(fileUtils, "fileDoesNotExistOrIsDirectory").returns(false); + sandbox.stub(fileUtils, "createEmptyTmpReleaseFolder"); + sandbox.stub(command, "release" as any).resolves({ succeeded: true }); + sandbox.stub(fileUtils, "removeReactTmpDir"); + sandbox.stub(ReactNativeTools, "runReactNativeBundleCommand"); + sandbox.stub(fs, "lstatSync").returns({ isDirectory: () => false } as any); + sandbox.stub(g2js, "parseFile").resolves({ + "project.ext.react": ["enableHermes: true", 'hermesCommand: "../../../hermes/is/here"'], + }); + + const childProcessStub = new events.EventEmitter() as any; + childProcessStub.stdout = { + on: () => {}, + }; + childProcessStub.stderr = { + on: () => {}, + }; + const childProcessSpawnStub = sandbox + .stub(cp, "spawn") + .onFirstCall() + .callsFake(() => { + setTimeout(() => { + childProcessStub.emit("close"); + }); + return childProcessStub as any; + }); + sandbox.stub(fs, "copyFile").yields(null); + sandbox.stub(fs, "unlink").yields(null); + + // Act + const result = await command.execute(); + + // Assert + sandbox.assert.calledWith(childProcessSpawnStub, "../hermes/is/here"); + expect(result.succeeded).to.be.true; + }); + it("project.ext.react is not defined in the app gradle file", async function () { const os = "Android"; // Arrange