Files
fourdst/electron/validate-dependencies.js

345 lines
12 KiB
JavaScript
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* Dependency Validation Script for 4DSTAR Electron App
*
* This script validates that all runtime dependencies are properly embedded
* and available in the packaged application.
*/
const fs = require('fs');
const path = require('path');
const { spawn } = require('child_process');
class DependencyValidator {
constructor() {
this.errors = [];
this.warnings = [];
this.projectRoot = path.resolve(__dirname, '..');
this.buildDir = path.join(this.projectRoot, 'build');
this.electronDir = __dirname;
}
log(message, type = 'info') {
const timestamp = new Date().toISOString();
const prefix = {
'info': '📋',
'success': '✅',
'warning': '⚠️',
'error': '❌'
}[type] || '';
console.log(`${prefix} [${timestamp}] ${message}`);
if (type === 'error') {
this.errors.push(message);
} else if (type === 'warning') {
this.warnings.push(message);
}
}
async validatePythonBackend() {
this.log('Validating Python backend dependencies...', 'info');
// Check if backend executable exists
const executableName = process.platform === 'win32' ? 'fourdst-backend.exe' : 'fourdst-backend';
const backendPath = path.join(this.buildDir, 'electron', 'dist', 'fourdst-backend', executableName);
if (!fs.existsSync(backendPath)) {
this.log(`Backend executable not found: ${backendPath}`, 'error');
return false;
}
this.log(`Backend executable found: ${backendPath}`, 'success');
// Check backend executable permissions
try {
const stats = fs.statSync(backendPath);
const isExecutable = !!(stats.mode & parseInt('111', 8));
if (!isExecutable) {
this.log('Backend executable lacks execute permissions', 'error');
return false;
}
this.log('Backend executable has proper permissions', 'success');
} catch (e) {
this.log(`Failed to check backend permissions: ${e.message}`, 'error');
return false;
}
// Test backend execution
return new Promise((resolve) => {
this.log('Testing backend execution...', 'info');
const testArgs = ['inspect_bundle', JSON.stringify({ bundle_path: '/nonexistent/test.fbundle' })];
const process = spawn(backendPath, testArgs, { timeout: 10000 });
let stdout = '';
let stderr = '';
process.stdout.on('data', (data) => {
stdout += data.toString();
});
process.stderr.on('data', (data) => {
stderr += data.toString();
});
process.on('close', (code) => {
// We expect this to fail since the bundle doesn't exist,
// but it should fail gracefully with JSON output
if (stdout.length > 0) {
try {
const result = JSON.parse(stdout.trim());
if (result.success === false && result.error) {
this.log('Backend produces valid JSON error responses', 'success');
resolve(true);
} else {
this.log('Backend JSON response format unexpected', 'warning');
resolve(true);
}
} catch (e) {
this.log(`Backend output is not valid JSON: ${e.message}`, 'error');
this.log(`Raw stdout: "${stdout.substring(0, 200)}"`, 'error');
resolve(false);
}
} else {
this.log('Backend produced no stdout output', 'error');
this.log(`Stderr: ${stderr}`, 'error');
resolve(false);
}
});
process.on('error', (err) => {
this.log(`Failed to execute backend: ${err.message}`, 'error');
resolve(false);
});
});
}
validateNodeDependencies() {
this.log('Validating Node.js dependencies...', 'info');
const packageJsonPath = path.join(this.electronDir, 'package.json');
if (!fs.existsSync(packageJsonPath)) {
this.log('package.json not found', 'error');
return false;
}
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
const dependencies = { ...packageJson.dependencies, ...packageJson.devDependencies };
let allFound = true;
for (const [dep, version] of Object.entries(dependencies)) {
const depPath = path.join(this.electronDir, 'node_modules', dep);
if (fs.existsSync(depPath)) {
this.log(`${dep}@${version}`, 'success');
} else {
this.log(`${dep}@${version} not found in node_modules`, 'error');
allFound = false;
}
}
// Check for native modules that might need special handling
const nativeModules = ['@electron/remote', 'python-shell'];
for (const mod of nativeModules) {
if (dependencies[mod]) {
const modPath = path.join(this.electronDir, 'node_modules', mod);
if (fs.existsSync(modPath)) {
// Check for native binaries
const hasNativeBinaries = this.findNativeBinaries(modPath);
if (hasNativeBinaries.length > 0) {
this.log(`Native binaries found in ${mod}: ${hasNativeBinaries.join(', ')}`, 'info');
}
}
}
}
return allFound;
}
findNativeBinaries(dir) {
const nativeExtensions = ['.node', '.so', '.dylib', '.dll'];
const binaries = [];
try {
const files = fs.readdirSync(dir, { recursive: true });
for (const file of files) {
const ext = path.extname(file);
if (nativeExtensions.includes(ext)) {
binaries.push(file);
}
}
} catch (e) {
// Directory might not exist or be accessible
}
return binaries;
}
validateElectronBuild() {
this.log('Validating Electron build configuration...', 'info');
const packageJsonPath = path.join(this.electronDir, 'package.json');
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
const buildConfig = packageJson.build;
if (!buildConfig) {
this.log('No build configuration found in package.json', 'error');
return false;
}
// Check extraResources configuration
if (!buildConfig.extraResources || !Array.isArray(buildConfig.extraResources)) {
this.log('No extraResources configuration found', 'error');
return false;
}
// Validate backend resource mapping
const backendResource = buildConfig.extraResources.find(res => res.to === 'backend/');
if (!backendResource) {
this.log('Backend resource mapping not found in extraResources', 'error');
return false;
}
// Check if source directory exists
const backendSourcePath = path.resolve(this.electronDir, backendResource.from);
if (!fs.existsSync(backendSourcePath)) {
this.log(`Backend source directory not found: ${backendSourcePath}`, 'error');
return false;
}
this.log('Electron build configuration validated', 'success');
return true;
}
validatePyInstallerSpec() {
this.log('Validating PyInstaller spec file...', 'info');
const specPath = path.join(this.electronDir, 'fourdst-backend.spec');
if (!fs.existsSync(specPath)) {
this.log('PyInstaller spec file not found', 'error');
return false;
}
const specContent = fs.readFileSync(specPath, 'utf8');
// Check for essential hidden imports
const requiredImports = [
'docker',
'cryptography',
'yaml',
'fourdst.core'
];
for (const imp of requiredImports) {
if (!specContent.includes(`'${imp}'`)) {
this.log(`Missing hidden import in spec: ${imp}`, 'warning');
} else {
this.log(`✓ Hidden import found: ${imp}`, 'success');
}
}
return true;
}
validateFileStructure() {
this.log('Validating project file structure...', 'info');
const requiredFiles = [
'package.json',
'main-refactored.js',
'bridge.py',
'fourdst-backend.spec',
'entitlements.mac.plist'
];
let allFound = true;
for (const file of requiredFiles) {
const filePath = path.join(this.electronDir, file);
if (fs.existsSync(filePath)) {
this.log(`${file}`, 'success');
} else {
this.log(`${file} not found`, 'error');
allFound = false;
}
}
// Check for main modules
const mainModulesDir = path.join(this.electronDir, 'main');
if (fs.existsSync(mainModulesDir)) {
this.log('✓ Main process modules directory found', 'success');
} else {
this.log('✗ Main process modules directory not found', 'error');
allFound = false;
}
// Check for renderer modules
const rendererModulesDir = path.join(this.electronDir, 'renderer');
if (fs.existsSync(rendererModulesDir)) {
this.log('✓ Renderer process modules directory found', 'success');
} else {
this.log('✗ Renderer process modules directory not found', 'error');
allFound = false;
}
return allFound;
}
async runValidation() {
this.log('Starting comprehensive dependency validation...', 'info');
this.log(`Project root: ${this.projectRoot}`, 'info');
this.log(`Electron directory: ${this.electronDir}`, 'info');
this.log(`Build directory: ${this.buildDir}`, 'info');
const results = {
fileStructure: this.validateFileStructure(),
nodeDependencies: this.validateNodeDependencies(),
electronBuild: this.validateElectronBuild(),
pyinstallerSpec: this.validatePyInstallerSpec(),
pythonBackend: await this.validatePythonBackend()
};
this.log('\n=== VALIDATION SUMMARY ===', 'info');
let allPassed = true;
for (const [test, passed] of Object.entries(results)) {
const status = passed ? '✅ PASS' : '❌ FAIL';
this.log(`${test}: ${status}`, passed ? 'success' : 'error');
if (!passed) allPassed = false;
}
if (this.warnings.length > 0) {
this.log(`\n⚠️ ${this.warnings.length} warnings found:`, 'warning');
this.warnings.forEach(warning => this.log(` - ${warning}`, 'warning'));
}
if (this.errors.length > 0) {
this.log(`\n${this.errors.length} errors found:`, 'error');
this.errors.forEach(error => this.log(` - ${error}`, 'error'));
}
if (allPassed && this.errors.length === 0) {
this.log('\n🎉 All validations passed! The app should be fully self-contained.', 'success');
return true;
} else {
this.log('\n💥 Validation failed. Please fix the issues above before packaging.', 'error');
return false;
}
}
}
// Run validation if called directly
if (require.main === module) {
const validator = new DependencyValidator();
validator.runValidation().then(success => {
process.exit(success ? 0 : 1);
}).catch(error => {
console.error('Validation failed with error:', error);
process.exit(1);
});
}
module.exports = { DependencyValidator };