-
-
Notifications
You must be signed in to change notification settings - Fork 525
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
electron-forge install #41
Changes from 14 commits
7c89359
56738dc
c3ed5b6
8fe867a
c539769
61191d3
c29a635
cf2db11
826dda1
2ab37e3
a9088af
23ea0de
0a21433
134b5f9
6557791
847696f
17af8c1
3b8155c
13605e1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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": [ | ||
|
@@ -64,14 +66,18 @@ | |
"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", | ||
"glob": "^7.1.1", | ||
"inquirer": "^2.0.0", | ||
"lodash.template": "^4.4.0", | ||
"log-symbols": "^1.0.2", | ||
"node-fetch": "^1.6.3", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It feels really weird to add two different modules that perform HTTP requests, even though I know that they're serving two different functions. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, I kind of wish nugget was built on fetch, or we used some fancy feedback module that was built on fetch. It's not that important. |
||
"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", | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,150 @@ | ||
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 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'); | ||
|
||
const GITHUB_API = 'https://api.github.com'; | ||
|
||
const main = async () => { | ||
const searchSpinner = ora.ora('Searching for Application').start(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this hasn't been promisified yet. |
||
|
||
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]; | ||
|
||
searchSpinner.text = 'Searching for Releases'; | ||
|
||
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$/, /\.dmg$/], | ||
linux: [/\.rpm$/, /\.deb$/], | ||
}; | ||
|
||
const 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) { | ||
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'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This folder should probably be removed when the command completes. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Probably, but it's in the |
||
const pathSafeRepo = repo.replace(/\//g, '-').replace(/\\/g, '-'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That could be one |
||
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) { | ||
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 }), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since every other installer is one line (except for the darwin zip installer), should this also go in a separate file? |
||
}, | ||
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(); | ||
}; | ||
|
||
main(); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import opn from 'opn'; | ||
|
||
export default async (filePath) => { | ||
await opn(filePath, { wait: false }); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
import fs from 'fs-promise'; | ||
import inquirer from 'inquirer'; | ||
import path from 'path'; | ||
import pify from 'pify'; | ||
import Sudoer from 'electron-sudo'; | ||
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 { | ||
const sudoer = new Sudoer({ name: 'Electron Forge' }); | ||
await sudoer.exec(moveCommand); | ||
} | ||
|
||
spawn('open', ['-R', targetApplicationPath], { detached: true }); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import { sudo } from '../../util/linux-installer'; | ||
|
||
export default async (filePath) => { | ||
await sudo('Debian', 'gdebi', `-n ${filePath}`); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import { sudo } from '../../util/linux-installer'; | ||
|
||
export default async (filePath) => { | ||
await sudo('RPM', 'dnf', `--assumeyes --nogpgcheck install ${filePath}`); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import { spawnSync } from 'child_process'; | ||
import Sudoer from 'electron-sudo'; | ||
|
||
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`); | ||
} | ||
}; | ||
|
||
export const sudo = (type, prog, args) => | ||
new Promise((resolve, reject) => { | ||
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').red); | ||
console.error(child.output.stderr.toString('utf8').red); | ||
return reject(new Error(`${prog} failed with status code ${code}`)); | ||
} | ||
resolve(); | ||
}); | ||
})); | ||
}); | ||
|
||
export default which; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just FYI, this can be changed to a version once automation-stack/electron-sudo#41 is merged & released.