const https = require('https'); const fs = require('fs'); const path = require('path'); const extract = require('extract-zip'); const GITHUB_REPO = 'simplex-chat/simplex-chat-libs'; const RELEASE_TAG = 'v7.1.0-beta.4'; const BACKEND = (process.env.SIMPLEX_BACKEND || process.env.npm_config_simplex_backend || 'sqlite').toLowerCase(); // A locally built libsimplex, copied into libs/ rather than loaded in place: // the addon's RUNPATH is $ORIGIN/../../libs. const LIBS_DIR_OVERRIDE = process.env.SIMPLEX_LIBS_DIR || process.env.npm_config_simplex_libs_dir; const LIB_NAMES = ['libsimplex.so', 'libsimplex.dylib', 'libsimplex.dll']; if (BACKEND !== 'sqlite' && BACKEND !== 'postgres') { console.error(`✗ Invalid SIMPLEX_BACKEND: "${BACKEND}". Must be "sqlite" or "postgres".`); process.exit(1); } if (BACKEND === 'postgres' && (process.platform !== 'linux' || process.arch !== 'x64')) { console.error(`✗ SIMPLEX_BACKEND=postgres is only supported on Linux x86_64.`); process.exit(1); } const ROOT_DIR = process.cwd(); // Root of the package being installed const LIBS_DIR = path.join(ROOT_DIR, 'libs') const INSTALLED_FILE = path.join(LIBS_DIR, 'installed.txt'); // Detect platform and architecture function getPlatformInfo() { const platform = process.platform; const arch = process.arch; let platformName; let archName; if (platform === 'linux') { platformName = 'linux'; } else if (platform === 'darwin') { platformName = 'macos'; } else if (platform === 'win32') { platformName = 'windows'; } else { throw new Error(`Unsupported platform: ${platform}`); } if (arch === 'x64') { archName = 'x86_64'; } else if (arch === 'arm64') { archName = 'aarch64'; } else { throw new Error(`Unsupported architecture: ${arch}`); } return { platformName, archName }; } // Cleanup on libs version mismatch function cleanLibsDirectory() { if (fs.existsSync(LIBS_DIR)) { console.log('Cleaning old libraries...'); fs.rmSync(LIBS_DIR, { recursive: true, force: true }); fs.mkdirSync(LIBS_DIR, { recursive: true }); console.log('✓ Old libraries removed'); } } // Check if libraries are already installed with the correct version function isAlreadyInstalled() { if (!fs.existsSync(INSTALLED_FILE)) { return false; } try { const installedVersion = fs.readFileSync(INSTALLED_FILE, 'utf-8').trim(); const expectedVersion = `${RELEASE_TAG}:${BACKEND}`; if (installedVersion === expectedVersion) { console.log(`✓ Libraries version ${RELEASE_TAG}:${BACKEND} already installed`); return true; } else { console.log(`Version mismatch: installed ${installedVersion}, need ${expectedVersion}`); cleanLibsDirectory(); return false; } } catch (err) { console.warn(`Could not read installed.txt: ${err.message}`); return false; } } // No version check: the files behind SIMPLEX_LIBS_DIR change on every rebuild. function installFromOverride() { if (!fs.existsSync(LIBS_DIR_OVERRIDE)) { throw new Error(`SIMPLEX_LIBS_DIR does not exist: ${LIBS_DIR_OVERRIDE}`); } const lib = LIB_NAMES.find((name) => fs.existsSync(path.join(LIBS_DIR_OVERRIDE, name))); if (!lib) { throw new Error(`No ${LIB_NAMES.join(' / ')} in SIMPLEX_LIBS_DIR: ${LIBS_DIR_OVERRIDE}`); } console.log(`Using libraries from SIMPLEX_LIBS_DIR: ${LIBS_DIR_OVERRIDE}`); cleanLibsDirectory(); copyDirSync(LIBS_DIR_OVERRIDE, LIBS_DIR); // Not a release tag: a later install without the variable sees a mismatch // and replaces these files with the released ones. fs.writeFileSync(INSTALLED_FILE, `${LIBS_DIR_OVERRIDE}:${BACKEND}`, 'utf-8'); console.log(`✓ Installed ${lib} and its runtime libraries from ${LIBS_DIR_OVERRIDE}`); } async function install() { try { if (LIBS_DIR_OVERRIDE) { installFromOverride(); return; } // Check if already installed if (isAlreadyInstalled()) { return; } const { platformName, archName } = getPlatformInfo(); const repoName = GITHUB_REPO.split('/')[1]; const backendSuffix = BACKEND === 'postgres' ? '-postgres' : ''; const zipFilename = `${repoName}-${platformName}-${archName}${backendSuffix}.zip`; const ZIP_URL = `https://github.com/${GITHUB_REPO}/releases/download/${RELEASE_TAG}/${zipFilename}`; const ZIP_PATH = path.join(ROOT_DIR, zipFilename); const TEMP_EXTRACT_DIR = path.join(ROOT_DIR, '.temp-extract'); console.log(`Detected: ${platformName} ${archName}`); console.log(`Backend: ${BACKEND}`); console.log(`Downloading: ${zipFilename}`); // Create libs directory if (!fs.existsSync(LIBS_DIR)) { fs.mkdirSync(LIBS_DIR, { recursive: true }); } // Download zip with error handling await downloadFile(ZIP_URL, ZIP_PATH); // Extract to temporary directory console.log('Extracting to temporary directory...'); if (!fs.existsSync(TEMP_EXTRACT_DIR)) { fs.mkdirSync(TEMP_EXTRACT_DIR, { recursive: true }); } await extract(ZIP_PATH, { dir: TEMP_EXTRACT_DIR }); // Move libs folder contents to final location console.log('Moving libraries to libs/...'); const libsSourcePath = path.join(TEMP_EXTRACT_DIR, 'libs'); if (fs.existsSync(libsSourcePath)) { // Copy all files from libs folder to LIBS_DIR const files = fs.readdirSync(libsSourcePath); files.forEach(file => { const src = path.join(libsSourcePath, file); const dest = path.join(LIBS_DIR, file); if (fs.statSync(src).isDirectory()) { copyDirSync(src, dest); } else { fs.copyFileSync(src, dest); } }); } else { throw new Error('libs folder not found in zip archive'); } // Write installed.txt with version fs.writeFileSync(INSTALLED_FILE, `${RELEASE_TAG}:${BACKEND}`, 'utf-8'); console.log(`✓ Wrote version ${RELEASE_TAG}:${BACKEND} to installed.txt`); // Cleanup fs.rmSync(TEMP_EXTRACT_DIR, { recursive: true, force: true }); fs.unlinkSync(ZIP_PATH); console.log('✓ Installation complete'); } catch (err) { console.error('✗ Failed:', err.message); process.exit(1); } } // Helper function to recursively copy directories function copyDirSync(src, dest) { if (!fs.existsSync(dest)) { fs.mkdirSync(dest, { recursive: true }); } const files = fs.readdirSync(src); files.forEach(file => { const srcFile = path.join(src, file); const destFile = path.join(dest, file); if (fs.statSync(srcFile).isDirectory()) { copyDirSync(srcFile, destFile); } else { fs.copyFileSync(srcFile, destFile); } }); } function downloadFile(url, dest) { return new Promise((resolve, reject) => { const file = fs.createWriteStream(dest); https.get(url, { headers: { 'User-Agent': 'Node.js' } }, (response) => { // Handle redirects if (response.statusCode === 302 || response.statusCode === 301) { file.destroy(); fs.unlink(dest, () => {}); return downloadFile(response.headers.location, dest) .then(resolve) .catch(reject); } // Handle 404 if (response.statusCode === 404) { file.destroy(); fs.unlink(dest, () => {}); reject(new Error( `Release artifact not found (404). Check:\n` + ` - Repository exists: ${url.split('/releases')[0]}\n` + ` - Release tag exists: ${RELEASE_TAG}\n` + ` - Artifact filename is correct` )); return; } // Handle 403 if (response.statusCode === 403) { file.destroy(); fs.unlink(dest, () => {}); reject(new Error( `Access denied (403). The repository may be private.\n` + `Set GITHUB_TOKEN environment variable for private repos.` )); return; } // Handle other HTTP errors if (response.statusCode < 200 || response.statusCode >= 300) { file.destroy(); fs.unlink(dest, () => {}); reject(new Error( `HTTP ${response.statusCode}: Failed to download from ${url}` )); return; } response.pipe(file); file.on('finish', () => { file.close(); resolve(); }); file.on('error', (err) => { fs.unlink(dest, () => {}); reject(new Error(`File write error: ${err.message}`)); }); }).on('error', (err) => { file.destroy(); fs.unlink(dest, () => {}); reject(new Error(`Download error: ${err.message}`)); }); }); } install();