diff --git a/src/package.ts b/src/package.ts index 6dac2581..cb5c0708 100644 --- a/src/package.ts +++ b/src/package.ts @@ -505,7 +505,7 @@ export class ManifestProcessor extends BaseProcessor { if (!minEngineVersion) { throw new Error('Failed to get minVersion of engines.vscode') } - + if (target) { if (engineSemver.version !== 'latest' && !semver.satisfies(minEngineVersion, '>=1.61', { includePrerelease: true })) { throw new Error( @@ -1804,12 +1804,20 @@ function writeVsix(files: IFile[], packagePath: string): Promise { () => new Promise((c, e) => { const zip = new yazl.ZipFile(); + const zipOptions: Partial = {}; + + // reproducible zip files + const sde = process.env.SOURCE_DATE_EPOCH; + if (sde) { + const epoch = parseInt(sde); + zipOptions.mtime = new Date(epoch * 1000); + files = files.sort((a, b) => a.path.localeCompare(b.path)) + } + files.forEach(f => 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, { ...zipOptions, mode: f.mode }) + : zip.addFile(f.localPath, f.path, { ...zipOptions, mode: f.mode }) ); zip.end(); diff --git a/src/test/package.test.ts b/src/test/package.test.ts index 467850a7..6b01a159 100644 --- a/src/test/package.test.ts +++ b/src/test/package.test.ts @@ -14,7 +14,7 @@ import { versionBump, VSIX, LicenseProcessor, - printAndValidatePackagedFiles, + printAndValidatePackagedFiles, pack } from '../package'; import { ManifestPackage } from '../manifest'; import * as path from 'path'; @@ -2286,13 +2286,13 @@ describe('ManifestProcessor', () => { }); it('should not throw error for engine version with x (e.g. 1.95.x)', async () => { - const root = fixture('uuid'); - const manifest = JSON.parse(await fs.promises.readFile(path.join(root, 'package.json'), 'utf8')); - manifest.engines.vscode = '1.95.x'; // Non-strict semver, but acceptable + const root = fixture('uuid'); + const manifest = JSON.parse(await fs.promises.readFile(path.join(root, 'package.json'), 'utf8')); + manifest.engines.vscode = '1.95.x'; // Non-strict semver, but acceptable - assert.doesNotThrow(() => new ManifestProcessor(manifest, { target: 'web' })); - assert.doesNotThrow(() => new ManifestProcessor(manifest, { preRelease: true })); - }); + assert.doesNotThrow(() => new ManifestProcessor(manifest, { target: 'web' })); + assert.doesNotThrow(() => new ManifestProcessor(manifest, { preRelease: true })); + }); }); describe('MarkdownProcessor', () => { @@ -3196,3 +3196,42 @@ describe('version', function () { assert.strictEqual(newManifest.version, '1.0.0'); }); }); + +describe('writeVsix', function () { + this.timeout(60_000); + + it('should be reproducible', async () => { + const exampleProject = fixture('manifestFiles'); + const fixtureDir = fixture(''); + + const testDir = tmp.dirSync({ unsafeCleanup: true, tmpdir: fixtureDir }); + const cwd = testDir.name + + try { + fs.cpSync(exampleProject, cwd, { recursive: true }); + + const createVsix = async (vsixPath: string, epoch: number) => { + process.env["SOURCE_DATE_EPOCH"] = `${epoch}`; + await pack({ cwd, packagePath: vsixPath }); + } + + const vsix1 = testDir.name + '/vsix1.vsix'; + const vsix2 = testDir.name + '/vsix2.vsix'; + const vsix3 = testDir.name + '/vsix3.vsix'; + + await createVsix(vsix1, 1000000000); + await createVsix(vsix2, 1000000000); + await createVsix(vsix3, 1000000002); + + const vsix1bytes = fs.readFileSync(vsix1); + const vsix2bytes = fs.readFileSync(vsix2); + const vsix3bytes = fs.readFileSync(vsix3); + + assert.deepStrictEqual(vsix1bytes, vsix2bytes); + assert.notDeepStrictEqual(vsix1bytes, vsix3bytes); + + } finally { + testDir.removeCallback(); + } + }); +});