Tooling: Switch AutoPkg generation to macOS-Pkg-Builder

This commit is contained in:
Mykola Grymalyuk
2024-05-27 13:25:51 -06:00
parent ec9ecbc7a9
commit 9428040f64
13 changed files with 50 additions and 1038 deletions
+176
View File
@@ -0,0 +1,176 @@
import sys
import time
import plistlib
import subprocess
from pathlib import Path
from opencore_legacy_patcher import constants
from opencore_legacy_patcher.support import subprocess_wrapper
class GenerateApplication:
"""
Generate OpenCore-Patcher.app
"""
def __init__(self, reset_pyinstaller_cache: bool = False, git_branch: str = None, git_commit_url: str = None, git_commit_date: str = None, analytics_key: str = None, analytics_endpoint: str = None) -> None:
"""
Initialize
"""
self._pyinstaller = [sys.executable, "-m", "PyInstaller"]
self._application_output = Path("./dist/OpenCore-Patcher.app")
self._reset_pyinstaller_cache = reset_pyinstaller_cache
self._git_branch = git_branch
self._git_commit_url = git_commit_url
self._git_commit_date = git_commit_date
self._analytics_key = analytics_key
self._analytics_endpoint = analytics_endpoint
def _generate_application(self) -> None:
"""
Generate PyInstaller Application
"""
if self._application_output.exists():
subprocess_wrapper.run_and_verify(["/bin/rm", "-rf", self._application_output], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
print("Generating OpenCore-Patcher.app")
_args = self._pyinstaller + ["./OpenCore-Patcher-GUI.spec", "--noconfirm"]
if self._reset_pyinstaller_cache:
_args.append("--clean")
subprocess_wrapper.run_and_verify(_args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
def _embed_analytics_key(self) -> None:
"""
Embed analytics key
"""
_file = Path("./opencore_legacy_patcher/support/analytics_handler.py")
if not all([self._analytics_key, self._analytics_endpoint]):
print("Analytics key or endpoint not provided, skipping embedding")
return
print("Embedding analytics data")
if not Path(_file).exists():
raise FileNotFoundError("analytics_handler.py not found")
lines = []
with open(_file, "r") as f:
lines = f.readlines()
for i, line in enumerate(lines):
if line.startswith("SITE_KEY: str = "):
lines[i] = f"SITE_KEY: str = \"{self._analytics_key}\"\n"
elif line.startswith("ANALYTICS_SERVER: str = "):
lines[i] = f"ANALYTICS_SERVER: str = \"{self._analytics_endpoint}\"\n"
with open(_file, "w") as f:
f.writelines(lines)
def _remove_analytics_key(self) -> None:
"""
Remove analytics key
"""
_file = Path("./opencore_legacy_patcher/support/analytics_handler.py")
if not all([self._analytics_key, self._analytics_endpoint]):
return
print("Removing analytics data")
if not _file.exists():
raise FileNotFoundError("analytics_handler.py not found")
lines = []
with open(_file, "r") as f:
lines = f.readlines()
for i, line in enumerate(lines):
if line.startswith("SITE_KEY: str = "):
lines[i] = "SITE_KEY: str = \"\"\n"
elif line.startswith("ANALYTICS_SERVER: str = "):
lines[i] = "ANALYTICS_SERVER: str = \"\"\n"
with open(_file, "w") as f:
f.writelines(lines)
def _patch_load_command(self):
"""
Patch LC_VERSION_MIN_MACOSX in Load Command to report 10.10
By default Pyinstaller will create binaries supporting 10.13+
However this limitation is entirely arbitrary for our libraries
and instead we're able to support 10.10 without issues.
To verify set version:
otool -l ./dist/OpenCore-Patcher.app/Contents/MacOS/OpenCore-Patcher
cmd LC_VERSION_MIN_MACOSX
cmdsize 16
version 10.13
sdk 10.9
"""
_file = self._application_output / "Contents" / "MacOS" / "OpenCore-Patcher"
_find = b'\x00\x0D\x0A\x00' # 10.13 (0xA0D)
_replace = b'\x00\x0A\x0A\x00' # 10.10 (0xA0A)
print("Patching LC_VERSION_MIN_MACOSX")
with open(_file, "rb") as f:
data = f.read()
data = data.replace(_find, _replace, 1)
with open(_file, "wb") as f:
f.write(data)
def _embed_git_data(self) -> None:
"""
Embed git data
"""
_file = self._application_output / "Contents" / "Info.plist"
_git_branch = self._git_branch or "Built from source"
_git_commit = self._git_commit_url or ""
_git_commit_date = self._git_commit_date or time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
print("Embedding git data")
_plist = plistlib.load(_file.open("rb"))
_plist["Github"] = {
"Branch": _git_branch,
"Commit URL": _git_commit,
"Commit Date": _git_commit_date
}
plistlib.dump(_plist, _file.open("wb"), sort_keys=True)
def _embed_resources(self) -> None:
"""
Embed resources
"""
print("Embedding resources")
for file in Path("payloads/Icon/AppIcons").glob("*.icns"):
subprocess_wrapper.run_and_verify(
["/bin/cp", str(file), self._application_output / "Contents" / "Resources/"],
stdout=subprocess.PIPE, stderr=subprocess.PIPE
)
def generate(self) -> None:
"""
Generate OpenCore-Patcher.app
"""
self._embed_analytics_key()
self._generate_application()
self._remove_analytics_key()
self._patch_load_command()
self._embed_git_data()
self._embed_resources()
+136
View File
@@ -0,0 +1,136 @@
"""
disk_images.py: Fetch and generate disk images (Universal-Binaries.dmg, payloads.dmg)
"""
import subprocess
from pathlib import Path
from opencore_legacy_patcher import constants
from opencore_legacy_patcher.support import subprocess_wrapper
class GenerateDiskImages:
def __init__(self, reset_dmg_cache: bool = False) -> None:
"""
Initialize
"""
self.reset_dmg_cache = reset_dmg_cache
def _delete_extra_binaries(self):
"""
Delete extra binaries from payloads directory
"""
whitelist_folders = [
"ACPI",
"Config",
"Drivers",
"Icon",
"Kexts",
"OpenCore",
"Tools",
"Launch Services",
]
whitelist_files = []
print("Deleting extra binaries...")
for file in Path("payloads").glob(pattern="*"):
if file.is_dir():
if file.name in whitelist_folders:
continue
print(f"- Deleting {file.name}")
subprocess_wrapper.run_and_verify(["/bin/rm", "-rf", file])
else:
if file.name in whitelist_files:
continue
print(f"- Deleting {file.name}")
subprocess_wrapper.run_and_verify(["/bin/rm", "-f", file])
def _generate_payloads_dmg(self):
"""
Generate disk image containing all payloads
Disk image will be password protected due to issues with
Apple's notarization system and inclusion of kernel extensions
"""
if Path("./payloads.dmg").exists():
if self.reset_dmg_cache is False:
print("- payloads.dmg already exists, skipping creation")
return
print("- Removing old payloads.dmg")
subprocess_wrapper.run_and_verify(
["/bin/rm", "-rf", "./payloads.dmg"],
stdout=subprocess.PIPE, stderr=subprocess.PIPE
)
print("Generating DMG...")
subprocess_wrapper.run_and_verify([
'/usr/bin/hdiutil', 'create', './payloads.dmg',
'-megabytes', '32000', # Overlays can only be as large as the disk image allows
'-format', 'UDZO', '-ov',
'-volname', 'OpenCore Patcher Resources (Base)',
'-fs', 'HFS+',
'-srcfolder', './payloads',
'-passphrase', 'password', '-encryption'
], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
print("DMG generation complete")
def _download_resources(self):
"""
Download required dependencies
"""
patcher_support_pkg_version = constants.Constants().patcher_support_pkg_version
required_resources = [
"Universal-Binaries.dmg"
]
print("Downloading required resources...")
for resource in required_resources:
if Path(f"./{resource}").exists():
if self.reset_dmg_cache is True:
print(f" - Removing old {resource}")
# Just to be safe
assert resource, "Resource cannot be empty"
assert resource not in ("/", "."), "Resource cannot be root"
subprocess_wrapper.run_and_verify(
["/bin/rm", "-rf", f"./{resource}"],
stdout=subprocess.PIPE, stderr=subprocess.PIPE
)
else:
print(f"- {resource} already exists, skipping download")
continue
print(f"- Downloading {resource}...")
subprocess_wrapper.run_and_verify(
[
"/usr/bin/curl", "-LO",
f"https://github.com/dortania/PatcherSupportPkg/releases/download/{patcher_support_pkg_version}/{resource}"
],
stdout=subprocess.PIPE, stderr=subprocess.PIPE
)
if not Path(f"./{resource}").exists():
print(f"- {resource} not found")
raise Exception(f"{resource} not found")
def generate(self) -> None:
"""
Generate disk images
"""
self._delete_extra_binaries()
self._generate_payloads_dmg()
self._download_resources()
+110
View File
@@ -0,0 +1,110 @@
"""
package.py: Generate packages (Installer, Uninstaller, AutoPkg-Assets)
"""
import macos_pkg_builder
from opencore_legacy_patcher import constants
class GeneratePackage:
"""
Generate OpenCore-Patcher.pkg
"""
def __init__(self) -> None:
"""
Initialize
"""
self._files = {
"./dist/OpenCore-Patcher.app": "/Library/Application Support/Dortania/OpenCore-Patcher.app",
"./ci_tooling/privileged_helper_tool/com.dortania.opencore-legacy-patcher.privileged-helper": "/Library/PrivilegedHelperTools/com.dortania.opencore-legacy-patcher.privileged-helper",
}
self._autopkg_files = {
"./payloads/Launch Services/com.dortania.opencore-legacy-patcher.auto-patch.plist": "/Library/LaunchAgents/com.dortania.opencore-legacy-patcher.auto-patch.plist",
}
self._autopkg_files.update(self._files)
def _generate_installer_welcome(self) -> str:
"""
Generate Welcome message for installer PKG
"""
_welcome = ""
_welcome += "# Overview\n"
_welcome += f"This package will install the OpenCore Legacy Patcher application (v{constants.Constants().patcher_version}) on your system."
_welcome += "\n\nAdditionally, a shortcut for OpenCore Legacy Patcher will be added in the '/Applications' folder."
_welcome += "\n\nThis package will not 'Build and Install OpenCore' or install any 'Root Patches' on your machine. If required, you can run OpenCore Legacy Patcher to install any patches you may need."
_welcome += f"\n\nFor more information on OpenCore Legacy Patcher usage, see our [documentation]({constants.Constants().guide_link}) and [GitHub repository]({constants.Constants().repo_link})."
_welcome += "\n\n"
_welcome += "## Files Installed"
_welcome += "\n\nInstallation of this package will add the following files to your system:"
for key, value in self._files.items():
_welcome += f"\n\n- `{value}`"
return _welcome
def _generate_uninstaller_welcome(self) -> str:
"""
Generate Welcome message for uninstaller PKG
"""
_welcome = ""
_welcome += "# Application Uninstaller\n"
_welcome += "This package will uninstall the OpenCore Legacy Patcher application and its Privileged Helper Tool from your system."
_welcome += "\n\n"
_welcome += "This will not remove any root patches or OpenCore configurations that you may have installed using OpenCore Legacy Patcher."
_welcome += "\n\n"
_welcome += f"For more information on OpenCore Legacy Patcher, see our [documentation]({constants.Constants().guide_link}) and [GitHub repository]({constants.Constants().repo_link})."
return _welcome
def generate(self) -> None:
"""
Generate OpenCore-Patcher.pkg
"""
print("Generating OpenCore-Patcher-Uninstaller.pkg")
assert macos_pkg_builder.Packages(
pkg_output="./dist/OpenCore-Patcher-Uninstaller.pkg",
pkg_bundle_id="com.dortania.opencore-legacy-patcher-uninstaller",
pkg_version=constants.Constants().patcher_version,
pkg_background="./ci_tooling/installation_pkg/PkgBackgroundUninstaller.png",
pkg_preinstall_script="./ci_tooling/installation_pkg/uninstall.sh",
pkg_as_distribution=True,
pkg_title="OpenCore Legacy Patcher Uninstaller",
pkg_welcome=self._generate_uninstaller_welcome(),
).build() is True
print("Generating OpenCore-Patcher.pkg")
assert macos_pkg_builder.Packages(
pkg_output="./dist/OpenCore-Patcher.pkg",
pkg_bundle_id="com.dortania.opencore-legacy-patcher",
pkg_version=constants.Constants().patcher_version,
pkg_allow_relocation=False,
pkg_as_distribution=True,
pkg_background="./ci_tooling/installation_pkg/PkgBackground.png",
pkg_preinstall_script="./ci_tooling/installation_pkg/preinstall.sh",
pkg_postinstall_script="./ci_tooling/installation_pkg/postinstall.sh",
pkg_file_structure=self._files,
pkg_title="OpenCore Legacy Patcher",
pkg_welcome=self._generate_installer_welcome(),
).build() is True
print("Generating AutoPkg-Assets.pkg")
assert macos_pkg_builder.Packages(
pkg_output="./dist/AutoPkg-Assets.pkg",
pkg_bundle_id="com.dortania.pkg.AutoPkg-Assets",
pkg_version=constants.Constants().patcher_version,
pkg_allow_relocation=False,
pkg_as_distribution=True,
pkg_background="./ci_tooling/autopkg/PkgBackground.png",
pkg_preinstall_script="./ci_tooling/autopkg/preinstall.sh",
pkg_postinstall_script="./ci_tooling/autopkg/postinstall.sh",
pkg_file_structure=self._autopkg_files,
pkg_title="AutoPkg Assets",
pkg_welcome="# DO NOT RUN AUTOPKG-ASSETS MANUALLY!\n\n## THIS CAN BREAK YOUR SYSTEM'S INSTALL!\n\nThis package should only ever be invoked by the Patcher itself, never downloaded or run by the user. Download the OpenCore-Patcher.pkg on the Github Repository.\n\n[OpenCore Legacy Patcher GitHub Release](https://github.com/dortania/OpenCore-Legacy-Patcher/releases/)",
).build() is True
+33
View File
@@ -0,0 +1,33 @@
"""
shim.py: Generate Update Shim
"""
from pathlib import Path
from opencore_legacy_patcher.support import subprocess_wrapper
class GenerateShim:
def __init__(self) -> None:
self._shim_path = "./ci_tooling/update_shim/OpenCore-Patcher.app"
self._shim_pkg = f"{self._shim_path}/Contents/Resources/OpenCore-Patcher.pkg"
self._build_pkg = "./dist/OpenCore-Patcher.pkg"
self._output_shim = "./dist/OpenCore-Patcher (Shim).app"
def generate(self) -> None:
"""
Generate Update Shim
"""
print("Generating Update Shim")
if Path(self._shim_pkg).exists():
Path(self._shim_pkg).unlink()
subprocess_wrapper.run_and_verify(["/bin/cp", "-R", self._build_pkg, self._shim_pkg])
if Path(self._output_shim).exists():
Path(self._output_shim).unlink()
subprocess_wrapper.run_and_verify(["/bin/cp", "-R", self._shim_path, self._output_shim])
+54
View File
@@ -0,0 +1,54 @@
"""
sign_notarize.py: Sign and Notarize a file
"""
import mac_signing_buddy
import macos_pkg_builder
from pathlib import Path
import macos_pkg_builder.utilities.signing
class SignAndNotarize:
def __init__(self, path: Path, signing_identity: str, notarization_apple_id: str, notarization_password: str, notarization_team_id: str, entitlements: str = None) -> None:
"""
Initialize
"""
self._path = path
self._signing_identity = signing_identity
self._notarization_apple_id = notarization_apple_id
self._notarization_password = notarization_password
self._notarization_team_id = notarization_team_id
self._entitlements = entitlements
def sign_and_notarize(self) -> None:
"""
Sign and Notarize
"""
if not all([self._signing_identity, self._notarization_apple_id, self._notarization_password, self._notarization_team_id]):
print("Signing and Notarization details not provided, skipping")
return
print(f"Signing {self._path.name}")
if self._path.name.endswith(".pkg"):
macos_pkg_builder.utilities.signing.SignPackage(
identity=self._signing_identity,
pkg=self._path,
).sign()
else:
mac_signing_buddy.Sign(
identity=self._signing_identity,
file=self._path,
**({"entitlements": self._entitlements} if self._entitlements else {}),
).sign()
print(f"Notarizing {self._path.name}")
mac_signing_buddy.Notarize(
apple_id=self._notarization_apple_id,
password=self._notarization_password,
team_id=self._notarization_team_id,
file=self._path,
).sign()