|
|
|
@@ -0,0 +1,407 @@
|
|
|
|
|
#!/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,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Minimum expected AppImage file size (50 MB)
|
|
|
|
|
const APPIMAGE_MIN_SIZE_MB = 50;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Verify AppImage contents.
|
|
|
|
|
* AppImages are ELF executables with an embedded SquashFS filesystem.
|
|
|
|
|
* We try unsquashfs first (can list SquashFS contents), then fall back
|
|
|
|
|
* to the AppImage's own --appimage-extract, and finally to a size check.
|
|
|
|
|
*/
|
|
|
|
|
function verifyAppImage(appImagePath) {
|
|
|
|
|
logInfo(`Verifying AppImage: ${path.basename(appImagePath)}`);
|
|
|
|
|
|
|
|
|
|
// Try unsquashfs -l (lists squashfs contents without extracting)
|
|
|
|
|
if (commandExists('unsquashfs')) {
|
|
|
|
|
const result = spawnSync('unsquashfs', ['-l', appImagePath], {
|
|
|
|
|
stdio: 'pipe',
|
|
|
|
|
encoding: 'utf-8',
|
|
|
|
|
maxBuffer: 50 * 1024 * 1024,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (result.error) {
|
|
|
|
|
logWarning(`unsquashfs failed: ${result.error.message}, falling back to size check`);
|
|
|
|
|
} else if (result.status !== 0) {
|
|
|
|
|
logWarning(`unsquashfs could not read AppImage, falling back to size check`);
|
|
|
|
|
} else {
|
|
|
|
|
const files = result.stdout.split('\n');
|
|
|
|
|
return verifyFileList(files, 'AppImage');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Try self-extraction to list contents (AppImages support --appimage-extract-and-run)
|
|
|
|
|
// Make the AppImage executable first
|
|
|
|
|
try {
|
|
|
|
|
fs.chmodSync(appImagePath, 0o755);
|
|
|
|
|
} catch (_) {
|
|
|
|
|
// Ignore chmod errors
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const extractResult = spawnSync(appImagePath, ['--appimage-extract', '--stdout'], {
|
|
|
|
|
stdio: 'pipe',
|
|
|
|
|
encoding: 'utf-8',
|
|
|
|
|
maxBuffer: 50 * 1024 * 1024,
|
|
|
|
|
timeout: 30000,
|
|
|
|
|
env: { ...process.env, APPIMAGE_EXTRACT_AND_RUN: '1' },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// --appimage-extract creates a squashfs-root directory; check if it exists
|
|
|
|
|
const squashfsRoot = path.join(path.dirname(appImagePath), 'squashfs-root');
|
|
|
|
|
if (fs.existsSync(squashfsRoot)) {
|
|
|
|
|
try {
|
|
|
|
|
const collectFiles = (dir, prefix = '') => {
|
|
|
|
|
const entries = [];
|
|
|
|
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
|
|
|
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
|
|
|
entries.push(rel);
|
|
|
|
|
if (entry.isDirectory()) {
|
|
|
|
|
entries.push(...collectFiles(path.join(dir, entry.name), rel));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return entries;
|
|
|
|
|
};
|
|
|
|
|
const files = collectFiles(squashfsRoot);
|
|
|
|
|
const verifyResult = verifyFileList(files, 'AppImage');
|
|
|
|
|
// Clean up extracted directory
|
|
|
|
|
fs.rmSync(squashfsRoot, { recursive: true, force: true });
|
|
|
|
|
return verifyResult;
|
|
|
|
|
} catch (e) {
|
|
|
|
|
logWarning(`Failed to read extracted AppImage contents: ${e.message}`);
|
|
|
|
|
fs.rmSync(squashfsRoot, { recursive: true, force: true });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Fall back to basic size validation (same approach as Flatpak)
|
|
|
|
|
logWarning('Could not inspect AppImage contents (unsquashfs not available). Using size validation.');
|
|
|
|
|
const issues = [];
|
|
|
|
|
const stats = fs.statSync(appImagePath);
|
|
|
|
|
|
|
|
|
|
if (stats.size === 0) {
|
|
|
|
|
return { verified: false, issues: ['AppImage file is empty'] };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (stats.size < APPIMAGE_MIN_SIZE_MB * 1024 * 1024) {
|
|
|
|
|
issues.push(
|
|
|
|
|
`AppImage file seems too small (${(stats.size / 1024 / 1024).toFixed(2)} MB, expected at least ${APPIMAGE_MIN_SIZE_MB} MB)`,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (issues.length === 0) {
|
|
|
|
|
logInfo('AppImage passed size validation (content inspection was not possible)');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
verified: issues.length === 0,
|
|
|
|
|
issues,
|
|
|
|
|
size: stats.size,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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(' - unsquashfs: sudo apt-get install squashfs-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,
|
|
|
|
|
};
|