dmg-builder 26.0.17 → 26.0.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,290 +1,961 @@
1
- # -*- coding: utf-8 -*-
2
- from __future__ import unicode_literals
3
-
1
+ import json
4
2
  import os
3
+ import platform
4
+ import plistlib
5
5
  import re
6
- import sys
7
-
8
- if sys.version_info.major == 3:
9
- try:
10
- from importlib import reload
11
- except ImportError:
12
- from imp import reload
13
- reload(sys) # To workaround the unbound issue
14
- else:
15
- reload(sys) # Reload is a hack
16
- sys.setdefaultencoding('UTF8')
17
-
18
- sys.path.append(os.path.normpath(os.path.join(os.path.dirname(__file__), "..")))
6
+ import shutil
7
+ import subprocess
8
+ import tempfile
9
+ import time
10
+ import tokenize
19
11
 
20
12
  try:
21
- {}.iteritems
22
- iteritems = lambda x: x.iteritems()
23
- iterkeys = lambda x: x.iterkeys()
24
- except AttributeError:
25
- iteritems = lambda x: x.items()
26
- iterkeys = lambda x: x.keys()
27
- try:
28
- unicode
29
- except NameError:
30
- unicode = str
13
+ import importlib_resources as resources
14
+ except ImportError:
15
+ from importlib import resources
31
16
 
32
- import biplist
33
- from mac_alias import *
34
- from ds_store import *
17
+ from ds_store import DSStore
18
+ from mac_alias import Alias, Bookmark
35
19
 
36
- from colors import parseColor
20
+ from . import colors, licensing
37
21
 
38
22
  try:
39
- from badge import badge
23
+ from . import badge
40
24
  except ImportError:
41
- badge = None
25
+ badge = None
26
+
27
+
28
+ _hexcolor_re = re.compile(r"#[0-9a-f]{3}(?:[0-9a-f]{3})?")
29
+
30
+ # The first element in the platform.mac_ver() tuple is a string containing the
31
+ # macOS version (e.g., '10.15.6'). Parse into an integer tuple.
32
+ MACOS_VERSION = tuple(int(v) for v in platform.mac_ver()[0].split("."))
33
+
42
34
 
43
35
  class DMGError(Exception):
44
- pass
45
-
46
-
47
- def build_dmg():
48
- options = {
49
- 'icon': None,
50
- 'badge_icon': None,
51
- 'sidebar_width': 180,
52
- 'arrange_by': None,
53
- 'grid_offset': (0, 0),
54
- 'grid_spacing': 100.0,
55
- 'scroll_position': (0.0, 0.0),
56
- 'show_icon_preview': False,
57
- 'text_size': os.environ['iconTextSize'],
58
- 'icon_size': os.environ['iconSize'],
59
- 'include_icon_view_settings': 'auto',
60
- 'include_list_view_settings': 'auto',
61
- 'list_icon_size': 16.0,
62
- 'list_text_size': 12.0,
63
- 'list_scroll_position': (0, 0),
64
- 'list_sort_by': 'name',
65
- 'list_columns': ('name', 'date-modified', 'size', 'kind', 'date-added'),
66
- 'list_column_widths': {
67
- 'name': 300,
68
- 'date-modified': 181,
69
- 'date-created': 181,
70
- 'date-added': 181,
71
- 'date-last-opened': 181,
72
- 'size': 97,
73
- 'kind': 115,
74
- 'label': 100,
75
- 'version': 75,
76
- 'comments': 300,
77
- },
78
- 'list_column_sort_directions': {
79
- 'name': 'ascending',
80
- 'date-modified': 'descending',
81
- 'date-created': 'descending',
82
- 'date-added': 'descending',
83
- 'date-last-opened': 'descending',
84
- 'size': 'descending',
85
- 'kind': 'ascending',
86
- 'label': 'ascending',
87
- 'version': 'ascending',
88
- 'comments': 'ascending',
36
+ def __init__(self, callback, message):
37
+ self.message = message
38
+ callback({"type": "error::fatal", "message": message})
39
+
40
+ def __str__(self):
41
+ return str(self.message)
42
+
43
+
44
+ def quiet_callback(info):
45
+ pass
46
+
47
+
48
+ def hdiutil(cmd, *args, **kwargs):
49
+ plist = kwargs.get("plist", True)
50
+ all_args = ["/usr/bin/hdiutil", cmd]
51
+ all_args.extend(args)
52
+ if plist:
53
+ all_args.append("-plist")
54
+ p = subprocess.Popen(
55
+ all_args,
56
+ stdout=subprocess.PIPE,
57
+ stderr=None if plist else subprocess.STDOUT,
58
+ close_fds=True,
59
+ )
60
+ output, _ = p.communicate()
61
+ if plist:
62
+ results = plistlib.loads(output)
63
+ else:
64
+ results = output.decode()
65
+ retcode = p.wait()
66
+ return retcode, results
67
+
68
+
69
+ def load_settings(filename, settings):
70
+ encoding = "utf-8"
71
+ with open(filename, "rb") as fp:
72
+ try:
73
+ encoding = tokenize.detect_encoding(fp.readline)[0]
74
+ except SyntaxError:
75
+ pass
76
+
77
+ with open(filename, encoding=encoding) as fp:
78
+ exec(compile(fp.read(), filename, "exec"), settings, settings)
79
+
80
+
81
+ def load_json(filename, settings):
82
+ """Read an appdmg .json spec.
83
+
84
+ Uses the defaults for appdmg, rather than the usual defaults for
85
+ dmgbuild.
86
+ """
87
+
88
+ with open(filename) as fp:
89
+ json_data = json.load(fp)
90
+
91
+ if "title" not in json_data:
92
+ raise ValueError("missing 'title' in JSON settings file")
93
+ if "contents" not in json_data:
94
+ raise ValueError("missing 'contents' in JSON settings file")
95
+
96
+ settings["volume_name"] = json_data["title"]
97
+ settings["icon"] = json_data.get("icon", None)
98
+ settings["badge_icon"] = json_data.get("badge-icon", None)
99
+ bk = json_data.get("background", None)
100
+ if bk is None:
101
+ bk = json_data.get("background-color", None)
102
+ if bk is not None:
103
+ settings["background"] = bk
104
+ settings["icon_size"] = json_data.get("icon-size", 80)
105
+ tsize = json_data.get("text-size", None)
106
+ if tsize is not None:
107
+ settings["text_size"] = tsize
108
+ wnd = json_data.get(
109
+ "window",
110
+ {
111
+ "position": {"x": 100, "y": 100},
112
+ "size": {"width": 640, "height": 480},
113
+ },
114
+ )
115
+ pos = wnd.get("position", {"x": 100, "y": 100})
116
+ siz = wnd.get("size", {"width": 640, "height": 480})
117
+ settings["window_rect"] = (
118
+ (pos.get("x", 100), pos.get("y", 100)),
119
+ (siz.get("width", 640), siz.get("height", 480)),
120
+ )
121
+ settings["format"] = json_data.get("format", "UDZO")
122
+ settings["filesystem"] = json_data.get("filesystem", "HFS+")
123
+ settings["compression_level"] = json_data.get("compression-level", None)
124
+ settings["license"] = json_data.get("license", None)
125
+ files = []
126
+ hide = []
127
+ hide_extensions = []
128
+ symlinks = {}
129
+ icon_locations = {}
130
+ for fileinfo in json_data.get("contents", []):
131
+ if "path" not in fileinfo:
132
+ raise ValueError("missing 'path' in contents in JSON settings file")
133
+ if "x" not in fileinfo:
134
+ raise ValueError("missing 'x' in contents in JSON settings file")
135
+ if "y" not in fileinfo:
136
+ raise ValueError("missing 'y' in contents in JSON settings file")
137
+
138
+ kind = fileinfo.get("type", "file")
139
+ path = fileinfo["path"]
140
+ name = fileinfo.get("name", os.path.basename(path.rstrip("/")))
141
+ if kind == "file":
142
+ files.append((path, name))
143
+ elif kind == "link":
144
+ symlinks[name] = path
145
+ elif kind == "position":
146
+ pass
147
+ icon_locations[name] = (fileinfo["x"], fileinfo["y"])
148
+ hide_ext = fileinfo.get("hide_extension", False)
149
+ if hide_ext:
150
+ hide_extensions.append(name)
151
+ hidden = fileinfo.get("hidden", False)
152
+ if hidden:
153
+ hide.append(name)
154
+
155
+ settings["files"] = files
156
+ settings["hide_extensions"] = hide_extensions
157
+ settings["hide"] = hide
158
+ settings["symlinks"] = symlinks
159
+ settings["icon_locations"] = icon_locations
160
+
161
+
162
+ def build_dmg( # noqa; C901
163
+ filename,
164
+ volume_name,
165
+ settings_file=None,
166
+ settings={},
167
+ defines={},
168
+ lookForHiDPI=True,
169
+ detach_retries=12,
170
+ callback=quiet_callback,
171
+ ):
172
+ options = {
173
+ # Default settings
174
+ "filename": filename,
175
+ "volume_name": volume_name,
176
+ "format": "UDBZ",
177
+ "filesystem": "HFS+",
178
+ "compression_level": None,
179
+ "size": None,
180
+ "files": [],
181
+ "symlinks": {},
182
+ "hide": [],
183
+ "hide_extensions": [],
184
+ "icon": None,
185
+ "badge_icon": None,
186
+ "background": None,
187
+ "show_status_bar": False,
188
+ "show_tab_view": False,
189
+ "show_toolbar": False,
190
+ "show_pathbar": False,
191
+ "show_sidebar": False,
192
+ "sidebar_width": 180,
193
+ "arrange_by": None,
194
+ "grid_offset": (0, 0),
195
+ "grid_spacing": 100.0,
196
+ "scroll_position": (0.0, 0.0),
197
+ "show_icon_preview": False,
198
+ "show_item_info": False,
199
+ "label_pos": "bottom",
200
+ "text_size": 16.0,
201
+ "icon_size": 128.0,
202
+ "include_icon_view_settings": "auto",
203
+ "include_list_view_settings": "auto",
204
+ "list_icon_size": 16.0,
205
+ "list_text_size": 12.0,
206
+ "list_scroll_position": (0, 0),
207
+ "list_sort_by": "name",
208
+ "list_use_relative_dates": True,
209
+ "list_calculate_all_sizes": False,
210
+ "list_columns": ("name", "date-modified", "size", "kind", "date-added"),
211
+ "list_column_widths": {
212
+ "name": 300,
213
+ "date-modified": 181,
214
+ "date-created": 181,
215
+ "date-added": 181,
216
+ "date-last-opened": 181,
217
+ "size": 97,
218
+ "kind": 115,
219
+ "label": 100,
220
+ "version": 75,
221
+ "comments": 300,
222
+ },
223
+ "list_column_sort_directions": {
224
+ "name": "ascending",
225
+ "date-modified": "descending",
226
+ "date-created": "descending",
227
+ "date-added": "descending",
228
+ "date-last-opened": "descending",
229
+ "size": "descending",
230
+ "kind": "ascending",
231
+ "label": "ascending",
232
+ "version": "ascending",
233
+ "comments": "ascending",
234
+ },
235
+ "window_rect": ((100, 100), (640, 280)),
236
+ "default_view": "icon-view",
237
+ "icon_locations": {},
238
+ "license": None,
239
+ "defines": defines,
240
+ }
241
+
242
+ callback({"type": "build::started"})
243
+
244
+ # Execute the settings file
245
+ if settings_file:
246
+ callback(
247
+ {
248
+ "type": "operation::start",
249
+ "operation": "settings::load",
250
+ }
251
+ )
252
+
253
+ # We now support JSON settings files using appdmg's format
254
+ if settings_file.endswith(".json"):
255
+ load_json(settings_file, options)
256
+ else:
257
+ load_settings(settings_file, options)
258
+
259
+ callback(
260
+ {
261
+ "type": "operation::finished",
262
+ "operation": "settings::load",
263
+ }
264
+ )
265
+
266
+ # Add any overrides
267
+ options.update(settings)
268
+
269
+ # Set up the finder data
270
+ bounds = options["window_rect"]
271
+
272
+ bounds_string = "{{{{{}, {}}}, {{{}, {}}}}}".format(
273
+ bounds[0][0], bounds[0][1], bounds[1][0], bounds[1][1]
274
+ )
275
+ bwsp = {
276
+ "ShowStatusBar": options["show_status_bar"],
277
+ "WindowBounds": bounds_string,
278
+ "ContainerShowSidebar": False,
279
+ "PreviewPaneVisibility": False,
280
+ "SidebarWidth": options["sidebar_width"],
281
+ "ShowTabView": options["show_tab_view"],
282
+ "ShowToolbar": options["show_toolbar"],
283
+ "ShowPathbar": options["show_pathbar"],
284
+ "ShowSidebar": options["show_sidebar"],
89
285
  }
90
- }
91
-
92
- # Set up the finder data
93
- bwsp = {
94
- 'ShowStatusBar': False,
95
- 'ContainerShowSidebar': False,
96
- 'PreviewPaneVisibility': False,
97
- 'SidebarWidth': options['sidebar_width'],
98
- 'ShowTabView': False,
99
- 'ShowToolbar': False,
100
- 'ShowPathbar': False,
101
- 'ShowSidebar': False
102
- }
103
-
104
- window_x = os.environ.get('windowX')
105
- if window_x:
106
- window_y = os.environ['windowY']
107
- bwsp['WindowBounds'] = '{{%s, %s}, {%s, %s}}' % (window_x,
108
- window_y,
109
- os.environ['windowWidth'],
110
- os.environ['windowHeight'])
111
-
112
- arrange_options = {
113
- 'name': 'name',
114
- 'date-modified': 'dateModified',
115
- 'date-created': 'dateCreated',
116
- 'date-added': 'dateAdded',
117
- 'date-last-opened': 'dateLastOpened',
118
- 'size': 'size',
119
- 'kind': 'kind',
120
- 'label': 'label',
121
- }
122
-
123
- icvp = {
124
- 'viewOptionsVersion': 1,
125
- 'backgroundType': 0,
126
- 'backgroundColorRed': 1.0,
127
- 'backgroundColorGreen': 1.0,
128
- 'backgroundColorBlue': 1.0,
129
- 'gridOffsetX': float(options['grid_offset'][0]),
130
- 'gridOffsetY': float(options['grid_offset'][1]),
131
- 'gridSpacing': float(options['grid_spacing']),
132
- 'arrangeBy': str(arrange_options.get(options['arrange_by'], 'none')),
133
- 'showIconPreview': options['show_icon_preview'] == True,
134
- 'showItemInfo': False,
135
- 'labelOnBottom': True,
136
- 'textSize': float(options['text_size']),
137
- 'iconSize': float(options['icon_size']),
138
- 'scrollPositionX': float(options['scroll_position'][0]),
139
- 'scrollPositionY': float(options['scroll_position'][1])
140
- }
141
-
142
- columns = {
143
- 'name': 'name',
144
- 'date-modified': 'dateModified',
145
- 'date-created': 'dateCreated',
146
- 'date-added': 'dateAdded',
147
- 'date-last-opened': 'dateLastOpened',
148
- 'size': 'size',
149
- 'kind': 'kind',
150
- 'label': 'label',
151
- 'version': 'version',
152
- 'comments': 'comments'
153
- }
154
-
155
- default_widths = {
156
- 'name': 300,
157
- 'date-modified': 181,
158
- 'date-created': 181,
159
- 'date-added': 181,
160
- 'date-last-opened': 181,
161
- 'size': 97,
162
- 'kind': 115,
163
- 'label': 100,
164
- 'version': 75,
165
- 'comments': 300,
166
- }
167
-
168
- default_sort_directions = {
169
- 'name': 'ascending',
170
- 'date-modified': 'descending',
171
- 'date-created': 'descending',
172
- 'date-added': 'descending',
173
- 'date-last-opened': 'descending',
174
- 'size': 'descending',
175
- 'kind': 'ascending',
176
- 'label': 'ascending',
177
- 'version': 'ascending',
178
- 'comments': 'ascending',
179
- }
180
-
181
- lsvp = {
182
- 'viewOptionsVersion': 1,
183
- 'sortColumn': columns.get(options['list_sort_by'], 'name'),
184
- 'textSize': float(options['list_text_size']),
185
- 'iconSize': float(options['list_icon_size']),
186
- 'showIconPreview': options['show_icon_preview'],
187
- 'scrollPositionX': options['list_scroll_position'][0],
188
- 'scrollPositionY': options['list_scroll_position'][1],
189
- 'useRelativeDates': True,
190
- 'calculateAllSizes': False,
191
- }
192
-
193
- lsvp['columns'] = {}
194
- cndx = {}
195
-
196
- for n, column in enumerate(options['list_columns']):
197
- cndx[column] = n
198
- width = options['list_column_widths'].get(column, default_widths[column])
199
- asc = 'ascending' == options['list_column_sort_directions'].get(column, default_sort_directions[column])
200
-
201
- lsvp['columns'][columns[column]] = {
202
- 'index': n,
203
- 'width': width,
204
- 'identifier': columns[column],
205
- 'visible': True,
206
- 'ascending': asc
286
+
287
+ arrange_options = {
288
+ "name": "name",
289
+ "date-modified": "dateModified",
290
+ "date-created": "dateCreated",
291
+ "date-added": "dateAdded",
292
+ "date-last-opened": "dateLastOpened",
293
+ "size": "size",
294
+ "kind": "kind",
295
+ "label": "label",
296
+ }
297
+
298
+ icvp = {
299
+ "viewOptionsVersion": 1,
300
+ "backgroundType": 0,
301
+ "backgroundColorRed": 1.0,
302
+ "backgroundColorGreen": 1.0,
303
+ "backgroundColorBlue": 1.0,
304
+ "gridOffsetX": float(options["grid_offset"][0]),
305
+ "gridOffsetY": float(options["grid_offset"][1]),
306
+ "gridSpacing": float(options["grid_spacing"]),
307
+ "arrangeBy": str(arrange_options.get(options["arrange_by"], "none")),
308
+ "showIconPreview": bool(options["show_icon_preview"]),
309
+ "showItemInfo": bool(options["show_item_info"]),
310
+ "labelOnBottom": options["label_pos"] == "bottom",
311
+ "textSize": float(options["text_size"]),
312
+ "iconSize": float(options["icon_size"]),
313
+ "scrollPositionX": float(options["scroll_position"][0]),
314
+ "scrollPositionY": float(options["scroll_position"][1]),
315
+ }
316
+
317
+ background = options["background"]
318
+
319
+ columns = {
320
+ "name": "name",
321
+ "date-modified": "dateModified",
322
+ "date-created": "dateCreated",
323
+ "date-added": "dateAdded",
324
+ "date-last-opened": "dateLastOpened",
325
+ "size": "size",
326
+ "kind": "kind",
327
+ "label": "label",
328
+ "version": "version",
329
+ "comments": "comments",
330
+ }
331
+
332
+ default_widths = {
333
+ "name": 300,
334
+ "date-modified": 181,
335
+ "date-created": 181,
336
+ "date-added": 181,
337
+ "date-last-opened": 181,
338
+ "size": 97,
339
+ "kind": 115,
340
+ "label": 100,
341
+ "version": 75,
342
+ "comments": 300,
343
+ }
344
+
345
+ default_sort_directions = {
346
+ "name": "ascending",
347
+ "date-modified": "descending",
348
+ "date-created": "descending",
349
+ "date-added": "descending",
350
+ "date-last-opened": "descending",
351
+ "size": "descending",
352
+ "kind": "ascending",
353
+ "label": "ascending",
354
+ "version": "ascending",
355
+ "comments": "ascending",
356
+ }
357
+
358
+ lsvp = {
359
+ "viewOptionsVersion": 1,
360
+ "sortColumn": columns.get(options["list_sort_by"], "name"),
361
+ "textSize": float(options["list_text_size"]),
362
+ "iconSize": float(options["list_icon_size"]),
363
+ "showIconPreview": options["show_icon_preview"],
364
+ "scrollPositionX": options["list_scroll_position"][0],
365
+ "scrollPositionY": options["list_scroll_position"][1],
366
+ "useRelativeDates": options["list_use_relative_dates"],
367
+ "calculateAllSizes": options["list_calculate_all_sizes"],
207
368
  }
208
369
 
209
- n = len(options['list_columns'])
210
- for k in iterkeys(columns):
211
- if cndx.get(k, None) is None:
212
- cndx[k] = n
213
- width = default_widths[k]
214
- asc = 'ascending' == default_sort_directions[k]
215
-
216
- lsvp['columns'][columns[column]] = {
217
- 'index': n,
218
- 'width': width,
219
- 'identifier': columns[column],
220
- 'visible': False,
221
- 'ascending': asc
370
+ lsvp["columns"] = {}
371
+ cndx = {}
372
+
373
+ for n, column in enumerate(options["list_columns"]):
374
+ cndx[column] = n
375
+ width = options["list_column_widths"].get(column, default_widths[column])
376
+ asc = "ascending" == options["list_column_sort_directions"].get(
377
+ column, default_sort_directions[column]
378
+ )
379
+
380
+ lsvp["columns"][columns[column]] = {
381
+ "index": n,
382
+ "width": width,
383
+ "identifier": columns[column],
384
+ "visible": True,
385
+ "ascending": asc,
386
+ }
387
+
388
+ n = len(options["list_columns"])
389
+ for k in columns:
390
+ if cndx.get(k, None) is None:
391
+ cndx[k] = n
392
+ width = default_widths[k]
393
+ asc = "ascending" == default_sort_directions[k]
394
+
395
+ lsvp["columns"][columns[column]] = {
396
+ "index": n,
397
+ "width": width,
398
+ "identifier": columns[column],
399
+ "visible": False,
400
+ "ascending": asc,
401
+ }
402
+
403
+ n += 1
404
+
405
+ default_view = options["default_view"]
406
+ views = {
407
+ "icon-view": b"icnv",
408
+ "column-view": b"clmv",
409
+ "list-view": b"Nlsv",
410
+ "coverflow": b"Flwv",
222
411
  }
223
412
 
224
- n += 1
225
-
226
- default_view = 'icon-view'
227
- views = {
228
- 'icon-view': b'icnv',
229
- 'column-view': b'clmv',
230
- 'list-view': b'Nlsv',
231
- 'coverflow': b'Flwv'
232
- }
233
-
234
- icvl = (b'type', views.get(default_view, 'icnv'))
235
-
236
- include_icon_view_settings = default_view == 'icon-view' \
237
- or options['include_icon_view_settings'] not in \
238
- ('auto', 'no', 0, False, None)
239
- include_list_view_settings = default_view in ('list-view', 'coverflow') \
240
- or options['include_list_view_settings'] not in \
241
- ('auto', 'no', 0, False, None)
242
-
243
- try:
244
- background_bmk = None
245
-
246
- background_color = os.environ.get('backgroundColor')
247
- background_file = os.environ.get('backgroundFile')
248
-
249
- if background_color:
250
- c = parseColor(background_color).to_rgb()
251
-
252
- icvp['backgroundType'] = 1
253
- icvp['backgroundColorRed'] = float(c.r)
254
- icvp['backgroundColorGreen'] = float(c.g)
255
- icvp['backgroundColorBlue'] = float(c.b)
256
- elif background_file:
257
- alias = Alias.for_file(background_file)
258
- background_bmk = Bookmark.for_file(background_file)
259
-
260
- icvp['backgroundType'] = 2
261
- icvp['backgroundImageAlias'] = biplist.Data(alias.to_bytes())
262
-
263
- image_dsstore = os.path.join(os.environ['volumePath'], '.DS_Store')
264
-
265
- f = "icon_locations = {\n" + os.environ['iconLocations'] + "\n}"
266
- exec (f, options, options)
267
-
268
- with DSStore.open(image_dsstore, 'w+') as d:
269
- d['.']['vSrn'] = ('long', 1)
270
- d['.']['bwsp'] = bwsp
271
- if include_icon_view_settings:
272
- d['.']['icvp'] = icvp
273
- if background_bmk:
274
- d['.']['pBBk'] = background_bmk
275
- if include_list_view_settings:
276
- d['.']['lsvp'] = lsvp
277
- d['.']['icvl'] = icvl
278
-
279
- d['.background']['Iloc'] = (2560, 170)
280
- d['.DS_Store']['Iloc'] = (2610, 170)
281
- d['.fseventsd']['Iloc'] = (2660, 170)
282
- d['.Trashes']['Iloc'] = (2710, 170)
283
- d['.VolumeIcon.icns']['Iloc'] = (2760, 170)
284
-
285
- for k, v in iteritems(options['icon_locations']):
286
- d[k]['Iloc'] = v
287
- except:
288
- raise
289
-
290
- build_dmg()
413
+ icvl = (b"type", views.get(default_view, "icnv"))
414
+
415
+ include_icon_view_settings = default_view == "icon-view" or options[
416
+ "include_icon_view_settings"
417
+ ] not in ("auto", "no", 0, False, None)
418
+ include_list_view_settings = default_view in ("list-view", "coverflow") or options[
419
+ "include_list_view_settings"
420
+ ] not in ("auto", "no", 0, False, None)
421
+
422
+ filename = options["filename"]
423
+ volume_name = options["volume_name"]
424
+
425
+ # Construct a writeable image to start with
426
+ dirname, basename = os.path.split(os.path.realpath(filename))
427
+ if not basename.endswith(".dmg"):
428
+ basename += ".dmg"
429
+ writableFile = tempfile.NamedTemporaryFile(
430
+ dir=dirname, prefix=".temp", suffix=basename
431
+ )
432
+
433
+ callback(
434
+ {
435
+ "type": "operation::start",
436
+ "operation": "size::calculate",
437
+ }
438
+ )
439
+
440
+ total_size = options["size"]
441
+ if total_size is None:
442
+ # Start with a size of 128MB - this way we don't need to calculate the
443
+ # size of the background image, volume icon, and .DS_Store file (and
444
+ # 128 MB should be well sufficient for even the most outlandish image
445
+ # sizes, like an uncompressed 5K multi-resolution TIFF)
446
+ total_size = 128 * 1024 * 1024
447
+
448
+ def roundup(x, n):
449
+ return x if x % n == 0 else x + n - x % n
450
+
451
+ for path in options["files"]:
452
+ if isinstance(path, tuple):
453
+ path = path[0]
454
+
455
+ if not os.path.islink(path) and os.path.isdir(path):
456
+ for dirpath, dirnames, filenames in os.walk(path):
457
+ for f in filenames:
458
+ fp = os.path.join(dirpath, f)
459
+ total_size += roundup(os.lstat(fp).st_size, 4096)
460
+ else:
461
+ total_size += roundup(os.lstat(path).st_size, 4096)
462
+
463
+ for name, target in options["symlinks"].items():
464
+ total_size += 4096
465
+
466
+ total_size = str(max(total_size / 1000, 1024)) + "K"
467
+
468
+ callback(
469
+ {
470
+ "type": "operation::finished",
471
+ "operation": "size::calculate",
472
+ "size": total_size,
473
+ }
474
+ )
475
+
476
+ callback(
477
+ {
478
+ "type": "command::start",
479
+ "command": "hdiutil::create",
480
+ }
481
+ )
482
+
483
+ filesystem = options["filesystem"].upper()
484
+ fs_args = "-c c=64,a=16,e=16" if filesystem != "APFS" else ""
485
+
486
+ ret, output = hdiutil(
487
+ "create",
488
+ "-ov",
489
+ "-volname",
490
+ volume_name,
491
+ "-fs",
492
+ filesystem,
493
+ "-fsargs",
494
+ fs_args,
495
+ "-size",
496
+ total_size,
497
+ writableFile.name,
498
+ )
499
+
500
+ callback(
501
+ {
502
+ "type": "command::finished",
503
+ "command": "hdiutil::create",
504
+ "ret": ret,
505
+ "output": output,
506
+ }
507
+ )
508
+
509
+ if ret:
510
+ raise DMGError(callback, f"Unable to create disk image: {output}")
511
+
512
+ callback(
513
+ {
514
+ "type": "command::start",
515
+ "command": "hdiutil::attach",
516
+ }
517
+ )
518
+
519
+ # IDME was deprecated in macOS 10.15/Catalina; as a result, use of -noidme
520
+ # started raising a warning.
521
+ if MACOS_VERSION >= (10, 15):
522
+ ret, output = hdiutil(
523
+ "attach", "-nobrowse", "-owners", "off", writableFile.name
524
+ )
525
+ else:
526
+ ret, output = hdiutil(
527
+ "attach", "-nobrowse", "-owners", "off", "-noidme", writableFile.name
528
+ )
529
+
530
+ callback(
531
+ {
532
+ "type": "command::finished",
533
+ "command": "hdiutil::attach",
534
+ "ret": ret,
535
+ "output": output,
536
+ }
537
+ )
538
+
539
+ if ret:
540
+ raise DMGError(callback, f"Unable to attach disk image: {output}")
541
+
542
+ callback(
543
+ {
544
+ "type": "operation::start",
545
+ "operation": "dmg::create",
546
+ }
547
+ )
548
+
549
+ try:
550
+ for info in output["system-entities"]:
551
+ if info.get("mount-point", None):
552
+ device = info["dev-entry"]
553
+ mount_point = info["mount-point"]
554
+
555
+ icon = options["icon"]
556
+ if badge:
557
+ badge_icon = options["badge_icon"]
558
+ else:
559
+ badge_icon = None
560
+ icon_target_path = os.path.join(mount_point, ".VolumeIcon.icns")
561
+ if icon:
562
+ shutil.copyfile(icon, icon_target_path)
563
+ elif badge_icon:
564
+ badge.badge_disk_icon(badge_icon, icon_target_path)
565
+
566
+ if icon or badge_icon:
567
+ subprocess.call(["/usr/bin/SetFile", "-a", "C", mount_point])
568
+
569
+ background_bmk = None
570
+
571
+ callback(
572
+ {
573
+ "type": "operation::start",
574
+ "operation": "background::create",
575
+ }
576
+ )
577
+
578
+ if not isinstance(background, str):
579
+ pass
580
+ elif colors.isAColor(background):
581
+ c = colors.parseColor(background).to_rgb()
582
+
583
+ icvp["backgroundType"] = 1
584
+ icvp["backgroundColorRed"] = float(c.r)
585
+ icvp["backgroundColorGreen"] = float(c.g)
586
+ icvp["backgroundColorBlue"] = float(c.b)
587
+ else:
588
+ if os.path.isfile(background):
589
+ # look to see if there are HiDPI resources available
590
+
591
+ if lookForHiDPI is True:
592
+ name, extension = os.path.splitext(os.path.basename(background))
593
+ orderedImages = [background]
594
+ imageDirectory = os.path.dirname(background)
595
+ if imageDirectory == "":
596
+ imageDirectory = "."
597
+ for candidateName in os.listdir(imageDirectory):
598
+ hasScale = re.match(
599
+ r"^(?P<name>.+)@(?P<scale>\d+)x(?P<extension>\.\w+)$",
600
+ candidateName,
601
+ )
602
+ if (
603
+ hasScale
604
+ and name == hasScale.group("name")
605
+ and extension == hasScale.group("extension")
606
+ ):
607
+ scale = int(hasScale.group("scale"))
608
+ if len(orderedImages) < scale:
609
+ orderedImages += [None] * (scale - len(orderedImages))
610
+ orderedImages[scale - 1] = os.path.join(
611
+ imageDirectory, candidateName
612
+ )
613
+
614
+ if len(orderedImages) > 1:
615
+ # compile the grouped tiff
616
+ backgroundFile = tempfile.NamedTemporaryFile(suffix=".tiff")
617
+ background = backgroundFile.name
618
+ output = tempfile.TemporaryFile(mode="w+")
619
+ try:
620
+ subprocess.check_call(
621
+ ["/usr/bin/tiffutil", "-cathidpicheck"]
622
+ + list(filter(None, orderedImages))
623
+ + ["-out", background],
624
+ stdout=output,
625
+ stderr=output,
626
+ )
627
+ except Exception as e:
628
+ output.seek(0)
629
+ raise ValueError(
630
+ 'unable to compile combined HiDPI file "%s" got error: %s\noutput: %s'
631
+ % (background, str(e), output.read())
632
+ )
633
+
634
+ _, kind = os.path.splitext(background)
635
+ path_in_image = os.path.join(mount_point, ".background" + kind)
636
+ shutil.copyfile(background, path_in_image)
637
+ else:
638
+ dmg_resources = resources.files("dmgbuild")
639
+ bg_resource = dmg_resources.joinpath(
640
+ "resources/" + background + ".tiff"
641
+ )
642
+ if bg_resource.is_file():
643
+ path_in_image = os.path.join(mount_point, ".background.tiff")
644
+ with bg_resource.open("rb") as in_file:
645
+ with open(path_in_image, "wb") as out_file:
646
+ out_file.write(in_file.read())
647
+ else:
648
+ raise ValueError('background file "%s" not found' % background)
649
+
650
+ alias = Alias.for_file(path_in_image)
651
+ background_bmk = Bookmark.for_file(path_in_image)
652
+
653
+ icvp["backgroundType"] = 2
654
+ icvp["backgroundImageAlias"] = alias.to_bytes()
655
+
656
+ callback(
657
+ {
658
+ "type": "operation::finished",
659
+ "operation": "background::create",
660
+ }
661
+ )
662
+
663
+ callback(
664
+ {
665
+ "type": "operation::start",
666
+ "operation": "files::add",
667
+ "total": len(options["files"]),
668
+ }
669
+ )
670
+
671
+ for f in options["files"]:
672
+ if isinstance(f, tuple):
673
+ f_in_image = os.path.join(mount_point, f[1])
674
+ f = f[0]
675
+ else:
676
+ basename = os.path.basename(f.rstrip("/"))
677
+ f_in_image = os.path.join(mount_point, basename)
678
+
679
+ callback(
680
+ {
681
+ "type": "operation::start",
682
+ "operation": "file::add",
683
+ "file": f_in_image,
684
+ }
685
+ )
686
+
687
+ # use system ditto command to preserve code signing, etc.
688
+ subprocess.call(["/usr/bin/ditto", f, f_in_image])
689
+
690
+ callback(
691
+ {
692
+ "type": "operation::finished",
693
+ "operation": "file::add",
694
+ "file": f_in_image,
695
+ }
696
+ )
697
+
698
+ callback(
699
+ {
700
+ "type": "operation::finished",
701
+ "operation": "files::add",
702
+ }
703
+ )
704
+
705
+ callback(
706
+ {
707
+ "type": "operation::start",
708
+ "operation": "symlinks::add",
709
+ "total": len(options["symlinks"]),
710
+ }
711
+ )
712
+
713
+ for name, target in options["symlinks"].items():
714
+ name_in_image = os.path.join(mount_point, name)
715
+ callback(
716
+ {
717
+ "type": "operation::start",
718
+ "operation": "symlink::add",
719
+ "file": name_in_image,
720
+ "target": target,
721
+ }
722
+ )
723
+ os.symlink(target, name_in_image)
724
+ callback(
725
+ {
726
+ "type": "operation::finished",
727
+ "operation": "symlink::add",
728
+ "file": name_in_image,
729
+ "target": target,
730
+ }
731
+ )
732
+
733
+ callback(
734
+ {
735
+ "type": "operation::finished",
736
+ "operation": "symlinks::add",
737
+ }
738
+ )
739
+
740
+ callback(
741
+ {
742
+ "type": "operation::start",
743
+ "operation": "extensions::hide",
744
+ }
745
+ )
746
+
747
+ to_hide = []
748
+ for name in options["hide_extensions"]:
749
+ name_in_image = os.path.join(mount_point, name)
750
+ to_hide.append(name_in_image)
751
+
752
+ if to_hide:
753
+ subprocess.call(["/usr/bin/SetFile", "-a", "E"] + to_hide)
754
+
755
+ to_hide = []
756
+ for name in options["hide"]:
757
+ name_in_image = os.path.join(mount_point, name)
758
+ to_hide.append(name_in_image)
759
+
760
+ if to_hide:
761
+ subprocess.call(["/usr/bin/SetFile", "-a", "V"] + to_hide)
762
+
763
+ callback(
764
+ {
765
+ "type": "operation::finished",
766
+ "operation": "extensions::hide",
767
+ }
768
+ )
769
+
770
+ userfn = options.get("create_hook", None)
771
+ if callable(userfn):
772
+ userfn(mount_point, options)
773
+
774
+ callback(
775
+ {
776
+ "type": "operation::start",
777
+ "operation": "dsstore::create",
778
+ }
779
+ )
780
+
781
+ image_dsstore = os.path.join(mount_point, ".DS_Store")
782
+
783
+ with DSStore.open(image_dsstore, "w+") as d:
784
+ d["."]["vSrn"] = ("long", 1)
785
+ d["."]["bwsp"] = bwsp
786
+ if include_icon_view_settings:
787
+ d["."]["icvp"] = icvp
788
+ if background_bmk:
789
+ d["."]["pBBk"] = background_bmk
790
+ if include_list_view_settings:
791
+ d["."]["lsvp"] = lsvp
792
+ d["."]["icvl"] = icvl
793
+
794
+ for k, v in options["icon_locations"].items():
795
+ d[k]["Iloc"] = v
796
+
797
+ callback(
798
+ {
799
+ "type": "operation::finished",
800
+ "operation": "dsstore::create",
801
+ }
802
+ )
803
+
804
+ # Delete .Trashes, if it gets created
805
+ shutil.rmtree(os.path.join(mount_point, ".Trashes"), True)
806
+ except Exception:
807
+ # Always try to detach
808
+ hdiutil("detach", "-force", device, plist=False)
809
+ raise
810
+
811
+ callback(
812
+ {
813
+ "type": "operation::finished",
814
+ "operation": "dmg::create",
815
+ }
816
+ )
817
+
818
+ # Flush writes before attempting to detach.
819
+ subprocess.check_call(("sync", "--file-system", mount_point))
820
+
821
+ retry_time = 1
822
+ for tries in range(detach_retries):
823
+ callback(
824
+ {
825
+ "type": "command::start",
826
+ "command": "hdiutil::detach",
827
+ }
828
+ )
829
+
830
+ ret, output = hdiutil("detach", device, plist=False)
831
+
832
+ callback(
833
+ {
834
+ "type": "command::finished",
835
+ "command": "hdiutil::detach",
836
+ "ret:": ret,
837
+ "output:": output,
838
+ }
839
+ )
840
+
841
+ if not ret:
842
+ break
843
+
844
+ # Exponentially backoff retries
845
+ retry_time *= 1.5
846
+ time.sleep(retry_time)
847
+
848
+ if ret:
849
+ hdiutil("detach", "-force", device, plist=False)
850
+ raise DMGError(callback, f"Unable to detach device cleanly: {output}")
851
+
852
+ callback(
853
+ {
854
+ "type": "command::start",
855
+ "command": "hdiutil::resize",
856
+ }
857
+ )
858
+
859
+ # Shrink the output to the minimum possible size
860
+ ret, output = hdiutil(
861
+ "resize", "-quiet", "-sectors", "min", writableFile.name, plist=False
862
+ )
863
+
864
+ callback(
865
+ {
866
+ "type": "command::finished",
867
+ "command": "hdiutil::resize",
868
+ "ret": ret,
869
+ "output": output,
870
+ }
871
+ )
872
+
873
+ if ret:
874
+ raise DMGError(callback, f"Unable to shrink: {output}")
875
+
876
+ callback(
877
+ {
878
+ "type": "operation::start",
879
+ "operation": "dmg::shrink",
880
+ }
881
+ )
882
+
883
+ key_prefix = {"UDZO": "zlib", "UDBZ": "bzip2", "ULFO": "lzfse", "ULMO": "lzma"}
884
+ compression_level = options["compression_level"]
885
+ if options["format"] in key_prefix and compression_level:
886
+ compression_args = [
887
+ "-imagekey",
888
+ key_prefix[options["format"]] + "-level=" + str(compression_level),
889
+ ]
890
+ else:
891
+ compression_args = []
892
+
893
+ callback(
894
+ {
895
+ "type": "command::start",
896
+ "command": "hdiutil::convert",
897
+ }
898
+ )
899
+
900
+ ret, output = hdiutil(
901
+ "convert",
902
+ writableFile.name,
903
+ "-format",
904
+ options["format"],
905
+ "-ov",
906
+ "-o",
907
+ filename,
908
+ *compression_args,
909
+ )
910
+
911
+ callback(
912
+ {
913
+ "type": "command::finished",
914
+ "command": "hdiutil::convert",
915
+ "ret": ret,
916
+ "output": output,
917
+ }
918
+ )
919
+
920
+ if ret:
921
+ raise DMGError(callback, f"Unable to convert: {output}")
922
+
923
+ callback(
924
+ {
925
+ "type": "operation::finished",
926
+ "operation": "dmg::shrink",
927
+ }
928
+ )
929
+
930
+ if options["license"]:
931
+ callback(
932
+ {
933
+ "type": "operation::start",
934
+ "command": "dmg::addlicense",
935
+ }
936
+ )
937
+
938
+ licenseDict = licensing.build_license(options["license"])
939
+
940
+ tempLicenseFile = open(os.path.join(dirname, "license.plist"), "wb")
941
+ plistlib.dump(licenseDict, tempLicenseFile)
942
+ tempLicenseFile.close()
943
+
944
+ # see https://developer.apple.com/forums/thread/668084
945
+ ret, output = hdiutil(
946
+ "udifrez", "-xml", tempLicenseFile.name, "", "-quiet", filename, plist=False
947
+ )
948
+
949
+ os.remove(tempLicenseFile.name)
950
+
951
+ if ret:
952
+ raise DMGError(callback, f"Unable to add license: {output}")
953
+
954
+ callback(
955
+ {
956
+ "type": "operation::finished",
957
+ "command": "dmg::addlicense",
958
+ }
959
+ )
960
+
961
+ callback({"type": "build::finished"})