2018-09-25 22:55:14 +03:00

407 lines
13 KiB
Python

# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import subprocess
import os.path
import biplist
import shutil
import stat
import re
import pkg_resources
import tokenize
import six
import sys
from mac_alias import *
from ds_store import *
from . import colors, badge
_hexcolor_re = re.compile(r'#[0-9a-f]{3}(?:[0-9a-f]{3})?')
class DMGError(Exception):
pass
def hdiutil(cmd, *args, **kwargs):
plist = kwargs.get('plist', True)
all_args = ['/usr/bin/hdiutil', cmd]
all_args.extend(args)
if plist:
all_args.append('-plist')
p = subprocess.Popen(all_args, stdout=subprocess.PIPE, close_fds=True)
output, errors = p.communicate()
if plist:
results = biplist.readPlistFromString(output)
else:
results = output
retcode = p.wait()
return retcode, results
# On Python 2 we can just execfile() it, but Python 3 deprecated that
def load_settings(filename, globs, locs):
if sys.version_info[0] == 2:
execfile(filename, globs, locs)
else:
encoding = 'utf-8'
with open(filename, 'rb') as fp:
try:
encoding = tokenize.detect_encoding(fp.readline)[0]
except SyntaxError:
pass
with open(filename, 'r', encoding=encoding) as fp:
exec(compile(fp.read(), filename, 'exec'), globs, locs)
def build_dmg(filename, volume_name, settings_file=None, defines={}):
settings = {
# Actual settings
'filename': filename,
'volume_name': volume_name,
'format': 'UDBZ',
'size': '100M',
'files': [],
'symlinks': {},
'icon': None,
'badge_icon': None,
'background': 'builtin-arrow',
'show_status_bar': False,
'show_tab_view': False,
'show_toolbar': False,
'show_pathbar': False,
'show_sidebar': False,
'sidebar_width': 180,
'arrange_by': None,
'grid_offset': (0, 0),
'grid_spacing': 120.0,
'scroll_position': (0, 0),
'show_icon_preview': False,
'show_item_info': False,
'label_pos': 'bottom',
'text_size': 16.0,
'icon_size': 128.0,
'include_icon_view_settings': 'auto',
'include_list_view_settings': 'auto',
'list_icon_size': 16.0,
'list_text_size': 12.0,
'list_scroll_position': (0, 0),
'list_sort_by': 'name',
'list_use_relative_dates': True,
'list_calculate_all_sizes': False,
'list_columns': ('name', 'date-modified', 'size', 'kind', 'date-added'),
'list_column_widths': {
'name': 300,
'date-modified': 181,
'date-created': 181,
'date-added': 181,
'date-last-opened': 181,
'size': 97,
'kind': 115,
'label': 100,
'version': 75,
'comments': 300,
},
'list_column_sort_directions': {
'name': 'ascending',
'date-modified': 'descending',
'date-created': 'descending',
'date-added': 'descending',
'date-last-opened': 'descending',
'size': 'descending',
'kind': 'ascending',
'label': 'ascending',
'version': 'ascending',
'comments': 'ascending',
},
'window_rect': ((100, 100), (640, 280)),
'default_view': 'icon-view',
'icon_locations': {},
'defines': defines
}
# Execute the settings file
if settings_file:
load_settings(settings_file, settings, settings)
# Set up the finder data
bounds = settings['window_rect']
bwsp = {
'ShowStatusBar': settings['show_status_bar'],
'WindowBounds': '{{ %s, %s }, { %s, %s }}' % (bounds[0][0],
bounds[0][1],
bounds[1][0],
bounds[1][1]),
'ContainerShowSidebar': False,
'SidebarWidth': settings['sidebar_width'],
'ShowTabView': settings['show_tab_view'],
'ShowToolbar': settings['show_toolbar'],
'ShowPathbar': settings['show_pathbar'],
'ShowSidebar': settings['show_sidebar']
}
arrange_options = {
'name': 'name',
'date-modified': 'dateModified',
'date-created': 'dateCreated',
'date-added': 'dateAdded',
'date-last-opened': 'dateLastOpened',
'size': 'size',
'kind': 'kind',
'label': 'label',
}
icvp = {
'viewOptionsVersion': 1,
'backgroundType': 0,
'backgroundColorRed': 1.0,
'backgroundColorGreen': 1.0,
'backgroundColorBlue': 1.0,
'gridOffsetX': settings['grid_offset'][0],
'gridOffsetY': settings['grid_offset'][1],
'gridSpacing': settings['grid_spacing'],
'arrangeBy': arrange_options.get(settings['arrange_by'], 'none'),
'showIconPreview': settings['show_icon_preview'],
'showItemInfo': settings['show_item_info'],
'labelOnBottom': settings['label_pos'] == 'bottom',
'textSize': settings['text_size'],
'iconSize': settings['icon_size'],
'scrollPositionX': settings['scroll_position'][0],
'scrollPositionY': settings['scroll_position'][1]
}
background = settings['background']
columns = {
'name': 'name',
'date-modified': 'dateModified',
'date-created': 'dateCreated',
'date-added': 'dateAdded',
'date-last-opened': 'dateLastOpened',
'size': 'size',
'kind': 'kind',
'label': 'label',
'version': 'version',
'comments': 'comments'
}
default_widths = {
'name': 300,
'date-modified': 181,
'date-created': 181,
'date-added': 181,
'date-last-opened': 181,
'size': 97,
'kind': 115,
'label': 100,
'version': 75,
'comments': 300,
}
default_sort_directions = {
'name': 'ascending',
'date-modified': 'descending',
'date-created': 'descending',
'date-added': 'descending',
'date-last-opened': 'descending',
'size': 'descending',
'kind': 'ascending',
'label': 'ascending',
'version': 'ascending',
'comments': 'ascending',
}
lsvp = {
'viewOptionsVersion': 1,
'sortColumn': columns.get(settings['list_sort_by'], 'name'),
'textSize': settings['list_text_size'],
'iconSize': settings['list_icon_size'],
'showIconPreview': settings['show_icon_preview'],
'scrollPositionX': settings['list_scroll_position'][0],
'scrollPositionY': settings['list_scroll_position'][1],
'useRelativeDates': settings['list_use_relative_dates'],
'calculateAllSizes': settings['list_calculate_all_sizes'],
}
lsvp['columns'] = {}
cndx = {}
for n, column in enumerate(settings['list_columns']):
cndx[column] = n
width = settings['list_column_widths'].get(column,
default_widths[column])
asc = 'ascending' == settings['list_column_sort_directions'].get(column,
default_sort_directions[column])
lsvp['columns'][columns[column]] = {
'index': n,
'width': width,
'identifier': columns[column],
'visible': True,
'ascending': asc
}
n = len(settings['list_columns'])
for k in six.iterkeys(columns):
if cndx.get(k, None) is None:
cndx[k] = n
width = default_widths[k]
asc = 'ascending' == default_sort_directions[k]
lsvp['columns'][columns[column]] = {
'index': n,
'width': width,
'identifier': columns[column],
'visible': False,
'ascending': asc
}
n += 1
default_view = settings['default_view']
views = {
'icon-view': b'icnv',
'column-view': b'clmv',
'list-view': b'Nlsv',
'coverflow': b'Flwv'
}
icvl = (b'type', views.get(default_view, 'icnv'))
include_icon_view_settings = default_view == 'icon-view' \
or settings['include_icon_view_settings'] not in \
('auto', 'no', 0, False, None)
include_list_view_settings = default_view in ('list-view', 'coverflow') \
or settings['include_list_view_settings'] not in \
('auto', 'no', 0, False, None)
filename = settings['filename']
volume_name = settings['volume_name']
# Construct a writeable image to start with
dirname,basename = os.path.split(filename)
if dirname == '':
dirname = '.'
tempname = os.path.join(dirname, 'tmp-' + basename)
ret, output = hdiutil('create',
'-ov',
'-volname', volume_name,
'-fs', 'HFS+',
'-fsargs', '-c c=64,a=16,e=16',
'-size', settings['size'],
tempname)
if ret:
raise DMGError('Unable to create disk image')
ret, output = hdiutil('attach',
'-nobrowse',
'-owners', 'off',
'-noidme',
tempname)
if ret:
raise DMGError('Unable to attach disk image')
try:
for info in output['system-entities']:
if info.get('mount-point', None):
device = info['dev-entry']
mount_point = info['mount-point']
icon = settings['icon']
badge_icon = settings['badge_icon']
icon_target_path = os.path.join(mount_point, '.VolumeIcon.icns')
if icon:
shutil.copyfile(icon, icon_target_path)
elif badge_icon:
badge.badge_disk_icon(badge_icon, icon_target_path)
if icon or badge_icon:
subprocess.call(['/usr/bin/SetFile', '-a', 'C', mount_point])
if not isinstance(background, basestring):
pass
elif background == 'builtin-arrow':
tiffdata = pkg_resources.resource_string(
'dmgbuild',
'resources/builtin-arrow.tiff')
path_in_image = os.path.join(mount_point, '.background.tiff')
with open(path_in_image, 'w') as f:
f.write(tiffdata)
alias = Alias.for_file(path_in_image)
icvp['backgroundType'] = 2
icvp['backgroundImageAlias'] = biplist.Data(alias.to_bytes())
elif colors.isAColor(background):
c = colors.parseColor(background).to_rgb()
icvp['backgroundType'] = 1
icvp['backgroundColorRed'] = c.r
icvp['backgroundColorGreen'] = c.g
icvp['backgroundColorBlue'] = c.b
elif os.path.exists(background):
basename = os.path.basename(background)
_, kind = os.path.splitext(basename)
path_in_image = os.path.join(mount_point, '.background' + kind)
shutil.copyfile(background, path_in_image)
alias = Alias.for_file(path_in_image)
icvp['backgroundType'] = 2
icvp['backgroundImageAlias'] = biplist.Data(alias.to_bytes())
else:
raise ValueError('background file "%s" not found' % background)
for f in settings['files']:
basename = os.path.basename(f)
f_in_image = os.path.join(mount_point, basename)
if stat.S_ISDIR(os.stat(f).st_mode):
shutil.copytree(f, f_in_image, symlinks=True)
else:
shutil.copyfile(f, f_in_image)
for name,target in six.iteritems(settings['symlinks']):
name_in_image = os.path.join(mount_point, name)
os.symlink(target, name_in_image)
userfn = settings.get('create_hook', None)
if callable(userfn):
userfn(mount_point, settings)
image_dsstore = os.path.join(mount_point, '.DS_Store')
with DSStore.open(image_dsstore, 'w+') as d:
d['.']['vSrn'] = ('long', 1)
d['.']['bwsp'] = bwsp
if include_icon_view_settings:
d['.']['icvp'] = icvp
if include_list_view_settings:
d['.']['lsvp'] = lsvp
d['.']['icvl'] = icvl
for k,v in six.iteritems(settings['icon_locations']):
d[k]['Iloc'] = v
except:
# Always try to detach
hdiutil('detach', device, plist=False)
raise
ret, output = hdiutil('detach', device, plist=False)
if ret:
raise DMGError('Unable to detach device')
ret, output = hdiutil('convert', tempname,
'-format', settings['format'],
'-ov',
'-o', filename)
if ret:
raise DMGError('Unable to convert')
os.remove(tempname)