create-dmg/cli.js

209 lines
5.6 KiB
JavaScript
Raw Normal View History

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');
const addLicenseAgreementIfNeeded = require('./sla.js');
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
$ create-dmg <app> [destination]
2018-04-29 12:02:41 +00:00
2018-04-29 13:58:01 +00:00
Options
--overwrite Overwrite existing DMG with the same name
--identity=<value> Manually set code signing identity (automatic by default)
--dmg-title=<value> Manually set DMG title (must be <=27 characters) [default: App name]
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'
},
identity: {
type: 'string'
},
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();
}
2019-12-06 09:20:17 +00:00
const infoPlistPath = path.join(appPath, 'Contents/Info.plist');
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)}\``);
process.exit(1);
}
2018-10-17 07:59:58 +00:00
throw error;
}
2017-03-27 14:09:27 +00:00
const ora = new Ora('Creating DMG');
ora.start();
async function init() {
2019-12-06 09:20:17 +00:00
let appInfo;
try {
appInfo = plist.parse(infoPlist);
} catch (_) {
const {stdout} = await execa('/usr/bin/plutil', ['-convert', 'xml1', '-o', '-', infoPlistPath]);
2019-12-06 09:20:17 +00:00
appInfo = plist.parse(stdout);
}
const appName = appInfo.CFBundleDisplayName || appInfo.CFBundleName;
if (!appName) {
throw new Error('The app must have `CFBundleDisplayName` or `CFBundleName` defined in its `Info.plist`.');
}
const dmgTitle = cli.flags.dmgTitle || 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
if (dmgTitle > 27) {
ora.fail('The disk image title cannot exceed 27 characters. This is a limitation in a dependency: https://github.com/LinusU/node-alias/issues/7');
process.exit(1);
}
if (cli.flags.overwrite) {
try {
fs.unlinkSync(dmgPath);
} catch (_) {}
2017-03-27 14:09:27 +00:00
}
2020-04-20 12:32:38 +00:00
const hasAppIcon = appInfo.CFBundleIconFile;
let composedIconPath;
if (hasAppIcon) {
ora.text = 'Creating icon';
const appIconName = appInfo.CFBundleIconFile.replace(/\.icns/, '');
composedIconPath = await composeIcon(path.join(appPath, 'Contents/Resources', `${appIconName}.icns`));
}
const minSystemVersion = (Object.prototype.hasOwnProperty.call(appInfo, 'LSMinimumSystemVersion') && appInfo.LSMinimumSystemVersion.length > 0) ? appInfo.LSMinimumSystemVersion.toString() : '10.11';
const minorVersion = Number(minSystemVersion.split('.')[1]) || 0;
const dmgFormat = (minorVersion >= 11) ? 'ULFO' : 'UDZO'; // ULFO requires 10.11+
ora.info(`Minimum runtime ${minSystemVersion} detected, using ${dmgFormat} format`).start();
const ee = appdmg({
target: dmgPath,
basepath: process.cwd(),
specification: {
title: dmgTitle,
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: dmgFormat,
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
ee.on('progress', info => {
if (info.type === 'step-begin') {
ora.text = info.title;
}
});
ee.on('finish', async () => {
try {
ora.text = 'Adding Software License Agreement if needed';
await addLicenseAgreementIfNeeded(dmgPath, dmgFormat);
2020-04-20 12:32:38 +00:00
if (hasAppIcon) {
ora.text = 'Replacing DMG icon';
// `seticon`` is a native tool to change files icons (Source: https://github.com/sveinbjornt/osxiconutils)
await execa(path.join(__dirname, 'seticon'), [composedIconPath, dmgPath]);
}
ora.text = 'Code signing DMG';
let identity;
const {stdout} = await execa('/usr/bin/security', ['find-identity', '-v', '-p', 'codesigning']);
if (cli.flags.identity && stdout.includes(`"${cli.flags.identity}"`)) {
identity = cli.flags.identity;
} else if (!cli.flags.identity && stdout.includes('Developer ID Application:')) {
identity = 'Developer ID Application';
} else if (!cli.flags.identity && stdout.includes('Mac Developer:')) {
identity = 'Mac Developer';
}
if (!identity) {
2019-05-12 16:44:54 +00:00
const error = new Error();
error.stderr = 'No suitable code signing identity found';
2019-05-12 16:44:54 +00:00
throw error;
}
await execa('/usr/bin/codesign', ['--sign', identity, dmgPath]);
const {stderr} = await execa('/usr/bin/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
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${Object.prototype.hasOwnProperty.call(error, 'stderr') ? error.stderr.trim() : error}`);
process.exit(2);
2017-03-27 14:09:27 +00:00
}
});
2017-03-27 14:09:27 +00:00
ee.on('error', error => {
2019-06-12 18:16:30 +00:00
ora.fail(`Building the DMG failed. ${error}`);
process.exit(1);
});
}
2017-03-27 14:09:27 +00:00
init().catch(error => {
2020-04-20 12:26:06 +00:00
ora.fail((error && error.stack) || error);
2017-03-27 14:09:27 +00:00
process.exit(1);
});