Skip to content
Permalink
Browse files

feat(installer): add inital app installer for macOS platform

  • Loading branch information
MarshallOfSound committed Dec 28, 2016
1 parent 1b6b727 commit da3150d93e1f1eca36ce3977a9c620edaa0c883b
Showing with 207 additions and 2 deletions.
  1. +8 −2 package.json
  2. +145 −0 src/electron-forge-install.js
  3. +1 −0 src/electron-forge.js
  4. +53 −0 src/installers/darwin/zip.js
@@ -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"
@@ -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();
@@ -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();
@@ -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 });
};

0 comments on commit da3150d

Please sign in to comment.
You can’t perform that action at this time.