2017-03-27 14:09:27 +00:00
#!/usr/bin/env node
'use strict' ;
const path = require ( 'path' ) ;
const fs = require ( 'fs' ) ;
const meow = require ( 'meow' ) ;
const appdmg = require ( 'appdmg' ) ;
const plist = require ( 'plist' ) ;
const Ora = require ( 'ora' ) ;
const execa = require ( 'execa' ) ;
2020-02-19 11:20:58 +00:00
const addLicenseAgreementIfNeeded = require ( './sla.js' ) ;
2019-05-12 16:40:16 +00:00
const composeIcon = require ( './compose-icon' ) ;
2017-03-27 14:09:27 +00:00
2018-04-29 12:07:51 +00:00
if ( process . platform !== 'darwin' ) {
console . error ( 'macOS only' ) ;
process . exit ( 1 ) ;
}
2017-03-27 14:09:27 +00:00
const cli = meow ( `
2018-04-29 12:02:41 +00:00
Usage
2018-04-29 11:57:59 +00:00
$ create - dmg < app > [ destination ]
2018-04-29 12:02:41 +00:00
2018-04-29 13:58:01 +00:00
Options
2019-12-06 09:27:35 +00:00
-- overwrite Overwrite existing DMG with the same name
-- identity = < value > Manually set code signing identity ( automatic by default )
-- dmg - title = < value > Manually set title of DMG volume ( only used if app name is > 27 character limit )
2018-04-29 13:58:01 +00:00
2018-04-29 12:02:41 +00:00
Examples
2017-03-27 14:09:27 +00:00
$ create - dmg 'Lungo.app'
2018-04-29 12:02:41 +00:00
$ create - dmg 'Lungo.app' Build / Releases
2018-04-29 13:58:01 +00:00
` , {
flags : {
overwrite : {
type : 'boolean'
2019-07-11 11:21:16 +00:00
} ,
identity : {
type : 'string'
2019-12-06 09:27:35 +00:00
} ,
dmgTitle : {
type : 'string'
2018-04-29 13:58:01 +00:00
}
}
} ) ;
2017-03-27 14:09:27 +00:00
2019-12-06 09:30:30 +00:00
let [ appPath , destinationPath ] = cli . input ;
2017-03-27 14:09:27 +00:00
2018-04-29 12:07:51 +00:00
if ( ! appPath ) {
2017-03-27 14:09:27 +00:00
console . error ( 'Specify an app' ) ;
process . exit ( 1 ) ;
}
2019-12-06 09:30:30 +00:00
if ( ! destinationPath ) {
destinationPath = process . cwd ( ) ;
2018-04-29 11:57:59 +00:00
}
2019-12-06 09:20:17 +00:00
const infoPlistPath = path . join ( appPath , 'Contents/Info.plist' ) ;
2017-03-31 02:04:34 +00:00
let infoPlist ;
try {
2019-12-06 09:20:17 +00:00
infoPlist = fs . readFileSync ( infoPlistPath , 'utf8' ) ;
2018-10-17 07:59:58 +00:00
} catch ( error ) {
if ( error . code === 'ENOENT' ) {
2018-04-29 12:07:51 +00:00
console . error ( ` Could not find \` ${ path . relative ( process . cwd ( ) , appPath ) } \` ` ) ;
2017-03-31 02:04:34 +00:00
process . exit ( 1 ) ;
}
2018-10-17 07:59:58 +00:00
throw error ;
2017-03-31 02:04:34 +00:00
}
2017-03-27 14:09:27 +00:00
const ora = new Ora ( 'Creating DMG' ) ;
ora . start ( ) ;
2019-05-12 16:40:16 +00:00
async function init ( ) {
2019-12-06 09:20:17 +00:00
let appInfo ;
try {
appInfo = plist . parse ( infoPlist ) ;
} catch ( _ ) {
2020-02-19 11:20:58 +00:00
const { stdout } = await execa ( '/usr/bin/plutil' , [ '-convert' , 'xml1' , '-o' , '-' , infoPlistPath ] ) ;
2019-12-06 09:20:17 +00:00
appInfo = plist . parse ( stdout ) ;
}
const appName = appInfo . CFBundleDisplayName || appInfo . CFBundleName ;
2020-02-19 11:20:58 +00:00
const dmgTitle = appName . length > 27 ? ( cli . flags . dmgTitle || appName ) : appName ;
2019-12-06 09:30:30 +00:00
const dmgPath = path . join ( destinationPath , ` ${ appName } ${ appInfo . CFBundleShortVersionString } .dmg ` ) ;
2019-12-06 09:20:17 +00:00
2019-05-12 16:40:16 +00:00
if ( cli . flags . overwrite ) {
try {
fs . unlinkSync ( dmgPath ) ;
} catch ( _ ) { }
2017-03-27 14:09:27 +00:00
}
2020-04-20 12:32:38 +00:00
const hasAppIcon = appInfo . CFBundleIconFile ;
let composedIconPath ;
if ( hasAppIcon ) {
ora . text = 'Creating icon' ;
const appIconName = appInfo . CFBundleIconFile . replace ( /\.icns/ , '' ) ;
composedIconPath = await composeIcon ( path . join ( appPath , 'Contents/Resources' , ` ${ appIconName } .icns ` ) ) ;
}
2019-05-12 16:40:16 +00:00
2020-02-19 11:20:58 +00:00
const minSystemVersion = ( Object . prototype . hasOwnProperty . call ( appInfo , 'LSMinimumSystemVersion' ) && appInfo . LSMinimumSystemVersion . length > 0 ) ? appInfo . LSMinimumSystemVersion . toString ( ) : '10.11' ;
const minorVersion = Number ( minSystemVersion . split ( '.' ) [ 1 ] ) || 0 ;
const dmgFormat = ( minorVersion >= 11 ) ? 'ULFO' : 'UDZO' ; // ULFO requires 10.11+
ora . info ( ` Minimum runtime ${ minSystemVersion } detected, using ${ dmgFormat } format ` ) . start ( ) ;
2019-05-12 16:40:16 +00:00
const ee = appdmg ( {
target : dmgPath ,
basepath : process . cwd ( ) ,
specification : {
2019-12-06 09:27:35 +00:00
title : dmgTitle ,
2019-05-12 16:40:16 +00:00
icon : composedIconPath ,
//
// Use transparent background and `background-color` option when this is fixed:
// https://github.com/LinusU/node-appdmg/issues/135
background : path . join ( _ _dirname , 'assets/dmg-background.png' ) ,
'icon-size' : 160 ,
2020-02-19 11:20:58 +00:00
format : dmgFormat ,
2019-05-12 16:40:16 +00:00
window : {
size : {
width : 660 ,
height : 400
}
} ,
contents : [
{
x : 180 ,
y : 170 ,
type : 'file' ,
path : appPath
} ,
{
x : 480 ,
y : 170 ,
type : 'link' ,
path : '/Applications'
}
]
}
} ) ;
2017-03-27 14:09:27 +00:00
2019-05-12 16:40:16 +00:00
ee . on ( 'progress' , info => {
if ( info . type === 'step-begin' ) {
ora . text = info . title ;
2018-05-01 14:37:08 +00:00
}
2019-05-12 16:40:16 +00:00
} ) ;
ee . on ( 'finish' , async ( ) => {
try {
2020-02-19 11:20:58 +00:00
ora . text = 'Adding Software License Agreement if needed' ;
await addLicenseAgreementIfNeeded ( dmgPath , dmgFormat ) ;
2020-04-20 12:32:38 +00:00
if ( hasAppIcon ) {
ora . text = 'Replacing DMG icon' ;
// `seticon`` is a native tool to change files icons (Source: https://github.com/sveinbjornt/osxiconutils)
await execa ( path . join ( _ _dirname , 'seticon' ) , [ composedIconPath , dmgPath ] ) ;
}
2019-05-12 16:40:16 +00:00
ora . text = 'Code signing DMG' ;
let identity ;
2020-02-19 11:20:58 +00:00
const { stdout } = await execa ( '/usr/bin/security' , [ 'find-identity' , '-v' , '-p' , 'codesigning' ] ) ;
2019-07-11 11:21:16 +00:00
if ( cli . flags . identity && stdout . includes ( ` " ${ cli . flags . identity } " ` ) ) {
identity = cli . flags . identity ;
} else if ( ! cli . flags . identity && stdout . includes ( 'Developer ID Application:' ) ) {
2019-05-12 16:40:16 +00:00
identity = 'Developer ID Application' ;
2019-07-11 11:21:16 +00:00
} else if ( ! cli . flags . identity && stdout . includes ( 'Mac Developer:' ) ) {
2019-05-12 16:40:16 +00:00
identity = 'Mac Developer' ;
2019-07-11 11:21:16 +00:00
}
if ( ! identity ) {
2019-05-12 16:44:54 +00:00
const error = new Error ( ) ;
2019-07-11 11:21:16 +00:00
error . stderr = 'No suitable code signing identity found' ;
2019-05-12 16:44:54 +00:00
throw error ;
2019-05-12 16:40:16 +00:00
}
2018-05-01 14:37:08 +00:00
2020-02-19 11:20:58 +00:00
await execa ( '/usr/bin/codesign' , [ '--sign' , identity , dmgPath ] ) ;
const { stderr } = await execa ( '/usr/bin/codesign' , [ dmgPath , '--display' , '--verbose=2' ] ) ;
2019-05-12 16:40:16 +00:00
const match = /^Authority=(.*)$/m . exec ( stderr ) ;
if ( ! match ) {
ora . fail ( 'Not code signed' ) ;
process . exit ( 1 ) ;
}
2017-03-27 14:09:27 +00:00
2019-05-12 16:40:16 +00:00
ora . info ( ` Code signing identity: ${ match [ 1 ] } ` ) . start ( ) ;
ora . succeed ( 'DMG created' ) ;
} catch ( error ) {
2020-02-19 11:20:58 +00:00
ora . fail ( ` Code signing failed. The DMG is fine, just not code signed. \n ${ Object . prototype . hasOwnProperty . call ( error , 'stderr' ) ? error . stderr . trim ( ) : error } ` ) ;
2019-05-12 16:40:16 +00:00
process . exit ( 2 ) ;
2017-03-27 14:09:27 +00:00
}
2019-05-12 16:40:16 +00:00
} ) ;
2017-03-27 14:09:27 +00:00
2019-05-12 16:40:16 +00:00
ee . on ( 'error' , error => {
2019-06-12 18:16:30 +00:00
ora . fail ( ` Building the DMG failed. ${ error } ` ) ;
2019-05-12 16:40:16 +00:00
process . exit ( 1 ) ;
} ) ;
}
2017-03-27 14:09:27 +00:00
2019-05-12 16:40:16 +00:00
init ( ) . catch ( error => {
2020-04-20 12:26:06 +00:00
ora . fail ( ( error && error . stack ) || error ) ;
2017-03-27 14:09:27 +00:00
process . exit ( 1 ) ;
} ) ;