diff --git a/apps/desktop/scripts/verify-linux-packages.cjs b/apps/desktop/scripts/verify-linux-packages.cjs new file mode 100644 index 00000000..92c128c4 --- /dev/null +++ b/apps/desktop/scripts/verify-linux-packages.cjs @@ -0,0 +1,341 @@ +#!/usr/bin/env node +/** + * Verify Linux package contents to ensure AppImage, deb, and Flatpak were built correctly. + * + * This script inspects each Linux package format to verify that the bundled Electron + * application (app.asar) is present and packages are valid. + * + * Usage: node scripts/verify-linux-packages.cjs [dist-dir] + */ + +const fs = require('fs'); +const path = require('path'); +const { spawnSync } = require('child_process'); + +// Minimum expected Flatpak file size (50 MB) +// Flatpak files are large OCI archives; anything smaller is suspicious +const FLATPAK_MIN_SIZE_MB = 50; + +// Colors for terminal output +const colors = { + reset: '\x1b[0m', + red: '\x1b[31m', + green: '\x1b[32m', + yellow: '\x1b[33m', + blue: '\x1b[34m', + cyan: '\x1b[36m', +}; + +function log(message, color = colors.reset) { + console.log(`${color}${message}${colors.reset}`); +} + +function logSuccess(message) { + log(`\u2713 ${message}`, colors.green); +} + +function logError(message) { + log(`\u2717 ${message}`, colors.red); +} + +function logWarning(message) { + log(`\u26A0 ${message}`, colors.yellow); +} + +function logInfo(message) { + log(`\u2139 ${message}`, colors.cyan); +} + +/** + * Check if a command exists + * Uses 'which' directly without shell interpolation to prevent command injection + */ +function commandExists(cmd) { + const result = spawnSync('which', [cmd], { stdio: 'ignore' }); + return result.status === 0; +} + +/** + * Find all Linux packages in the dist directory + */ +function findPackages(distDir) { + const packages = { + appImage: null, + deb: null, + flatpak: null, + }; + + if (!fs.existsSync(distDir)) { + logError(`Distribution directory not found: ${distDir}`); + return packages; + } + + const files = fs.readdirSync(distDir); + + for (const file of files) { + const fullPath = path.join(distDir, file); + + if (file.endsWith('.AppImage')) { + if (!packages.appImage) { + packages.appImage = fullPath; + } else { + logWarning(`Multiple AppImage files found, using first: ${path.basename(packages.appImage)}`); + } + } else if (file.endsWith('.deb')) { + if (!packages.deb) { + packages.deb = fullPath; + } else { + logWarning(`Multiple deb files found, using first: ${path.basename(packages.deb)}`); + } + } else if (file.endsWith('.flatpak')) { + if (!packages.flatpak) { + packages.flatpak = fullPath; + } else { + logWarning(`Multiple Flatpak files found, using first: ${path.basename(packages.flatpak)}`); + } + } + } + + return packages; +} + +/** + * Verify that a file listing contains the bundled Electron app (app.asar) + * @param {string[]} files - List of files from package + * @param {string} packageType - Type of package (for error messages) + * @returns {Object} Verification result with verified flag and issues array + */ +function verifyFileList(files, packageType) { + const issues = []; + + // Check for app.asar (the bundled Electron application) + // Use boundary-safe match to avoid false positives from resources/app.asar.unpacked + const appAsarPattern = /[\\/]resources[\\/]app\.asar$/; + const appAsarFound = files.some((f) => appAsarPattern.test(f.trim())); + if (!appAsarFound) { + issues.push(`app.asar not found in ${packageType} — the Electron app bundle is missing`); + } + + return { + verified: issues.length === 0, + issues, + fileCount: files.filter((f) => f.trim()).length, + }; +} + +/** + * Verify AppImage contents using bsdtar (libarchive) + */ +function verifyAppImage(appImagePath) { + logInfo(`Verifying AppImage: ${path.basename(appImagePath)}`); + + if (!commandExists('bsdtar')) { + logWarning('bsdtar not found. Install with: sudo apt-get install libarchive-tools'); + logWarning('Skipping AppImage verification'); + return { verified: false, reason: 'bsdtar not available', critical: true }; + } + + const result = spawnSync('bsdtar', ['-t', '-f', appImagePath], { + stdio: 'pipe', + encoding: 'utf-8', + maxBuffer: 50 * 1024 * 1024, + }); + + if (result.error) { + logError(`Failed to execute bsdtar: ${result.error.message}`); + return { verified: false, issues: [`Command execution failed: ${result.error.message}`] }; + } + + if (result.status !== 0) { + logError(`Failed to read AppImage: ${result.stderr}`); + return { verified: false, issues: ['Failed to extract file list'] }; + } + + const files = result.stdout.split('\n'); + return verifyFileList(files, 'AppImage'); +} + +/** + * Verify deb package contents + */ +function verifyDeb(debPath) { + logInfo(`Verifying deb package: ${path.basename(debPath)}`); + + if (!commandExists('dpkg-deb')) { + logWarning('dpkg-deb not found. Skipping deb verification'); + return { verified: false, reason: 'dpkg-deb not available', critical: true }; + } + + const result = spawnSync('dpkg-deb', ['-c', debPath], { + stdio: 'pipe', + encoding: 'utf-8', + maxBuffer: 50 * 1024 * 1024, + }); + + if (result.error) { + logError(`Failed to execute dpkg-deb: ${result.error.message}`); + return { verified: false, issues: [`Command execution failed: ${result.error.message}`] }; + } + + if (result.status !== 0) { + logError(`Failed to read deb package: ${result.stderr}`); + return { verified: false, issues: ['Failed to extract file list'] }; + } + + const files = result.stdout.split('\n'); + return verifyFileList(files, 'deb package'); +} + +/** + * Verify Flatpak package contents + * Flatpak OCI archives are complex to inspect, so we do basic validation + */ +function verifyFlatpak(flatpakPath) { + logInfo(`Verifying Flatpak package: ${path.basename(flatpakPath)}`); + + const issues = []; + + if (!fs.existsSync(flatpakPath)) { + return { verified: false, issues: ['Flatpak file does not exist'] }; + } + + const stats = fs.statSync(flatpakPath); + if (stats.size === 0) { + return { verified: false, issues: ['Flatpak file is empty'] }; + } + + if (stats.size < FLATPAK_MIN_SIZE_MB * 1024 * 1024) { + issues.push( + `Flatpak file seems too small (${(stats.size / 1024 / 1024).toFixed(2)} MB, expected at least ${FLATPAK_MIN_SIZE_MB} MB)`, + ); + } + + return { + verified: issues.length === 0, + issues, + size: stats.size, + }; +} + +/** + * Main verification function + */ +function main() { + const distDir = process.argv[2] || path.join(__dirname, '..', 'dist'); + + log('\n=== Linux Package Verification ===\n', colors.blue); + logInfo(`Distribution directory: ${distDir}\n`); + + const packages = findPackages(distDir); + + // Report found packages — all three targets are required + let missingTargets = false; + + if (packages.appImage) { + logSuccess(`Found AppImage: ${path.basename(packages.appImage)}`); + } else { + logError('No AppImage found — expected build target is missing'); + missingTargets = true; + } + + if (packages.deb) { + logSuccess(`Found deb: ${path.basename(packages.deb)}`); + } else { + logError('No deb package found — expected build target is missing'); + missingTargets = true; + } + + if (packages.flatpak) { + logSuccess(`Found Flatpak: ${path.basename(packages.flatpak)}`); + } else { + logError('No Flatpak package found — expected build target is missing'); + missingTargets = true; + } + + if (missingTargets) { + logError('\nOne or more expected Linux package targets are missing!'); + process.exit(1); + } + + log(''); + + // Verify each package + const results = {}; + + if (packages.appImage) { + results.appImage = verifyAppImage(packages.appImage); + } + + if (packages.deb) { + results.deb = verifyDeb(packages.deb); + } + + if (packages.flatpak) { + results.flatpak = verifyFlatpak(packages.flatpak); + } + + // Print results + log('\n=== Verification Results ===\n', colors.blue); + + let hasFailures = false; + let hasCriticalSkips = false; + + for (const [type, result] of Object.entries(results)) { + if (result.reason) { + if (result.critical) { + logError(`${type}: CRITICAL - SKIPPED (${result.reason})`); + hasCriticalSkips = true; + } else { + logWarning(`${type}: SKIPPED (${result.reason})`); + } + } else if (result.verified) { + logSuccess(`${type}: VERIFIED`); + if (result.fileCount) { + logInfo(` Files: ${result.fileCount}`); + } + if (result.size) { + logInfo(` Size: ${(result.size / 1024 / 1024).toFixed(2)} MB`); + } + } else { + logError(`${type}: FAILED`); + hasFailures = true; + for (const issue of result.issues || []) { + logError(` - ${issue}`); + } + } + } + + log(''); + + if (hasFailures || hasCriticalSkips) { + logError('\n=== VERIFICATION FAILED ===\n'); + if (hasFailures) { + log('Some packages are missing critical files. This will cause runtime errors.\n', colors.red); + } + if (hasCriticalSkips) { + log('Some packages could not be verified due to missing required tools.\n', colors.red); + log('Install required tools:\n', colors.red); + log(' - bsdtar: sudo apt-get install libarchive-tools\n', colors.red); + log(' - dpkg-deb: sudo apt-get install dpkg\n', colors.red); + } + process.exit(1); + } else { + logSuccess('\n=== ALL PACKAGES VERIFIED ===\n'); + log('All Linux packages contain the required files.\n', colors.green); + process.exit(0); + } +} + +// Only run main if this file is executed directly (not imported) +if (require.main === module) { + main(); +} + +// Export for testing +module.exports = { + findPackages, + verifyFileList, + verifyAppImage, + verifyDeb, + verifyFlatpak, +};