Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Builds pass macOS notarization #2025

Merged
merged 26 commits into from
Sep 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
e2cdf0a
Move output location of license file to resources
TechnicalPirate Aug 24, 2023
240d298
Force signing
TechnicalPirate Aug 24, 2023
abb4d66
Delete Existing .app dir before build
Aug 24, 2023
549ed95
Change output to /Resources
TechnicalPirate Aug 25, 2023
69cbf9c
Changed to kat-assemble codesign for verification reasons
TechnicalPirate Aug 25, 2023
8dcf437
Trying an idea
TechnicalPirate Aug 25, 2023
cf9e533
Removed repeated arg (-vvv is --verbose)
TechnicalPirate Aug 25, 2023
4f7c694
WIP reimplementation of code sign
TechnicalPirate Aug 30, 2023
5ccee47
Changed to capture un-extensioned files
TechnicalPirate Aug 30, 2023
deeb3a0
doh! recursion is important
TechnicalPirate Aug 30, 2023
ad5b51e
Hookup options
TechnicalPirate Aug 31, 2023
e23bc76
Test recursive codesign
Aug 31, 2023
36cf1f0
Added string cast
Aug 31, 2023
f204621
Whitespace fix
Aug 31, 2023
20e44c0
Split out verify_signature
Aug 31, 2023
0dfec60
cleanup
Aug 31, 2023
d16ffd7
Added information about new options for codesign.
Aug 31, 2023
b4de221
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Aug 31, 2023
9a225f2
fix html build
marcelotduarte Sep 1, 2023
3304c8f
make ruff and pylint happy
marcelotduarte Sep 1, 2023
a764fd0
Rename isMachOFile for public access
Sep 1, 2023
2d158ad
use isMachOFile to determine binaries to sign
Sep 1, 2023
7cf6c66
Updated sample with codesign options
Sep 1, 2023
c5c842c
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 1, 2023
ad7442d
Review feedback
Sep 4, 2023
e3ffaff
Merge remote-tracking branch 'origin/feature/simple_restructure' into…
Sep 4, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
180 changes: 162 additions & 18 deletions cx_Freeze/command/bdist_mac.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import plistlib
import shutil
import subprocess
from pathlib import Path

from setuptools import Command

Expand All @@ -17,6 +18,7 @@
changeLoadReference,
)

from ..darwintools import isMachOFile
from ..exception import OptionError

__all__ = ["BdistDMG", "BdistMac"]
Expand Down Expand Up @@ -176,6 +178,11 @@ class BdistMac(Command):
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,
Expand All @@ -188,6 +195,28 @@ class BdistMac(Command):
"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):
Expand All @@ -204,7 +233,12 @@ def initialize_options(self):
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
Expand Down Expand Up @@ -298,6 +332,7 @@ def set_relative_reference_paths(self, build_dir: str, bin_dir: str):

for darwin_file in self.darwin_tracker:
# get the relative path to darwin_file in build directory
print(f"Setting relative_reference_path for: {darwin_file}")
relative_copy_dest = os.path.relpath(
darwin_file.getBuildPath(), build_dir
)
Expand All @@ -306,7 +341,6 @@ def set_relative_reference_paths(self, build_dir: str, bin_dir: str):
# bundle. This is the file that needs to have its dynamic load
# references updated.
file_path_in_bin_dir = os.path.join(bin_dir, relative_copy_dest)

# for each file that this darwin_file references, update the
# reference as necessary; if the file is copied into the binary
# package, change the reference to be relative to @executable_path
Expand Down Expand Up @@ -400,19 +434,46 @@ def run(self):
)
self.contents_dir = os.path.join(self.bundle_dir, "Contents")
self.resources_dir = os.path.join(self.contents_dir, "Resources")
self.resources_lib_dir = os.path.join(
self.contents_dir, "Resources", "lib"
)
self.bin_dir = os.path.join(self.contents_dir, "MacOS")
self.frameworks_dir = os.path.join(self.contents_dir, "Frameworks")

# Remove App if it already exists
# ( avoids confusing issues where prior builds persist! )
if os.path.exists(self.bundle_dir):
shutil.rmtree(self.bundle_dir)
print(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)

# Build the app directory structure
self.mkpath(self.resources_dir)
self.mkpath(self.bin_dir)
self.mkpath(self.frameworks_dir)
self.mkpath(self.resources_dir) # /Resources
self.mkpath(self.resources_lib_dir) # /Resources/lib
self.mkpath(self.bin_dir) # /MacOS
self.mkpath(self.frameworks_dir) # /Frameworks

# Copy to relevent subfolders
print(f"Executable name: {executable} - {build_exe.build_exe}")
self.copy_tree(build_exe.build_exe, self.bin_dir)
self.copy_tree(
os.path.join(self.bin_dir, "lib"), self.resources_lib_dir
)
shutil.rmtree(os.path.join(self.bin_dir, "lib"))
# Make symlink between contents/MacOS and resources/lib so we can use
# none-relative reference paths in order to pass codesign...
origin = os.path.join(self.bin_dir, "lib")
relative_reference = os.path.relpath(
self.resources_lib_dir, self.bin_dir
)
print(
"Creating symlink - "
f"Target: {origin} <-> Source: {relative_reference}"
)
os.symlink(relative_reference, origin, target_is_directory=True)

# Copy the icon
if self.iconfile:
Expand Down Expand Up @@ -459,26 +520,109 @@ def run(self):
if self.absolute_reference_path:
self.execute(self.set_absolute_reference_paths, ())

# Move license file to resources as it can't be signed
src_lfp = os.path.join(self.bin_dir, "frozen_application_license.txt")
if os.path.exists(src_lfp):
shutil.move(src_lfp, self.resources_dir)
print(f"Moved: {src_lfp} -> {self.resources_dir}")

# For a Qt application, run some tweaks
self.execute(self.prepare_qt_app, ())

# Sign the app bundle if a key is specified
if self.codesign_identity:
signargs = ["codesign", "-s", self.codesign_identity]
self._codesign(self.bundle_dir)

def _codesign(self, root_path):
"""Run codesign on all .so, .dylib and binary files in reverse order.
Signing from inside-out.
"""
if not self.codesign_identity:
return

if self.codesign_entitlements:
signargs.append("--entitlements")
signargs.append(self.codesign_entitlements)
print(f"About to sign: '{self.bundle_dir}'")
binaries_to_sign = []

if self.codesign_deep:
signargs.insert(1, "--deep")
# 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 self.codesign_resource_rules:
signargs.insert(
1, "--resource-rules=" + self.codesign_resource_rules
)
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")

signargs.append(self.bundle_dir)
def _get_sign_args(self):
signargs = ["codesign", "--sign", self.codesign_identity, "--force"]

if subprocess.call(signargs) != 0:
raise OSError("Code signing of app bundle failed")
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):
print(f"Signing file: {file_path}")
sign_args.append(file_path)
subprocess.run(sign_args, check=False)

def _verify_signature(self):
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()}")
johan-ronnkvist marked this conversation as resolved.
Show resolved Hide resolved
raise
10 changes: 5 additions & 5 deletions cx_Freeze/darwintools.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
# directories are included in the current rpath.


def _isMachOFile(path: Path) -> bool:
def isMachOFile(path: Path) -> bool:
"""Determines whether the file is a Mach-O file."""
if not path.is_file():
return False
Expand Down Expand Up @@ -114,7 +114,7 @@ def __init__(
# unresolved load path.
self.machOReferenceForTargetPath: dict[Path, MachOReference] = {}

if not _isMachOFile(self.path):
if not isMachOFile(self.path):
self.isMachO = False
return

Expand Down Expand Up @@ -254,7 +254,7 @@ def resolveExecutable(self, path: str) -> Path:
def resolveRPath(self, path: str) -> Path | None:
for rpath in self.getRPath():
test_path = rpath / Path(path).relative_to("@rpath")
if _isMachOFile(test_path):
if isMachOFile(test_path):
return test_path
if not self.strict:
# If not strictly enforcing rpath, return None here, and leave any
Expand Down Expand Up @@ -302,7 +302,7 @@ def resolvePath(self, path: str) -> Path | None:
if test_path.is_absolute(): # just use the path, if it is absolute
return test_path
test_path = self.path.parent / path
if _isMachOFile(test_path):
if isMachOFile(test_path):
return test_path.resolve()
if self.strict:
raise PlatformError(
Expand Down Expand Up @@ -497,7 +497,7 @@ def changeLoadReference(
def applyAdHocSignature(fileName: str):
if platform.machine() != "arm64":
return

print("Applying AdHocSignature")
args = (
"codesign",
"--sign",
Expand Down
10 changes: 10 additions & 0 deletions doc/src/setup_script.rst
Original file line number Diff line number Diff line change
Expand Up @@ -621,8 +621,18 @@ bundle (a .app directory).
* - .. option:: codesign_entitlements
- The path to an entitlements file to use for your application's code
signature.
* - .. option:: codesign_timestamp
- Use --timestamp when running codesign.
* - .. option:: codesign_strict
- Use --strict when running codesign.
* - .. option:: codesign_verify
- Use --verify when running codesign.
* - .. option:: spctl_assess
- Run spctl-assess to asses output from codesign.
* - .. option:: codesign_deep
- Boolean for whether to codesign using the --deep option.
* - .. option:: codesign_options
- Comma seperated string of options to use with codesign --options.
* - .. option:: codesign_resource_rules
- Plist file to be passed to codesign's --resource-rules option.
* - .. option:: absolute_reference_path
Expand Down
10 changes: 10 additions & 0 deletions samples/pyside2/codesign-entitlements.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
</dict>
</plist>
26 changes: 19 additions & 7 deletions samples/pyside2/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from __future__ import annotations

import sys
import os

from cx_Freeze import Executable, setup

Expand Down Expand Up @@ -45,19 +46,30 @@
# base="Win32GUI" should be used only for Windows GUI app
base = "Win32GUI" if sys.platform == "win32" else None

build_exe_options = {
# exclude packages that are not really needed
"excludes": ["tkinter", "unittest", "email", "http", "xml", "pydoc"],
"include_files": include_files,
"zip_include_packages": ["PySide2", "shiboken2"],
options = {
johan-ronnkvist marked this conversation as resolved.
Show resolved Hide resolved
"build_exe": {
# exclude packages that are not really needed
"excludes": ["tkinter", "unittest", "email", "http", "xml", "pydoc"],
"include_files": include_files,
"zip_include_packages": ["PySide2", "shiboken2"],
},
"bdist_mac": {
"custom_info_plist": None, # Set this to use a custom info.plist file
"codesign_entitlements": os.path.join(
os.path.dirname(__file__), "codesign-entitlements.plist"
),
"codesign_identity": None, # Set this to enable signing with custom identity (replaces adhoc signature)
"codesign_options": "runtime", # Ensure codesign uses 'hardened runtime'
"codesign_verify": False, # Enable to get more verbose logging regarding codesign
"spctl_assess": False, # Enable to get more verbose logging regarding codesign
},
}

executables = [Executable("test_pyside2.py", base=base)]

setup(
name="simple_PySide2",
version="0.5",
description="Sample cx_Freeze PySide2 script",
options={"build_exe": build_exe_options},
options=options,
executables=executables,
)