Add mount device custom icon generation (#33)
This commit is contained in:
parent
8a77dd1f60
commit
a428a90e55
175
cli.js
175
cli.js
|
@ -7,6 +7,7 @@ const appdmg = require('appdmg');
|
||||||
const plist = require('plist');
|
const plist = require('plist');
|
||||||
const Ora = require('ora');
|
const Ora = require('ora');
|
||||||
const execa = require('execa');
|
const execa = require('execa');
|
||||||
|
const composeIcon = require('./compose-icon');
|
||||||
|
|
||||||
if (process.platform !== 'darwin') {
|
if (process.platform !== 'darwin') {
|
||||||
console.error('macOS only');
|
console.error('macOS only');
|
||||||
|
@ -56,94 +57,106 @@ try {
|
||||||
|
|
||||||
const appInfo = plist.parse(infoPlist);
|
const appInfo = plist.parse(infoPlist);
|
||||||
const appName = appInfo.CFBundleDisplayName || appInfo.CFBundleName;
|
const appName = appInfo.CFBundleDisplayName || appInfo.CFBundleName;
|
||||||
// `const appIconName = appInfo.CFBundleIconFile.replace(/\.icns/, '');
|
const appIconName = appInfo.CFBundleIconFile.replace(/\.icns/, '');
|
||||||
const dmgPath = path.join(destPath, `${appName} ${appInfo.CFBundleShortVersionString}.dmg`);
|
const dmgPath = path.join(destPath, `${appName} ${appInfo.CFBundleShortVersionString}.dmg`);
|
||||||
|
|
||||||
const ora = new Ora('Creating DMG');
|
const ora = new Ora('Creating DMG');
|
||||||
ora.start();
|
ora.start();
|
||||||
|
|
||||||
if (cli.flags.overwrite) {
|
async function init() {
|
||||||
try {
|
if (cli.flags.overwrite) {
|
||||||
fs.unlinkSync(dmgPath);
|
try {
|
||||||
} catch (_) {}
|
fs.unlinkSync(dmgPath);
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
ora.text = 'Creating icon';
|
||||||
|
const composedIconPath = await composeIcon(path.join(appPath, 'Contents/Resources', `${appIconName}.icns`));
|
||||||
|
|
||||||
|
const ee = appdmg({
|
||||||
|
target: dmgPath,
|
||||||
|
basepath: process.cwd(),
|
||||||
|
specification: {
|
||||||
|
title: appName,
|
||||||
|
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'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ee.on('progress', info => {
|
||||||
|
if (info.type === 'step-begin') {
|
||||||
|
ora.text = info.title;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ee.on('finish', async () => {
|
||||||
|
try {
|
||||||
|
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('security', ['find-identity', '-v', '-p', 'codesigning']);
|
||||||
|
if (stdout.includes('Developer ID Application:')) {
|
||||||
|
identity = 'Developer ID Application';
|
||||||
|
} else if (stdout.includes('Mac Developer:')) {
|
||||||
|
identity = 'Mac Developer';
|
||||||
|
} else {
|
||||||
|
const err = new Error();
|
||||||
|
err.stderr = 'No usable identity found';
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ee.on('error', error => {
|
||||||
|
ora.fail(error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const ee = appdmg({
|
init().catch(error => {
|
||||||
target: dmgPath,
|
|
||||||
basepath: process.cwd(),
|
|
||||||
specification: {
|
|
||||||
title: appName,
|
|
||||||
// Disabled because of #16
|
|
||||||
// icon: path.join(appPath, 'Contents/Resources', `${appIconName}.icns`),
|
|
||||||
//
|
|
||||||
// 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'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
ee.on('progress', info => {
|
|
||||||
if (info.type === 'step-begin') {
|
|
||||||
ora.text = info.title;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
ee.on('finish', async () => {
|
|
||||||
ora.text = 'Code signing DMG';
|
|
||||||
|
|
||||||
try {
|
|
||||||
let identity;
|
|
||||||
const {stdout} = await execa('security', ['find-identity', '-v', '-p', 'codesigning']);
|
|
||||||
if (stdout.includes('Developer ID Application:')) {
|
|
||||||
identity = 'Developer ID Application';
|
|
||||||
} else if (stdout.includes('Mac Developer:')) {
|
|
||||||
identity = 'Mac Developer';
|
|
||||||
} else {
|
|
||||||
const err = new Error();
|
|
||||||
err.stderr = 'No usable identity found';
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
ee.on('error', error => {
|
|
||||||
ora.fail(error);
|
ora.fail(error);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,64 @@
|
||||||
|
const fs = require('fs');
|
||||||
|
const {promisify} = require('util');
|
||||||
|
const execa = require('execa');
|
||||||
|
const tempy = require('tempy');
|
||||||
|
const gm = require('gm').subClass({imageMagick: true});
|
||||||
|
const icns = require('icns-lib');
|
||||||
|
|
||||||
|
const readFile = promisify(fs.readFile);
|
||||||
|
const writeFile = promisify(fs.writeFile);
|
||||||
|
|
||||||
|
const filterMap = (map, filterFn) => Object.entries(map).filter(filterFn).reduce((out, [key, item]) => ({...out, [key]: item}), {});
|
||||||
|
// Drive icon from /System/Library/Extensions/IOStorageFamily.kext/Contents/Resources/Removable.icns
|
||||||
|
const baseDiskIconPath = `${__dirname}/disk-icon.icns`;
|
||||||
|
|
||||||
|
async function composeIcon(type, appIcon, mountIcon, composedIcon) {
|
||||||
|
mountIcon = gm(mountIcon);
|
||||||
|
appIcon = gm(appIcon);
|
||||||
|
const appIconSize = await promisify(appIcon.size.bind(appIcon))();
|
||||||
|
const mountIconSize = appIconSize;
|
||||||
|
|
||||||
|
// Change the perspective of the app icon to match the mount drive icon
|
||||||
|
appIcon = appIcon.out('-matte').out('-virtual-pixel', 'transparent').out('-distort', 'Perspective', `1,1 ${appIconSize.width * 0.08},1 ${appIconSize.width},1 ${appIconSize.width * 0.92},1 1,${appIconSize.height} 1,${appIconSize.height} ${appIconSize.width},${appIconSize.height} ${appIconSize.width},${appIconSize.height}`);
|
||||||
|
// Resize the app icon to fit it inside the mount icon, aspect ration should not be kept to create the perspective illution
|
||||||
|
appIcon = appIcon.resize(appIconSize.width / 1.7, appIconSize.height / 1.78, '!');
|
||||||
|
|
||||||
|
const tempAppIconPath = tempy.file({extension: 'png'});
|
||||||
|
await promisify(appIcon.write.bind(appIcon))(tempAppIconPath);
|
||||||
|
// Compose the two icons
|
||||||
|
const iconGravityFactor = mountIconSize.height * 0.155;
|
||||||
|
mountIcon = mountIcon.composite(tempAppIconPath).gravity('Center').geometry(`+0-${iconGravityFactor}`);
|
||||||
|
|
||||||
|
composedIcon[type] = await promisify(mountIcon.toBuffer.bind(mountIcon))();
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasGm = async () => {
|
||||||
|
try {
|
||||||
|
await execa('gm', ['-version']);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code === 'ENOENT') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = async function (appIconPath) {
|
||||||
|
if (!await hasGm()) {
|
||||||
|
return baseDiskIconPath;
|
||||||
|
}
|
||||||
|
const baseDiskIcons = filterMap(icns.parse(await readFile(baseDiskIconPath)), ([key]) => icns.isImageType(key));
|
||||||
|
const appIcon = filterMap(icns.parse(await readFile(appIconPath)), ([key]) => icns.isImageType(key));
|
||||||
|
|
||||||
|
const composedIcon = {};
|
||||||
|
await Promise.all(Object.entries(appIcon).map(async ([type, icon]) => {
|
||||||
|
if (baseDiskIcons[type]) {
|
||||||
|
return composeIcon(type, icon, baseDiskIcons[type], composedIcon);
|
||||||
|
}
|
||||||
|
console.warn('There is no base image for this type', type);
|
||||||
|
}));
|
||||||
|
const tempComposedIcon = tempy.file({extension: 'icns'});
|
||||||
|
await writeFile(tempComposedIcon, icns.format(composedIcon));
|
||||||
|
return tempComposedIcon;
|
||||||
|
};
|
Binary file not shown.
Binary file not shown.
After Width: | Height: | Size: 76 KiB |
Binary file not shown.
After Width: | Height: | Size: 67 KiB |
10
package.json
10
package.json
|
@ -20,7 +20,9 @@
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"cli.js",
|
"cli.js",
|
||||||
"assets"
|
"assets",
|
||||||
|
"disk-icon.icns",
|
||||||
|
"seticon"
|
||||||
],
|
],
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"cli-app",
|
"cli-app",
|
||||||
|
@ -38,13 +40,15 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"appdmg": "^0.5.2",
|
"appdmg": "^0.5.2",
|
||||||
"execa": "^1.0.0",
|
"execa": "^1.0.0",
|
||||||
|
"gm": "^1.23.1",
|
||||||
|
"icns-lib": "^1.0.1",
|
||||||
"meow": "^5.0.0",
|
"meow": "^5.0.0",
|
||||||
"ora": "^3.0.0",
|
"ora": "^3.0.0",
|
||||||
"plist": "^3.0.1"
|
"plist": "^3.0.1",
|
||||||
|
"tempy": "^0.2.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"ava": "^0.25.0",
|
"ava": "^0.25.0",
|
||||||
"tempy": "^0.2.1",
|
|
||||||
"xo": "^0.23.0"
|
"xo": "^0.23.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
14
readme.md
14
readme.md
|
@ -47,6 +47,20 @@ It will try to code sign the DMG, but the DMG is still created and fine even if
|
||||||
|
|
||||||
<img src="screenshot-dmg.png" width="772">
|
<img src="screenshot-dmg.png" width="772">
|
||||||
|
|
||||||
|
### DMG Icon
|
||||||
|
|
||||||
|
[GraphicsMagick](http://www.graphicsmagick.org/) is required to create the DMG icon based on the application icon and macOS mounted device icon.
|
||||||
|
|
||||||
|
### Steps using Brew
|
||||||
|
```bash
|
||||||
|
brew install imagemagick
|
||||||
|
brew install graphicsmagick
|
||||||
|
```
|
||||||
|
|
||||||
|
### Icon Example
|
||||||
|
|
||||||
|
<img src="icon-example-app.png" width="300">
|
||||||
|
<img src="icon-example.png" width="300">
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue