#!/usr/bin/env -S npx ts-node --resolveJsonModule import * as path from "path"; import { createWriteStream, promises as fs } from "fs"; import * as childProcess from "child_process"; import tar from "tar"; import * as asar from "@electron/asar"; import fetch from "node-fetch"; import { promises as stream } from "stream"; import riotDesktopPackageJson from "../package.json"; import { setPackageVersion } from "./set-version"; const PUB_KEY_URL = "https://packages.riot.im/element-release-key.asc"; const PACKAGE_URL_PREFIX = "https://github.com/element-hq/element-web/releases/download/"; const DEVELOP_TGZ_URL = "https://develop.element.io/develop.tar.gz"; const ASAR_PATH = "webapp.asar"; async function downloadToFile(url: string, filename: string): Promise { console.log("Downloading " + url + "..."); try { const resp = await fetch(url); if (!resp.ok) throw new Error(`unexpected response ${resp.statusText}`); if (!resp.body) throw new Error(`unexpected response has no body ${resp.statusText}`); await stream.pipeline(resp.body, createWriteStream(filename)); } catch (e) { console.error(e); try { await fs.unlink(filename); } catch (_) {} throw e; } } async function verifyFile(filename: string): Promise { return new Promise((resolve, reject) => { childProcess.execFile("gpg", ["--verify", filename + ".asc", filename], (error) => { if (error) { reject(error); } else { resolve(); } }); }); } async function main(): Promise { let verify = true; let importkey = false; let pkgDir = "packages"; let deployDir = "deploys"; let cfgDir: string | undefined; let targetVersion: string | undefined; let filename: string | undefined; let url: string | undefined; let setVersion = false; while (process.argv.length > 2) { switch (process.argv[2]) { case "--noverify": verify = false; break; case "--importkey": importkey = true; break; case "--packages": process.argv.shift(); pkgDir = process.argv[2]; break; case "--deploys": process.argv.shift(); deployDir = process.argv[2]; break; case "--cfgdir": case "-d": process.argv.shift(); cfgDir = process.argv[2]; break; default: targetVersion = process.argv[2]; } process.argv.shift(); } if (targetVersion === undefined) { targetVersion = "v" + riotDesktopPackageJson.version; } else if (targetVersion !== "develop") { setVersion = true; // version was specified } if (targetVersion === "develop") { filename = "develop.tar.gz"; url = DEVELOP_TGZ_URL; verify = false; // develop builds aren't signed } else if (targetVersion.includes("://")) { filename = targetVersion.substring(targetVersion.lastIndexOf("/") + 1); url = targetVersion; verify = false; // manually verified } else { filename = `element-${targetVersion}.tar.gz`; url = PACKAGE_URL_PREFIX + targetVersion + "/" + filename; } const haveGpg = await new Promise((resolve) => { childProcess.execFile("gpg", ["--version"], (error) => { resolve(!error); }); }); if (importkey) { if (!haveGpg) { console.log("Can't import key without working GPG binary: install GPG and try again"); return 1; } await new Promise((resolve, reject) => { const gpgProc = childProcess.execFile("gpg", ["--import"], (error) => { if (error) { console.log("Failed to import key", error); } else { console.log("Key imported!"); } resolve(!error); }); fetch(PUB_KEY_URL) .then((resp) => { stream.pipeline(resp.body, gpgProc.stdin!).catch(reject); }) .catch(reject); }); return 0; } if (cfgDir === undefined) { console.log("No config directory set"); console.log("Specify a config directory with --cfgdir or -d"); console.log("To build with no config (and no auto-update), pass the empty string (-d '')"); return 1; } if (verify && !haveGpg) { console.log("No working GPG binary: install GPG or pass --noverify to skip verification"); return 1; } let haveDeploy = false; let expectedDeployDir = path.join(deployDir, path.basename(filename).replace(/\.tar\.gz/, "")); try { await fs.opendir(expectedDeployDir); console.log(expectedDeployDir + "already exists"); haveDeploy = true; } catch (e) {} if (!haveDeploy) { const outPath = path.join(pkgDir, filename); try { await fs.stat(outPath); console.log("Already have " + filename + ": not redownloading"); } catch (e) { try { await downloadToFile(url, outPath); } catch (e) { console.log("Failed to download " + url, e); return 1; } } if (verify) { try { await fs.stat(outPath + ".asc"); console.log("Already have " + filename + ".asc: not redownloading"); } catch (e) { try { await downloadToFile(url + ".asc", outPath + ".asc"); } catch (e) { console.log("Failed to download " + url, e); return 1; } } try { await verifyFile(outPath); console.log(outPath + " downloaded and verified"); } catch (e) { console.log("Signature verification failed!", e); return 1; } } else { console.log(outPath + " downloaded but NOT verified"); } await tar.x({ file: outPath, cwd: deployDir, onentry: (entry) => { // Find the appropriate extraction path, only needed for `develop` where the dir name is unknown if (entry.type === "Directory" && !path.join(deployDir, entry.path).startsWith(expectedDeployDir)) { expectedDeployDir = path.join(deployDir, entry.path); } }, }); } try { await fs.stat(ASAR_PATH); console.log(ASAR_PATH + " already present: removing"); await fs.unlink(ASAR_PATH); } catch (e) {} if (cfgDir.length) { const configJsonSource = path.join(cfgDir, "config.json"); const configJsonDest = path.join(expectedDeployDir, "config.json"); console.log(configJsonSource + " -> " + configJsonDest); await fs.copyFile(configJsonSource, configJsonDest); } else { console.log("Skipping config file"); } console.log("Pack " + expectedDeployDir + " -> " + ASAR_PATH); await asar.createPackage(expectedDeployDir, ASAR_PATH); if (setVersion) { const semVer = (await fs.readFile(path.join(expectedDeployDir, "version"), "utf-8")).trim(); console.log("Updating version to " + semVer); await setPackageVersion(semVer); } console.log("Done!"); } main() .then((ret) => { process.exit(ret); }) .catch((e) => { console.error(e); process.exit(1); });