diff --git a/src/package.ts b/src/package.ts index 6dac2581..bb60db51 100644 --- a/src/package.ts +++ b/src/package.ts @@ -1796,7 +1796,7 @@ export function collect(manifest: ManifestPackage, options: IPackageOptions = {} }); } -function writeVsix(files: IFile[], packagePath: string): Promise { +export function writeVsix(files: IFile[], packagePath: string): Promise { return fs.promises .unlink(packagePath) .catch(err => (err.code !== 'ENOENT' ? Promise.reject(err) : Promise.resolve(null))) @@ -1804,13 +1804,21 @@ function writeVsix(files: IFile[], packagePath: string): Promise { () => new Promise((c, e) => { const zip = new yazl.ZipFile(); - files.forEach(f => + + const sde = process.env.SOURCE_DATE_EPOCH; + const epoch = sde ? parseInt(sde) : undefined; + const mtime = epoch ? new Date(epoch * 1000) : undefined; + + files.sort((a, b) => a.path.localeCompare(b.path)).forEach(f => { + let options = { + mode: f.mode, + } as any; + if (mtime) options.mtime = mtime; + isInMemoryFile(f) - ? zip.addBuffer(typeof f.contents === 'string' ? Buffer.from(f.contents, 'utf8') : f.contents, f.path, { - mode: f.mode, - }) - : zip.addFile(f.localPath, f.path, { mode: f.mode }) - ); + ? zip.addBuffer(typeof f.contents === 'string' ? Buffer.from(f.contents, 'utf8') : f.contents, f.path, options) + : zip.addFile(f.localPath, f.path, options) + }); zip.end(); const zipStream = fs.createWriteStream(packagePath); diff --git a/src/test/package.test.ts b/src/test/package.test.ts index 467850a7..ab8c74ec 100644 --- a/src/test/package.test.ts +++ b/src/test/package.test.ts @@ -15,6 +15,7 @@ import { VSIX, LicenseProcessor, printAndValidatePackagedFiles, + writeVsix, } from '../package'; import { ManifestPackage } from '../manifest'; import * as path from 'path'; @@ -26,6 +27,7 @@ import { XMLManifest, parseXmlManifest, parseContentTypes } from '../xml'; import { flatten, log } from '../util'; import { validatePublisher } from '../validation'; import * as jsonc from 'jsonc-parser'; +import { globSync } from 'glob'; // don't warn in tests console.warn = () => null; @@ -3196,3 +3198,41 @@ describe('version', function () { assert.strictEqual(newManifest.version, '1.0.0'); }); }); + +describe('writeVsix', () => { + it('should be reproducible', async () => { + const dir = tmp.dirSync({ unsafeCleanup: true }); + const cwd = dir.name + + const srcDir = fixture('manifestFiles'); + fs.cpSync(srcDir, cwd, { recursive: true }); + + const files = globSync("**", { cwd }); + + process.env["SOURCE_DATE_EPOCH"] = '1000000000'; + + const vsix1 = tmp.fileSync(); + const epoch1 = 1000000001 + files.forEach((f) => fs.utimesSync(path.join(cwd, f), epoch1, epoch1)); + const manifest1 = await readManifest(cwd); + const iFiles1 = await collect(manifest1, { cwd }); + await writeVsix(iFiles1, vsix1.name); + + // different timestamp and iFiles are reversed + const vsix2 = tmp.fileSync(); + const epoch2 = 1000000002 + files.forEach((f) => fs.utimesSync(path.join(cwd, f), epoch2, epoch2)); + const manifest2 = await readManifest(cwd); + const iFiles2 = (await collect(manifest2, { cwd })).reverse(); + await writeVsix(iFiles2, vsix2.name); + + const vsix1bytes = fs.readFileSync(vsix1.name); + const vsix2bytes = fs.readFileSync(vsix2.name); + + assert.deepStrictEqual(vsix1bytes, vsix2bytes); + + dir.removeCallback(); + vsix1.removeCallback(); + vsix2.removeCallback(); + }); +});