Software licensing, support for pre-10.11 (#47)
Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
This commit is contained in:
parent
c9b232da8f
commit
7d684a271a
|
@ -0,0 +1,33 @@
|
|||
data 'TMPL' (128, "LPic") {
|
||||
$"1344 6566 6175 6C74 204C 616E 6775 6167" /* .Default Languag */
|
||||
$"6520 4944 4457 5244 0543 6F75 6E74 4F43" /* e IDDWRD.CountOC */
|
||||
$"4E54 042A 2A2A 2A4C 5354 430B 7379 7320" /* NT.****LSTC.sys */
|
||||
$"6C61 6E67 2049 4444 5752 441E 6C6F 6361" /* lang IDDWRD.loca */
|
||||
$"6C20 7265 7320 4944 2028 6F66 6673 6574" /* l res ID (offset */
|
||||
$"2066 726F 6D20 3530 3030 4457 5244 1032" /* from 5000DWRD.2 */
|
||||
$"2D62 7974 6520 6C61 6E67 7561 6765 3F44" /* -byte language?D */
|
||||
$"5752 4404 2A2A 2A2A 4C53 5445" /* WRD.****LSTE */
|
||||
};
|
||||
|
||||
data 'LPic' (5000) {
|
||||
$"0000 0001 0000 0000 0000"
|
||||
};
|
||||
|
||||
data 'STR#' (5000, "English") {
|
||||
$"0006 0745 6E67 6C69 7368 0541 6772 6565" /* ...English.Agree */
|
||||
$"0844 6973 6167 7265 6505 5072 696E 7407" /* .Disagree.Print. */
|
||||
$"5361 7665 2E2E 2E7B 4966 2079 6F75 2061" /* Save...{If you a */
|
||||
$"6772 6565 2077 6974 6820 7468 6520 7465" /* gree with the te */
|
||||
$"726D 7320 6F66 2074 6869 7320 6C69 6365" /* rms of this lice */
|
||||
$"6E73 652C 2070 7265 7373 2022 4167 7265" /* nse, press "Agre */
|
||||
$"6522 2074 6F20 696E 7374 616C 6C20 7468" /* e" to install th */
|
||||
$"6520 736F 6674 7761 7265 2E20 2049 6620" /* e software. If */
|
||||
$"796F 7520 646F 206E 6F74 2061 6772 6565" /* you do not agree */
|
||||
$"2C20 7072 6573 7320 2244 6973 6167 7265" /* , press "Disagre */
|
||||
$"6522 2E" /* e". */
|
||||
};
|
||||
|
||||
data 'styl' (5000, "English") {
|
||||
$"0001 0000 0000 000E 0011 0015 0000 000C"
|
||||
$"0000 0000 0000"
|
||||
};
|
23
cli.js
23
cli.js
|
@ -7,6 +7,7 @@ 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');
|
||||
|
||||
if (process.platform !== 'darwin') {
|
||||
|
@ -73,13 +74,13 @@ async function init() {
|
|||
try {
|
||||
appInfo = plist.parse(infoPlist);
|
||||
} catch (_) {
|
||||
const {stdout} = await execa('plutil', ['-convert', 'xml1', '-o', '-', infoPlistPath]);
|
||||
const {stdout} = await execa('/usr/bin/plutil', ['-convert', 'xml1', '-o', '-', infoPlistPath]);
|
||||
appInfo = plist.parse(stdout);
|
||||
}
|
||||
|
||||
const appName = appInfo.CFBundleDisplayName || appInfo.CFBundleName;
|
||||
const appIconName = appInfo.CFBundleIconFile.replace(/\.icns/, '');
|
||||
const dmgTitle = appName.length > 27 ? (cli.flags['dmg-title'] || appName) : appName;
|
||||
const dmgTitle = appName.length > 27 ? (cli.flags.dmgTitle || appName) : appName;
|
||||
const dmgPath = path.join(destinationPath, `${appName} ${appInfo.CFBundleShortVersionString}.dmg`);
|
||||
|
||||
if (cli.flags.overwrite) {
|
||||
|
@ -91,6 +92,11 @@ async function init() {
|
|||
ora.text = 'Creating icon';
|
||||
const 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(),
|
||||
|
@ -102,7 +108,7 @@ async function init() {
|
|||
// https://github.com/LinusU/node-appdmg/issues/135
|
||||
background: path.join(__dirname, 'assets/dmg-background.png'),
|
||||
'icon-size': 160,
|
||||
format: 'ULFO',
|
||||
format: dmgFormat,
|
||||
window: {
|
||||
size: {
|
||||
width: 660,
|
||||
|
@ -134,13 +140,16 @@ async function init() {
|
|||
|
||||
ee.on('finish', async () => {
|
||||
try {
|
||||
ora.text = 'Adding Software License Agreement if needed';
|
||||
await addLicenseAgreementIfNeeded(dmgPath, dmgFormat);
|
||||
|
||||
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']);
|
||||
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:')) {
|
||||
|
@ -155,8 +164,8 @@ async function init() {
|
|||
throw error;
|
||||
}
|
||||
|
||||
await execa('codesign', ['--sign', identity, dmgPath]);
|
||||
const {stderr} = await execa('codesign', [dmgPath, '--display', '--verbose=2']);
|
||||
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) {
|
||||
|
@ -167,7 +176,7 @@ async function init() {
|
|||
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()}`);
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -24,7 +24,9 @@
|
|||
"compose-icon.js",
|
||||
"assets",
|
||||
"disk-icon.icns",
|
||||
"seticon"
|
||||
"seticon",
|
||||
"sla.js",
|
||||
"base.r"
|
||||
],
|
||||
"keywords": [
|
||||
"cli-app",
|
||||
|
|
10
readme.md
10
readme.md
|
@ -43,17 +43,23 @@ $ create-dmg --help
|
|||
|
||||
## DMG
|
||||
|
||||
The DMG requires macOS 10.11 or later and has the filename `App Name 0.0.0.dmg`, for example `Lungo 1.0.0.dmg`.
|
||||
The DMG detects the minimum runtime of the app, and uses ULFO (macOS 10.11 or later) or UDZO as appropriate. The resulting image has the filename `App Name 0.0.0.dmg`, for example `Lungo 1.0.0.dmg`.
|
||||
|
||||
It will try to code sign the DMG, but the DMG is still created and fine even if the code signing fails, for example if you don't have a developer certificate.
|
||||
|
||||
<img src="screenshot-dmg.png" width="772">
|
||||
|
||||
### Software license
|
||||
|
||||
If `license.txt`, `license.rtf`, or `sla.r` ([raw SLAResources file](https://download.developer.apple.com/Developer_Tools/software_licensing_for_udif/slas_for_udifs_1.0.dmg)) are present in the same folder as the app, they will be added as a software agreement when opening the image. The image will not be mounted unless the user indicates agreement with the license.
|
||||
|
||||
`/usr/bin/rez` [Command Line Tools for Xcode](https://developer.apple.com/download/more/) must be installed.
|
||||
|
||||
### DMG Icon
|
||||
|
||||
[GraphicsMagick](http://www.graphicsmagick.org) is required to create the custom DMG icon that's based on the app icon and the macOS mounted device icon.
|
||||
|
||||
#### Steps using Homebrew
|
||||
#### Steps using [Homebrew](https://brew.sh)
|
||||
|
||||
```
|
||||
$ brew install graphicsmagick imagemagick
|
||||
|
|
|
@ -0,0 +1,106 @@
|
|||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const execa = require('execa');
|
||||
const tempy = require('tempy');
|
||||
|
||||
function getRtfUnicodeEscapedString(text) {
|
||||
let result = '';
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
if (text[i] === '\\' || text[i] === '{' || text[i] === '}' || text[i] === '\n') {
|
||||
result += `\\${text[i]}`;
|
||||
} else if (text[i] === '\r') {
|
||||
// ignore
|
||||
} else if (text.charCodeAt(i) <= 0x7F) {
|
||||
result += text[i];
|
||||
} else {
|
||||
result += `\\u${text.codePointAt(i)}?`;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function wrapInRtf(text) {
|
||||
return '\t$"7B5C 7274 6631 5C61 6E73 695C 616E 7369"\n' +
|
||||
'\t$"6370 6731 3235 325C 636F 636F 6172 7466"\n' +
|
||||
'\t$"3135 3034 5C63 6F63 6F61 7375 6272 7466"\n' +
|
||||
'\t$"3833 300A 7B5C 666F 6E74 7462 6C5C 6630"\n' +
|
||||
'\t$"5C66 7377 6973 735C 6663 6861 7273 6574"\n' +
|
||||
'\t$"3020 4865 6C76 6574 6963 613B 7D0A 7B5C"\n' +
|
||||
'\t$"636F 6C6F 7274 626C 3B5C 7265 6432 3535"\n' +
|
||||
'\t$"5C67 7265 656E 3235 355C 626C 7565 3235"\n' +
|
||||
'\t$"353B 7D0A 7B5C 2A5C 6578 7061 6E64 6564"\n' +
|
||||
'\t$"636F 6C6F 7274 626C 3B3B 7D0A 5C70 6172"\n' +
|
||||
'\t$"645C 7478 3536 305C 7478 3131 3230 5C74"\n' +
|
||||
'\t$"7831 3638 305C 7478 3232 3430 5C74 7832"\n' +
|
||||
'\t$"3830 305C 7478 3333 3630 5C74 7833 3932"\n' +
|
||||
'\t$"305C 7478 3434 3830 5C74 7835 3034 305C"\n' +
|
||||
'\t$"7478 3536 3030 5C74 7836 3136 305C 7478"\n' +
|
||||
'\t$"616C 5C70 6172 7469 6768 7465 6E66 6163"\n' +
|
||||
'\t$"746F 7230 0A0A 5C66 305C 6673 3234 205C"\n' +
|
||||
`${serializeString('63663020' + Buffer.from(getRtfUnicodeEscapedString(text)).toString('hex').toUpperCase() + '7D')}`;
|
||||
}
|
||||
|
||||
function serializeString(text) {
|
||||
return '\t$"' + text.match(/.{1,32}/g).map(x => x.match(/.{1,4}/g).join(' ')).join('"\n\t$"') + '"';
|
||||
}
|
||||
|
||||
module.exports = async (dmgPath, dmgFormat) => {
|
||||
// Valid SLA filenames
|
||||
const rawSlaFile = path.join(process.cwd(), 'sla.r');
|
||||
const rtfSlaFile = path.join(process.cwd(), 'license.rtf');
|
||||
const txtSlaFile = path.join(process.cwd(), 'license.txt');
|
||||
|
||||
const hasRaw = fs.existsSync(rawSlaFile);
|
||||
const hasRtf = fs.existsSync(rtfSlaFile);
|
||||
const hasTxt = fs.existsSync(txtSlaFile);
|
||||
|
||||
if (!hasRaw && !hasRtf && !hasTxt) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tempDmgPath = tempy.file({extension: 'dmg'});
|
||||
|
||||
// UDCO or UDRO format is required to be able to unflatten
|
||||
// Convert and unflatten DMG (original format will be restored at the end)
|
||||
await execa('/usr/bin/hdiutil', ['convert', '-format', 'UDCO', dmgPath, '-o', tempDmgPath]);
|
||||
await execa('/usr/bin/hdiutil', ['unflatten', tempDmgPath]);
|
||||
|
||||
if (hasRaw) {
|
||||
// If user-defined sla.r file exists, add it to dmg with 'rez' utility
|
||||
await execa('/usr/bin/rez', ['-a', rawSlaFile, '-o', tempDmgPath]);
|
||||
} else {
|
||||
// Generate sla.r file from text/rtf file
|
||||
// Use base.r file as a starting point
|
||||
let data = fs.readFileSync(path.join(__dirname, 'base.r'), 'utf8');
|
||||
let plainText = '';
|
||||
|
||||
// Generate RTF version and preserve plain text
|
||||
data += '\ndata \'RTF \' (5000, "English") {\n';
|
||||
|
||||
if (hasRtf) {
|
||||
data += serializeString((fs.readFileSync(rtfSlaFile).toString('hex').toUpperCase()));
|
||||
({stdout: plainText} = await execa('/usr/bin/textutil', ['-convert', 'txt', '-stdout', rtfSlaFile]));
|
||||
} else {
|
||||
plainText = fs.readFileSync(txtSlaFile, 'utf8');
|
||||
data += wrapInRtf(plainText);
|
||||
}
|
||||
|
||||
data += '\n};\n';
|
||||
|
||||
// Generate plain text version
|
||||
// Used as an alternate for command-line deployments
|
||||
data += '\ndata \'TEXT\' (5000, "English") {\n';
|
||||
data += serializeString(Buffer.from(plainText, 'utf8').toString('hex').toUpperCase());
|
||||
data += '\n};\n';
|
||||
|
||||
// Save sla.r file, add it to DMG with `rez` utility
|
||||
const tempSlaFile = tempy.file({extension: 'r'});
|
||||
fs.writeFileSync(tempSlaFile, data, 'utf8');
|
||||
await execa('/usr/bin/rez', ['-a', tempSlaFile, '-o', tempDmgPath]);
|
||||
}
|
||||
|
||||
// Flatten and convert back to original dmgFormat
|
||||
await execa('/usr/bin/hdiutil', ['flatten', tempDmgPath]);
|
||||
await execa('/usr/bin/hdiutil', ['convert', '-format', dmgFormat, tempDmgPath, '-o', dmgPath, '-ov']);
|
||||
};
|
Loading…
Reference in New Issue