Skip to content

Commit

Permalink
Allow for reproducible .vsix packages
Browse files Browse the repository at this point in the history
Running the same build produces .vsix package that have the same
content, but are not bit for bit the same, making it somewhat
complicated to verify reproducible builds. Two changes are needed to fix
this:

1. The mtime of each file added to the .vsix archive is included in each
   archive entry, so builds that happen at different times will have
   different entry timestamps. To fix this, if the SOURCE_DATE_EPOCH
   environment variable is defined, it it now used as entry timestamp
   value instead. Builds will now be reproducible as long as they set
   the same SOURCE_DATE_EPOCH value. If the environment variable is not
   defined or is not an integer, the current behavior is used.
2. The order that files are collected in preparation for packaging into
   the .vsix file is non-deterministic, which can lead to archives with
   the same content but in different orders. To fix this, files are
   sorted by archive entry name prior to adding.

Fixes #906
  • Loading branch information
stevedlawrence committed Dec 10, 2024
1 parent 0a22db7 commit c2a324c
Show file tree
Hide file tree
Showing 2 changed files with 55 additions and 7 deletions.
22 changes: 15 additions & 7 deletions src/package.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1796,21 +1796,29 @@ export function collect(manifest: ManifestPackage, options: IPackageOptions = {}
});
}

function writeVsix(files: IFile[], packagePath: string): Promise<void> {
export function writeVsix(files: IFile[], packagePath: string): Promise<void> {
return fs.promises
.unlink(packagePath)
.catch(err => (err.code !== 'ENOENT' ? Promise.reject(err) : Promise.resolve(null)))
.then(
() =>
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);
Expand Down
40 changes: 40 additions & 0 deletions src/test/package.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
VSIX,
LicenseProcessor,
printAndValidatePackagedFiles,
writeVsix,
} from '../package';
import { ManifestPackage } from '../manifest';
import * as path from 'path';
Expand All @@ -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;
Expand Down Expand Up @@ -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();
});
});

0 comments on commit c2a324c

Please sign in to comment.