Skip to content

Commit 7d684a2

Browse files
dxdcsindresorhus
andauthored
Software licensing, support for pre-10.11 (#47)
Co-authored-by: Sindre Sorhus <[email protected]>
1 parent c9b232d commit 7d684a2

File tree

5 files changed

+166
-10
lines changed

5 files changed

+166
-10
lines changed

base.r

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
data 'TMPL' (128, "LPic") {
2+
$"1344 6566 6175 6C74 204C 616E 6775 6167" /* .Default Languag */
3+
$"6520 4944 4457 5244 0543 6F75 6E74 4F43" /* e IDDWRD.CountOC */
4+
$"4E54 042A 2A2A 2A4C 5354 430B 7379 7320" /* NT.****LSTC.sys */
5+
$"6C61 6E67 2049 4444 5752 441E 6C6F 6361" /* lang IDDWRD.loca */
6+
$"6C20 7265 7320 4944 2028 6F66 6673 6574" /* l res ID (offset */
7+
$"2066 726F 6D20 3530 3030 4457 5244 1032" /* from 5000DWRD.2 */
8+
$"2D62 7974 6520 6C61 6E67 7561 6765 3F44" /* -byte language?D */
9+
$"5752 4404 2A2A 2A2A 4C53 5445" /* WRD.****LSTE */
10+
};
11+
12+
data 'LPic' (5000) {
13+
$"0000 0001 0000 0000 0000"
14+
};
15+
16+
data 'STR#' (5000, "English") {
17+
$"0006 0745 6E67 6C69 7368 0541 6772 6565" /* ...English.Agree */
18+
$"0844 6973 6167 7265 6505 5072 696E 7407" /* .Disagree.Print. */
19+
$"5361 7665 2E2E 2E7B 4966 2079 6F75 2061" /* Save...{If you a */
20+
$"6772 6565 2077 6974 6820 7468 6520 7465" /* gree with the te */
21+
$"726D 7320 6F66 2074 6869 7320 6C69 6365" /* rms of this lice */
22+
$"6E73 652C 2070 7265 7373 2022 4167 7265" /* nse, press "Agre */
23+
$"6522 2074 6F20 696E 7374 616C 6C20 7468" /* e" to install th */
24+
$"6520 736F 6674 7761 7265 2E20 2049 6620" /* e software. If */
25+
$"796F 7520 646F 206E 6F74 2061 6772 6565" /* you do not agree */
26+
$"2C20 7072 6573 7320 2244 6973 6167 7265" /* , press "Disagre */
27+
$"6522 2E" /* e". */
28+
};
29+
30+
data 'styl' (5000, "English") {
31+
$"0001 0000 0000 000E 0011 0015 0000 000C"
32+
$"0000 0000 0000"
33+
};

cli.js

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const appdmg = require('appdmg');
77
const plist = require('plist');
88
const Ora = require('ora');
99
const execa = require('execa');
10+
const addLicenseAgreementIfNeeded = require('./sla.js');
1011
const composeIcon = require('./compose-icon');
1112

1213
if (process.platform !== 'darwin') {
@@ -73,13 +74,13 @@ async function init() {
7374
try {
7475
appInfo = plist.parse(infoPlist);
7576
} catch (_) {
76-
const {stdout} = await execa('plutil', ['-convert', 'xml1', '-o', '-', infoPlistPath]);
77+
const {stdout} = await execa('/usr/bin/plutil', ['-convert', 'xml1', '-o', '-', infoPlistPath]);
7778
appInfo = plist.parse(stdout);
7879
}
7980

8081
const appName = appInfo.CFBundleDisplayName || appInfo.CFBundleName;
8182
const appIconName = appInfo.CFBundleIconFile.replace(/\.icns/, '');
82-
const dmgTitle = appName.length > 27 ? (cli.flags['dmg-title'] || appName) : appName;
83+
const dmgTitle = appName.length > 27 ? (cli.flags.dmgTitle || appName) : appName;
8384
const dmgPath = path.join(destinationPath, `${appName} ${appInfo.CFBundleShortVersionString}.dmg`);
8485

8586
if (cli.flags.overwrite) {
@@ -91,6 +92,11 @@ async function init() {
9192
ora.text = 'Creating icon';
9293
const composedIconPath = await composeIcon(path.join(appPath, 'Contents/Resources', `${appIconName}.icns`));
9394

95+
const minSystemVersion = (Object.prototype.hasOwnProperty.call(appInfo, 'LSMinimumSystemVersion') && appInfo.LSMinimumSystemVersion.length > 0) ? appInfo.LSMinimumSystemVersion.toString() : '10.11';
96+
const minorVersion = Number(minSystemVersion.split('.')[1]) || 0;
97+
const dmgFormat = (minorVersion >= 11) ? 'ULFO' : 'UDZO'; // ULFO requires 10.11+
98+
ora.info(`Minimum runtime ${minSystemVersion} detected, using ${dmgFormat} format`).start();
99+
94100
const ee = appdmg({
95101
target: dmgPath,
96102
basepath: process.cwd(),
@@ -102,7 +108,7 @@ async function init() {
102108
// https://github.com/LinusU/node-appdmg/issues/135
103109
background: path.join(__dirname, 'assets/dmg-background.png'),
104110
'icon-size': 160,
105-
format: 'ULFO',
111+
format: dmgFormat,
106112
window: {
107113
size: {
108114
width: 660,
@@ -134,13 +140,16 @@ async function init() {
134140

135141
ee.on('finish', async () => {
136142
try {
143+
ora.text = 'Adding Software License Agreement if needed';
144+
await addLicenseAgreementIfNeeded(dmgPath, dmgFormat);
145+
137146
ora.text = 'Replacing DMG icon';
138147
// `seticon`` is a native tool to change files icons (Source: https://github.com/sveinbjornt/osxiconutils)
139148
await execa(path.join(__dirname, 'seticon'), [composedIconPath, dmgPath]);
140149

141150
ora.text = 'Code signing DMG';
142151
let identity;
143-
const {stdout} = await execa('security', ['find-identity', '-v', '-p', 'codesigning']);
152+
const {stdout} = await execa('/usr/bin/security', ['find-identity', '-v', '-p', 'codesigning']);
144153
if (cli.flags.identity && stdout.includes(`"${cli.flags.identity}"`)) {
145154
identity = cli.flags.identity;
146155
} else if (!cli.flags.identity && stdout.includes('Developer ID Application:')) {
@@ -155,8 +164,8 @@ async function init() {
155164
throw error;
156165
}
157166

158-
await execa('codesign', ['--sign', identity, dmgPath]);
159-
const {stderr} = await execa('codesign', [dmgPath, '--display', '--verbose=2']);
167+
await execa('/usr/bin/codesign', ['--sign', identity, dmgPath]);
168+
const {stderr} = await execa('/usr/bin/codesign', [dmgPath, '--display', '--verbose=2']);
160169

161170
const match = /^Authority=(.*)$/m.exec(stderr);
162171
if (!match) {
@@ -167,7 +176,7 @@ async function init() {
167176
ora.info(`Code signing identity: ${match[1]}`).start();
168177
ora.succeed('DMG created');
169178
} catch (error) {
170-
ora.fail(`Code signing failed. The DMG is fine, just not code signed.\n${error.stderr.trim()}`);
179+
ora.fail(`Code signing failed. The DMG is fine, just not code signed.\n${Object.prototype.hasOwnProperty.call(error, 'stderr') ? error.stderr.trim() : error}`);
171180
process.exit(2);
172181
}
173182
});

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@
2424
"compose-icon.js",
2525
"assets",
2626
"disk-icon.icns",
27-
"seticon"
27+
"seticon",
28+
"sla.js",
29+
"base.r"
2830
],
2931
"keywords": [
3032
"cli-app",

readme.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,17 +43,23 @@ $ create-dmg --help
4343

4444
## DMG
4545

46-
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`.
46+
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`.
4747

4848
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.
4949

5050
<img src="screenshot-dmg.png" width="772">
5151

52+
### Software license
53+
54+
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.
55+
56+
`/usr/bin/rez` [Command Line Tools for Xcode](https://developer.apple.com/download/more/) must be installed.
57+
5258
### DMG Icon
5359

5460
[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.
5561

56-
#### Steps using Homebrew
62+
#### Steps using [Homebrew](https://brew.sh)
5763

5864
```
5965
$ brew install graphicsmagick imagemagick

sla.js

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
const fs = require('fs');
2+
const path = require('path');
3+
const execa = require('execa');
4+
const tempy = require('tempy');
5+
6+
function getRtfUnicodeEscapedString(text) {
7+
let result = '';
8+
for (let i = 0; i < text.length; i++) {
9+
if (text[i] === '\\' || text[i] === '{' || text[i] === '}' || text[i] === '\n') {
10+
result += `\\${text[i]}`;
11+
} else if (text[i] === '\r') {
12+
// ignore
13+
} else if (text.charCodeAt(i) <= 0x7F) {
14+
result += text[i];
15+
} else {
16+
result += `\\u${text.codePointAt(i)}?`;
17+
}
18+
}
19+
20+
return result;
21+
}
22+
23+
function wrapInRtf(text) {
24+
return '\t$"7B5C 7274 6631 5C61 6E73 695C 616E 7369"\n' +
25+
'\t$"6370 6731 3235 325C 636F 636F 6172 7466"\n' +
26+
'\t$"3135 3034 5C63 6F63 6F61 7375 6272 7466"\n' +
27+
'\t$"3833 300A 7B5C 666F 6E74 7462 6C5C 6630"\n' +
28+
'\t$"5C66 7377 6973 735C 6663 6861 7273 6574"\n' +
29+
'\t$"3020 4865 6C76 6574 6963 613B 7D0A 7B5C"\n' +
30+
'\t$"636F 6C6F 7274 626C 3B5C 7265 6432 3535"\n' +
31+
'\t$"5C67 7265 656E 3235 355C 626C 7565 3235"\n' +
32+
'\t$"353B 7D0A 7B5C 2A5C 6578 7061 6E64 6564"\n' +
33+
'\t$"636F 6C6F 7274 626C 3B3B 7D0A 5C70 6172"\n' +
34+
'\t$"645C 7478 3536 305C 7478 3131 3230 5C74"\n' +
35+
'\t$"7831 3638 305C 7478 3232 3430 5C74 7832"\n' +
36+
'\t$"3830 305C 7478 3333 3630 5C74 7833 3932"\n' +
37+
'\t$"305C 7478 3434 3830 5C74 7835 3034 305C"\n' +
38+
'\t$"7478 3536 3030 5C74 7836 3136 305C 7478"\n' +
39+
'\t$"616C 5C70 6172 7469 6768 7465 6E66 6163"\n' +
40+
'\t$"746F 7230 0A0A 5C66 305C 6673 3234 205C"\n' +
41+
`${serializeString('63663020' + Buffer.from(getRtfUnicodeEscapedString(text)).toString('hex').toUpperCase() + '7D')}`;
42+
}
43+
44+
function serializeString(text) {
45+
return '\t$"' + text.match(/.{1,32}/g).map(x => x.match(/.{1,4}/g).join(' ')).join('"\n\t$"') + '"';
46+
}
47+
48+
module.exports = async (dmgPath, dmgFormat) => {
49+
// Valid SLA filenames
50+
const rawSlaFile = path.join(process.cwd(), 'sla.r');
51+
const rtfSlaFile = path.join(process.cwd(), 'license.rtf');
52+
const txtSlaFile = path.join(process.cwd(), 'license.txt');
53+
54+
const hasRaw = fs.existsSync(rawSlaFile);
55+
const hasRtf = fs.existsSync(rtfSlaFile);
56+
const hasTxt = fs.existsSync(txtSlaFile);
57+
58+
if (!hasRaw && !hasRtf && !hasTxt) {
59+
return;
60+
}
61+
62+
const tempDmgPath = tempy.file({extension: 'dmg'});
63+
64+
// UDCO or UDRO format is required to be able to unflatten
65+
// Convert and unflatten DMG (original format will be restored at the end)
66+
await execa('/usr/bin/hdiutil', ['convert', '-format', 'UDCO', dmgPath, '-o', tempDmgPath]);
67+
await execa('/usr/bin/hdiutil', ['unflatten', tempDmgPath]);
68+
69+
if (hasRaw) {
70+
// If user-defined sla.r file exists, add it to dmg with 'rez' utility
71+
await execa('/usr/bin/rez', ['-a', rawSlaFile, '-o', tempDmgPath]);
72+
} else {
73+
// Generate sla.r file from text/rtf file
74+
// Use base.r file as a starting point
75+
let data = fs.readFileSync(path.join(__dirname, 'base.r'), 'utf8');
76+
let plainText = '';
77+
78+
// Generate RTF version and preserve plain text
79+
data += '\ndata \'RTF \' (5000, "English") {\n';
80+
81+
if (hasRtf) {
82+
data += serializeString((fs.readFileSync(rtfSlaFile).toString('hex').toUpperCase()));
83+
({stdout: plainText} = await execa('/usr/bin/textutil', ['-convert', 'txt', '-stdout', rtfSlaFile]));
84+
} else {
85+
plainText = fs.readFileSync(txtSlaFile, 'utf8');
86+
data += wrapInRtf(plainText);
87+
}
88+
89+
data += '\n};\n';
90+
91+
// Generate plain text version
92+
// Used as an alternate for command-line deployments
93+
data += '\ndata \'TEXT\' (5000, "English") {\n';
94+
data += serializeString(Buffer.from(plainText, 'utf8').toString('hex').toUpperCase());
95+
data += '\n};\n';
96+
97+
// Save sla.r file, add it to DMG with `rez` utility
98+
const tempSlaFile = tempy.file({extension: 'r'});
99+
fs.writeFileSync(tempSlaFile, data, 'utf8');
100+
await execa('/usr/bin/rez', ['-a', tempSlaFile, '-o', tempDmgPath]);
101+
}
102+
103+
// Flatten and convert back to original dmgFormat
104+
await execa('/usr/bin/hdiutil', ['flatten', tempDmgPath]);
105+
await execa('/usr/bin/hdiutil', ['convert', '-format', dmgFormat, tempDmgPath, '-o', dmgPath, '-ov']);
106+
};

0 commit comments

Comments
 (0)