mateclaw/mateclaw-desktop/scripts/trim-playwright-driver.cjs

361 lines
12 KiB
JavaScript

const fs = require('node:fs')
const path = require('node:path')
const zlib = require('node:zlib')
const ZIP_STORED = 0
const ZIP_DEFLATED = 8
const DRIVER_BUNDLE_PATTERN = /^BOOT-INF\/lib\/driver-bundle-[^/]+\.jar$/
const ARCH_X64 = 1
const ARCH_ARM64 = 3
async function afterPack(context) {
const platform = context.electronPlatformName || context.packager?.platform?.name
const arch = normalizeArch(context.arch)
const keepDriverDirectory = resolveDriverDirectory(platform, arch)
const appJarPath = findAppJar(context)
if (!appJarPath) {
throw new Error(`[trim-playwright-driver] app.jar not found in ${context.appOutDir}`)
}
const result = await trimDriverBundleInAppJar(appJarPath, keepDriverDirectory)
const keptLabel = asArray(keepDriverDirectory).join(', ')
console.log(
`[trim-playwright-driver] ${path.relative(process.cwd(), appJarPath)}: ` +
`kept ${keptLabel}, removed ${result.removedDriverEntries} driver entries, ` +
`${formatBytes(result.beforeBytes)} -> ${formatBytes(result.afterBytes)}`
)
}
function findAppJar(context) {
const appOutDir = context.appOutDir
const productFilename = context.packager?.appInfo?.productFilename || context.packager?.appInfo?.productName || 'MateClaw'
const platform = context.electronPlatformName || context.packager?.platform?.name
const candidates = []
if (platform === 'darwin') {
candidates.push(path.join(appOutDir, `${productFilename}.app`, 'Contents', 'Resources', 'app.jar'))
}
candidates.push(path.join(appOutDir, 'resources', 'app.jar'))
for (const candidate of candidates) {
if (fs.existsSync(candidate)) return candidate
}
const found = findFirstFile(appOutDir, 'app.jar', 4)
return found
}
function findFirstFile(root, fileName, maxDepth, depth = 0) {
if (!root || depth > maxDepth || !fs.existsSync(root)) return null
for (const dirent of fs.readdirSync(root, { withFileTypes: true })) {
const fullPath = path.join(root, dirent.name)
if (dirent.isFile() && dirent.name === fileName) return fullPath
if (dirent.isDirectory()) {
const found = findFirstFile(fullPath, fileName, maxDepth, depth + 1)
if (found) return found
}
}
return null
}
async function trimDriverBundleInAppJar(appJarPath, keepDriverDirectory) {
const appJarBuffer = fs.readFileSync(appJarPath)
const outerEntries = readZipEntries(appJarBuffer)
const driverBundleEntry = outerEntries.find((entry) => DRIVER_BUNDLE_PATTERN.test(entry.name))
const keepDriverDirectories = asArray(keepDriverDirectory)
if (!driverBundleEntry) {
throw new Error(`Playwright driver-bundle jar not found in ${appJarPath}`)
}
const innerEntries = readZipEntries(driverBundleEntry.data)
let removedDriverEntries = 0
let keptDriverEntries = 0
const trimmedInnerEntries = innerEntries.filter((entry) => {
if (!entry.name.startsWith('driver/')) return true
if (keepDriverDirectories.some((directory) => entry.name.startsWith(`${directory}/`))) {
keptDriverEntries += 1
return true
}
removedDriverEntries += 1
return false
})
if (keptDriverEntries === 0) {
throw new Error(
`No entries kept under ${keepDriverDirectories.join(', ')}; Playwright driver layout may have changed`
)
}
const trimmedDriverBundle = createZipBuffer(trimmedInnerEntries.map(cloneEntryForWrite))
const rewrittenOuterEntries = outerEntries.map((entry) => {
if (entry.name !== driverBundleEntry.name) return cloneEntryForWrite(entry)
return {
...cloneEntryForWrite(entry),
data: trimmedDriverBundle,
method: ZIP_STORED,
}
})
const rewrittenAppJar = createZipBuffer(rewrittenOuterEntries)
fs.writeFileSync(appJarPath, rewrittenAppJar)
return {
beforeBytes: appJarBuffer.length,
afterBytes: rewrittenAppJar.length,
driverBundleName: driverBundleEntry.name,
keptDriverEntries,
removedDriverEntries,
}
}
function cloneEntryForWrite(entry) {
return {
name: entry.name,
data: Buffer.from(entry.data),
method: entry.method,
date: entry.date,
comment: entry.comment,
externalAttributes: entry.externalAttributes,
}
}
function resolveDriverDirectory(platform, arch) {
const normalizedArch = normalizeArch(arch)
if (platform === 'darwin') {
if (normalizedArch === 'arm64') return 'driver/mac-arm64'
if (normalizedArch === 'x64') return 'driver/mac'
if (normalizedArch === 'universal') return ['driver/mac', 'driver/mac-arm64']
}
if (platform === 'linux') {
if (normalizedArch === 'arm64') return 'driver/linux-arm64'
if (normalizedArch === 'x64') return 'driver/linux'
}
if (platform === 'win32') {
return 'driver/win32_x64'
}
throw new Error(`Unsupported platform/arch for Playwright driver trim: ${platform}/${arch}`)
}
function asArray(value) {
return Array.isArray(value) ? value : [value]
}
function normalizeArch(arch) {
if (arch === ARCH_X64 || arch === 'x64') return 'x64'
if (arch === ARCH_ARM64 || arch === 'arm64') return 'arm64'
if (arch === 'universal' || arch === 4) return 'universal'
return String(arch)
}
function readZipEntries(buffer) {
const eocdOffset = findEndOfCentralDirectory(buffer)
const centralDirectorySize = buffer.readUInt32LE(eocdOffset + 12)
const centralDirectoryOffset = buffer.readUInt32LE(eocdOffset + 16)
const entries = []
let offset = centralDirectoryOffset
const centralDirectoryEnd = centralDirectoryOffset + centralDirectorySize
while (offset < centralDirectoryEnd) {
const signature = buffer.readUInt32LE(offset)
if (signature !== 0x02014b50) {
throw new Error(`Invalid central directory signature at offset ${offset}`)
}
const flags = buffer.readUInt16LE(offset + 8)
const method = buffer.readUInt16LE(offset + 10)
const dosTime = buffer.readUInt16LE(offset + 12)
const dosDate = buffer.readUInt16LE(offset + 14)
const crc = buffer.readUInt32LE(offset + 16)
const compressedSize = buffer.readUInt32LE(offset + 20)
const uncompressedSize = buffer.readUInt32LE(offset + 24)
const fileNameLength = buffer.readUInt16LE(offset + 28)
const extraLength = buffer.readUInt16LE(offset + 30)
const commentLength = buffer.readUInt16LE(offset + 32)
const externalAttributes = buffer.readUInt32LE(offset + 38)
const localHeaderOffset = buffer.readUInt32LE(offset + 42)
const name = buffer.toString('utf8', offset + 46, offset + 46 + fileNameLength)
const comment = buffer.subarray(offset + 46 + fileNameLength + extraLength, offset + 46 + fileNameLength + extraLength + commentLength)
const localSignature = buffer.readUInt32LE(localHeaderOffset)
if (localSignature !== 0x04034b50) {
throw new Error(`Invalid local file header signature for ${name}`)
}
const localNameLength = buffer.readUInt16LE(localHeaderOffset + 26)
const localExtraLength = buffer.readUInt16LE(localHeaderOffset + 28)
const dataOffset = localHeaderOffset + 30 + localNameLength + localExtraLength
const compressedData = buffer.subarray(dataOffset, dataOffset + compressedSize)
let data
if (method === ZIP_STORED) {
data = Buffer.from(compressedData)
} else if (method === ZIP_DEFLATED) {
data = zlib.inflateRawSync(compressedData)
} else {
throw new Error(`Unsupported ZIP method ${method} for ${name}`)
}
if (data.length !== uncompressedSize) {
throw new Error(`Unexpected uncompressed size for ${name}: ${data.length} !== ${uncompressedSize}`)
}
entries.push({
name,
data,
method,
flags,
crc,
date: dosToDate(dosDate, dosTime),
comment: Buffer.from(comment),
externalAttributes,
})
offset += 46 + fileNameLength + extraLength + commentLength
}
return entries
}
function createZipBuffer(entries) {
const localParts = []
const centralParts = []
let offset = 0
for (const entry of entries) {
const nameBuffer = Buffer.from(entry.name)
const data = Buffer.isBuffer(entry.data) ? entry.data : Buffer.from(entry.data || '')
const method = entry.method ?? ZIP_DEFLATED
const compressedData = method === ZIP_STORED ? data : zlib.deflateRawSync(data)
const crc = crc32(data)
const { dosDate, dosTime } = dateToDos(entry.date)
const localHeader = Buffer.alloc(30 + nameBuffer.length)
localHeader.writeUInt32LE(0x04034b50, 0)
localHeader.writeUInt16LE(20, 4)
localHeader.writeUInt16LE(0x0800, 6)
localHeader.writeUInt16LE(method, 8)
localHeader.writeUInt16LE(dosTime, 10)
localHeader.writeUInt16LE(dosDate, 12)
localHeader.writeUInt32LE(crc, 14)
localHeader.writeUInt32LE(compressedData.length, 18)
localHeader.writeUInt32LE(data.length, 22)
localHeader.writeUInt16LE(nameBuffer.length, 26)
localHeader.writeUInt16LE(0, 28)
nameBuffer.copy(localHeader, 30)
localParts.push(localHeader, compressedData)
const comment = Buffer.isBuffer(entry.comment) ? entry.comment : Buffer.alloc(0)
const centralHeader = Buffer.alloc(46 + nameBuffer.length + comment.length)
centralHeader.writeUInt32LE(0x02014b50, 0)
centralHeader.writeUInt16LE(20, 4)
centralHeader.writeUInt16LE(20, 6)
centralHeader.writeUInt16LE(0x0800, 8)
centralHeader.writeUInt16LE(method, 10)
centralHeader.writeUInt16LE(dosTime, 12)
centralHeader.writeUInt16LE(dosDate, 14)
centralHeader.writeUInt32LE(crc, 16)
centralHeader.writeUInt32LE(compressedData.length, 20)
centralHeader.writeUInt32LE(data.length, 24)
centralHeader.writeUInt16LE(nameBuffer.length, 28)
centralHeader.writeUInt16LE(0, 30)
centralHeader.writeUInt16LE(comment.length, 32)
centralHeader.writeUInt16LE(0, 34)
centralHeader.writeUInt16LE(0, 36)
centralHeader.writeUInt32LE(entry.externalAttributes || 0, 38)
centralHeader.writeUInt32LE(offset, 42)
nameBuffer.copy(centralHeader, 46)
comment.copy(centralHeader, 46 + nameBuffer.length)
centralParts.push(centralHeader)
offset += localHeader.length + compressedData.length
}
const centralDirectoryOffset = offset
const centralDirectory = Buffer.concat(centralParts)
const centralDirectorySize = centralDirectory.length
const eocd = Buffer.alloc(22)
eocd.writeUInt32LE(0x06054b50, 0)
eocd.writeUInt16LE(0, 4)
eocd.writeUInt16LE(0, 6)
eocd.writeUInt16LE(entries.length, 8)
eocd.writeUInt16LE(entries.length, 10)
eocd.writeUInt32LE(centralDirectorySize, 12)
eocd.writeUInt32LE(centralDirectoryOffset, 16)
eocd.writeUInt16LE(0, 20)
return Buffer.concat([...localParts, centralDirectory, eocd])
}
function findEndOfCentralDirectory(buffer) {
const minOffset = Math.max(0, buffer.length - 0xffff - 22)
for (let offset = buffer.length - 22; offset >= minOffset; offset -= 1) {
if (buffer.readUInt32LE(offset) === 0x06054b50) return offset
}
throw new Error('End of central directory not found')
}
function dateToDos(date) {
const value = date instanceof Date ? date : new Date(1980, 0, 1, 0, 0, 0)
const year = Math.max(1980, value.getFullYear())
return {
dosDate: ((year - 1980) << 9) | ((value.getMonth() + 1) << 5) | value.getDate(),
dosTime: (value.getHours() << 11) | (value.getMinutes() << 5) | Math.floor(value.getSeconds() / 2),
}
}
function dosToDate(dosDate, dosTime) {
const day = dosDate & 0x1f
const month = (dosDate >> 5) & 0x0f
const year = ((dosDate >> 9) & 0x7f) + 1980
const second = (dosTime & 0x1f) * 2
const minute = (dosTime >> 5) & 0x3f
const hour = (dosTime >> 11) & 0x1f
return new Date(year, Math.max(0, month - 1), day || 1, hour, minute, second)
}
function crc32(buffer) {
let crc = 0xffffffff
for (let i = 0; i < buffer.length; i += 1) {
crc = CRC_TABLE[(crc ^ buffer[i]) & 0xff] ^ (crc >>> 8)
}
return (crc ^ 0xffffffff) >>> 0
}
function makeCrcTable() {
const table = new Uint32Array(256)
for (let i = 0; i < 256; i += 1) {
let value = i
for (let bit = 0; bit < 8; bit += 1) {
value = value & 1 ? 0xedb88320 ^ (value >>> 1) : value >>> 1
}
table[i] = value >>> 0
}
return table
}
function formatBytes(bytes) {
return `${(bytes / 1024 / 1024).toFixed(1)} MB`
}
const CRC_TABLE = makeCrcTable()
module.exports = afterPack
module.exports.default = afterPack
module.exports.createZipBuffer = createZipBuffer
module.exports.readZipEntries = readZipEntries
module.exports.resolveDriverDirectory = resolveDriverDirectory
module.exports.trimDriverBundleInAppJar = trimDriverBundleInAppJar
module.exports.ZIP_STORED = ZIP_STORED
module.exports.ZIP_DEFLATED = ZIP_DEFLATED