Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 55 additions & 3 deletions script/react-native-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,48 @@ export async function takeHermesBaseBytecode(
return baseReleaseBundle;
}

// Hermes BytecodeFileHeader: uint64 magic, uint32 version (little-endian), stable since Hermes v0.1
const HERMES_BYTECODE_MAGIC = BigInt("0x1F1903C103BC1FC6");

function getHermesBytecodeVersion(bytecode: Buffer): number | null {
if (bytecode.length < 12 || bytecode.readBigUInt64LE(0) !== HERMES_BYTECODE_MAGIC) {
return null;
}
return bytecode.readUInt32LE(8);
}

// Compile an empty input to stdout and read the version from the emitted header
export function getHermesCompilerBytecodeVersion(hermesCommand: string): number | null {
const result = childProcess.spawnSync(hermesCommand, ["-emit-binary", "-"], { input: "", timeout: 30000 });
return result.status === 0 ? getHermesBytecodeVersion(result.stdout) : null;
}

// hermesc -base-bytecode aborts on a base of another bytecode version; the base is only a diff optimization
export function resolveHermesBaseBytecode(baseBytecode: string, compilerBytecodeVersion: number | null): string | null {
const baseBytecodeVersion = getHermesBytecodeVersion(fs.readFileSync(baseBytecode));
if (baseBytecodeVersion === null) {
log(chalk.yellow("\nBase release bundle is not Hermes bytecode, skipping -base-bytecode.\n"));
return null;
}

if (compilerBytecodeVersion === null) {
log(chalk.yellow("\nCould not detect the Hermes compiler bytecode version, skipping -base-bytecode.\n"));
return null;
}

if (baseBytecodeVersion !== compilerBytecodeVersion) {
log(
chalk.yellow(
`\nWarning: base release is Hermes bytecode v${baseBytecodeVersion}, local compiler produces v${compilerBytecodeVersion}. ` +
`Skipping -base-bytecode. Binaries on v${baseBytecodeVersion} can't run this update.\n`
)
);
return null;
}

return baseBytecode;
}

export async function runHermesEmitBinaryCommand(
command: cli.IReleaseReactCommand,
bundleName: string,
Expand Down Expand Up @@ -102,16 +144,21 @@ export async function runHermesEmitBinaryCommand(
hermesArgs.push("-output-source-map");
}

if (baseBytecode) {
hermesArgs.push("-base-bytecode", baseBytecode);
const hermesCommand = await getHermesCommand(gradleFile);

const resolvedBaseBytecode =
baseBytecode && resolveHermesBaseBytecode(baseBytecode, getHermesCompilerBytecodeVersion(hermesCommand));
if (resolvedBaseBytecode) {
hermesArgs.push("-base-bytecode", resolvedBaseBytecode);
}

console.log(chalk.cyan("Converting JS bundle to byte code via Hermes, running command:\n"));
const hermesCommand = await getHermesCommand(gradleFile);
const hermesProcess = childProcess.spawn(hermesCommand, hermesArgs);
console.log(`${hermesCommand} ${hermesArgs.join(" ")}`);

return new Promise<void>((resolve, reject) => {
hermesProcess.on("error", reject);

hermesProcess.stdout.on("data", (data: Buffer) => {
console.log(data.toString().trim());
});
Expand All @@ -123,6 +170,7 @@ export async function runHermesEmitBinaryCommand(
hermesProcess.on("close", (exitCode: number, signal: string) => {
if (exitCode !== 0) {
reject(new Error(`"hermes" command failed (exitCode=${exitCode}, signal=${signal}).`));
return;
}
// Copy HBC bundle to overwrite JS bundle
const source = path.join(outputFolder, bundleName + ".hbc");
Expand All @@ -131,11 +179,13 @@ export async function runHermesEmitBinaryCommand(
if (err) {
console.error(err);
reject(new Error(`Copying file ${source} to ${destination} failed. "hermes" previously exited with code ${exitCode}.`));
return;
}
fs.unlink(source, (err) => {
if (err) {
console.error(err);
reject(err);
return;
}
resolve(null as void);
});
Expand Down Expand Up @@ -202,13 +252,15 @@ export async function runHermesEmitBinaryCommand(
composeSourceMapsProcess.on("close", (exitCode: number, signal: string) => {
if (exitCode !== 0) {
reject(new Error(`"compose-source-maps" command failed (exitCode=${exitCode}, signal=${signal}).`));
return;
}

// Delete the HBC sourceMap, otherwise it will be included in 'code-push' bundle as well
fs.unlink(jsCompilerSourceMapFile, (err) => {
if (err) {
console.error(err);
reject(err);
return;
}

resolve(null);
Expand Down
75 changes: 75 additions & 0 deletions test/hermes-bytecode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import * as assert from "assert";
import * as fs from "fs";
import * as os from "os";
import * as path from "path";
import * as sinon from "sinon";
import * as cmdexec from "../script/command-executor";
import { getHermesCompilerBytecodeVersion, resolveHermesBaseBytecode } from "../script/react-native-utils";

function hermesHeader(version: number): Buffer {
const header = Buffer.alloc(128);
header.writeBigUInt64LE(BigInt("0x1F1903C103BC1FC6"), 0);
header.writeUInt32LE(version, 8);
return header;
}

describe("Hermes base bytecode", () => {
let testDirectory: string;
let sandbox: sinon.SinonSandbox;
let log: sinon.SinonStub;

function createBundle(contents: Buffer | string): string {
const bundle = path.join(testDirectory, "index.android.bundle");
fs.writeFileSync(bundle, contents);
return bundle;
}

function createCompiler(script: string): string {
const compiler = path.join(testDirectory, "hermesc");
fs.writeFileSync(compiler, `#!/usr/bin/env node\n${script}\n`, { mode: 0o755 });
return compiler;
}

beforeEach(() => {
testDirectory = fs.mkdtempSync(path.join(os.tmpdir(), "revopush-hermes-"));
sandbox = sinon.createSandbox();
log = sandbox.stub(cmdexec, "log");
});

afterEach(() => {
sandbox.restore();
fs.rmSync(testDirectory, { recursive: true, force: true });
});

it("reads the compiler bytecode version from the bytecode it emits", () => {
const compiler = createCompiler(`process.stdout.write(Buffer.from("${hermesHeader(98).toString("hex")}", "hex"));`);
assert.strictEqual(getHermesCompilerBytecodeVersion(compiler), 98);
});

it("returns null when the compiler fails or emits something other than Hermes bytecode", () => {
assert.strictEqual(getHermesCompilerBytecodeVersion(createCompiler("process.exit(1);")), null);
assert.strictEqual(getHermesCompilerBytecodeVersion(createCompiler(`process.stdout.write("var a;");`)), null);
assert.strictEqual(getHermesCompilerBytecodeVersion(path.join(testDirectory, "missing-hermesc")), null);
});

it("keeps the base bytecode when versions match", () => {
const bundle = createBundle(hermesHeader(96));
assert.strictEqual(resolveHermesBaseBytecode(bundle, 96), bundle);
sinon.assert.notCalled(log);
});

it("skips the base bytecode and warns when versions differ", () => {
const bundle = createBundle(hermesHeader(96));
assert.strictEqual(resolveHermesBaseBytecode(bundle, 98), null);
sinon.assert.calledWithMatch(log, sinon.match(/v96.*v98/s));
});

it("skips the base bytecode when the compiler version is unknown", () => {
assert.strictEqual(resolveHermesBaseBytecode(createBundle(hermesHeader(96)), null), null);
});

it("skips the base bytecode when the base is not Hermes bytecode", () => {
assert.strictEqual(resolveHermesBaseBytecode(createBundle("__d(function(){});"), 96), null);
assert.strictEqual(resolveHermesBaseBytecode(createBundle(Buffer.from([0xc6, 0x1f])), 96), null);
});
});
Loading