2017-03-27 14:09:27 +00:00
|
|
|
#!/usr/bin/env node
|
|
|
|
'use strict';
|
|
|
|
const path = require('path');
|
|
|
|
const fs = require('fs');
|
|
|
|
const meow = require('meow');
|
|
|
|
const appdmg = require('appdmg');
|
|
|
|
const plist = require('plist');
|
|
|
|
const Ora = require('ora');
|
|
|
|
const execa = require('execa');
|
2019-05-12 16:40:16 +00:00
|
|
|
const composeIcon = require('./compose-icon');
|
2017-03-27 14:09:27 +00:00
|
|
|
|
2018-04-29 12:07:51 +00:00
|
|
|
if (process.platform !== 'darwin') {
|
|
|
|
console.error('macOS only');
|
|
|
|
process.exit(1);
|
|
|
|
}
|
|
|
|
|
2017-03-27 14:09:27 +00:00
|
|
|
const cli = meow(`
|
2018-04-29 12:02:41 +00:00
|
|
|
Usage
|
2018-04-29 11:57:59 +00:00
|
|
|
$ create-dmg <app> [destination]
|
2018-04-29 12:02:41 +00:00
|
|
|
|
2018-04-29 13:58:01 +00:00
|
|
|
Options
|
2019-12-06 09:27:35 +00:00
|
|
|
--overwrite Overwrite existing DMG with the same name
|
|
|
|
--identity=<value> Manually set code signing identity (automatic by default)
|
|
|
|
--dmg-title=<value> Manually set title of DMG volume (only used if app name is >27 character limit)
|
2018-04-29 13:58:01 +00:00
|
|
|
|
2018-04-29 12:02:41 +00:00
|
|
|
Examples
|
2017-03-27 14:09:27 +00:00
|
|
|
$ create-dmg 'Lungo.app'
|
2018-04-29 12:02:41 +00:00
|
|
|
$ create-dmg 'Lungo.app' Build/Releases
|
2018-04-29 13:58:01 +00:00
|
|
|
`, {
|
|
|
|
flags: {
|
|
|
|
overwrite: {
|
|
|
|
type: 'boolean'
|
2019-07-11 11:21:16 +00:00
|
|
|
},
|
|
|
|
identity: {
|
|
|
|
type: 'string'
|
2019-12-06 09:27:35 +00:00
|
|
|
},
|
|
|
|
dmgTitle: {
|
|
|
|
type: 'string'
|
2018-04-29 13:58:01 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
2017-03-27 14:09:27 +00:00
|
|
|
|
2019-12-06 09:30:30 +00:00
|
|
|
let [appPath, destinationPath] = cli.input;
|
2017-03-27 14:09:27 +00:00
|
|
|
|
2018-04-29 12:07:51 +00:00
|
|
|
if (!appPath) {
|
2017-03-27 14:09:27 +00:00
|
|
|
console.error('Specify an app');
|
|
|
|
process.exit(1);
|
|
|
|
}
|
|
|
|
|
2019-12-06 09:30:30 +00:00
|
|
|
if (!destinationPath) {
|
|
|
|
destinationPath = process.cwd();
|
2018-04-29 11:57:59 +00:00
|
|
|
}
|
|
|
|
|
2019-12-06 09:20:17 +00:00
|
|
|
const infoPlistPath = path.join(appPath, 'Contents/Info.plist');
|
|
|
|
|
2017-03-31 02:04:34 +00:00
|
|
|
let infoPlist;
|
|
|
|
try {
|
2019-12-06 09:20:17 +00:00
|
|
|
infoPlist = fs.readFileSync(infoPlistPath, 'utf8');
|
2018-10-17 07:59:58 +00:00
|
|
|
} catch (error) {
|
|
|
|
if (error.code === 'ENOENT') {
|
2018-04-29 12:07:51 +00:00
|
|
|
console.error(`Could not find \`${path.relative(process.cwd(), appPath)}\``);
|
2017-03-31 02:04:34 +00:00
|
|
|
process.exit(1);
|
|
|
|
}
|
|
|
|
|
2018-10-17 07:59:58 +00:00
|
|
|
throw error;
|
2017-03-31 02:04:34 +00:00
|
|
|
}
|
|
|
|
|
2017-03-27 14:09:27 +00:00
|
|
|
const ora = new Ora('Creating DMG');
|
|
|
|
ora.start();
|
|
|
|
|
2019-05-12 16:40:16 +00:00
|
|
|
async function init() {
|
2019-12-06 09:20:17 +00:00
|
|
|
let appInfo;
|
|
|
|
try {
|
|
|
|
appInfo = plist.parse(infoPlist);
|
|
|
|
} catch (_) {
|
|
|
|
const {stdout} = await execa('plutil', ['-convert', 'xml1', '-o', '-', infoPlistPath]);
|
|
|
|
appInfo = plist.parse(stdout);
|
|
|
|
}
|
|
|
|
|
|
|
|
const appName = appInfo.CFBundleDisplayName || appInfo.CFBundleName;
|
|
|
|
const appIconName = appInfo.CFBundleIconFile.replace(/\.icns/, '');
|
2019-12-06 09:27:35 +00:00
|
|
|
const dmgTitle = appName.length > 27 ? (cli.flags['dmg-title'] || appName) : appName;
|
2019-12-06 09:30:30 +00:00
|
|
|
const dmgPath = path.join(destinationPath, `${appName} ${appInfo.CFBundleShortVersionString}.dmg`);
|
2019-12-06 09:20:17 +00:00
|
|
|
|
2019-05-12 16:40:16 +00:00
|
|
|
if (cli.flags.overwrite) {
|
|
|
|
try {
|
|
|
|
fs.unlinkSync(dmgPath);
|
|
|
|
} catch (_) {}
|
2017-03-27 14:09:27 +00:00
|
|
|
}
|
|
|
|
|
2019-05-12 16:40:16 +00:00
|
|
|
ora.text = 'Creating icon';
|
|
|
|
const composedIconPath = await composeIcon(path.join(appPath, 'Contents/Resources', `${appIconName}.icns`));
|
|
|
|
|
|
|
|
const ee = appdmg({
|
|
|
|
target: dmgPath,
|
|
|
|
basepath: process.cwd(),
|
|
|
|
specification: {
|
2019-12-06 09:27:35 +00:00
|
|
|
title: dmgTitle,
|
2019-05-12 16:40:16 +00:00
|
|
|
icon: composedIconPath,
|
|
|
|
//
|
|
|
|
// Use transparent background and `background-color` option when this is fixed:
|
|
|
|
// https://github.com/LinusU/node-appdmg/issues/135
|
|
|
|
background: path.join(__dirname, 'assets/dmg-background.png'),
|
|
|
|
'icon-size': 160,
|
|
|
|
format: 'ULFO',
|
|
|
|
window: {
|
|
|
|
size: {
|
|
|
|
width: 660,
|
|
|
|
height: 400
|
|
|
|
}
|
|
|
|
},
|
|
|
|
contents: [
|
|
|
|
{
|
|
|
|
x: 180,
|
|
|
|
y: 170,
|
|
|
|
type: 'file',
|
|
|
|
path: appPath
|
|
|
|
},
|
|
|
|
{
|
|
|
|
x: 480,
|
|
|
|
y: 170,
|
|
|
|
type: 'link',
|
|
|
|
path: '/Applications'
|
|
|
|
}
|
|
|
|
]
|
|
|
|
}
|
|
|
|
});
|
2017-03-27 14:09:27 +00:00
|
|
|
|
2019-05-12 16:40:16 +00:00
|
|
|
ee.on('progress', info => {
|
|
|
|
if (info.type === 'step-begin') {
|
|
|
|
ora.text = info.title;
|
2018-05-01 14:37:08 +00:00
|
|
|
}
|
2019-05-12 16:40:16 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
ee.on('finish', async () => {
|
|
|
|
try {
|
|
|
|
ora.text = 'Replacing DMG icon';
|
2019-05-12 16:44:54 +00:00
|
|
|
// `seticon`` is a native tool to change files icons (Source: https://github.com/sveinbjornt/osxiconutils)
|
2019-05-12 16:40:16 +00:00
|
|
|
await execa(path.join(__dirname, 'seticon'), [composedIconPath, dmgPath]);
|
|
|
|
|
|
|
|
ora.text = 'Code signing DMG';
|
|
|
|
let identity;
|
|
|
|
const {stdout} = await execa('security', ['find-identity', '-v', '-p', 'codesigning']);
|
2019-07-11 11:21:16 +00:00
|
|
|
if (cli.flags.identity && stdout.includes(`"${cli.flags.identity}"`)) {
|
|
|
|
identity = cli.flags.identity;
|
|
|
|
} else if (!cli.flags.identity && stdout.includes('Developer ID Application:')) {
|
2019-05-12 16:40:16 +00:00
|
|
|
identity = 'Developer ID Application';
|
2019-07-11 11:21:16 +00:00
|
|
|
} else if (!cli.flags.identity && stdout.includes('Mac Developer:')) {
|
2019-05-12 16:40:16 +00:00
|
|
|
identity = 'Mac Developer';
|
2019-07-11 11:21:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (!identity) {
|
2019-05-12 16:44:54 +00:00
|
|
|
const error = new Error();
|
2019-07-11 11:21:16 +00:00
|
|
|
error.stderr = 'No suitable code signing identity found';
|
2019-05-12 16:44:54 +00:00
|
|
|
throw error;
|
2019-05-12 16:40:16 +00:00
|
|
|
}
|
2018-05-01 14:37:08 +00:00
|
|
|
|
2019-05-12 16:40:16 +00:00
|
|
|
await execa('codesign', ['--sign', identity, dmgPath]);
|
|
|
|
const {stderr} = await execa('codesign', [dmgPath, '--display', '--verbose=2']);
|
|
|
|
|
|
|
|
const match = /^Authority=(.*)$/m.exec(stderr);
|
|
|
|
if (!match) {
|
|
|
|
ora.fail('Not code signed');
|
|
|
|
process.exit(1);
|
|
|
|
}
|
2017-03-27 14:09:27 +00:00
|
|
|
|
2019-05-12 16:40:16 +00:00
|
|
|
ora.info(`Code signing identity: ${match[1]}`).start();
|
|
|
|
ora.succeed('DMG created');
|
|
|
|
} catch (error) {
|
|
|
|
ora.fail(`Code signing failed. The DMG is fine, just not code signed.\n${error.stderr.trim()}`);
|
|
|
|
process.exit(2);
|
2017-03-27 14:09:27 +00:00
|
|
|
}
|
2019-05-12 16:40:16 +00:00
|
|
|
});
|
2017-03-27 14:09:27 +00:00
|
|
|
|
2019-05-12 16:40:16 +00:00
|
|
|
ee.on('error', error => {
|
2019-06-12 18:16:30 +00:00
|
|
|
ora.fail(`Building the DMG failed. ${error}`);
|
2019-05-12 16:40:16 +00:00
|
|
|
process.exit(1);
|
|
|
|
});
|
|
|
|
}
|
2017-03-27 14:09:27 +00:00
|
|
|
|
2019-05-12 16:40:16 +00:00
|
|
|
init().catch(error => {
|
2018-04-29 12:07:51 +00:00
|
|
|
ora.fail(error);
|
2017-03-27 14:09:27 +00:00
|
|
|
process.exit(1);
|
|
|
|
});
|