diff --git a/profiler-cli/src/daemon.ts b/profiler-cli/src/daemon.ts index baf0e2b5e0..4a54b6e664 100644 --- a/profiler-cli/src/daemon.ts +++ b/profiler-cli/src/daemon.ts @@ -32,6 +32,7 @@ import { writeStartupError, } from './session'; import { + describeProfileVersionError, describeSessionDirFailure, describeSocketListenError, describeStaleSocketFailure, @@ -58,12 +59,10 @@ function formatProfileLoadError(error: unknown): string { error instanceof ProfileVersionError || (error instanceof Error && error.name === 'ProfileVersionError') ) { - const versionError = error as ProfileVersionError; - return ( - `This profile is version ${versionError.profileVersion}, but this profiler-cli only ` + - `supports up to version ${versionError.supportedVersion} of the ${versionError.formatName} profile format.\n` + - `Update to the latest version with:\n` + - ` npm install -g ${PACKAGE_NAME}@latest` + return describeProfileVersionError( + error as ProfileVersionError, + PACKAGE_NAME, + process.argv[1] ); } return error instanceof Error ? error.message : String(error); diff --git a/profiler-cli/src/diagnostics.ts b/profiler-cli/src/diagnostics.ts index 1f30b55739..d2dacd3474 100644 --- a/profiler-cli/src/diagnostics.ts +++ b/profiler-cli/src/diagnostics.ts @@ -15,6 +15,7 @@ import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; +import type { ProfileVersionError } from 'firefox-profiler/profile-logic/errors'; /** * Advice shown whenever the failure looks like a sandbox restriction on the @@ -346,3 +347,58 @@ export function indentBlock(text: string, prefix: string = ' '): string { .map((line) => `${prefix}${line}`) .join('\n'); } + +/** + * Where `./mach` keeps its downloaded tools, including the copies of samply + * and profiler-cli that Firefox's bootstrap installs. + */ +export function getMozbuildStatePath(): string { + return ( + process.env.MOZBUILD_STATE_PATH || path.join(os.homedir(), '.mozbuild') + ); +} + +function realpathOrSelf(filePath: string): string { + try { + return fs.realpathSync(filePath); + } catch { + return path.resolve(filePath); + } +} + +/** + * Whether the profiler-cli script at `scriptPath` is the copy that + * `./mach bootstrap` installed from Firefox's in-tree toolchain. + */ +export function isInstalledByMachBootstrap(scriptPath: string): boolean { + const toolDir = realpathOrSelf( + path.join(getMozbuildStatePath(), 'profiler-cli') + ); + const relative = path.relative(toolDir, realpathOrSelf(scriptPath)); + return ( + relative !== '' && !relative.startsWith('..') && !path.isAbsolute(relative) + ); +} + +/** + * Explain that a profile is too new for this build and how to update it. + * The bootstrapped copy can only be updated through "./mach bootstrap". + */ +export function describeProfileVersionError( + error: ProfileVersionError, + packageName: string, + scriptPath: string +): string { + const versionLine = `This profile is version ${error.profileVersion}, but this profiler-cli only supports up to version ${error.supportedVersion} of the ${error.formatName} profile format.`; + if (isInstalledByMachBootstrap(scriptPath)) { + return [ + versionLine, + 'This profiler-cli was installed by "./mach bootstrap". Update your Firefox checkout to the latest main branch and run "./mach bootstrap" again.', + ].join('\n'); + } + return [ + versionLine, + 'Update to the latest version with:', + ` npm install -g ${packageName}@latest`, + ].join('\n'); +} diff --git a/profiler-cli/src/samply.ts b/profiler-cli/src/samply.ts index 8e036713d1..3b619e51f6 100644 --- a/profiler-cli/src/samply.ts +++ b/profiler-cli/src/samply.ts @@ -10,8 +10,8 @@ import * as child_process from 'child_process'; import * as fs from 'fs'; -import * as os from 'os'; import * as path from 'path'; +import { getMozbuildStatePath } from './diagnostics'; export type SamplyServer = { binaryPath: string; @@ -37,16 +37,6 @@ function isExecutableFile(filePath: string): boolean { } } -/** - * Where `./mach` keeps its downloaded tools. Firefox's bootstrap installs - * samply there, so it is a good fallback when samply is not on PATH. - */ -function getMozbuildStatePath(): string { - return ( - process.env.MOZBUILD_STATE_PATH || path.join(os.homedir(), '.mozbuild') - ); -} - /** * Resolve the samply binary: PROFILER_CLI_SAMPLY_PATH if set, then PATH, then * the mozbuild state directory. Returns null when none of them has it. An diff --git a/profiler-cli/src/test/unit/diagnostics.test.ts b/profiler-cli/src/test/unit/diagnostics.test.ts index 6534c23313..4fc3602dbc 100644 --- a/profiler-cli/src/test/unit/diagnostics.test.ts +++ b/profiler-cli/src/test/unit/diagnostics.test.ts @@ -12,6 +12,7 @@ import * as os from 'os'; import * as path from 'path'; import { assertSocketPathUsable, + describeProfileVersionError, describeSessionDirFailure, describeSocketConnectError, describeSocketListenError, @@ -20,6 +21,8 @@ import { getErrnoCode, indentBlock, } from '../../diagnostics'; +import { ProfileVersionError } from 'firefox-profiler/profile-logic/errors'; +import { snapshotEnv } from '../integration/fake-samply'; function errnoError(code: string, message: string): NodeJS.ErrnoException { const error: NodeJS.ErrnoException = new Error(message); @@ -229,4 +232,51 @@ describe('profiler-cli diagnostics', function () { expect(indentBlock('a\nb')).toBe(' a\n b'); }); }); + + describe('describeProfileVersionError', function () { + const PACKAGE = '@firefox-devtools/profiler-cli'; + const error = new ProfileVersionError('processed', 78, 77); + let restoreEnv: () => void; + + beforeEach(function () { + restoreEnv = snapshotEnv(['MOZBUILD_STATE_PATH']); + process.env.MOZBUILD_STATE_PATH = path.join(tmpDir, 'mozbuild'); + }); + + afterEach(function () { + restoreEnv(); + }); + + it('points npm installs at npm', function () { + const message = describeProfileVersionError( + error, + PACKAGE, + path.join(tmpDir, 'npm', 'profiler-cli', 'dist', 'profiler-cli.js') + ); + expect(message).toContain('version 78'); + expect(message).toContain('up to version 77 of the processed'); + expect(message).toContain(`npm install -g ${PACKAGE}@latest`); + expect(message).not.toContain('mach bootstrap'); + }); + + it('points bootstrapped installs at mach bootstrap', function () { + const message = describeProfileVersionError( + error, + PACKAGE, + path.join(tmpDir, 'mozbuild', 'profiler-cli', 'dist', 'profiler-cli.js') + ); + expect(message).toContain('version 78'); + expect(message).toContain('./mach bootstrap'); + expect(message).not.toContain('npm install -g'); + }); + + it('does not treat a sibling directory of the bootstrapped copy as bootstrapped', function () { + const message = describeProfileVersionError( + error, + PACKAGE, + path.join(tmpDir, 'mozbuild', 'profiler-cli-old', 'profiler-cli.js') + ); + expect(message).not.toContain('mach bootstrap'); + }); + }); });