Skip to content

Commit 8bc3d9c

Browse files
authored
feat(cli-repl): add dependency info to --build-info MONGOSH-1286 (#1492)
… and add a built-in `builtInfo()` function that also returns this information.
1 parent 040fafa commit 8bc3d9c

File tree

7 files changed

+128
-19
lines changed

7 files changed

+128
-19
lines changed

packages/cli-repl/src/build-info.ts

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
1-
export type BuildInfo = {
1+
import os from 'os';
2+
import { CliServiceProvider } from '@mongosh/service-provider-server';
3+
import { getCryptLibraryPaths } from './crypt-library-paths';
4+
5+
export interface BuildInfo {
26
version: string;
37
nodeVersion: string;
48
distributionKind: 'unpackaged' | 'packaged' | 'compiled';
9+
runtimeArch: typeof process['arch'];
10+
runtimePlatform: typeof process['platform'];
511
buildArch: typeof process['arch'];
612
buildPlatform: typeof process['platform'];
713
buildTarget: string;
@@ -10,18 +16,64 @@ export type BuildInfo = {
1016
opensslVersion: string;
1117
sharedOpenssl: boolean;
1218
segmentApiKey?: string;
13-
};
19+
deps: ReturnType<typeof CliServiceProvider.getVersionInformation> & {
20+
cryptSharedLibraryVersion?: string;
21+
}
22+
}
23+
24+
function getSystemArch(): typeof process['arch'] {
25+
return process.platform === 'darwin'
26+
? os.cpus().some((cpu) => {
27+
// process.arch / os.arch() will return the arch for which the node
28+
// binary was compiled. Checking if one of the CPUs has Apple in its
29+
// name is the way to check (there is slight difference between the
30+
// earliest models naming and a current one, so we check only for
31+
// Apple in the name)
32+
return /Apple/.test(cpu.model);
33+
})
34+
? 'arm64'
35+
: 'x64'
36+
: process.arch;
37+
}
1438

1539
/**
1640
* Return an object with information about this mongosh instance,
1741
* in particular, when it was built and how.
1842
*/
19-
export function buildInfo({ withSegmentApiKey }: { withSegmentApiKey?: boolean } = {}): BuildInfo {
43+
export async function buildInfo({
44+
withSegmentApiKey,
45+
withCryptSharedVersionInfo,
46+
}: {
47+
withSegmentApiKey?: boolean,
48+
withCryptSharedVersionInfo?: boolean,
49+
} = {}): Promise<BuildInfo> {
50+
const dependencyVersionInfo: BuildInfo['deps'] = {
51+
...CliServiceProvider.getVersionInformation()
52+
};
53+
try {
54+
if (withCryptSharedVersionInfo) {
55+
const version = (await getCryptLibraryPaths()).expectedVersion?.versionStr;
56+
if (version) {
57+
dependencyVersionInfo.cryptSharedLibraryVersion = version;
58+
}
59+
}
60+
} catch {
61+
/* ignore */
62+
}
63+
2064
const runtimeData = {
2165
nodeVersion: process.version,
2266
opensslVersion: process.versions.openssl,
23-
sharedOpenssl: !!process.config.variables.node_shared_openssl
67+
sharedOpenssl: !!process.config.variables.node_shared_openssl,
68+
// Runtime arch can differ e.g. because x64 binaries can run
69+
// on M1 cpus
70+
runtimeArch: getSystemArch(),
71+
// Runtime platform can differ e.g. because homebrew on macOS uses
72+
// npm packages published from Linux
73+
runtimePlatform: process.platform,
74+
deps: { ...dependencyVersionInfo }
2475
};
76+
2577
try {
2678
const buildInfo = { ...require('./build-info.json'), ...runtimeData };
2779
if (!withSegmentApiKey) {

packages/cli-repl/src/cli-repl.ts

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -229,12 +229,12 @@ export class CliRepl implements MongoshIOProvider {
229229
logger.info('MONGOSH', mongoLogId(1_000_000_000), 'log', 'Starting log', {
230230
execPath: process.execPath,
231231
envInfo: redactSensitiveData(this.getLoggedEnvironmentVariables()),
232-
...buildInfo()
232+
...await buildInfo({ withCryptSharedVersionInfo: true })
233233
});
234234

235235
let analyticsSetupError: Error | null = null;
236236
try {
237-
this.setupAnalytics();
237+
await this.setupAnalytics();
238238
} catch (err: any) {
239239
// Need to delay emitting the error on the bus so that logging is in place
240240
// as well
@@ -304,6 +304,7 @@ export class CliRepl implements MongoshIOProvider {
304304
throw err;
305305
}
306306
const initialized = await this.mongoshRepl.initialize(initialServiceProvider);
307+
this.injectReplFunctions();
307308

308309
const commandLineLoadFiles = this.cliOptions.fileNames ?? [];
309310
const evalScripts = this.cliOptions.eval ?? [];
@@ -372,12 +373,28 @@ export class CliRepl implements MongoshIOProvider {
372373
await this.mongoshRepl.startRepl(initialized);
373374
}
374375

375-
setupAnalytics(): void {
376+
injectReplFunctions(): void {
377+
const functions = {
378+
async buildInfo() {
379+
return await buildInfo({ withCryptSharedVersionInfo: true });
380+
}
381+
} as const;
382+
const { context } = this.mongoshRepl.runtimeState().repl;
383+
for (const [name, impl] of Object.entries(functions)) {
384+
context[name] = (...args: Parameters<typeof impl>) => {
385+
return Object.assign(impl(...args), {
386+
[Symbol.for('@@mongosh.syntheticPromise')]: true
387+
});
388+
};
389+
}
390+
}
391+
392+
async setupAnalytics(): Promise<void> {
376393
if (process.env.IS_MONGOSH_EVERGREEN_CI && !this.analyticsOptions?.alwaysEnable) {
377394
throw new Error('no analytics setup for the mongosh CI environment');
378395
}
379396
// build-info.json is created as a part of the release process
380-
const apiKey = this.analyticsOptions?.apiKey ?? buildInfo({ withSegmentApiKey: true }).segmentApiKey;
397+
const apiKey = this.analyticsOptions?.apiKey ?? (await buildInfo({ withSegmentApiKey: true })).segmentApiKey;
381398
if (!apiKey) {
382399
throw new Error('no analytics API key defined');
383400
}

packages/cli-repl/src/crypt-library-paths.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/* eslint-disable chai-friendly/no-unused-expressions */
12
import path from 'path';
23
import { promises as fs, constants as fsConstants } from 'fs';
34
import type { MongoshBus } from '@mongosh/types';
@@ -17,7 +18,7 @@ export interface CryptLibraryPathResult {
1718
* that we are supposed to use.
1819
*/
1920
export async function getCryptLibraryPaths(
20-
bus: MongoshBus,
21+
bus: MongoshBus | undefined = undefined,
2122
pretendProcessExecPathForTesting: string | undefined = undefined): Promise<CryptLibraryPathResult> {
2223
const execPath = pretendProcessExecPathForTesting ?? process.execPath;
2324

@@ -41,7 +42,7 @@ export async function getCryptLibraryPaths(
4142
try {
4243
const permissionsMismatch = await ensureMatchingPermissions(libraryCandidate, execPathStat);
4344
if (permissionsMismatch) {
44-
bus.emit('mongosh:crypt-library-load-skip', {
45+
bus?.emit?.('mongosh:crypt-library-load-skip', {
4546
cryptSharedLibPath: libraryCandidate,
4647
reason: 'permissions mismatch',
4748
details: permissionsMismatch
@@ -54,17 +55,17 @@ export async function getCryptLibraryPaths(
5455
cryptSharedLibPath: libraryCandidate,
5556
expectedVersion: version
5657
};
57-
bus.emit('mongosh:crypt-library-load-found', result);
58+
bus?.emit?.('mongosh:crypt-library-load-found', result);
5859
return result;
5960
} catch (err: any) {
60-
bus.emit('mongosh:crypt-library-load-skip', {
61+
bus?.emit?.('mongosh:crypt-library-load-skip', {
6162
cryptSharedLibPath: libraryCandidate,
6263
reason: err.message
6364
});
6465
}
6566
}
6667
} else {
67-
bus.emit('mongosh:crypt-library-load-skip', {
68+
bus?.emit?.('mongosh:crypt-library-load-skip', {
6869
cryptSharedLibPath: '',
6970
reason: 'Skipping CSFLE library searching because this is not a single-executable mongosh'
7071
});

packages/cli-repl/src/run.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,12 +72,12 @@ import net from 'net';
7272

7373
if (options.version) {
7474
// eslint-disable-next-line no-console
75-
console.log(buildInfo().version);
75+
console.log((await buildInfo()).version);
7676
return;
7777
}
7878
if (options.buildInfo) {
7979
// eslint-disable-next-line no-console
80-
console.log(JSON.stringify(buildInfo(), null, ' '));
80+
console.log(JSON.stringify(await buildInfo({ withCryptSharedVersionInfo: true }), null, ' '));
8181
return;
8282
}
8383
if (options.smokeTests) {

packages/cli-repl/src/smoke-tests.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export async function runSmokeTests(smokeTestServer: string | undefined, executa
2121
assert(!!smokeTestServer, 'Make sure MONGOSH_SMOKE_TEST_SERVER is set in CI');
2222
}
2323

24-
const expectFipsSupport = !!process.env.MONGOSH_SMOKE_TEST_OS_HAS_FIPS_SUPPORT && buildInfo().sharedOpenssl;
24+
const expectFipsSupport = !!process.env.MONGOSH_SMOKE_TEST_OS_HAS_FIPS_SUPPORT && (await buildInfo()).sharedOpenssl;
2525
console.log('FIPS support required to pass?', { expectFipsSupport });
2626

2727
for (const { input, output, testArgs, includeStderr, exitCode } of [{

packages/cli-repl/test/e2e.spec.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,17 @@ describe('e2e', function() {
4040
expect(Object.keys(data)).to.deep.equal([
4141
'version', 'distributionKind', 'buildArch', 'buildPlatform',
4242
'buildTarget', 'buildTime', 'gitVersion', 'nodeVersion',
43-
'opensslVersion', 'sharedOpenssl'
43+
'opensslVersion', 'sharedOpenssl', 'runtimeArch', 'runtimePlatform',
44+
'deps'
4445
]);
4546
expect(data.version).to.be.a('string');
4647
expect(data.nodeVersion).to.be.a('string');
4748
expect(data.distributionKind).to.be.a('string');
4849
expect(['unpackaged', 'packaged', 'compiled'].includes(data.distributionKind)).to.be.true;
4950
expect(data.buildArch).to.be.a('string');
5051
expect(data.buildPlatform).to.be.a('string');
52+
expect(data.runtimeArch).to.be.a('string');
53+
expect(data.runtimePlatform).to.be.a('string');
5154
expect(data.opensslVersion).to.be.a('string');
5255
expect(data.sharedOpenssl).to.be.a('boolean');
5356
if (data.distributionKind !== 'unpackaged') {
@@ -57,6 +60,19 @@ describe('e2e', function() {
5760
expect(data.buildTime).to.equal(null);
5861
expect(data.gitVersion).to.equal(null);
5962
}
63+
expect(data.deps.nodeDriverVersion).to.be.a('string');
64+
expect(data.deps.libmongocryptVersion).to.be.a('string');
65+
expect(data.deps.libmongocryptNodeBindingsVersion).to.be.a('string');
66+
});
67+
68+
it('provides build info via the buildInfo() builtin', async() => {
69+
const shell = TestShell.start({ args: [ '--quiet', '--eval', 'JSON.stringify(buildInfo().deps)', '--nodb' ] });
70+
await shell.waitForExit();
71+
shell.assertNoErrors();
72+
const deps = JSON.parse(shell.output);
73+
expect(deps.nodeDriverVersion).to.be.a('string');
74+
expect(deps.libmongocryptVersion).to.be.a('string');
75+
expect(deps.libmongocryptNodeBindingsVersion).to.be.a('string');
6076
});
6177
});
6278

packages/service-provider-server/src/cli-service-provider.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,12 @@ function normalizeEndpointAndAuthConfiguration(
151151
];
152152
}
153153

154+
interface DependencyVersionInfo {
155+
nodeDriverVersion?: string;
156+
libmongocryptVersion?: string;
157+
libmongocryptNodeBindingsVersion?: string;
158+
}
159+
154160
/**
155161
* Encapsulates logic for the service provider for the mongosh CLI.
156162
*/
@@ -242,15 +248,32 @@ class CliServiceProvider extends ServiceProviderCore implements ServiceProvider
242248
this.currentClientOptions = clientOptions;
243249
this.baseCmdOptions = { ... DEFAULT_BASE_OPTIONS }; // currently do not have any user-specified connection-wide command options, but I imagine we will eventually
244250
this.dbcache = new WeakMap();
251+
this.fle = CliServiceProvider.getLibmongocryptBindings();
252+
}
253+
254+
private static getLibmongocryptBindings(): FLE | undefined {
245255
try {
246256
// The .extension() call may seem unnecessary, since that is the default
247257
// for the top-level exports from mongodb-client-encryption anyway.
248258
// However, for the browser runtime, we externalize mongodb-client-encryption
249259
// since it is a native addon package; that means that if 'mongodb' is
250260
// included in the bundle, it won't be able to find it, and instead needs
251261
// to receive it as an explicitly passed dependency.
252-
this.fle = require('mongodb-client-encryption').extension(require('mongodb'));
253-
} catch { /* not empty */ }
262+
return require('mongodb-client-encryption').extension(require('mongodb'));
263+
} catch {
264+
return undefined;
265+
}
266+
}
267+
268+
static getVersionInformation(): DependencyVersionInfo {
269+
function tryCall<Fn extends() => any>(fn: Fn): ReturnType<Fn> | undefined {
270+
try { return fn(); } catch { return; }
271+
}
272+
return {
273+
nodeDriverVersion: tryCall(() => require('mongodb/package.json').version),
274+
libmongocryptVersion: tryCall(() => this.getLibmongocryptBindings()?.ClientEncryption.libmongocryptVersion),
275+
libmongocryptNodeBindingsVersion: tryCall(() => require('mongodb-client-encryption/package.json').version),
276+
};
254277
}
255278

256279
async getNewConnection(uri: string, options: Partial<DevtoolsConnectOptions> = {}): Promise<CliServiceProvider> {

0 commit comments

Comments
 (0)