const assert = require('node:assert/strict') const { createHash } = require('node:crypto') const { createReadStream, existsSync, readdirSync, readFileSync, writeFileSync, } = require('node:fs') const { join, relative } = require('node:path') const { execFileSync } = require('node:child_process') const asar = require('@electron/asar') const root = join(__dirname, '..') const metadata = require('../package.json') const sha = (bytes) => createHash('sha256').update(bytes).digest('hex') async function fileHash(path) { const hash = createHash('sha256') for await (const bytes of createReadStream(path)) hash.update(bytes) return hash.digest('hex') } function resourcesIn(directory) { const app = readdirSync(directory).find((name) => name.endsWith('.app')) return app ? join(directory, app, 'Contents', 'Resources') : join(directory, 'resources') } function verifyAsar(path) { const files = asar.listPackage(path).map((name) => name.replace(/^\//, '')) for (const name of files) { assert.ok( !/(^|\/)(?:\.git|\.env(?:\.[^/]*)?|credentials\.bin|windows\.json|selection\.json|__tests__)(?:\/|$)|\.test\.(?:js|ts)$|\.(?:pem|key|p12|pfx)$/.test( name, ), `Forbidden packaged file: ${name}`, ) assert.ok( /^(?:node_modules(?:\/|$)|dist(?:\/|$)|dist-electron(?:\/|$)|package\.json$)/.test(name), `Unexpected packaged file: ${name}`, ) } for (const name of [ 'package.json', 'dist/index.html', 'dist-electron/electron/main.js', 'dist-electron/electron/preload.js', 'dist-electron/electron/online.js', 'dist-electron/electron/diagnostics.js', ]) assert.ok(files.includes(name), `Missing ${name}`) const pkg = JSON.parse(asar.extractFile(path, 'package.json')) assert.equal(pkg.version, metadata.version) assert.equal(pkg.name, metadata.name) return pkg } function binaryArchitecture(path, platform) { const bytes = readFileSync(path) if (platform === 'linux') { assert.equal(bytes.subarray(0, 4).toString('hex'), '7f454c46') return { 62: 'x64', 183: 'arm64' }[bytes.readUInt16LE(18)] } if (platform === 'win32') { assert.equal(bytes.subarray(0, 2).toString(), 'MZ') const offset = bytes.readUInt32LE(0x3c) assert.equal(bytes.subarray(offset, offset + 4).toString(), 'PE\0\0') return { 0x8664: 'x64', 0xaa64: 'arm64' }[bytes.readUInt16LE(offset + 4)] } assert.equal(bytes.subarray(0, 4).toString('hex'), 'cffaedfe') return { 0x1000007: 'x64', 0x100000c: 'arm64' }[bytes.readUInt32LE(4)] } function executableIn(directory, platform) { if (platform === 'darwin') { const app = readdirSync(directory).find((name) => name.endsWith('.app')) return join(directory, app, 'Contents', 'MacOS', metadata.build.productName) } return join(directory, platform === 'win32' ? `${metadata.build.productName}.exe` : metadata.name) } async function verifyDirectory(directory) { const resources = resourcesIn(directory) const info = JSON.parse(readFileSync(join(resources, 'build-info.json'))) assert.equal(info.formatVersion, 1) assert.equal(info.version, metadata.version) assert.ok(['linux', 'win32', 'darwin'].includes(info.platform)) assert.ok(['x64', 'arm64'].includes(info.arch)) verifyAsar(join(resources, 'app.asar')) assert.deepEqual(Object.keys(info.files).sort(), [ 'app.asar', 'fixtures/audio.wav', 'fixtures/video.mp4', ]) for (const [name, expected] of Object.entries(info.files)) assert.equal(await fileHash(join(resources, name)), expected, `Hash mismatch: ${name}`) assert.deepEqual(readdirSync(join(resources, 'fixtures')).sort(), ['audio.wav', 'video.mp4']) assert.ok( !existsSync(join(resources, 'app.asar.unpacked')), 'Unexpected unpacked application files', ) assert.equal(binaryArchitecture(executableIn(directory, info.platform), info.platform), info.arch) return info } async function afterPack(context) { const resources = resourcesIn(context.appOutDir) verifyAsar(join(resources, 'app.asar')) const files = {} for (const name of ['app.asar', 'fixtures/audio.wav', 'fixtures/video.mp4']) files[name] = await fileHash(join(resources, name)) let revision = null let dirty = true try { const result = execFileSync('git', ['rev-parse', 'HEAD'], { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], }).trim() if (/^[a-f0-9]{40,64}$/.test(result)) revision = result dirty = !!execFileSync('git', ['status', '--porcelain'], { cwd: root, encoding: 'utf8' }).trim() } catch { /* Экспорт исходников без .git тоже может быть собран. */ } const ffmpeg = execFileSync('ffmpeg', ['-version'], { encoding: 'utf8' }).split('\n')[0] const info = { formatVersion: 1, version: metadata.version, platform: context.electronPlatformName, arch: require('builder-util').Arch[context.arch], revision, dirty, tools: { node: process.versions.node, electron: context.packager.info.framework.version, builder: require('electron-builder/package.json').version, ffmpeg, }, lockfileSha256: sha(readFileSync(join(root, 'package-lock.json'))), files, } writeFileSync(join(resources, 'build-info.json'), JSON.stringify(info, null, 2) + '\n') await verifyDirectory(context.appOutDir) console.log('RELEASE_CONTENTS_PASS', relative(root, context.appOutDir), info.platform, info.arch) } module.exports = afterPack Object.assign(module.exports, { fileHash, verifyAsar, verifyDirectory, binaryArchitecture, resourcesIn, })