Skip to content
Closed
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
2 changes: 2 additions & 0 deletions packages/debugger-frontend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,5 @@ node scripts/debugger-frontend/sync-and-build --branch 0.73-stable
```

By default, this will clone and build from [react/react-native-devtools-frontend](https://github.com/react/react-native-devtools-frontend).

The updated files are committed on completion, with a generated summary and changelog of the synced revisions.
85 changes: 85 additions & 0 deletions scripts/debugger-frontend/commit-backend.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/**
* 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
*/

/*::
export type Command = [string, Array<string>];

export type CommitMessageParts = Readonly<{
title: string,
summary: string,
changelogTable: string,
changelogEntry: string,
}>;

// Commands are returned rather than run so that the caller can report them
// consistently with the rest of the sync.
export type CommitBackend = Readonly<{
// Must print nothing when the working directory is clean.
status: Command,
// Joined with blank lines; empty entries are dropped.
messageBlocks: (parts: CommitMessageParts) => ReadonlyArray<string>,
commit: (packagePath: string, messageFile: string) => ReadonlyArray<Command>,
}>;

export type ResolveContext = Readonly<{
createDiff: boolean,
noBuild: boolean,
}>;
*/

const GIT /*: CommitBackend */ = {
status: ['git', ['status', '--porcelain', '--', '.']],
messageBlocks: ({title, summary, changelogTable, changelogEntry}) => [
title,
summary,
changelogTable,
`Changelog: ${changelogEntry}`,
],
commit: (packagePath, messageFile) => [
['git', ['add', '-A', '--', packagePath]],
['git', ['commit', '-F', messageFile, '--', packagePath]],
],
};

function moduleExists(modulePath /*: string */) /*: boolean */ {
try {
require.resolve(modulePath);
return true;
} catch {
return false;
}
}

// Resolved before requiring so that a failure to load the module is not
// mistaken for its absence in the open source repo. The filename is
// deliberately not `commit-backend.fb.js`: Flow and Metro resolve `X.fb.js`
// ahead of `X.js`, but Node - which runs this script - does not.
const resolveFb /*: ?(context: ResolveContext) => Promise<?CommitBackend> */ =
moduleExists('./fbsource-backend.fb.js')
? // $FlowFixMe[cannot-resolve-module] - not resolvable in OSS
require('./fbsource-backend.fb.js')
: null;

async function resolveCommitBackend(
context /*: ResolveContext */,
) /*: Promise<CommitBackend> */ {
const fbBackend = await resolveFb?.(context);
if (fbBackend != null) {
return fbBackend;
}
if (context.createDiff) {
throw new Error('--create-diff requires an fbsource checkout');
}
return GIT;
}

module.exports = {
resolveCommitBackend,
};
182 changes: 73 additions & 109 deletions scripts/debugger-frontend/sync-and-build.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
* @format
*/

const {PACKAGES_DIR} = require('../shared/consts');
const {PACKAGES_DIR, REPO_ROOT} = require('../shared/consts');
const {resolveCommitBackend} = require('./commit-backend');
// $FlowFixMe[untyped-import]: TODO type ansi-styles
const ansiStyles = require('ansi-styles');
const {execSync, spawnSync} = require('node:child_process');
Expand Down Expand Up @@ -40,15 +41,7 @@ const config = {
};

/*::
type DiffBaseInfo = {
packagePath: string,
baseGitRevision: string,
};

type ParsedBuildInfo = {
gitRevision: string,
isLocalCheckout: boolean,
};
import type {CommitBackend} from './commit-backend';
*/

async function main() {
Expand Down Expand Up @@ -95,10 +88,12 @@ async function main() {

await checkRequiredTools();
const packagePath = path.join(PACKAGES_DIR, 'debugger-frontend');
let diffBaseInfo;
if (createDiff) {
diffBaseInfo = await checkCanCreateDiff(packagePath);
}
const commitBackend = await resolveCommitBackend({
createDiff: createDiff === true,
noBuild,
});
await checkCanCommit(packagePath, commitBackend);
const baseGitRevision = await readGitRevision(packagePath);
const {checkoutPath} = await buildDebuggerFrontend(
packagePath,
scratchPath,
Expand All @@ -109,14 +104,19 @@ async function main() {
noBuild,
},
);
if (createDiff && diffBaseInfo) {
await createSyncDiff(diffBaseInfo, scratchPath, {checkoutPath, noBuild});
}
await commitSync({
baseGitRevision,
checkoutPath,
commitBackend,
noBuild,
packagePath,
scratchPath,
});
await cleanup(scratchPath, keepScratch === true);
if (!noBuild) {
process.stdout.write(
styleText('green', 'Sync done.') +
' Check in any updated files under packages/debugger-frontend.\n',
' Committed updated files under packages/debugger-frontend.\n',
);
}
}
Expand All @@ -128,15 +128,16 @@ function showHelp() {
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.
If an existing checkout path is provided, builds it instead. The updated files
are committed on completion.

Options:
--branch The DevTools frontend branch to use. Ignored when
providing a local checkout path.
--create-diff Submit the commit as a draft diff (Meta-internal).
--nohooks Don't run gclient hooks in the devtools checkout (useful
for existing checkouts).
--keep-scratch Don't clean up temporary files.
--create-diff Create a diff with the updated files.
--no-build Skip actually building and updating the frontend.
`);
}
Expand Down Expand Up @@ -453,70 +454,36 @@ async function spawnSafe(
}
}

async function checkCanCreateDiff(
async function checkCanCommit(
packagePath /*: string */,
) /*: Promise<DiffBaseInfo> */ {
process.stdout.write('Checking that we can create a diff' + '\n');
try {
const {stdout: hgRootStdout} = await spawnSafe('hg', ['root'], {
cwd: packagePath,
stdio: ['ignore', 'pipe', 'inherit'],
});
const repoRoot = hgRootStdout.toString().trim();
const projectid = (
await fs.readFile(path.join(repoRoot, '.projectid'), 'utf8')
).trim();
if (projectid !== 'fbsource') {
throw new Error(
'Expected .projectid to contain "fbsource" but found: ' + projectid,
);
}
await spawnSafe('jf', ['-v'], {cwd: packagePath, stdio: 'ignore'});
} catch (e) {
process.stderr.write(
'Must be in an fbsource checkout (Meta-only) to create a diff\n',
);
throw e;
}
try {
const {stdout: hgStatusStdout} = await spawnSafe(
'hg',
['status', 'BUILD_INFO'],
{cwd: packagePath, stdio: ['ignore', 'pipe', 'inherit']},
commitBackend /*: CommitBackend */,
) {
process.stdout.write('Checking that we can commit' + '\n');
const [statusCmd, statusArgs] = commitBackend.status;
const {stdout} = await spawnSafe(statusCmd, statusArgs, {
cwd: packagePath,
stdio: ['ignore', 'pipe', 'inherit'],
});
const pendingChanges = stdout.toString().trim();
if (pendingChanges !== '') {
throw new Error(
`Must have a clean working copy under ${path.relative(REPO_ROOT, packagePath)} to commit:\n${pendingChanges}`,
);
if (hgStatusStdout.toString().trim() !== '') {
throw new Error(
'Must have a clean base BUILD_INFO file to create a diff',
);
}
const {gitRevision: baseGitRevision} = await readBuildInfo(packagePath);
return {
packagePath,
baseGitRevision,
};
} catch (e) {
process.stderr.write('Must have a BUILD_INFO file to create a diff\n');
throw e;
}
}

async function readBuildInfo(
async function readGitRevision(
packagePath /*: string*/,
) /*: Promise<ParsedBuildInfo> */ {
) /*: Promise<string> */ {
const buildInfo = await fs.readFile(
path.join(packagePath, 'BUILD_INFO'),
'utf8',
);
const GIT_REV_RE = /^Git revision: ([0-9a-f]{40})/m;
const gitRevision = nullthrows(
return nullthrows(
GIT_REV_RE.exec(buildInfo),
'Could not extract git revision from BUILD_INFO',
)[1];
const isLocalCheckout = !/^Is local checkout: false$/m.test(buildInfo);
return {
isLocalCheckout,
gitRevision,
};
}

function generateChangelogTable(
Expand All @@ -542,7 +509,6 @@ function generateChangelogTable(
const limitedCommits = commits.slice(0, maxCommits);

const tableRows = [
'',
'### Changelog',
'',
'| Commit | Author | Date/Time | Subject |',
Expand Down Expand Up @@ -582,19 +548,26 @@ function generateChangelogTable(
return changelogTable;
}

async function createSyncDiff(
diffBaseInfo /*: DiffBaseInfo */,
scratchPath /*: string */,
async function commitSync(
{
baseGitRevision,
checkoutPath,
commitBackend,
noBuild,
} /*: Readonly<{checkoutPath: string, noBuild: boolean}> */,
packagePath,
scratchPath,
} /*: Readonly<{
baseGitRevision: string,
checkoutPath: string,
commitBackend: CommitBackend,
noBuild: boolean,
packagePath: string,
scratchPath: string,
}> */,
) {
process.stdout.write('Creating a sync diff\n');
const {packagePath, baseGitRevision} = diffBaseInfo;
process.stdout.write('Committing updated files\n');
const baseGitRevisionShort = baseGitRevision.slice(0, 7);
const {gitRevision: newGitRevision, isLocalCheckout} =
await readBuildInfo(packagePath);
const newGitRevision = await readGitRevision(packagePath);
const newGitRevisionShort = newGitRevision.slice(0, 7);

// Generate the changelog table
Expand All @@ -604,39 +577,30 @@ async function createSyncDiff(
newGitRevision,
);

const commitMessage = [
(isLocalCheckout || noBuild ? 'DO NOT LAND ' : '') +
`[RN] Update debugger-frontend from ${baseGitRevisionShort}...${newGitRevisionShort}`,
'',
'Summary:',
`Changelog: [Internal] - Update \`@react-native/debugger-frontend\` from ${baseGitRevisionShort}...${newGitRevisionShort}`,
'',
`Resyncs \`@react-native/debugger-frontend\` from GitHub - see \`rn-chrome-devtools-frontend\` [changelog](${DEVTOOLS_FRONTEND_REPO_URL}/compare/${baseGitRevision}...${newGitRevision}).`,
'',
changelogTable,
'',
'Test Plan: CI',
'',
'Reviewers: #rn-debugging',
'',
'Tags: msdkland[metro]',
'',
].join('\n');
const revisionRange = `${baseGitRevisionShort}...${newGitRevisionShort}`;
const title =
(noBuild ? 'DO NOT LAND ' : '') +
`[RN] Update debugger-frontend from ${revisionRange}`;
const changelogEntry = `[Internal] - Update \`@react-native/debugger-frontend\` from ${revisionRange}`;
const compareUrl = `${DEVTOOLS_FRONTEND_REPO_URL}/compare/${baseGitRevision}...${newGitRevision}`;
const summary =
'Resyncs `@react-native/debugger-frontend` from GitHub - see ' +
'`rn-chrome-devtools-frontend` ' +
`[changelog](${compareUrl}).`;

const commitMessage =
commitBackend
.messageBlocks({title, summary, changelogTable, changelogEntry})
.filter(block => block !== '')
.join('\n\n') + '\n';

const commitMessageFile = path.join(scratchPath, 'commit-msg');
await fs.writeFile(commitMessageFile, commitMessage);
await spawnSafe(
'hg',
['commit', packagePath, '--addremove', '-l', commitMessageFile],
{cwd: packagePath},
);
await spawnSafe('jf', ['submit', '--draft'], {
cwd: packagePath,
});
if (noBuild) {
await spawnSafe('jf', ['action', '--abandon'], {
cwd: packagePath,
});
for (const [cmd, args] of commitBackend.commit(
packagePath,
commitMessageFile,
)) {
await spawnSafe(cmd, args, {cwd: packagePath});
}
}

Expand Down
Loading