Path: blob/main/tools/build_config/buildMacOSInstaller.py
169674 views
#!/usr/bin/env python1# -*- coding: utf-8 -*-2# Eclipse SUMO, Simulation of Urban MObility; see https://eclipse.dev/sumo3# Copyright (C) 2008-2025 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 re23import shutil24import subprocess25import sys26import tempfile27from glob import iglob2829sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))30from sumolib.options import ArgumentParser # noqa3132from build_config.version import get_pep440_version # noqa3334try:35from delocate.cmd.delocate_path import delocate_path36except ImportError:37print("Error: delocate module is not installed. Please install it using 'pip install delocate'.")38sys.exit(1)3940try:41from dmgbuild.core import build_dmg42except ImportError:43print("Error: dmgbuild module is not installed. Please install it using 'pip install dmgbuild'.")44sys.exit(1)454647def transform_pep440_version(version):48post_pattern = re.compile(r"^(.*)\.post\d+$")49match = post_pattern.match(version)50if match:51return f"{match.group(1)}-git"52else:53return version545556def parse_args(def_dmg_name, def_pkg_name):57def_build_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..", "sumo-build"))58def_output_fw_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..", "framework"))59def_output_apps_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..", "apps"))60def_output_fw_pkg_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..", "framework-pkg"))61def_output_apps_pkg_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..", "apps-pkg"))62def_output_pkg_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..",63"installer", def_pkg_name))64def_output_dmg_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", def_dmg_name))6566op = ArgumentParser(description="Build an installer for macOS (dmg file)")6768# We can set one these actions exclusively69action_group = op.add_mutually_exclusive_group("Actions")70action_group.add_argument("--create-framework-dir", dest="create_framework_dir", action="store_true")71action_group.add_argument("--create-framework-pkg", dest="create_framework_pkg", action="store_true")72action_group.add_argument("--create-apps-dir", dest="create_apps_dir", action="store_true")73action_group.add_argument("--create-apps-pkg", dest="create_apps_pkg", action="store_true")74action_group.add_argument("--create-installer-pkg", dest="create_installer_pkg", action="store_true")75action_group.add_argument("--create-installer-dmg", dest="create_installer_dmg", action="store_true")7677# ... and supply some arguments78op.add_argument("--build-dir", dest="build_dir", default=def_build_dir)79op.add_argument("--framework-dir", dest="framework_dir", default=def_output_fw_dir)80op.add_argument("--framework-pkg-dir", dest="framework_pkg_dir", default=def_output_fw_pkg_dir)81op.add_argument("--apps-dir", dest="apps_dir", default=def_output_apps_dir)82op.add_argument("--apps-pkg-dir", dest="apps_pkg_dir", default=def_output_apps_pkg_dir)83op.add_argument("--installer-pkg-file", dest="installer_pkg_file", default=def_output_pkg_path)84op.add_argument("--installer-dmg-file", dest="installer_dmg_file", default=def_output_dmg_path)8586args = op.parse_args()8788# Validate the basic argument logic89if args.build_dir is not None and args.create_framework_dir is None:90print("Error: build directory can only be set when creating the framework directory.", file=sys.stderr)91sys.exit(1)9293if args.framework_pkg_dir is not None and args.create_framework_pkg is None:94print("Error: framework pkg directory can only be set when creating the framework pkg.", file=sys.stderr)95sys.exit(1)9697if args.apps_dir is not None and args.create_apps_dir is None:98print("Error: apps directory can only be set when creating the apps.", file=sys.stderr)99sys.exit(1)100101return args102103104def create_framework_dir(name, longname, pkg_id, version, sumo_build_directory, framework_output_dir):105# Create the directory structure for the framework bundle106# see: https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPFrameworks/Concepts/FrameworkAnatomy.html # noqa107#108# EclipseSUMO.framework/109# ├── EclipseSUMO --> Versions/Current/EclipseSUMO110# ├── Resources --> Versions/Current/Resources111# └── Versions112# ├── v1_20_0113# │ ├── EclipseSUMO114# │ └── Resources115# │ └── Info.plist116# └── Current --> v_1_20_0117118print(" - Creating directory structure")119os.makedirs(framework_output_dir, exist_ok=False)120121framework_dir = os.path.join(framework_output_dir, f"{name}.framework")122version_dir = os.path.join(framework_dir, f"Versions/{version}")123os.makedirs(os.path.join(version_dir, name), exist_ok=True)124os.makedirs(os.path.join(version_dir, "Resources"), exist_ok=True)125126os.symlink(f"{version}/", os.path.join(framework_dir, "Versions/Current"), True)127os.symlink(f"Versions/Current/{name}/", os.path.join(framework_dir, name), True)128os.symlink("Versions/Current/Resources/", os.path.join(framework_dir, "Resources"), True)129130# Create the Info.plist file131plist_file = os.path.join(version_dir, "Resources", "Info.plist")132print(" - Creating properties list")133plist_content = {134"CFBundleExecutable": longname,135"CFBundleIdentifier": pkg_id,136"CFBundleName": longname,137"CFBundleVersion": version,138"CFBundleShortVersionString": version,139}140with open(plist_file, "wb") as f:141plistlib.dump(plist_content, f)142143# Copy files from the current repository clone to version_dir/EclipseSUMO144print(" - Installing Eclipse SUMO build")145cmake_install_command = [146"cmake",147"--install",148sumo_build_directory,149"--prefix",150os.path.join(version_dir, name),151]152subprocess.run(cmake_install_command, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)153154# Remove unneeded libsumo and libtraci folders from the tools directory155# (If the user needs libsumo or libtraci, they should install these tools with pip)156print(" - Removing libsumo and libtraci folders from tools")157shutil.rmtree(os.path.join(version_dir, name, "share", "sumo", "tools", "libsumo"), ignore_errors=True)158shutil.rmtree(os.path.join(version_dir, name, "share", "sumo", "tools", "libtraci"), ignore_errors=True)159160# We need to add a symlink to the binary folder to have the same folder structure161os.symlink("../../bin", os.path.join(version_dir, name, "share", "sumo", "bin"))162163# Determine library dependencies164print(" - Delocating binaries and libraries")165os.chdir(os.path.join(version_dir, name))166167# - libraries that landed in the lib folder need to be delocated as well168lib_dir = os.path.join(version_dir, name, "lib")169bin_dir = os.path.join(version_dir, name, "bin")170for pattern in ("*.jnilib", "*.dylib"):171for file in iglob(os.path.join(lib_dir, pattern)):172file_name = os.path.basename(file)173shutil.move(os.path.join(lib_dir, file_name), os.path.join(bin_dir, file_name))174175# Start the delocation of the libraries and binaries176delocate_path("./bin", lib_filt_func=None, lib_path="./lib", sanitize_rpaths=True)177178# - and we need to move them back to the lib folder179for pattern in ("*.jnilib", "*.dylib"):180for file in iglob(os.path.join(bin_dir, pattern)):181file_name = os.path.basename(file)182shutil.move(os.path.join(bin_dir, file_name), os.path.join(lib_dir, file_name))183184# Add proj db files from /opt/homebrew/Cellar/proj/<X.Y.Z>/share/proj185print(" - Copying additional files (e.g. proj.db)")186proj_dir = "/opt/homebrew/Cellar/proj"187proj_file_list = ["GL27", "ITRF2000", "ITRF2008", "ITRF2014", "nad.lst", "nad27", "nad83", "other.extra", "proj.db",188"proj.ini", "projjson.schema.json", "triangulation.schema.json", "world", "CH", "deformation_model.schema.json"] # noqa189dest_dir = os.path.join(version_dir, name, "share", "proj")190if os.path.exists(proj_dir):191first_dir = next(iter(os.listdir(proj_dir)), None)192if first_dir:193source_dir = os.path.join(proj_dir, first_dir, "share/proj")194if os.path.exists(source_dir):195os.makedirs(dest_dir, exist_ok=True)196for file in proj_file_list:197shutil.copy2(os.path.join(source_dir, file), os.path.join(dest_dir, file))198199200def create_framework_pkg(name, pkg_id, version, framework_dir, framework_pkg_dir):201# Build the framework package202os.makedirs(framework_pkg_dir, exist_ok=False)203pkg_name = f"{name}-{version}.pkg"204pkg_path = os.path.join(framework_pkg_dir, pkg_name)205pkg_build_command = [206"pkgbuild",207"--root",208os.path.join(framework_dir, f"{name}.framework"),209"--identifier",210pkg_id,211"--version",212version,213"--install-location",214f"/Library/Frameworks/{name}.framework",215f"{pkg_path}",216]217print(f" - Using the call {pkg_build_command}")218print(f" - Calling pkgbuild to create \"{pkg_path}\"")219subprocess.run(pkg_build_command, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)220pkg_size = os.path.getsize(pkg_path)221return name, pkg_name, pkg_id, pkg_path, pkg_size222223224def create_app_dir(app_name, exec_call, framework_name, pkg_id, version, icns_path, app_output_dir):225# Example app structure:226# SUMO-GUI.app227# └── Contents228# └── Info.plist229# ├── MacOS230# │ └── SUMO-GUI231# └── Resources232# └── iconsfile.icns233234print(" . Creating directory structure")235os.makedirs(os.path.join(app_output_dir, f"{app_name}.app", "Contents", "MacOS"))236os.makedirs(os.path.join(app_output_dir, f"{app_name}.app", "Contents", "Resources"))237238print(" . Creating launcher")239launcher_content = f"""#!/bin/bash240export SUMO_HOME="/Library/Frameworks/{framework_name}.framework/Versions/Current/{framework_name}/share/sumo"241{exec_call}242"""243launcher_path = os.path.join(app_output_dir, f"{app_name}.app", "Contents", "MacOS", app_name)244with open(launcher_path, "w") as launcher:245launcher.write(launcher_content)246os.chmod(launcher_path, 0o755)247248# Copy the icons249print(" . Copying icons")250shutil.copy(icns_path, os.path.join(app_output_dir, f"{app_name}.app", "Contents", "Resources", "iconfile.icns"))251252# Create plist file253print(" . Creating properties file")254plist_file = os.path.join(app_output_dir, f"{app_name}.app", "Contents", "Info.plist")255plist_content = {256"CFBundleExecutable": app_name,257"CFBundleIdentifier": pkg_id,258"CFBundleName": app_name,259"CFBundleVersion": version,260"CFBundleShortVersionString": version,261"CFBundleIconFile": "iconfile.icns",262}263with open(plist_file, "wb") as f:264plistlib.dump(plist_content, f)265266267def create_app_pkg(app_name, pkg_id, version, app_dir, apps_pkg_dir):268pkg_name = f"Launcher-{app_name}-{version}.pkg"269pkg_path = os.path.join(apps_pkg_dir, pkg_name)270pkg_build_command = [271"pkgbuild",272"--root",273os.path.join(app_dir, f"{app_name}.app"),274"--identifier",275pkg_id,276"--version",277version,278"--install-location",279f"/Applications/{app_name}.app",280f"{pkg_path}",281]282subprocess.run(pkg_build_command, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)283pkg_size = os.path.getsize(pkg_path)284return app_name, pkg_name, pkg_id, pkg_path, pkg_size285286287def create_installer(framework_pkg, apps_pkg, version, installer_pkg_file):288""""Creates the installer package289290framework_pkg: framework info [framework_path, framework_id]291apps_pkg: apps info [[app1_path, app1_id], [app2_path, app2_id], ...]292id: id of the pkg-file for the installer293version: 1.20.0294installer_pkg_file: name of the output pkg file295"""296297# Create a temporary directory to assemble everything for the installer298temp_dir = tempfile.mkdtemp()299300# Copy the framework pkg file and the launcher apps pkg files301shutil.copy(framework_pkg[0], temp_dir)302for app_pkg in apps_pkg:303shutil.copy(app_pkg[0], temp_dir)304305# Add license, background and other nice stuff306resources_dir = os.path.join(temp_dir, "Resources")307os.makedirs(resources_dir)308sumo_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..")309sumo_data_installer_dir = os.path.join(sumo_dir, "build_config", "macos", "installer")310shutil.copy(os.path.join(sumo_data_installer_dir, "background.png"), resources_dir)311shutil.copy(os.path.join(sumo_dir, "LICENSE"), os.path.join(resources_dir, "LICENSE.txt"))312313# Create conclusion.html in the installer resources folder314with open(os.path.join(resources_dir, "conclusion.html"), "w") as file:315file.write("""<!DOCTYPE html>316<html lang="en">317<head>318<meta charset="UTF-8">319<style>320body {321font-family: Helvetica;322font-size: 14px;323}324</style>325</head>326<body>327<div>328<h4>Important:</h4>329<ul>330<li>331For applications with a graphical user interface to function properly, please ensure332you have <b>XQuartz</b> installed.333It can be obtained from: <a href="https://www.xquartz.org" target="_blank">XQuartz</a>.334</li>335<li>336You may need to install Python 3, if it is not installed yet. Python is required for the337Scenario Wizard and other tools.338</li>339<li>340If you intend to use SUMO from the command line, please remember to set341the <b>SUMO_HOME</b> environment variable and add it to the <b>PATH</b> variable.342<br>343For more details, visit the344<a href="https://sumo.dlr.de/docs/Installing/index.html#macos" target="_blank">345SUMO macOS installation guide346</a>.347</li>348</ul>349</p>350<p>For support options, including the "sumo-user" mailing list, please visit:351<a href="https://eclipse.dev/sumo/contact/" target="_blank">SUMO Contact</a>.352</p>353</div>354</body>355</html>356""")357358# Create distribution.xml359print(" - Creating distribution.xml")360size = os.path.getsize(framework_pkg[0]) // 1024361path = os.path.basename(framework_pkg[0])362refs = f" <pkg-ref id='{framework_pkg[1]}' version='{version}' installKBytes='{size}'>{path}</pkg-ref>"363364for app_pkg in apps_pkg:365size = os.path.getsize(app_pkg[0]) // 1024366path = os.path.basename(app_pkg[0])367refs += f"\n <pkg-ref id='{app_pkg[1]}' version='{version}' installKBytes='{size}'>{path}</pkg-ref>"368369# See: https://developer.apple.com/library/archive/documentation/370# DeveloperTools/Reference/DistributionDefinitionRef/Chapters/Distribution_XML_Ref.html371distribution_content = f"""<?xml version="1.0" encoding="utf-8"?>372<installer-gui-script minSpecVersion="2">373<title>Eclipse SUMO</title>374<allowed-os-versions><os-version min="10.14"/></allowed-os-versions>375<license file="LICENSE.txt"/>376<background file="background.png" alignment="bottomleft" mime-type="image/png" scaling="none" />377<conclusion file="conclusion.html" mime-type="text/html"/>378<options customize="allow" require-scripts="false" rootVolumeOnly="true" hostArchitectures="arm64"/>379<choices-outline>380<line choice="default"/>381</choices-outline>382<choice id="default" title="Eclipse SUMO {version}">383{refs}384</choice>385</installer-gui-script>386"""387distribution_path = os.path.join(temp_dir, "distribution.xml")388with open(distribution_path, "w") as f:389f.write(distribution_content)390391# Call productbuild392print(" - Calling productbuild")393productbuild_command = [394"productbuild",395"--distribution",396distribution_path,397"--package-path",398temp_dir,399"--resources",400resources_dir,401installer_pkg_file,402]403subprocess.run(productbuild_command, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)404405# Removing temporary build directory406print(" - Cleaning up")407shutil.rmtree(temp_dir)408409410def create_dmg(dmg_title, version, installer_pkg_path, installer_dmg_path):411412if os.path.exists(installer_dmg_path):413print(" - Removing existing disk image before creating a new disk image")414os.remove(installer_dmg_path)415416print(" - Preparing disk image folder")417dmg_prep_folder = tempfile.mkdtemp()418419# Copy the installer pkg420shutil.copy(installer_pkg_path, dmg_prep_folder)421422# Add the uninstall script423uninstall_script_path = os.path.join(dmg_prep_folder, "uninstall.command")424with open(uninstall_script_path, "w") as f:425f.write("""#!/bin/bash426echo "This will uninstall Eclipse SUMO and its components."427read -p "Are you sure? (y/N): " CONFIRM428if [[ ! "$CONFIRM" =~ ^[Yy]$ ]]; then429echo "Uninstallation aborted."430exit 0431fi432433# Request sudo privileges434osascript -e 'do shell script "sudo -v" with administrator privileges'435436# Define installed paths437FRAMEWORK="/Library/Frameworks/EclipseSUMO.framework"438APP1="/Applications/SUMO sumo-gui.app"439APP2="/Applications/SUMO netedit.app"440APP3="/Applications/SUMO Scenario Wizard.app"441442# Remove framework443if [ -d "$FRAMEWORK" ]; then444echo "Removing framework: $FRAMEWORK"445sudo rm -rf "$FRAMEWORK"446else447echo "Framework not found: $FRAMEWORK"448fi449450# Remove apps451for APP in "$APP1" "$APP2" "$APP3"; do452if [ -d "$APP" ]; then453echo "Removing application: $APP"454sudo rm -rf "$APP"455else456echo "Application not found: $APP"457fi458done459460echo "Eclipse SUMO has been successfully uninstalled!"461exit 0462""")463# Make the script executable464os.chmod(uninstall_script_path, 0o755)465466# Collect all files and add to the dmg467print(" - Collecting files and calculating file size")468files_to_store = []469total_size = 0470for root, _, files in os.walk(dmg_prep_folder):471for file in files:472files_to_store.append((os.path.join(root, file), file))473total_size += os.path.getsize(os.path.join(root, file))474475print(" - Building diskimage")476settings = {477"volume_name": f"Eclipse SUMO {version}",478"size": f"{total_size // 1024 * 1.2}K",479"files": files_to_store,480# FIXME: add background and badge481}482build_dmg(installer_dmg_path, dmg_title, settings=settings)483484print(" - Cleaning up")485shutil.rmtree(dmg_prep_folder)486487488def main():489base_id = "org.eclipse.sumo"490default_framework_name = "EclipseSUMO"491default_framework_long_name = "Eclipse SUMO"492version = transform_pep440_version(get_pep440_version())493default_pkg_name = f"sumo-{version}.pkg"494default_dmg_name = f"sumo-{version}.dmg"495496# Which launcher apps do we have?497app_list = [498(499"SUMO sumo-gui",500'exec "$SUMO_HOME/bin/sumo-gui" "$@" &',501default_framework_name,502f"{base_id}.apps.sumo-gui",503version,504"sumo-gui.icns",505"sumo-gui"506),507(508"SUMO netedit",509'exec "$SUMO_HOME/bin/netedit" "$@" &',510default_framework_name,511f"{base_id}.apps.netedit",512version,513"netedit.icns",514"netedit"515),516(517"SUMO Scenario Wizard",518(519"python $SUMO_HOME/tools/osmWebWizard.py ||"520"python3 $SUMO_HOME/tools/osmWebWizard.py &"521),522default_framework_name,523f"{base_id}.apps.scenario-wizard",524version,525"scenario-wizard.icns",526"scenario-wizard"527),528]529530# Parse and check the command line arguments531opts = parse_args(default_dmg_name, default_pkg_name)532533# Let's see what we need to do534if opts.create_framework_dir:535if os.path.exists(opts.framework_dir):536print(f"Directory {opts.framework_dir} already exists. Aborting.")537sys.exit(1)538if not os.path.exists(opts.build_dir):539print(f"Error: build directory '{opts.build_dir}' does not exist.", file=sys.stderr)540sys.exit(1)541if not os.path.exists(os.path.join(opts.build_dir, "CMakeCache.txt")):542print(f"Error: directory '{opts.build_dir}' is not a build directory.", file=sys.stderr)543sys.exit(1)544545print(f"Creating {default_framework_name} framework directory: \"{opts.framework_dir}\"")546create_framework_dir(default_framework_name, default_framework_long_name, f"{base_id}.framework", version,547opts.build_dir, opts.framework_dir)548print(f"Successfully created {default_framework_name} framework directory")549550elif opts.create_framework_pkg:551if os.path.exists(opts.framework_pkg_dir):552print(f"Directory {opts.framework_pkg_dir} already exists. Aborting.")553sys.exit(1)554if not os.path.exists(opts.framework_dir):555print(f"Error: framework directory '{opts.framework_dir}' does not exist.", file=sys.stderr)556sys.exit(1)557558print(f"Creating {default_framework_name} framework *.pkg file")559print(f" - Using framework directory: \"{opts.framework_dir}\"")560_, pkg_name, _, _, pkg_size = create_framework_pkg(default_framework_name, f"{base_id}.framework", version,561opts.framework_dir, opts.framework_pkg_dir)562print(f"Successfully created \"{pkg_name}\" ({pkg_size / (1024 * 1024):.2f} MB)")563564elif opts.create_apps_dir:565if os.path.exists(opts.apps_dir):566print(f"Directory {opts.apps_dir} already exists. Aborting.")567sys.exit(1)568569print(f"Creating {default_framework_name} launcher apps directories")570os.makedirs(opts.apps_dir, exist_ok=False)571for app_name, app_binary, app_framework, app_id, app_ver, app_icons, app_folder in app_list:572app_dir = os.path.join(opts.apps_dir, app_folder)573print(f" - Building app directory for '{app_name}' in folder {app_dir}")574os.makedirs(app_dir)575icon_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..",576"build_config", "macos", "installer", app_icons)577create_app_dir(app_name, app_binary, app_framework, app_id, app_ver, icon_path, app_dir)578print(f" - Successfully created app directory for '{app_name}'")579580elif opts.create_apps_pkg:581if os.path.exists(opts.apps_pkg_dir):582print(f"Directory {opts.apps_pkg_dir} already exists. Aborting.")583sys.exit(1)584585print(f"Creating {default_framework_name} launcher app pkg files")586os.makedirs(opts.apps_pkg_dir, exist_ok=False)587588for app_name, app_binary, app_framework, app_id, app_ver, app_icons, app_folder in app_list:589app_dir = os.path.join(opts.apps_dir, app_folder)590_, pkg_name, _, _, pkg_size = create_app_pkg(app_name, app_id, app_ver, app_dir, opts.apps_pkg_dir)591print(f" - Created \"{pkg_name}\" ({pkg_size / (1024 * 1024):.2f} MB)")592593elif opts.create_installer_pkg:594if os.path.exists(os.path.dirname(opts.installer_pkg_file)):595print(f"Error: pkg output directory '{os.path.dirname(opts.installer_pkg_file)}' exists.",596file=sys.stderr)597sys.exit(1)598599# Create the output directory for the installer pkg600os.makedirs(os.path.dirname(opts.installer_pkg_file))601602print("Building installer pkg file")603# Where do we find our pkgs?604fw_pkg = [os.path.join(opts.framework_pkg_dir, f"{default_framework_name}-{version}.pkg"),605f"{base_id}.framework"]606app_pkgs = []607for app_name, app_binary, app_framework, app_id, app_ver, app_icons, app_folder in app_list:608app_pkgs.append([os.path.join(opts.apps_pkg_dir, f"Launcher-{app_name}-{version}.pkg"), app_id])609610# Build the installer pkg file611create_installer(fw_pkg, app_pkgs, version, opts.installer_pkg_file)612pkg_size = os.path.getsize(opts.installer_pkg_file)613614print(f"Installer pkg file created: \"{opts.installer_pkg_file}\" ({pkg_size / (1024 * 1024):.2f} MB)")615616elif opts.create_installer_dmg:617if not os.path.exists(os.path.dirname(opts.installer_dmg_file)):618print(f"Error: output directory '{os.path.dirname(opts.installer_dmg_file)}' does not exist.",619file=sys.stderr)620sys.exit(1)621622if not os.path.exists(opts.installer_pkg_file):623print(f"Error: installer pkg file '{opts.installer_pkg_file}' does not exist.",624file=sys.stderr)625sys.exit(1)626627print("Building installer disk image (dmg file)")628create_dmg(default_framework_long_name, version, opts.installer_pkg_file, opts.installer_dmg_file)629pkg_size = os.path.getsize(opts.installer_dmg_file)630print(f"Successfully built disk image: \"{opts.installer_dmg_file}\" ({pkg_size / (1024 * 1024):.2f} MB)")631632633if __name__ == "__main__":634main()635636637