diff --git a/.gitignore b/.gitignore index d6a8c9332..c09bbaa15 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,5 @@ coverage.xml htmlcov tests/coverage_html_report tests/.coverage +.venv/ +.vscode/ diff --git a/cx_Freeze/__init__.py b/cx_Freeze/__init__.py index 682cd13bb..61a930ec0 100644 --- a/cx_Freeze/__init__.py +++ b/cx_Freeze/__init__.py @@ -33,7 +33,8 @@ __all__ += ["bdist_msi"] elif sys.platform == "darwin": - from cx_Freeze.command.bdist_mac import bdist_dmg, bdist_mac + from cx_Freeze.command.bdist_dmg import bdist_dmg + from cx_Freeze.command.bdist_mac import bdist_mac __all__ += ["bdist_dmg", "bdist_mac"] else: diff --git a/cx_Freeze/command/bdist_dmg.py b/cx_Freeze/command/bdist_dmg.py new file mode 100644 index 000000000..7a5fedcb6 --- /dev/null +++ b/cx_Freeze/command/bdist_dmg.py @@ -0,0 +1,358 @@ +"""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 is 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 = f"{','.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, ()) diff --git a/cx_Freeze/command/bdist_mac.py b/cx_Freeze/command/bdist_mac.py index 5712d389d..1772e2963 100644 --- a/cx_Freeze/command/bdist_mac.py +++ b/cx_Freeze/command/bdist_mac.py @@ -1,5 +1,5 @@ -"""Implements the 'bdist_dmg' and 'bdist_mac' commands (create macOS dmg and/or -app blundle. +"""Implements the 'bdist_mac' commands (create macOS +app blundle). """ from __future__ import annotations @@ -21,100 +21,7 @@ ) from cx_Freeze.exception import OptionError -__all__ = ["bdist_dmg", "bdist_mac"] - - -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"), - ] - - def initialize_options(self) -> None: - self.volume_label = self.distribution.get_fullname() - self.applications_shortcut = False - self.silent = None - - def finalize_options(self) -> None: - if self.silent is None: - self.silent = False - - 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) - - createargs = [ - "hdiutil", - "create", - ] - if self.silent: - createargs += ["-quiet"] - createargs += [ - "-fs", - "HFSX", - "-format", - "UDZO", - self.dmg_name, - "-imagekey", - "zlib-level=9", - "-srcfolder", - self.dist_dir, - "-volname", - self.volume_label, - ] - - if self.applications_shortcut: - apps_folder_link = os.path.join(self.dist_dir, "Applications") - os.symlink( - "/Applications", apps_folder_link, target_is_directory=True - ) - - # Create the dmg - if subprocess.call(createargs) != 0: - msg = "creation of the dmg failed" - raise OSError(msg) - - 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, ()) +__all__ = ["bdist_mac"] class bdist_mac(Command): diff --git a/doc/src/bdist_dmg.rst b/doc/src/bdist_dmg.rst index 5436c24f3..4a100c9a9 100644 --- a/doc/src/bdist_dmg.rst +++ b/doc/src/bdist_dmg.rst @@ -19,6 +19,72 @@ installation. image * - .. option:: silent (-s) - suppress all output except warnings + * - .. option:: format + - Format of the DMG disk image Default is UDZO + * - .. option:: filesystem + - Filesystem of the DMG disk image Default is HFS+ + * - .. option:: size + - 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. + * - .. option:: background + - A rgb color in the form #3344ff, svg named color like goldenrod, a path + to an image, or the words 'builtin-arrow'. Default is None. + * - .. option:: show_status_bar + - Show the status bar in the Finder window. Default is False. + * - .. option:: show_tab_view + - Show the tab view in the Finder window. Default is False. + * - .. option:: show_path_bar + - Show the path bar in the Finder window. Default is False. + * - .. option:: show_sidebar + - Show the sidebar in the Finder window. Default is False. + * - .. option:: sidebar_width + - Width of the sidebar in the Finder window. Default is None. + * - .. option:: windows_rect + - Window rectangle in the form x,y,width,height" + The position of the window in ((x, y), (w, h)) format, with y coordinates + 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 coordinates. 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. + * - .. option:: icon_locations + - A dictionary specifying the coordinates of items in the root directory of + the disk image, where the keys are filenames and the values are (x, y) + tuples. For example, + icon_locations = { "Applications": (100, 100), "README.txt": (200, 100) } + * - .. option:: default_view + - The default view of the Finder window. Possible values are + "icon-view", "list-view", "column-view", "coverflow". + * - .. option:: show_icon_preview + - Show icon preview in the Finder window. Default is False. + * - .. option:: license + - 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']}} + +.. versionadded:: 7.2 + ``format``, ``filesystem``, ``size``, ``background``, ``show_status_bar``, + ``show_tab_view``, ``show_path_bar``, ``show_sidebar``, ``sidebar_width``, + ``windows_rect``, ``icon_locations``, ``default_view``, ``show_icon_preview``, + ``license`` options. + +The above options come from the `dmgbuild` package. For more information, see +the `dmgbuild documentation `_. This is the equivalent help to specify the same options on the command line: diff --git a/pyproject.toml b/pyproject.toml index aea5179b2..b554b2a1a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,6 +50,7 @@ dependencies = [ "patchelf>=0.14 ;sys_platform == 'linux' and platform_machine == 'armv7l'", "patchelf>=0.14 ;sys_platform == 'linux' and platform_machine == 'ppc64le'", "patchelf>=0.14 ;sys_platform == 'linux' and platform_machine == 's390x'", + "dmgbuild>=1.6.1 ;sys_platform == 'darwin'" ] dynamic = ["version"] diff --git a/requirements.txt b/requirements.txt index 1c11636d0..bfcee5f5b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,3 +10,4 @@ patchelf>=0.14 ;sys_platform == 'linux' and platform_machine == 'aarch64' patchelf>=0.14 ;sys_platform == 'linux' and platform_machine == 'armv7l' patchelf>=0.14 ;sys_platform == 'linux' and platform_machine == 'ppc64le' patchelf>=0.14 ;sys_platform == 'linux' and platform_machine == 's390x' +dmgbuild>=1.6.1 ;sys_platform == 'darwin' diff --git a/samples/dmg/.gitignore b/samples/dmg/.gitignore new file mode 100644 index 000000000..fce19e421 --- /dev/null +++ b/samples/dmg/.gitignore @@ -0,0 +1 @@ +settings.py diff --git a/samples/dmg/hello.py b/samples/dmg/hello.py new file mode 100644 index 000000000..080a9fed2 --- /dev/null +++ b/samples/dmg/hello.py @@ -0,0 +1,21 @@ +import sys +from datetime import datetime, timezone + +today = datetime.now(tz=timezone.utc) +print("Hello from cx_Freeze") +print(f"The current date is {today:%B %d, %Y %H:%M:%S}\n") + +print(f"Executable: {sys.executable}") +print(f"Prefix: {sys.prefix}") +print(f"Default encoding: {sys.getdefaultencoding()}") +print(f"File system encoding: {sys.getfilesystemencoding()}\n") + +print("ARGUMENTS:") +for arg in sys.argv: + print(f"{arg}") +print() + +print("PATH:") +for path in sys.path: + print(f"{path}") +print() diff --git a/samples/dmg/setup.py b/samples/dmg/setup.py new file mode 100644 index 000000000..72757117f --- /dev/null +++ b/samples/dmg/setup.py @@ -0,0 +1,52 @@ +"""A very simple setup script to create a single executable. + +hello.py is a very simple 'Hello, world' type script which also displays the +environment in which the script runs. + +Run the build process by running the command 'python setup.py build' + +If everything works well you should find a subdirectory in the build +subdirectory that contains the files needed to run the script without Python +""" + +from cx_Freeze import Executable, setup + +executables = [ + Executable( + script="hello.py", + # You can also specify an icon for the executable that will be reused for the dmg + # only the first executable is used for the icon + # icon="../../cx_Freeze/icons/python.icns" #noqa: ERA001 + ) +] + +setup( + name="hello", + version="0.1.2.3", + description="Sample cx_Freeze script", + executables=executables, + options={ + "bdist_mac": { + "bundle_name": "hello", + }, + "bdist_dmg": { + "applications_shortcut": True, + "volume_label": "Howdy Yall", + "background": "builtin-arrow", + "license": { + "default-language": "en_US", + "licenses": {"en_US": "Do it right, do it legal, do it safe."}, + "buttons": { + "en_US": [ + "English", + "Agree", + "Disagree", + "Print", + "Save", + "If you agree, click Agree to continue the installation. If you do not agree, click Disagree to cancel the installation.", + ] + }, + }, + }, + }, +) diff --git a/samples/dmg_layout/hello.py b/samples/dmg_layout/hello.py new file mode 100644 index 000000000..080a9fed2 --- /dev/null +++ b/samples/dmg_layout/hello.py @@ -0,0 +1,21 @@ +import sys +from datetime import datetime, timezone + +today = datetime.now(tz=timezone.utc) +print("Hello from cx_Freeze") +print(f"The current date is {today:%B %d, %Y %H:%M:%S}\n") + +print(f"Executable: {sys.executable}") +print(f"Prefix: {sys.prefix}") +print(f"Default encoding: {sys.getdefaultencoding()}") +print(f"File system encoding: {sys.getfilesystemencoding()}\n") + +print("ARGUMENTS:") +for arg in sys.argv: + print(f"{arg}") +print() + +print("PATH:") +for path in sys.path: + print(f"{path}") +print() diff --git a/samples/dmg_layout/hello2.py b/samples/dmg_layout/hello2.py new file mode 100644 index 000000000..080a9fed2 --- /dev/null +++ b/samples/dmg_layout/hello2.py @@ -0,0 +1,21 @@ +import sys +from datetime import datetime, timezone + +today = datetime.now(tz=timezone.utc) +print("Hello from cx_Freeze") +print(f"The current date is {today:%B %d, %Y %H:%M:%S}\n") + +print(f"Executable: {sys.executable}") +print(f"Prefix: {sys.prefix}") +print(f"Default encoding: {sys.getdefaultencoding()}") +print(f"File system encoding: {sys.getfilesystemencoding()}\n") + +print("ARGUMENTS:") +for arg in sys.argv: + print(f"{arg}") +print() + +print("PATH:") +for path in sys.path: + print(f"{path}") +print() diff --git a/samples/dmg_layout/setup.py b/samples/dmg_layout/setup.py new file mode 100644 index 000000000..e04142e9b --- /dev/null +++ b/samples/dmg_layout/setup.py @@ -0,0 +1,56 @@ +"""A very simple setup script to create a single executable. + +hello.py is a very simple 'Hello, world' type script which also displays the +environment in which the script runs. + +Run the build process by running the command 'python setup.py build' + +If everything works well you should find a subdirectory in the build +subdirectory that contains the files needed to run the script without Python +""" + +from cx_Freeze import Executable, setup + +executables = [ + Executable( + script="hello.py", + # You can also specify an icon for the executable that will be reused for the dmg + # only the first executable is used for the icon + # icon="../../cx_Freeze/icons/python.icns" #noqa: ERA001 + ), + Executable(script="hello2.py"), +] + +setup( + name="hello", + version="0.1.2.3", + description="Sample cx_Freeze script", + executables=executables, + options={ + "bdist_mac": { + "bundle_name": "hello", + }, + "bdist_dmg": { + "applications_shortcut": True, + "volume_label": "Howdy Yall", + # from the svg color list, but all of these work too https://dmgbuild.readthedocs.io/en/latest/settings.html#background + "background": "darkviolet", + "show_status_bar": True, + "show_tab_view": True, + "show_path_bar": True, + "show_sidebar": True, + "sidebar_width": 150, + "silent": False, + "default_view": "icon-view", + "list_icon_size": 48, + "list_text_size": 12, + "list_scroll_position": (0, 0), + "list_columns": ["name", "size"], + "list_column_widths": {"name": 200, "size": 100}, + "list_column_sort_directions": { + "name": "ascending", + "size": "ascending", + }, + }, + }, +) diff --git a/tests/test_command_bdist_dmg.py b/tests/test_command_bdist_dmg.py new file mode 100644 index 000000000..6287a5e4a --- /dev/null +++ b/tests/test_command_bdist_dmg.py @@ -0,0 +1,66 @@ +"""Tests for cx_Freeze.command.bdist_dmg.""" + +from __future__ import annotations + +import sys +from pathlib import Path +from subprocess import run + +import pytest + +bdist_dmg = pytest.importorskip( + "cx_Freeze.command.bdist_dmg", reason="macOS tests" +).bdist_dmg + +if sys.platform != "darwin": + pytest.skip(reason="macOS tests", allow_module_level=True) + +SAMPLES_DIR = Path(__file__).resolve().parent.parent / "samples" + + +@pytest.mark.datafiles(SAMPLES_DIR / "dmg") +def test_bdist_dmg(datafiles: Path) -> None: + """Test the simple sample with bdist_dmg.""" + name = "Howdy Yall" + dist_created = datafiles / "build" + + process = run( + [sys.executable, "setup.py", "bdist_dmg"], + text=True, + capture_output=True, + check=False, + cwd=datafiles, + ) + if process.returncode != 0: + expected_err = "hdiutil: create failed - Resource busy" + if expected_err in process.stderr: + pytest.xfail(expected_err) + else: + pytest.fail(process.stderr) + + file_created = dist_created / f"{name}.dmg" + assert file_created.is_file(), f"{name}.dmg" + + +@pytest.mark.datafiles(SAMPLES_DIR / "dmg_layout") +def test_bdist_dmg_custom_layout(datafiles: Path) -> None: + """Test the simple sample with bdist_dmg.""" + name = "Howdy Yall" + dist_created = datafiles / "build" + + process = run( + [sys.executable, "setup.py", "bdist_dmg"], + text=True, + capture_output=True, + check=False, + cwd=datafiles, + ) + if process.returncode != 0: + expected_err = "hdiutil: create failed - Resource busy" + if expected_err in process.stderr: + pytest.xfail(expected_err) + else: + pytest.fail(process.stderr) + + file_created = dist_created / f"{name}.dmg" + assert file_created.is_file(), f"{name}.dmg" diff --git a/tests/test_command_bdist_mac.py b/tests/test_command_bdist_mac.py index 90e2dc11b..92b273ce8 100644 --- a/tests/test_command_bdist_mac.py +++ b/tests/test_command_bdist_mac.py @@ -4,7 +4,6 @@ import sys from pathlib import Path -from subprocess import run import pytest from generate_samples import run_command @@ -41,29 +40,3 @@ def test_bdist_mac(datafiles: Path) -> None: base_name = f"{name}-{version}" file_created = dist_created / f"{base_name}.app" assert file_created.is_dir(), f"{base_name}.app" - - -@pytest.mark.datafiles(SAMPLES_DIR / "simple") -def test_bdist_dmg(datafiles: Path) -> None: - """Test the simple sample with bdist_dmg.""" - name = "hello" - version = "0.1.2.3" - dist_created = datafiles / "build" - - process = run( - [sys.executable, "setup.py", "bdist_dmg"], - text=True, - capture_output=True, - check=False, - cwd=datafiles, - ) - if process.returncode != 0: - expected_err = "hdiutil: create failed - Resource busy" - if expected_err in process.stderr: - pytest.xfail(expected_err) - else: - pytest.fail(process.stderr) - - base_name = f"{name}-{version}" - file_created = dist_created / f"{base_name}.dmg" - assert file_created.is_file(), f"{base_name}.dmg"