Skip to content

Commit

Permalink
Software licensing, support for pre-10.11 (#47)
Browse files Browse the repository at this point in the history
Co-authored-by: Sindre Sorhus <[email protected]>
  • Loading branch information
dxdc and sindresorhus authored Feb 19, 2020
1 parent c9b232d commit 7d684a2
Show file tree
Hide file tree
Showing 5 changed files with 166 additions and 10 deletions.
33 changes: 33 additions & 0 deletions base.r
Original file line number Diff line number Diff line change
@@ -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 changes: 16 additions & 7 deletions cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand Down Expand Up @@ -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) {
Expand All @@ -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(),
Expand All @@ -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,
Expand Down Expand Up @@ -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:')) {
Expand All @@ -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) {
Expand All @@ -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);
}
});
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@
"compose-icon.js",
"assets",
"disk-icon.icns",
"seticon"
"seticon",
"sla.js",
"base.r"
],
"keywords": [
"cli-app",
Expand Down
10 changes: 8 additions & 2 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
106 changes: 106 additions & 0 deletions sla.js
Original file line number Diff line number Diff line change
@@ -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']);
};

0 comments on commit 7d684a2

Please sign in to comment.