-
Notifications
You must be signed in to change notification settings - Fork 778
/
Copy pathreproducible-builds.js
243 lines (220 loc) · 8.12 KB
/
reproducible-builds.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
// Helper for the "Reproducible builds" job.
//
// Prerequisites:
// * Node.js 18+, npm 10+
// * Git 2.11+
// * tar, shasum, gunzip (preinstalled on Linux/macOS)
'use strict';
const cp = require('child_process');
const fs = require('fs');
const path = require('path');
const util = require('util');
const utils = require('./utils.js');
const execFile = util.promisify(cp.execFile);
const tempDir = path.join(__dirname, '../temp', 'reproducible-builds');
const SRC_REPO = 'https://github.com/qunitjs/qunit.git';
/**
* QUnit 2.17.0 and later are fully reproducible with this script.
*
* Known caveats:
*
* QUnit 2.14.1 - 2.16.0:
* - File headers included an uncontrolled "current" timestamp.
* This would have to be ignored or replaced prior to comparison.
* - The build wrote files to "/dist" instead of "/qunit".
*
* QUnit 2.15.0:
* - Contained some CR (\r) characters in comments from fuzzysort.js,
* which get normalized to LF (\n) by Git and npm, but not in the actual builds
* and in what we publish to the CDN. This was fixed in qunit@2.16.0 and qunit@2.17.0.
*
* QUnit 2.17.0 - 2.21.0:
* - These were built and published using npm 8 or npm 9.
* In npm 10, upstream changed gzip encoding slightly for the npm-pack tarball (.tgz). This
* means a tarball from npm 10+ is not byte-for-byte identical to ones generated by npm 8 or 9.
* After gzip-decompression, however, the tar stream is byte-for-byte identical.
* Either use npm 8 or 9 to verify these, or verify the tarball after gzip decompression.
*
* QUnit 3.0.0-alpha.3:
* - The package-lock.json file was a few commits behind what was actually released,
* thus reproducing it uees a slightly Rollup/Babel version that outputs with slightly
* different code formatting.
*/
const VERIFY_COUNT = 5;
const EXCLUDE = ['3.0.0-alpha.3'];
async function buildRelease (version, cacheDir = null) {
console.log(`... ${version}: checking out the source`);
const gitDir = path.join(tempDir, `git-${version}`);
utils.cleanDir(gitDir);
await execFile('git', ['clone', '-q', '-b', version, '--depth=5', SRC_REPO, gitDir]);
// Remove any artefacts that were checked into Git
utils.cleanDir(gitDir + '/qunit/');
console.log(`... ${version}: installing development dependencies from npm`);
const npmEnv = {
npm_config_cache: cacheDir,
npm_config_update_notifier: 'false',
PATH: process.env.PATH,
QUNIT_BUILD_RELEASE: '1',
PUPPETEER_CACHE_DIR: path.join(cacheDir, 'puppeteer_download')
};
cp.execFileSync('npm', ['install'], {
env: npmEnv,
cwd: gitDir
});
console.log(`... ${version}: building release`);
await execFile('npm', ['run', 'build'], {
env: npmEnv,
cwd: gitDir
});
console.log(`... ${version}: packing npm package`);
await execFile('npm', ['pack'], {
env: npmEnv,
cwd: gitDir
});
return {
js: {
name: gitDir + '/qunit/qunit.js',
contents: fs.readFileSync(gitDir + '/qunit/qunit.js', 'utf8')
},
css: {
name: gitDir + '/qunit/qunit.css',
contents: fs.readFileSync(gitDir + '/qunit/qunit.css', 'utf8')
},
tgz: {
name: gitDir + `/qunit-${version}.tgz`,
contents: cp.execSync(
`gunzip --stdout qunit-${version}.tgz | shasum -a 256 -b`,
{ encoding: 'utf8', cwd: gitDir }
)
}
};
}
const Reproducible = {
async fetch () {
// Fetch official releases first and store them in memory (not on disk). Only after that will
// we run the build commands (which involve unaudited npm packages as dev deps) which could
// modify anything on disk. Hence don't store what we want to compare against on disk.
const releases = {};
{
// This may take a while locally, when removing previous builds.
utils.cleanDir(tempDir);
}
{
console.log('Fetching releases from jQuery CDN...');
const cdnIndexUrl = 'https://releases.jquery.com/resources/cdn.json';
const data = JSON.parse(await utils.download(cdnIndexUrl));
for (const release of data.qunit.all.slice(0, VERIFY_COUNT)) {
if (EXCLUDE.includes(release.version)) {
continue;
}
releases[release.version] = {
cdn: {
js: {
name: `https://code.jquery.com/${release.filename}`,
contents: await utils.download(`https://code.jquery.com/${release.filename}`)
},
css: {
name: `https://code.jquery.com/${release.theme}`,
contents: await utils.download(`https://code.jquery.com/${release.theme}`)
}
}
};
}
}
{
console.log('Fetching releases from npmjs.org...');
const npmIndexUrl = 'https://registry.npmjs.org/qunit';
const data = JSON.parse(await utils.download(npmIndexUrl));
for (const version in releases) {
if (!data.versions[version]) {
throw new Error(`QUnit ${version} is missing from https://www.npmjs.com/package/qunit`);
}
const tarball = data.versions[version].dist.tarball;
const tarFile = path.join(tempDir, path.basename(tarball));
await utils.downloadFile(tarball, tarFile);
releases[version].npm = {
js: {
name: `npm:${path.basename(tarball)}#package/qunit/qunit.js`,
contents: cp.execFileSync(
'tar', ['-xOf', tarFile, 'package/qunit/qunit.js'],
{ encoding: 'utf8' }
)
},
css: {
name: `npm:${path.basename(tarball)}#package/qunit/qunit.css`,
contents: cp.execFileSync(
'tar', ['-xOf', tarFile, 'package/qunit/qunit.css'],
{ encoding: 'utf8' }
)
},
tgz: {
name: `npm:${path.basename(tarball)}`,
contents: cp.execSync(
`gunzip --stdout ${path.basename(tarball)} | shasum -a 256 -b`,
{ encoding: 'utf8', cwd: tempDir }
)
}
};
}
}
{
console.log('Reproducing release builds...');
const cacheDir = path.join(tempDir, 'cache');
utils.cleanDir(cacheDir);
// Start builds in parallel and await results.
const buildPromises = [];
for (const version in releases) {
buildPromises.push(
(releases[version].buildPromise = buildRelease(version, cacheDir))
);
}
await Promise.all(buildPromises);
const diffs = [];
for (const version in releases) {
const release = releases[version];
const build = await release.buildPromise;
let verified = true;
for (const distro in release) {
for (const file in release[distro]) {
if (release[distro][file].contents === build[file].contents) {
console.log(
`... ${version}: ${release[distro][file].name} matches ${build[file].name}`
);
} else {
verified = false;
console.error(
`QUnit ${version} ${file} from ${distro} differs from build`
);
const buildFile = `qunit-${version}-build.${file}`;
const releaseFile = `qunit-${version}-${distro}.${file}`;
fs.writeFileSync(buildFile, utils.verboseNonPrintable(build[file].contents));
fs.writeFileSync(releaseFile, utils.verboseNonPrintable(release[distro][file].contents));
diffs.push(
`--- ${build[file].name}\n+++ ${release[distro][file].name}\n`,
utils.getDiff(buildFile, releaseFile, { ignoreWhitespace: false })
.split('\n').slice(2).join('\n')
);
fs.rmSync(buildFile);
fs.rmSync(releaseFile);
}
}
}
if (verified) {
console.log(`QUnit ${version} is reproducible and matches distributions!`);
}
}
diffs.forEach(diff => {
process.stdout.write(diff);
});
if (diffs.length) {
throw new Error('One or more distributions differ from the reproduced build');
}
}
}
};
(async function main () {
await Reproducible.fetch();
}()).catch(e => {
console.error(e.toString());
process.exit(1);
});