dmg-builder 26.5.0 → 26.7.0

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