From 7c89359da4d3f79e71bff18d646441ec45d5dd90 Mon Sep 17 00:00:00 2001 From: Samuel Attard Date: Wed, 28 Dec 2016 16:39:57 +1300 Subject: [PATCH 01/19] feat(installer): add inital app installer for macOS platform --- package.json | 10 ++- src/electron-forge-install.js | 145 ++++++++++++++++++++++++++++++++++ src/electron-forge.js | 1 + src/installers/darwin/zip.js | 53 +++++++++++++ 4 files changed, 207 insertions(+), 2 deletions(-) create mode 100644 src/electron-forge-install.js create mode 100644 src/installers/darwin/zip.js diff --git a/package.json b/package.json index d1fc598ae1..3a54478f90 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,8 @@ "release:patch": "changelog -p && node ci/fix-changelog.js && git add CHANGELOG.md && git commit -m 'updated CHANGELOG.md' && npm version patch && git push origin && git push origin --tags", "release:minor": "changelog -m && node ci/fix-changelog.js && git add CHANGELOG.md && git commit -m 'updated CHANGELOG.md' && npm version minor && git push origin && git push origin --tags", "release:major": "changelog -M && node ci/fix-changelog.js && git add CHANGELOG.md && git commit -m 'updated CHANGELOG.md' && npm version major && git push origin && git push origin --tags", - "watch": "gulp watch" + "watch": "gulp watch", + "watch-link": "nodemon --watch src --exec \"npm link\"" }, "author": "Samuel Attard", "license": "MIT", @@ -38,7 +39,8 @@ "generate-changelog": "^1.0.2", "gulp": "^3.9.1", "gulp-babel": "^6.1.2", - "mocha": "^3.2.0" + "mocha": "^3.2.0", + "nodemon": "^1.11.0" }, "babel": { "presets": [ @@ -71,11 +73,15 @@ "inquirer": "^2.0.0", "lodash.template": "^4.4.0", "log-symbols": "^1.0.2", + "node-fetch": "^1.6.3", "node-gyp": "^3.4.0", + "nugget": "^2.0.1", + "opn": "^4.0.2", "ora": "^0.4.0", "pify": "^2.3.0", "resolve-package": "^1.0.1", "semver": "^5.3.0", + "sudo-prompt": "^6.2.1", "username": "^2.2.2", "yarn-or-npm": "^2.0.2", "zip-folder": "^1.0.0" diff --git a/src/electron-forge-install.js b/src/electron-forge-install.js new file mode 100644 index 0000000000..b10bbcaeda --- /dev/null +++ b/src/electron-forge-install.js @@ -0,0 +1,145 @@ +import 'colors'; +import debug from 'debug'; +import fetch from 'node-fetch'; +import fs from 'fs-promise'; +import inquirer from 'inquirer'; +import opn from 'opn'; +import os from 'os'; +import path from 'path'; +import pify from 'pify'; +import program from 'commander'; +import nugget from 'nugget'; +import ora from 'ora'; +import semver from 'semver'; + +import './util/terminate'; + +import darwinZipInstaller from './installers/darwin/zip'; + +const d = debug('electron-forge:lint'); + +const GITHUB_API = 'https://api.github.com'; + +const main = async () => { + const searchSpinner = ora.ora('Searching for Application').start(); + + let repo; + + program + .version(require('../package.json').version) + .arguments('[repository]') + .action((repository) => { + repo = repository; + }) + .parse(process.argv); + + if (!repo || repo.indexOf('/') === -1) { + searchSpinner.fail(); + console.error('Invalid repository name, must be in the format owner/name'.red); + process.exit(1); + } + + d('searching for repo:', repo); + let releases; + try { + releases = await (await fetch(`${GITHUB_API}/repos/${repo}/releases`)).json(); + } catch (err) { + // Ignore error + } + if (!releases || releases.message === 'Not Found' || !Array.isArray(releases)) { + searchSpinner.fail(); + console.error(`Failed to find releases for repository "${repo}". Please check the name and try again.`.red); + process.exit(1); + } + + const sortedReleases = releases.sort((releaseA, releaseB) => { + let tagA = releaseA.tag_name; + if (tagA.substr(0, 1) === 'v') tagA = tagA.substr(1); + let tagB = releaseB.tag_name; + if (tagB.substr(0, 1) === 'v') tagB = tagB.substr(1); + return (semver.gt(tagB, tagA) ? 1 : -1); + }); + const latestRelease = sortedReleases[0]; + + const assets = latestRelease.assets; + if (!assets || !Array.isArray(assets)) { + searchSpinner.fail(); + console.error('Could not find any assets for the latest release'.red); + process.exit(1); + } + + const installTargets = { + win32: ['.exe'], + darwin: ['OSX.zip', 'darwin.zip', 'macOS.zip', 'mac.zip'], + linux: ['.rpm', '.deb', '.flatpak'], + }; + + const possibleAssets = assets.filter((asset) => { + const targetSuffixes = installTargets[process.platform]; + for (const suffix of targetSuffixes) { + if (asset.name.endsWith(suffix)) return true; + } + return false; + }); + + if (possibleAssets.length === 0) { + searchSpinner.fail(); + console.error('Failed to find any installable assets for target platform:'.red, process.platform.cyan); + process.exit(1); + } + + searchSpinner.succeed(); + console.info('Found latest release:', `${latestRelease.tag_name}`.cyan); + + let targetAsset = possibleAssets[0]; + if (possibleAssets.length > 1) { + const { assetID } = await inquirer.createPromptModule()({ + type: 'list', + name: 'assetID', + message: 'Multiple potential assets found, please choose one from the list below:'.cyan, + choices: possibleAssets.map(asset => ({ name: asset.name, value: asset.id })), + }); + + targetAsset = possibleAssets.find(asset => asset.id === assetID); + } + + const tmpdir = path.resolve(os.tmpdir(), 'forge-install'); + const pathSafeRepo = repo.replace(/\//g, '-').replace(/\\/g, '-'); + const filename = `${pathSafeRepo}-${latestRelease.tag_name}-${targetAsset.name}.forge-install`; + + const fullFilePath = path.resolve(tmpdir, filename); + if (!await fs.exists(fullFilePath) || (await fs.stat(fullFilePath)).size !== targetAsset.size) { + await fs.mkdirs(tmpdir); + + const nuggetOpts = { + target: filename, + dir: tmpdir, + resume: true, + strictSSL: true, + }; + await pify(nugget)(targetAsset.browser_download_url, nuggetOpts); + } + + const installSpinner = ora.ora('Installing Application').start(); + + const installActions = { + win32: { + '.exe': async filePath => await opn(filePath, { wait: false }), + }, + darwin: { + '.zip': darwinZipInstaller, + }, + linux: { + '.deb': async () => {}, + '.rpm': async () => {}, + '.flatpak': async () => {}, + }, + }; + + const suffixFnIdent = Object.keys(installActions[process.platform]).find(suffix => targetAsset.name.endsWith(suffix)); + await installActions[process.platform][suffixFnIdent](fullFilePath, installSpinner); + + installSpinner.succeed(); +}; + +main(); diff --git a/src/electron-forge.js b/src/electron-forge.js index 6e03ea262e..8afe4682e6 100644 --- a/src/electron-forge.js +++ b/src/electron-forge.js @@ -30,6 +30,7 @@ import config from './util/config'; .command('make', 'Generate distributables for the current Electron application') .command('start', 'Start the current Electron application') .command('publish', 'Publish the current Electron application to GitHub') + .command('install', 'Install an Electron application from GitHub') .parse(process.argv); config.reset(); diff --git a/src/installers/darwin/zip.js b/src/installers/darwin/zip.js new file mode 100644 index 0000000000..3ac728422e --- /dev/null +++ b/src/installers/darwin/zip.js @@ -0,0 +1,53 @@ +import fs from 'fs-promise'; +import inquirer from 'inquirer'; +import path from 'path'; +import pify from 'pify'; +import sudo from 'sudo-prompt'; +import { exec, spawn } from 'child_process'; + +export default async (filePath, installSpinner) => { + await new Promise((resolve) => { + const child = spawn('unzip', ['-q', '-o', path.basename(filePath)], { + cwd: path.dirname(filePath), + }); + child.stdout.on('data', () => {}); + child.stderr.on('data', () => {}); + child.on('exit', () => resolve()); + }); + let writeAccess = true; + try { + await fs.access('/Applications', fs.W_OK); + } catch (err) { + writeAccess = false; + } + const appPath = (await fs.readdir(path.dirname(filePath))).filter(file => file.endsWith('.app')) + .map(file => path.resolve(path.dirname(filePath), file)) + .sort((fA, fB) => fs.statSync(fA).ctime.getTime() - fs.statSync(fB).ctime.getTime())[0]; + + const targetApplicationPath = `/Applications/${path.basename(appPath)}`; + if (await fs.exists(targetApplicationPath)) { + installSpinner.stop(); + const { confirm } = await inquirer.createPromptModule()({ + type: 'confirm', + name: 'confirm', + message: `The application "${path.basename(targetApplicationPath)}" appears to already exist in /Applications. Do you want to replace it?`, + }); + if (!confirm) { + throw new Error('Installation stopped by user'); + } else { + installSpinner.start(); + await fs.remove(targetApplicationPath); + } + } + + const moveCommand = `mv "${appPath}" "${targetApplicationPath}"`; + if (writeAccess) { + await pify(exec)(moveCommand); + } else { + await pify(sudo.exec)(moveCommand, { + name: 'Electron Forge', + }); + } + + spawn('open', ['-R', targetApplicationPath], { detached: true }); +}; From 56738dcbd86b53d0cfb89e1e2fd6ca15f50555c4 Mon Sep 17 00:00:00 2001 From: Samuel Attard Date: Wed, 28 Dec 2016 16:40:31 +1300 Subject: [PATCH 02/19] chore(generic): add installer to cz config --- .cz.js | 1 + 1 file changed, 1 insertion(+) diff --git a/.cz.js b/.cz.js index ac3933dda6..108dc4a708 100644 --- a/.cz.js +++ b/.cz.js @@ -20,6 +20,7 @@ module.exports = { { name: 'tests' }, { name: 'initializer' }, { name: 'publisher' }, + { name: 'installer' }, { name: 'generic' }, ], allowCustomScopes: true, From c3ed5b624384ac25a46eb3663dfaecda81c55ace Mon Sep 17 00:00:00 2001 From: Samuel Attard Date: Wed, 28 Dec 2016 16:59:51 +1300 Subject: [PATCH 03/19] refactor(installer): update the ora text wh have resolved a repo but not found a release --- src/electron-forge-install.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/electron-forge-install.js b/src/electron-forge-install.js index b10bbcaeda..0076f21c6e 100644 --- a/src/electron-forge-install.js +++ b/src/electron-forge-install.js @@ -61,6 +61,8 @@ const main = async () => { }); const latestRelease = sortedReleases[0]; + searchSpinner.text = 'Searching for Releases'; + const assets = latestRelease.assets; if (!assets || !Array.isArray(assets)) { searchSpinner.fail(); From 8fe867ab3b07ed83134f9c10cbd6f12653f762e4 Mon Sep 17 00:00:00 2001 From: Samuel Attard Date: Wed, 28 Dec 2016 22:49:32 +1300 Subject: [PATCH 04/19] feat(installer): add DMG support for macOS installer --- src/electron-forge-install.js | 4 +++- src/installers/darwin/dmg.js | 14 ++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 src/installers/darwin/dmg.js diff --git a/src/electron-forge-install.js b/src/electron-forge-install.js index 0076f21c6e..1c47b9a105 100644 --- a/src/electron-forge-install.js +++ b/src/electron-forge-install.js @@ -14,6 +14,7 @@ import semver from 'semver'; import './util/terminate'; +import darwinDMGInstaller from './installers/darwin/dmg'; import darwinZipInstaller from './installers/darwin/zip'; const d = debug('electron-forge:lint'); @@ -72,7 +73,7 @@ const main = async () => { const installTargets = { win32: ['.exe'], - darwin: ['OSX.zip', 'darwin.zip', 'macOS.zip', 'mac.zip'], + darwin: ['OSX.zip', 'darwin.zip', 'macOS.zip', 'mac.zip', '.dmg'], linux: ['.rpm', '.deb', '.flatpak'], }; @@ -130,6 +131,7 @@ const main = async () => { }, darwin: { '.zip': darwinZipInstaller, + '.dmg': darwinDMGInstaller, }, linux: { '.deb': async () => {}, diff --git a/src/installers/darwin/dmg.js b/src/installers/darwin/dmg.js new file mode 100644 index 0000000000..4ceadfe6e2 --- /dev/null +++ b/src/installers/darwin/dmg.js @@ -0,0 +1,14 @@ +import fs from 'fs-promise'; +import opn from 'opn'; +import path from 'path'; +import pify from 'pify'; +import { exec } from 'child_process'; + +export default async (filePath) => { + const DMGPath = path.join(path.dirname(filePath), path.parse(filePath).name); + if (await fs.exists(DMGPath)) { + await fs.remove(DMGPath); + } + await pify(exec)(`cp "${filePath}" "${DMGPath}"`); + await opn(DMGPath, { wait: false }); +}; From c5397694fee690fd027a2ba0557f7b7d0c85ff2e Mon Sep 17 00:00:00 2001 From: Samuel Attard Date: Thu, 29 Dec 2016 15:43:15 +1300 Subject: [PATCH 05/19] fix(installer): wildcard the extension matchers --- src/electron-forge-install.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/electron-forge-install.js b/src/electron-forge-install.js index 1c47b9a105..8893f8f407 100644 --- a/src/electron-forge-install.js +++ b/src/electron-forge-install.js @@ -72,15 +72,15 @@ const main = async () => { } const installTargets = { - win32: ['.exe'], - darwin: ['OSX.zip', 'darwin.zip', 'macOS.zip', 'mac.zip', '.dmg'], - linux: ['.rpm', '.deb', '.flatpak'], + win32: [/\.exe$/], + darwin: [/OSX.*\.zip$/, /darwin.*\.zip$/, /macOS.*\.zip$/, /mac.*\.zip$/, /\.dmg$/], + linux: [/\.rpm$/, /\.deb$/, /\.flatpak$/], }; const possibleAssets = assets.filter((asset) => { const targetSuffixes = installTargets[process.platform]; for (const suffix of targetSuffixes) { - if (asset.name.endsWith(suffix)) return true; + if (suffix.test(asset.name)) return true; } return false; }); From 61191d3887ccc64e3668f6277626707281ff0f7f Mon Sep 17 00:00:00 2001 From: Mark Lee Date: Thu, 29 Dec 2016 18:11:38 -0800 Subject: [PATCH 06/19] feat(installer): add deb installer --- src/electron-forge-install.js | 3 ++- src/installers/linux/deb.js | 8 ++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 src/installers/linux/deb.js diff --git a/src/electron-forge-install.js b/src/electron-forge-install.js index 8893f8f407..2bf06132e6 100644 --- a/src/electron-forge-install.js +++ b/src/electron-forge-install.js @@ -16,6 +16,7 @@ import './util/terminate'; import darwinDMGInstaller from './installers/darwin/dmg'; import darwinZipInstaller from './installers/darwin/zip'; +import linuxDebInstaller from './installers/linux/deb'; const d = debug('electron-forge:lint'); @@ -134,7 +135,7 @@ const main = async () => { '.dmg': darwinDMGInstaller, }, linux: { - '.deb': async () => {}, + '.deb': linuxDebInstaller, '.rpm': async () => {}, '.flatpak': async () => {}, }, diff --git a/src/installers/linux/deb.js b/src/installers/linux/deb.js new file mode 100644 index 0000000000..c05949c391 --- /dev/null +++ b/src/installers/linux/deb.js @@ -0,0 +1,8 @@ +import pify from 'pify'; +import sudo from 'sudo-prompt'; + +export default async (filePath) => { + await pify(sudo.exec)(`gdebi -n ${filePath}`, { + name: 'Electron Forge', + }); +}; From c29a6355488b886fb05e2fe4af9cbf600b88639b Mon Sep 17 00:00:00 2001 From: Mark Lee Date: Thu, 29 Dec 2016 20:29:57 -0800 Subject: [PATCH 07/19] refactor(installer): check that the linux installer program exists first --- src/installers/linux/deb.js | 6 ++++-- src/util/linux-installer.js | 9 +++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) create mode 100644 src/util/linux-installer.js diff --git a/src/installers/linux/deb.js b/src/installers/linux/deb.js index c05949c391..98d9ba30c3 100644 --- a/src/installers/linux/deb.js +++ b/src/installers/linux/deb.js @@ -1,8 +1,10 @@ import pify from 'pify'; import sudo from 'sudo-prompt'; +import linuxInstaller from '../../util/linux-installer'; + export default async (filePath) => { - await pify(sudo.exec)(`gdebi -n ${filePath}`, { + linuxInstaller('Debian', 'gdebi', pify(sudo.exec)(`gdebi -n ${filePath}`, { name: 'Electron Forge', - }); + })); }; diff --git a/src/util/linux-installer.js b/src/util/linux-installer.js new file mode 100644 index 0000000000..0314a306df --- /dev/null +++ b/src/util/linux-installer.js @@ -0,0 +1,9 @@ +import { spawnSync } from 'child_process'; + +export default async (type, prog, promise) => { + if (spawnSync('which', [prog]).status === 0) { + await promise; + } else { + throw new Error(`${prog} is required to install ${type} packages`); + } +}; From cf2db114b60b70571df857a3beb373c7c26bd2ca Mon Sep 17 00:00:00 2001 From: Mark Lee Date: Fri, 30 Dec 2016 19:22:45 -0800 Subject: [PATCH 08/19] feat(installer): don't suffix temp install files with .forge-install --- src/electron-forge-install.js | 2 +- src/installers/darwin/dmg.js | 11 +---------- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/src/electron-forge-install.js b/src/electron-forge-install.js index 2bf06132e6..0fa389d2b4 100644 --- a/src/electron-forge-install.js +++ b/src/electron-forge-install.js @@ -109,7 +109,7 @@ const main = async () => { const tmpdir = path.resolve(os.tmpdir(), 'forge-install'); const pathSafeRepo = repo.replace(/\//g, '-').replace(/\\/g, '-'); - const filename = `${pathSafeRepo}-${latestRelease.tag_name}-${targetAsset.name}.forge-install`; + const filename = `${pathSafeRepo}-${latestRelease.tag_name}-${targetAsset.name}`; const fullFilePath = path.resolve(tmpdir, filename); if (!await fs.exists(fullFilePath) || (await fs.stat(fullFilePath)).size !== targetAsset.size) { diff --git a/src/installers/darwin/dmg.js b/src/installers/darwin/dmg.js index 4ceadfe6e2..d3fb4197e7 100644 --- a/src/installers/darwin/dmg.js +++ b/src/installers/darwin/dmg.js @@ -1,14 +1,5 @@ -import fs from 'fs-promise'; import opn from 'opn'; -import path from 'path'; -import pify from 'pify'; -import { exec } from 'child_process'; export default async (filePath) => { - const DMGPath = path.join(path.dirname(filePath), path.parse(filePath).name); - if (await fs.exists(DMGPath)) { - await fs.remove(DMGPath); - } - await pify(exec)(`cp "${filePath}" "${DMGPath}"`); - await opn(DMGPath, { wait: false }); + await opn(filePath, { wait: false }); }; From 826dda1544212f1fa2f0cb2e65c6c72da9c35a0c Mon Sep 17 00:00:00 2001 From: Mark Lee Date: Fri, 30 Dec 2016 19:28:43 -0800 Subject: [PATCH 09/19] feat(installer): add rpm installer --- src/electron-forge-install.js | 7 +++++-- src/installers/linux/rpm.js | 10 ++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) create mode 100644 src/installers/linux/rpm.js diff --git a/src/electron-forge-install.js b/src/electron-forge-install.js index 0fa389d2b4..09ee4990e5 100644 --- a/src/electron-forge-install.js +++ b/src/electron-forge-install.js @@ -17,6 +17,7 @@ import './util/terminate'; import darwinDMGInstaller from './installers/darwin/dmg'; import darwinZipInstaller from './installers/darwin/zip'; import linuxDebInstaller from './installers/linux/deb'; +import linuxRPMInstaller from './installers/linux/rpm'; const d = debug('electron-forge:lint'); @@ -136,8 +137,10 @@ const main = async () => { }, linux: { '.deb': linuxDebInstaller, - '.rpm': async () => {}, - '.flatpak': async () => {}, + '.rpm': linuxRPMInstaller, + '.flatpak': async () => { + console.error('Not yet supported'); + }, }, }; diff --git a/src/installers/linux/rpm.js b/src/installers/linux/rpm.js new file mode 100644 index 0000000000..ee9e99564b --- /dev/null +++ b/src/installers/linux/rpm.js @@ -0,0 +1,10 @@ +import pify from 'pify'; +import sudo from 'sudo-prompt'; + +import linuxInstaller from '../../util/linux-installer'; + +export default async (filePath) => { + linuxInstaller('RPM', 'dnf', pify(sudo.exec)(`dnf --assumeyes --nogpgcheck install ${filePath}`, { + name: 'Electron Forge', + })); +}; From 2ab37e3b089d456f0a9b0a1bd2e63e19ff246566 Mon Sep 17 00:00:00 2001 From: Mark Lee Date: Fri, 30 Dec 2016 23:13:37 -0800 Subject: [PATCH 10/19] refactor(installer): replace sudo-prompt with git branch of electron-sudo for Linux installers --- package.json | 1 + src/installers/linux/deb.js | 9 ++------- src/installers/linux/rpm.js | 9 ++------- src/util/linux-installer.js | 20 ++++++++++++++++++-- 4 files changed, 23 insertions(+), 16 deletions(-) diff --git a/package.json b/package.json index 3a54478f90..7f50f5b4ba 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "debug": "^2.3.3", "electron-installer-dmg": "^0.1.2", "electron-packager": "^8.4.0", + "electron-sudo": "malept/electron-sudo#fix-linux-sudo-detection", "electron-winstaller": "^2.5.0", "fs-promise": "^1.0.0", "github": "^7.2.0", diff --git a/src/installers/linux/deb.js b/src/installers/linux/deb.js index 98d9ba30c3..66e6969350 100644 --- a/src/installers/linux/deb.js +++ b/src/installers/linux/deb.js @@ -1,10 +1,5 @@ -import pify from 'pify'; -import sudo from 'sudo-prompt'; - -import linuxInstaller from '../../util/linux-installer'; +import { sudo } from '../../util/linux-installer'; export default async (filePath) => { - linuxInstaller('Debian', 'gdebi', pify(sudo.exec)(`gdebi -n ${filePath}`, { - name: 'Electron Forge', - })); + sudo('Debian', 'gdebi', `-n ${filePath}`); }; diff --git a/src/installers/linux/rpm.js b/src/installers/linux/rpm.js index ee9e99564b..3d9e4c8d56 100644 --- a/src/installers/linux/rpm.js +++ b/src/installers/linux/rpm.js @@ -1,10 +1,5 @@ -import pify from 'pify'; -import sudo from 'sudo-prompt'; - -import linuxInstaller from '../../util/linux-installer'; +import { sudo } from '../../util/linux-installer'; export default async (filePath) => { - linuxInstaller('RPM', 'dnf', pify(sudo.exec)(`dnf --assumeyes --nogpgcheck install ${filePath}`, { - name: 'Electron Forge', - })); + sudo('RPM', 'dnf', `--assumeyes --nogpgcheck install ${filePath}`); }; diff --git a/src/util/linux-installer.js b/src/util/linux-installer.js index 0314a306df..1be65d254c 100644 --- a/src/util/linux-installer.js +++ b/src/util/linux-installer.js @@ -1,9 +1,25 @@ import { spawnSync } from 'child_process'; +import { default as Sudoer } from 'electron-sudo'; -export default async (type, prog, promise) => { +async function which(type, prog, promise) { if (spawnSync('which', [prog]).status === 0) { await promise; } else { throw new Error(`${prog} is required to install ${type} packages`); } -}; +} + +async function sudo(type, prog, args) { + const sudoer = new Sudoer({ name: 'Electron Forge' }); + which(type, prog, sudoer.spawn(`${prog} ${args}`).then((child) => { + child.on('exit', async (code) => { + if (code !== 0) { + console.error(child.output.stdout.toString('utf8')); + console.error(child.output.stderr.toString('utf8')); + throw new Error(`${prog} failed with status code ${code}`); + } + }); + })); +} + +export { which as default, sudo }; From a9088afe11e69102be1237fa9914aff76c5de964 Mon Sep 17 00:00:00 2001 From: Mark Lee Date: Sat, 31 Dec 2016 09:02:08 -0800 Subject: [PATCH 11/19] fix(installer): remove flatpak check It appears that it's pretty nontrivial to install a single flatpak file without a flatpak repository. --- src/electron-forge-install.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/electron-forge-install.js b/src/electron-forge-install.js index 09ee4990e5..03611573d0 100644 --- a/src/electron-forge-install.js +++ b/src/electron-forge-install.js @@ -76,7 +76,7 @@ const main = async () => { const installTargets = { win32: [/\.exe$/], darwin: [/OSX.*\.zip$/, /darwin.*\.zip$/, /macOS.*\.zip$/, /mac.*\.zip$/, /\.dmg$/], - linux: [/\.rpm$/, /\.deb$/, /\.flatpak$/], + linux: [/\.rpm$/, /\.deb$/], }; const possibleAssets = assets.filter((asset) => { @@ -138,9 +138,6 @@ const main = async () => { linux: { '.deb': linuxDebInstaller, '.rpm': linuxRPMInstaller, - '.flatpak': async () => { - console.error('Not yet supported'); - }, }, }; From 23ea0de3f1cf8519dd06c7b85a0904b4fdfaa2fe Mon Sep 17 00:00:00 2001 From: Samuel Attard Date: Sun, 1 Jan 2017 09:26:14 +1300 Subject: [PATCH 12/19] fix(installer): await promises through the linux install chain --- src/installers/linux/deb.js | 2 +- src/installers/linux/rpm.js | 2 +- src/util/linux-installer.js | 34 +++++++++++++++++++--------------- 3 files changed, 21 insertions(+), 17 deletions(-) diff --git a/src/installers/linux/deb.js b/src/installers/linux/deb.js index 66e6969350..04c4a7b7c7 100644 --- a/src/installers/linux/deb.js +++ b/src/installers/linux/deb.js @@ -1,5 +1,5 @@ import { sudo } from '../../util/linux-installer'; export default async (filePath) => { - sudo('Debian', 'gdebi', `-n ${filePath}`); + await sudo('Debian', 'gdebi', `-n ${filePath}`); }; diff --git a/src/installers/linux/rpm.js b/src/installers/linux/rpm.js index 3d9e4c8d56..687ce5e871 100644 --- a/src/installers/linux/rpm.js +++ b/src/installers/linux/rpm.js @@ -1,5 +1,5 @@ import { sudo } from '../../util/linux-installer'; export default async (filePath) => { - sudo('RPM', 'dnf', `--assumeyes --nogpgcheck install ${filePath}`); + await sudo('RPM', 'dnf', `--assumeyes --nogpgcheck install ${filePath}`); }; diff --git a/src/util/linux-installer.js b/src/util/linux-installer.js index 1be65d254c..b1d411d37e 100644 --- a/src/util/linux-installer.js +++ b/src/util/linux-installer.js @@ -1,25 +1,29 @@ import { spawnSync } from 'child_process'; import { default as Sudoer } from 'electron-sudo'; -async function which(type, prog, promise) { +const which = async (type, prog, promise) => { if (spawnSync('which', [prog]).status === 0) { await promise; } else { throw new Error(`${prog} is required to install ${type} packages`); } -} +}; -async function sudo(type, prog, args) { - const sudoer = new Sudoer({ name: 'Electron Forge' }); - which(type, prog, sudoer.spawn(`${prog} ${args}`).then((child) => { - child.on('exit', async (code) => { - if (code !== 0) { - console.error(child.output.stdout.toString('utf8')); - console.error(child.output.stderr.toString('utf8')); - throw new Error(`${prog} failed with status code ${code}`); - } - }); - })); -} +export const sudo = (type, prog, args) => + new Promise((resolve, reject) => { + const sudoer = new Sudoer({ name: 'Electron Forge' }); -export { which as default, sudo }; + which(type, prog, sudoer.spawn(`${prog} ${args}`) + .then((child) => { + child.on('exit', async (code) => { + if (code !== 0) { + console.error(child.output.stdout.toString('utf8').red); + console.error(child.output.stderr.toString('utf8').red); + return reject(new Error(`${prog} failed with status code ${code}`)); + } + resolve(); + }); + })); + }); + +export default which; From 0a21433848c79867bc95937ed344ffed7ead1f1c Mon Sep 17 00:00:00 2001 From: Mark Lee Date: Sat, 31 Dec 2016 13:02:05 -0800 Subject: [PATCH 13/19] refactor(installer): finish replacing sudo-prompt with electron-sudo --- package.json | 1 - src/installers/darwin/zip.js | 7 +++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 7f50f5b4ba..2ed0ba6c56 100644 --- a/package.json +++ b/package.json @@ -82,7 +82,6 @@ "pify": "^2.3.0", "resolve-package": "^1.0.1", "semver": "^5.3.0", - "sudo-prompt": "^6.2.1", "username": "^2.2.2", "yarn-or-npm": "^2.0.2", "zip-folder": "^1.0.0" diff --git a/src/installers/darwin/zip.js b/src/installers/darwin/zip.js index 3ac728422e..fe91706b0f 100644 --- a/src/installers/darwin/zip.js +++ b/src/installers/darwin/zip.js @@ -2,7 +2,7 @@ import fs from 'fs-promise'; import inquirer from 'inquirer'; import path from 'path'; import pify from 'pify'; -import sudo from 'sudo-prompt'; +import { default as Sudoer } from 'electron-sudo'; import { exec, spawn } from 'child_process'; export default async (filePath, installSpinner) => { @@ -44,9 +44,8 @@ export default async (filePath, installSpinner) => { if (writeAccess) { await pify(exec)(moveCommand); } else { - await pify(sudo.exec)(moveCommand, { - name: 'Electron Forge', - }); + const sudoer = new Sudoer({ name: 'Electron Forge' }); + await sudoer.exec(moveCommand); } spawn('open', ['-R', targetApplicationPath], { detached: true }); From 134b5f9d0e49f926b84d57c6907bc49eae0aadba Mon Sep 17 00:00:00 2001 From: Mark Lee Date: Sat, 31 Dec 2016 13:23:52 -0800 Subject: [PATCH 14/19] Fix weird copy/paste --- src/installers/darwin/zip.js | 2 +- src/util/linux-installer.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/installers/darwin/zip.js b/src/installers/darwin/zip.js index fe91706b0f..32702d3a87 100644 --- a/src/installers/darwin/zip.js +++ b/src/installers/darwin/zip.js @@ -2,7 +2,7 @@ import fs from 'fs-promise'; import inquirer from 'inquirer'; import path from 'path'; import pify from 'pify'; -import { default as Sudoer } from 'electron-sudo'; +import Sudoer from 'electron-sudo'; import { exec, spawn } from 'child_process'; export default async (filePath, installSpinner) => { diff --git a/src/util/linux-installer.js b/src/util/linux-installer.js index b1d411d37e..54d1226447 100644 --- a/src/util/linux-installer.js +++ b/src/util/linux-installer.js @@ -1,5 +1,5 @@ import { spawnSync } from 'child_process'; -import { default as Sudoer } from 'electron-sudo'; +import Sudoer from 'electron-sudo'; const which = async (type, prog, promise) => { if (spawnSync('which', [prog]).status === 0) { From 6557791d33fff05b615490b651cd7bbff9caf991 Mon Sep 17 00:00:00 2001 From: Samuel Attard Date: Sun, 1 Jan 2017 10:52:07 +1300 Subject: [PATCH 15/19] Revert "refactor(installer): finish replacing sudo-prompt with electron-sudo" This reverts commit 0a21433848c79867bc95937ed344ffed7ead1f1c. --- package.json | 1 + src/installers/darwin/zip.js | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 2ed0ba6c56..7f50f5b4ba 100644 --- a/package.json +++ b/package.json @@ -82,6 +82,7 @@ "pify": "^2.3.0", "resolve-package": "^1.0.1", "semver": "^5.3.0", + "sudo-prompt": "^6.2.1", "username": "^2.2.2", "yarn-or-npm": "^2.0.2", "zip-folder": "^1.0.0" diff --git a/src/installers/darwin/zip.js b/src/installers/darwin/zip.js index 32702d3a87..3ac728422e 100644 --- a/src/installers/darwin/zip.js +++ b/src/installers/darwin/zip.js @@ -2,7 +2,7 @@ import fs from 'fs-promise'; import inquirer from 'inquirer'; import path from 'path'; import pify from 'pify'; -import Sudoer from 'electron-sudo'; +import sudo from 'sudo-prompt'; import { exec, spawn } from 'child_process'; export default async (filePath, installSpinner) => { @@ -44,8 +44,9 @@ export default async (filePath, installSpinner) => { if (writeAccess) { await pify(exec)(moveCommand); } else { - const sudoer = new Sudoer({ name: 'Electron Forge' }); - await sudoer.exec(moveCommand); + await pify(sudo.exec)(moveCommand, { + name: 'Electron Forge', + }); } spawn('open', ['-R', targetApplicationPath], { detached: true }); From 847696fa8307b92086ff4e8039b104070cb29f08 Mon Sep 17 00:00:00 2001 From: Samuel Attard Date: Sun, 1 Jan 2017 11:03:58 +1300 Subject: [PATCH 16/19] chore(installer): use the ora helper in the install command --- src/electron-forge-install.js | 143 +++++++++++++++++----------------- src/installers/darwin/zip.js | 3 +- 2 files changed, 72 insertions(+), 74 deletions(-) diff --git a/src/electron-forge-install.js b/src/electron-forge-install.js index 03611573d0..a90bb9eb2a 100644 --- a/src/electron-forge-install.js +++ b/src/electron-forge-install.js @@ -3,16 +3,16 @@ import debug from 'debug'; import fetch from 'node-fetch'; import fs from 'fs-promise'; import inquirer from 'inquirer'; +import nugget from 'nugget'; import opn from 'opn'; import os from 'os'; import path from 'path'; import pify from 'pify'; import program from 'commander'; -import nugget from 'nugget'; -import ora from 'ora'; import semver from 'semver'; import './util/terminate'; +import asyncOra from './util/ora-handler'; import darwinDMGInstaller from './installers/darwin/dmg'; import darwinZipInstaller from './installers/darwin/zip'; @@ -24,8 +24,6 @@ const d = debug('electron-forge:lint'); const GITHUB_API = 'https://api.github.com'; const main = async () => { - const searchSpinner = ora.ora('Searching for Application').start(); - let repo; program @@ -36,64 +34,65 @@ const main = async () => { }) .parse(process.argv); - if (!repo || repo.indexOf('/') === -1) { - searchSpinner.fail(); - console.error('Invalid repository name, must be in the format owner/name'.red); - process.exit(1); - } + let latestRelease; + let possibleAssets = []; - d('searching for repo:', repo); - let releases; - try { - releases = await (await fetch(`${GITHUB_API}/repos/${repo}/releases`)).json(); - } catch (err) { - // Ignore error - } - if (!releases || releases.message === 'Not Found' || !Array.isArray(releases)) { - searchSpinner.fail(); - console.error(`Failed to find releases for repository "${repo}". Please check the name and try again.`.red); - process.exit(1); - } + await asyncOra('Searching for Application', async (searchSpinner) => { + if (!repo || repo.indexOf('/') === -1) { + // eslint-disable-next-line no-throw-literal + throw 'Invalid repository name, must be in the format owner/name'; + } - const sortedReleases = releases.sort((releaseA, releaseB) => { - let tagA = releaseA.tag_name; - if (tagA.substr(0, 1) === 'v') tagA = tagA.substr(1); - let tagB = releaseB.tag_name; - if (tagB.substr(0, 1) === 'v') tagB = tagB.substr(1); - return (semver.gt(tagB, tagA) ? 1 : -1); - }); - const latestRelease = sortedReleases[0]; + d('searching for repo:', repo); + let releases; + try { + releases = await (await fetch(`${GITHUB_API}/repos/${repo}/releases`)).json(); + } catch (err) { + // Ignore error + } - searchSpinner.text = 'Searching for Releases'; + if (!releases || releases.message === 'Not Found' || !Array.isArray(releases)) { + // eslint-disable-next-line no-throw-literal + throw `Failed to find releases for repository "${repo}". Please check the name and try again.`; + } - const assets = latestRelease.assets; - if (!assets || !Array.isArray(assets)) { - searchSpinner.fail(); - console.error('Could not find any assets for the latest release'.red); - process.exit(1); - } + const sortedReleases = releases.sort((releaseA, releaseB) => { + let tagA = releaseA.tag_name; + if (tagA.substr(0, 1) === 'v') tagA = tagA.substr(1); + let tagB = releaseB.tag_name; + if (tagB.substr(0, 1) === 'v') tagB = tagB.substr(1); + return (semver.gt(tagB, tagA) ? 1 : -1); + }); + latestRelease = sortedReleases[0]; - const installTargets = { - win32: [/\.exe$/], - darwin: [/OSX.*\.zip$/, /darwin.*\.zip$/, /macOS.*\.zip$/, /mac.*\.zip$/, /\.dmg$/], - linux: [/\.rpm$/, /\.deb$/], - }; + searchSpinner.text = 'Searching for Releases'; // eslint-disable-line - const possibleAssets = assets.filter((asset) => { - const targetSuffixes = installTargets[process.platform]; - for (const suffix of targetSuffixes) { - if (suffix.test(asset.name)) return true; + const assets = latestRelease.assets; + if (!assets || !Array.isArray(assets)) { + // eslint-disable-next-line no-throw-literal + throw 'Could not find any assets for the latest release'; } - return false; - }); - if (possibleAssets.length === 0) { - searchSpinner.fail(); - console.error('Failed to find any installable assets for target platform:'.red, process.platform.cyan); - process.exit(1); - } + const installTargets = { + win32: [/\.exe$/], + darwin: [/OSX.*\.zip$/, /darwin.*\.zip$/, /macOS.*\.zip$/, /mac.*\.zip$/, /\.dmg$/], + linux: [/\.rpm$/, /\.deb$/], + }; + + possibleAssets = assets.filter((asset) => { + const targetSuffixes = installTargets[process.platform]; + for (const suffix of targetSuffixes) { + if (suffix.test(asset.name)) return true; + } + return false; + }); + + if (possibleAssets.length === 0) { + // eslint-disable-next-line no-throw-literal + throw `Failed to find any installable assets for target platform: ${`${process.platform}`.cyan}`; + } + }); - searchSpinner.succeed(); console.info('Found latest release:', `${latestRelease.tag_name}`.cyan); let targetAsset = possibleAssets[0]; @@ -125,26 +124,24 @@ const main = async () => { await pify(nugget)(targetAsset.browser_download_url, nuggetOpts); } - const installSpinner = ora.ora('Installing Application').start(); - - const installActions = { - win32: { - '.exe': async filePath => await opn(filePath, { wait: false }), - }, - darwin: { - '.zip': darwinZipInstaller, - '.dmg': darwinDMGInstaller, - }, - linux: { - '.deb': linuxDebInstaller, - '.rpm': linuxRPMInstaller, - }, - }; - - const suffixFnIdent = Object.keys(installActions[process.platform]).find(suffix => targetAsset.name.endsWith(suffix)); - await installActions[process.platform][suffixFnIdent](fullFilePath, installSpinner); - - installSpinner.succeed(); + await asyncOra('Installing Application', async (installSpinner) => { + const installActions = { + win32: { + '.exe': async filePath => await opn(filePath, { wait: false }), + }, + darwin: { + '.zip': darwinZipInstaller, + '.dmg': darwinDMGInstaller, + }, + linux: { + '.deb': linuxDebInstaller, + '.rpm': linuxRPMInstaller, + }, + }; + + const suffixFnIdent = Object.keys(installActions[process.platform]).find(suffix => targetAsset.name.endsWith(suffix)); + await installActions[process.platform][suffixFnIdent](fullFilePath, installSpinner); + }); }; main(); diff --git a/src/installers/darwin/zip.js b/src/installers/darwin/zip.js index 3ac728422e..ef717baf03 100644 --- a/src/installers/darwin/zip.js +++ b/src/installers/darwin/zip.js @@ -33,7 +33,8 @@ export default async (filePath, installSpinner) => { message: `The application "${path.basename(targetApplicationPath)}" appears to already exist in /Applications. Do you want to replace it?`, }); if (!confirm) { - throw new Error('Installation stopped by user'); + // eslint-disable-next-line no-throw-literal + throw 'Installation stopped by user'; } else { installSpinner.start(); await fs.remove(targetApplicationPath); From 17af8c19c19ffb6fdb7c174986fd117636715ef8 Mon Sep 17 00:00:00 2001 From: Samuel Attard Date: Sun, 1 Jan 2017 11:10:07 +1300 Subject: [PATCH 17/19] fix(installer): dont fetch prerelease versions unless instructed --- src/electron-forge-install.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/electron-forge-install.js b/src/electron-forge-install.js index a90bb9eb2a..2c36feab98 100644 --- a/src/electron-forge-install.js +++ b/src/electron-forge-install.js @@ -29,6 +29,7 @@ const main = async () => { program .version(require('../package.json').version) .arguments('[repository]') + .option('--prerelease', 'Fetch prerelease versions') .action((repository) => { repo = repository; }) @@ -56,6 +57,8 @@ const main = async () => { throw `Failed to find releases for repository "${repo}". Please check the name and try again.`; } + releases = releases.filter(release => !release.prerelease || program.prerelease); + const sortedReleases = releases.sort((releaseA, releaseB) => { let tagA = releaseA.tag_name; if (tagA.substr(0, 1) === 'v') tagA = tagA.substr(1); @@ -93,7 +96,7 @@ const main = async () => { } }); - console.info('Found latest release:', `${latestRelease.tag_name}`.cyan); + console.info(`Found latest release${program.prerelease ? ' (including prereleases)' : ''}: ${latestRelease.tag_name.cyan}`); let targetAsset = possibleAssets[0]; if (possibleAssets.length > 1) { From 3b8155cfed1266794e19780133e4cf808152dbeb Mon Sep 17 00:00:00 2001 From: Samuel Attard Date: Sun, 1 Jan 2017 11:17:21 +1300 Subject: [PATCH 18/19] fix(installer): fix installer debug key --- src/electron-forge-install.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/electron-forge-install.js b/src/electron-forge-install.js index 2c36feab98..318ad31a9a 100644 --- a/src/electron-forge-install.js +++ b/src/electron-forge-install.js @@ -19,7 +19,7 @@ import darwinZipInstaller from './installers/darwin/zip'; import linuxDebInstaller from './installers/linux/deb'; import linuxRPMInstaller from './installers/linux/rpm'; -const d = debug('electron-forge:lint'); +const d = debug('electron-forge:install'); const GITHUB_API = 'https://api.github.com'; From 13605e1f792c9c8daa91eba62a1b61d84c041dd3 Mon Sep 17 00:00:00 2001 From: Samuel Attard Date: Sun, 1 Jan 2017 11:20:03 +1300 Subject: [PATCH 19/19] refactor(installer): use single regexp to make repo path safe --- src/electron-forge-install.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/electron-forge-install.js b/src/electron-forge-install.js index 318ad31a9a..5708aa8d1e 100644 --- a/src/electron-forge-install.js +++ b/src/electron-forge-install.js @@ -111,7 +111,7 @@ const main = async () => { } const tmpdir = path.resolve(os.tmpdir(), 'forge-install'); - const pathSafeRepo = repo.replace(/\//g, '-').replace(/\\/g, '-'); + const pathSafeRepo = repo.replace(/[/\\]/g, '-'); const filename = `${pathSafeRepo}-${latestRelease.tag_name}-${targetAsset.name}`; const fullFilePath = path.resolve(tmpdir, filename);