mirror of
https://github.com/dortania/OpenCore-Legacy-Patcher.git
synced 2026-04-14 12:48:18 +10:00
Merge pull request #1029 from dortania/oop-rework
Rework additional core libraries
This commit is contained in:
1
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
1
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -95,7 +95,6 @@ body:
|
||||
description: What variant of our software are you running?
|
||||
options:
|
||||
- GUI (Graphical User Interface)
|
||||
- TUI (Text User Interface)
|
||||
- CLI (Command Line Interface)
|
||||
- Other/Non-Applicable
|
||||
validations:
|
||||
|
||||
5
.github/workflows/build-app-wxpython.yml
vendored
5
.github/workflows/build-app-wxpython.yml
vendored
@@ -11,12 +11,14 @@ jobs:
|
||||
name: Build wxPython
|
||||
runs-on: x86_64_mojave
|
||||
if: github.repository_owner == 'dortania'
|
||||
|
||||
env:
|
||||
branch: ${{ github.ref }}
|
||||
commiturl: ${{ github.event.head_commit.url }}${{ github.event.release.html_url }}
|
||||
commitdate: ${{ github.event.head_commit.timestamp }}${{ github.event.release.published_at }}
|
||||
MAC_NOTARIZATION_USERNAME: ${{ secrets.MAC_NOTARIZATION_USERNAME }}
|
||||
MAC_NOTARIZATION_PASSWORD: ${{ secrets.MAC_NOTARIZATION_PASSWORD }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- run: /Library/Frameworks/Python.framework/Versions/3.10/bin/python3 Build-Binary.command --reset_binaries --branch "${{ env.branch }}" --commit "${{ env.commiturl }}" --commit_date "${{ env.commitdate }}"
|
||||
@@ -30,11 +32,13 @@ jobs:
|
||||
with:
|
||||
name: OpenCore-Patcher.app (GUI)
|
||||
path: OpenCore-Patcher-GUI.app.zip
|
||||
|
||||
- name: Upload Package to Artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: AutoPkg-Assets.pkg
|
||||
path: ./dist/AutoPkg-Assets.pkg
|
||||
|
||||
- name: Upload Binary to Release
|
||||
if: github.event_name == 'release'
|
||||
uses: svenstaro/upload-release-action@e74ff71f7d8a4c4745b560a485cc5fdb9b5b999d
|
||||
@@ -43,6 +47,7 @@ jobs:
|
||||
file: OpenCore-Patcher-GUI.app.zip
|
||||
tag: ${{ github.ref }}
|
||||
file_glob: true
|
||||
|
||||
- name: Upload Package to Release
|
||||
if: github.event_name == 'release'
|
||||
uses: svenstaro/upload-release-action@e74ff71f7d8a4c4745b560a485cc5fdb9b5b999d
|
||||
|
||||
24
.github/workflows/build-app.yml
vendored
24
.github/workflows/build-app.yml
vendored
@@ -1,24 +0,0 @@
|
||||
name: CI - Build TUI
|
||||
|
||||
on:
|
||||
push:
|
||||
workflow_dispatch:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build TUI
|
||||
runs-on: x86_64_mojave
|
||||
env:
|
||||
branch: ${{ github.ref }}
|
||||
commiturl: ${{ github.event.head_commit.url }}${{ github.event.release.html_url }}
|
||||
commitdate: ${{ github.event.head_commit.timestamp }}${{ github.event.release.published_at }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- run: /Library/Frameworks/Python.framework/Versions/3.10/bin/python3 Build-Binary.command --build_tui --reset_binaries --branch "${{ env.branch }}" --commit "${{ env.commiturl }}" --commit_date "${{ env.commitdate }}"
|
||||
- run: 'codesign -s "Developer ID Application: Mykola Grymalyuk (S74BDJXQMD)" -v --force --deep --timestamp --entitlements ./payloads/entitlements.plist -o runtime "dist/OpenCore-Patcher.app"'
|
||||
- run: cd dist; zip -r ../OpenCore-Patcher-TUI.app.zip OpenCore-Patcher.app
|
||||
|
||||
- name: Validate OpenCore
|
||||
run: ./dist/OpenCore-Patcher.app/Contents/MacOS/OpenCore-Patcher --validate
|
||||
1
.github/workflows/build-site.yml
vendored
1
.github/workflows/build-site.yml
vendored
@@ -9,6 +9,7 @@ jobs:
|
||||
build:
|
||||
name: Build Site and Deploy
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository_owner == 'dortania'
|
||||
steps:
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
|
||||
20
.github/workflows/validate.yml
vendored
Normal file
20
.github/workflows/validate.yml
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
name: CI - Validation
|
||||
|
||||
on:
|
||||
push:
|
||||
workflow_dispatch:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Validate
|
||||
runs-on: x86_64_mojave
|
||||
if: github.repository_owner == 'dortania'
|
||||
env:
|
||||
branch: ${{ github.ref }}
|
||||
commiturl: ${{ github.event.head_commit.url }}${{ github.event.release.html_url }}
|
||||
commitdate: ${{ github.event.head_commit.timestamp }}${{ github.event.release.published_at }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- run: /Library/Frameworks/Python.framework/Versions/3.10/bin/python3 OpenCore-Patcher-GUI.command --validate
|
||||
@@ -1,14 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# This script's main purpose is to handle the following:
|
||||
# - Download PatcherSupportPkg resources
|
||||
# - Convert payloads directory into DMG (GUI only)
|
||||
# - Build Binary via Pyinstaller
|
||||
# - Add Launcher.sh (TUI only)
|
||||
# - Patch 'LC_VERSION_MIN_MACOSX' to OS X 10.10
|
||||
# - Add commit data to Info.plist
|
||||
|
||||
# Copyright (C) 2022 - Mykola Grymalyuk
|
||||
# Generate stand alone application for OpenCore-Patcher
|
||||
# Copyright (C) 2022-2023 - Mykola Grymalyuk
|
||||
|
||||
from pathlib import Path
|
||||
import time
|
||||
@@ -21,26 +14,49 @@ import sys
|
||||
|
||||
from resources import constants
|
||||
|
||||
class create_binary:
|
||||
|
||||
class CreateBinary:
|
||||
"""
|
||||
Library for creating OpenCore-Patcher application
|
||||
|
||||
This script's main purpose is to handle the following:
|
||||
- Download external dependancies (ex. PatcherSupportPkg)
|
||||
- Convert payloads directory into DMG
|
||||
- Build Binary via Pyinstaller
|
||||
- Patch 'LC_VERSION_MIN_MACOSX' to OS X 10.10
|
||||
- Add commit data to Info.plist
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
start = time.time()
|
||||
print("- Starting build script")
|
||||
self.set_cwd()
|
||||
self.args = self.parse_arguments()
|
||||
|
||||
self.preflight_processes()
|
||||
self.build_binary()
|
||||
self.postflight_processes()
|
||||
self.args = self._parse_arguments()
|
||||
|
||||
self._set_cwd()
|
||||
|
||||
self._preflight_processes()
|
||||
self._build_binary()
|
||||
self._postflight_processes()
|
||||
print(f"- Build script completed in {str(round(time.time() - start, 2))} seconds")
|
||||
|
||||
def set_cwd(self):
|
||||
|
||||
def _set_cwd(self):
|
||||
"""
|
||||
Initialize current working directory to parent of this script
|
||||
"""
|
||||
|
||||
os.chdir(Path(__file__).resolve().parent)
|
||||
print(f"- Current Working Directory: \n\t{os.getcwd()}")
|
||||
|
||||
def parse_arguments(self):
|
||||
|
||||
def _parse_arguments(self):
|
||||
"""
|
||||
Parse arguments passed to script
|
||||
"""
|
||||
|
||||
parser = argparse.ArgumentParser(description='Builds OpenCore-Patcher binary')
|
||||
parser.add_argument('--build_tui', action='store_true', help='Builds TUI binary, if omitted GUI binary is built')
|
||||
parser.add_argument('--branch', type=str, help='Git branch name')
|
||||
parser.add_argument('--commit', type=str, help='Git commit URL')
|
||||
parser.add_argument('--commit_date', type=str, help='Git commit date')
|
||||
@@ -48,7 +64,12 @@ class create_binary:
|
||||
args = parser.parse_args()
|
||||
return args
|
||||
|
||||
def setup_pathing(self):
|
||||
|
||||
def _setup_pathing(self):
|
||||
"""
|
||||
Initialize pathing for pyinstaller
|
||||
"""
|
||||
|
||||
python_path = sys.executable
|
||||
python_binary = python_path.split("/")[-1]
|
||||
python_bin_dir = python_path.strip(python_binary)
|
||||
@@ -70,25 +91,36 @@ class create_binary:
|
||||
|
||||
self.pyinstaller_path = pyinstaller_path
|
||||
|
||||
def preflight_processes(self):
|
||||
|
||||
def _preflight_processes(self):
|
||||
"""
|
||||
Start preflight processes
|
||||
"""
|
||||
|
||||
print("- Starting preflight processes")
|
||||
self.setup_pathing()
|
||||
self.delete_extra_binaries()
|
||||
self.download_resources()
|
||||
if not self.args.build_tui:
|
||||
# payloads.dmg is only needed for GUI builds
|
||||
self.generate_payloads_dmg()
|
||||
self._setup_pathing()
|
||||
self._delete_extra_binaries()
|
||||
self._download_resources()
|
||||
self._generate_payloads_dmg()
|
||||
|
||||
|
||||
def _postflight_processes(self):
|
||||
"""
|
||||
Start postflight processes
|
||||
"""
|
||||
|
||||
def postflight_processes(self):
|
||||
print("- Starting postflight processes")
|
||||
if self.args.build_tui:
|
||||
self.move_launcher()
|
||||
self.patch_load_command()
|
||||
self.add_commit_data()
|
||||
self.post_flight_cleanup()
|
||||
self.mini_validate()
|
||||
self._patch_load_command()
|
||||
self._add_commit_data()
|
||||
self._post_flight_cleanup()
|
||||
self._mini_validate()
|
||||
|
||||
|
||||
def _build_binary(self):
|
||||
"""
|
||||
Build binary via pyinstaller
|
||||
"""
|
||||
|
||||
def build_binary(self):
|
||||
if Path(f"./dist/OpenCore-Patcher.app").exists():
|
||||
print("- Found OpenCore-Patcher.app, removing...")
|
||||
rm_output = subprocess.run(
|
||||
@@ -101,12 +133,8 @@ class create_binary:
|
||||
raise Exception("Remove failed")
|
||||
|
||||
|
||||
if self.args.build_tui:
|
||||
print("- Building TUI binary...")
|
||||
build_args = [self.pyinstaller_path, "./OpenCore-Patcher.spec", "--noconfirm"]
|
||||
else:
|
||||
print("- Building GUI binary...")
|
||||
build_args = [self.pyinstaller_path, "./OpenCore-Patcher-GUI.spec", "--noconfirm"]
|
||||
print("- Building GUI binary...")
|
||||
build_args = [self.pyinstaller_path, "./OpenCore-Patcher-GUI.spec", "--noconfirm"]
|
||||
|
||||
build_result = subprocess.run(build_args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
if build_result.returncode != 0:
|
||||
@@ -114,27 +142,52 @@ class create_binary:
|
||||
print(build_result.stderr.decode('utf-8'))
|
||||
raise Exception("Build failed")
|
||||
|
||||
def delete_extra_binaries(self):
|
||||
delete_files = [
|
||||
"AutoPkg-Assets.pkg",
|
||||
"AutoPkg-Assets.pkg.zip",
|
||||
"InstallAssistant.pkg",
|
||||
"InstallAssistant.pkg.integrityDataV1",
|
||||
"KDK.dmg",
|
||||
|
||||
def _delete_extra_binaries(self):
|
||||
"""
|
||||
Delete extra binaries from payloads directory
|
||||
"""
|
||||
|
||||
whitelist_folders = [
|
||||
"ACPI",
|
||||
"Config",
|
||||
"Drivers",
|
||||
"Icon",
|
||||
"InstallPackage",
|
||||
"Kexts",
|
||||
"OpenCore",
|
||||
"Tools",
|
||||
]
|
||||
|
||||
whitelist_files = [
|
||||
"com.dortania.opencore-legacy-patcher.auto-patch.plist",
|
||||
"entitlements.plist",
|
||||
"launcher.sh",
|
||||
"OC-Patcher-TUI.icns",
|
||||
"OC-Patcher.icns",
|
||||
"Universal-Binaries.zip",
|
||||
]
|
||||
|
||||
|
||||
print("- Deleting extra binaries...")
|
||||
for file in Path("payloads").glob(pattern="*"):
|
||||
if file.name in delete_files or file.name.startswith("OpenCore-Legacy-Patcher"):
|
||||
if file.is_dir():
|
||||
if file.name in whitelist_folders:
|
||||
continue
|
||||
print(f" - Deleting {file.name}")
|
||||
file.unlink()
|
||||
elif (Path(file) / Path("Contents/Resources/createinstallmedia")).exists():
|
||||
print(f" - Deleting {file}")
|
||||
subprocess.run(["rm", "-rf", file])
|
||||
elif Path(file).is_dir() and file.name == "Universal-Binaries":
|
||||
print(f" - Deleting {file}")
|
||||
subprocess.run(["rm", "-rf", file])
|
||||
else:
|
||||
if file.name in whitelist_files:
|
||||
continue
|
||||
print(f" - Deleting {file.name}")
|
||||
subprocess.run(["rm", "-f", file])
|
||||
|
||||
|
||||
def _download_resources(self):
|
||||
"""
|
||||
Download required dependencies
|
||||
"""
|
||||
|
||||
def download_resources(self):
|
||||
patcher_support_pkg_version = constants.Constants().patcher_support_pkg_version
|
||||
required_resources = [
|
||||
"Universal-Binaries.zip"
|
||||
@@ -181,21 +234,29 @@ class create_binary:
|
||||
print(mv_output.stderr.decode('utf-8'))
|
||||
raise Exception("Move failed")
|
||||
|
||||
def generate_payloads_dmg(self):
|
||||
|
||||
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.args.reset_binaries:
|
||||
print(" - Removing old payloads.dmg")
|
||||
rm_output = subprocess.run(
|
||||
["rm", "-rf", "./payloads.dmg"],
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
||||
)
|
||||
if rm_output.returncode != 0:
|
||||
print("- Remove failed")
|
||||
print(rm_output.stderr.decode('utf-8'))
|
||||
raise Exception("Remove failed")
|
||||
else:
|
||||
if not self.args.reset_binaries:
|
||||
print(" - payloads.dmg already exists, skipping creation")
|
||||
return
|
||||
|
||||
print(" - Removing old payloads.dmg")
|
||||
rm_output = subprocess.run(
|
||||
["rm", "-rf", "./payloads.dmg"],
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
||||
)
|
||||
if rm_output.returncode != 0:
|
||||
print("- Remove failed")
|
||||
print(rm_output.stderr.decode('utf-8'))
|
||||
raise Exception("Remove failed")
|
||||
|
||||
print(" - Generating DMG...")
|
||||
dmg_output = subprocess.run([
|
||||
'hdiutil', 'create', './payloads.dmg',
|
||||
@@ -213,7 +274,12 @@ class create_binary:
|
||||
|
||||
print(" - DMG generation complete")
|
||||
|
||||
def add_commit_data(self):
|
||||
|
||||
def _add_commit_data(self):
|
||||
"""
|
||||
Add commit data to Info.plist
|
||||
"""
|
||||
|
||||
if not self.args.branch and not self.args.commit and not self.args.commit_date:
|
||||
print(" - No commit data provided, adding source info")
|
||||
branch = "Built from source"
|
||||
@@ -233,20 +299,25 @@ class create_binary:
|
||||
}
|
||||
plistlib.dump(plist, Path(plist_path).open("wb"), sort_keys=True)
|
||||
|
||||
def patch_load_command(self):
|
||||
# Patches 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
|
||||
|
||||
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
|
||||
|
||||
"""
|
||||
|
||||
print(" - Patching LC_VERSION_MIN_MACOSX")
|
||||
path = './dist/OpenCore-Patcher.app/Contents/MacOS/OpenCore-Patcher'
|
||||
find = b'\x00\x0D\x0A\x00' # 10.13 (0xA0D)
|
||||
@@ -257,19 +328,12 @@ class create_binary:
|
||||
with open(path, 'wb') as f:
|
||||
f.write(data)
|
||||
|
||||
def move_launcher(self):
|
||||
print(" - Adding TUI launcher")
|
||||
mv_output = subprocess.run(
|
||||
["cp", "./payloads/launcher.sh", "./dist/OpenCore-Patcher.app/Contents/MacOS/Launcher"],
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
||||
)
|
||||
if mv_output.returncode != 0:
|
||||
print(" - Move failed")
|
||||
print(mv_output.stderr.decode('utf-8'))
|
||||
raise Exception("Move failed")
|
||||
|
||||
def post_flight_cleanup(self):
|
||||
# Remove ./dist/OpenCore-Patcher
|
||||
def _post_flight_cleanup(self):
|
||||
"""
|
||||
Post flight cleanup
|
||||
"""
|
||||
|
||||
path = "./dist/OpenCore-Patcher"
|
||||
print(f" - Removing {path}")
|
||||
rm_output = subprocess.run(
|
||||
@@ -281,9 +345,12 @@ class create_binary:
|
||||
print(rm_output.stderr.decode('utf-8'))
|
||||
raise Exception(f"Remove failed: {path}")
|
||||
|
||||
def mini_validate(self):
|
||||
# Ensure binary can start
|
||||
# Only build a single config, TUI CI will do in-depth validation
|
||||
|
||||
def _mini_validate(self):
|
||||
"""
|
||||
Validate generated binary
|
||||
"""
|
||||
|
||||
print(" - Validating binary")
|
||||
validate_output = subprocess.run(
|
||||
["./dist/OpenCore-Patcher.app/Contents/MacOS/OpenCore-Patcher", "--build", "--model", "MacPro3,1"],
|
||||
@@ -294,5 +361,6 @@ class create_binary:
|
||||
print(validate_output.stderr.decode('utf-8'))
|
||||
raise Exception("Validation failed")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
create_binary()
|
||||
CreateBinary()
|
||||
@@ -25,7 +25,10 @@
|
||||
- Allows for more reliable network calls and downloads
|
||||
- Better supports network timeouts and disconnects
|
||||
- Dramatically less noise in console during downloads
|
||||
- Removed unused sys_patch_downloader.py module
|
||||
- Removed unused modules:
|
||||
- sys_patch_downloader.py
|
||||
- run.py
|
||||
- TUI modules
|
||||
- Build Server Changes:
|
||||
- Upgrade Python backend to 3.10.9
|
||||
- Upgrade Python modules:
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
from resources import main
|
||||
|
||||
if __name__ == '__main__':
|
||||
main.OpenCoreLegacyPatcher(True)
|
||||
main.OpenCoreLegacyPatcher()
|
||||
@@ -1,6 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# Copyright (C) 2020-2022, Dhinak G, Mykola Grymalyuk
|
||||
from resources import main
|
||||
|
||||
if __name__ == '__main__':
|
||||
main.OpenCoreLegacyPatcher()
|
||||
@@ -1,44 +0,0 @@
|
||||
# -*- mode: python ; coding: utf-8 -*-
|
||||
import sys, os
|
||||
sys.path.append(os.path.abspath(os.getcwd()))
|
||||
from resources import constants
|
||||
block_cipher = None
|
||||
|
||||
|
||||
a = Analysis(['OpenCore-Patcher.command'],
|
||||
pathex=['resources', 'data'],
|
||||
binaries=[],
|
||||
datas=[('payloads', 'payloads')],
|
||||
hiddenimports=[],
|
||||
hookspath=[],
|
||||
runtime_hooks=[],
|
||||
excludes=['wxPython', 'wxpython'],
|
||||
win_no_prefer_redirects=False,
|
||||
win_private_assemblies=False,
|
||||
cipher=block_cipher,
|
||||
noarchive=False)
|
||||
pyz = PYZ(a.pure, a.zipped_data,
|
||||
cipher=block_cipher)
|
||||
exe = EXE(pyz,
|
||||
a.scripts,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
[],
|
||||
name='OpenCore-Patcher',
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
upx=True,
|
||||
upx_exclude=[],
|
||||
runtime_tmpdir=None,
|
||||
console=True )
|
||||
app = BUNDLE(exe,
|
||||
name='OpenCore-Patcher.app',
|
||||
icon="payloads/OC-Patcher-TUI.icns",
|
||||
bundle_identifier="com.dortania.opencore-legacy-patcher-tui",
|
||||
info_plist={
|
||||
"CFBundleShortVersionString": constants.Constants().patcher_version,
|
||||
"CFBundleExecutable": "MacOS/Launcher",
|
||||
"NSHumanReadableCopyright": constants.Constants().copyright_date,
|
||||
})
|
||||
@@ -55,7 +55,7 @@ See `-h`/`--help` for more information on supported CLI arguments.
|
||||
|
||||
## Generating prebuilt binaries
|
||||
|
||||
The main goal of generating prebuilt binaries is to strip the requirement of a local python installation for users. For developers, there's very little benefit besides enabling dark mode support in the GUI. For development, simply use the OpenCore-Patcher.command file with a python3 installation.
|
||||
The main goal of generating prebuilt binaries is to strip the requirement of a local python installation for users. For developers, there's very little benefit besides enabling dark mode support in the GUI. For development, simply use the OpenCore-Patcher-GUI.command file with a python3 installation.
|
||||
|
||||
* Note that due to PyInstaller's linking mechanism, binaries generated on Catalina and newer are not compatible with High Sierra and older
|
||||
* To ensure the largest compatibility, generate binaries on macOS Mojave. These binaries will be compatible with macOS 10.9 to macOS 12.
|
||||
@@ -68,7 +68,6 @@ pip3 install pyinstaller
|
||||
cd ~/Developer/OpenCore-Legacy-Patcher/
|
||||
# Create the pyinstaller based Application
|
||||
# Optional Arguments
|
||||
# '--build_tui': Create TUI vairant (deprecated)
|
||||
# '--reset_binaries': Redownload and generate support files
|
||||
python3 Build-Binary.command
|
||||
# Open build folder
|
||||
|
||||
@@ -6,19 +6,20 @@
|
||||
# - 0x3 used in 11.0.1 dyld source:
|
||||
# - https://github.com/apple-oss-distributions/dyld/blob/5c9192436bb195e7a8fe61f22a229ee3d30d8222/testing/test-cases/kernel-hello-world.dtest/main.c#L2
|
||||
|
||||
class apple_mobile_file_integrity:
|
||||
import enum
|
||||
|
||||
class AppleMobileFileIntegrity(enum.IntEnum):
|
||||
# Names set are solely for readability
|
||||
# Internal names are unknown
|
||||
amfi_values = {
|
||||
"AMFI_ALLOW_TASK_FOR_PID": False, # 0x1 - Allow Task for PID (alt. amfi_unrestrict_task_for_pid=0x1)
|
||||
"AMFI_ALLOW_INVALID_SIGNATURE": False, # 0x2 - Reduce sig enforcement (alt. amfi_allow_any_signature=0x1)
|
||||
"AMFI_LV_ENFORCE_THIRD_PARTY": False, # 0x4 - Don't mark external binaries as platform binaries
|
||||
"AMFI_UNKNOWN_1": False, # 0x8
|
||||
"AMFI_UNKNOWN_2": False, # 0x10
|
||||
"AMFI_UNKNOWN_3": False, # 0x20
|
||||
"AMFI_UNKNOWN_4": False, # 0x40
|
||||
"AMFI_ALLOW_EVERYTHING": False, # 0x80 - Disable sig enforcement and Library Validation (alt. amfi_get_out_of_my_way=0x1)
|
||||
},
|
||||
AMFI_ALLOW_TASK_FOR_PID: int = 0x1 # Allow Task for PID (alt. amfi_unrestrict_task_for_pid=0x1)
|
||||
AMFI_ALLOW_INVALID_SIGNATURE: int = 0x2 # Reduce sig enforcement (alt. amfi_allow_any_signature=0x1)
|
||||
AMFI_LV_ENFORCE_THIRD_PARTY: int = 0x4 # Don't mark external binaries as platform binaries
|
||||
AMFI_UNKNOWN_1: int = 0x8
|
||||
AMFI_UNKNOWN_2: int = 0x10
|
||||
AMFI_UNKNOWN_3: int = 0x20
|
||||
AMFI_UNKNOWN_4: int = 0x40
|
||||
AMFI_ALLOW_EVERYTHING: int = 0x80 # Disable sig enforcement and Library Validation (alt. amfi_get_out_of_my_way=0x1)
|
||||
|
||||
|
||||
# Internally within AMFI.kext, Apple references 0x2 and 0x80 as both 'Disable signature enforcement'
|
||||
# However 0x80 is a higher privilege than 0x2, and breaks TCC support in OS (ex. Camera, Microphone, etc prompts)
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
# Mirrors of Apple's InstallAssistant.pkg
|
||||
# Currently only listing important Installers no longer on Apple's servers
|
||||
|
||||
Install_macOS_Big_Sur_11_2_3 = {
|
||||
"Version": "11.2.3",
|
||||
"Build": "20D91",
|
||||
"Link": "https://archive.org/download/install-assistant-20D91/InstallAssistant.pkg",
|
||||
"Size": 12211077798,
|
||||
"Source": "Archive.org",
|
||||
"integrity": None,
|
||||
}
|
||||
@@ -22,7 +22,7 @@ Regarding OS support, see below:
|
||||
|
||||
| Support Entry | Supported OSes | Description | Comment |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| HostOS | macOS 10.9 - macOS 13 | Refers to OSes where running OpenCore-Patcher.app are supported | Supports 10.7+ if [Python 3.9 or higher](https://www.python.org/downloads/) is manually installed, simply run the `OpenCore-Patcher.command` located in the repo |
|
||||
| HostOS | macOS 10.9 - macOS 13 | Refers to OSes where running OpenCore-Patcher.app are supported | Supports 10.7+ if [Python 3.9 or higher](https://www.python.org/downloads/) is manually installed, simply run the `OpenCore-Patcher-GUI.command` located in the repo |
|
||||
| TargetOS | macOS 11 - macOS 13 | Refers to OSes that can be patched to run with OpenCore | May support 10.4 and newer (in a potentially broken state). No support provided. |
|
||||
|
||||
### MacBook
|
||||
|
||||
@@ -20,14 +20,11 @@ And voila! No more USB drive required.
|
||||
|
||||
To do this, run the OpenCore Patcher and head to Patcher Settings:
|
||||
|
||||
| GUI Settings | TUI Settings
|
||||
| :--- | :--- |
|
||||
| |  |
|
||||

|
||||
|
||||
Here you can change different patcher settings, however the main interest is:
|
||||
|
||||
* Show Boot Picker (GUI)
|
||||
* Set ShowPicker Mode (TUI)
|
||||
* Show Boot Picker
|
||||
|
||||
Once you've toggled them both off, build your OpenCore EFI once again and install to your desired drive. Now to show the OpenCore selector, you can simply hold down the "ESC" key while clicking on EFI boot, and then you can release the "ESC" key when you see the cursor arrow at the top left.
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
import enum
|
||||
from resources import utilities
|
||||
from data import amfi_data
|
||||
|
||||
|
||||
class AmfiConfigDetectLevel(enum.IntEnum):
|
||||
@@ -67,32 +68,31 @@ class AmfiConfigurationDetection:
|
||||
|
||||
amfi_value = 0
|
||||
for arg in self.boot_args:
|
||||
if arg.startswith("amfi="):
|
||||
try:
|
||||
amfi_value = arg.split("=")
|
||||
if len(amfi_value) != 2:
|
||||
return
|
||||
amfi_value = amfi_value[1]
|
||||
if amfi_value.startswith("0x"):
|
||||
amfi_value = int(amfi_value, 16)
|
||||
else:
|
||||
amfi_value = int(amfi_value)
|
||||
except:
|
||||
if not arg.startswith("amfi="):
|
||||
continue
|
||||
try:
|
||||
amfi_value = arg.split("=")
|
||||
if len(amfi_value) != 2:
|
||||
return
|
||||
break
|
||||
amfi_value = amfi_value[1]
|
||||
if amfi_value.startswith("0x"):
|
||||
amfi_value = int(amfi_value, 16)
|
||||
else:
|
||||
amfi_value = int(amfi_value)
|
||||
except:
|
||||
return
|
||||
break
|
||||
|
||||
if amfi_value == 0:
|
||||
return
|
||||
|
||||
if amfi_value & 0x1:
|
||||
self.AMFI_ALLOW_TASK_FOR_PID = True
|
||||
if amfi_value & 0x2:
|
||||
self.AMFI_ALLOW_INVALID_SIGNATURE = True
|
||||
if amfi_value & 0x4:
|
||||
self.AMFI_LV_ENFORCE_THIRD_PARTY = True
|
||||
if amfi_value & 0x80:
|
||||
self.AMFI_ALLOW_EVERYTHING = True
|
||||
self.SKIP_LIBRARY_VALIDATION = True
|
||||
self.AMFI_ALLOW_TASK_FOR_PID: bool = amfi_value & amfi_data.AppleMobileFileIntegrity.AMFI_ALLOW_TASK_FOR_PID
|
||||
self.AMFI_ALLOW_INVALID_SIGNATURE: bool = amfi_value & amfi_data.AppleMobileFileIntegrity.AMFI_ALLOW_INVALID_SIGNATURE
|
||||
self.AMFI_LV_ENFORCE_THIRD_PARTY: bool = amfi_value & amfi_data.AppleMobileFileIntegrity.AMFI_LV_ENFORCE_THIRD_PARTY
|
||||
|
||||
if amfi_value & amfi_data.AppleMobileFileIntegrity.AMFI_ALLOW_EVERYTHING:
|
||||
self.AMFI_ALLOW_EVERYTHING = True
|
||||
self.SKIP_LIBRARY_VALIDATION = True
|
||||
self.AMFI_ALLOW_INVALID_SIGNATURE = True
|
||||
|
||||
|
||||
|
||||
@@ -1,116 +1,190 @@
|
||||
import sys
|
||||
from resources import defaults, utilities, validation
|
||||
from resources.sys_patch import sys_patch, sys_patch_auto
|
||||
from resources.build import build
|
||||
from data import model_array
|
||||
import threading
|
||||
import time
|
||||
import logging
|
||||
import sys
|
||||
|
||||
from resources import defaults, utilities, validation, constants
|
||||
from resources.sys_patch import sys_patch, sys_patch_auto
|
||||
from resources.build import build
|
||||
from data import model_array
|
||||
|
||||
|
||||
# Generic building args
|
||||
class arguments:
|
||||
def __init__(self):
|
||||
|
||||
def __init__(self, global_constants: constants.Constants):
|
||||
self.constants: constants.Constants = global_constants
|
||||
|
||||
self.args = utilities.check_cli_args()
|
||||
|
||||
def parse_arguments(self, settings):
|
||||
self._parse_arguments()
|
||||
|
||||
|
||||
def _parse_arguments(self):
|
||||
"""
|
||||
Parses arguments passed to the patcher
|
||||
"""
|
||||
|
||||
if self.args.validate:
|
||||
validation.validate(settings)
|
||||
elif self.args.build:
|
||||
self._validation_handler()
|
||||
return
|
||||
|
||||
if self.args.build:
|
||||
self._build_handler()
|
||||
return
|
||||
|
||||
if self.args.patch_sys_vol:
|
||||
self._sys_patch_handler()
|
||||
return
|
||||
|
||||
if self.args.unpatch_sys_vol:
|
||||
self._sys_unpatch_handler()
|
||||
return
|
||||
|
||||
if self.args.auto_patch:
|
||||
self._sys_patch_auto_handler()
|
||||
return
|
||||
|
||||
|
||||
def _validation_handler(self):
|
||||
"""
|
||||
Enter validation mode
|
||||
"""
|
||||
|
||||
validation.PatcherValidation(self.constants)
|
||||
|
||||
|
||||
def _sys_patch_handler(self):
|
||||
"""
|
||||
Start root volume patching
|
||||
"""
|
||||
|
||||
logging.info("- Set System Volume patching")
|
||||
if "Library/InstallerSandboxes/" in str(self.constants.payload_path):
|
||||
logging.info("- Running from Installer Sandbox")
|
||||
thread = threading.Thread(target=sys_patch.PatchSysVolume(self.constants.custom_model or self.constants.computer.real_model, self.constants, None).start_patch)
|
||||
thread.start()
|
||||
while thread.is_alive():
|
||||
utilities.block_os_updaters()
|
||||
time.sleep(1)
|
||||
else:
|
||||
sys_patch.PatchSysVolume(self.constants.custom_model or self.constants.computer.real_model, self.constants, None).start_patch()
|
||||
|
||||
|
||||
def _sys_unpatch_handler(self):
|
||||
"""
|
||||
Start root volume unpatching
|
||||
"""
|
||||
logging.info("- Set System Volume unpatching")
|
||||
sys_patch.PatchSysVolume(self.constants.custom_model or self.constants.computer.real_model, self.constants, None).start_unpatch()
|
||||
|
||||
|
||||
def _sys_patch_auto_handler(self):
|
||||
"""
|
||||
Start root volume auto patching
|
||||
"""
|
||||
|
||||
logging.info("- Set Auto patching")
|
||||
sys_patch_auto.AutomaticSysPatch(self.constants).start_auto_patch()
|
||||
|
||||
|
||||
def _build_handler(self):
|
||||
"""
|
||||
Start config building process
|
||||
"""
|
||||
|
||||
if self.args.model:
|
||||
if self.args.model:
|
||||
if self.args.model:
|
||||
logging.info(f"- Using custom model: {self.args.model}")
|
||||
settings.custom_model = self.args.model
|
||||
defaults.generate_defaults(settings.custom_model, False, settings)
|
||||
elif settings.computer.real_model not in model_array.SupportedSMBIOS and settings.allow_oc_everywhere is False:
|
||||
logging.info(
|
||||
"""Your model is not supported by this patcher for running unsupported OSes!"
|
||||
logging.info(f"- Using custom model: {self.args.model}")
|
||||
self.constants.custom_model = self.args.model
|
||||
defaults.GenerateDefaults(self.constants.custom_model, False, self.constants)
|
||||
elif self.constants.computer.real_model not in model_array.SupportedSMBIOS and self.constants.allow_oc_everywhere is False:
|
||||
logging.info(
|
||||
"""Your model is not supported by this patcher for running unsupported OSes!"
|
||||
|
||||
If you plan to create the USB for another machine, please select the "Change Model" option in the menu."""
|
||||
)
|
||||
sys.exit(1)
|
||||
else:
|
||||
logging.info(f"- Using detected model: {settings.computer.real_model}")
|
||||
defaults.generate_defaults(settings.custom_model, True, settings)
|
||||
If you plan to create the USB for another machine, please select the "Change Model" option in the menu."""
|
||||
)
|
||||
sys.exit(1)
|
||||
else:
|
||||
logging.info(f"- Using detected model: {self.constants.computer.real_model}")
|
||||
defaults.GenerateDefaults(self.constants.custom_model, True, self.constants)
|
||||
|
||||
if self.args.disk:
|
||||
logging.info(f"- Install Disk set: {self.args.disk}")
|
||||
settings.disk = self.args.disk
|
||||
if self.args.verbose:
|
||||
logging.info("- Set verbose configuration")
|
||||
settings.verbose_debug = True
|
||||
else:
|
||||
settings.verbose_debug = False # Override Defaults detected
|
||||
if self.args.debug_oc:
|
||||
logging.info("- Set OpenCore DEBUG configuration")
|
||||
settings.opencore_debug = True
|
||||
settings.opencore_build = "DEBUG"
|
||||
if self.args.debug_kext:
|
||||
logging.info("- Set kext DEBUG configuration")
|
||||
settings.kext_debug = True
|
||||
if self.args.hide_picker:
|
||||
logging.info("- Set HidePicker configuration")
|
||||
settings.showpicker = False
|
||||
if self.args.disable_sip:
|
||||
logging.info("- Set Disable SIP configuration")
|
||||
settings.sip_status = False
|
||||
else:
|
||||
settings.sip_status = True # Override Defaults detected
|
||||
if self.args.disable_smb:
|
||||
logging.info("- Set Disable SecureBootModel configuration")
|
||||
settings.secure_status = False
|
||||
else:
|
||||
settings.secure_status = True # Override Defaults detected
|
||||
if self.args.vault:
|
||||
logging.info("- Set Vault configuration")
|
||||
settings.vault = True
|
||||
if self.args.firewire:
|
||||
logging.info("- Set FireWire Boot configuration")
|
||||
settings.firewire_boot = True
|
||||
if self.args.nvme:
|
||||
logging.info("- Set NVMe Boot configuration")
|
||||
settings.nvme_boot = True
|
||||
if self.args.wlan:
|
||||
logging.info("- Set Wake on WLAN configuration")
|
||||
settings.enable_wake_on_wlan = True
|
||||
if self.args.disable_tb:
|
||||
logging.info("- Set Disable Thunderbolt configuration")
|
||||
settings.disable_tb = True
|
||||
if self.args.force_surplus:
|
||||
logging.info("- Forcing SurPlus override configuration")
|
||||
settings.force_surplus = True
|
||||
if self.args.moderate_smbios:
|
||||
logging.info("- Set Moderate SMBIOS Patching configuration")
|
||||
settings.serial_settings = "Moderate"
|
||||
if self.args.smbios_spoof:
|
||||
if self.args.smbios_spoof == "Minimal":
|
||||
settings.serial_settings = "Minimal"
|
||||
elif self.args.smbios_spoof == "Moderate":
|
||||
settings.serial_settings = "Moderate"
|
||||
elif self.args.smbios_spoof == "Advanced":
|
||||
settings.serial_settings = "Advanced"
|
||||
else:
|
||||
logging.info(f"- Unknown SMBIOS arg passed: {self.args.smbios_spoof}")
|
||||
if self.args.disk:
|
||||
logging.info(f"- Install Disk set: {self.args.disk}")
|
||||
self.constants.disk = self.args.disk
|
||||
|
||||
if self.args.support_all:
|
||||
logging.info("- Building for natively supported model")
|
||||
settings.allow_oc_everywhere = True
|
||||
settings.serial_settings = "None"
|
||||
build.build_opencore(settings.custom_model or settings.computer.real_model, settings).build_opencore()
|
||||
elif self.args.patch_sys_vol:
|
||||
logging.info("- Set System Volume patching")
|
||||
if self.args.verbose:
|
||||
logging.info("- Set verbose configuration")
|
||||
self.constants.verbose_debug = True
|
||||
else:
|
||||
self.constants.verbose_debug = False # Override Defaults detected
|
||||
|
||||
if "Library/InstallerSandboxes/" in str(settings.payload_path):
|
||||
logging.info("- Running from Installer Sandbox")
|
||||
thread = threading.Thread(target=sys_patch.PatchSysVolume(settings.custom_model or settings.computer.real_model, settings, None).start_patch)
|
||||
thread.start()
|
||||
while thread.is_alive():
|
||||
utilities.block_os_updaters()
|
||||
time.sleep(1)
|
||||
if self.args.debug_oc:
|
||||
logging.info("- Set OpenCore DEBUG configuration")
|
||||
self.constants.opencore_debug = True
|
||||
self.constants.opencore_build = "DEBUG"
|
||||
|
||||
if self.args.debug_kext:
|
||||
logging.info("- Set kext DEBUG configuration")
|
||||
self.constants.kext_debug = True
|
||||
|
||||
if self.args.hide_picker:
|
||||
logging.info("- Set HidePicker configuration")
|
||||
self.constants.showpicker = False
|
||||
|
||||
if self.args.disable_sip:
|
||||
logging.info("- Set Disable SIP configuration")
|
||||
self.constants.sip_status = False
|
||||
else:
|
||||
self.constants.sip_status = True # Override Defaults detected
|
||||
|
||||
if self.args.disable_smb:
|
||||
logging.info("- Set Disable SecureBootModel configuration")
|
||||
self.constants.secure_status = False
|
||||
else:
|
||||
self.constants.secure_status = True # Override Defaults detected
|
||||
|
||||
if self.args.vault:
|
||||
logging.info("- Set Vault configuration")
|
||||
self.constants.vault = True
|
||||
|
||||
if self.args.firewire:
|
||||
logging.info("- Set FireWire Boot configuration")
|
||||
self.constants.firewire_boot = True
|
||||
|
||||
if self.args.nvme:
|
||||
logging.info("- Set NVMe Boot configuration")
|
||||
self.constants.nvme_boot = True
|
||||
|
||||
if self.args.wlan:
|
||||
logging.info("- Set Wake on WLAN configuration")
|
||||
self.constants.enable_wake_on_wlan = True
|
||||
|
||||
if self.args.disable_tb:
|
||||
logging.info("- Set Disable Thunderbolt configuration")
|
||||
self.constants.disable_tb = True
|
||||
|
||||
if self.args.force_surplus:
|
||||
logging.info("- Forcing SurPlus override configuration")
|
||||
self.constants.force_surplus = True
|
||||
|
||||
if self.args.moderate_smbios:
|
||||
logging.info("- Set Moderate SMBIOS Patching configuration")
|
||||
self.constants.serial_settings = "Moderate"
|
||||
|
||||
if self.args.smbios_spoof:
|
||||
if self.args.smbios_spoof == "Minimal":
|
||||
self.constants.serial_settings = "Minimal"
|
||||
elif self.args.smbios_spoof == "Moderate":
|
||||
self.constants.serial_settings = "Moderate"
|
||||
elif self.args.smbios_spoof == "Advanced":
|
||||
self.constants.serial_settings = "Advanced"
|
||||
else:
|
||||
sys_patch.PatchSysVolume(settings.custom_model or settings.computer.real_model, settings, None).start_patch()
|
||||
elif self.args.unpatch_sys_vol:
|
||||
logging.info("- Set System Volume unpatching")
|
||||
sys_patch.PatchSysVolume(settings.custom_model or settings.computer.real_model, settings, None).start_unpatch()
|
||||
elif self.args.auto_patch:
|
||||
logging.info("- Set Auto patching")
|
||||
sys_patch_auto.AutomaticSysPatch(settings).start_auto_patch()
|
||||
logging.info(f"- Unknown SMBIOS arg passed: {self.args.smbios_spoof}")
|
||||
|
||||
if self.args.support_all:
|
||||
logging.info("- Building for natively supported model")
|
||||
self.constants.allow_oc_everywhere = True
|
||||
self.constants.serial_settings = "None"
|
||||
|
||||
build.build_opencore(self.constants.custom_model or self.constants.computer.real_model, self.constants).build_opencore()
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,29 +1,40 @@
|
||||
# Parse Commit Info from binary's info.plist
|
||||
# App Structure:
|
||||
# OpenCore-Patcher.app:
|
||||
# Contents:
|
||||
# MacOS:
|
||||
# OpenCore-Patcher
|
||||
# Info.plist
|
||||
|
||||
from pathlib import Path
|
||||
import plistlib
|
||||
|
||||
class commit_info:
|
||||
class ParseCommitInfo:
|
||||
|
||||
def __init__(self, binary_path: str):
|
||||
"""
|
||||
Parameters:
|
||||
binary_path (str): Path to binary
|
||||
"""
|
||||
|
||||
def __init__(self, binary_path):
|
||||
self.binary_path = str(binary_path)
|
||||
self.plist_path = self.convert_binary_path_to_plist_path()
|
||||
self.plist_path = self._convert_binary_path_to_plist_path()
|
||||
|
||||
|
||||
def convert_binary_path_to_plist_path(self):
|
||||
def _convert_binary_path_to_plist_path(self):
|
||||
"""
|
||||
Resolve Info.plist path from binary path
|
||||
"""
|
||||
|
||||
if Path(self.binary_path).exists():
|
||||
plist_path = self.binary_path.replace("MacOS/OpenCore-Patcher", "Info.plist")
|
||||
if Path(plist_path).exists() and plist_path.endswith(".plist"):
|
||||
return plist_path
|
||||
return None
|
||||
|
||||
|
||||
def generate_commit_info(self):
|
||||
"""
|
||||
Generate commit info from Info.plist
|
||||
|
||||
Returns:
|
||||
tuple: (Branch, Commit Date, Commit URL)
|
||||
"""
|
||||
|
||||
if self.plist_path:
|
||||
plist_info = plistlib.load(Path(self.plist_path).open("rb"))
|
||||
if "Github" in plist_info:
|
||||
|
||||
@@ -1,36 +1,52 @@
|
||||
# Generate Default Data
|
||||
from resources import utilities, device_probe, generate_smbios, global_settings
|
||||
from data import smbios_data, cpu_data, os_data
|
||||
import subprocess
|
||||
|
||||
from resources import (
|
||||
utilities,
|
||||
device_probe,
|
||||
generate_smbios,
|
||||
global_settings,
|
||||
constants
|
||||
)
|
||||
from data import (
|
||||
smbios_data,
|
||||
cpu_data,
|
||||
os_data
|
||||
)
|
||||
|
||||
class generate_defaults:
|
||||
|
||||
def __init__(self, model, host_is_target, settings):
|
||||
self.model = model
|
||||
self.constants = settings
|
||||
self.host_is_target = host_is_target
|
||||
class GenerateDefaults:
|
||||
|
||||
def __init__(self, model: str, host_is_target: bool, global_constants: constants.Constants):
|
||||
self.constants: constants.Constants = global_constants
|
||||
|
||||
self.model: str = model
|
||||
self.host_is_target: str = host_is_target
|
||||
|
||||
# Reset Variables
|
||||
self.constants.sip_status = True
|
||||
self.constants.secure_status = False
|
||||
self.constants.disable_cs_lv = False
|
||||
self.constants.disable_amfi = False
|
||||
self.constants.fu_status = True
|
||||
self.constants.fu_arguments = None
|
||||
self.constants.sip_status: bool = True
|
||||
self.constants.secure_status: bool = False
|
||||
self.constants.disable_cs_lv: bool = False
|
||||
self.constants.disable_amfi: bool = False
|
||||
self.constants.fu_status: bool = True
|
||||
|
||||
self.constants.custom_serial_number = ""
|
||||
self.constants.custom_board_serial_number = ""
|
||||
self.constants.fu_arguments: str = None
|
||||
|
||||
self.general_probe()
|
||||
self.nvram_probe()
|
||||
self.gpu_probe()
|
||||
self.networking_probe()
|
||||
self.misc_hardwares_probe()
|
||||
self.smbios_probe()
|
||||
self.constants.custom_serial_number: str = ""
|
||||
self.constants.custom_board_serial_number: str = ""
|
||||
|
||||
self._general_probe()
|
||||
self._nvram_probe()
|
||||
self._gpu_probe()
|
||||
self._networking_probe()
|
||||
self._misc_hardwares_probe()
|
||||
self._smbios_probe()
|
||||
|
||||
|
||||
def general_probe(self):
|
||||
def _general_probe(self):
|
||||
"""
|
||||
General probe for data
|
||||
"""
|
||||
|
||||
if "Book" in self.model:
|
||||
self.constants.set_content_caching = False
|
||||
@@ -40,11 +56,11 @@ class generate_defaults:
|
||||
if self.model in ["MacBookPro8,2", "MacBookPro8,3"]:
|
||||
# Users disabling TS2 most likely have a faulty dGPU
|
||||
# users can override this in settings
|
||||
ts2_status = global_settings.global_settings().read_property("MacBookPro_TeraScale_2_Accel")
|
||||
ts2_status = global_settings.GlobalEnviromentSettings().read_property("MacBookPro_TeraScale_2_Accel")
|
||||
if ts2_status is True:
|
||||
self.constants.allow_ts2_accel = True
|
||||
else:
|
||||
global_settings.global_settings().write_property("MacBookPro_TeraScale_2_Accel", False)
|
||||
global_settings.GlobalEnviromentSettings().write_property("MacBookPro_TeraScale_2_Accel", False)
|
||||
self.constants.allow_ts2_accel = False
|
||||
|
||||
if self.model in smbios_data.smbios_dictionary:
|
||||
@@ -61,14 +77,19 @@ class generate_defaults:
|
||||
# Check if running in RecoveryOS
|
||||
self.constants.recovery_status = utilities.check_recovery()
|
||||
|
||||
if global_settings.global_settings().read_property("Force_Web_Drivers") is True:
|
||||
if global_settings.GlobalEnviromentSettings().read_property("Force_Web_Drivers") is True:
|
||||
self.constants.force_nv_web = True
|
||||
|
||||
result = global_settings.global_settings().read_property("ShouldNukeKDKs")
|
||||
result = global_settings.GlobalEnviromentSettings().read_property("ShouldNukeKDKs")
|
||||
if result is False:
|
||||
self.constants.should_nuke_kdks = False
|
||||
|
||||
def smbios_probe(self):
|
||||
|
||||
def _smbios_probe(self):
|
||||
"""
|
||||
SMBIOS specific probe
|
||||
"""
|
||||
|
||||
if not self.host_is_target:
|
||||
if self.model in ["MacPro4,1", "MacPro5,1"]:
|
||||
# Allow H.265 on AMD
|
||||
@@ -99,7 +120,11 @@ class generate_defaults:
|
||||
self.constants.force_vmm = False
|
||||
|
||||
|
||||
def nvram_probe(self):
|
||||
def _nvram_probe(self):
|
||||
"""
|
||||
NVRAM specific probe
|
||||
"""
|
||||
|
||||
if not self.host_is_target:
|
||||
return
|
||||
|
||||
@@ -120,7 +145,11 @@ class generate_defaults:
|
||||
self.constants.custom_cpu_model_value = custom_cpu_model_value.split("%00")[0]
|
||||
|
||||
|
||||
def networking_probe(self):
|
||||
def _networking_probe(self):
|
||||
"""
|
||||
Networking specific probe
|
||||
"""
|
||||
|
||||
if self.host_is_target:
|
||||
if not (
|
||||
(
|
||||
@@ -157,7 +186,11 @@ class generate_defaults:
|
||||
self.constants.fu_status = True
|
||||
self.constants.fu_arguments = " -disable_sidecar_mac"
|
||||
|
||||
def misc_hardwares_probe(self):
|
||||
|
||||
def _misc_hardwares_probe(self):
|
||||
"""
|
||||
Misc probe
|
||||
"""
|
||||
if self.host_is_target:
|
||||
if self.constants.computer.usb_controllers:
|
||||
if self.model in smbios_data.smbios_dictionary:
|
||||
@@ -170,7 +203,11 @@ class generate_defaults:
|
||||
break
|
||||
|
||||
|
||||
def gpu_probe(self):
|
||||
def _gpu_probe(self):
|
||||
"""
|
||||
Graphics specific probe
|
||||
"""
|
||||
|
||||
gpu_dict = []
|
||||
if self.host_is_target:
|
||||
gpu_dict = self.constants.computer.gpus
|
||||
|
||||
@@ -9,18 +9,23 @@ import logging
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
class global_settings:
|
||||
|
||||
class GlobalEnviromentSettings:
|
||||
"""
|
||||
Library for querying and writing global enviroment settings
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.file_name = ".com.dortania.opencore-legacy-patcher.plist"
|
||||
self.global_settings_folder = "/Users/Shared"
|
||||
self.global_settings_plist = f"{self.global_settings_folder}/{self.file_name}"
|
||||
self.file_name: str = ".com.dortania.opencore-legacy-patcher.plist"
|
||||
self.global_settings_folder: str = "/Users/Shared"
|
||||
self.global_settings_plist: str = f"{self.global_settings_folder}/{self.file_name}"
|
||||
|
||||
self._generate_settings_file()
|
||||
self._convert_defaults_to_global_settings()
|
||||
self._fix_file_permission()
|
||||
|
||||
|
||||
def read_property(self, property_name):
|
||||
def read_property(self, property_name: str):
|
||||
"""
|
||||
Reads a property from the global settings file
|
||||
"""
|
||||
@@ -32,7 +37,7 @@ class global_settings:
|
||||
return None
|
||||
|
||||
|
||||
def write_property(self, property_name, property_value):
|
||||
def write_property(self, property_name: str, property_value):
|
||||
"""
|
||||
Writes a property to the global settings file
|
||||
"""
|
||||
|
||||
@@ -33,7 +33,6 @@ from resources import (
|
||||
install,
|
||||
installer,
|
||||
utilities,
|
||||
run,
|
||||
generate_smbios,
|
||||
updates,
|
||||
integrity_verification,
|
||||
@@ -256,7 +255,7 @@ class wx_python_gui:
|
||||
if local_build_date <= installed_build_date:
|
||||
return False
|
||||
|
||||
elif updates.check_binary_updates(self.constants).check_if_build_newer(local_version, application_version) is False:
|
||||
elif updates.CheckBinaryUpdates(self.constants)._check_if_build_newer(local_version, application_version) is False:
|
||||
return False
|
||||
|
||||
# Ask user if they want to move the application to the Applications folder
|
||||
@@ -307,11 +306,11 @@ class wx_python_gui:
|
||||
return
|
||||
|
||||
did_find_update = False
|
||||
ignore_updates = global_settings.global_settings().read_property("IgnoreAppUpdates")
|
||||
ignore_updates = global_settings.GlobalEnviromentSettings().read_property("IgnoreAppUpdates")
|
||||
if ignore_updates is not True:
|
||||
self.constants.ignore_updates = False
|
||||
self.constants.has_checked_updates = True
|
||||
dict = updates.check_binary_updates(self.constants).check_binary_updates()
|
||||
dict = updates.CheckBinaryUpdates(self.constants).check_binary_updates()
|
||||
if dict:
|
||||
for entry in dict:
|
||||
version = dict[entry]["Version"]
|
||||
@@ -331,7 +330,7 @@ class wx_python_gui:
|
||||
elif response == wx.ID_NO:
|
||||
logging.info("- Setting IgnoreAppUpdates to True")
|
||||
self.constants.ignore_updates = True
|
||||
global_settings.global_settings().write_property("IgnoreAppUpdates", True)
|
||||
global_settings.GlobalEnviromentSettings().write_property("IgnoreAppUpdates", True)
|
||||
else:
|
||||
self.constants.ignore_updates = True
|
||||
logging.info("- Ignoring App Updates due to defaults")
|
||||
@@ -589,10 +588,10 @@ class wx_python_gui:
|
||||
if self.constants.start_build_install is True:
|
||||
self.build_install_menu()
|
||||
elif "--gui_patch" in sys.argv:
|
||||
self.patches = sys_patch_detect.detect_root_patch(self.computer.real_model, self.constants).detect_patch_set()
|
||||
self.patches = sys_patch_detect.DetectRootPatch(self.computer.real_model, self.constants).detect_patch_set()
|
||||
self.root_patch_start()
|
||||
elif "--gui_unpatch" in sys.argv:
|
||||
self.patches = sys_patch_detect.detect_root_patch(self.computer.real_model, self.constants).detect_patch_set()
|
||||
self.patches = sys_patch_detect.DetectRootPatch(self.computer.real_model, self.constants).detect_patch_set()
|
||||
self.root_patch_revert()
|
||||
self.finished_auto_patch = True
|
||||
self.constants.start_build_install = False
|
||||
@@ -1079,7 +1078,7 @@ class wx_python_gui:
|
||||
)
|
||||
self.subheader.Centre(wx.HORIZONTAL)
|
||||
|
||||
patches = sys_patch_detect.detect_root_patch(self.computer.real_model, self.constants).detect_patch_set()
|
||||
patches = sys_patch_detect.DetectRootPatch(self.computer.real_model, self.constants).detect_patch_set()
|
||||
self.patches = patches
|
||||
can_unpatch = patches["Validation: Unpatching Possible"]
|
||||
if not any(not patch.startswith("Settings") and not patch.startswith("Validation") and patches[patch] is True for patch in patches):
|
||||
@@ -2310,8 +2309,11 @@ class wx_python_gui:
|
||||
|
||||
def start_script(self):
|
||||
utilities.disable_sleep_while_running()
|
||||
args = [self.constants.oclp_helper_path, "/bin/sh", self.constants.installer_sh_path]
|
||||
output, error, returncode = run.Run()._stream_output(comm=args)
|
||||
args = [self.constants.oclp_helper_path, "/bin/sh", self.constants.installer_sh_path]
|
||||
result = subprocess.run(args, capture_output=True, text=True)
|
||||
output = result.stdout
|
||||
error = result.stderr if result.stderr else ""
|
||||
|
||||
if "Install media now available at" in output:
|
||||
logging.info("- Successfully created macOS installer")
|
||||
while self.download_thread.is_alive():
|
||||
@@ -2645,11 +2647,11 @@ class wx_python_gui:
|
||||
if user_choice == self.computer.real_model:
|
||||
logging.info(f"Using Real Model: {user_choice}")
|
||||
self.constants.custom_model = None
|
||||
defaults.generate_defaults(self.computer.real_model, True, self.constants)
|
||||
defaults.GenerateDefaults(self.computer.real_model, True, self.constants)
|
||||
else:
|
||||
logging.info(f"Using Custom Model: {user_choice}")
|
||||
self.constants.custom_model = user_choice
|
||||
defaults.generate_defaults(self.constants.custom_model, False, self.constants)
|
||||
defaults.GenerateDefaults(self.constants.custom_model, False, self.constants)
|
||||
# Reload Settings
|
||||
self.settings_menu(None)
|
||||
|
||||
@@ -2986,7 +2988,7 @@ class wx_python_gui:
|
||||
else:
|
||||
logging.info("Nuke KDKs disabled")
|
||||
self.constants.should_nuke_kdks = False
|
||||
global_settings.global_settings().write_property("ShouldNukeKDKs", self.constants.should_nuke_kdks)
|
||||
global_settings.GlobalEnviromentSettings().write_property("ShouldNukeKDKs", self.constants.should_nuke_kdks)
|
||||
|
||||
def disable_library_validation_click(self, event):
|
||||
if self.disable_library_validation_checkbox.GetValue():
|
||||
@@ -3009,9 +3011,9 @@ class wx_python_gui:
|
||||
def set_ignore_app_updates_click(self, event):
|
||||
self.constants.ignore_updates = self.set_ignore_app_updates_checkbox.GetValue()
|
||||
if self.constants.ignore_updates is True:
|
||||
global_settings.global_settings().write_property("IgnoreAppUpdates", True)
|
||||
global_settings.GlobalEnviromentSettings().write_property("IgnoreAppUpdates", True)
|
||||
else:
|
||||
global_settings.global_settings().write_property("IgnoreAppUpdates", False)
|
||||
global_settings.GlobalEnviromentSettings().write_property("IgnoreAppUpdates", False)
|
||||
|
||||
def firewire_click(self, event=None):
|
||||
if self.firewire_boot_checkbox.GetValue():
|
||||
@@ -3096,21 +3098,21 @@ class wx_python_gui:
|
||||
def ts2_accel_click(self, event=None):
|
||||
if self.set_terascale_accel_checkbox.GetValue():
|
||||
logging.info("TS2 Acceleration Enabled")
|
||||
global_settings.global_settings().write_property("MacBookPro_TeraScale_2_Accel", True)
|
||||
global_settings.GlobalEnviromentSettings().write_property("MacBookPro_TeraScale_2_Accel", True)
|
||||
self.constants.allow_ts2_accel = True
|
||||
else:
|
||||
logging.info("TS2 Acceleration Disabled")
|
||||
global_settings.global_settings().write_property("MacBookPro_TeraScale_2_Accel", False)
|
||||
global_settings.GlobalEnviromentSettings().write_property("MacBookPro_TeraScale_2_Accel", False)
|
||||
self.constants.allow_ts2_accel = False
|
||||
|
||||
def force_web_drivers_click(self, event=None):
|
||||
if self.force_web_drivers_checkbox.GetValue():
|
||||
logging.info("Force Web Drivers Enabled")
|
||||
global_settings.global_settings().write_property("Force_Web_Drivers", True)
|
||||
global_settings.GlobalEnviromentSettings().write_property("Force_Web_Drivers", True)
|
||||
self.constants.force_nv_web = True
|
||||
else:
|
||||
logging.info("Force Web Drivers Disabled")
|
||||
global_settings.global_settings().write_property("Force_Web_Drivers", False)
|
||||
global_settings.GlobalEnviromentSettings().write_property("Force_Web_Drivers", False)
|
||||
self.constants.force_nv_web = False
|
||||
|
||||
def windows_gmux_click(self, event=None):
|
||||
@@ -3816,19 +3818,19 @@ OpenCore Legacy Patcher by default knows the most ideal
|
||||
self.subheader_4.SetPosition(wx.Point(0, self.subheader2_2.GetPosition().y + self.subheader2_2.GetSize().height+ 5))
|
||||
self.subheader_4.Centre(wx.HORIZONTAL)
|
||||
|
||||
is_dark_menu_bar = subprocess.run(["defaults", "read", "-g", "Moraea_DarkMenuBar"], stdout=subprocess.PIPE).stdout.decode("utf-8").strip()
|
||||
is_dark_menu_bar = subprocess.run(["defaults", "read", "-g", "Moraea_DarkMenuBar"], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL).stdout.decode("utf-8").strip()
|
||||
if is_dark_menu_bar in ["1", "true"]:
|
||||
is_dark_menu_bar = True
|
||||
else:
|
||||
is_dark_menu_bar = False
|
||||
|
||||
is_blur_enabled = subprocess.run(["defaults", "read", "-g", "Moraea_BlurBeta"], stdout=subprocess.PIPE).stdout.decode("utf-8").strip()
|
||||
is_blur_enabled = subprocess.run(["defaults", "read", "-g", "Moraea_BlurBeta"], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL).stdout.decode("utf-8").strip()
|
||||
if is_blur_enabled in ["1", "true"]:
|
||||
is_blur_enabled = True
|
||||
else:
|
||||
is_blur_enabled = False
|
||||
|
||||
is_rim_disabled = subprocess.run(["defaults", "read", "-g", "Moraea_RimBetaDisabled"], stdout=subprocess.PIPE).stdout.decode("utf-8").strip()
|
||||
is_rim_disabled = subprocess.run(["defaults", "read", "-g", "Moraea_RimBetaDisabled"], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL).stdout.decode("utf-8").strip()
|
||||
if is_rim_disabled in ["1", "true"]:
|
||||
is_rim_disabled = True
|
||||
else:
|
||||
|
||||
@@ -8,7 +8,7 @@ import shutil
|
||||
import os
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from resources import utilities, constants, tui_helpers
|
||||
from resources import utilities, constants
|
||||
from data import os_data
|
||||
|
||||
class tui_disk_installation:
|
||||
@@ -75,65 +75,6 @@ class tui_disk_installation:
|
||||
return supported_partitions
|
||||
|
||||
|
||||
def copy_efi(self):
|
||||
utilities.cls()
|
||||
utilities.header(["Installing OpenCore to Drive"])
|
||||
|
||||
if not self.constants.opencore_release_folder.exists():
|
||||
tui_helpers.TUIOnlyLogging.info(
|
||||
["Installing OpenCore to Drive"],
|
||||
"Press [Enter] to go back.\n",
|
||||
[
|
||||
"""OpenCore folder missing!
|
||||
Please build OpenCore first!"""
|
||||
],
|
||||
).start()
|
||||
return
|
||||
|
||||
logging.info("\nDisk picker is loading...")
|
||||
|
||||
all_disks = self.list_disks()
|
||||
menu = tui_helpers.TUIMenu(
|
||||
["Select Disk"],
|
||||
"Please select the disk you would like to install OpenCore to: ",
|
||||
in_between=["Missing disks? Ensure they have an EFI or FAT32 partition."],
|
||||
return_number_instead_of_direct_call=True,
|
||||
loop=True,
|
||||
)
|
||||
for disk in all_disks:
|
||||
menu.add_menu_option(f"{disk}: {all_disks[disk]['name']} ({all_disks[disk]['size']})", key=disk[4:])
|
||||
|
||||
response = menu.start()
|
||||
|
||||
if response == -1:
|
||||
return
|
||||
|
||||
disk_identifier = "disk" + response
|
||||
selected_disk = all_disks[disk_identifier]
|
||||
|
||||
menu = tui_helpers.TUIMenu(
|
||||
["Select Partition"],
|
||||
"Please select the partition you would like to install OpenCore to: ",
|
||||
return_number_instead_of_direct_call=True,
|
||||
loop=True,
|
||||
in_between=["Missing partitions? Ensure they are formatted as an EFI or FAT32.", "", "* denotes likely candidate."],
|
||||
)
|
||||
for partition in selected_disk["partitions"]:
|
||||
if selected_disk["partitions"][partition]["fs"] not in ("msdos", "EFI"):
|
||||
continue
|
||||
text = f"{partition}: {selected_disk['partitions'][partition]['name']} ({utilities.human_fmt(selected_disk['partitions'][partition]['size'])})"
|
||||
if selected_disk["partitions"][partition]["type"] == "EFI" or (
|
||||
selected_disk["partitions"][partition]["type"] == "Microsoft Basic Data" and selected_disk["partitions"][partition]["size"] < 1024 * 1024 * 512
|
||||
): # 512 megabytes:
|
||||
text += " *"
|
||||
menu.add_menu_option(text, key=partition[len(disk_identifier) + 1 :])
|
||||
|
||||
response = menu.start()
|
||||
|
||||
if response == -1:
|
||||
return
|
||||
self.install_opencore(f"{disk_identifier}s{response}")
|
||||
|
||||
def install_opencore(self, full_disk_identifier):
|
||||
def determine_sd_card(media_name):
|
||||
# Array filled with common SD Card names
|
||||
@@ -169,18 +110,13 @@ Please build OpenCore first!"""
|
||||
# cancelled prompt
|
||||
return
|
||||
else:
|
||||
if self.constants.gui_mode is False:
|
||||
tui_helpers.TUIOnlyLogging.info(
|
||||
["Copying OpenCore"], "Press [Enter] to go back.\n", ["An error occurred!"] + result.stderr.decode().split("\n") + [""]
|
||||
).start()
|
||||
else:
|
||||
logging.info("An error occurred!")
|
||||
logging.info(result.stderr.decode())
|
||||
logging.info("An error occurred!")
|
||||
logging.info(result.stderr.decode())
|
||||
|
||||
# Check if we're in Safe Mode, and if so, tell user FAT32 is unsupported
|
||||
if utilities.check_boot_mode() == "safe_boot":
|
||||
logging.info("\nSafe Mode detected. FAT32 is unsupported by macOS in this mode.")
|
||||
logging.info("Please disable Safe Mode and try again.")
|
||||
# Check if we're in Safe Mode, and if so, tell user FAT32 is unsupported
|
||||
if utilities.check_boot_mode() == "safe_boot":
|
||||
logging.info("\nSafe Mode detected. FAT32 is unsupported by macOS in this mode.")
|
||||
logging.info("Please disable Safe Mode and try again.")
|
||||
return
|
||||
partition_info = plistlib.loads(subprocess.run(f"diskutil info -plist {full_disk_identifier}".split(), stdout=subprocess.PIPE).stdout.decode().strip().encode())
|
||||
parent_disk = partition_info["ParentWholeDisk"]
|
||||
@@ -252,10 +188,7 @@ Please build OpenCore first!"""
|
||||
logging.info("\nPress [Enter] to continue.\n")
|
||||
input()
|
||||
else:
|
||||
if self.constants.gui_mode is False:
|
||||
tui_helpers.TUIOnlyLogging.info(["Copying OpenCore"], "Press [Enter] to go back.\n", ["EFI failed to mount!"]).start()
|
||||
else:
|
||||
logging.info("EFI failed to mount!")
|
||||
logging.info("EFI failed to mount!")
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import plistlib
|
||||
import subprocess
|
||||
import tempfile
|
||||
import logging
|
||||
from resources import utilities, tui_helpers, network_handler
|
||||
from resources import utilities, network_handler
|
||||
|
||||
def list_local_macOS_installers():
|
||||
# Finds all applicable macOS installers
|
||||
@@ -325,52 +325,6 @@ def format_drive(disk_id):
|
||||
input("\nPress Enter to exit")
|
||||
return False
|
||||
|
||||
def select_disk_to_format():
|
||||
utilities.cls()
|
||||
utilities.header(["Installing OpenCore to Drive"])
|
||||
|
||||
logging.info("\nDisk picker is loading...")
|
||||
|
||||
all_disks = {}
|
||||
# TODO: AllDisksAndPartitions is not supported in Snow Leopard and older
|
||||
try:
|
||||
# High Sierra and newer
|
||||
disks = plistlib.loads(subprocess.run("diskutil list -plist physical".split(), stdout=subprocess.PIPE).stdout.decode().strip().encode())
|
||||
except ValueError:
|
||||
# Sierra and older
|
||||
disks = plistlib.loads(subprocess.run("diskutil list -plist".split(), stdout=subprocess.PIPE).stdout.decode().strip().encode())
|
||||
for disk in disks["AllDisksAndPartitions"]:
|
||||
disk_info = plistlib.loads(subprocess.run(f"diskutil info -plist {disk['DeviceIdentifier']}".split(), stdout=subprocess.PIPE).stdout.decode().strip().encode())
|
||||
try:
|
||||
all_disks[disk["DeviceIdentifier"]] = {"identifier": disk_info["DeviceNode"], "name": disk_info["MediaName"], "size": disk_info["TotalSize"], "removable": disk_info["Internal"], "partitions": {}}
|
||||
except KeyError:
|
||||
# Avoid crashing with CDs installed
|
||||
continue
|
||||
menu = tui_helpers.TUIMenu(
|
||||
["Select Disk to write the macOS Installer onto"],
|
||||
"Please select the disk you would like to install OpenCore to: ",
|
||||
in_between=["Missing drives? Verify they are 14GB+ and external (ie. USB)", "", "Ensure all data is backed up on selected drive, entire drive will be erased!"],
|
||||
return_number_instead_of_direct_call=True,
|
||||
loop=True,
|
||||
)
|
||||
for disk in all_disks:
|
||||
# Strip disks that are under 14GB (15,032,385,536 bytes)
|
||||
# createinstallmedia isn't great at detecting if a disk has enough space
|
||||
if not any(all_disks[disk]['size'] > 15032385536 for partition in all_disks[disk]):
|
||||
continue
|
||||
# Strip internal disks as well (avoid user formatting their SSD/HDD)
|
||||
# Ensure user doesn't format their boot drive
|
||||
if not any(all_disks[disk]['removable'] is False for partition in all_disks[disk]):
|
||||
continue
|
||||
menu.add_menu_option(f"{disk}: {all_disks[disk]['name']} ({utilities.human_fmt(all_disks[disk]['size'])})", key=disk[4:])
|
||||
|
||||
response = menu.start()
|
||||
|
||||
if response == -1:
|
||||
return None
|
||||
|
||||
return response
|
||||
|
||||
|
||||
|
||||
def list_disk_to_format():
|
||||
|
||||
@@ -15,8 +15,7 @@ import os
|
||||
|
||||
import logging
|
||||
|
||||
from resources import utilities, network_handler
|
||||
from resources.constants import Constants
|
||||
from resources import utilities, network_handler, constants
|
||||
from data import os_data
|
||||
|
||||
KDK_INSTALL_PATH: str = "/Library/Developer/KDKs"
|
||||
@@ -50,8 +49,8 @@ class KernelDebugKitObject:
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, constants: Constants, host_build: str, host_version: str, ignore_installed: bool = False, passive: bool = False):
|
||||
self.constants: Constants = constants
|
||||
def __init__(self, global_constants: constants.Constants, host_build: str, host_version: str, ignore_installed: bool = False, passive: bool = False):
|
||||
self.constants: constants.Constants = global_constants
|
||||
|
||||
self.host_build: str = host_build # ex. 20A5384c
|
||||
self.host_version: str = host_version # ex. 11.0.1
|
||||
@@ -565,6 +564,8 @@ class KernelDebugKitUtilities:
|
||||
logging.info(f"- Installing KDK package: {kdk_path.name}")
|
||||
logging.info(f" - This may take a while...")
|
||||
|
||||
# TODO: Check whether enough disk space is available
|
||||
|
||||
result = utilities.elevated(["installer", "-pkg", kdk_path, "-target", "/"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
if result.returncode != 0:
|
||||
logging.info("- Failed to install KDK:")
|
||||
|
||||
@@ -4,21 +4,16 @@ import sys
|
||||
import time
|
||||
import logging
|
||||
import threading
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
from data import model_array
|
||||
from resources.build import build
|
||||
from resources.gui import gui_main
|
||||
from resources import (
|
||||
cli_menu,
|
||||
constants,
|
||||
utilities,
|
||||
device_probe,
|
||||
os_probe,
|
||||
defaults,
|
||||
arguments,
|
||||
install,
|
||||
tui_helpers,
|
||||
reroute_payloads,
|
||||
commit_info,
|
||||
logging_handler
|
||||
@@ -26,36 +21,50 @@ from resources import (
|
||||
|
||||
|
||||
class OpenCoreLegacyPatcher:
|
||||
def __init__(self, launch_gui=False):
|
||||
self.constants = constants.Constants()
|
||||
self.constants.wxpython_variant = launch_gui
|
||||
"""
|
||||
Initial entry point for starting OpenCore Legacy Patcher
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
logging_handler.InitializeLoggingSupport()
|
||||
|
||||
self.constants: constants.Constants = constants.Constants()
|
||||
|
||||
self.constants.wxpython_variant: bool = True
|
||||
|
||||
logging.info(f"- Loading OpenCore Legacy Patcher v{self.constants.patcher_version}...")
|
||||
|
||||
self.generate_base_data()
|
||||
self._generate_base_data()
|
||||
|
||||
if utilities.check_cli_args() is None:
|
||||
if launch_gui is True:
|
||||
utilities.disable_cls()
|
||||
from resources.gui import gui_main
|
||||
gui_main.wx_python_gui(self.constants).main_menu(None)
|
||||
else:
|
||||
self.main_menu()
|
||||
gui_main.wx_python_gui(self.constants).main_menu(None)
|
||||
|
||||
|
||||
def generate_base_data(self):
|
||||
def _generate_base_data(self):
|
||||
"""
|
||||
Generate base data required for the patcher to run
|
||||
"""
|
||||
|
||||
# Generate OS data
|
||||
os_data = os_probe.OSProbe()
|
||||
self.constants.detected_os = os_data.detect_kernel_major()
|
||||
self.constants.detected_os_minor = os_data.detect_kernel_minor()
|
||||
self.constants.detected_os_build = os_data.detect_os_build()
|
||||
self.constants.detected_os_version = os_data.detect_os_version()
|
||||
|
||||
# Generate computer data
|
||||
self.constants.computer = device_probe.Computer.probe()
|
||||
self.constants.recovery_status = utilities.check_recovery()
|
||||
self.computer = self.constants.computer
|
||||
self.constants.booted_oc_disk = utilities.find_disk_off_uuid(utilities.clean_device_path(self.computer.opencore_path))
|
||||
if self.constants.computer.firmware_vendor:
|
||||
if self.constants.computer.firmware_vendor != "Apple":
|
||||
self.constants.host_is_hackintosh = True
|
||||
|
||||
# Generate environment data
|
||||
self.constants.recovery_status = utilities.check_recovery()
|
||||
utilities.disable_cls()
|
||||
|
||||
# Generate binary data
|
||||
launcher_script = None
|
||||
launcher_binary = sys.executable
|
||||
if "python" in launcher_binary:
|
||||
@@ -65,83 +74,40 @@ class OpenCoreLegacyPatcher:
|
||||
launcher_script = launcher_script.replace("/resources/main.py", "/OpenCore-Patcher-GUI.command")
|
||||
self.constants.launcher_binary = launcher_binary
|
||||
self.constants.launcher_script = launcher_script
|
||||
self.constants.unpack_thread = threading.Thread(target=reroute_payloads.reroute_payloads(self.constants).setup_tmp_disk_image)
|
||||
self.constants.unpack_thread.start()
|
||||
self.constants.commit_info = commit_info.commit_info(self.constants.launcher_binary).generate_commit_info()
|
||||
|
||||
# Now that we have commit info, update nightly link
|
||||
# Initialize working directory
|
||||
self.constants.unpack_thread = threading.Thread(target=reroute_payloads.RoutePayloadDiskImage, args=(self.constants,))
|
||||
self.constants.unpack_thread.start()
|
||||
|
||||
# Generate commit info
|
||||
self.constants.commit_info = commit_info.ParseCommitInfo(self.constants.launcher_binary).generate_commit_info()
|
||||
if self.constants.commit_info[0] not in ["Running from source", "Built from source"]:
|
||||
# Now that we have commit info, update nightly link
|
||||
branch = self.constants.commit_info[0]
|
||||
branch = branch.replace("refs/heads/", "")
|
||||
self.constants.installer_pkg_url_nightly = self.constants.installer_pkg_url_nightly.replace("main", branch)
|
||||
|
||||
defaults.generate_defaults(self.computer.real_model, True, self.constants)
|
||||
# Generate defaults
|
||||
defaults.GenerateDefaults(self.computer.real_model, True, self.constants)
|
||||
|
||||
if utilities.check_cli_args() is not None:
|
||||
logging.info("- Detected arguments, switching to CLI mode")
|
||||
self.constants.gui_mode = True # Assumes no user interaction is required
|
||||
ignore_args = ["--auto_patch", "--gui_patch", "--gui_unpatch"]
|
||||
if not any(x in sys.argv for x in ignore_args):
|
||||
self.constants.current_path = Path.cwd()
|
||||
self.constants.cli_mode = True
|
||||
if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"):
|
||||
logging.info("- Rerouting payloads location")
|
||||
self.constants.payload_path = sys._MEIPASS / Path("payloads")
|
||||
ignore_args = ignore_args.pop(0)
|
||||
if not any(x in sys.argv for x in ignore_args):
|
||||
while self.constants.unpack_thread.is_alive():
|
||||
time.sleep(0.1)
|
||||
arguments.arguments().parse_arguments(self.constants)
|
||||
else:
|
||||
if utilities.check_cli_args() is None:
|
||||
logging.info(f"- No arguments present, loading {'GUI' if self.constants.wxpython_variant is True else 'TUI'} mode")
|
||||
return
|
||||
|
||||
logging.info("- Detected arguments, switching to CLI mode")
|
||||
self.constants.gui_mode = True # Assumes no user interaction is required
|
||||
|
||||
def main_menu(self):
|
||||
response = None
|
||||
while not (response and response == -1):
|
||||
title = [
|
||||
f"OpenCore Legacy Patcher v{self.constants.patcher_version}",
|
||||
f"Selected Model: {self.constants.custom_model or self.computer.real_model}",
|
||||
]
|
||||
ignore_args = ["--auto_patch", "--gui_patch", "--gui_unpatch"]
|
||||
if not any(x in sys.argv for x in ignore_args):
|
||||
self.constants.current_path = Path.cwd()
|
||||
self.constants.cli_mode = True
|
||||
if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"):
|
||||
logging.info("- Rerouting payloads location")
|
||||
self.constants.payload_path = sys._MEIPASS / Path("payloads")
|
||||
ignore_args = ignore_args.pop(0)
|
||||
|
||||
if (self.constants.custom_model or self.computer.real_model) not in model_array.SupportedSMBIOS and self.constants.allow_oc_everywhere is False:
|
||||
in_between = [
|
||||
"Your model is not supported by this patcher for running unsupported OSes!",
|
||||
"",
|
||||
'If you plan to create the USB for another machine, please select the \n"Change Model" option in the menu.',
|
||||
"",
|
||||
'If you want to run OCLP on a native Mac, please toggle \n"Allow OpenCore on native Models" in settings',
|
||||
]
|
||||
elif not self.constants.custom_model and self.computer.real_model == "iMac7,1" and "SSE4.1" not in self.computer.cpu.flags:
|
||||
in_between = [
|
||||
"Your model requires a CPU upgrade to a CPU supporting SSE4.1+ to be supported by this patcher!",
|
||||
"",
|
||||
f'If you plan to create the USB for another {self.computer.real_model} with SSE4.1+, please select the "Change Model" option in the menu.',
|
||||
]
|
||||
elif self.constants.custom_model == "iMac7,1":
|
||||
in_between = ["This model is supported", "However please ensure the CPU has been upgraded to support SSE4.1+"]
|
||||
else:
|
||||
in_between = ["This model is supported"]
|
||||
if not any(x in sys.argv for x in ignore_args):
|
||||
while self.constants.unpack_thread.is_alive():
|
||||
time.sleep(0.1)
|
||||
|
||||
menu = tui_helpers.TUIMenu(title, "Please select an option: ", in_between=in_between, auto_number=True, top_level=True)
|
||||
|
||||
options = (
|
||||
[["Build OpenCore", build.build_opencore(self.constants.custom_model or self.constants.computer.real_model, self.constants).build_opencore]]
|
||||
if ((self.constants.custom_model or self.computer.real_model) in model_array.SupportedSMBIOS) or self.constants.allow_oc_everywhere is True
|
||||
else []
|
||||
) + [
|
||||
["Install OpenCore to USB/internal drive", install.tui_disk_installation(self.constants).copy_efi],
|
||||
["Post-Install Volume Patch", cli_menu.MenuOptions(self.constants.custom_model or self.computer.real_model, self.constants).PatchVolume],
|
||||
["Change Model", cli_menu.MenuOptions(self.constants.custom_model or self.computer.real_model, self.constants).change_model],
|
||||
["Patcher Settings", cli_menu.MenuOptions(self.constants.custom_model or self.computer.real_model, self.constants).patcher_settings],
|
||||
["Installer Creation", cli_menu.MenuOptions(self.constants.custom_model or self.computer.real_model, self.constants).download_macOS],
|
||||
["Credits", cli_menu.MenuOptions(self.constants.custom_model or self.computer.real_model, self.constants).credits],
|
||||
]
|
||||
|
||||
for option in options:
|
||||
menu.add_menu_option(option[0], function=option[1])
|
||||
|
||||
response = menu.start()
|
||||
|
||||
if getattr(sys, "frozen", False) and self.constants.recovery_status is False:
|
||||
subprocess.run("""osascript -e 'tell application "Terminal" to close first window' & exit""", shell=True)
|
||||
arguments.arguments(self.constants)
|
||||
|
||||
@@ -9,21 +9,31 @@ import tempfile
|
||||
import atexit
|
||||
import logging
|
||||
|
||||
class reroute_payloads:
|
||||
def __init__(self, constants):
|
||||
self.constants = constants
|
||||
from resources import constants
|
||||
|
||||
class RoutePayloadDiskImage:
|
||||
|
||||
def __init__(self, global_constants: constants.Constants):
|
||||
self.constants: constants.Constants = global_constants
|
||||
|
||||
self._setup_tmp_disk_image()
|
||||
|
||||
|
||||
def _setup_tmp_disk_image(self):
|
||||
"""
|
||||
Initialize temp directory and mount payloads.dmg
|
||||
Create overlay for patcher to write to
|
||||
|
||||
Currently only applicable for GUI variant and not running from source
|
||||
"""
|
||||
|
||||
def setup_tmp_disk_image(self):
|
||||
# Create a temp directory to mount the payloads.dmg
|
||||
# Then reroute r/w to this new temp directory
|
||||
# Currently only applicable for GUI variant
|
||||
if self.constants.wxpython_variant is True and not self.constants.launcher_script:
|
||||
logging.info("- Running in Binary GUI mode, switching to tmp directory")
|
||||
self.temp_dir = tempfile.TemporaryDirectory()
|
||||
logging.info(f"- New payloads location: {self.temp_dir.name}")
|
||||
logging.info("- Creating payloads directory")
|
||||
Path(self.temp_dir.name / Path("payloads")).mkdir(parents=True, exist_ok=True)
|
||||
self.unmount_active_dmgs(unmount_all_active=False)
|
||||
self._unmount_active_dmgs(unmount_all_active=False)
|
||||
output = subprocess.run(
|
||||
[
|
||||
"hdiutil", "attach", "-noverify", f"{self.constants.payload_path}.dmg",
|
||||
@@ -38,16 +48,25 @@ class reroute_payloads:
|
||||
logging.info("- Mounted payloads.dmg")
|
||||
self.constants.current_path = Path(self.temp_dir.name)
|
||||
self.constants.payload_path = Path(self.temp_dir.name) / Path("payloads")
|
||||
atexit.register(self.unmount_active_dmgs, unmount_all_active=False)
|
||||
atexit.register(self._unmount_active_dmgs, unmount_all_active=False)
|
||||
else:
|
||||
logging.info("- Failed to mount payloads.dmg")
|
||||
logging.info(f"Output: {output.stdout.decode()}")
|
||||
logging.info(f"Return Code: {output.returncode}")
|
||||
|
||||
def unmount_active_dmgs(self, unmount_all_active=True):
|
||||
# Find all DMGs that are mounted, and forcefully unmount them
|
||||
# If our disk image was previously mounted, we need to unmount it to use again
|
||||
# This can happen if we crash during a previous secession, however 'atexit' class should hopefully avoid this
|
||||
|
||||
def _unmount_active_dmgs(self, unmount_all_active=True):
|
||||
"""
|
||||
Unmounts disk images associated with OCLP
|
||||
|
||||
Finds all DMGs that are mounted, and forcefully unmount them
|
||||
If our disk image was previously mounted, we need to unmount it to use again
|
||||
This can happen if we crash during a previous secession, however 'atexit' class should hopefully avoid this
|
||||
|
||||
Parameters:
|
||||
unmount_all_active (bool): If True, unmount all active DMGs, otherwise only unmount our own DMG
|
||||
"""
|
||||
|
||||
dmg_info = subprocess.run(["hdiutil", "info", "-plist"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
dmg_info = plistlib.loads(dmg_info.stdout)
|
||||
|
||||
|
||||
152
resources/run.py
152
resources/run.py
@@ -1,152 +0,0 @@
|
||||
# Module for running processes with real time output
|
||||
# Written by CorpNewt
|
||||
# Source: https://github.com/corpnewt/pymodules/blob/884c3de15b6a2570afde52fe8a14a3e946ffb18a/run.py
|
||||
|
||||
import sys, subprocess, time, threading, shlex, logging
|
||||
from queue import Queue, Empty
|
||||
|
||||
ON_POSIX = 'posix' in sys.builtin_module_names
|
||||
|
||||
class Run:
|
||||
|
||||
def __init__(self):
|
||||
return
|
||||
|
||||
def _read_output(self, pipe, q):
|
||||
try:
|
||||
for line in iter(lambda: pipe.read(1), b''):
|
||||
q.put(line)
|
||||
except ValueError:
|
||||
pass
|
||||
pipe.close()
|
||||
|
||||
def _create_thread(self, output):
|
||||
# Creates a new queue and thread object to watch based on the output pipe sent
|
||||
q = Queue()
|
||||
t = threading.Thread(target=self._read_output, args=(output, q))
|
||||
t.daemon = True
|
||||
return (q,t)
|
||||
|
||||
def _stream_output(self, comm, shell = False):
|
||||
output = error = ""
|
||||
p = None
|
||||
try:
|
||||
if shell and type(comm) is list:
|
||||
comm = " ".join(shlex.quote(x) for x in comm)
|
||||
if not shell and type(comm) is str:
|
||||
comm = shlex.split(comm)
|
||||
p = subprocess.Popen(comm, shell=shell, stdout=subprocess.PIPE, stderr=subprocess.PIPE, bufsize=0, universal_newlines=True, close_fds=ON_POSIX)
|
||||
# Setup the stdout thread/queue
|
||||
q,t = self._create_thread(p.stdout)
|
||||
qe,te = self._create_thread(p.stderr)
|
||||
# Start both threads
|
||||
t.start()
|
||||
te.start()
|
||||
|
||||
while True:
|
||||
c = z = ""
|
||||
try: c = q.get_nowait()
|
||||
except Empty: pass
|
||||
else:
|
||||
sys.stdout.write(c)
|
||||
output += c
|
||||
sys.stdout.flush()
|
||||
try: z = qe.get_nowait()
|
||||
except Empty: pass
|
||||
else:
|
||||
sys.stderr.write(z)
|
||||
error += z
|
||||
sys.stderr.flush()
|
||||
if not c==z=="": continue # Keep going until empty
|
||||
# No output - see if still running
|
||||
p.poll()
|
||||
if p.returncode != None:
|
||||
# Subprocess ended
|
||||
break
|
||||
# No output, but subprocess still running - stall for 20ms
|
||||
time.sleep(0.02)
|
||||
|
||||
o, e = p.communicate()
|
||||
return (output+o, error+e, p.returncode)
|
||||
except:
|
||||
if p:
|
||||
try: o, e = p.communicate()
|
||||
except: o = e = ""
|
||||
return (output+o, error+e, p.returncode)
|
||||
return ("", "Command not found!", 1)
|
||||
|
||||
def _decode(self, value, encoding="utf-8", errors="ignore"):
|
||||
# Helper method to only decode if bytes type
|
||||
if sys.version_info >= (3,0) and isinstance(value, bytes):
|
||||
return value.decode(encoding,errors)
|
||||
return value
|
||||
|
||||
def _run_command(self, comm, shell = False):
|
||||
c = None
|
||||
try:
|
||||
if shell and type(comm) is list:
|
||||
comm = " ".join(shlex.quote(x) for x in comm)
|
||||
if not shell and type(comm) is str:
|
||||
comm = shlex.split(comm)
|
||||
p = subprocess.Popen(comm, shell=shell, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
c = p.communicate()
|
||||
except:
|
||||
if c == None:
|
||||
return ("", "Command not found!", 1)
|
||||
return (self._decode(c[0]), self._decode(c[1]), p.returncode)
|
||||
|
||||
def run(self, command_list, leave_on_fail = False):
|
||||
# Command list should be an array of dicts
|
||||
if type(command_list) is dict:
|
||||
# We only have one command
|
||||
command_list = [command_list]
|
||||
output_list = []
|
||||
for comm in command_list:
|
||||
args = comm.get("args", [])
|
||||
shell = comm.get("shell", False)
|
||||
stream = comm.get("stream", False)
|
||||
sudo = comm.get("sudo", False)
|
||||
stdout = comm.get("stdout", False)
|
||||
stderr = comm.get("stderr", False)
|
||||
mess = comm.get("message", None)
|
||||
show = comm.get("show", False)
|
||||
|
||||
if not mess == None:
|
||||
logging.info(mess)
|
||||
|
||||
if not len(args):
|
||||
# nothing to process
|
||||
continue
|
||||
if sudo:
|
||||
# Check if we have sudo
|
||||
out = self._run_command(["which", "sudo"])
|
||||
if "sudo" in out[0]:
|
||||
# Can sudo
|
||||
if type(args) is list:
|
||||
args.insert(0, out[0].replace("\n", "")) # add to start of list
|
||||
elif type(args) is str:
|
||||
args = out[0].replace("\n", "") + " " + args # add to start of string
|
||||
|
||||
if show:
|
||||
logging.info(" ".join(args))
|
||||
|
||||
if stream:
|
||||
# Stream it!
|
||||
out = self._stream_output(args, shell)
|
||||
else:
|
||||
# Just run and gather output
|
||||
out = self._run_command(args, shell)
|
||||
if stdout and len(out[0]):
|
||||
logging.info(out[0])
|
||||
if stderr and len(out[1]):
|
||||
logging.info(out[1])
|
||||
# Append output
|
||||
output_list.append(out)
|
||||
# Check for errors
|
||||
if leave_on_fail and out[2] != 0:
|
||||
# Got an error - leave
|
||||
break
|
||||
if len(output_list) == 1:
|
||||
# We only ran one command - just return that output
|
||||
return output_list[0]
|
||||
return output_list
|
||||
@@ -46,9 +46,9 @@ from data import os_data
|
||||
|
||||
|
||||
class PatchSysVolume:
|
||||
def __init__(self, model, versions, hardware_details=None):
|
||||
def __init__(self, model: str, global_constants: constants.Constants, hardware_details: list = None):
|
||||
self.model = model
|
||||
self.constants: constants.Constants() = versions
|
||||
self.constants: constants.Constants = global_constants
|
||||
self.computer = self.constants.computer
|
||||
self.root_mount_path = None
|
||||
self.root_supports_snapshot = utilities.check_if_root_is_apfs_snapshot()
|
||||
@@ -61,9 +61,9 @@ class PatchSysVolume:
|
||||
# GUI will detect hardware patches before starting PatchSysVolume()
|
||||
# However the TUI will not, so allow for data to be passed in manually avoiding multiple calls
|
||||
if hardware_details is None:
|
||||
hardware_details = sys_patch_detect.detect_root_patch(self.computer.real_model, self.constants).detect_patch_set()
|
||||
hardware_details = sys_patch_detect.DetectRootPatch(self.computer.real_model, self.constants).detect_patch_set()
|
||||
self.hardware_details = hardware_details
|
||||
self.init_pathing(custom_root_mount_path=None, custom_data_mount_path=None)
|
||||
self._init_pathing(custom_root_mount_path=None, custom_data_mount_path=None)
|
||||
|
||||
self.skip_root_kmutil_requirement = self.hardware_details["Settings: Supports Auxiliary Cache"]
|
||||
|
||||
@@ -72,7 +72,15 @@ class PatchSysVolume:
|
||||
if Path(self.constants.payload_local_binaries_root_path).exists():
|
||||
shutil.rmtree(self.constants.payload_local_binaries_root_path)
|
||||
|
||||
def init_pathing(self, custom_root_mount_path=None, custom_data_mount_path=None):
|
||||
def _init_pathing(self, custom_root_mount_path: Path = None, custom_data_mount_path: Path = None):
|
||||
"""
|
||||
Initializes the pathing for root volume patching
|
||||
|
||||
Parameters:
|
||||
custom_root_mount_path (Path): Custom path to mount the root volume
|
||||
custom_data_mount_path (Path): Custom path to mount the data volume
|
||||
|
||||
"""
|
||||
if custom_root_mount_path and custom_data_mount_path:
|
||||
self.mount_location = custom_root_mount_path
|
||||
self.data_mount_location = custom_data_mount_path
|
||||
@@ -83,11 +91,12 @@ class PatchSysVolume:
|
||||
else:
|
||||
self.mount_location = ""
|
||||
self.mount_location_data = ""
|
||||
|
||||
self.mount_extensions = f"{self.mount_location}/System/Library/Extensions"
|
||||
self.mount_application_support = f"{self.mount_location_data}/Library/Application Support"
|
||||
|
||||
|
||||
def mount_root_vol(self):
|
||||
def _mount_root_vol(self):
|
||||
# Returns boolean if Root Volume is available
|
||||
self.root_mount_path = utilities.get_disk_path()
|
||||
if self.root_mount_path.startswith("disk"):
|
||||
@@ -113,7 +122,7 @@ class PatchSysVolume:
|
||||
return False
|
||||
|
||||
|
||||
def merge_kdk_with_root(self, save_hid_cs=False):
|
||||
def _merge_kdk_with_root(self, save_hid_cs=False):
|
||||
if self.skip_root_kmutil_requirement is True:
|
||||
return
|
||||
if self.constants.detected_os < os_data.os_data.ventura:
|
||||
@@ -211,7 +220,7 @@ class PatchSysVolume:
|
||||
utilities.elevated(["rm", "-rf", f"{self.constants.payload_path}/IOHIDEventDriver_CodeSignature.bak"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
|
||||
|
||||
def unpatch_root_vol(self):
|
||||
def _unpatch_root_vol(self):
|
||||
if self.constants.detected_os > os_data.os_data.catalina and self.root_supports_snapshot is True:
|
||||
logging.info("- Reverting to last signed APFS snapshot")
|
||||
result = utilities.elevated(["bless", "--mount", self.mount_location, "--bootefi", "--last-sealed-snapshot"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
@@ -221,18 +230,18 @@ class PatchSysVolume:
|
||||
logging.info(result.stdout.decode())
|
||||
logging.info("- Failed to revert snapshot via Apple's 'bless' command")
|
||||
else:
|
||||
self.clean_skylight_plugins()
|
||||
self.delete_nonmetal_enforcement()
|
||||
self.clean_auxiliary_kc()
|
||||
self._clean_skylight_plugins()
|
||||
self._delete_nonmetal_enforcement()
|
||||
self._clean_auxiliary_kc()
|
||||
self.constants.root_patcher_succeeded = True
|
||||
logging.info("- Unpatching complete")
|
||||
logging.info("\nPlease reboot the machine for patches to take effect")
|
||||
|
||||
def rebuild_snapshot(self):
|
||||
if self.rebuild_kernel_collection() is True:
|
||||
def _rebuild_snapshot(self):
|
||||
if self._rebuild_kernel_collection() is True:
|
||||
self.update_preboot_kernel_cache()
|
||||
self.rebuild_dyld_shared_cache()
|
||||
if self.create_new_apfs_snapshot() is True:
|
||||
self._rebuild_dyld_shared_cache()
|
||||
if self._create_new_apfs_snapshot() is True:
|
||||
logging.info("- Patching complete")
|
||||
logging.info("\nPlease reboot the machine for patches to take effect")
|
||||
if self.needs_kmutil_exemptions is True:
|
||||
@@ -241,7 +250,7 @@ class PatchSysVolume:
|
||||
if self.constants.gui_mode is False:
|
||||
input("\nPress [ENTER] to continue")
|
||||
|
||||
def rebuild_kernel_collection(self):
|
||||
def _rebuild_kernel_collection(self):
|
||||
logging.info("- Rebuilding Kernel Cache (This may take some time)")
|
||||
if self.constants.detected_os > os_data.os_data.catalina:
|
||||
# Base Arguments
|
||||
@@ -328,15 +337,15 @@ class PatchSysVolume:
|
||||
return False
|
||||
|
||||
for file in ["KextPolicy", "KextPolicy-shm", "KextPolicy-wal"]:
|
||||
self.remove_file("/private/var/db/SystemPolicyConfiguration/", file)
|
||||
self._remove_file("/private/var/db/SystemPolicyConfiguration/", file)
|
||||
else:
|
||||
# Install RSRHelper utility to handle desynced KCs
|
||||
sys_patch_helpers.sys_patch_helpers(self.constants).install_rsr_repair_binary()
|
||||
sys_patch_helpers.SysPatchHelpers(self.constants).install_rsr_repair_binary()
|
||||
|
||||
logging.info("- Successfully built new kernel cache")
|
||||
return True
|
||||
|
||||
def create_new_apfs_snapshot(self):
|
||||
def _create_new_apfs_snapshot(self):
|
||||
if self.root_supports_snapshot is True:
|
||||
logging.info("- Creating new APFS snapshot")
|
||||
bless = utilities.elevated(
|
||||
@@ -353,24 +362,25 @@ class PatchSysVolume:
|
||||
if "Can't use last-sealed-snapshot or create-snapshot on non system volume" in bless.stdout.decode():
|
||||
logging.info("- This is an APFS bug with Monterey and newer! Perform a clean installation to ensure your APFS volume is built correctly")
|
||||
return False
|
||||
self.unmount_drive()
|
||||
self._unmount_drive()
|
||||
return True
|
||||
|
||||
def unmount_drive(self):
|
||||
def _unmount_drive(self):
|
||||
logging.info("- Unmounting Root Volume (Don't worry if this fails)")
|
||||
utilities.elevated(["diskutil", "unmount", self.root_mount_path], stdout=subprocess.PIPE).stdout.decode().strip().encode()
|
||||
|
||||
def rebuild_dyld_shared_cache(self):
|
||||
if self.constants.detected_os <= os_data.os_data.catalina:
|
||||
logging.info("- Rebuilding dyld shared cache")
|
||||
utilities.process_status(utilities.elevated(["update_dyld_shared_cache", "-root", f"{self.mount_location}/"]))
|
||||
def _rebuild_dyld_shared_cache(self):
|
||||
if self.constants.detected_os > os_data.os_data.catalina:
|
||||
return
|
||||
logging.info("- Rebuilding dyld shared cache")
|
||||
utilities.process_status(utilities.elevated(["update_dyld_shared_cache", "-root", f"{self.mount_location}/"]))
|
||||
|
||||
def update_preboot_kernel_cache(self):
|
||||
if self.constants.detected_os == os_data.os_data.catalina:
|
||||
logging.info("- Rebuilding preboot kernel cache")
|
||||
utilities.process_status(utilities.elevated(["kcditto"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT))
|
||||
|
||||
def clean_skylight_plugins(self):
|
||||
def _clean_skylight_plugins(self):
|
||||
if (Path(self.mount_application_support) / Path("SkyLightPlugins/")).exists():
|
||||
logging.info("- Found SkylightPlugins folder, removing old plugins")
|
||||
utilities.process_status(utilities.elevated(["rm", "-Rf", f"{self.mount_application_support}/SkyLightPlugins"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT))
|
||||
@@ -379,14 +389,14 @@ class PatchSysVolume:
|
||||
logging.info("- Creating SkylightPlugins folder")
|
||||
utilities.process_status(utilities.elevated(["mkdir", "-p", f"{self.mount_application_support}/SkyLightPlugins/"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT))
|
||||
|
||||
def delete_nonmetal_enforcement(self):
|
||||
def _delete_nonmetal_enforcement(self):
|
||||
for arg in ["useMetal", "useIOP"]:
|
||||
result = subprocess.run(["defaults", "read", "/Library/Preferences/com.apple.CoreDisplay", arg], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL).stdout.decode("utf-8").strip()
|
||||
if result in ["0", "false", "1", "true"]:
|
||||
logging.info(f"- Removing non-Metal Enforcement Preference: {arg}")
|
||||
utilities.elevated(["defaults", "delete", "/Library/Preferences/com.apple.CoreDisplay", arg])
|
||||
|
||||
def clean_auxiliary_kc(self):
|
||||
def _clean_auxiliary_kc(self):
|
||||
# When reverting root volume patches, the AuxKC will still retain the UUID
|
||||
# it was built against. Thus when Boot/SysKC are reverted, Aux will break
|
||||
# To resolve this, delete all installed kexts in /L*/E* and rebuild the AuxKC
|
||||
@@ -407,7 +417,7 @@ class PatchSysVolume:
|
||||
for file in oclp_plist_data[key]["Install"][location]:
|
||||
if not file.endswith(".kext"):
|
||||
continue
|
||||
self.remove_file("/Library/Extensions", file)
|
||||
self._remove_file("/Library/Extensions", file)
|
||||
|
||||
# Handle situations where users migrated from older OSes with a lot of garbage in /L*/E*
|
||||
# ex. Nvidia Web Drivers, NetUSB, dosdude1's patches, etc.
|
||||
@@ -431,17 +441,17 @@ class PatchSysVolume:
|
||||
# ex. Symlinks pointing to symlinks pointing to dead files
|
||||
pass
|
||||
|
||||
def write_patchset(self, patchset):
|
||||
def _write_patchset(self, patchset):
|
||||
destination_path = f"{self.mount_location}/System/Library/CoreServices"
|
||||
file_name = "OpenCore-Legacy-Patcher.plist"
|
||||
destination_path_file = f"{destination_path}/{file_name}"
|
||||
if sys_patch_helpers.sys_patch_helpers(self.constants).generate_patchset_plist(patchset, file_name, self.kdk_path):
|
||||
if sys_patch_helpers.SysPatchHelpers(self.constants).generate_patchset_plist(patchset, file_name, self.kdk_path):
|
||||
logging.info("- Writing patchset information to Root Volume")
|
||||
if Path(destination_path_file).exists():
|
||||
utilities.process_status(utilities.elevated(["rm", destination_path_file], stdout=subprocess.PIPE, stderr=subprocess.STDOUT))
|
||||
utilities.process_status(utilities.elevated(["cp", f"{self.constants.payload_path}/{file_name}", destination_path], stdout=subprocess.PIPE, stderr=subprocess.STDOUT))
|
||||
|
||||
def add_auxkc_support(self, install_file, source_folder_path, install_patch_directory, destination_folder_path):
|
||||
def _add_auxkc_support(self, install_file, source_folder_path, install_patch_directory, destination_folder_path):
|
||||
# In macOS Ventura, KDKs are required to build new Boot and System KCs
|
||||
# However for some patch sets, we're able to use the Auxiliary KCs with '/Library/Extensions'
|
||||
|
||||
@@ -477,11 +487,11 @@ class PatchSysVolume:
|
||||
plist_data["OSBundleRequired"] = "Auxiliary"
|
||||
plistlib.dump(plist_data, plist_path.open("wb"))
|
||||
|
||||
self.check_kexts_needs_authentication(install_file)
|
||||
self._check_kexts_needs_authentication(install_file)
|
||||
|
||||
return updated_install_location
|
||||
|
||||
def check_kexts_needs_authentication(self, kext_name):
|
||||
def _check_kexts_needs_authentication(self, kext_name):
|
||||
# Verify whether the user needs to authenticate in System Preferences
|
||||
# Specifically under 'private/var/db/KernelManagement/AuxKC/CurrentAuxKC/com.apple.kcgen.instructions.plist'
|
||||
# ["kextsToBuild"][i]:
|
||||
@@ -503,21 +513,21 @@ class PatchSysVolume:
|
||||
logging.info(f" - {kext_name} requires authentication in System Preferences")
|
||||
self.constants.needs_to_open_preferences = True # Notify in GUI to open System Preferences
|
||||
|
||||
def patch_root_vol(self):
|
||||
def _patch_root_vol(self):
|
||||
logging.info(f"- Running patches for {self.model}")
|
||||
if self.patch_set_dictionary != {}:
|
||||
self.execute_patchset(self.patch_set_dictionary)
|
||||
self._execute_patchset(self.patch_set_dictionary)
|
||||
else:
|
||||
self.execute_patchset(sys_patch_detect.detect_root_patch(self.computer.real_model, self.constants).generate_patchset(self.hardware_details))
|
||||
self._execute_patchset(sys_patch_detect.DetectRootPatch(self.computer.real_model, self.constants).generate_patchset(self.hardware_details))
|
||||
|
||||
if self.constants.wxpython_variant is True and self.constants.detected_os >= os_data.os_data.big_sur:
|
||||
sys_patch_auto.AutomaticSysPatch(self.constants).install_auto_patcher_launch_agent()
|
||||
|
||||
self.rebuild_snapshot()
|
||||
self._rebuild_snapshot()
|
||||
|
||||
def execute_patchset(self, required_patches):
|
||||
def _execute_patchset(self, required_patches):
|
||||
source_files_path = str(self.constants.payload_local_binaries_root_path)
|
||||
self.preflight_checks(required_patches, source_files_path)
|
||||
self._preflight_checks(required_patches, source_files_path)
|
||||
for patch in required_patches:
|
||||
logging.info("- Installing Patchset: " + patch)
|
||||
if "Remove" in required_patches[patch]:
|
||||
@@ -525,7 +535,7 @@ class PatchSysVolume:
|
||||
logging.info("- Remove Files at: " + remove_patch_directory)
|
||||
for remove_patch_file in required_patches[patch]["Remove"][remove_patch_directory]:
|
||||
destination_folder_path = str(self.mount_location) + remove_patch_directory
|
||||
self.remove_file(destination_folder_path, remove_patch_file)
|
||||
self._remove_file(destination_folder_path, remove_patch_file)
|
||||
|
||||
|
||||
for method_install in ["Install", "Install Non-Root"]:
|
||||
@@ -539,10 +549,10 @@ class PatchSysVolume:
|
||||
else:
|
||||
if install_patch_directory == "/Library/Extensions":
|
||||
self.needs_kmutil_exemptions = True
|
||||
self.check_kexts_needs_authentication(install_file)
|
||||
self._check_kexts_needs_authentication(install_file)
|
||||
destination_folder_path = str(self.mount_location_data) + install_patch_directory
|
||||
|
||||
updated_destination_folder_path = self.add_auxkc_support(install_file, source_folder_path, install_patch_directory, destination_folder_path)
|
||||
updated_destination_folder_path = self._add_auxkc_support(install_file, source_folder_path, install_patch_directory, destination_folder_path)
|
||||
|
||||
if destination_folder_path != updated_destination_folder_path:
|
||||
# Update required_patches to reflect the new destination folder path
|
||||
@@ -553,7 +563,7 @@ class PatchSysVolume:
|
||||
|
||||
destination_folder_path = updated_destination_folder_path
|
||||
|
||||
self.install_new_file(source_folder_path, destination_folder_path, install_file)
|
||||
self._install_new_file(source_folder_path, destination_folder_path, install_file)
|
||||
|
||||
if "Processes" in required_patches[patch]:
|
||||
for process in required_patches[patch]["Processes"]:
|
||||
@@ -566,24 +576,24 @@ class PatchSysVolume:
|
||||
logging.info(f"- Running Process:\n{process}")
|
||||
utilities.process_status(subprocess.run(process, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True))
|
||||
if any(x in required_patches for x in ["AMD Legacy GCN", "AMD Legacy Polaris", "AMD Legacy Vega"]):
|
||||
sys_patch_helpers.sys_patch_helpers(self.constants).disable_window_server_caching()
|
||||
sys_patch_helpers.SysPatchHelpers(self.constants).disable_window_server_caching()
|
||||
if any(x in required_patches for x in ["Intel Ivy Bridge", "Intel Haswell"]):
|
||||
sys_patch_helpers.sys_patch_helpers(self.constants).remove_news_widgets()
|
||||
self.write_patchset(required_patches)
|
||||
sys_patch_helpers.SysPatchHelpers(self.constants).remove_news_widgets()
|
||||
self._write_patchset(required_patches)
|
||||
|
||||
def preflight_checks(self, required_patches, source_files_path):
|
||||
def _preflight_checks(self, required_patches, source_files_path):
|
||||
logging.info("- Running Preflight Checks before patching")
|
||||
|
||||
# Make sure old SkyLight plugins aren't being used
|
||||
self.clean_skylight_plugins()
|
||||
self._clean_skylight_plugins()
|
||||
# Make sure non-Metal Enforcement preferences are not present
|
||||
self.delete_nonmetal_enforcement()
|
||||
self._delete_nonmetal_enforcement()
|
||||
# Make sure we clean old kexts in /L*/E* that are not in the patchset
|
||||
self.clean_auxiliary_kc()
|
||||
self._clean_auxiliary_kc()
|
||||
|
||||
# Make sure SNB kexts are compatible with the host
|
||||
if "Intel Sandy Bridge" in required_patches:
|
||||
sys_patch_helpers.sys_patch_helpers(self.constants).snb_board_id_patch(source_files_path)
|
||||
sys_patch_helpers.SysPatchHelpers(self.constants).snb_board_id_patch(source_files_path)
|
||||
|
||||
for patch in required_patches:
|
||||
# Check if all files are present
|
||||
@@ -599,11 +609,11 @@ class PatchSysVolume:
|
||||
should_save_cs = False
|
||||
if "Legacy USB 1.1" in required_patches:
|
||||
should_save_cs = True
|
||||
self.merge_kdk_with_root(save_hid_cs=should_save_cs)
|
||||
self._merge_kdk_with_root(save_hid_cs=should_save_cs)
|
||||
|
||||
logging.info("- Finished Preflight, starting patching")
|
||||
|
||||
def install_new_file(self, source_folder, destination_folder, file_name):
|
||||
def _install_new_file(self, source_folder, destination_folder, file_name):
|
||||
# .frameworks are merged
|
||||
# .kexts and .apps are deleted and replaced
|
||||
file_name_str = str(file_name)
|
||||
@@ -616,7 +626,7 @@ class PatchSysVolume:
|
||||
# merge with rsync
|
||||
logging.info(f" - Installing: {file_name}")
|
||||
utilities.elevated(["rsync", "-r", "-i", "-a", f"{source_folder}/{file_name}", f"{destination_folder}/"], stdout=subprocess.PIPE)
|
||||
self.fix_permissions(destination_folder + "/" + file_name)
|
||||
self._fix_permissions(destination_folder + "/" + file_name)
|
||||
elif Path(source_folder + "/" + file_name_str).is_dir():
|
||||
# Applicable for .kext, .app, .plugin, .bundle, all of which are directories
|
||||
if Path(destination_folder + "/" + file_name).exists():
|
||||
@@ -625,7 +635,7 @@ class PatchSysVolume:
|
||||
else:
|
||||
logging.info(f" - Installing: {file_name}")
|
||||
utilities.process_status(utilities.elevated(["cp", "-R", f"{source_folder}/{file_name}", destination_folder], stdout=subprocess.PIPE, stderr=subprocess.STDOUT))
|
||||
self.fix_permissions(destination_folder + "/" + file_name)
|
||||
self._fix_permissions(destination_folder + "/" + file_name)
|
||||
else:
|
||||
# Assume it's an individual file, replace as normal
|
||||
if Path(destination_folder + "/" + file_name).exists():
|
||||
@@ -634,9 +644,9 @@ class PatchSysVolume:
|
||||
else:
|
||||
logging.info(f" - Installing: {file_name}")
|
||||
utilities.process_status(utilities.elevated(["cp", f"{source_folder}/{file_name}", destination_folder], stdout=subprocess.PIPE, stderr=subprocess.STDOUT))
|
||||
self.fix_permissions(destination_folder + "/" + file_name)
|
||||
self._fix_permissions(destination_folder + "/" + file_name)
|
||||
|
||||
def remove_file(self, destination_folder, file_name):
|
||||
def _remove_file(self, destination_folder, file_name):
|
||||
if Path(destination_folder + "/" + file_name).exists():
|
||||
logging.info(f" - Removing: {file_name}")
|
||||
if Path(destination_folder + "/" + file_name).is_dir():
|
||||
@@ -645,7 +655,7 @@ class PatchSysVolume:
|
||||
utilities.process_status(utilities.elevated(["rm", f"{destination_folder}/{file_name}"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT))
|
||||
|
||||
|
||||
def fix_permissions(self, destination_file):
|
||||
def _fix_permissions(self, destination_file):
|
||||
chmod_args = ["chmod", "-Rf", "755", destination_file]
|
||||
chown_args = ["chown", "-Rf", "root:wheel", destination_file]
|
||||
if not Path(destination_file).is_dir():
|
||||
@@ -656,7 +666,7 @@ class PatchSysVolume:
|
||||
utilities.process_status(utilities.elevated(chown_args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT))
|
||||
|
||||
|
||||
def check_files(self):
|
||||
def _check_files(self):
|
||||
if Path(self.constants.payload_local_binaries_root_path).exists():
|
||||
logging.info("- Local PatcherSupportPkg resources available, continuing...")
|
||||
return True
|
||||
@@ -675,7 +685,7 @@ class PatchSysVolume:
|
||||
def start_patch(self):
|
||||
logging.info("- Starting Patch Process")
|
||||
logging.info(f"- Determining Required Patch set for Darwin {self.constants.detected_os}")
|
||||
self.patch_set_dictionary = sys_patch_detect.detect_root_patch(self.computer.real_model, self.constants).generate_patchset(self.hardware_details)
|
||||
self.patch_set_dictionary = sys_patch_detect.DetectRootPatch(self.computer.real_model, self.constants).generate_patchset(self.hardware_details)
|
||||
|
||||
if self.patch_set_dictionary == {}:
|
||||
change_menu = None
|
||||
@@ -689,11 +699,11 @@ class PatchSysVolume:
|
||||
logging.info("- Continuing root patching")
|
||||
if change_menu in ["y", "Y"]:
|
||||
logging.info("- Verifying whether Root Patching possible")
|
||||
if sys_patch_detect.detect_root_patch(self.computer.real_model, self.constants).verify_patch_allowed(print_errors=not self.constants.wxpython_variant) is True:
|
||||
if sys_patch_detect.DetectRootPatch(self.computer.real_model, self.constants).verify_patch_allowed(print_errors=not self.constants.wxpython_variant) is True:
|
||||
logging.info("- Patcher is capable of patching")
|
||||
if self.check_files():
|
||||
if self.mount_root_vol() is True:
|
||||
self.patch_root_vol()
|
||||
if self._check_files():
|
||||
if self._mount_root_vol() is True:
|
||||
self._patch_root_vol()
|
||||
if self.constants.gui_mode is False:
|
||||
input("\nPress [ENTER] to return to the main menu")
|
||||
else:
|
||||
@@ -708,9 +718,9 @@ class PatchSysVolume:
|
||||
|
||||
def start_unpatch(self):
|
||||
logging.info("- Starting Unpatch Process")
|
||||
if sys_patch_detect.detect_root_patch(self.computer.real_model, self.constants).verify_patch_allowed(print_errors=True) is True:
|
||||
if self.mount_root_vol() is True:
|
||||
self.unpatch_root_vol()
|
||||
if sys_patch_detect.DetectRootPatch(self.computer.real_model, self.constants).verify_patch_allowed(print_errors=True) is True:
|
||||
if self._mount_root_vol() is True:
|
||||
self._unpatch_root_vol()
|
||||
if self.constants.gui_mode is False:
|
||||
input("\nPress [ENTER] to return to the main menu")
|
||||
else:
|
||||
|
||||
@@ -1,29 +1,42 @@
|
||||
# Auto Patching's main purpose is to try and tell the user they're missing root patches
|
||||
# New users may not realize OS updates remove our patches, so we try and run when nessasary
|
||||
# Conditions for running:
|
||||
# - Verify running GUI (TUI users can write their own scripts)
|
||||
# - Verify the Snapshot Seal is intact (if not, assume user is running patches)
|
||||
# - Verify this model needs patching (if not, assume user upgraded hardware and OCLP was not removed)
|
||||
# - Verify there are no updates for OCLP (ensure we have the latest patch sets)
|
||||
# If all these tests pass, start Root Patcher
|
||||
# Copyright (C) 2022, Mykola Grymalyuk
|
||||
|
||||
from pathlib import Path
|
||||
import plistlib
|
||||
import subprocess
|
||||
import webbrowser
|
||||
import logging
|
||||
from resources import utilities, updates, global_settings, network_handler
|
||||
from pathlib import Path
|
||||
|
||||
from resources import utilities, updates, global_settings, network_handler, constants
|
||||
from resources.sys_patch import sys_patch_detect
|
||||
from resources.gui import gui_main
|
||||
|
||||
class AutomaticSysPatch:
|
||||
|
||||
def __init__(self, constants):
|
||||
self.constants = constants
|
||||
class AutomaticSysPatch:
|
||||
"""
|
||||
Library of functions for launch agent, including automatic patching
|
||||
"""
|
||||
|
||||
def __init__(self, global_constants: constants.Constants):
|
||||
self.constants: constants.Constants = global_constants
|
||||
|
||||
|
||||
def start_auto_patch(self):
|
||||
"""
|
||||
Initiates automatic patching
|
||||
|
||||
Auto Patching's main purpose is to try and tell the user they're missing root patches
|
||||
New users may not realize OS updates remove our patches, so we try and run when nessasary
|
||||
|
||||
Conditions for running:
|
||||
- Verify running GUI (TUI users can write their own scripts)
|
||||
- Verify the Snapshot Seal is intact (if not, assume user is running patches)
|
||||
- Verify this model needs patching (if not, assume user upgraded hardware and OCLP was not removed)
|
||||
- Verify there are no updates for OCLP (ensure we have the latest patch sets)
|
||||
|
||||
If all these tests pass, start Root Patcher
|
||||
|
||||
"""
|
||||
|
||||
logging.info("- Starting Automatic Patching")
|
||||
if self.constants.wxpython_variant is False:
|
||||
logging.info("- Auto Patch option is not supported on TUI, please use GUI")
|
||||
@@ -31,7 +44,7 @@ class AutomaticSysPatch:
|
||||
|
||||
if utilities.check_seal() is True:
|
||||
logging.info("- Detected Snapshot seal intact, detecting patches")
|
||||
patches = sys_patch_detect.detect_root_patch(self.constants.computer.real_model, self.constants).detect_patch_set()
|
||||
patches = sys_patch_detect.DetectRootPatch(self.constants.computer.real_model, self.constants).detect_patch_set()
|
||||
if not any(not patch.startswith("Settings") and not patch.startswith("Validation") and patches[patch] is True for patch in patches):
|
||||
patches = []
|
||||
if patches:
|
||||
@@ -46,7 +59,7 @@ class AutomaticSysPatch:
|
||||
if patches[patch] is True and not patch.startswith("Settings") and not patch.startswith("Validation"):
|
||||
patch_string += f"- {patch}\n"
|
||||
# Check for updates
|
||||
dict = updates.check_binary_updates(self.constants).check_binary_updates()
|
||||
dict = updates.CheckBinaryUpdates(self.constants).check_binary_updates()
|
||||
if not dict:
|
||||
logging.info("- No new binaries found on Github, proceeding with patching")
|
||||
if self.constants.launcher_script is None:
|
||||
@@ -113,26 +126,35 @@ class AutomaticSysPatch:
|
||||
else:
|
||||
logging.info("- Detected Snapshot seal not intact, skipping")
|
||||
|
||||
if self.determine_if_versions_match() is False:
|
||||
self.determine_if_boot_matches()
|
||||
if self._determine_if_versions_match():
|
||||
self._determine_if_boot_matches()
|
||||
|
||||
|
||||
def determine_if_versions_match(self):
|
||||
def _determine_if_versions_match(self):
|
||||
"""
|
||||
Determine if the booted version of OCLP matches the installed version
|
||||
|
||||
ie. Installed app is 0.2.0, but EFI version is 0.1.0
|
||||
|
||||
Returns:
|
||||
bool: True if versions match, False if not
|
||||
"""
|
||||
|
||||
logging.info("- Checking booted vs installed OCLP Build")
|
||||
if self.constants.computer.oclp_version is None:
|
||||
logging.info("- Booted version not found")
|
||||
return False
|
||||
return True
|
||||
|
||||
if self.constants.computer.oclp_version == self.constants.patcher_version:
|
||||
logging.info("- Versions match")
|
||||
return False
|
||||
return True
|
||||
|
||||
# Check if installed version is newer than booted version
|
||||
if updates.check_binary_updates(self.constants).check_if_build_newer(
|
||||
if updates.CheckBinaryUpdates(self.constants)._check_if_build_newer(
|
||||
self.constants.computer.oclp_version.split("."), self.constants.patcher_version.split(".")
|
||||
) is True:
|
||||
logging.info("- Installed version is newer than booted version")
|
||||
return False
|
||||
return True
|
||||
|
||||
args = [
|
||||
"osascript",
|
||||
@@ -150,18 +172,25 @@ class AutomaticSysPatch:
|
||||
self.constants.start_build_install = True
|
||||
gui_main.wx_python_gui(self.constants).main_menu(None)
|
||||
|
||||
return True
|
||||
return False
|
||||
|
||||
def determine_if_boot_matches(self):
|
||||
# Goal of this function is to determine whether the user
|
||||
# is using a USB drive to Boot OpenCore but macOS does not
|
||||
# reside on the same drive as the USB.
|
||||
|
||||
# If we determine them to be mismatched, notify the user
|
||||
# and ask if they want to install to install to disk
|
||||
def _determine_if_boot_matches(self):
|
||||
"""
|
||||
Determine if the boot drive matches the macOS drive
|
||||
ie. Booted from USB, but macOS is on internal disk
|
||||
|
||||
Goal of this function is to determine whether the user
|
||||
is using a USB drive to Boot OpenCore but macOS does not
|
||||
reside on the same drive as the USB.
|
||||
|
||||
If we determine them to be mismatched, notify the user
|
||||
and ask if they want to install to install to disk.
|
||||
"""
|
||||
|
||||
logging.info("- Determining if macOS drive matches boot drive")
|
||||
should_notify = global_settings.global_settings().read_property("AutoPatch_Notify_Mismatched_Disks")
|
||||
|
||||
should_notify = global_settings.GlobalEnviromentSettings().read_property("AutoPatch_Notify_Mismatched_Disks")
|
||||
if should_notify is False:
|
||||
logging.info("- Skipping due to user preference")
|
||||
return
|
||||
@@ -223,9 +252,16 @@ class AutomaticSysPatch:
|
||||
|
||||
|
||||
def install_auto_patcher_launch_agent(self):
|
||||
# Installs the following:
|
||||
# - OpenCore-Patcher.app in /Library/Application Support/Dortania/
|
||||
# - com.dortania.opencore-legacy-patcher.auto-patch.plist in /Library/LaunchAgents/
|
||||
"""
|
||||
Install the Auto Patcher Launch Agent
|
||||
|
||||
Installs the following:
|
||||
- OpenCore-Patcher.app in /Library/Application Support/Dortania/
|
||||
- com.dortania.opencore-legacy-patcher.auto-patch.plist in /Library/LaunchAgents/
|
||||
|
||||
See start_auto_patch() comments for more info
|
||||
"""
|
||||
|
||||
if self.constants.launcher_script is not None:
|
||||
logging.info("- Skipping Auto Patcher Launch Agent, not supported when running from source")
|
||||
return
|
||||
|
||||
@@ -3,19 +3,39 @@
|
||||
# Used when supplying data to sys_patch.py
|
||||
# Copyright (C) 2020-2022, Dhinak G, Mykola Grymalyuk
|
||||
|
||||
from resources import constants, device_probe, utilities, amfi_detect, network_handler, kdk_handler
|
||||
from resources.sys_patch import sys_patch_helpers
|
||||
from data import model_array, os_data, sip_data, sys_patch_dict, smbios_data, cpu_data
|
||||
|
||||
import py_sip_xnu
|
||||
from pathlib import Path
|
||||
import plistlib
|
||||
import logging
|
||||
import py_sip_xnu
|
||||
from pathlib import Path
|
||||
|
||||
from resources import (
|
||||
constants,
|
||||
device_probe,
|
||||
utilities,
|
||||
amfi_detect,
|
||||
network_handler,
|
||||
kdk_handler
|
||||
)
|
||||
from data import (
|
||||
model_array,
|
||||
os_data,
|
||||
sip_data,
|
||||
sys_patch_dict,
|
||||
smbios_data,
|
||||
cpu_data
|
||||
)
|
||||
|
||||
|
||||
class DetectRootPatch:
|
||||
"""
|
||||
Library for querying root volume patches applicable for booted system
|
||||
"""
|
||||
|
||||
def __init__(self, model: str, global_constants: constants.Constants):
|
||||
self.model: str = model
|
||||
|
||||
self.constants: constants.Constants = global_constants
|
||||
|
||||
class detect_root_patch:
|
||||
def __init__(self, model, versions):
|
||||
self.model = model
|
||||
self.constants: constants.Constants() = versions
|
||||
self.computer = self.constants.computer
|
||||
|
||||
# GPU Patch Detection
|
||||
@@ -63,7 +83,12 @@ class detect_root_patch:
|
||||
self.missing_nv_web_opengl = False
|
||||
self.missing_nv_compat = False
|
||||
|
||||
def detect_gpus(self):
|
||||
|
||||
def _detect_gpus(self):
|
||||
"""
|
||||
Query GPUs and set flags for applicable patches
|
||||
"""
|
||||
|
||||
gpus = self.constants.computer.gpus
|
||||
non_metal_os = os_data.os_data.catalina
|
||||
for i, gpu in enumerate(gpus):
|
||||
@@ -75,7 +100,7 @@ class detect_root_patch:
|
||||
self.amfi_must_disable = True
|
||||
if os_data.os_data.ventura in self.constants.legacy_accel_support:
|
||||
self.amfi_shim_bins = True
|
||||
self.legacy_keyboard_backlight = self.check_legacy_keyboard_backlight()
|
||||
self.legacy_keyboard_backlight = self._check_legacy_keyboard_backlight()
|
||||
self.requires_root_kc = True
|
||||
elif gpu.arch == device_probe.NVIDIA.Archs.Kepler and self.constants.force_nv_web is False:
|
||||
if self.constants.detected_os > os_data.os_data.big_sur:
|
||||
@@ -165,7 +190,7 @@ class detect_root_patch:
|
||||
self.amfi_must_disable = True
|
||||
if os_data.os_data.ventura in self.constants.legacy_accel_support:
|
||||
self.amfi_shim_bins = True
|
||||
self.legacy_keyboard_backlight = self.check_legacy_keyboard_backlight()
|
||||
self.legacy_keyboard_backlight = self._check_legacy_keyboard_backlight()
|
||||
self.requires_root_kc = True
|
||||
elif gpu.arch == device_probe.Intel.Archs.Sandy_Bridge:
|
||||
if self.constants.detected_os > non_metal_os:
|
||||
@@ -173,7 +198,7 @@ class detect_root_patch:
|
||||
self.amfi_must_disable = True
|
||||
if os_data.os_data.ventura in self.constants.legacy_accel_support:
|
||||
self.amfi_shim_bins = True
|
||||
self.legacy_keyboard_backlight = self.check_legacy_keyboard_backlight()
|
||||
self.legacy_keyboard_backlight = self._check_legacy_keyboard_backlight()
|
||||
self.requires_root_kc = True
|
||||
elif gpu.arch == device_probe.Intel.Archs.Ivy_Bridge:
|
||||
if self.constants.detected_os > os_data.os_data.big_sur:
|
||||
@@ -220,17 +245,21 @@ class detect_root_patch:
|
||||
self.requires_root_kc = True
|
||||
else:
|
||||
if self.requires_root_kc is True:
|
||||
self.missing_kdk = not self.check_kdk()
|
||||
self.missing_kdk = not self._check_kdk()
|
||||
|
||||
self.check_networking_support()
|
||||
self._check_networking_support()
|
||||
|
||||
|
||||
def check_networking_support(self):
|
||||
# On macOS Ventura, networking support is required to download KDKs.
|
||||
# However for machines such as BCM94322, BCM94328 and Atheros chipsets,
|
||||
# users may only have wifi as their only supported network interface.
|
||||
# Thus we'll allow for KDK-less installs for these machines on first run.
|
||||
# On subsequent runs, we'll require networking to be enabled.
|
||||
def _check_networking_support(self):
|
||||
"""
|
||||
Query for network requirement, ex. KDK downloading
|
||||
|
||||
On macOS Ventura, networking support is required to download KDKs.
|
||||
However for machines such as BCM94322, BCM94328 and Atheros chipsets,
|
||||
users may only have wifi as their only supported network interface.
|
||||
Thus we'll allow for KDK-less installs for these machines on first run.
|
||||
On subsequent runs, we'll require networking to be enabled.
|
||||
"""
|
||||
|
||||
if self.constants.detected_os < os_data.os_data.ventura:
|
||||
return
|
||||
@@ -272,7 +301,11 @@ class detect_root_patch:
|
||||
self.legacy_keyboard_backlight = False
|
||||
|
||||
|
||||
def check_dgpu_status(self):
|
||||
def _check_dgpu_status(self):
|
||||
"""
|
||||
Query whether system has an active dGPU
|
||||
"""
|
||||
|
||||
dgpu = self.constants.computer.dgpu
|
||||
if dgpu:
|
||||
if dgpu.class_code and dgpu.class_code == 0xFFFFFFFF:
|
||||
@@ -281,25 +314,45 @@ class detect_root_patch:
|
||||
return True
|
||||
return False
|
||||
|
||||
def detect_demux(self):
|
||||
|
||||
def _detect_demux(self):
|
||||
"""
|
||||
Query whether system has been demuxed (ex. MacBookPro8,2, disabled dGPU)
|
||||
"""
|
||||
|
||||
# If GFX0 is missing, assume machine was demuxed
|
||||
# -wegnoegpu would also trigger this, so ensure arg is not present
|
||||
if not "-wegnoegpu" in (utilities.get_nvram("boot-args", decode=True) or ""):
|
||||
igpu = self.constants.computer.igpu
|
||||
dgpu = self.check_dgpu_status()
|
||||
dgpu = self._check_dgpu_status()
|
||||
if igpu and not dgpu:
|
||||
return True
|
||||
return False
|
||||
|
||||
def check_legacy_keyboard_backlight(self):
|
||||
|
||||
def _check_legacy_keyboard_backlight(self):
|
||||
"""
|
||||
Query whether system has a legacy keyboard backlight
|
||||
|
||||
Returns:
|
||||
bool: True if legacy keyboard backlight, False otherwise
|
||||
"""
|
||||
|
||||
# iMac12,x+ have an 'ACPI0008' device, but it's not a keyboard backlight
|
||||
# Best to assume laptops will have a keyboard backlight
|
||||
if self.model.startswith("MacBook"):
|
||||
return self.constants.computer.ambient_light_sensor
|
||||
return False
|
||||
|
||||
def check_nv_web_nvram(self):
|
||||
# First check boot-args, then dedicated nvram variable
|
||||
|
||||
def _check_nv_web_nvram(self):
|
||||
"""
|
||||
Query for Nvidia Web Driver property: nvda_drv_vrl or nvda_drv
|
||||
|
||||
Returns:
|
||||
bool: True if property is present, False otherwise
|
||||
"""
|
||||
|
||||
nv_on = utilities.get_nvram("boot-args", decode=True)
|
||||
if nv_on:
|
||||
if "nvda_drv_vrl=" in nv_on:
|
||||
@@ -309,8 +362,17 @@ class detect_root_patch:
|
||||
return True
|
||||
return False
|
||||
|
||||
def check_nv_web_opengl(self):
|
||||
# First check boot-args, then whether property exists on GPU
|
||||
|
||||
def _check_nv_web_opengl(self):
|
||||
"""
|
||||
Query for Nvidia Web Driver property: ngfxgl
|
||||
|
||||
Verify Web Drivers will run in OpenGL mode
|
||||
|
||||
Returns:
|
||||
bool: True if property is present, False otherwise
|
||||
"""
|
||||
|
||||
nv_on = utilities.get_nvram("boot-args", decode=True)
|
||||
if nv_on:
|
||||
if "ngfxgl=" in nv_on:
|
||||
@@ -321,8 +383,17 @@ class detect_root_patch:
|
||||
return True
|
||||
return False
|
||||
|
||||
def check_nv_compat(self):
|
||||
# Check for 'nv_web' in boot-args, then whether property exists on GPU
|
||||
|
||||
def _check_nv_compat(self):
|
||||
"""
|
||||
Query for Nvidia Web Driver property: ngfxcompat
|
||||
|
||||
Verify Web Drivers will skip NVDAStartupWeb compatibility check
|
||||
|
||||
Returns:
|
||||
bool: True if property is present, False otherwise
|
||||
"""
|
||||
|
||||
nv_on = utilities.get_nvram("boot-args", decode=True)
|
||||
if nv_on:
|
||||
if "ngfxcompat=" in nv_on:
|
||||
@@ -333,13 +404,37 @@ class detect_root_patch:
|
||||
return True
|
||||
return False
|
||||
|
||||
def check_whatevergreen(self):
|
||||
|
||||
def _check_whatevergreen(self):
|
||||
"""
|
||||
Query whether WhateverGreen.kext is loaded
|
||||
|
||||
Returns:
|
||||
bool: True if loaded, False otherwise
|
||||
"""
|
||||
|
||||
return utilities.check_kext_loaded("WhateverGreen", self.constants.detected_os)
|
||||
|
||||
def check_kdk(self):
|
||||
|
||||
def _check_kdk(self):
|
||||
"""
|
||||
Query whether Kernel Debug Kit is installed
|
||||
|
||||
Returns:
|
||||
bool: True if installed, False otherwise
|
||||
"""
|
||||
|
||||
return kdk_handler.KernelDebugKitObject(self.constants, self.constants.detected_os_build, self.constants.detected_os_version, passive=True).kdk_already_installed
|
||||
|
||||
def check_sip(self):
|
||||
|
||||
def _check_sip(self):
|
||||
"""
|
||||
Query System Integrity checks required for patching
|
||||
|
||||
Returns:
|
||||
tuple: (list, str, str) of SIP values, SIP hex, SIP error message
|
||||
"""
|
||||
|
||||
if self.constants.detected_os > os_data.os_data.catalina:
|
||||
if self.nvidia_web is True:
|
||||
sip = sip_data.system_integrity_protection.root_patch_sip_big_sur_3rd_part_kexts
|
||||
@@ -365,7 +460,15 @@ class detect_root_patch:
|
||||
sip_value = f"For Hackintoshes, please set csr-active-config to '03060000' ({sip_hex})\nFor non-OpenCore Macs, please run 'csrutil disable' in RecoveryOS"
|
||||
return (sip, sip_value, sip_hex)
|
||||
|
||||
def check_uhci_ohci(self):
|
||||
|
||||
def _check_uhci_ohci(self):
|
||||
"""
|
||||
Query whether host has UHCI/OHCI controllers, and requires USB 1.1 patches
|
||||
|
||||
Returns:
|
||||
bool: True if UHCI/OHCI patches required, False otherwise
|
||||
"""
|
||||
|
||||
if self.constants.detected_os < os_data.os_data.ventura:
|
||||
return False
|
||||
|
||||
@@ -399,10 +502,19 @@ class detect_root_patch:
|
||||
|
||||
return False
|
||||
|
||||
|
||||
# Entry point for patch set detection
|
||||
def detect_patch_set(self):
|
||||
"""
|
||||
Query patch sets required for host
|
||||
|
||||
Returns:
|
||||
dict: Dictionary of patch sets
|
||||
"""
|
||||
|
||||
self.has_network = network_handler.NetworkUtilities().verify_network_connection()
|
||||
|
||||
if self.check_uhci_ohci() is True:
|
||||
if self._check_uhci_ohci() is True:
|
||||
self.legacy_uhci_ohci = True
|
||||
self.requires_root_kc = True
|
||||
|
||||
@@ -435,12 +547,12 @@ class detect_root_patch:
|
||||
if self.constants.detected_os > os_data.os_data.high_sierra:
|
||||
if self.model in ["MacBookPro8,2", "MacBookPro8,3"]:
|
||||
# Ref: https://doslabelectronics.com/Demux.html
|
||||
if self.detect_demux() is True:
|
||||
if self._detect_demux() is True:
|
||||
self.legacy_gmux = True
|
||||
else:
|
||||
self.legacy_gmux = True
|
||||
|
||||
self.detect_gpus()
|
||||
self._detect_gpus()
|
||||
|
||||
self.root_patch_dict = {
|
||||
"Graphics: Nvidia Tesla": self.nvidia_tesla,
|
||||
@@ -467,11 +579,11 @@ class detect_root_patch:
|
||||
"Settings: Supports Auxiliary Cache": not self.requires_root_kc,
|
||||
"Settings: Kernel Debug Kit missing": self.missing_kdk if self.constants.detected_os >= os_data.os_data.ventura.value else False,
|
||||
"Validation: Patching Possible": self.verify_patch_allowed(),
|
||||
"Validation: Unpatching Possible": self.verify_unpatch_allowed(),
|
||||
f"Validation: SIP is enabled (Required: {self.check_sip()[2]} or higher)": self.sip_enabled,
|
||||
"Validation: Unpatching Possible": self._verify_unpatch_allowed(),
|
||||
f"Validation: SIP is enabled (Required: {self._check_sip()[2]} or higher)": self.sip_enabled,
|
||||
f"Validation: Currently Booted SIP: ({hex(py_sip_xnu.SipXnu().get_sip_status().value)})": self.sip_enabled,
|
||||
"Validation: SecureBootModel is enabled": self.sbm_enabled,
|
||||
f"Validation: {'AMFI' if self.constants.host_is_hackintosh is True or self.get_amfi_level_needed() > 2 else 'Library Validation'} is enabled": self.amfi_enabled if self.amfi_must_disable is True else False,
|
||||
f"Validation: {'AMFI' if self.constants.host_is_hackintosh is True or self._get_amfi_level_needed() > 2 else 'Library Validation'} is enabled": self.amfi_enabled if self.amfi_must_disable is True else False,
|
||||
"Validation: FileVault is enabled": self.fv_enabled,
|
||||
"Validation: System is dosdude1 patched": self.dosdude_patched,
|
||||
"Validation: WhateverGreen.kext missing": self.missing_whatever_green if self.nvidia_web is True else False,
|
||||
@@ -483,30 +595,53 @@ class detect_root_patch:
|
||||
|
||||
return self.root_patch_dict
|
||||
|
||||
def get_amfi_level_needed(self):
|
||||
if self.amfi_must_disable is True:
|
||||
if self.constants.detected_os > os_data.os_data.catalina:
|
||||
if self.constants.detected_os >= os_data.os_data.ventura:
|
||||
if self.amfi_shim_bins is True:
|
||||
# Currently we require AMFI outright disabled
|
||||
# in Ventura to work with shim'd binaries
|
||||
return amfi_detect.AmfiConfigDetectLevel.ALLOW_ALL
|
||||
return amfi_detect.AmfiConfigDetectLevel.LIBRARY_VALIDATION
|
||||
return amfi_detect.AmfiConfigDetectLevel.NO_CHECK
|
||||
|
||||
def verify_patch_allowed(self, print_errors=False):
|
||||
sip_dict = self.check_sip()
|
||||
def _get_amfi_level_needed(self):
|
||||
"""
|
||||
Query the AMFI level needed for the patcher to work
|
||||
|
||||
Returns:
|
||||
int: AMFI level needed
|
||||
"""
|
||||
|
||||
if self.amfi_must_disable is False:
|
||||
return amfi_detect.AmfiConfigDetectLevel.NO_CHECK
|
||||
|
||||
if self.constants.detected_os < os_data.os_data.big_sur:
|
||||
return amfi_detect.AmfiConfigDetectLevel.NO_CHECK
|
||||
|
||||
if self.constants.detected_os >= os_data.os_data.ventura:
|
||||
if self.amfi_shim_bins is True:
|
||||
# Currently we require AMFI outright disabled
|
||||
# in Ventura to work with shim'd binaries
|
||||
return amfi_detect.AmfiConfigDetectLevel.ALLOW_ALL
|
||||
|
||||
return amfi_detect.AmfiConfigDetectLevel.LIBRARY_VALIDATION
|
||||
|
||||
|
||||
def verify_patch_allowed(self, print_errors: bool = False):
|
||||
"""
|
||||
Validate that the patcher can be run
|
||||
|
||||
Parameters:
|
||||
print_errors (bool): Print errors to console
|
||||
|
||||
Returns:
|
||||
bool: True if patching is allowed, False otherwise
|
||||
"""
|
||||
|
||||
sip_dict = self._check_sip()
|
||||
sip = sip_dict[0]
|
||||
sip_value = sip_dict[1]
|
||||
|
||||
self.sip_enabled, self.sbm_enabled, self.fv_enabled, self.dosdude_patched = utilities.patching_status(sip, self.constants.detected_os)
|
||||
self.amfi_enabled = not amfi_detect.AmfiConfigurationDetection().check_config(self.get_amfi_level_needed())
|
||||
self.amfi_enabled = not amfi_detect.AmfiConfigurationDetection().check_config(self._get_amfi_level_needed())
|
||||
|
||||
if self.nvidia_web is True:
|
||||
self.missing_nv_web_nvram = not self.check_nv_web_nvram()
|
||||
self.missing_nv_web_opengl = not self.check_nv_web_opengl()
|
||||
self.missing_nv_compat = not self.check_nv_compat()
|
||||
self.missing_whatever_green = not self.check_whatevergreen()
|
||||
self.missing_nv_web_nvram = not self._check_nv_web_nvram()
|
||||
self.missing_nv_web_opengl = not self._check_nv_web_opengl()
|
||||
self.missing_nv_compat = not self._check_nv_compat()
|
||||
self.missing_whatever_green = not self._check_whatevergreen()
|
||||
|
||||
if print_errors is True:
|
||||
if self.sip_enabled is True:
|
||||
@@ -574,28 +709,54 @@ class detect_root_patch:
|
||||
]
|
||||
):
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
def verify_unpatch_allowed(self, print_errors=False):
|
||||
# Must be called after verify_patch_allowed
|
||||
return True
|
||||
|
||||
|
||||
def _verify_unpatch_allowed(self):
|
||||
"""
|
||||
Validate that the unpatcher can be run
|
||||
|
||||
Preconditions:
|
||||
Must be called after verify_patch_allowed()
|
||||
|
||||
Returns:
|
||||
bool: True if unpatching is allowed, False otherwise
|
||||
"""
|
||||
|
||||
return not self.sip_enabled
|
||||
|
||||
def generate_patchset(self, hardware_details):
|
||||
all_hardware_patchset = sys_patch_dict.SystemPatchDictionary(self.constants.detected_os, self.constants.detected_os_minor, self.constants.legacy_accel_support)
|
||||
required_patches = {}
|
||||
|
||||
def generate_patchset(self, hardware_details: dict):
|
||||
"""
|
||||
Generate Patchset dictionary for the current system
|
||||
|
||||
Parameters:
|
||||
hardware_details (dict): Dictionary of hardware details generated by detect_patch_set()
|
||||
|
||||
Returns:
|
||||
dict: Dictionary of patches to be applied from sys_patch_dict.py
|
||||
"""
|
||||
|
||||
all_hardware_patchset: dict = sys_patch_dict.SystemPatchDictionary(self.constants.detected_os, self.constants.detected_os_minor, self.constants.legacy_accel_support)
|
||||
required_patches: dict = {}
|
||||
|
||||
utilities.cls()
|
||||
|
||||
logging.info("- The following patches will be applied:")
|
||||
|
||||
if hardware_details["Graphics: Intel Ironlake"] is True:
|
||||
required_patches.update({"Non-Metal Common": all_hardware_patchset["Graphics"]["Non-Metal Common"]})
|
||||
required_patches.update({"WebKit Monterey Common": all_hardware_patchset["Graphics"]["WebKit Monterey Common"]})
|
||||
required_patches.update({"Intel Ironlake": all_hardware_patchset["Graphics"]["Intel Ironlake"]})
|
||||
|
||||
if hardware_details["Graphics: Intel Sandy Bridge"] is True:
|
||||
required_patches.update({"Non-Metal Common": all_hardware_patchset["Graphics"]["Non-Metal Common"]})
|
||||
required_patches.update({"Non-Metal ColorSync Workaround": all_hardware_patchset["Graphics"]["Non-Metal ColorSync Workaround"]})
|
||||
required_patches.update({"High Sierra GVA": all_hardware_patchset["Graphics"]["High Sierra GVA"]})
|
||||
required_patches.update({"WebKit Monterey Common": all_hardware_patchset["Graphics"]["WebKit Monterey Common"]})
|
||||
required_patches.update({"Intel Sandy Bridge": all_hardware_patchset["Graphics"]["Intel Sandy Bridge"]})
|
||||
|
||||
if hardware_details["Graphics: Intel Ivy Bridge"] is True:
|
||||
required_patches.update({"Metal 3802 Common": all_hardware_patchset["Graphics"]["Metal 3802 Common"]})
|
||||
required_patches.update({"Catalina GVA": all_hardware_patchset["Graphics"]["Catalina GVA"]})
|
||||
@@ -603,23 +764,28 @@ class detect_root_patch:
|
||||
required_patches.update({"Big Sur OpenCL": all_hardware_patchset["Graphics"]["Big Sur OpenCL"]})
|
||||
required_patches.update({"WebKit Monterey Common": all_hardware_patchset["Graphics"]["WebKit Monterey Common"]})
|
||||
required_patches.update({"Intel Ivy Bridge": all_hardware_patchset["Graphics"]["Intel Ivy Bridge"]})
|
||||
|
||||
if hardware_details["Graphics: Intel Haswell"] is True:
|
||||
required_patches.update({"Metal 3802 Common": all_hardware_patchset["Graphics"]["Metal 3802 Common"]})
|
||||
required_patches.update({"Monterey GVA": all_hardware_patchset["Graphics"]["Monterey GVA"]})
|
||||
required_patches.update({"Monterey OpenCL": all_hardware_patchset["Graphics"]["Monterey OpenCL"]})
|
||||
required_patches.update({"Intel Haswell": all_hardware_patchset["Graphics"]["Intel Haswell"]})
|
||||
|
||||
if hardware_details["Graphics: Intel Broadwell"] is True:
|
||||
required_patches.update({"Monterey GVA": all_hardware_patchset["Graphics"]["Monterey GVA"]})
|
||||
required_patches.update({"Monterey OpenCL": all_hardware_patchset["Graphics"]["Monterey OpenCL"]})
|
||||
required_patches.update({"Intel Broadwell": all_hardware_patchset["Graphics"]["Intel Broadwell"]})
|
||||
|
||||
if hardware_details["Graphics: Intel Skylake"] is True:
|
||||
required_patches.update({"Monterey GVA": all_hardware_patchset["Graphics"]["Monterey GVA"]})
|
||||
required_patches.update({"Monterey OpenCL": all_hardware_patchset["Graphics"]["Monterey OpenCL"]})
|
||||
required_patches.update({"Intel Skylake": all_hardware_patchset["Graphics"]["Intel Skylake"]})
|
||||
|
||||
if hardware_details["Graphics: Nvidia Tesla"] is True:
|
||||
required_patches.update({"Non-Metal Common": all_hardware_patchset["Graphics"]["Non-Metal Common"]})
|
||||
required_patches.update({"WebKit Monterey Common": all_hardware_patchset["Graphics"]["WebKit Monterey Common"]})
|
||||
required_patches.update({"Nvidia Tesla": all_hardware_patchset["Graphics"]["Nvidia Tesla"]})
|
||||
|
||||
if hardware_details["Graphics: Nvidia Web Drivers"] is True:
|
||||
required_patches.update({"Non-Metal Common": all_hardware_patchset["Graphics"]["Non-Metal Common"]})
|
||||
required_patches.update({"Non-Metal IOAccelerator Common": all_hardware_patchset["Graphics"]["Non-Metal IOAccelerator Common"]})
|
||||
@@ -627,6 +793,7 @@ class detect_root_patch:
|
||||
required_patches.update({"WebKit Monterey Common": all_hardware_patchset["Graphics"]["WebKit Monterey Common"]})
|
||||
required_patches.update({"Nvidia Web Drivers": all_hardware_patchset["Graphics"]["Nvidia Web Drivers"]})
|
||||
required_patches.update({"Non-Metal Enforcement": all_hardware_patchset["Graphics"]["Non-Metal Enforcement"]})
|
||||
|
||||
if hardware_details["Graphics: Nvidia Kepler"] is True:
|
||||
required_patches.update({"Revert Metal Downgrade": all_hardware_patchset["Graphics"]["Revert Metal Downgrade"]})
|
||||
required_patches.update({"Metal 3802 Common": all_hardware_patchset["Graphics"]["Metal 3802 Common"]})
|
||||
@@ -641,11 +808,13 @@ class detect_root_patch:
|
||||
if "Catalina GVA" in required_patches:
|
||||
del(required_patches["Catalina GVA"])
|
||||
break
|
||||
|
||||
if hardware_details["Graphics: AMD TeraScale 1"] is True:
|
||||
required_patches.update({"Non-Metal Common": all_hardware_patchset["Graphics"]["Non-Metal Common"]})
|
||||
required_patches.update({"WebKit Monterey Common": all_hardware_patchset["Graphics"]["WebKit Monterey Common"]})
|
||||
required_patches.update({"AMD TeraScale Common": all_hardware_patchset["Graphics"]["AMD TeraScale Common"]})
|
||||
required_patches.update({"AMD TeraScale 1": all_hardware_patchset["Graphics"]["AMD TeraScale 1"]})
|
||||
|
||||
if hardware_details["Graphics: AMD TeraScale 2"] is True:
|
||||
required_patches.update({"Non-Metal Common": all_hardware_patchset["Graphics"]["Non-Metal Common"]})
|
||||
required_patches.update({"Non-Metal IOAccelerator Common": all_hardware_patchset["Graphics"]["Non-Metal IOAccelerator Common"]})
|
||||
@@ -656,6 +825,7 @@ class detect_root_patch:
|
||||
# TeraScale 2 MacBooks with faulty GPUs are highly prone to crashing with AMDRadeonX3000 attached
|
||||
# Additionally, AMDRadeonX3000 requires IOAccelerator downgrade which is not installed without 'Non-Metal IOAccelerator Common'
|
||||
del(required_patches["AMD TeraScale 2"]["Install"]["/System/Library/Extensions"]["AMDRadeonX3000.kext"])
|
||||
|
||||
if hardware_details["Graphics: AMD Legacy GCN"] is True or hardware_details["Graphics: AMD Legacy Polaris"] is True:
|
||||
required_patches.update({"Revert Metal Downgrade": all_hardware_patchset["Graphics"]["Revert Metal Downgrade"]})
|
||||
required_patches.update({"Monterey GVA": all_hardware_patchset["Graphics"]["Monterey GVA"]})
|
||||
@@ -666,6 +836,7 @@ class detect_root_patch:
|
||||
required_patches.update({"AMD Legacy Polaris": all_hardware_patchset["Graphics"]["AMD Legacy Polaris"]})
|
||||
if "AVX2" not in self.constants.computer.cpu.leafs:
|
||||
required_patches.update({"AMD OpenCL": all_hardware_patchset["Graphics"]["AMD OpenCL"]})
|
||||
|
||||
if hardware_details["Graphics: AMD Legacy Vega"] is True:
|
||||
required_patches.update({"Monterey GVA": all_hardware_patchset["Graphics"]["Monterey GVA"]})
|
||||
required_patches.update({"Monterey OpenCL": all_hardware_patchset["Graphics"]["Monterey OpenCL"]})
|
||||
@@ -673,20 +844,26 @@ class detect_root_patch:
|
||||
required_patches.update({"AMD OpenCL": all_hardware_patchset["Graphics"]["AMD OpenCL"]})
|
||||
if hardware_details["Graphics: AMD Legacy GCN"] is True:
|
||||
required_patches.update({"AMD Legacy Vega Extended": all_hardware_patchset["Graphics"]["AMD Legacy Vega Extended"]})
|
||||
|
||||
if hardware_details["Brightness: Legacy Backlight Control"] is True:
|
||||
required_patches.update({"Legacy Backlight Control": all_hardware_patchset["Brightness"]["Legacy Backlight Control"]})
|
||||
|
||||
if hardware_details["Audio: Legacy Realtek"] is True:
|
||||
if self.model in ["iMac7,1", "iMac8,1"]:
|
||||
required_patches.update({"Legacy Realtek": all_hardware_patchset["Audio"]["Legacy Realtek"]})
|
||||
else:
|
||||
required_patches.update({"Legacy Non-GOP": all_hardware_patchset["Audio"]["Legacy Non-GOP"]})
|
||||
|
||||
if hardware_details["Networking: Legacy Wireless"] is True:
|
||||
required_patches.update({"Legacy Wireless": all_hardware_patchset["Networking"]["Legacy Wireless"]})
|
||||
required_patches.update({"Legacy Wireless Extended": all_hardware_patchset["Networking"]["Legacy Wireless Extended"]})
|
||||
|
||||
if hardware_details["Miscellaneous: Legacy GMUX"] is True:
|
||||
required_patches.update({"Legacy GMUX": all_hardware_patchset["Miscellaneous"]["Legacy GMUX"]})
|
||||
|
||||
if hardware_details["Miscellaneous: Legacy Keyboard Backlight"] is True:
|
||||
required_patches.update({"Legacy Keyboard Backlight": all_hardware_patchset["Miscellaneous"]["Legacy Keyboard Backlight"]})
|
||||
|
||||
if hardware_details["Miscellaneous: Legacy USB 1.1"] is True:
|
||||
required_patches.update({"Legacy USB 1.1": all_hardware_patchset["Miscellaneous"]["Legacy USB 1.1"]})
|
||||
|
||||
|
||||
@@ -1,58 +1,86 @@
|
||||
# Additional support functions for sys_patch.py
|
||||
# Copyright (C) 2020-2022, Dhinak G, Mykola Grymalyuk
|
||||
# Copyright (C) 2020-2023, Dhinak G, Mykola Grymalyuk
|
||||
|
||||
import subprocess
|
||||
import tempfile
|
||||
from data import os_data
|
||||
from resources import generate_smbios, utilities
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
import plistlib
|
||||
import os
|
||||
import logging
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
from resources import kdk_handler, bplist
|
||||
|
||||
class sys_patch_helpers:
|
||||
|
||||
def __init__(self, constants):
|
||||
self.constants = constants
|
||||
from data import os_data
|
||||
from resources import bplist, constants, generate_smbios, utilities
|
||||
|
||||
|
||||
def snb_board_id_patch(self, source_files_path):
|
||||
# AppleIntelSNBGraphicsFB hard codes the supported Board IDs for Sandy Bridge iGPUs
|
||||
# Because of this, the kext errors out on unsupported systems
|
||||
# This function simply patches in a supported Board ID, using 'determine_best_board_id_for_sandy()'
|
||||
# to supplement the ideal Board ID
|
||||
class SysPatchHelpers:
|
||||
"""
|
||||
Library of helper functions for sys_patch.py and related libraries
|
||||
"""
|
||||
|
||||
def __init__(self, global_constants: constants.Constants):
|
||||
self.constants: constants.Constants = global_constants
|
||||
|
||||
|
||||
def snb_board_id_patch(self, source_files_path: str):
|
||||
"""
|
||||
Patch AppleIntelSNBGraphicsFB.kext to support unsupported Board IDs
|
||||
|
||||
AppleIntelSNBGraphicsFB hard codes the supported Board IDs for Sandy Bridge iGPUs
|
||||
Because of this, the kext errors out on unsupported systems
|
||||
This function simply patches in a supported Board ID, using 'determine_best_board_id_for_sandy()'
|
||||
to supplement the ideal Board ID
|
||||
|
||||
Parameters:
|
||||
source_files_path (str): Path to the source files
|
||||
|
||||
"""
|
||||
|
||||
source_files_path = str(source_files_path)
|
||||
if self.constants.computer.reported_board_id not in self.constants.sandy_board_id_stock:
|
||||
logging.info(f"- Found unsupported Board ID {self.constants.computer.reported_board_id}, performing AppleIntelSNBGraphicsFB bin patching")
|
||||
board_to_patch = generate_smbios.determine_best_board_id_for_sandy(self.constants.computer.reported_board_id, self.constants.computer.gpus)
|
||||
logging.info(f"- Replacing {board_to_patch} with {self.constants.computer.reported_board_id}")
|
||||
|
||||
board_to_patch_hex = bytes.fromhex(board_to_patch.encode('utf-8').hex())
|
||||
reported_board_hex = bytes.fromhex(self.constants.computer.reported_board_id.encode('utf-8').hex())
|
||||
if self.constants.computer.reported_board_id in self.constants.sandy_board_id_stock:
|
||||
return
|
||||
|
||||
if len(board_to_patch_hex) > len(reported_board_hex):
|
||||
# Pad the reported Board ID with zeros to match the length of the board to patch
|
||||
reported_board_hex = reported_board_hex + bytes(len(board_to_patch_hex) - len(reported_board_hex))
|
||||
elif len(board_to_patch_hex) < len(reported_board_hex):
|
||||
logging.info(f"- Error: Board ID {self.constants.computer.reported_board_id} is longer than {board_to_patch}")
|
||||
raise Exception("Host's Board ID is longer than the kext's Board ID, cannot patch!!!")
|
||||
logging.info(f"- Found unsupported Board ID {self.constants.computer.reported_board_id}, performing AppleIntelSNBGraphicsFB bin patching")
|
||||
|
||||
path = source_files_path + "/10.13.6/System/Library/Extensions/AppleIntelSNBGraphicsFB.kext/Contents/MacOS/AppleIntelSNBGraphicsFB"
|
||||
if Path(path).exists():
|
||||
with open(path, 'rb') as f:
|
||||
data = f.read()
|
||||
data = data.replace(board_to_patch_hex, reported_board_hex)
|
||||
with open(path, 'wb') as f:
|
||||
f.write(data)
|
||||
else:
|
||||
logging.info(f"- Error: Could not find {path}")
|
||||
raise Exception("Failed to find AppleIntelSNBGraphicsFB.kext, cannot patch!!!")
|
||||
board_to_patch = generate_smbios.determine_best_board_id_for_sandy(self.constants.computer.reported_board_id, self.constants.computer.gpus)
|
||||
logging.info(f"- Replacing {board_to_patch} with {self.constants.computer.reported_board_id}")
|
||||
|
||||
board_to_patch_hex = bytes.fromhex(board_to_patch.encode('utf-8').hex())
|
||||
reported_board_hex = bytes.fromhex(self.constants.computer.reported_board_id.encode('utf-8').hex())
|
||||
|
||||
if len(board_to_patch_hex) > len(reported_board_hex):
|
||||
# Pad the reported Board ID with zeros to match the length of the board to patch
|
||||
reported_board_hex = reported_board_hex + bytes(len(board_to_patch_hex) - len(reported_board_hex))
|
||||
elif len(board_to_patch_hex) < len(reported_board_hex):
|
||||
logging.info(f"- Error: Board ID {self.constants.computer.reported_board_id} is longer than {board_to_patch}")
|
||||
raise Exception("Host's Board ID is longer than the kext's Board ID, cannot patch!!!")
|
||||
|
||||
path = source_files_path + "/10.13.6/System/Library/Extensions/AppleIntelSNBGraphicsFB.kext/Contents/MacOS/AppleIntelSNBGraphicsFB"
|
||||
if not Path(path).exists():
|
||||
logging.info(f"- Error: Could not find {path}")
|
||||
raise Exception("Failed to find AppleIntelSNBGraphicsFB.kext, cannot patch!!!")
|
||||
|
||||
with open(path, 'rb') as f:
|
||||
data = f.read()
|
||||
data = data.replace(board_to_patch_hex, reported_board_hex)
|
||||
with open(path, 'wb') as f:
|
||||
f.write(data)
|
||||
|
||||
|
||||
def generate_patchset_plist(self, patchset, file_name, kdk_used):
|
||||
def generate_patchset_plist(self, patchset: dict, file_name: str, kdk_used: Path):
|
||||
"""
|
||||
Generate patchset file for user reference
|
||||
|
||||
Parameters:
|
||||
patchset (dict): Dictionary of patchset, see sys_patch_detect.py and sys_patch_dict.py
|
||||
file_name (str): Name of the file to write to
|
||||
kdk_used (Path): Path to the KDK used, if any
|
||||
|
||||
Returns:
|
||||
bool: True if successful, False if not
|
||||
|
||||
"""
|
||||
|
||||
source_path = f"{self.constants.payload_path}"
|
||||
source_path_file = f"{source_path}/{file_name}"
|
||||
|
||||
@@ -68,23 +96,35 @@ class sys_patch_helpers:
|
||||
"Kernel Debug Kit Used": f"{kdk_string}",
|
||||
"OS Version": f"{self.constants.detected_os}.{self.constants.detected_os_minor} ({self.constants.detected_os_build})",
|
||||
}
|
||||
|
||||
data.update(patchset)
|
||||
|
||||
if Path(source_path_file).exists():
|
||||
os.remove(source_path_file)
|
||||
|
||||
# Need to write to a safe location
|
||||
plistlib.dump(data, Path(source_path_file).open("wb"), sort_keys=False)
|
||||
|
||||
if Path(source_path_file).exists():
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def disable_window_server_caching(self):
|
||||
# On legacy GCN GPUs, the WindowServer cache generated creates
|
||||
# corrupted Opaque shaders.
|
||||
# To work-around this, we disable WindowServer caching
|
||||
# And force macOS into properly generating the Opaque shaders
|
||||
"""
|
||||
Disable WindowServer's asset caching
|
||||
|
||||
On legacy GCN GPUs, the WindowServer cache generated creates
|
||||
corrupted Opaque shaders.
|
||||
|
||||
To work-around this, we disable WindowServer caching
|
||||
And force macOS into properly generating the Opaque shaders
|
||||
"""
|
||||
|
||||
if self.constants.detected_os < os_data.os_data.ventura:
|
||||
return
|
||||
|
||||
logging.info("- Disabling WindowServer Caching")
|
||||
# Invoke via 'bash -c' to resolve pathing
|
||||
utilities.elevated(["bash", "-c", "rm -rf /private/var/folders/*/*/*/WindowServer/com.apple.WindowServer"])
|
||||
@@ -96,11 +136,17 @@ class sys_patch_helpers:
|
||||
|
||||
|
||||
def remove_news_widgets(self):
|
||||
# On Ivy Bridge and Haswell iGPUs, RenderBox will crash the News Widgets in
|
||||
# Notification Centre. To ensure users can access Notifications normally,
|
||||
# we manually remove all News Widgets
|
||||
"""
|
||||
Remove News Widgets from Notification Centre
|
||||
|
||||
On Ivy Bridge and Haswell iGPUs, RenderBox will crash the News Widgets in
|
||||
Notification Centre. To ensure users can access Notifications normally,
|
||||
we manually remove all News Widgets
|
||||
"""
|
||||
|
||||
if self.constants.detected_os < os_data.os_data.ventura:
|
||||
return
|
||||
|
||||
logging.info("- Parsing Notification Centre Widgets")
|
||||
file_path = "~/Library/Containers/com.apple.notificationcenterui/Data/Library/Preferences/com.apple.notificationcenterui.plist"
|
||||
file_path = Path(file_path).expanduser()
|
||||
@@ -112,22 +158,27 @@ class sys_patch_helpers:
|
||||
did_find = False
|
||||
with open(file_path, "rb") as f:
|
||||
data = plistlib.load(f)
|
||||
if "widgets" in data:
|
||||
if "instances" in data["widgets"]:
|
||||
for widget in list(data["widgets"]["instances"]):
|
||||
widget_data = bplist.BPListReader(widget).parse()
|
||||
for entry in widget_data:
|
||||
if not 'widget' in entry:
|
||||
continue
|
||||
sub_data = bplist.BPListReader(widget_data[entry]).parse()
|
||||
for sub_entry in sub_data:
|
||||
if not '$object' in sub_entry:
|
||||
continue
|
||||
if not b'com.apple.news' in sub_data[sub_entry][2]:
|
||||
continue
|
||||
logging.info(f" - Found News Widget to remove: {sub_data[sub_entry][2].decode('ascii')}")
|
||||
data["widgets"]["instances"].remove(widget)
|
||||
did_find = True
|
||||
if "widgets" not in data:
|
||||
return
|
||||
|
||||
if "instances" not in data["widgets"]:
|
||||
return
|
||||
|
||||
for widget in list(data["widgets"]["instances"]):
|
||||
widget_data = bplist.BPListReader(widget).parse()
|
||||
for entry in widget_data:
|
||||
if 'widget' not in entry:
|
||||
continue
|
||||
sub_data = bplist.BPListReader(widget_data[entry]).parse()
|
||||
for sub_entry in sub_data:
|
||||
if not '$object' in sub_entry:
|
||||
continue
|
||||
if not b'com.apple.news' in sub_data[sub_entry][2]:
|
||||
continue
|
||||
logging.info(f" - Found News Widget to remove: {sub_data[sub_entry][2].decode('ascii')}")
|
||||
data["widgets"]["instances"].remove(widget)
|
||||
did_find = True
|
||||
|
||||
if did_find:
|
||||
with open(file_path, "wb") as f:
|
||||
plistlib.dump(data, f, sort_keys=False)
|
||||
@@ -135,16 +186,23 @@ class sys_patch_helpers:
|
||||
|
||||
|
||||
def install_rsr_repair_binary(self):
|
||||
# With macOS 13.2, Apple implemented the Rapid Security Response System
|
||||
# However Apple added a half baked snapshot reversion system if seal was broken,
|
||||
# which forgets to handle Preboot BootKC syncing
|
||||
"""
|
||||
Installs RSRRepair
|
||||
|
||||
# Thus this application will try to re-sync the BootKC with SysKC in the event of a panic
|
||||
# Reference: https://github.com/dortania/OpenCore-Legacy-Patcher/issues/1019
|
||||
RSRRepair is a utility that will sync the SysKC and BootKC in the event of a panic
|
||||
|
||||
# This is a (hopefully) temporary work-around, however likely to stay.
|
||||
# RSRRepair has the added bonus of fixing desynced KCs from 'bless', so useful in Big Sur+
|
||||
# https://github.com/flagersgit/RSRRepair
|
||||
With macOS 13.2, Apple implemented the Rapid Security Response System
|
||||
However Apple added a half baked snapshot reversion system if seal was broken,
|
||||
which forgets to handle Preboot BootKC syncing.
|
||||
|
||||
Thus this application will try to re-sync the BootKC with SysKC in the event of a panic
|
||||
Reference: https://github.com/dortania/OpenCore-Legacy-Patcher/issues/1019
|
||||
|
||||
This is a (hopefully) temporary work-around, however likely to stay.
|
||||
RSRRepair has the added bonus of fixing desynced KCs from 'bless', so useful in Big Sur+
|
||||
Source: https://github.com/flagersgit/RSRRepair
|
||||
|
||||
"""
|
||||
|
||||
if self.constants.detected_os < os_data.os_data.big_sur:
|
||||
return
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
# Copyright (C) 2020-2022, Dhinak G, Mykola Grymalyuk
|
||||
from resources import utilities
|
||||
|
||||
class TUIMenu:
|
||||
def __init__(self, title, prompt, options=None, return_number_instead_of_direct_call=False, add_quit=True, auto_number=False, in_between=None, top_level=False, loop=False):
|
||||
self.title = title
|
||||
self.prompt = prompt
|
||||
self.in_between = in_between or []
|
||||
self.options = options or []
|
||||
self.return_number_instead_of_direct_call = return_number_instead_of_direct_call
|
||||
self.auto_number = auto_number
|
||||
self.add_quit = add_quit
|
||||
self.top_level = top_level
|
||||
self.loop = loop
|
||||
self.added_quit = False
|
||||
|
||||
def add_menu_option(self, name, description="", function=None, key=""):
|
||||
self.options.append([key, name, description, function])
|
||||
|
||||
def start(self):
|
||||
return_option = ["Q", "Quit"] if self.top_level else ["B", "Back"]
|
||||
if self.add_quit and not self.added_quit:
|
||||
self.add_menu_option(return_option[1], function=None, key=return_option[0])
|
||||
self.added_quit = True
|
||||
|
||||
while True:
|
||||
utilities.cls()
|
||||
utilities.header(self.title)
|
||||
print()
|
||||
|
||||
for i in self.in_between:
|
||||
print(i)
|
||||
if self.in_between:
|
||||
print()
|
||||
|
||||
for index, option in enumerate(self.options):
|
||||
if self.auto_number and not (index == (len(self.options) - 1) and self.add_quit):
|
||||
option[0] = str((index + 1))
|
||||
print(option[0] + ". " + option[1])
|
||||
for i in option[2]:
|
||||
print("\t" + i)
|
||||
|
||||
print()
|
||||
selected = input(self.prompt)
|
||||
|
||||
keys = [option[0].upper() for option in self.options]
|
||||
if not selected or selected.upper() not in keys:
|
||||
if self.loop:
|
||||
continue
|
||||
else:
|
||||
return
|
||||
if self.add_quit and selected.upper() == return_option[0]:
|
||||
return -1
|
||||
elif self.return_number_instead_of_direct_call:
|
||||
return self.options[keys.index(selected.upper())][0]
|
||||
else:
|
||||
self.options[keys.index(selected.upper())][3]() if self.options[keys.index(selected.upper())][3] else None
|
||||
if not self.loop:
|
||||
return
|
||||
|
||||
|
||||
class TUIOnlyPrint:
|
||||
def __init__(self, title, prompt, in_between=None):
|
||||
self.title = title
|
||||
self.prompt = prompt
|
||||
self.in_between = in_between or []
|
||||
|
||||
def start(self):
|
||||
utilities.cls()
|
||||
utilities.header(self.title)
|
||||
print()
|
||||
|
||||
for i in self.in_between:
|
||||
print(i)
|
||||
if self.in_between:
|
||||
print()
|
||||
|
||||
return input(self.prompt)
|
||||
@@ -5,21 +5,31 @@
|
||||
import requests
|
||||
import logging
|
||||
|
||||
from resources import network_handler
|
||||
from resources import network_handler, constants
|
||||
|
||||
REPO_LATEST_RELEASE_URL: str = "https://api.github.com/repos/dortania/OpenCore-Legacy-Patcher/releases/latest"
|
||||
|
||||
|
||||
class check_binary_updates:
|
||||
def __init__(self, constants):
|
||||
self.constants = constants
|
||||
self.binary_version = self.constants.patcher_version
|
||||
self.binary_version_array = self.binary_version.split(".")
|
||||
self.binary_version_array = [int(x) for x in self.binary_version_array]
|
||||
self.binary_url = "https://api.github.com/repos/dortania/OpenCore-Legacy-Patcher/releases/latest"
|
||||
class CheckBinaryUpdates:
|
||||
def __init__(self, global_constants: constants.Constants):
|
||||
self.constants: constants.Constants = global_constants
|
||||
|
||||
self.available_binaries = {}
|
||||
self.binary_version = self.constants.patcher_version
|
||||
self.binary_version_array = [int(x) for x in self.binary_version.split(".")]
|
||||
|
||||
|
||||
def check_if_build_newer(self, remote_version=None, local_version=None):
|
||||
def _check_if_build_newer(self, remote_version: list = None, local_version: list = None):
|
||||
"""
|
||||
Check if the remote version is newer than the local version
|
||||
|
||||
Parameters:
|
||||
remote_version (list): Remote version to compare against
|
||||
local_version (list): Local version to compare against
|
||||
|
||||
Returns:
|
||||
bool: True if remote version is newer, False if not
|
||||
"""
|
||||
|
||||
if remote_version is None:
|
||||
remote_version = self.remote_version_array
|
||||
if local_version is None:
|
||||
@@ -39,13 +49,32 @@ class check_binary_updates:
|
||||
|
||||
return False
|
||||
|
||||
def determine_local_build_type(self):
|
||||
|
||||
def _determine_local_build_type(self):
|
||||
"""
|
||||
Check if the local build is a GUI or TUI build
|
||||
|
||||
Returns:
|
||||
str: "GUI" or "TUI"
|
||||
"""
|
||||
|
||||
if self.constants.wxpython_variant is True:
|
||||
return "GUI"
|
||||
else:
|
||||
return "TUI"
|
||||
|
||||
def determine_remote_type(self, remote_name):
|
||||
|
||||
def _determine_remote_type(self, remote_name: str):
|
||||
"""
|
||||
Check if the remote build is a GUI or TUI build
|
||||
|
||||
Parameters:
|
||||
remote_name (str): Name of the remote build
|
||||
|
||||
Returns:
|
||||
str: "GUI" or "TUI"
|
||||
"""
|
||||
|
||||
if "TUI" in remote_name:
|
||||
return "TUI"
|
||||
elif "GUI" in remote_name:
|
||||
@@ -53,45 +82,43 @@ class check_binary_updates:
|
||||
else:
|
||||
return "Unknown"
|
||||
|
||||
|
||||
def check_binary_updates(self):
|
||||
# logging.info("- Checking for updates...")
|
||||
if network_handler.NetworkUtilities(self.binary_url).verify_network_connection():
|
||||
# logging.info("- Network connection functional")
|
||||
response = requests.get(self.binary_url)
|
||||
data_set = response.json()
|
||||
# logging.info("- Retrieved latest version data")
|
||||
self.remote_version = data_set["tag_name"]
|
||||
# logging.info(f"- Latest version: {self.remote_version}")
|
||||
self.remote_version_array = self.remote_version.split(".")
|
||||
self.remote_version_array = [
|
||||
int(x) for x in self.remote_version_array
|
||||
]
|
||||
if self.check_if_build_newer() is True:
|
||||
# logging.info("- Remote version is newer")
|
||||
for asset in data_set["assets"]:
|
||||
logging.info(f"- Found asset: {asset['name']}")
|
||||
if self.determine_remote_type(asset["name"]) == self.determine_local_build_type():
|
||||
# logging.info(f"- Found matching asset: {asset['name']}")
|
||||
self.available_binaries.update({
|
||||
asset['name']: {
|
||||
"Name":
|
||||
asset["name"],
|
||||
"Version":
|
||||
self.remote_version,
|
||||
"Link":
|
||||
asset["browser_download_url"],
|
||||
"Type":
|
||||
self.determine_remote_type(asset["name"]),
|
||||
"Github Link":
|
||||
f"https://github.com/dortania/OpenCore-Legacy-Patcher/releases/{self.remote_version}"
|
||||
}
|
||||
})
|
||||
break
|
||||
if self.available_binaries:
|
||||
return self.available_binaries
|
||||
else:
|
||||
# logging.info("- No matching binaries available")
|
||||
return None
|
||||
# else:
|
||||
# logging.info("- Failed to connect to GitHub API")
|
||||
"""
|
||||
Check if any updates are available for the OpenCore Legacy Patcher binary
|
||||
|
||||
Returns:
|
||||
dict: Dictionary with Link and Version of the latest binary update if available
|
||||
"""
|
||||
|
||||
available_binaries: list = {}
|
||||
|
||||
if not network_handler.NetworkUtilities(REPO_LATEST_RELEASE_URL).verify_network_connection():
|
||||
return None
|
||||
|
||||
response = requests.get(REPO_LATEST_RELEASE_URL)
|
||||
data_set = response.json()
|
||||
|
||||
self.remote_version = data_set["tag_name"]
|
||||
|
||||
self.remote_version_array = self.remote_version.split(".")
|
||||
self.remote_version_array = [int(x) for x in self.remote_version_array]
|
||||
|
||||
if self._check_if_build_newer() is False:
|
||||
return None
|
||||
|
||||
for asset in data_set["assets"]:
|
||||
logging.info(f"- Found asset: {asset['name']}")
|
||||
if self._determine_remote_type(asset["name"]) == self._determine_local_build_type():
|
||||
available_binaries.update({
|
||||
asset['name']: {
|
||||
"Name": asset["name"],
|
||||
"Version": self.remote_version,
|
||||
"Link": asset["browser_download_url"],
|
||||
"Type": self._determine_remote_type(asset["name"]),
|
||||
"Github Link": f"https://github.com/dortania/OpenCore-Legacy-Patcher/releases/{self.remote_version}"
|
||||
}
|
||||
})
|
||||
return available_binaries
|
||||
|
||||
return None
|
||||
@@ -9,7 +9,6 @@ import os
|
||||
import binascii
|
||||
import argparse
|
||||
import atexit
|
||||
import requests
|
||||
import shutil
|
||||
import py_sip_xnu
|
||||
|
||||
|
||||
@@ -1,51 +1,70 @@
|
||||
import logging
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
from resources.sys_patch import sys_patch_helpers
|
||||
from resources.build import build
|
||||
from resources import constants
|
||||
from data import example_data, model_array, sys_patch_dict, os_data
|
||||
from pathlib import Path
|
||||
import logging
|
||||
|
||||
|
||||
def validate(settings):
|
||||
# Runs through ocvalidate to check for errors
|
||||
class PatcherValidation:
|
||||
"""
|
||||
Validation class for the patcher
|
||||
|
||||
valid_dumps = [
|
||||
example_data.MacBookPro.MacBookPro92_Stock,
|
||||
example_data.MacBookPro.MacBookPro111_Stock,
|
||||
example_data.MacBookPro.MacBookPro133_Stock,
|
||||
# example_data.MacBookPro.MacBookPro171_Stock,
|
||||
example_data.Macmini.Macmini52_Stock,
|
||||
example_data.Macmini.Macmini61_Stock,
|
||||
example_data.Macmini.Macmini71_Stock,
|
||||
# example_data.Macmini.Macmini91_Stock,
|
||||
example_data.iMac.iMac81_Stock,
|
||||
example_data.iMac.iMac112_Stock,
|
||||
example_data.iMac.iMac122_Upgraded,
|
||||
example_data.iMac.iMac122_Upgraded_Nvidia,
|
||||
example_data.iMac.iMac151_Stock,
|
||||
example_data.MacPro.MacPro31_Stock,
|
||||
example_data.MacPro.MacPro31_Upgrade,
|
||||
example_data.MacPro.MacPro31_Modern_AMD,
|
||||
example_data.MacPro.MacPro31_Modern_Kepler,
|
||||
example_data.MacPro.MacPro41_Upgrade,
|
||||
example_data.MacPro.MacPro41_Modern_AMD,
|
||||
example_data.MacPro.MacPro41_51__Flashed_Modern_AMD,
|
||||
example_data.MacPro.MacPro41_51_Flashed_NVIDIA_WEB_DRIVERS,
|
||||
]
|
||||
Primarily for Continuous Integration
|
||||
"""
|
||||
|
||||
valid_dumps_native = [
|
||||
example_data.iMac.iMac201_Stock,
|
||||
example_data.MacBookPro.MacBookPro141_SSD_Upgrade,
|
||||
]
|
||||
def __init__(self, global_constants: constants.Constants):
|
||||
self.constants: constants.Constants = global_constants
|
||||
|
||||
settings.validate = True
|
||||
self.constants.validate = True
|
||||
|
||||
self.valid_dumps = [
|
||||
example_data.MacBookPro.MacBookPro92_Stock,
|
||||
example_data.MacBookPro.MacBookPro111_Stock,
|
||||
example_data.MacBookPro.MacBookPro133_Stock,
|
||||
|
||||
example_data.Macmini.Macmini52_Stock,
|
||||
example_data.Macmini.Macmini61_Stock,
|
||||
example_data.Macmini.Macmini71_Stock,
|
||||
|
||||
example_data.iMac.iMac81_Stock,
|
||||
example_data.iMac.iMac112_Stock,
|
||||
example_data.iMac.iMac122_Upgraded,
|
||||
example_data.iMac.iMac122_Upgraded_Nvidia,
|
||||
example_data.iMac.iMac151_Stock,
|
||||
|
||||
example_data.MacPro.MacPro31_Stock,
|
||||
example_data.MacPro.MacPro31_Upgrade,
|
||||
example_data.MacPro.MacPro31_Modern_AMD,
|
||||
example_data.MacPro.MacPro31_Modern_Kepler,
|
||||
example_data.MacPro.MacPro41_Upgrade,
|
||||
example_data.MacPro.MacPro41_Modern_AMD,
|
||||
example_data.MacPro.MacPro41_51__Flashed_Modern_AMD,
|
||||
example_data.MacPro.MacPro41_51_Flashed_NVIDIA_WEB_DRIVERS,
|
||||
]
|
||||
|
||||
self.valid_dumps_native = [
|
||||
example_data.iMac.iMac201_Stock,
|
||||
example_data.MacBookPro.MacBookPro141_SSD_Upgrade,
|
||||
]
|
||||
|
||||
self._validate_configs()
|
||||
self._validate_sys_patch()
|
||||
|
||||
|
||||
def _build_prebuilt(self):
|
||||
"""
|
||||
Generate a build for each predefined model
|
||||
Then validate against ocvalidate
|
||||
"""
|
||||
|
||||
def build_prebuilt():
|
||||
for model in model_array.SupportedSMBIOS:
|
||||
logging.info(f"Validating predefined model: {model}")
|
||||
settings.custom_model = model
|
||||
build.build_opencore(settings.custom_model, settings).build_opencore()
|
||||
result = subprocess.run([settings.ocvalidate_path, f"{settings.opencore_release_folder}/EFI/OC/config.plist"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
self.constants.custom_model = model
|
||||
build.build_opencore(self.constants.custom_model, self.constants).build_opencore()
|
||||
result = subprocess.run([self.constants.ocvalidate_path, f"{self.constants.opencore_release_folder}/EFI/OC/config.plist"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
if result.returncode != 0:
|
||||
logging.info("Error on build!")
|
||||
logging.info(result.stdout.decode())
|
||||
@@ -53,24 +72,35 @@ def validate(settings):
|
||||
else:
|
||||
logging.info(f"Validation succeeded for predefined model: {model}")
|
||||
|
||||
def build_dumps():
|
||||
for model in valid_dumps:
|
||||
settings.computer = model
|
||||
settings.custom_model = ""
|
||||
logging.info(f"Validating dumped model: {settings.computer.real_model}")
|
||||
build.build_opencore(settings.computer.real_model, settings).build_opencore()
|
||||
result = subprocess.run([settings.ocvalidate_path, f"{settings.opencore_release_folder}/EFI/OC/config.plist"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
|
||||
def _build_dumps(self):
|
||||
"""
|
||||
Generate a build for each predefined model
|
||||
Then validate against ocvalidate
|
||||
"""
|
||||
|
||||
for model in self.valid_dumps:
|
||||
self.constants.computer = model
|
||||
self.constants.custom_model = ""
|
||||
logging.info(f"Validating dumped model: {self.constants.computer.real_model}")
|
||||
build.build_opencore(self.constants.computer.real_model, self.constants).build_opencore()
|
||||
result = subprocess.run([self.constants.ocvalidate_path, f"{self.constants.opencore_release_folder}/EFI/OC/config.plist"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
if result.returncode != 0:
|
||||
logging.info("Error on build!")
|
||||
logging.info(result.stdout.decode())
|
||||
raise Exception(f"Validation failed for predefined model: {settings.computer.real_model}")
|
||||
raise Exception(f"Validation failed for predefined model: {self.constants.computer.real_model}")
|
||||
else:
|
||||
logging.info(f"Validation succeeded for predefined model: {settings.computer.real_model}")
|
||||
logging.info(f"Validation succeeded for predefined model: {self.constants.computer.real_model}")
|
||||
|
||||
|
||||
def validate_root_patch_files(major_kernel, minor_kernel):
|
||||
patchset = sys_patch_dict.SystemPatchDictionary(major_kernel, minor_kernel, settings.legacy_accel_support)
|
||||
def _validate_root_patch_files(self, major_kernel, minor_kernel):
|
||||
"""
|
||||
Validate that all files in the patchset are present in the payload
|
||||
"""
|
||||
|
||||
patchset = sys_patch_dict.SystemPatchDictionary(major_kernel, minor_kernel, self.constants.legacy_accel_support)
|
||||
host_os_float = float(f"{major_kernel}.{minor_kernel}")
|
||||
|
||||
for patch_subject in patchset:
|
||||
for patch_core in patchset[patch_subject]:
|
||||
patch_os_min_float = float(f'{patchset[patch_subject][patch_core]["OS Support"]["Minimum OS Support"]["OS Major"]}.{patchset[patch_subject][patch_core]["OS Support"]["Minimum OS Support"]["OS Minor"]}')
|
||||
@@ -81,58 +111,83 @@ def validate(settings):
|
||||
if install_type in patchset[patch_subject][patch_core]:
|
||||
for install_directory in patchset[patch_subject][patch_core][install_type]:
|
||||
for install_file in patchset[patch_subject][patch_core][install_type][install_directory]:
|
||||
source_file = str(settings.payload_local_binaries_root_path) + "/" + patchset[patch_subject][patch_core][install_type][install_directory][install_file] + install_directory + "/" + install_file
|
||||
source_file = str(self.constants.payload_local_binaries_root_path) + "/" + patchset[patch_subject][patch_core][install_type][install_directory][install_file] + install_directory + "/" + install_file
|
||||
if not Path(source_file).exists():
|
||||
logging.info(f"File not found: {source_file}")
|
||||
raise Exception(f"Failed to find {source_file}")
|
||||
|
||||
logging.info(f"- Validating against Darwin {major_kernel}.{minor_kernel}")
|
||||
if not sys_patch_helpers.sys_patch_helpers(settings).generate_patchset_plist(patchset, f"OpenCore-Legacy-Patcher-{major_kernel}.{minor_kernel}.plist", None):
|
||||
if not sys_patch_helpers.SysPatchHelpers(self.constants).generate_patchset_plist(patchset, f"OpenCore-Legacy-Patcher-{major_kernel}.{minor_kernel}.plist", None):
|
||||
raise Exception("Failed to generate patchset plist")
|
||||
|
||||
# Remove the plist file after validation
|
||||
Path(settings.payload_path / f"OpenCore-Legacy-Patcher-{major_kernel}.{minor_kernel}.plist").unlink()
|
||||
Path(self.constants.payload_path / f"OpenCore-Legacy-Patcher-{major_kernel}.{minor_kernel}.plist").unlink()
|
||||
|
||||
|
||||
def validate_sys_patch():
|
||||
if Path(settings.payload_local_binaries_root_path_zip).exists():
|
||||
def _validate_sys_patch(self):
|
||||
"""
|
||||
Validates sys_patch modules
|
||||
"""
|
||||
|
||||
if Path(self.constants.payload_local_binaries_root_path_zip).exists():
|
||||
logging.info("Validating Root Patch File integrity")
|
||||
if not Path(settings.payload_local_binaries_root_path).exists():
|
||||
subprocess.run(["ditto", "-V", "-x", "-k", "--sequesterRsrc", "--rsrc", settings.payload_local_binaries_root_path_zip, settings.payload_path], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
if not Path(self.constants.payload_local_binaries_root_path).exists():
|
||||
subprocess.run(
|
||||
[
|
||||
"ditto", "-V", "-x", "-k", "--sequesterRsrc", "--rsrc",
|
||||
self.constants.payload_local_binaries_root_path_zip,
|
||||
self.constants.payload_path
|
||||
],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT
|
||||
)
|
||||
for supported_os in [os_data.os_data.big_sur, os_data.os_data.monterey, os_data.os_data.ventura]:
|
||||
for i in range(0, 10):
|
||||
validate_root_patch_files(supported_os, i)
|
||||
self._validate_root_patch_files(supported_os, i)
|
||||
logging.info("Validating SNB Board ID patcher")
|
||||
settings.computer.reported_board_id = "Mac-7BA5B2DFE22DDD8C"
|
||||
sys_patch_helpers.sys_patch_helpers(settings).snb_board_id_patch(settings.payload_local_binaries_root_path)
|
||||
self.constants.computer.reported_board_id = "Mac-7BA5B2DFE22DDD8C"
|
||||
sys_patch_helpers.SysPatchHelpers(self.constants).snb_board_id_patch(self.constants.payload_local_binaries_root_path)
|
||||
|
||||
# Clean up
|
||||
subprocess.run(
|
||||
[
|
||||
"rm", "-rf", self.constants.payload_local_binaries_root_path
|
||||
],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT
|
||||
)
|
||||
else:
|
||||
logging.info("- Skipping Root Patch File integrity validation")
|
||||
|
||||
|
||||
def validate_configs():
|
||||
def _validate_configs(self):
|
||||
"""
|
||||
Validates build modules
|
||||
"""
|
||||
|
||||
# First run is with default settings
|
||||
build_prebuilt()
|
||||
build_dumps()
|
||||
self._build_prebuilt()
|
||||
self._build_dumps()
|
||||
|
||||
# Second run, flip all settings
|
||||
settings.verbose_debug = True
|
||||
settings.opencore_debug = True
|
||||
settings.opencore_build = "DEBUG"
|
||||
settings.kext_debug = True
|
||||
settings.kext_variant = "DEBUG"
|
||||
settings.kext_debug = True
|
||||
settings.showpicker = False
|
||||
settings.sip_status = False
|
||||
settings.secure_status = True
|
||||
settings.firewire_boot = True
|
||||
settings.nvme_boot = True
|
||||
settings.enable_wake_on_wlan = True
|
||||
settings.disable_tb = True
|
||||
settings.force_surplus = True
|
||||
settings.software_demux = True
|
||||
settings.serial_settings = "Minimal"
|
||||
build_prebuilt()
|
||||
build_dumps()
|
||||
self.constants.verbose_debug = True
|
||||
self.constants.opencore_debug = True
|
||||
self.constants.opencore_build = "DEBUG"
|
||||
self.constants.kext_debug = True
|
||||
self.constants.kext_variant = "DEBUG"
|
||||
self.constants.kext_debug = True
|
||||
self.constants.showpicker = False
|
||||
self.constants.sip_status = False
|
||||
self.constants.secure_status = True
|
||||
self.constants.firewire_boot = True
|
||||
self.constants.nvme_boot = True
|
||||
self.constants.enable_wake_on_wlan = True
|
||||
self.constants.disable_tb = True
|
||||
self.constants.force_surplus = True
|
||||
self.constants.software_demux = True
|
||||
self.constants.serial_settings = "Minimal"
|
||||
|
||||
self._build_prebuilt()
|
||||
self._build_dumps()
|
||||
|
||||
validate_configs()
|
||||
validate_sys_patch()
|
||||
subprocess.run(["rm", "-rf", self.constants.build_path], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
Reference in New Issue
Block a user