Pyside6Template/ci/codesign.py
Joey Yakimowich-Payne baf362371f Add proper code signing
2022-11-08 12:22:07 -07:00

224 lines
6.8 KiB
Python

# -*- coding: utf-8 -*-
import os
import shutil
import sys
from pathlib import Path
from typing import Generator, List, Optional
from macholib.MachO import MachO
import contextlib
import fcntl
import os
import subprocess
import sys
import time
from macholib.util import is_platform_file
def _dosign(*path):
with reset_blocking_status():
subprocess.check_call(
(
"codesign",
"--deep",
"--force",
"-s",
"-",
"--preserve-metadata=identifier,entitlements,flags,runtime",
"-f",
"-vvvv",
)
+ path,
)
def _macho_find(path):
for basename, _dirs, files in os.walk(path):
for fn in files:
path = os.path.join(basename, fn)
if is_platform_file(path):
yield path
def codesign_adhoc(bundle):
"""
(Re)sign a bundle
Signing should be done "depth-first", sign
libraries before signing the libraries/executables
linking to them.
The current implementation is a crude hack,
but is better than nothing. Signing properly requires
performing a topological sort using dependencies.
"codesign" will resign the entire bundle, but only
if partial signatures are valid.
"""
# try:
# _dosign(bundle)
# return
# except subprocess.CalledProcessError:
# pass
platfiles = list(_macho_find(bundle))
print("sign", platfiles)
while platfiles:
failed = []
for file in platfiles:
failed = []
try:
_dosign(file)
except subprocess.CalledProcessError:
failed.append(file)
if failed == platfiles:
raise RuntimeError("Cannot sign bundle %r" % (bundle,))
platfiles = failed
for _ in range(5):
try:
_dosign(bundle)
break
except subprocess.CalledProcessError:
time.sleep(1)
continue
@contextlib.contextmanager
def reset_blocking_status():
"""
Contextmanager that resets the non-blocking status of
the std* streams as necessary. Used with all calls of
xcode tools, mostly because ibtool tends to set the
std* streams to non-blocking.
"""
orig_nonblocking = [
fcntl.fcntl(fd, fcntl.F_GETFL) & os.O_NONBLOCK for fd in (0, 1, 2)
]
try:
yield
finally:
for fd, is_nonblocking in zip((0, 1, 2), orig_nonblocking):
cur = fcntl.fcntl(fd, fcntl.F_GETFL)
if is_nonblocking:
reset = cur | os.O_NONBLOCK
else:
reset = cur & ~os.O_NONBLOCK
if cur != reset:
print("Resetting blocking status of %s" % (fd,))
fcntl.fcntl(fd, fcntl.F_SETFL, reset)
def create_symlink(folder: Path) -> None:
"""Create the appropriate symlink in the MacOS folder
pointing to the Resources folder.
"""
sibbling = Path(str(folder).replace("MacOS", ""))
# PyQt5/Qt/qml/QtQml/Models.2
root = str(sibbling).partition("Contents")[2].lstrip("/")
# ../../../../
backward = "../" * (root.count("/") + 1)
# ../../../../Resources/PyQt5/Qt/qml/QtQml/Models.2
good_path = f"{backward}Resources/{root}"
folder.symlink_to(good_path)
def fix_dll(dll: Path) -> None:
"""Fix the DLL lookup paths to use relative ones for Qt dependencies.
Inspiration: PyInstaller/depend/dylib.py:mac_set_relative_dylib_deps()
Currently one header is pointing to (we are in the Resources folder):
@loader_path/../../../../QtCore (it is referencing to the old MacOS folder)
It will be converted to:
@loader_path/../../../../../../MacOS/QtCore
"""
def match_func(pth: str) -> Optional[str]:
"""Callback function for MachO.rewriteLoadCommands() that is
called on every lookup path setted in the DLL headers.
By returning None for system libraries, it changes nothing.
Else we return a relative path pointing to the good file
in the MacOS folder.
"""
basename = os.path.basename(pth)
if not basename.startswith("Qt"):
return None
return f"@loader_path{good_path}/{basename}"
# Resources/PyQt5/Qt/qml/QtQuick/Controls.2/Fusion
root = str(dll.parent).partition("Contents")[2][1:]
# /../../../../../../..
backward = "/.." * (root.count("/") + 1)
# /../../../../../../../MacOS
good_path = f"{backward}/MacOS"
# Rewrite Mach headers with corrected @loader_path
mach_dll = MachO(dll)
mach_dll.rewriteLoadCommands(match_func)
with open(mach_dll.filename, "rb+") as f:
for header in mach_dll.headers:
f.seek(0)
mach_dll.write(f)
f.seek(0, 2)
f.flush()
def find_problematic_folders(folder: Path) -> Generator[Path, None, None]:
"""Recursively yields problematic folders (containing a dot in their name)."""
for path in folder.iterdir():
print(path)
if not path.is_dir() or path.is_symlink():
# Skip simlinks as they are allowed (even with a dot)
continue
if "." in path.name:
yield path
else:
yield from find_problematic_folders(path)
def move_contents_to_resources(folder: Path) -> Generator[Path, None, None]:
"""Recursively move any non symlink file from a problematic folder
to the sibbling one in Resources.
"""
for path in folder.iterdir():
if path.is_symlink():
continue
if path.name == "qml":
yield from move_contents_to_resources(path)
else:
sibbling = Path(str(path).replace("MacOS", "Resources"))
sibbling.parent.mkdir(parents=True, exist_ok=True)
shutil.move(path, sibbling)
yield sibbling
def main(args: List[str]):
"""
Fix the application to allow codesign (NXDRIVE-1301).
Take one or more .app as arguments: "Nuxeo Drive.app".
To overall process will:
- move problematic folders from MacOS to Resources
- fix the DLLs lookup paths
- create the appropriate symbolic link
"""
for app in args:
name = os.path.basename(app)
print(f">>> [{name}] Fixing Qt folder names")
path = Path(app) / "Contents" / "MacOS"
for folder in find_problematic_folders(path):
for file in move_contents_to_resources(folder):
try:
fix_dll(file)
except (ValueError, IsADirectoryError):
continue
shutil.rmtree(folder)
create_symlink(folder)
print(f" !! Fixed {folder}")
codesign_adhoc(app)
print(f">>> [{name}] Application fixed.")
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))