diff --git a/packages/build/src/packaging/package/helpers.ts b/packages/build/src/packaging/package/helpers.ts index ede417ffa8..11f459733d 100644 --- a/packages/build/src/packaging/package/helpers.ts +++ b/packages/build/src/packaging/package/helpers.ts @@ -21,22 +21,27 @@ export async function execFile(...args: Parameters { - // For the tarball and the zip file: We put license and readme texts at the - // root of the package, and put all binaries into /bin. +export async function createCompressedArchiveContents(archiveRootName: string, pkg: PackageInformation): Promise { + // For the tarball and the zip file: + // - We add a single top-level folder to contain all contents + // - We put license and readme texts directly in the top-level folder, and put all binaries into folder/bin. const tmpDir = path.join(__dirname, '..', '..', '..', 'tmp', `pkg-${Date.now()}-${Math.random()}`); - await fs.mkdir(tmpDir, { recursive: true }); + const archiveRoot = path.join(tmpDir, archiveRootName); + await fs.mkdir(archiveRoot, { recursive: true }); const docFiles = [ ...pkg.otherDocFilePaths, ...pkg.binaries.map(({ license }) => license) ]; for (const { sourceFilePath, packagedFilePath } of docFiles) { - await fs.copyFile(sourceFilePath, path.join(tmpDir, packagedFilePath), COPYFILE_FICLONE); + await fs.copyFile(sourceFilePath, path.join(archiveRoot, packagedFilePath), COPYFILE_FICLONE); } - await fs.mkdir(path.join(tmpDir, 'bin')); + await fs.mkdir(path.join(archiveRoot, 'bin')); for (const { sourceFilePath } of pkg.binaries) { - await fs.copyFile(sourceFilePath, path.join(tmpDir, 'bin', path.basename(sourceFilePath)), COPYFILE_FICLONE); + await fs.copyFile(sourceFilePath, path.join(archiveRoot, 'bin', path.basename(sourceFilePath)), COPYFILE_FICLONE); } return tmpDir; } diff --git a/packages/build/src/packaging/package/tarball.spec.ts b/packages/build/src/packaging/package/tarball.spec.ts index 5437c2bdd0..87e6ce3bdf 100644 --- a/packages/build/src/packaging/package/tarball.spec.ts +++ b/packages/build/src/packaging/package/tarball.spec.ts @@ -1,4 +1,7 @@ +import { expect } from 'chai'; +import { spawnSync } from 'child_process'; import { promises as fs } from 'fs'; +import path from 'path'; import { withTempPackageEach } from '../../../test/helpers'; import { createPackage } from './create-package'; @@ -8,5 +11,16 @@ describe('package tarball', () => { it('packages the executable(s)', async() => { const tarball = await createPackage(tmpPkg.tarballDir, 'linux-x64', tmpPkg.pkgConfig); await fs.access(tarball.path); + const tarname = path.basename(tarball.path).replace(/\.tgz$/, ''); + + const unzip = spawnSync('tar', [ + 'tf', tarball.path + ], { encoding: 'utf-8' }); + expect(unzip.error).to.be.undefined; + expect(unzip.stderr).to.be.empty; + + expect( + unzip.stdout.split('\n').filter(l => !!l).every(l => l.startsWith(`${tarname}/`)) + ).to.be.true; }); }); diff --git a/packages/build/src/packaging/package/tarball.ts b/packages/build/src/packaging/package/tarball.ts index cfa25e27b2..c70e7738e7 100644 --- a/packages/build/src/packaging/package/tarball.ts +++ b/packages/build/src/packaging/package/tarball.ts @@ -1,4 +1,5 @@ import { promises as fs } from 'fs'; +import path from 'path'; import rimraf from 'rimraf'; import tar from 'tar'; import { promisify } from 'util'; @@ -9,7 +10,8 @@ import { PackageInformation } from './package-information'; * Create a tarball archive for posix. */ export async function createTarballPackage(pkg: PackageInformation, outFile: string): Promise { - const tmpDir = await createCompressedArchiveContents(pkg); + const filename = path.basename(outFile).replace(/\.[^.]+$/, ''); + const tmpDir = await createCompressedArchiveContents(filename, pkg); await tar.c({ gzip: true, file: outFile, diff --git a/packages/build/src/packaging/package/zip.spec.ts b/packages/build/src/packaging/package/zip.spec.ts index be87b38585..c1c0ba037f 100644 --- a/packages/build/src/packaging/package/zip.spec.ts +++ b/packages/build/src/packaging/package/zip.spec.ts @@ -1,4 +1,5 @@ import { expect } from 'chai'; +import { spawnSync } from 'child_process'; import { promises as fs } from 'fs'; import * as path from 'path'; import sinon from 'ts-sinon'; @@ -20,6 +21,21 @@ describe('package zip', () => { it('packages the executable(s)', async() => { const tarball = await createPackage(tmpPkg.tarballDir, 'win32-x64', tmpPkg.pkgConfig); await fs.access(tarball.path); + const zipname = path.basename(tarball.path).replace(/\.zip$/, ''); + + const unzip = spawnSync('unzip', [ + '-l', tarball.path + ], { encoding: 'utf-8' }); + expect(unzip.error).to.be.undefined; + expect(unzip.stderr).to.be.empty; + + const lines = unzip.stdout.split('\n'); + expect(lines).to.have.length(13); + + for (let i = 3; i < 10; i++) { + const filename = /([^\s]+)$/.exec(lines[i])?.[1] ?? ''; + expect(filename.startsWith(`${zipname}/`)).to.be.true; + } }); it('falls back to 7zip if zip is not available', async() => { diff --git a/packages/build/src/packaging/package/zip.ts b/packages/build/src/packaging/package/zip.ts index 9e81b4959c..f999c3292d 100644 --- a/packages/build/src/packaging/package/zip.ts +++ b/packages/build/src/packaging/package/zip.ts @@ -1,3 +1,4 @@ +import path from 'path'; import rimraf from 'rimraf'; import { promisify } from 'util'; import { createCompressedArchiveContents, execFile as execFileFn } from './helpers'; @@ -15,7 +16,8 @@ export async function createZipPackage( // evergreen macOS and Windows machines, respectively, at this point. // In either case, using these has the advantage of preserving executable permissions // as opposed to using libraries like adm-zip. - const tmpDir = await createCompressedArchiveContents(pkg); + const filename = path.basename(outFile).replace(/\.[^.]+$/, ''); + const tmpDir = await createCompressedArchiveContents(filename, pkg); try { await execFile('zip', ['-r', outFile, '.'], { cwd: tmpDir }); } catch (err) {