-
-
Notifications
You must be signed in to change notification settings - Fork 535
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(installer): add inital app installer for macOS platform
- Loading branch information
1 parent
1b6b727
commit da3150d
Showing
4 changed files
with
207 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 }); | ||
}; |