Skip to content

Commit af9a3e7

Browse files
chore(NODE-6726): make install libmongocrypt script faster (#66)
1 parent 38e84d2 commit af9a3e7

File tree

4 files changed

+120
-85
lines changed

4 files changed

+120
-85
lines changed

Diff for: .github/docker/Dockerfile.glibc

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ ENV PATH=$PATH:/nodejs/bin
1111
WORKDIR /mongodb-client-encryption
1212
COPY . .
1313

14-
RUN apt-get -qq update && apt-get -qq install -y python3 build-essential && ldd --version
14+
RUN apt-get -qq update && apt-get -qq install -y python3 build-essential git && ldd --version
1515

1616
RUN npm run install:libmongocrypt
1717

Diff for: .github/scripts/get-commit-from-ref.sh

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#! /usr/bin/env bash
2+
set -o errexit
3+
4+
git clone https://github.com/mongodb/libmongocrypt.git _libmongocrypt
5+
cd _libmongocrypt
6+
7+
git checkout --detach $REF
8+
9+
COMMIT_HASH=$(git rev-parse HEAD)
10+
11+
echo "COMMIT_HASH=$COMMIT_HASH"
12+
13+
cd -
14+
rm -rf _libmongocrypt

Diff for: .github/scripts/libmongocrypt.mjs

+16-84
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,7 @@ import events from 'node:events';
88
import path from 'node:path';
99
import https from 'node:https';
1010
import stream from 'node:stream/promises';
11-
import url from 'node:url';
12-
13-
const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
14-
15-
/** Resolves to the root of this repository */
16-
function resolveRoot(...paths) {
17-
return path.resolve(__dirname, '..', '..', ...paths);
18-
}
11+
import { buildLibmongocryptDownloadUrl, getLibmongocryptPrebuildName, resolveRoot, run } from './utils.mjs';
1912

2013
async function parseArguments() {
2114
const pkg = JSON.parse(await fs.readFile(resolveRoot('package.json'), 'utf8'));
@@ -26,7 +19,6 @@ async function parseArguments() {
2619
clean: { short: 'c', type: 'boolean', default: false },
2720
build: { short: 'b', type: 'boolean', default: false },
2821
dynamic: { type: 'boolean', default: false },
29-
fastDownload: { type: 'boolean', default: false }, // Potentially incorrect download, only for the brave and impatient
3022
'skip-bindings': { type: 'boolean', default: false },
3123
help: { short: 'h', type: 'boolean', default: false }
3224
};
@@ -46,7 +38,6 @@ async function parseArguments() {
4638
return {
4739
url: args.values.gitURL,
4840
ref: args.values.libVersion,
49-
fastDownload: args.values.fastDownload,
5041
clean: args.values.clean,
5142
build: args.values.build,
5243
dynamic: args.values.dynamic,
@@ -55,26 +46,6 @@ async function parseArguments() {
5546
};
5647
}
5748

58-
/** `xtrace` style command runner, uses spawn so that stdio is inherited */
59-
async function run(command, args = [], options = {}) {
60-
const commandDetails = `+ ${command} ${args.join(' ')}${options.cwd ? ` (in: ${options.cwd})` : ''}`;
61-
console.error(commandDetails);
62-
const proc = child_process.spawn(command, args, {
63-
shell: process.platform === 'win32',
64-
stdio: 'inherit',
65-
cwd: resolveRoot('.'),
66-
...options
67-
});
68-
await events.once(proc, 'exit');
69-
70-
if (proc.exitCode != 0) throw new Error(`CRASH(${proc.exitCode}): ${commandDetails}`);
71-
}
72-
73-
/** CLI flag maker: `toFlags({a: 1, b: 2})` yields `['-a=1', '-b=2']` */
74-
function toFlags(object) {
75-
return Array.from(Object.entries(object)).map(([k, v]) => `-${k}=${v}`);
76-
}
77-
7849
export async function cloneLibMongoCrypt(libmongocryptRoot, { url, ref }) {
7950
console.error('fetching libmongocrypt...', { url, ref });
8051
await fs.rm(libmongocryptRoot, { recursive: true, force: true });
@@ -87,14 +58,19 @@ export async function cloneLibMongoCrypt(libmongocryptRoot, { url, ref }) {
8758
}
8859

8960
export async function buildLibMongoCrypt(libmongocryptRoot, nodeDepsRoot, options) {
61+
/** CLI flag maker: `toFlags({a: 1, b: 2})` yields `['-a=1', '-b=2']` */
62+
function toCLIFlags(object) {
63+
return Array.from(Object.entries(object)).map(([k, v]) => `-${k}=${v}`);
64+
}
65+
9066
console.error('building libmongocrypt...');
9167

9268
const nodeBuildRoot = resolveRoot(nodeDepsRoot, 'tmp', 'libmongocrypt-build');
9369

9470
await fs.rm(nodeBuildRoot, { recursive: true, force: true });
9571
await fs.mkdir(nodeBuildRoot, { recursive: true });
9672

97-
const CMAKE_FLAGS = toFlags({
73+
const CMAKE_FLAGS = toCLIFlags({
9874
/**
9975
* We provide crypto hooks from Node.js binding to openssl (so disable system crypto)
10076
* TODO: NODE-5455
@@ -127,12 +103,12 @@ export async function buildLibMongoCrypt(libmongocryptRoot, nodeDepsRoot, option
127103

128104
const WINDOWS_CMAKE_FLAGS =
129105
process.platform === 'win32' // Windows is still called "win32" when it is 64-bit
130-
? toFlags({ Thost: 'x64', A: 'x64', DENABLE_WINDOWS_STATIC_RUNTIME: 'ON' })
106+
? toCLIFlags({ Thost: 'x64', A: 'x64', DENABLE_WINDOWS_STATIC_RUNTIME: 'ON' })
131107
: [];
132108

133109
const DARWIN_CMAKE_FLAGS =
134110
process.platform === 'darwin' // The minimum darwin target version we want for
135-
? toFlags({ DCMAKE_OSX_DEPLOYMENT_TARGET: '10.12' })
111+
? toCLIFlags({ DCMAKE_OSX_DEPLOYMENT_TARGET: '10.12' })
136112
: [];
137113

138114
const cmakeProgram = process.platform === 'win32' ? 'cmake.exe' : 'cmake';
@@ -149,35 +125,18 @@ export async function buildLibMongoCrypt(libmongocryptRoot, nodeDepsRoot, option
149125
});
150126
}
151127

152-
export async function downloadLibMongoCrypt(nodeDepsRoot, { ref, fastDownload }) {
153-
const downloadURL =
154-
ref === 'latest'
155-
? 'https://mciuploads.s3.amazonaws.com/libmongocrypt/all/master/latest/libmongocrypt-all.tar.gz'
156-
: `https://mciuploads.s3.amazonaws.com/libmongocrypt/all/${ref}/libmongocrypt-all.tar.gz`;
128+
export async function downloadLibMongoCrypt(nodeDepsRoot, { ref }) {
129+
const prebuild = getLibmongocryptPrebuildName();
130+
131+
const downloadURL = buildLibmongocryptDownloadUrl(ref, prebuild);
157132

158133
console.error('downloading libmongocrypt...', downloadURL);
159134
const destination = resolveRoot(`_libmongocrypt-${ref}`);
160135

161136
await fs.rm(destination, { recursive: true, force: true });
162137
await fs.mkdir(destination);
163138

164-
const platformMatrix = {
165-
['darwin-arm64']: 'macos',
166-
['darwin-x64']: 'macos',
167-
['linux-ppc64']: 'rhel-71-ppc64el',
168-
['linux-s390x']: 'rhel72-zseries-test',
169-
['linux-arm64']: 'ubuntu1804-arm64',
170-
['linux-x64']: 'rhel-70-64-bit',
171-
['win32-x64']: 'windows-test'
172-
};
173-
174-
const detectedPlatform = `${process.platform}-${process.arch}`;
175-
const prebuild = platformMatrix[detectedPlatform];
176-
if (prebuild == null) throw new Error(`Unsupported: ${detectedPlatform}`);
177-
178-
console.error(`Platform: ${detectedPlatform} Prebuild: ${prebuild}`);
179-
180-
const downloadDestination = `${prebuild}/nocrypto`;
139+
const downloadDestination = `nocrypto`;
181140
const unzipArgs = ['-xzv', '-C', `_libmongocrypt-${ref}`, downloadDestination];
182141
console.error(`+ tar ${unzipArgs.join(' ')}`);
183142
const unzip = child_process.spawn('tar', unzipArgs, {
@@ -190,35 +149,8 @@ export async function downloadLibMongoCrypt(nodeDepsRoot, { ref, fastDownload })
190149

191150
const start = performance.now();
192151

193-
let signal;
194-
if (fastDownload) {
195-
/**
196-
* Tar will print out each file it finds inside MEMBER (ex. macos/nocrypto)
197-
* For each file it prints, we give it a deadline of 3 seconds to print the next one.
198-
* If nothing prints after 3 seconds we exit early.
199-
* This depends on the tar file being in order and un-tar-able in under 3sec.
200-
*/
201-
const controller = new AbortController();
202-
signal = controller.signal;
203-
let firstMemberSeen = true;
204-
let timeout;
205-
unzip.stderr.on('data', chunk => {
206-
process.stderr.write(chunk, () => {
207-
if (firstMemberSeen) {
208-
firstMemberSeen = false;
209-
timeout = setTimeout(() => {
210-
clearTimeout(timeout);
211-
unzip.stderr.removeAllListeners('data');
212-
controller.abort();
213-
}, 3_000);
214-
}
215-
timeout?.refresh();
216-
});
217-
});
218-
}
219-
220152
try {
221-
await stream.pipeline(response, unzip.stdin, { signal });
153+
await stream.pipeline(response, unzip.stdin);
222154
} catch {
223155
await fs.access(path.join(`_libmongocrypt-${ref}`, downloadDestination));
224156
}
@@ -228,7 +160,7 @@ export async function downloadLibMongoCrypt(nodeDepsRoot, { ref, fastDownload })
228160
console.error(`downloaded libmongocrypt in ${(end - start) / 1000} secs...`);
229161

230162
await fs.rm(nodeDepsRoot, { recursive: true, force: true });
231-
await fs.cp(resolveRoot(destination, prebuild, 'nocrypto'), nodeDepsRoot, { recursive: true });
163+
await fs.cp(resolveRoot(destination, 'nocrypto'), nodeDepsRoot, { recursive: true });
232164
const potentialLib64Path = path.join(nodeDepsRoot, 'lib64');
233165
try {
234166
await fs.rename(potentialLib64Path, path.join(nodeDepsRoot, 'lib'));

Diff for: .github/scripts/utils.mjs

+89
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
// @ts-check
2+
3+
import { execSync } from "child_process";
4+
import path from "path";
5+
import url from 'node:url';
6+
import { spawn } from "node:child_process";
7+
import { once } from "node:events";
8+
9+
const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
10+
11+
/** Resolves to the root of this repository */
12+
export function resolveRoot(...paths) {
13+
return path.resolve(__dirname, '..', '..', ...paths);
14+
}
15+
16+
export function getCommitFromRef(ref) {
17+
console.error(`resolving ref: ${ref}`);
18+
const script = resolveRoot('.github', 'scripts', 'get-commit-from-ref.sh');
19+
const output = execSync(`bash ${script}`, { env: { REF: ref }, encoding: 'utf-8' })
20+
21+
const regex = /COMMIT_HASH=(?<hash>[a-zA-Z0-9]+)/
22+
const result = regex.exec(output);
23+
24+
if (!result?.groups) throw new Error('unable to parse ref.')
25+
26+
const { hash } = result.groups;
27+
28+
console.error(`resolved to: ${hash}`);
29+
return hash;
30+
}
31+
32+
export function buildLibmongocryptDownloadUrl(ref, platform) {
33+
const hash = getCommitFromRef(ref);
34+
35+
// sort of a hack - if we have an official release version, it'll be in the form `major.minor`. otherwise,
36+
// we'd expect a commit hash or `master`.
37+
if (ref.includes('.')) {
38+
const [major, minor, _patch] = ref.split('.');
39+
40+
// Just a note: it may appear that this logic _doesn't_ support patch releases but it actually does.
41+
// libmongocrypt's creates release branches for minor releases in the form `r<major>.<minor>`.
42+
// Any patches made to this branch are committed as tags in the form <major>.<minor>.<patch>.
43+
// So, the branch that is used for the AWS s3 upload is `r<major>.<minor>` and the commit hash
44+
// is the commit hash we parse from the `getCommitFromRef()` (which handles switching to git tags and
45+
// getting the commit hash at that tag just fine).
46+
const branch = `r${major}.${minor}`
47+
48+
return `https://mciuploads.s3.amazonaws.com/libmongocrypt-release/${platform}/${branch}/${hash}/libmongocrypt.tar.gz`;
49+
}
50+
51+
// just a note here - `master` refers to the branch, the hash is the commit on that branch.
52+
// if we ever need to download binaries from a non-master branch (or non-release branch),
53+
// this will need to be modified somehow.
54+
return `https://mciuploads.s3.amazonaws.com/libmongocrypt/${platform}/master/${hash}/libmongocrypt.tar.gz`;
55+
}
56+
57+
export function getLibmongocryptPrebuildName() {
58+
const platformMatrix = {
59+
['darwin-arm64']: 'macos',
60+
['darwin-x64']: 'macos',
61+
['linux-ppc64']: 'rhel-71-ppc64el',
62+
['linux-s390x']: 'rhel72-zseries-test',
63+
['linux-arm64']: 'ubuntu1804-arm64',
64+
['linux-x64']: 'rhel-70-64-bit',
65+
['win32-x64']: 'windows-test'
66+
};
67+
68+
const detectedPlatform = `${process.platform}-${process.arch}`;
69+
const prebuild = platformMatrix[detectedPlatform];
70+
71+
if (prebuild == null) throw new Error(`Unsupported: ${detectedPlatform}`);
72+
73+
return prebuild;
74+
}
75+
76+
/** `xtrace` style command runner, uses spawn so that stdio is inherited */
77+
export async function run(command, args = [], options = {}) {
78+
const commandDetails = `+ ${command} ${args.join(' ')}${options.cwd ? ` (in: ${options.cwd})` : ''}`;
79+
console.error(commandDetails);
80+
const proc = spawn(command, args, {
81+
shell: process.platform === 'win32',
82+
stdio: 'inherit',
83+
cwd: resolveRoot('.'),
84+
...options
85+
});
86+
await once(proc, 'exit');
87+
88+
if (proc.exitCode != 0) throw new Error(`CRASH(${proc.exitCode}): ${commandDetails}`);
89+
}

0 commit comments

Comments
 (0)