Software licensing, support for pre-10.11 (#47)

Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
This commit is contained in:
dxdc 2020-02-19 11:20:58 +00:00 committed by GitHub
parent c9b232da8f
commit 7d684a271a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 166 additions and 10 deletions

33
base.r Normal file
View File

@ -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
View File

@ -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);
}
});

View File

@ -24,7 +24,9 @@
"compose-icon.js",
"assets",
"disk-icon.icns",
"seticon"
"seticon",
"sla.js",
"base.r"
],
"keywords": [
"cli-app",

View File

@ -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

106
sla.js Normal file
View File

@ -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']);
};