├── 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 | ![Visualization](/help/help.png?raw=true) 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 | --------------------------------------------------------------------------------