Files
react-native/scripts/debugger-frontend/sync-and-build.js
T
Alex Hunt 80ec096e7c Create shared consts module for scripts
Summary:
Tidy up, removes risk of breakage when relocating inner modules.

Changelog: [Internal]

Reviewed By: lunaleaps

Differential Revision: D53813025

fbshipit-source-id: 43dc4a57191363546d6858cff311e260d416192f
2024-02-16 03:21:25 -08:00

388 lines
11 KiB
JavaScript

/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall react_native
*/
const {PACKAGES_DIR} = require('../consts');
const {parseArgs} = require('@pkgjs/parseargs');
// $FlowFixMe[untyped-import]: TODO type ansi-styles
const ansiStyles = require('ansi-styles');
const chalk = require('chalk');
const {execSync, spawnSync} = require('child_process');
const {promises: fs} = require('fs');
const {hostname, tmpdir, userInfo} = require('os');
const path = require('path');
// $FlowFixMe[untyped-import]: TODO type rimraf
const rimraf = require('rimraf');
// $FlowFixMe[untyped-import]: TODO type signedsource
const SignedSource = require('signedsource');
// $FlowFixMe[untyped-import]: TODO type supports-color
const supportsColor = require('supports-color');
const DEVTOOLS_FRONTEND_REPO_URL =
'https://github.com/facebookexperimental/rn-chrome-devtools-frontend';
const config = {
allowPositionals: true,
options: {
branch: {type: 'string'},
'keep-scratch': {type: 'boolean'},
nohooks: {type: 'boolean'},
help: {type: 'boolean'},
},
};
async function main() {
const {
positionals,
values: {help, branch, nohooks, 'keep-scratch': keepScratch},
} = parseArgs(config);
if (help === true) {
showHelp();
process.exitCode = 0;
return;
}
const localCheckoutPath = positionals?.[0];
if (branch == null && !localCheckoutPath?.length) {
console.error(chalk.red('Error: Missing option --branch'));
showHelp();
process.exitCode = 1;
return;
}
console.log('\n' + chalk.bold.inverse('Syncing debugger-frontend') + '\n');
const scratchPath = await fs.mkdtemp(
path.join(tmpdir(), 'debugger-frontend-build-'),
);
process.stdout.write(chalk.dim(`Scratch path: ${scratchPath}\n\n`));
await checkRequiredTools();
await buildDebuggerFrontend(scratchPath, localCheckoutPath, {
branch: branch ?? '',
gclientSyncOptions: {nohooks: nohooks === true},
});
await cleanup(scratchPath, keepScratch === true);
process.stdout.write(
chalk.green('Sync done.') +
' Check in any updated files under packages/debugger-frontend.\n',
);
}
function showHelp() {
console.log(`
Usage: node scripts/debugger-frontend/sync-and-build [OPTIONS] [checkout path]
Sync and build the debugger frontend into @react-native/debugger-frontend.
By default, checks out the currently pinned revision of the DevTools frontend.
If an existing checkout path is provided, builds it instead.
Options:
--branch The DevTools frontend branch to use. Ignored when
providing a local checkout path.
--nohooks Don't run gclient hooks in the devtools checkout (useful
for existing checkouts).
--keep-scratch Don't clean up temporary files.
`);
}
async function checkRequiredTools() {
process.stdout.write('Checking that required tools are available' + '\n');
await spawnSafe('git', ['--version'], {stdio: 'ignore'});
try {
await spawnSafe('gclient', ['--version'], {stdio: 'ignore'});
await spawnSafe('which', ['gn'], {stdio: 'ignore'});
await spawnSafe('which', ['autoninja'], {stdio: 'ignore'});
} catch (e) {
process.stderr.write(
'Install depot_tools first: ' +
'https://commondatastorage.googleapis.com/chrome-infra-docs/flat/depot_tools/docs/html/depot_tools_tutorial.html#_setting_up' +
'\n',
);
throw e;
}
process.stdout.write('\n');
}
async function buildDebuggerFrontend(
scratchPath /*: string */,
localCheckoutPath /*: ?string */,
{branch, gclientSyncOptions} /*: $ReadOnly<{
branch: string,
gclientSyncOptions: $ReadOnly<{nohooks: boolean}>,
}>*/,
) {
let checkoutPath;
if (localCheckoutPath == null) {
const scratchCheckoutPath = path.join(scratchPath, 'devtools-frontend');
await fs.mkdir(scratchPath, {recursive: true});
await checkoutDevToolsFrontend(scratchCheckoutPath, branch);
checkoutPath = scratchCheckoutPath;
} else {
checkoutPath = localCheckoutPath;
}
await setupGclientWorkspace(scratchPath, checkoutPath, gclientSyncOptions);
const {buildPath, gnArgsSummary} = await performReleaseBuild(checkoutPath);
const packagePath = path.join(PACKAGES_DIR, 'debugger-frontend');
const destPathInPackage = path.join(packagePath, 'dist', 'third-party');
await cleanPackageFiles(destPathInPackage);
await copyFrontendFilesToPackage(buildPath, destPathInPackage);
await copyLicenseToPackage(checkoutPath, destPathInPackage);
await generateBuildInfo({
checkoutPath,
packagePath,
branch,
isLocalCheckout: localCheckoutPath != null,
gclientSyncOptions,
gnArgsSummary,
});
}
async function checkoutDevToolsFrontend(
checkoutPath /*: string */,
branch /*: string */,
) {
process.stdout.write('Checking out devtools-frontend\n');
await fs.mkdir(checkoutPath, {recursive: true});
await spawnSafe('git', [
'clone',
DEVTOOLS_FRONTEND_REPO_URL,
'--branch',
branch,
'--single-branch',
'--depth',
'1',
checkoutPath,
]);
process.stdout.write('\n');
}
async function setupGclientWorkspace(
scratchPath /*: string */,
checkoutPath /*: string */,
{nohooks} /*: $ReadOnly<{nohooks: boolean}> */,
) {
process.stdout.write('Setting up gclient workspace' + '\n');
await spawnSafe(
'gclient',
['config', '--unmanaged', checkoutPath, '--name', 'devtools-frontend'],
{
cwd: scratchPath,
},
);
await spawnSafe(
'gclient',
['sync', '--no-history', ...(nohooks ? ['--nohooks'] : [])],
{
env: {
...process.env,
DEPOT_TOOLS_UPDATE: '0',
},
cwd: scratchPath,
},
);
process.stdout.write('\n');
}
async function performReleaseBuild(
checkoutPath /*: string */,
) /*: Promise<{buildPath: string, gnArgsSummary: string}> */ {
process.stdout.write('Performing release build of devtools-frontend' + '\n');
const buildPath = path.join(checkoutPath, 'out/Release');
await fs.mkdir(buildPath, {recursive: true});
await fs.writeFile(
path.join(buildPath, 'args.gn'),
// NOTE: Per the DevTools repo's documentation, is_official_build has nothing
// to do with branding and only controls certain release build optimisations.
'is_official_build=true\n',
);
await spawnSafe('gn', ['gen', 'out/Release'], {
cwd: checkoutPath,
});
const {stdout: gnArgsStdout} = await spawnSafe(
'gn',
['args', 'out/Release', '--list', '--short', '--overrides-only'],
{
cwd: checkoutPath,
stdio: ['ignore', 'pipe', 'inherit'],
},
);
const gnArgsSummary = gnArgsStdout.toString().trim();
process.stdout.write(chalk.dim(gnArgsSummary) + '\n');
await spawnSafe('autoninja', ['-C', 'out/Release'], {cwd: checkoutPath});
process.stdout.write('\n');
return {gnArgsSummary, buildPath};
}
async function cleanPackageFiles(destPathInPackage /*: string */) {
process.stdout.write(
'Cleaning stale generated files in debugger-frontend' + '\n',
);
rimraf.sync(destPathInPackage);
process.stdout.write('\n');
}
async function copyFrontendFilesToPackage(
buildPath /*: string */,
destPathInPackage /*: string */,
) {
process.stdout.write(
'Copying built devtools-frontend files to debugger-frontend' + '\n\n',
);
// The DevTools build generates a manifest of all files meant for packaging
// into Chrome. These are exactly the files we need to ship.
const files = JSON.parse(
await fs.readFile(
path.join(buildPath, 'gen', 'input_grd_files.json'),
'utf8',
),
);
await Promise.all(
files.map(async file => {
const destPath = path.join(destPathInPackage, file);
const destDir = path.dirname(destPath);
await fs.mkdir(destDir, {recursive: true});
await fs.copyFile(path.join(buildPath, 'gen', file), destPath);
}),
);
}
async function copyLicenseToPackage(
checkoutPath /*: string */,
destPathInPackage /*: string */,
) {
process.stdout.write(
'Copying LICENSE from devtools-frontend to debugger-frontend package\n\n',
);
await fs.copyFile(
path.join(checkoutPath, 'LICENSE'),
path.join(destPathInPackage, 'LICENSE'),
);
}
async function generateBuildInfo(
info /*: $ReadOnly<{
checkoutPath: string,
isLocalCheckout: boolean,
branch: string,
packagePath: string,
gclientSyncOptions: $ReadOnly<{nohooks: boolean}>,
gnArgsSummary: string,
}> */,
) {
process.stdout.write('Generating BUILD_INFO for debugger-frontend\n\n');
const gitStatusLines = execSync('git status --porcelain', {
cwd: info.checkoutPath,
encoding: 'utf-8',
})
.trim()
.split('\n')
.map(line => line.trim())
.filter(line => line.length > 0)
.map(line => ' ' + line);
if (!gitStatusLines.length) {
gitStatusLines.push(' <no changes>');
}
const gnSummaryLines = info.gnArgsSummary
.split('\n')
.map(line => ' ' + line.trim());
if (!gnSummaryLines.length) {
gnSummaryLines.push(' <none>');
}
const contents = [
SignedSource.getSigningToken(),
'Git revision: ' +
execSync('git rev-parse HEAD', {
cwd: info.checkoutPath,
encoding: 'utf-8',
}).trim(),
'Built with --nohooks: ' + String(info.gclientSyncOptions.nohooks),
'Is local checkout: ' + String(info.isLocalCheckout),
...(!info.isLocalCheckout
? [
'Remote URL: ' + DEVTOOLS_FRONTEND_REPO_URL,
'Remote branch: ' + info.branch,
]
: ['Hostname: ' + hostname(), 'User: ' + userInfo().username]),
'GN build args (overrides only): ',
...gnSummaryLines,
'Git status in checkout:',
...gitStatusLines,
'',
].join('\n');
await fs.writeFile(
path.join(info.packagePath, 'BUILD_INFO'),
SignedSource.signFile(contents),
);
}
async function cleanup(scratchPath /*: string */, keepScratch /*: boolean */) {
if (!keepScratch) {
process.stdout.write('Cleaning up temporary files\n\n');
await rimraf.sync(scratchPath);
} else {
process.stdout.write(
'Not cleaning up temporary files because of --keep-scratch\n\n',
);
}
}
async function spawnSafe(
cmd /*: string */,
args /*: Array<string> */ = [],
opts /*: child_process$spawnSyncOpts */ = {},
) /*: Promise<{
stdout: string | Buffer,
stderr: string | Buffer,
}> */ {
process.stdout.write(` > ${cmd} ${args.join(' ')}\n`);
if (supportsColor.stdout) {
process.stdout.write(ansiStyles.dim.open);
}
if (supportsColor.stderr) {
process.stderr.write(ansiStyles.dim.open);
}
try {
const {error, status, signal, stdout, stderr} = spawnSync(cmd, args, {
stdio: ['ignore', 'inherit', 'inherit'],
...opts,
});
if (error) {
throw error;
}
if (status != null && status !== 0) {
throw new Error(`Command failed with exit code ${status}`);
}
if (signal != null) {
throw new Error(`Command terminated by signal ${signal}`);
}
return {stdout, stderr};
} finally {
if (supportsColor.stdout) {
process.stdout.write(ansiStyles.dim.close);
}
if (supportsColor.stderr) {
process.stderr.write(ansiStyles.dim.close);
}
}
}
if (require.main === module) {
// eslint-disable-next-line no-void
void main();
}