Path: blob/main/tools/build_config/buildMacOSInstaller.py
193904 views
#!/usr/bin/env python1# -*- coding: utf-8 -*-2# Eclipse SUMO, Simulation of Urban MObility; see https://eclipse.dev/sumo3# Copyright (C) 2008-2026 German Aerospace Center (DLR) and others.4# This program and the accompanying materials are made available under the5# terms of the Eclipse Public License 2.0 which is available at6# https://www.eclipse.org/legal/epl-2.0/7# This Source Code may also be made available under the following Secondary8# Licenses when the conditions for such availability set forth in the Eclipse9# Public License 2.0 are satisfied: GNU General Public License, version 210# or later which is available at11# https://www.gnu.org/licenses/old-licenses/gpl-2.0-standalone.html12# SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later1314# @file buildMacOSInstaller.py15# @author Robert Hilbrich16# @date 2024-07-161718# Creates the macOS installer for the current version of SUMO.1920import os21import plistlib22import shutil23import subprocess24import sys25import tempfile26from glob import iglob2728sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))29from sumolib.options import ArgumentParser # noqa3031from build_config.version import get_pep440_version # noqa3233try:34from delocate.cmd.delocate_path import delocate_path35except ImportError:36print("Error: delocate module is not installed. Please install it using 'pip install delocate'.")37sys.exit(1)3839try:40from dmgbuild.core import build_dmg41except ImportError:42print("Error: dmgbuild module is not installed. Please install it using 'pip install dmgbuild'.")43sys.exit(1)444546def parse_args(def_dmg_name, def_pkg_name):47op = ArgumentParser(description="Build an installer for macOS (dmg file)")4849# We can set one these actions exclusively50action_group = op.add_mutually_exclusive_group("Actions")51action_group.add_argument("--create-framework-dir", action="store_true")52action_group.add_argument("--create-framework-pkg", action="store_true")53action_group.add_argument("--create-apps-dir", action="store_true")54action_group.add_argument("--create-apps-pkg", action="store_true")55action_group.add_argument("--create-installer-pkg", action="store_true")56action_group.add_argument("--create-installer-dmg", action="store_true")57action_group.add_argument("--all", action="store_true")5859# ... and supply some arguments60base_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", ".."))61def_output_dmg_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", def_dmg_name))62op.add_argument("--build-dir", default=os.path.join(base_dir, "sumo-build"))63op.add_argument("--framework-dir", default=os.path.join(base_dir, "framework"))64op.add_argument("--framework-pkg-dir", default=os.path.join(base_dir, "framework-pkg"))65op.add_argument("--apps-dir", default=os.path.join(base_dir, "apps"))66op.add_argument("--apps-pkg-dir", default=os.path.join(base_dir, "apps-pkg"))67op.add_argument("--installer-pkg-file", default=os.path.join(base_dir, "installer", def_pkg_name))68op.add_argument("--installer-dmg-file", default=def_output_dmg_path)69op.add_argument("--clean", action="store_true")7071args = op.parse_args()7273# Validate the basic argument logic74if args.build_dir is not None and args.create_framework_dir is None and args.all is None:75print("Error: build directory can only be set when creating the framework directory.", file=sys.stderr)76sys.exit(1)7778if args.framework_pkg_dir is not None and args.create_framework_pkg is None and args.all is None:79print("Error: framework pkg directory can only be set when creating the framework pkg.", file=sys.stderr)80sys.exit(1)8182if args.apps_dir is not None and args.create_apps_dir is None and args.all is None:83print("Error: apps directory can only be set when creating the apps.", file=sys.stderr)84sys.exit(1)8586return args878889def create_framework_dir(name, longname, pkg_id, version, sumo_build_directory, framework_output_dir):90# Create the directory structure for the framework bundle91# see: https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPFrameworks/Concepts/FrameworkAnatomy.html # noqa92#93# EclipseSUMO.framework/94# ├── EclipseSUMO --> Versions/Current/EclipseSUMO95# ├── Resources --> Versions/Current/Resources96# └── Versions97# ├── v1_20_098# │ ├── EclipseSUMO99# │ └── Resources100# │ └── Info.plist101# └── Current --> v_1_20_0102103print(" - Creating directory structure")104os.makedirs(framework_output_dir, exist_ok=False)105106framework_dir = os.path.join(framework_output_dir, f"{name}.framework")107version_dir = os.path.join(framework_dir, f"Versions/{version}")108os.makedirs(os.path.join(version_dir, name), exist_ok=True)109os.makedirs(os.path.join(version_dir, "Resources"), exist_ok=True)110111os.symlink(f"{version}/", os.path.join(framework_dir, "Versions/Current"), True)112os.symlink(f"Versions/Current/{name}/", os.path.join(framework_dir, name), True)113os.symlink("Versions/Current/Resources/", os.path.join(framework_dir, "Resources"), True)114115# Create the Info.plist file116plist_file = os.path.join(version_dir, "Resources", "Info.plist")117print(" - Creating properties list")118plist_content = {119"CFBundleExecutable": longname,120"CFBundleIdentifier": pkg_id,121"CFBundleName": longname,122"CFBundleVersion": version,123"CFBundleShortVersionString": version,124}125with open(plist_file, "wb") as f:126plistlib.dump(plist_content, f)127128# Copy files from the current repository clone to version_dir/EclipseSUMO129print(" - Installing Eclipse SUMO build")130cmake_install_command = [131"cmake",132"--install",133sumo_build_directory,134"--prefix",135os.path.join(version_dir, name),136]137subprocess.run(cmake_install_command, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)138139# Remove unneeded libsumo and libtraci folders from the tools directory140# (If the user needs libsumo or libtraci, they should install these tools with pip)141print(" - Removing libsumo and libtraci folders from tools")142shutil.rmtree(os.path.join(version_dir, name, "share", "sumo", "tools", "libsumo"), ignore_errors=True)143shutil.rmtree(os.path.join(version_dir, name, "share", "sumo", "tools", "libtraci"), ignore_errors=True)144145# We need to add a symlink to the binary folder to have the same folder structure146os.symlink("../../bin", os.path.join(version_dir, name, "share", "sumo", "bin"))147148# Determine library dependencies149print(" - Delocating binaries and libraries")150os.chdir(os.path.join(version_dir, name))151152# - libraries that landed in the lib folder need to be delocated as well153lib_dir = os.path.join(version_dir, name, "lib")154bin_dir = os.path.join(version_dir, name, "bin")155for pattern in ("*.jnilib", "*.dylib"):156for file in iglob(os.path.join(lib_dir, pattern)):157file_name = os.path.basename(file)158shutil.move(os.path.join(lib_dir, file_name), os.path.join(bin_dir, file_name))159160# Start the delocation of the libraries and binaries161delocate_path("./bin", lib_filt_func=None, lib_path="./lib", sanitize_rpaths=True)162163# - and we need to move them back to the lib folder164for pattern in ("*.jnilib", "*.dylib"):165for file in iglob(os.path.join(bin_dir, pattern)):166file_name = os.path.basename(file)167shutil.move(os.path.join(bin_dir, file_name), os.path.join(lib_dir, file_name))168169# Add proj db files from /opt/homebrew/Cellar/proj/<X.Y.Z>/share/proj170print(" - Copying additional files (e.g. proj.db)")171proj_dir = "/opt/homebrew/Cellar/proj"172proj_file_list = ["GL27", "ITRF2000", "ITRF2008", "ITRF2014", "nad.lst", "nad27", "nad83", "other.extra", "proj.db",173"proj.ini", "projjson.schema.json", "triangulation.schema.json", "world", "CH", "deformation_model.schema.json"] # noqa174dest_dir = os.path.join(version_dir, name, "share", "proj")175if os.path.exists(proj_dir):176first_dir = next(iter(os.listdir(proj_dir)), None)177if first_dir:178source_dir = os.path.join(proj_dir, first_dir, "share/proj")179if os.path.exists(source_dir):180os.makedirs(dest_dir, exist_ok=True)181for file in proj_file_list:182shutil.copy2(os.path.join(source_dir, file), os.path.join(dest_dir, file))183184185def create_framework_pkg(name, pkg_id, version, framework_dir, framework_pkg_dir):186# Build the framework package187os.makedirs(framework_pkg_dir, exist_ok=False)188pkg_name = f"{name}-{version}.pkg"189pkg_path = os.path.join(framework_pkg_dir, pkg_name)190pkg_build_command = [191"pkgbuild",192"--root",193os.path.join(framework_dir, f"{name}.framework"),194"--identifier",195pkg_id,196"--version",197version,198"--install-location",199f"/Library/Frameworks/{name}.framework",200f"{pkg_path}",201]202print(f" - Using the call {pkg_build_command}")203print(f" - Calling pkgbuild to create \"{pkg_path}\"")204subprocess.run(pkg_build_command, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)205pkg_size = os.path.getsize(pkg_path)206return name, pkg_name, pkg_id, pkg_path, pkg_size207208209def create_app_dir(app_name, exec_call, framework_name, pkg_id, version, icns_path, app_output_dir):210# Example app structure:211# SUMO-GUI.app212# └── Contents213# └── Info.plist214# ├── MacOS215# │ └── SUMO-GUI216# └── Resources217# └── iconsfile.icns218219print(" . Creating directory structure")220os.makedirs(os.path.join(app_output_dir, f"{app_name}.app", "Contents", "MacOS"))221os.makedirs(os.path.join(app_output_dir, f"{app_name}.app", "Contents", "Resources"))222223print(" . Creating launcher")224launcher_content = f"""#!/bin/bash225export SUMO_HOME="/Library/Frameworks/{framework_name}.framework/Versions/Current/{framework_name}/share/sumo"226{exec_call}227"""228launcher_path = os.path.join(app_output_dir, f"{app_name}.app", "Contents", "MacOS", app_name)229with open(launcher_path, "w") as launcher:230launcher.write(launcher_content)231os.chmod(launcher_path, 0o755)232233# Copy the icons234print(" . Copying icons")235shutil.copy(icns_path, os.path.join(app_output_dir, f"{app_name}.app", "Contents", "Resources", "iconfile.icns"))236237# Create plist file238print(" . Creating properties file")239plist_file = os.path.join(app_output_dir, f"{app_name}.app", "Contents", "Info.plist")240plist_content = {241"CFBundleExecutable": app_name,242"CFBundleIdentifier": pkg_id,243"CFBundleName": app_name,244"CFBundleVersion": version,245"CFBundleShortVersionString": version,246"CFBundleIconFile": "iconfile.icns",247}248with open(plist_file, "wb") as f:249plistlib.dump(plist_content, f)250251252def create_app_pkg(app_name, pkg_id, version, app_dir, apps_pkg_dir):253pkg_name = f"Launcher-{app_name}-{version}.pkg"254pkg_path = os.path.join(apps_pkg_dir, pkg_name)255pkg_build_command = [256"pkgbuild",257"--root",258os.path.join(app_dir, f"{app_name}.app"),259"--identifier",260pkg_id,261"--version",262version,263"--install-location",264f"/Applications/{app_name}.app",265f"{pkg_path}",266]267subprocess.run(pkg_build_command, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)268pkg_size = os.path.getsize(pkg_path)269return app_name, pkg_name, pkg_id, pkg_path, pkg_size270271272def create_installer(framework_pkg, apps_pkg, version, installer_pkg_file):273""""Creates the installer package274275framework_pkg: framework info [framework_path, framework_id]276apps_pkg: apps info [[app1_path, app1_id], [app2_path, app2_id], ...]277id: id of the pkg-file for the installer278version: 1.20.0279installer_pkg_file: name of the output pkg file280"""281282# Create a temporary directory to assemble everything for the installer283temp_dir = tempfile.mkdtemp()284285# Copy the framework pkg file and the launcher apps pkg files286shutil.copy(framework_pkg[0], temp_dir)287for app_pkg in apps_pkg:288shutil.copy(app_pkg[0], temp_dir)289290# Add license, background and other nice stuff291resources_dir = os.path.join(temp_dir, "Resources")292os.makedirs(resources_dir)293sumo_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..")294sumo_data_installer_dir = os.path.join(sumo_dir, "build_config", "macos", "installer")295shutil.copy(os.path.join(sumo_data_installer_dir, "background.png"), resources_dir)296shutil.copy(os.path.join(sumo_dir, "LICENSE"), os.path.join(resources_dir, "LICENSE.txt"))297298# Create conclusion.html in the installer resources folder299with open(os.path.join(resources_dir, "conclusion.html"), "w") as file:300file.write("""<!DOCTYPE html>301<html lang="en">302<head>303<meta charset="UTF-8">304<style>305body {306font-family: Helvetica;307font-size: 14px;308}309</style>310</head>311<body>312<div>313<h4>Important:</h4>314<ul>315<li>316For applications with a graphical user interface to function properly, please ensure317you have <b>XQuartz</b> installed.318It can be obtained from: <a href="https://www.xquartz.org" target="_blank">XQuartz</a>.319</li>320<li>321You may need to install Python 3, if it is not installed yet. Python is required for the322Scenario Wizard and other tools.323</li>324<li>325If you intend to use SUMO from the command line, please remember to set326the <b>SUMO_HOME</b> environment variable and add it to the <b>PATH</b> variable.327<br>328For more details, visit the329<a href="https://sumo.dlr.de/docs/Installing/index.html#macos" target="_blank">330SUMO macOS installation guide331</a>.332</li>333</ul>334</p>335<p>For support options, including the "sumo-user" mailing list, please visit:336<a href="https://eclipse.dev/sumo/contact/" target="_blank">SUMO Contact</a>.337</p>338</div>339</body>340</html>341""")342343# Create distribution.xml344print(" - Creating distribution.xml")345size = os.path.getsize(framework_pkg[0]) // 1024346path = os.path.basename(framework_pkg[0])347refs = f" <pkg-ref id='{framework_pkg[1]}' version='{version}' installKBytes='{size}'>{path}</pkg-ref>"348349for app_pkg in apps_pkg:350size = os.path.getsize(app_pkg[0]) // 1024351path = os.path.basename(app_pkg[0])352refs += f"\n <pkg-ref id='{app_pkg[1]}' version='{version}' installKBytes='{size}'>{path}</pkg-ref>"353354# See: https://developer.apple.com/library/archive/documentation/355# DeveloperTools/Reference/DistributionDefinitionRef/Chapters/Distribution_XML_Ref.html356distribution_content = f"""<?xml version="1.0" encoding="utf-8"?>357<installer-gui-script minSpecVersion="2">358<title>Eclipse SUMO</title>359<allowed-os-versions><os-version min="10.14"/></allowed-os-versions>360<license file="LICENSE.txt"/>361<background file="background.png" alignment="bottomleft" mime-type="image/png" scaling="none" />362<conclusion file="conclusion.html" mime-type="text/html"/>363<options customize="allow" require-scripts="false" rootVolumeOnly="true" hostArchitectures="arm64"/>364<choices-outline>365<line choice="default"/>366</choices-outline>367<choice id="default" title="Eclipse SUMO {version}">368{refs}369</choice>370</installer-gui-script>371"""372distribution_path = os.path.join(temp_dir, "distribution.xml")373with open(distribution_path, "w") as f:374f.write(distribution_content)375376# Call productbuild377print(" - Calling productbuild")378productbuild_command = [379"productbuild",380"--distribution",381distribution_path,382"--package-path",383temp_dir,384"--resources",385resources_dir,386installer_pkg_file,387]388subprocess.run(productbuild_command, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)389390# Removing temporary build directory391print(" - Cleaning up")392shutil.rmtree(temp_dir)393394395def create_dmg(dmg_title, version, installer_pkg_path, installer_dmg_path):396397if os.path.exists(installer_dmg_path):398print(" - Removing existing disk image before creating a new disk image")399os.remove(installer_dmg_path)400401print(" - Preparing disk image folder")402dmg_prep_folder = tempfile.mkdtemp()403404# Copy the installer pkg405shutil.copy(installer_pkg_path, dmg_prep_folder)406407# Add the uninstall script408uninstall_script_path = os.path.join(dmg_prep_folder, "uninstall.command")409with open(uninstall_script_path, "w") as f:410f.write("""#!/bin/bash411echo "This will uninstall Eclipse SUMO and its components."412read -p "Are you sure? (y/N): " CONFIRM413if [[ ! "$CONFIRM" =~ ^[Yy]$ ]]; then414echo "Uninstallation aborted."415exit 0416fi417418# Request sudo privileges419osascript -e 'do shell script "sudo -v" with administrator privileges'420421# Define installed paths422FRAMEWORK="/Library/Frameworks/EclipseSUMO.framework"423APP1="/Applications/SUMO sumo-gui.app"424APP2="/Applications/SUMO netedit.app"425APP3="/Applications/SUMO Scenario Wizard.app"426427# Remove framework428if [ -d "$FRAMEWORK" ]; then429echo "Removing framework: $FRAMEWORK"430sudo rm -rf "$FRAMEWORK"431else432echo "Framework not found: $FRAMEWORK"433fi434435# Remove apps436for APP in "$APP1" "$APP2" "$APP3"; do437if [ -d "$APP" ]; then438echo "Removing application: $APP"439sudo rm -rf "$APP"440else441echo "Application not found: $APP"442fi443done444445echo "Eclipse SUMO has been successfully uninstalled!"446exit 0447""")448# Make the script executable449os.chmod(uninstall_script_path, 0o755)450451# Collect all files and add to the dmg452print(" - Collecting files and calculating file size")453files_to_store = []454total_size = 0455for root, _, files in os.walk(dmg_prep_folder):456for file in files:457files_to_store.append((os.path.join(root, file), file))458total_size += os.path.getsize(os.path.join(root, file))459460print(" - Building diskimage")461settings = {462"volume_name": f"Eclipse SUMO {version}",463"size": f"{total_size // 1024 * 1.2}K",464"files": files_to_store,465# FIXME: add background and badge466}467build_dmg(installer_dmg_path, dmg_title, settings=settings)468469print(" - Cleaning up")470shutil.rmtree(dmg_prep_folder)471472473def main():474base_id = "org.eclipse.sumo"475default_framework_name = "EclipseSUMO"476default_framework_long_name = "Eclipse SUMO"477version = get_pep440_version()478if ".post" in version:479version = "git"480default_pkg_name = f"sumo-{version}.pkg"481default_dmg_name = f"sumo-{version}.dmg"482483# Which launcher apps do we have?484app_list = [485(486"SUMO sumo-gui",487'exec "$SUMO_HOME/bin/sumo-gui" "$@" &',488default_framework_name,489f"{base_id}.apps.sumo-gui",490version,491"sumo-gui.icns",492"sumo-gui"493),494(495"SUMO netedit",496'exec "$SUMO_HOME/bin/netedit" "$@" &',497default_framework_name,498f"{base_id}.apps.netedit",499version,500"netedit.icns",501"netedit"502),503(504"SUMO Scenario Wizard",505(506"python $SUMO_HOME/tools/osmWebWizard.py ||"507"python3 $SUMO_HOME/tools/osmWebWizard.py &"508),509default_framework_name,510f"{base_id}.apps.scenario-wizard",511version,512"scenario-wizard.icns",513"scenario-wizard"514),515]516517# Parse and check the command line arguments518opts = parse_args(default_dmg_name, default_pkg_name)519520if opts.clean:521for d in (opts.framework_dir, opts.framework_pkg_dir, opts.apps_dir, opts.apps_pkg_dir,522os.path.dirname(opts.installer_pkg_file)):523shutil.rmtree(d, ignore_errors=True)524# Let's see what we need to do525if opts.all or opts.create_framework_dir:526if os.path.exists(opts.framework_dir):527print(f"Directory {opts.framework_dir} already exists. Aborting.")528sys.exit(1)529if not os.path.exists(opts.build_dir):530print(f"Error: build directory '{opts.build_dir}' does not exist.", file=sys.stderr)531sys.exit(1)532if not os.path.exists(os.path.join(opts.build_dir, "CMakeCache.txt")):533print(f"Error: directory '{opts.build_dir}' is not a build directory.", file=sys.stderr)534sys.exit(1)535536print(f"Creating {default_framework_name} framework directory: \"{opts.framework_dir}\"")537create_framework_dir(default_framework_name, default_framework_long_name, f"{base_id}.framework", version,538opts.build_dir, opts.framework_dir)539print(f"Successfully created {default_framework_name} framework directory")540541if opts.all or opts.create_framework_pkg:542if os.path.exists(opts.framework_pkg_dir):543print(f"Directory {opts.framework_pkg_dir} already exists. Aborting.")544sys.exit(1)545if not os.path.exists(opts.framework_dir):546print(f"Error: framework directory '{opts.framework_dir}' does not exist.", file=sys.stderr)547sys.exit(1)548549print(f"Creating {default_framework_name} framework *.pkg file")550print(f" - Using framework directory: \"{opts.framework_dir}\"")551_, pkg_name, _, _, pkg_size = create_framework_pkg(default_framework_name, f"{base_id}.framework", version,552opts.framework_dir, opts.framework_pkg_dir)553print(f"Successfully created \"{pkg_name}\" ({pkg_size / (1024 * 1024):.2f} MB)")554555if opts.all or opts.create_apps_dir:556if os.path.exists(opts.apps_dir):557print(f"Directory {opts.apps_dir} already exists. Aborting.")558sys.exit(1)559560print(f"Creating {default_framework_name} launcher apps directories")561os.makedirs(opts.apps_dir, exist_ok=False)562for app_name, app_binary, app_framework, app_id, app_ver, app_icons, app_folder in app_list:563app_dir = os.path.join(opts.apps_dir, app_folder)564print(f" - Building app directory for '{app_name}' in folder {app_dir}")565os.makedirs(app_dir)566icon_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..",567"build_config", "macos", "installer", app_icons)568create_app_dir(app_name, app_binary, app_framework, app_id, app_ver, icon_path, app_dir)569print(f" - Successfully created app directory for '{app_name}'")570571if opts.all or opts.create_apps_pkg:572if os.path.exists(opts.apps_pkg_dir):573print(f"Directory {opts.apps_pkg_dir} already exists. Aborting.")574sys.exit(1)575576print(f"Creating {default_framework_name} launcher app pkg files")577os.makedirs(opts.apps_pkg_dir, exist_ok=False)578579for app_name, app_binary, app_framework, app_id, app_ver, app_icons, app_folder in app_list:580app_dir = os.path.join(opts.apps_dir, app_folder)581_, pkg_name, _, _, pkg_size = create_app_pkg(app_name, app_id, app_ver, app_dir, opts.apps_pkg_dir)582print(f" - Created \"{pkg_name}\" ({pkg_size / (1024 * 1024):.2f} MB)")583584if opts.all or opts.create_installer_pkg:585if os.path.exists(os.path.dirname(opts.installer_pkg_file)):586print(f"Error: pkg output directory '{os.path.dirname(opts.installer_pkg_file)}' exists.",587file=sys.stderr)588sys.exit(1)589590# Create the output directory for the installer pkg591os.makedirs(os.path.dirname(opts.installer_pkg_file))592593print("Building installer pkg file")594# Where do we find our pkgs?595fw_pkg = [os.path.join(opts.framework_pkg_dir, f"{default_framework_name}-{version}.pkg"),596f"{base_id}.framework"]597app_pkgs = []598for app_name, app_binary, app_framework, app_id, app_ver, app_icons, app_folder in app_list:599app_pkgs.append([os.path.join(opts.apps_pkg_dir, f"Launcher-{app_name}-{version}.pkg"), app_id])600601# Build the installer pkg file602create_installer(fw_pkg, app_pkgs, version, opts.installer_pkg_file)603pkg_size = os.path.getsize(opts.installer_pkg_file)604605print(f"Installer pkg file created: \"{opts.installer_pkg_file}\" ({pkg_size / (1024 * 1024):.2f} MB)")606607if opts.all or opts.create_installer_dmg:608if not os.path.exists(os.path.dirname(opts.installer_dmg_file)):609print(f"Error: output directory '{os.path.dirname(opts.installer_dmg_file)}' does not exist.",610file=sys.stderr)611sys.exit(1)612613if not os.path.exists(opts.installer_pkg_file):614print(f"Error: installer pkg file '{opts.installer_pkg_file}' does not exist.",615file=sys.stderr)616sys.exit(1)617618print("Building installer disk image (dmg file)")619create_dmg(default_framework_long_name, version, opts.installer_pkg_file, opts.installer_dmg_file)620pkg_size = os.path.getsize(opts.installer_dmg_file)621print(f"Successfully built disk image: \"{opts.installer_dmg_file}\" ({pkg_size / (1024 * 1024):.2f} MB)")622623624if __name__ == "__main__":625main()626627628