386 lines
14 KiB
Python
386 lines
14 KiB
Python
"""Implements the 'bdist_dmg' command (create macOS dmg and/or app bundle)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import shutil
|
|
from typing import ClassVar
|
|
|
|
from dmgbuild import build_dmg
|
|
from setuptools import Command
|
|
|
|
import cx_Freeze.icons
|
|
from cx_Freeze import Executable
|
|
from cx_Freeze.exception import OptionError
|
|
|
|
__all__ = ["bdist_dmg"]
|
|
|
|
|
|
class bdist_dmg(Command):
|
|
"""Create a Mac DMG disk image containing the Mac application bundle."""
|
|
|
|
description = (
|
|
"create a Mac DMG disk image containing the Mac application bundle"
|
|
)
|
|
user_options: ClassVar[list[tuple[str, str | None, str]]] = [
|
|
("volume-label=", None, "Volume label of the DMG disk image"),
|
|
(
|
|
"applications-shortcut=",
|
|
None,
|
|
"Boolean for whether to include "
|
|
"shortcut to Applications in the DMG disk image",
|
|
),
|
|
("silent", "s", "suppress all output except warnings"),
|
|
("format=", None, 'format of the disk image [default: "UDZO"]'),
|
|
(
|
|
"filesystem=",
|
|
None,
|
|
'filesystem of the disk image [default: "HFS+"]',
|
|
),
|
|
(
|
|
"size=",
|
|
None,
|
|
"If defined, specifies the size of the filesystem within the "
|
|
"image. If this is not defined, cx_Freeze (and then dmgbuild) "
|
|
"will attempt to determine a reasonable size for the image. "
|
|
"If you set this, you should set it large enough to hold the "
|
|
"files you intend to copy into the image. The syntax is the "
|
|
"same as for the -size argument to hdiutil, i.e. you can use "
|
|
"the suffixes `b`, `k`, `m`, `g`, `t`, `p` and `e` for bytes, "
|
|
"kilobytes, megabytes, gigabytes, terabytes, exabytes and "
|
|
"petabytes respectively.",
|
|
),
|
|
(
|
|
"background",
|
|
"b",
|
|
"A rgb color in the form #3344ff, svg named color like goldenrod, "
|
|
"a path to an image, or the words 'builtin-arrow' [default: None]",
|
|
),
|
|
(
|
|
"show-status-bar",
|
|
None,
|
|
"Show the status bar in the Finder window. Default is False.",
|
|
),
|
|
(
|
|
"show-tab-view",
|
|
None,
|
|
"Show the tab view in the Finder window. Default is False.",
|
|
),
|
|
(
|
|
"show-path-bar",
|
|
None,
|
|
"Show the path bar in the Finder window. Default is False.",
|
|
),
|
|
(
|
|
"show-sidebar",
|
|
None,
|
|
"Show the sidebar in the Finder window. Default is False.",
|
|
),
|
|
(
|
|
"sidebar-width",
|
|
None,
|
|
"Width of the sidebar in the Finder window. Default is None.",
|
|
),
|
|
(
|
|
"window-rect",
|
|
None,
|
|
"Window rectangle in the form x, y, width, height. The position "
|
|
"of the window in ((x, y), (w, h)) format, with y co-ordinates "
|
|
"running from bottom to top. The Finder makes sure that the "
|
|
"window will be on the user's display, so if you want your window "
|
|
"at the top left of the display you could use (0, 100000) as the "
|
|
"x, y co-ordinates. Unfortunately it doesn't appear to be "
|
|
"possible to position the window relative to the top left or "
|
|
"relative to the centre of the user's screen.",
|
|
),
|
|
(
|
|
"icon-locations",
|
|
None,
|
|
"A dictionary specifying the co-ordinates of items in the root "
|
|
"directory of the disk image, where the keys are filenames and "
|
|
"the values are (x, y) tuples. e.g.: "
|
|
'icon-locations = { "Applications": (100, 100), '
|
|
'"README.txt": (200, 100) }',
|
|
),
|
|
(
|
|
"default-view",
|
|
None,
|
|
"The default view of the Finder window. Possible values are "
|
|
'"icon-view", "list-view", "column-view", "coverflow".',
|
|
),
|
|
(
|
|
"show-icon-preview",
|
|
None,
|
|
"Show icon preview in the Finder window. Default is False.",
|
|
),
|
|
(
|
|
"license",
|
|
None,
|
|
"Dictionary specifying license details with 'default-language', "
|
|
"'licenses', and 'buttons'."
|
|
"default-language: Language code (e.g., 'en_US') if no matching "
|
|
"system language."
|
|
"licenses: Map of language codes to license file paths "
|
|
"(e.g., {'en_US': 'path/to/license_en.txt'})."
|
|
"buttons: Map of language codes to UI strings "
|
|
"([language, agree, disagree, print, save, instruction])."
|
|
"Example: {'default-language': 'en_US', "
|
|
"'licenses': {'en_US': 'path/to/license_en.txt'}, "
|
|
"'buttons': {'en_US': ['English', 'Agree', 'Disagree', 'Print', "
|
|
"'Save', 'Instruction text']}}",
|
|
),
|
|
]
|
|
|
|
def initialize_options(self) -> None:
|
|
self.silent = None
|
|
self.volume_label = self.distribution.get_fullname()
|
|
self.applications_shortcut = False
|
|
self._symlinks = {}
|
|
self._files = []
|
|
self.format = "UDZO"
|
|
self.filesystem = "HFS+"
|
|
self.size = None
|
|
self.background = None
|
|
self.show_status_bar = False
|
|
self.show_tab_view = False
|
|
self.show_path_bar = False
|
|
self.show_sidebar = False
|
|
self.sidebar_width = None
|
|
self.window_rect = None
|
|
self.hide = None
|
|
self.hide_extensions = None
|
|
self.icon_locations = None
|
|
self.default_view = None
|
|
self.show_icon_preview = False
|
|
self.license = None
|
|
|
|
# Non-exposed options
|
|
self.include_icon_view_settings = "auto"
|
|
self.include_list_view_settings = "auto"
|
|
self.arrange_by = None
|
|
self.grid_offset = None
|
|
self.grid_spacing = None
|
|
self.scroll_position = None
|
|
self.label_pos = None
|
|
self.text_size = None
|
|
self.icon_size = None
|
|
self.list_icon_size = None
|
|
self.list_text_size = None
|
|
self.list_scroll_position = None
|
|
self.list_sort_by = None
|
|
self.list_use_relative_dates = None
|
|
self.list_calculate_all_sizes = None
|
|
self.list_columns = None
|
|
self.list_column_widths = None
|
|
self.list_column_sort_directions = None
|
|
|
|
def finalize_options(self) -> None:
|
|
if not self.volume_label:
|
|
msg = "volume-label must be set"
|
|
raise OptionError(msg)
|
|
if self.applications_shortcut:
|
|
self._symlinks["Applications"] = "/Applications"
|
|
if self.silent is None:
|
|
self.silent = False
|
|
|
|
self.finalize_dmgbuild_options()
|
|
|
|
def finalize_dmgbuild_options(self) -> None:
|
|
if self.background:
|
|
self.background = self.background.strip()
|
|
if self.background == "builtin-arrow" and (
|
|
self.icon_locations or self.window_rect
|
|
):
|
|
msg = (
|
|
"background='builtin-arrow' cannot be used with "
|
|
"icon_locations or window_rect"
|
|
)
|
|
raise OptionError(msg)
|
|
if not self.arrange_by:
|
|
self.arrange_by = None
|
|
if not self.grid_offset:
|
|
self.grid_offset = (0, 0)
|
|
if not self.grid_spacing:
|
|
self.grid_spacing = 100
|
|
if not self.scroll_position:
|
|
self.scroll_position = (0, 0)
|
|
if not self.label_pos:
|
|
self.label_pos = "bottom"
|
|
if not self.text_size:
|
|
self.text_size = 16
|
|
if not self.icon_size:
|
|
self.icon_size = 128
|
|
|
|
def build_dmg(self) -> None:
|
|
# Remove DMG if it already exists
|
|
if os.path.exists(self.dmg_name):
|
|
os.unlink(self.dmg_name)
|
|
|
|
# Make dist folder
|
|
self.dist_dir = os.path.join(self.build_dir, "dist")
|
|
if os.path.exists(self.dist_dir):
|
|
shutil.rmtree(self.dist_dir)
|
|
self.mkpath(self.dist_dir)
|
|
|
|
# Copy App Bundle
|
|
dest_dir = os.path.join(
|
|
self.dist_dir, os.path.basename(self.bundle_dir)
|
|
)
|
|
if self.silent:
|
|
shutil.copytree(self.bundle_dir, dest_dir, symlinks=True)
|
|
else:
|
|
self.copy_tree(self.bundle_dir, dest_dir, preserve_symlinks=True)
|
|
|
|
# Add the App Bundle to the list of files
|
|
self._files.append(self.bundle_dir)
|
|
|
|
# set the app_name for the application bundle
|
|
app_name = os.path.basename(self.bundle_dir)
|
|
# Set the defaults
|
|
if (
|
|
self.background == "builtin-arrow"
|
|
and not self.icon_locations
|
|
and not self.window_rect
|
|
):
|
|
self.icon_locations = {
|
|
"Applications": (500, 120),
|
|
app_name: (140, 120),
|
|
}
|
|
self.window_rect = ((100, 100), (640, 380))
|
|
|
|
executables = self.distribution.executables # type: list[Executable]
|
|
executable: Executable = executables[0]
|
|
if len(executables) > 1:
|
|
self.warn(
|
|
"using the first executable as entrypoint: "
|
|
f"{executable.target_name}"
|
|
)
|
|
if executable.icon is None:
|
|
icon_name = "setup.icns"
|
|
icon_source_dir = os.path.dirname(cx_Freeze.icons.__file__)
|
|
self.icon = os.path.join(icon_source_dir, icon_name)
|
|
else:
|
|
self.icon = os.path.abspath(executable.icon)
|
|
|
|
with open("settings.py", "w") as f:
|
|
|
|
def add_param(name, value) -> None:
|
|
# if value is a string, add quotes
|
|
if isinstance(value, (str)):
|
|
f.write(f"{name} = '{value}'\n")
|
|
else:
|
|
f.write(f"{name} = {value}\n")
|
|
|
|
# Some fields expect and allow None, others don't
|
|
# so we need to check for None and not add them for
|
|
# the fields that don't allow it
|
|
|
|
# Disk Image Settings
|
|
add_param("filename", self.dmg_name)
|
|
add_param("volume_label", self.volume_label)
|
|
add_param("format", self.format)
|
|
add_param("filesystem", self.filesystem)
|
|
add_param("size", self.size)
|
|
|
|
# Content Settings
|
|
add_param("files", self._files)
|
|
add_param("symlinks", self._symlinks)
|
|
if self.hide:
|
|
add_param("hide", self.hide)
|
|
if self.hide_extensions:
|
|
add_param("hide_extensions", self.hide_extensions)
|
|
# Only one of these can be set
|
|
if self.icon_locations:
|
|
add_param("icon_locations", self.icon_locations)
|
|
if self.icon:
|
|
add_param("icon", self.icon)
|
|
# We don't need to set this, as we only support icns
|
|
# add param ( "badge_icon", self.badge_icon)
|
|
|
|
# Window Settings
|
|
add_param("background", self.background)
|
|
add_param("show_status_bar", self.show_status_bar)
|
|
add_param("show_tab_view", self.show_tab_view)
|
|
add_param("show_pathbar", self.show_path_bar)
|
|
add_param("show_sidebar", self.show_sidebar)
|
|
add_param("sidebar_width", self.sidebar_width)
|
|
if self.window_rect:
|
|
add_param("window_rect", self.window_rect)
|
|
if self.default_view:
|
|
add_param("default_view", self.default_view)
|
|
|
|
add_param("show_icon_preview", self.show_icon_preview)
|
|
add_param(
|
|
"include_icon_view_settings", self.include_icon_view_settings
|
|
)
|
|
add_param(
|
|
"include_list_view_settings", self.include_list_view_settings
|
|
)
|
|
|
|
# Icon View Settings\
|
|
add_param("arrange_by", self.arrange_by)
|
|
add_param("grid_offset", self.grid_offset)
|
|
add_param("grid_spacing", self.grid_spacing)
|
|
add_param("scroll_position", self.scroll_position)
|
|
add_param("label_pos", self.label_pos)
|
|
if self.text_size:
|
|
add_param("text_size", self.text_size)
|
|
if self.icon_size:
|
|
add_param("icon_size", self.icon_size)
|
|
if self.icon_locations:
|
|
add_param("icon_locations", self.icon_locations)
|
|
|
|
# List View Settings
|
|
if self.list_icon_size:
|
|
add_param("list_icon_size", self.list_icon_size)
|
|
if self.list_text_size:
|
|
add_param("list_text_size", self.list_text_size)
|
|
if self.list_scroll_position:
|
|
add_param("list_scroll_position", self.list_scroll_position)
|
|
add_param("list_sort_by", self.list_sort_by)
|
|
add_param("list_use_relative_dates", self.list_use_relative_dates)
|
|
add_param(
|
|
"list_calculate_all_sizes", self.list_calculate_all_sizes
|
|
)
|
|
if self.list_columns:
|
|
add_param("list_columns", self.list_columns)
|
|
if self.list_column_widths:
|
|
add_param("list_column_widths", self.list_column_widths)
|
|
if self.list_column_sort_directions:
|
|
add_param(
|
|
"list_column_sort_directions",
|
|
self.list_column_sort_directions,
|
|
)
|
|
|
|
# License Settings
|
|
add_param("license", self.license)
|
|
|
|
def log_handler(msg: dict[str, str]) -> None:
|
|
if not self.silent:
|
|
loggable = ",".join(
|
|
f"{key}: {value}" for key, value in msg.items()
|
|
)
|
|
self.announce(loggable)
|
|
|
|
build_dmg(
|
|
self.dmg_name,
|
|
self.volume_label,
|
|
"settings.py",
|
|
callback=log_handler,
|
|
)
|
|
|
|
def run(self) -> None:
|
|
# Create the application bundle
|
|
self.run_command("bdist_mac")
|
|
|
|
# Find the location of the application bundle and the build dir
|
|
self.bundle_dir = self.get_finalized_command("bdist_mac").bundle_dir
|
|
self.build_dir = self.get_finalized_command("build_exe").build_base
|
|
|
|
# Set the file name of the DMG to be built
|
|
self.dmg_name = os.path.join(
|
|
self.build_dir, self.volume_label + ".dmg"
|
|
)
|
|
|
|
self.execute(self.build_dmg, ())
|