├── test
├── assets
│ ├── TestDoc.txt
│ ├── TestApp.m
│ ├── TestBkg.png
│ ├── TestBkg.xcf
│ ├── TestBkg@2x.png
│ ├── TestIcon.icns
│ ├── TestApp.app
│ │ └── Contents
│ │ │ ├── MacOS
│ │ │ └── testapp
│ │ │ ├── Resources
│ │ │ └── TestIcon.icns
│ │ │ └── Info.plist
│ ├── appdmg-legacy.json
│ ├── appdmg-bg-color.json
│ └── appdmg.json
├── accepted-1.png
├── accepted-2.png
├── accepted-3.png
├── accepted-1@2x.png
├── accepted-2@2x.png
├── accepted-3@2x.png
├── lib
│ ├── image-format.js
│ └── visually-verify-image.js
├── bin.js
└── api.js
├── index.js
├── help
├── help.png
└── help.xcf
├── .gitignore
├── lib
├── colors.js
├── legacy.js
├── util.js
├── hdiutil.js
├── pipeline.js
└── appdmg.js
├── package.json
├── LICENSE
├── schema.json
├── bin
└── appdmg.js
└── README.md
/test/assets/TestDoc.txt:
--------------------------------------------------------------------------------
1 | This is just a test.
2 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | module.exports = exports = require('./lib/appdmg')
2 |
--------------------------------------------------------------------------------
/help/help.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LinusU/node-appdmg/HEAD/help/help.png
--------------------------------------------------------------------------------
/help/help.xcf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LinusU/node-appdmg/HEAD/help/help.xcf
--------------------------------------------------------------------------------
/test/accepted-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LinusU/node-appdmg/HEAD/test/accepted-1.png
--------------------------------------------------------------------------------
/test/accepted-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LinusU/node-appdmg/HEAD/test/accepted-2.png
--------------------------------------------------------------------------------
/test/accepted-3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LinusU/node-appdmg/HEAD/test/accepted-3.png
--------------------------------------------------------------------------------
/test/accepted-1@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LinusU/node-appdmg/HEAD/test/accepted-1@2x.png
--------------------------------------------------------------------------------
/test/accepted-2@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LinusU/node-appdmg/HEAD/test/accepted-2@2x.png
--------------------------------------------------------------------------------
/test/accepted-3@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LinusU/node-appdmg/HEAD/test/accepted-3@2x.png
--------------------------------------------------------------------------------
/test/assets/TestApp.m:
--------------------------------------------------------------------------------
1 |
2 | int main(int argc, const char* argv[]) {
3 | return 0;
4 | }
5 |
6 |
--------------------------------------------------------------------------------
/test/assets/TestBkg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LinusU/node-appdmg/HEAD/test/assets/TestBkg.png
--------------------------------------------------------------------------------
/test/assets/TestBkg.xcf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LinusU/node-appdmg/HEAD/test/assets/TestBkg.xcf
--------------------------------------------------------------------------------
/test/assets/TestBkg@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LinusU/node-appdmg/HEAD/test/assets/TestBkg@2x.png
--------------------------------------------------------------------------------
/test/assets/TestIcon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LinusU/node-appdmg/HEAD/test/assets/TestIcon.icns
--------------------------------------------------------------------------------
/test/assets/TestApp.app/Contents/MacOS/testapp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LinusU/node-appdmg/HEAD/test/assets/TestApp.app/Contents/MacOS/testapp
--------------------------------------------------------------------------------
/test/assets/TestApp.app/Contents/Resources/TestIcon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LinusU/node-appdmg/HEAD/test/assets/TestApp.app/Contents/Resources/TestIcon.icns
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | lib-cov
2 | *.seed
3 | *.log
4 | *.csv
5 | *.dat
6 | *.out
7 | *.pid
8 | *.gz
9 |
10 | pids
11 | logs
12 | results
13 |
14 | npm-debug.log
15 | node_modules
16 |
--------------------------------------------------------------------------------
/test/assets/appdmg-legacy.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Test Title",
3 | "app": "TestApp.app",
4 | "background": "TestBkg.png",
5 | "icon": "TestIcon.icns",
6 | "icons": {
7 | "size": 80,
8 | "app": [192, 344],
9 | "alias": [448, 344]
10 | },
11 | "extra": [
12 | ["TestDoc.txt", 512, 128]
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------
/test/lib/image-format.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const spawnSync = require('child_process').spawnSync
4 |
5 | function imageFormat (imagePath) {
6 | const arg = ['imageinfo', '-format', imagePath]
7 | const out = spawnSync('hdiutil', arg).stdout
8 |
9 | return out.toString().trim()
10 | }
11 |
12 | module.exports = imageFormat
13 |
--------------------------------------------------------------------------------
/test/assets/appdmg-bg-color.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Test Title",
3 | "icon": "TestIcon.icns",
4 | "background-color": "mintcream",
5 | "contents": [
6 | { "x": 448, "y": 344, "type": "link", "path": "/Applications" },
7 | { "x": 192, "y": 344, "type": "file", "path": "TestApp.app" },
8 | { "x": 512, "y": 128, "type": "file", "path": "TestDoc.txt" }
9 | ]
10 | }
11 |
--------------------------------------------------------------------------------
/test/assets/TestApp.app/Contents/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleExecutable
6 | testapp
7 | CFBundleIconFile
8 | TestIcon.icns
9 | CFBundleName
10 | Test App
11 |
12 |
13 |
--------------------------------------------------------------------------------
/test/assets/appdmg.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Test Title",
3 | "icon": "TestIcon.icns",
4 | "background": "TestBkg.png",
5 | "contents": [
6 | { "x": 448, "y": 344, "type": "link", "path": "/Applications" },
7 | { "x": 192, "y": 344, "type": "file", "path": "TestApp.app" },
8 | { "x": 512, "y": 128, "type": "file", "path": "TestDoc.txt" },
9 | { "x": 512, "y": 900, "type": "position", "path": ".VolumeIcon.icns" }
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/lib/colors.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const reset = '\u001b[0m'
4 | const colors = {
5 | black: '\u001b[0;30m',
6 | red: '\u001b[0;31m',
7 | green: '\u001b[0;32m',
8 | yellow: '\u001b[0;33m',
9 | blue: '\u001b[0;34m',
10 | purple: '\u001b[0;35m',
11 | cyan: '\u001b[0;36m',
12 | white: '\u001b[0;37m'
13 | }
14 |
15 | for (const key of Object.keys(colors)) {
16 | exports[key] = function (text) {
17 | return `${colors[key]}${text}${reset}`
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/lib/legacy.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | function convert (src) {
4 | const obj = {}
5 |
6 | obj.title = src.title
7 | obj.icon = src.icon
8 | obj.background = src.background
9 |
10 | obj['icon-size'] = src.icons.size
11 |
12 | obj.contents = [
13 | { x: src.icons.alias[0], y: src.icons.alias[1], type: 'link', path: '/Applications' },
14 | { x: src.icons.app[0], y: src.icons.app[1], type: 'file', path: src.app }
15 | ]
16 |
17 | for (const extra of (src.extra || [])) {
18 | obj.contents.push({
19 | x: extra[1],
20 | y: extra[2],
21 | type: 'file',
22 | path: extra[0]
23 | })
24 | }
25 |
26 | return obj
27 | }
28 |
29 | exports.convert = convert
30 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "appdmg",
3 | "version": "0.6.6",
4 | "license": "MIT",
5 | "author": "Linus Unnebäck ",
6 | "bin": "bin/appdmg.js",
7 | "repository": "LinusU/node-appdmg",
8 | "dependencies": {
9 | "async": "^1.4.2",
10 | "ds-store": "^0.1.5",
11 | "execa": "^1.0.0",
12 | "fs-temp": "^1.0.0",
13 | "fs-xattr": "^0.3.0",
14 | "image-size": "^0.7.4",
15 | "is-my-json-valid": "^2.20.0",
16 | "minimist": "^1.1.3",
17 | "parse-color": "^1.0.0",
18 | "path-exists": "^4.0.0",
19 | "repeat-string": "^1.5.4"
20 | },
21 | "engines": {
22 | "node": ">=8.5"
23 | },
24 | "os": [
25 | "darwin"
26 | ],
27 | "scripts": {
28 | "test": "standard && mocha -b"
29 | },
30 | "devDependencies": {
31 | "capture-window": "^0.1.3",
32 | "looks-same": "^7.2.1",
33 | "mocha": "^6.1.4",
34 | "standard": "^12.0.1"
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2013 Linus Unnebäck
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of
6 | this software and associated documentation files (the "Software"), to deal in
7 | the Software without restriction, including without limitation the rights to
8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9 | the Software, and to permit persons to whom the Software is furnished to do so,
10 | subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/test/bin.js:
--------------------------------------------------------------------------------
1 | /* eslint-env mocha */
2 |
3 | 'use strict'
4 |
5 | const pkg = require('../package.json')
6 |
7 | const fs = require('fs')
8 | const path = require('path')
9 | const temp = require('fs-temp')
10 | const assert = require('assert')
11 | const spawnSync = require('child_process').spawnSync
12 |
13 | const bin = path.join(__dirname, '..', 'bin', 'appdmg.js')
14 |
15 | function bufferContains (buffer, needle) {
16 | return (buffer.toString().indexOf(needle) !== -1)
17 | }
18 |
19 | describe('bin', function () {
20 | it('should print version number', function () {
21 | const res = spawnSync(bin, [ '--version' ])
22 |
23 | assert.ok(bufferContains(res.stderr, pkg.version))
24 | })
25 |
26 | it('should print usage', function () {
27 | const res = spawnSync(bin, [ '--help' ])
28 |
29 | assert.ok(bufferContains(res.stderr, 'Usage:'))
30 | })
31 |
32 | it('should create dmg file', function () {
33 | this.timeout(60000)
34 |
35 | const source = path.join(__dirname, 'assets', 'appdmg.json')
36 | const targetDir = temp.mkdirSync()
37 | const targetPath = path.join(targetDir, 'Test.dmg')
38 |
39 | const res = spawnSync(bin, [ source, targetPath ])
40 |
41 | fs.unlinkSync(targetPath)
42 | fs.rmdirSync(targetDir)
43 |
44 | assert.ok(bufferContains(res.stderr, targetPath))
45 | })
46 | })
47 |
--------------------------------------------------------------------------------
/lib/util.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const execa = require('execa')
4 | const pathExists = require('path-exists')
5 | const util = require('util')
6 | const xattr = require('fs-xattr')
7 |
8 | exports.sh = function (prog, args, cb) {
9 | util.callbackify(() => execa(prog, args))(cb)
10 | }
11 |
12 | exports.dusm = function (path, cb) {
13 | exports.sh('du', ['-sm', path], (err, res) => {
14 | if (err) return cb(err)
15 |
16 | if (res.stderr.length > 0) {
17 | return cb(new Error(`du -sm: ${res.stderr}`))
18 | }
19 |
20 | const m = /^([0-9]+)\t/.exec(res.stdout)
21 | if (m === null) {
22 | console.log(res.stdout)
23 | return cb(new Error('du -sm: Unknown error'))
24 | }
25 |
26 | return cb(null, parseInt(m[1], 10))
27 | })
28 | }
29 |
30 | exports.tiffutil = function (a, b, out, cb) {
31 | exports.sh('tiffutil', ['-cathidpicheck', a, b, '-out', out], (err) => cb(err))
32 | }
33 |
34 | exports.seticonflag = function (path, cb) {
35 | const buf = Buffer.alloc(32)
36 | buf.writeUInt8(4, 8)
37 | util.callbackify(() => xattr.set(path, 'com.apple.FinderInfo', buf))(cb)
38 | }
39 |
40 | exports.codesign = function (identity, identifier, path, cb) {
41 | let args = ['--verbose', '--sign', identity]
42 | if (identifier) {
43 | args.push('--identifier', identifier)
44 | }
45 | args.push(path)
46 | exports.sh('codesign', args, (err) => cb(err))
47 | }
48 |
49 | exports.pathExists = function (path, cb) {
50 | util.callbackify(() => pathExists(path))(cb)
51 | }
52 |
--------------------------------------------------------------------------------
/lib/hdiutil.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const fs = require('fs')
4 | const temp = require('fs-temp')
5 | const util = require('./util')
6 |
7 | exports.convert = function (source, format, target, cb) {
8 | const args = [
9 | 'convert', source,
10 | '-ov',
11 | '-format', format,
12 | '-imagekey', 'zlib-level=9',
13 | '-o', target
14 | ]
15 |
16 | util.sh('hdiutil', args, function (err) {
17 | if (err) {
18 | fs.unlink(target, () => cb(err))
19 | } else {
20 | cb(null, target)
21 | }
22 | })
23 | }
24 |
25 | exports.create = function (volname, size, filesystem, cb) {
26 | temp.template('%s.dmg').writeFile('', function (err, outname) {
27 | if (err) return cb(err)
28 |
29 | const args = [
30 | 'create', outname,
31 | '-ov',
32 | '-fs', filesystem || 'HFS+',
33 | '-size', size,
34 | '-volname', volname
35 | ]
36 |
37 | util.sh('hdiutil', args, function (err) {
38 | if (!err) return cb(null, outname)
39 |
40 | fs.unlink(outname, () => cb(err))
41 | })
42 | })
43 | }
44 |
45 | exports.attach = function (path, cb) {
46 | const args = [
47 | 'attach', path,
48 | '-nobrowse',
49 | '-noverify',
50 | '-noautoopen'
51 | ]
52 |
53 | util.sh('hdiutil', args, function (err, res) {
54 | if (err) return cb(err)
55 |
56 | const m = /\s+(\/Volumes\/.+)$/m.exec(res.stdout)
57 | if (m === null) return cb(new Error('Failed to mount image'))
58 |
59 | cb(null, m[1])
60 | })
61 | }
62 |
63 | exports.detach = function (path, cb) {
64 | const args = ['detach', path]
65 |
66 | let attempts = 0
67 | function attemptDetach (err) {
68 | attempts += 1
69 | if (err && (err.exitCode === 16 || err.code === 16) && attempts <= 8) {
70 | setTimeout(function () {
71 | util.sh('hdiutil', args, attemptDetach)
72 | }, 1000 * Math.pow(2, attempts - 1))
73 | } else {
74 | cb(err)
75 | }
76 | }
77 |
78 | util.sh('hdiutil', args, attemptDetach)
79 | }
80 |
--------------------------------------------------------------------------------
/lib/pipeline.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const EventEmitter = require('events').EventEmitter
4 |
5 | class Pipeline extends EventEmitter {
6 | constructor () {
7 | super()
8 |
9 | this.steps = []
10 | this.totalSteps = 0
11 | this.currentStep = 0
12 |
13 | this.cleanupList = []
14 | this.cleanupStore = {}
15 | }
16 |
17 | _progress (obj) {
18 | obj.current = this.currentStep
19 | obj.total = this.totalSteps
20 |
21 | this.emit('progress', obj)
22 | }
23 |
24 | _runStep (step, nextAction, cb) {
25 | const next = (err) => {
26 | if (err) {
27 | this._progress({ type: 'step-end', status: 'error' })
28 | this.hasErrored = true
29 | this.runRemainingCleanups(function (err2) {
30 | if (err2) console.error(err2)
31 | cb(err)
32 | })
33 | } else {
34 | this._progress({ type: 'step-end', status: 'ok' })
35 | this[nextAction](cb)
36 | }
37 | }
38 |
39 | next.skip = () => {
40 | this._progress({ type: 'step-end', status: 'skip' })
41 | this[nextAction](cb)
42 | }
43 |
44 | this.currentStep++
45 | this._progress({ type: 'step-begin', title: step.title })
46 | step.fn(next)
47 | }
48 |
49 | addStep (title, fn) {
50 | this.totalSteps++
51 | this.steps.push({ title: title, fn: fn })
52 | }
53 |
54 | addCleanupStep (id, title, fn) {
55 | this.cleanupList.push(id)
56 | this.cleanupStore[id] = { title: title, fn: fn }
57 | }
58 |
59 | expectAdditional (n) {
60 | this.totalSteps += n
61 | }
62 |
63 | runCleanup (id, cb) {
64 | const fn = this.cleanupStore[id].fn
65 | const idx = this.cleanupList.indexOf(id)
66 |
67 | if (idx === -1) throw new Error(`No step with id: ${id}`)
68 |
69 | delete this.cleanupStore[id]
70 | this.cleanupList.splice(idx, 1)
71 |
72 | return fn(cb, this.hasErrored)
73 | }
74 |
75 | runRemainingCleanups (cb) {
76 | if (this.cleanupList.length === 0) return cb(null)
77 |
78 | const idx = this.cleanupList.length - 1
79 | const id = this.cleanupList[idx]
80 |
81 | const step = {
82 | title: this.cleanupStore[id].title,
83 | fn: (cb) => this.runCleanup(id, cb)
84 | }
85 |
86 | this._runStep(step, 'runRemainingCleanups', cb)
87 | }
88 |
89 | _run (cb) {
90 | if (this.steps.length === 0) return this.runRemainingCleanups(cb)
91 |
92 | const step = this.steps.shift()
93 |
94 | this._runStep(step, '_run', cb)
95 | }
96 |
97 | run () {
98 | process.nextTick(() => {
99 | this._run((err) => {
100 | if (err) {
101 | this.emit('error', err)
102 | } else {
103 | this.emit('finish')
104 | }
105 | })
106 | })
107 |
108 | return this
109 | }
110 | }
111 |
112 | module.exports = Pipeline
113 |
--------------------------------------------------------------------------------
/schema.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://json-schema.org/draft-04/schema#",
3 | "type": "object",
4 | "additionalProperties": false,
5 | "properties": {
6 | "title": {
7 | "type": "string"
8 | },
9 | "icon": {
10 | "type": "string"
11 | },
12 | "background": {
13 | "type": "string"
14 | },
15 | "background-color": {
16 | "type": "string",
17 | "format": "css-color"
18 | },
19 | "icon-size": {
20 | "type": "integer"
21 | },
22 | "window": {
23 | "type": "object",
24 | "properties": {
25 | "position": {
26 | "type": "object",
27 | "properties": {
28 | "x": {
29 | "type": "integer"
30 | },
31 | "y": {
32 | "type": "integer"
33 | }
34 | },
35 | "required": [
36 | "x",
37 | "y"
38 | ]
39 | },
40 | "size": {
41 | "type": "object",
42 | "properties": {
43 | "width": {
44 | "type": "integer"
45 | },
46 | "height": {
47 | "type": "integer"
48 | }
49 | },
50 | "required": [
51 | "width",
52 | "height"
53 | ]
54 | }
55 | }
56 | },
57 | "format": {
58 | "type": "string",
59 | "enum": [
60 | "UDRW",
61 | "UDRO",
62 | "UDCO",
63 | "UDZO",
64 | "ULFO",
65 | "ULMO",
66 | "UDBZ"
67 | ]
68 | },
69 | "filesystem": {
70 | "type": "string",
71 | "enum": [
72 | "HFS+",
73 | "APFS"
74 | ]
75 | },
76 | "contents": {
77 | "type": "array",
78 | "items": {
79 | "type": "object",
80 | "properties": {
81 | "x": {
82 | "type": "integer"
83 | },
84 | "y": {
85 | "type": "integer"
86 | },
87 | "type": {
88 | "type": "string",
89 | "enum": [
90 | "link",
91 | "file",
92 | "position"
93 | ]
94 | },
95 | "path": {
96 | "type": "string"
97 | },
98 | "name": {
99 | "type": "string"
100 | }
101 | },
102 | "required": [
103 | "x",
104 | "y",
105 | "type",
106 | "path"
107 | ]
108 | }
109 | },
110 | "code-sign": {
111 | "type": "object",
112 | "properties": {
113 | "signing-identity": {
114 | "type": "string"
115 | },
116 | "identifier": {
117 | "type": "string"
118 | }
119 | },
120 | "required": [
121 | "signing-identity"
122 | ]
123 | }
124 | },
125 | "required": [
126 | "title",
127 | "contents"
128 | ]
129 | }
130 |
--------------------------------------------------------------------------------
/bin/appdmg.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | 'use strict'
4 |
5 | process.title = 'appdmg'
6 |
7 | const path = require('path')
8 | const minimist = require('minimist')
9 | const pkg = require('../package.json')
10 | const appdmg = require('../index.js')
11 | const colors = require('../lib/colors')
12 | const repeatString = require('repeat-string')
13 |
14 | function maybeWithColor (color, text) {
15 | if (!process.stderr.isTTY) return text
16 |
17 | return colors[color](text)
18 | }
19 |
20 | process.on('uncaughtException', function (err) {
21 | if (!argv.quiet) {
22 | process.stderr.write('\n')
23 | }
24 |
25 | if (argv === undefined || argv.verbose) {
26 | process.stderr.write(`${err.stack}\n\n`)
27 | }
28 |
29 | process.stderr.write(`${maybeWithColor('red', `${err.name}: ${err.message}`)}\n`)
30 | process.exit(1)
31 | })
32 |
33 | const usage = [
34 | 'Generate beautiful dmg-images for your OS X applications.',
35 | '',
36 | 'Usage: appdmg ',
37 | '',
38 | 'json-path: Path to the JSON Specification file',
39 | 'dmg-path: Path at which to place the final dmg',
40 | '',
41 | 'Options:',
42 | '',
43 | '-v, --verbose',
44 | ' Verbose error output',
45 | '',
46 | '--quiet',
47 | ' Suppresses progress output',
48 | '',
49 | '--help',
50 | ' Display usage and exit',
51 | '',
52 | '--version',
53 | ' Display version and exit',
54 | ''
55 | ].join('\n')
56 |
57 | const argv = minimist(process.argv.slice(2), {
58 | boolean: [ 'verbose', 'quiet', 'help', 'version' ],
59 | alias: { v: 'verbose' }
60 | })
61 |
62 | if (argv.version) {
63 | process.stderr.write(`node-appdmg v${pkg.version}\n`)
64 | process.exit(0)
65 | }
66 |
67 | if (argv.help || argv._.length < 2) {
68 | process.stderr.write(`${usage}\n`)
69 | process.exit(0)
70 | }
71 |
72 | if (argv._.length > 2) {
73 | throw new Error('Too many arguments')
74 | }
75 |
76 | if (path.extname(argv._[0]) !== '.json') {
77 | throw new Error('Input must have the .json file extension')
78 | }
79 |
80 | if (path.extname(argv._[1]) !== '.dmg') {
81 | throw new Error('Output must have the .dmg file extension')
82 | }
83 |
84 | const source = argv._[0]
85 | const target = argv._[1]
86 | const p = appdmg({ source, target })
87 |
88 | p.on('progress', function (info) {
89 | if (argv.quiet) return
90 |
91 | if (info.type === 'step-begin') {
92 | const line = `[${info.current <= 9 ? ' ' : ''}${info.current}/${info.total}] ${info.title}...`
93 | process.stderr.write(`${line}${repeatString(' ', 45 - line.length)}`)
94 | }
95 |
96 | if (info.type === 'step-end') {
97 | const op = ({
98 | ok: ['green', ' OK '],
99 | skip: ['yellow', 'SKIP'],
100 | error: ['red', 'FAIL']
101 | }[info.status])
102 |
103 | process.stderr.write(`[${maybeWithColor(op[0], op[1])}]\n`)
104 | }
105 | })
106 |
107 | p.on('finish', function () {
108 | if (argv.quiet) return
109 |
110 | process.stderr.write(`\n${maybeWithColor('green', 'Your image is ready:')}\n${target}\n`)
111 | })
112 |
--------------------------------------------------------------------------------
/test/lib/visually-verify-image.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const fs = require('fs')
4 | const temp = require('fs-temp').template('%s.png')
5 | const looksSame = require('looks-same')
6 | const spawnSync = require('child_process').spawnSync
7 | const captureWindow = require('capture-window')
8 | const sizeOf = require('image-size')
9 |
10 | const hdiutil = require('../../lib/hdiutil')
11 |
12 | const toleranceOpts = { tolerance: 20 }
13 |
14 | function retry (fn, cb) {
15 | let triesLeft = 8
16 |
17 | function runIteration () {
18 | fn(function (err) {
19 | if (!err) return cb(null)
20 | if (--triesLeft === 0) return cb(err)
21 |
22 | setTimeout(runIteration, 150)
23 | })
24 | }
25 |
26 | setTimeout(runIteration, 700)
27 | }
28 |
29 | function captureAndVerify (title, expectedPath, cb) {
30 | captureWindow('Finder', title, function (err, pngPath) {
31 | if (err) return cb(err)
32 |
33 | const actualSize = sizeOf(pngPath)
34 | const expectedSize = sizeOf(expectedPath)
35 |
36 | // If the actual size is scaled by two, use the retina image.
37 | if (actualSize.width === expectedSize.width * 2 && actualSize.height === expectedSize.height * 2) {
38 | expectedPath = expectedPath.replace(/(\.[^.]*)$/, '@2x$1')
39 | }
40 |
41 | looksSame(pngPath, expectedPath, toleranceOpts, function (err1, ok) {
42 | fs.unlink(pngPath, function (err2) {
43 | if (err1) return cb(err1)
44 | if (err2) return cb(err2)
45 | if (ok) return cb(null)
46 |
47 | cb(Object.assign(new Error('Image looks visually incorrect'), { code: 'VISUALLY_INCORRECT' }))
48 | })
49 | })
50 | })
51 | }
52 |
53 | function captureAndSaveDiff (title, expectedPath, cb) {
54 | captureWindow('Finder', title, function (err, pngPath) {
55 | if (err) return cb(err)
56 |
57 | const opts = Object.assign({
58 | reference: expectedPath,
59 | current: pngPath,
60 | highlightColor: '#f0f'
61 | }, toleranceOpts)
62 |
63 | looksSame.createDiff(opts, function (err, data) {
64 | if (err) return cb(err)
65 |
66 | temp.writeFile(data, function (err, diffPath) {
67 | if (err) return cb(err)
68 |
69 | cb(null, { diff: diffPath, actual: pngPath })
70 | })
71 | })
72 | })
73 | }
74 |
75 | function visuallyVerifyImage (imagePath, title, expectedPath, cb) {
76 | hdiutil.attach(imagePath, function (err, mountPath) {
77 | if (err) return cb(err)
78 |
79 | function done (err1) {
80 | function detach (err3) {
81 | hdiutil.detach(mountPath, function (err2) {
82 | if (err1) return cb(err1)
83 | if (err2) return cb(err2)
84 | if (err3) return cb(err3)
85 |
86 | cb(null)
87 | })
88 | }
89 |
90 | if (!err1 || err1.code !== 'VISUALLY_INCORRECT') {
91 | return detach()
92 | }
93 |
94 | captureAndSaveDiff(title, expectedPath, function (err3, res) {
95 | if (err3) return detach(err3)
96 |
97 | console.error('A diff of the images have been saved to:', res.diff)
98 | console.error('The actual image have been saved to:', res.actual)
99 | detach()
100 | })
101 | }
102 |
103 | try {
104 | spawnSync('open', ['-a', 'Finder', mountPath])
105 | } catch (spawnErr) {
106 | return done(spawnErr)
107 | }
108 |
109 | retry(function (cb) {
110 | captureAndVerify(title, expectedPath, cb)
111 | }, done)
112 | })
113 | }
114 |
115 | module.exports = visuallyVerifyImage
116 |
--------------------------------------------------------------------------------
/test/api.js:
--------------------------------------------------------------------------------
1 | /* eslint-env mocha */
2 |
3 | 'use strict'
4 |
5 | const fs = require('fs')
6 | const path = require('path')
7 | const temp = require('fs-temp')
8 | const assert = require('assert')
9 |
10 | const appdmg = require('../')
11 | const imageFormat = require('./lib/image-format')
12 | const visuallyVerifyImage = require('./lib/visually-verify-image')
13 |
14 | const STEPS = 22
15 |
16 | function runAppdmg (opts, verify, cb) {
17 | let progressCalled = 0
18 | const ee = appdmg(opts)
19 |
20 | ee.on('progress', function () {
21 | progressCalled++
22 | })
23 |
24 | ee.on('finish', function () {
25 | try {
26 | assert.strictEqual(progressCalled, STEPS * 2)
27 | assert.strictEqual(imageFormat(opts.target), verify.format)
28 | } catch (err) {
29 | return cb(err)
30 | }
31 |
32 | const expected = path.join(__dirname, verify.visually)
33 | visuallyVerifyImage(opts.target, verify.title, expected, cb)
34 | })
35 | }
36 |
37 | describe('api', function () {
38 | let targetDir, targetPath
39 |
40 | beforeEach(function () {
41 | targetDir = temp.mkdirSync()
42 | targetPath = path.join(targetDir, 'Test.dmg')
43 | })
44 |
45 | afterEach(function () {
46 | fs.unlinkSync(targetPath)
47 | fs.rmdirSync(path.dirname(targetPath))
48 | })
49 |
50 | it('creates an image from a modern specification', function (done) {
51 | this.timeout(60000) // 1 minute
52 |
53 | const opts = {
54 | target: targetPath,
55 | source: path.join(__dirname, 'assets', 'appdmg.json')
56 | }
57 |
58 | const verify = {
59 | format: 'UDZO',
60 | title: 'Test Title',
61 | visually: 'accepted-1.png'
62 | }
63 |
64 | runAppdmg(opts, verify, done)
65 | })
66 |
67 | it('creates an image from a legacy specification', function (done) {
68 | this.timeout(60000) // 1 minute
69 |
70 | const opts = {
71 | target: targetPath,
72 | source: path.join(__dirname, 'assets', 'appdmg-legacy.json')
73 | }
74 |
75 | const verify = {
76 | format: 'UDZO',
77 | title: 'Test Title',
78 | visually: 'accepted-1.png'
79 | }
80 |
81 | runAppdmg(opts, verify, done)
82 | })
83 |
84 | it('creates an image from a passed options', function (done) {
85 | this.timeout(60000) // 1 minute
86 |
87 | const opts = {
88 | target: targetPath,
89 | basepath: path.join(__dirname, 'assets'),
90 | specification: {
91 | title: 'Test Title',
92 | icon: 'TestIcon.icns',
93 | background: 'TestBkg.png',
94 | contents: [
95 | { x: 448, y: 344, type: 'link', path: '/Applications' },
96 | { x: 192, y: 344, type: 'file', path: 'TestApp.app' },
97 | { x: 512, y: 128, type: 'file', path: 'TestDoc.txt' }
98 | ]
99 | }
100 | }
101 |
102 | const verify = {
103 | format: 'UDZO',
104 | title: 'Test Title',
105 | visually: 'accepted-1.png'
106 | }
107 |
108 | runAppdmg(opts, verify, done)
109 | })
110 |
111 | it('creates an image without compression', function (done) {
112 | this.timeout(60000) // 1 minute
113 |
114 | const opts = {
115 | target: targetPath,
116 | basepath: path.join(__dirname, 'assets'),
117 | specification: {
118 | title: 'Test Title',
119 | icon: 'TestIcon.icns',
120 | background: 'TestBkg.png',
121 | format: 'UDRO',
122 | contents: [
123 | { x: 448, y: 344, type: 'link', path: '/Applications' },
124 | { x: 192, y: 344, type: 'file', path: 'TestApp.app' },
125 | { x: 512, y: 128, type: 'file', path: 'TestDoc.txt' }
126 | ]
127 | }
128 | }
129 |
130 | const verify = {
131 | format: 'UDRO',
132 | title: 'Test Title',
133 | visually: 'accepted-1.png'
134 | }
135 |
136 | runAppdmg(opts, verify, done)
137 | })
138 |
139 | it('creates an image with a background color', function (done) {
140 | this.timeout(60000) // 1 minute
141 |
142 | const opts = {
143 | target: targetPath,
144 | source: path.join(__dirname, 'assets', 'appdmg-bg-color.json')
145 | }
146 |
147 | const verify = {
148 | format: 'UDZO',
149 | title: 'Test Title',
150 | visually: 'accepted-2.png'
151 | }
152 |
153 | runAppdmg(opts, verify, done)
154 | })
155 |
156 | it('creates an image with custom names', function (done) {
157 | this.timeout(60000) // 1 minute
158 |
159 | const opts = {
160 | target: targetPath,
161 | basepath: path.join(__dirname, 'assets'),
162 | specification: {
163 | title: 'Test Title',
164 | icon: 'TestIcon.icns',
165 | background: 'TestBkg.png',
166 | contents: [
167 | { x: 448, y: 344, type: 'link', path: '/Applications', name: 'System Apps' },
168 | { x: 192, y: 344, type: 'file', path: 'TestApp.app', name: 'My Nice App.app' },
169 | { x: 512, y: 128, type: 'file', path: 'TestDoc.txt', name: 'Documentation.txt' }
170 | ]
171 | }
172 | }
173 |
174 | const verify = {
175 | format: 'UDZO',
176 | title: 'Test Title',
177 | visually: 'accepted-3.png'
178 | }
179 |
180 | runAppdmg(opts, verify, done)
181 | })
182 | })
183 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # node-appdmg
2 |
3 | Generate beautiful DMG-images for your OS X applications.
4 |
5 | ## Installation
6 |
7 | ```sh
8 | npm install -g appdmg
9 | ```
10 |
11 | ## Usage
12 |
13 | ```sh
14 | appdmg
15 | ```
16 |
17 | - `json-path`: Path to the JSON Specification file
18 | - `dmg-path`: Path at which to place the final DMG
19 |
20 | ## Test
21 |
22 | To produce a test DMG to your desktop, run the following command:
23 |
24 | ```sh
25 | appdmg test/assets/appdmg.json ~/Desktop/test.dmg
26 | ```
27 |
28 | ## JSON Input
29 |
30 | 
31 |
32 | The JSON input for the image follows a simple structure. All paths are relative to
33 | the JSON file's path.
34 |
35 | ### Example
36 |
37 | ```json
38 | {
39 | "title": "Test Application",
40 | "icon": "test-app.icns",
41 | "background": "test-background.png",
42 | "contents": [
43 | { "x": 448, "y": 344, "type": "link", "path": "/Applications" },
44 | { "x": 192, "y": 344, "type": "file", "path": "TestApp.app" }
45 | ]
46 | }
47 | ```
48 |
49 | ### Specification
50 |
51 | - `title` (string, required) - The title of the produced DMG, which will be shown when mounted
52 | - `icon` (string, optional) - Path to your icon, which will be shown when mounted
53 | - `background` (string, optional) - Path to your background
54 | - `background-color` (string, optional) - Background color (accepts css colors)
55 | - `icon-size` (number, optional) - Size of all the icons inside the DMG
56 | - `window` (object, optional) - Window options
57 | - `position` (object, optional) - Position when opened
58 | - `x` (number, required) - X position relative to left of the screen
59 | - `y` (number, required) - Y position relative to bottom of the screen
60 | - `size` (object, optional) - Window size
61 | - `width` (number, required) - Window width
62 | - `height` (number, required) - Window height
63 | - `format` (enum[string], optional) - Disk image format
64 | - `UDRW` - UDIF read/write image
65 | - `UDRO` - UDIF read-only image
66 | - `UDCO` - UDIF ADC-compressed image
67 | - `UDZO` - UDIF zlib-compressed image
68 | - `UDBZ` - UDIF bzip2-compressed image (OS X 10.4+ only)
69 | - `ULFO` - UDIF lzfse-compressed image (OS X 10.11+ only)
70 | - `ULMO` - UDIF lzma-compressed image (macOS 10.15+ only)
71 | - `filesystem` (enum[string], optional) - Disk image filesystem
72 | - `HFS+`
73 | - `APFS` (macOS 10.13+ only)
74 | - `contents` (array[object], required) - This is the contents of your DMG.
75 | - `x` (number, required) - X position relative to icon center
76 | - `y` (number, required) - Y position relative to icon center
77 | - `type` (enum[string], required)
78 | - `link` - Creates a link to the specified target
79 | - `file` - Adds a file to the DMG
80 | - `position` - Positions a present file
81 | - `path` (string, required) - Path to the file
82 | - `name` (string, optional) - Name of the file within the DMG
83 | - `code-sign` (object, optional) - Options for codesigning the DMG
84 | - `signing-identity` (string, required) - The identity with which to sign the resulting DMG
85 | - `identifier` (string, optional) - Explicitly set the unique identifier string that is embedded in code signatures
86 |
87 | `0.1.x` used a different JSON format. This format is still supported but
88 | deprecated, please update your json.
89 |
90 | ### Retina background
91 |
92 | Finder can display retina backgrounds if packaged correctly into a `.tiff`
93 | file. `appdmg` will do this for you automatically if it can find a file
94 | with the same name as the background appended with `@2x`.
95 |
96 | E.g. if the json contains `"background": "TestBkg.png"` then add a file
97 | with the name `TestBkg@2x.png` into the same folder.
98 |
99 | ## API
100 |
101 | The application can also be called from within
102 | another javascript file, example:
103 |
104 | ```javascript
105 |
106 | const appdmg = require('appdmg');
107 | const ee = appdmg({ source: 'test/appdmg.json', target: 'test.dmg' });
108 |
109 | ee.on('progress', function (info) {
110 |
111 | // info.current is the current step
112 | // info.total is the total number of steps
113 | // info.type is on of 'step-begin', 'step-end'
114 |
115 | // 'step-begin'
116 | // info.title is the title of the current step
117 |
118 | // 'step-end'
119 | // info.status is one of 'ok', 'skip', 'fail'
120 |
121 | });
122 |
123 | ee.on('finish', function () {
124 | // There now is a `test.dmg` file
125 | });
126 |
127 | ee.on('error', function (err) {
128 | // An error occurred
129 | });
130 |
131 | ```
132 |
133 | You can also pass in the specification directly instead of reading it from a file. `basepath` should be a path which will be used to resolve other paths in the specification.
134 |
135 | ```javascript
136 | const ee = appdmg({
137 | target: 'test.dmg',
138 | basepath: __dirname,
139 | specification: {
140 | "title": "Test Title",
141 | // ...
142 | }
143 | });
144 | ```
145 |
146 | ## OS Support
147 |
148 | Currently the only supported os is Mac OS X.
149 |
150 | Track the status of this here: https://github.com/LinusU/node-appdmg/issues/14
151 |
152 | ## Hidden files
153 |
154 | By default hidden files will show for users with `com.apple.finder AppleShowAllFiles`
155 | set to `TRUE`. This can be worked around by moving all hidden files outside the initial
156 | window size (using `"type": "position"`), this has the side-effect of enabling a scrollbar.
157 |
158 | Files to usually move:
159 |
160 | - `.background`
161 | - `.DS_Store`
162 | - `.Trashes`
163 | - `.VolumeIcon.icns`
164 |
165 | ## Alternatives
166 |
167 | - [create-dmg](https://github.com/andreyvit/create-dmg/blob/master/README.md), a Bash script
168 | - [dmgbuild](https://pypi.python.org/pypi/dmgbuild), a Python version
169 |
--------------------------------------------------------------------------------
/lib/appdmg.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const fs = require('fs')
4 | const os = require('os')
5 | const path = require('path')
6 |
7 | const async = require('async')
8 | const DSStore = require('ds-store')
9 | const sizeOf = require('image-size')
10 | const validator = require('is-my-json-valid')
11 | const parseColor = require('parse-color')
12 |
13 | const util = require('./util')
14 | const hdiutil = require('./hdiutil')
15 | const Pipeline = require('./pipeline')
16 | const schema = require('../schema')
17 |
18 | const validateSpec = validator(schema, {
19 | formats: {
20 | 'css-color': (text) => Boolean(parseColor(text).rgb)
21 | }
22 | })
23 |
24 | function hasKeys (obj, props) {
25 | function hasKey (key) { return obj.hasOwnProperty(key) }
26 |
27 | return (props.filter(hasKey).length === props.length)
28 | }
29 |
30 | function parseOptions (options) {
31 | if (typeof options !== 'object') {
32 | throw new Error('`options` must be an object')
33 | }
34 |
35 | if (hasKeys(options, ['target']) === false) {
36 | throw new Error('Missing option `target`')
37 | }
38 |
39 | const parsed = {}
40 | const hasSource = hasKeys(options, ['source'])
41 | const hasSpec = hasKeys(options, ['basepath', 'specification'])
42 |
43 | if (hasSource === hasSpec) {
44 | throw new Error('Supply one of `source` or `(basepath, specification)`')
45 | }
46 |
47 | if (hasSource) {
48 | parsed.hasSpec = false
49 | parsed.source = options.source
50 | parsed.target = options.target
51 | parsed.resolveBase = path.dirname(options.source)
52 | }
53 |
54 | if (hasSpec) {
55 | parsed.hasSpec = true
56 | parsed.target = options.target
57 | parsed.opts = options.specification
58 | parsed.resolveBase = options.basepath
59 | }
60 |
61 | return parsed
62 | }
63 |
64 | module.exports = exports = function (options) {
65 | if (process.platform !== 'darwin') {
66 | throw new Error(`Platform not supported: ${process.platform}`)
67 | }
68 |
69 | const global = parseOptions(options)
70 | const resolvePath = (to) => path.resolve(global.resolveBase, to)
71 |
72 | const pipeline = new Pipeline()
73 |
74 | /**
75 | **
76 | **/
77 |
78 | pipeline.addStep('Looking for target', function (next) {
79 | fs.writeFile(global.target, '', { flag: 'wx' }, function (err) {
80 | if (err && err.code === 'EEXIST') return next(new Error('Target already exists'))
81 | if (err) return next(err)
82 |
83 | pipeline.addCleanupStep('unlink-target', 'Removing target image', function (next, hasErrored) {
84 | if (hasErrored) {
85 | fs.unlink(global.target, next)
86 | } else {
87 | next(null)
88 | }
89 | })
90 | next(null)
91 | })
92 | })
93 |
94 | /**
95 | **
96 | **/
97 |
98 | pipeline.addStep('Reading JSON Specification', function (next) {
99 | if (global.hasSpec) return next.skip()
100 |
101 | fs.readFile(global.source, function (err, buffer) {
102 | if (err && err.code === 'ENOENT' && err.path) {
103 | next(new Error(`JSON Specification not found at: ${err.path}`))
104 | } else {
105 | global.specbuffer = buffer
106 | next(err)
107 | }
108 | })
109 | })
110 |
111 | /**
112 | **
113 | **/
114 |
115 | pipeline.addStep('Parsing JSON Specification', function (next) {
116 | if (global.hasSpec) return next.skip()
117 |
118 | try {
119 | const obj = JSON.parse(global.specbuffer.toString())
120 |
121 | if (obj.icons) {
122 | const legacy = require('./legacy')
123 | global.opts = legacy.convert(obj)
124 | } else {
125 | global.opts = obj
126 | }
127 |
128 | next(null)
129 | } catch (err) {
130 | next(err)
131 | }
132 | })
133 |
134 | /**
135 | **
136 | **/
137 |
138 | pipeline.addStep('Validating JSON Specification', function (next) {
139 | if (validateSpec(global.opts)) return next(null)
140 |
141 | function formatError (error) {
142 | return `${error.field} ${error.message}`
143 | }
144 |
145 | const message = validateSpec.errors.map(formatError).join(', ')
146 |
147 | next(new Error(message))
148 | })
149 |
150 | /**
151 | **
152 | **/
153 |
154 | pipeline.addStep('Looking for files', function (next) {
155 | function find (type) {
156 | return global.opts.contents.filter(function (e) {
157 | return (e.type === type)
158 | })
159 | }
160 |
161 | global.links = find('link')
162 | global.files = find('file')
163 |
164 | async.each(global.files, function (file, cb) {
165 | const path = resolvePath(file.path)
166 |
167 | util.pathExists(path, function (err, exists) {
168 | if (err) {
169 | cb(err)
170 | } else if (exists) {
171 | cb(null)
172 | } else {
173 | cb(new Error(`"${file.path}" not found at: ${path}`))
174 | }
175 | })
176 | }, next)
177 | })
178 |
179 | /**
180 | **
181 | **/
182 |
183 | pipeline.addStep('Calculating size of image', function (next) {
184 | const dusm = util.dusm.bind(util)
185 | const paths = global.files.map((e) => resolvePath(e.path))
186 |
187 | async.map(paths, dusm, function (err, sizes) {
188 | if (err) return next(err)
189 |
190 | let megabytes = sizes.reduce((p, c) => p + c, 0)
191 |
192 | // FIXME: I think that this has something to do
193 | // with blocksize and minimum file size...
194 | // This should work for now but requires more
195 | // space than it should. Note that this does
196 | // not effect the final image.
197 | megabytes = megabytes * 1.5
198 |
199 | global.megabytes = (megabytes + 32)
200 | next(null)
201 | })
202 | })
203 |
204 | /**
205 | **
206 | **/
207 |
208 | pipeline.addStep('Creating temporary image', function (next) {
209 | hdiutil.create(global.opts.title, `${global.megabytes}m`, global.opts.filesystem, function (err, temporaryImagePath) {
210 | if (err) return next(err)
211 |
212 | pipeline.addCleanupStep('unlink-temporary-image', 'Removing temporary image', function (next) {
213 | fs.unlink(temporaryImagePath, next)
214 | })
215 |
216 | global.temporaryImagePath = temporaryImagePath
217 | next(null)
218 | })
219 | })
220 |
221 | /**
222 | **
223 | **/
224 |
225 | pipeline.addStep('Mounting temporary image', function (next) {
226 | hdiutil.attach(global.temporaryImagePath, function (err, temporaryMountPath) {
227 | if (err) return next(err)
228 |
229 | pipeline.addCleanupStep('unmount-temporary-image', 'Unmounting temporary image', function (next) {
230 | hdiutil.detach(temporaryMountPath, next)
231 | })
232 |
233 | global.temporaryMountPath = temporaryMountPath
234 | next(null)
235 | })
236 | })
237 |
238 | /**
239 | **
240 | **/
241 |
242 | pipeline.addStep('Making hidden background folder', function (next) {
243 | global.bkgdir = path.join(global.temporaryMountPath, '.background')
244 | fs.mkdir(global.bkgdir, next)
245 | })
246 |
247 | /**
248 | **
249 | **/
250 |
251 | pipeline.addStep('Copying background', function (next) {
252 | if (!global.opts.background) return next.skip()
253 |
254 | const absolutePath = resolvePath(global.opts.background)
255 | const retinaPath = absolutePath.replace(/\.([a-z]+)$/, '@2x.$1')
256 |
257 | function copyRetinaBackground (next) {
258 | const originalExt = path.extname(global.opts.background)
259 | const outputName = `${path.basename(global.opts.background, originalExt)}.tiff`
260 | const finalPath = path.join(global.bkgdir, outputName)
261 | global.bkgname = path.join('.background', outputName)
262 | util.tiffutil(absolutePath, retinaPath, finalPath, next)
263 | }
264 |
265 | function copyPlainBackground (next) {
266 | const finalPath = path.join(global.bkgdir, path.basename(global.opts.background))
267 | global.bkgname = path.join('.background', path.basename(global.opts.background))
268 | fs.copyFile(absolutePath, finalPath, next)
269 | }
270 |
271 | util.pathExists(retinaPath, function (err, exists) {
272 | if (err) {
273 | return next(err)
274 | } else if (exists) {
275 | copyRetinaBackground(next)
276 | } else {
277 | copyPlainBackground(next)
278 | }
279 | })
280 | })
281 |
282 | /**
283 | **
284 | **/
285 |
286 | pipeline.addStep('Reading background dimensions', function (next) {
287 | if (!global.opts.background) return next.skip()
288 |
289 | sizeOf(resolvePath(global.opts.background), function (err, value) {
290 | if (err) return next(err)
291 |
292 | global.bkgsize = [value.width, value.height]
293 | next(null)
294 | })
295 | })
296 |
297 | /**
298 | **
299 | **/
300 |
301 | pipeline.addStep('Copying icon', function (next) {
302 | if (global.opts.icon) {
303 | const finalPath = path.join(global.temporaryMountPath, '.VolumeIcon.icns')
304 | fs.copyFile(resolvePath(global.opts.icon), finalPath, next)
305 | } else {
306 | next.skip()
307 | }
308 | })
309 |
310 | /**
311 | **
312 | **/
313 |
314 | pipeline.addStep('Setting icon', function (next) {
315 | if (global.opts.icon) {
316 | util.seticonflag(global.temporaryMountPath, next)
317 | } else {
318 | next.skip()
319 | }
320 | })
321 |
322 | /**
323 | **
324 | **/
325 |
326 | pipeline.addStep('Creating links', function (next) {
327 | if (global.links.length === 0) {
328 | return next.skip()
329 | }
330 |
331 | async.each(global.links, function (entry, cb) {
332 | const name = entry.name || path.basename(entry.path)
333 | const finalPath = path.join(global.temporaryMountPath, name)
334 |
335 | fs.symlink(entry.path, finalPath, cb)
336 | }, next)
337 | })
338 |
339 | /**
340 | **
341 | **/
342 |
343 | pipeline.addStep('Copying files', function (next) {
344 | if (global.files.length === 0) {
345 | return next.skip()
346 | }
347 |
348 | async.each(global.files, function (entry, cb) {
349 | const name = entry.name || path.basename(entry.path)
350 | const finalPath = path.join(global.temporaryMountPath, name)
351 |
352 | util.sh('cp', ['-R', resolvePath(entry.path), finalPath], cb)
353 | }, next)
354 | })
355 |
356 | /**
357 | **
358 | **/
359 |
360 | pipeline.addStep('Making all the visuals', function (next) {
361 | const ds = new DSStore()
362 |
363 | ds.vSrn(1)
364 | ds.setIconSize(global.opts['icon-size'] || 80)
365 |
366 | if (global.opts['background-color']) {
367 | const rgb = parseColor(global.opts['background-color']).rgb
368 | ds.setBackgroundColor(rgb[0] / 255, rgb[1] / 255, rgb[2] / 255)
369 | }
370 |
371 | if (global.opts.background) {
372 | ds.setBackgroundPath(path.join(global.temporaryMountPath, global.bkgname))
373 | }
374 |
375 | if (global.opts.window && global.opts.window.size) {
376 | ds.setWindowSize(global.opts.window.size.width, global.opts.window.size.height)
377 | } else if (global.bkgsize) {
378 | ds.setWindowSize(global.bkgsize[0], global.bkgsize[1])
379 | } else {
380 | ds.setWindowSize(640, 480)
381 | }
382 |
383 | if (global.opts.window && global.opts.window.position) {
384 | ds.setWindowPos(global.opts.window.position.x, global.opts.window.position.y)
385 | }
386 |
387 | for (const e of global.opts.contents) {
388 | ds.setIconPos(e.name || path.basename(e.path), e.x, e.y)
389 | }
390 |
391 | ds.write(path.join(global.temporaryMountPath, '.DS_Store'), (err) => next(err))
392 | })
393 |
394 | /**
395 | **
396 | **/
397 |
398 | pipeline.addStep('Blessing image', function (next) {
399 | // Blessing does not work for APFS disk images
400 | if (global.opts.filesystem !== 'APFS') {
401 | const args = [
402 | '--folder', global.temporaryMountPath
403 | ]
404 |
405 | if (os.arch() !== 'arm64') {
406 | args.push('--openfolder', global.temporaryMountPath)
407 | }
408 |
409 | util.sh('bless', args, next)
410 | } else {
411 | next.skip()
412 | }
413 | })
414 |
415 | /**
416 | **
417 | **/
418 |
419 | pipeline.addStep('Unmounting temporary image', function (next) {
420 | pipeline.runCleanup('unmount-temporary-image', next)
421 | })
422 |
423 | /**
424 | **
425 | **/
426 |
427 | pipeline.addStep('Finalizing image', function (next) {
428 | const format = (global.opts.format || 'UDZO')
429 |
430 | hdiutil.convert(global.temporaryImagePath, format, global.target, next)
431 | })
432 |
433 | /**
434 | **
435 | **/
436 |
437 | pipeline.addStep('Signing image', function (next) {
438 | const codeSignOptions = global.opts['code-sign']
439 | if (codeSignOptions && codeSignOptions['signing-identity']) {
440 | const codeSignIdentity = codeSignOptions['signing-identity']
441 | const codeSignIdentifier = codeSignOptions['identifier']
442 | util.codesign(codeSignIdentity, codeSignIdentifier, global.target, next)
443 | } else {
444 | return next.skip()
445 | }
446 | })
447 |
448 | /**
449 | **
450 | **/
451 |
452 | pipeline.expectAdditional(1)
453 |
454 | return pipeline.run()
455 | }
456 |
--------------------------------------------------------------------------------