Skip to content

Commit

Permalink
feat(installer): manually mount and scan a DMG file when installing f…
Browse files Browse the repository at this point in the history
…or the .app

copy the .app file discovered from the DMG into the users Applications directory
  • Loading branch information
MarshallOfSound authored and malept committed Jan 24, 2017
1 parent 9a2f36c commit 7ea5af8
Show file tree
Hide file tree
Showing 4 changed files with 116 additions and 35 deletions.
33 changes: 30 additions & 3 deletions src/installers/darwin/dmg.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,32 @@
import opn from 'opn';
import { spawn } from 'child_process';
import fs from 'fs-promise';
import path from 'path';

export default async (filePath) => {
await opn(filePath, { wait: false });
import { getMountedImages, mountImage, unmountImage } from '../../util/hdiutil';
import moveApp from '../../util/move-app';

export default async (filePath, installSpinner) => {
const mounts = await getMountedImages();
let targetMount = mounts.find(mount => mount.imagePath === filePath);

if (!targetMount) {
targetMount = await mountImage(filePath);
}

try {
const volumePath = path.resolve('/Volumes', targetMount.mountPath);
const appName = (await fs.readdir(volumePath)).find(file => file.endsWith('.app'));
if (!appName) {
// eslint-disable-next-line no-throw-literal
throw 'Failed to find .app file in DMG';
}
const appPath = path.resolve(volumePath, appName);
const targetApplicationPath = `/Applications/${path.basename(appPath)}`;

await moveApp(appPath, targetApplicationPath, installSpinner, true);

spawn('open', ['-R', targetApplicationPath], { detached: true });
} finally {
await unmountImage(targetMount);
}
};
36 changes: 4 additions & 32 deletions src/installers/darwin/zip.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import { spawn } from 'child_process';
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';
import moveApp from '../../util/move-app';

export default async (filePath, installSpinner) => {
await new Promise((resolve) => {
Expand All @@ -14,40 +12,14 @@ export default async (filePath, installSpinner) => {
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 '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',
});
}
await moveApp(appPath, targetApplicationPath, installSpinner);

spawn('open', ['-R', targetApplicationPath], { detached: true });
};
42 changes: 42 additions & 0 deletions src/util/hdiutil.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { spawnPromise } from 'spawn-rx';
import debug from 'debug';

const d = debug('electron-forge:hdiutil');

export const getMountedImages = async () => {
const output = await spawnPromise('hdiutil', ['info']);
const mounts = output.split(/====\n/g);
mounts.shift();

const mountObjects = [];

for (const mount of mounts) {
try {
const mountPath = /\/Volumes\/(.+)\n/g.exec(mount)[1];
const imagePath = /image-path +: +(.+)\n/g.exec(mount)[1];
mountObjects.push({ mountPath, imagePath });
} catch (err) {
// Ignore
}
}

d('identified active mounts', mountObjects);
return mountObjects;
};

export const mountImage = async (filePath) => {
d('mounting image:', filePath);
const output = await spawnPromise('hdiutil', ['attach', '-noautoopen', '-nobrowse', '-noverify', filePath]);
const mountPath = /\/Volumes\/(.+)\n/g.exec(output)[1];
d('mounted at:', mountPath);

return {
mountPath,
iamgePath: filePath,
};
};

export const unmountImage = async (mount) => {
d('unmounting current mount:', mount);
await spawnPromise('hdiutil', ['unmount', '-force', `/Volumes/${mount.mountPath}`]);
};
40 changes: 40 additions & 0 deletions src/util/move-app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import fs from 'fs-promise';
import inquirer from 'inquirer';
import path from 'path';
import pify from 'pify';
import sudo from 'sudo-prompt';
import { exec } from 'child_process';

export default async (appPath, targetApplicationPath, spinner, copyInstead = false) => {
let writeAccess = true;
try {
await fs.access('/Applications', fs.W_OK);
} catch (err) {
writeAccess = false;
}

if (await fs.exists(targetApplicationPath)) {
spinner.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) {
// eslint-disable-next-line no-throw-literal
throw 'Installation stopped by user';
} else {
spinner.start();
await fs.remove(targetApplicationPath);
}
}

const moveCommand = `${copyInstead ? 'cp -r' : 'mv'} "${appPath}" "${targetApplicationPath}"`;
if (writeAccess) {
await pify(exec)(moveCommand);
} else {
await pify(sudo.exec)(moveCommand, {
name: 'Electron Forge',
});
}
};

0 comments on commit 7ea5af8

Please sign in to comment.