Files
aufbau2csv/venv3_12/Lib/site-packages/cx_Freeze/command/bdist_mac.py

530 lines
19 KiB
Python

"""Implements the 'bdist_mac' commands (create macOS
app blundle).
"""
from __future__ import annotations
import os
import plistlib
import shutil
import subprocess
from pathlib import Path
from typing import ClassVar
from setuptools import Command
from cx_Freeze.common import normalize_to_list
from cx_Freeze.darwintools import (
apply_adhoc_signature,
change_load_reference,
isMachOFile,
)
from cx_Freeze.exception import OptionError
__all__ = ["bdist_mac"]
class bdist_mac(Command):
"""Create a Mac application bundle."""
description = "create a Mac application bundle"
plist_items: list[tuple[str, str]]
include_frameworks: list[str]
include_resources: list[str]
user_options: ClassVar[list[tuple[str, str | None, str]]] = [
("iconfile=", None, "Path to an icns icon file for the application."),
(
"qt-menu-nib=",
None,
"Location of qt_menu.nib folder for Qt "
"applications. Will be auto-detected by default.",
),
(
"bundle-name=",
None,
"File name for the bundle application "
"without the .app extension.",
),
(
"plist-items=",
None,
"A list of key-value pairs (type: list[tuple[str, str]]) to "
"be added to the app bundle Info.plist file.",
),
(
"custom-info-plist=",
None,
"File to be used as the Info.plist in "
"the app bundle. A basic one will be generated by default.",
),
(
"include-frameworks=",
None,
"A comma separated list of Framework "
"directories to include in the app bundle.",
),
(
"include-resources=",
None,
"A list of tuples of additional "
"files to include in the app bundle's resources directory, with "
"the first element being the source, and second the destination "
"file or directory name.",
),
(
"codesign-identity=",
None,
"The identity of the key to be used to sign the app bundle.",
),
(
"codesign-entitlements=",
None,
"The path to an entitlements file "
"to use for your application's code signature.",
),
(
"codesign-deep=",
None,
"Boolean for whether to codesign using the --deep option.",
),
(
"codesign-timestamp",
None,
"Boolean for whether to codesign using the --timestamp option.",
),
(
"codesign-resource-rules",
None,
"Plist file to be passed to "
"codesign's --resource-rules option.",
),
(
"absolute-reference-path=",
None,
"Path to use for all referenced "
"libraries instead of @executable_path.",
),
(
"codesign-verify",
None,
"Boolean to verify codesign of the .app bundle using the codesign "
"command",
),
(
"spctl-assess",
None,
"Boolean to verify codesign of the .app bundle using the spctl "
"command",
),
(
"codesign-strict=",
None,
"Boolean for whether to codesign using the --strict option.",
),
(
"codesign-options=",
None,
"Option flags to be embedded in the code signature",
),
]
def initialize_options(self) -> None:
self.list_options = [
"plist_items",
"include_frameworks",
"include_resources",
]
for option in self.list_options:
setattr(self, option, [])
self.absolute_reference_path = None
self.bundle_name = self.distribution.get_fullname()
self.codesign_deep = None
self.codesign_entitlements = None
self.codesign_identity = None
self.codesign_timestamp = None
self.codesign_strict = None
self.codesign_options = None
self.codesign_resource_rules = None
self.codesign_verify = None
self.spctl_assess = None
self.custom_info_plist = None
self.iconfile = None
self.qt_menu_nib = False
self.build_base = None
self.build_dir = None
def finalize_options(self) -> None:
# Make sure all options of multiple values are lists
for option in self.list_options:
setattr(self, option, normalize_to_list(getattr(self, option)))
for item in self.plist_items:
if not isinstance(item, tuple) or len(item) != 2:
msg = (
"Error, plist_items must be a list of key, value pairs "
"(list[tuple[str, str]]) (bad list item)."
)
raise OptionError(msg)
# Define the paths within the application bundle
self.set_undefined_options(
"build_exe",
("build_base", "build_base"),
("build_exe", "build_dir"),
)
self.bundle_dir = os.path.join(
self.build_base, f"{self.bundle_name}.app"
)
self.contents_dir = os.path.join(self.bundle_dir, "Contents")
self.bin_dir = os.path.join(self.contents_dir, "MacOS")
self.frameworks_dir = os.path.join(self.contents_dir, "Frameworks")
self.resources_dir = os.path.join(self.contents_dir, "Resources")
self.helpers_dir = os.path.join(self.contents_dir, "Helpers")
def create_plist(self) -> None:
"""Create the Contents/Info.plist file."""
# Use custom plist if supplied, otherwise create a simple default.
if self.custom_info_plist:
with open(self.custom_info_plist, "rb") as file:
contents = plistlib.load(file)
else:
contents = {
"CFBundleIconFile": "icon.icns",
"CFBundleDevelopmentRegion": "English",
"CFBundleIdentifier": self.bundle_name,
# Specify that bundle is an application bundle
"CFBundlePackageType": "APPL",
# Cause application to run in high-resolution mode by default
# (Without this, applications run from application bundle may
# be pixelated)
"NSHighResolutionCapable": "True",
}
# Ensure CFBundleExecutable is set correctly
contents["CFBundleExecutable"] = self.bundle_executable
# add custom items to the plist file
for key, value in self.plist_items:
contents[key] = value
with open(os.path.join(self.contents_dir, "Info.plist"), "wb") as file:
plistlib.dump(contents, file)
def set_absolute_reference_paths(self, path=None) -> None:
"""For all files in Contents/MacOS, set their linked library paths to
be absolute paths using the given path instead of @executable_path.
"""
if not path:
path = self.absolute_reference_path
files = os.listdir(self.bin_dir)
for filename in files:
filepath = os.path.join(self.bin_dir, filename)
# Skip some file types
if filepath[-1:] in ("txt", "zip") or os.path.isdir(filepath):
continue
out = subprocess.check_output(
("otool", "-L", filepath), encoding="utf_8"
)
for line in out.splitlines()[1:]:
lib = line.lstrip("\t").split(" (compat")[0]
if lib.startswith("@executable_path"):
replacement = lib.replace("@executable_path", path)
path, name = os.path.split(replacement)
# see if we provide the referenced file;
# if so, change the reference
if name in files:
change_load_reference(filepath, lib, replacement)
apply_adhoc_signature(filepath)
def find_qt_menu_nib(self) -> str | None:
"""Returns a location of a qt_menu.nib folder, or None if this is not
a Qt application.
"""
if self.qt_menu_nib:
return self.qt_menu_nib
if any(n.startswith("PyQt4.QtCore") for n in os.listdir(self.bin_dir)):
name = "PyQt4"
elif any(
n.startswith("PySide.QtCore") for n in os.listdir(self.bin_dir)
):
name = "PySide"
else:
return None
qtcore = __import__(name, fromlist=["QtCore"]).QtCore
libpath = str(
qtcore.QLibraryInfo.location(qtcore.QLibraryInfo.LibrariesPath)
)
for subpath in [
"QtGui.framework/Resources/qt_menu.nib",
"Resources/qt_menu.nib",
]:
path = os.path.join(libpath, subpath)
if os.path.exists(path):
return path
# Last resort: fixed paths (macports)
for path in [
"/opt/local/Library/Frameworks/QtGui.framework/Versions/"
"4/Resources/qt_menu.nib"
]:
if os.path.exists(path):
return path
print("Could not find qt_menu.nib")
msg = "Could not find qt_menu.nib"
raise OSError(msg)
def prepare_qt_app(self) -> None:
"""Add resource files for a Qt application. Should do nothing if the
application does not use QtCore.
"""
qt_conf = os.path.join(self.resources_dir, "qt.conf")
qt_conf_2 = os.path.join(self.resources_dir, "qt_bdist_mac.conf")
if os.path.exists(qt_conf_2):
self.execute(
shutil.move,
(qt_conf_2, qt_conf),
msg=f"moving {qt_conf_2} -> {qt_conf}",
)
nib_locn = self.find_qt_menu_nib()
if nib_locn is None:
return
# Copy qt_menu.nib
self.copy_tree(
nib_locn, os.path.join(self.resources_dir, "qt_menu.nib")
)
# qt.conf needs to exist, but needn't have any content
if not os.path.exists(qt_conf):
with open(qt_conf, "wb"):
pass
def run(self) -> None:
self.run_command("build_exe")
# Remove App if it already exists
# ( avoids confusing issues where prior builds persist! )
if os.path.exists(self.bundle_dir):
self.execute(
shutil.rmtree,
(self.bundle_dir,),
msg=f"staging - removed existing '{self.bundle_dir}'",
)
# Find the executable name
executable = self.distribution.executables[0].target_name
_, self.bundle_executable = os.path.split(executable)
print(f"Executable name: {self.build_dir}/{executable}")
# Build the app directory structure
self.mkpath(self.bin_dir) # /MacOS
self.mkpath(self.frameworks_dir) # /Frameworks
self.mkpath(self.resources_dir) # /Resources
# Copy the full build_exe to Contents/Resources
self.copy_tree(self.build_dir, self.resources_dir)
# Move only executables in Contents/Resources to Contents/MacOS
for executable in self.distribution.executables:
source = os.path.join(self.resources_dir, executable.target_name)
target = os.path.join(self.bin_dir, executable.target_name)
self.move_file(source, target)
# Make symlink between folders under Resources such as lib and others
# specified by the user in include_files and Contents/MacOS so we can
# use non-relative reference paths to pass codesign...
for filename in os.listdir(self.resources_dir):
target = os.path.join(self.resources_dir, filename)
if os.path.isdir(target):
origin = os.path.join(self.bin_dir, filename)
relative_reference = os.path.relpath(target, self.bin_dir)
self.execute(
os.symlink,
(relative_reference, origin, True),
msg=f"linking {origin} -> {relative_reference}",
)
# Copy the icon
if self.iconfile:
self.copy_file(
self.iconfile, os.path.join(self.resources_dir, "icon.icns")
)
# Copy in Frameworks
for framework in self.include_frameworks:
self.copy_tree(
framework,
os.path.join(self.frameworks_dir, os.path.basename(framework)),
)
# Copy in Resources
for resource, destination in self.include_resources:
if os.path.isdir(resource):
self.copy_tree(
resource, os.path.join(self.resources_dir, destination)
)
else:
parent_dirs = os.path.dirname(
os.path.join(self.resources_dir, destination)
)
os.makedirs(parent_dirs, exist_ok=True)
self.copy_file(
resource, os.path.join(self.resources_dir, destination)
)
# Create the Info.plist file
self.execute(self.create_plist, (), msg="creating Contents/Info.plist")
# Make library references absolute if enabled
if self.absolute_reference_path:
self.execute(
self.set_absolute_reference_paths,
(),
msg="set absolute reference path "
f"'{self.absolute_reference_path}",
)
# For a Qt application, run some tweaks
self.execute(self.prepare_qt_app, ())
# Move Contents/Resources/share/*.app to Contents/Helpers
share_dir = os.path.join(self.resources_dir, "share")
if os.path.isdir(share_dir):
for filename in os.listdir(share_dir):
if not filename.endswith(".app"):
continue
# create /Helpers only if required
self.mkpath(self.helpers_dir)
source = os.path.join(share_dir, filename)
target = os.path.join(self.helpers_dir, filename)
self.execute(
shutil.move,
(source, target),
msg=f"moving {source} -> {target}",
)
if os.path.isdir(target):
origin = os.path.join(target, "Contents", "MacOS", "lib")
relative_reference = os.path.relpath(
os.path.join(self.resources_dir, "lib"),
os.path.join(target, "Contents", "MacOS"),
)
self.execute(
os.symlink,
(relative_reference, origin, True),
msg=f"linking {origin} -> {relative_reference}",
)
# Sign the app bundle if a key is specified
self.execute(
self._codesign,
(self.bundle_dir,),
msg=f"sign: '{self.bundle_dir}'",
)
def _codesign(self, root_path) -> None:
"""Run codesign on all .so, .dylib and binary files in reverse order.
Signing from inside-out.
"""
if not self.codesign_identity:
return
binaries_to_sign = []
# Identify all binary files
for dirpath, _, filenames in os.walk(root_path):
for filename in filenames:
full_path = Path(os.path.join(dirpath, filename))
if isMachOFile(full_path):
binaries_to_sign.append(full_path)
# Sort files by depth, so we sign the deepest files first
binaries_to_sign.sort(key=lambda x: str(x).count(os.sep), reverse=True)
for binary_path in binaries_to_sign:
self._codesign_file(binary_path, self._get_sign_args())
self._verify_signature()
print("Finished .app signing")
def _get_sign_args(self) -> list[str]:
signargs = ["codesign", "--sign", self.codesign_identity, "--force"]
if self.codesign_timestamp:
signargs.append("--timestamp")
if self.codesign_strict:
signargs.append(f"--strict={self.codesign_strict}")
if self.codesign_deep:
signargs.append("--deep")
if self.codesign_options:
signargs.append("--options")
signargs.append(self.codesign_options)
if self.codesign_entitlements:
signargs.append("--entitlements")
signargs.append(self.codesign_entitlements)
return signargs
def _codesign_file(self, file_path, sign_args) -> None:
print(f"Signing file: {file_path}")
sign_args.append(file_path)
subprocess.run(sign_args, check=False)
def _verify_signature(self) -> None:
if self.codesign_verify:
verify_args = [
"codesign",
"-vvv",
"--deep",
"--strict",
self.bundle_dir,
]
print("Running codesign verification")
result = subprocess.run(
verify_args, capture_output=True, text=True, check=False
)
print("ExitCode:", result.returncode)
print(" stdout:", result.stdout)
print(" stderr:", result.stderr)
if self.spctl_assess:
spctl_args = [
"spctl",
"--assess",
"--raw",
"--verbose=10",
"--type",
"exec",
self.bundle_dir,
]
try:
completed_process = subprocess.run(
spctl_args,
check=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
)
print(
"spctl command's output: "
f"{completed_process.stdout.decode()}"
)
except subprocess.CalledProcessError as error:
print(f"spctl check got an error: {error.stdout.decode()}")
raise