Compare commits
255 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cada66a6b5 | ||
|
|
1ef98e0a4b | ||
|
|
4b3f7b837e | ||
|
|
17443b4fbf | ||
|
|
557f0a1bd9 | ||
|
|
77bb25b3a5 | ||
|
|
6a48038696 | ||
|
|
e6251da97a | ||
|
|
7239730c44 | ||
|
|
980e102675 | ||
|
|
dd66bca395 | ||
|
|
831495923a | ||
|
|
c9882f84bc | ||
|
|
4110f7a553 | ||
|
|
e99de2360a | ||
|
|
41ce771c33 | ||
|
|
c0f951411a | ||
|
|
47be068aaf | ||
|
|
49f6a62926 | ||
|
|
1cf0e3d363 | ||
|
|
739c488698 | ||
|
|
a6de37adf0 | ||
|
|
7bb00fd695 | ||
|
|
26fdb2ca2c | ||
|
|
af77e7979e | ||
|
|
3c5f5f84d2 | ||
|
|
6ab297423e | ||
|
|
2435ea747c | ||
|
|
d0eac893fe | ||
|
|
c62fefb575 | ||
|
|
87234740bd | ||
|
|
47a1c6a2e5 | ||
|
|
cee8d8e768 | ||
|
|
e133beee36 | ||
|
|
0388eaa6f2 | ||
|
|
984ba65d41 | ||
|
|
a6879ede75 | ||
|
|
bfcabb3a4a | ||
|
|
efdb278691 | ||
|
|
dd1b60c8be | ||
|
|
80e836e3bb | ||
|
|
597a4a6e14 | ||
|
|
ca85d0d4ca | ||
|
|
abea113272 | ||
|
|
48cf833cb8 | ||
|
|
ea3ff5d3b6 | ||
|
|
01d666e2c3 | ||
|
|
d8904bb3c0 | ||
|
|
d725798c60 | ||
|
|
bad9dbaecc | ||
|
|
79f6754c4f | ||
|
|
12a2d5ade1 | ||
|
|
051196993f | ||
|
|
969cc65ae5 | ||
|
|
4a7b6437a0 | ||
|
|
a2cdc0339a | ||
|
|
7d0bbf62bf | ||
|
|
c8b6eec14f | ||
|
|
0427c9ef44 | ||
|
|
663bec68bf | ||
|
|
cf28e8d2b5 | ||
|
|
b1d0d733d9 | ||
|
|
4f0b605786 | ||
|
|
c031917a12 | ||
|
|
d8a79cf67e | ||
|
|
18b6ce8684 | ||
|
|
d875cdf6a5 | ||
|
|
80ea0cd217 | ||
|
|
77da01dfb4 | ||
|
|
a5319bf432 | ||
|
|
3c5d93f79e | ||
|
|
2ca2bc451a | ||
|
|
ab9208da58 | ||
|
|
3132b0dcb7 | ||
|
|
b22c28e07b | ||
|
|
aa81b0ba49 | ||
|
|
ba5dd16201 | ||
|
|
5f30adab73 | ||
|
|
4c9c7965b6 | ||
|
|
1e86681d3c | ||
|
|
79cf5cb86f | ||
|
|
e6a3536e31 | ||
|
|
af733dc08c | ||
|
|
1d564ed653 | ||
|
|
f20c9e3a09 | ||
|
|
422eee04b7 | ||
|
|
1f23ceef7f | ||
|
|
5c402e1820 | ||
|
|
a92cbe94a1 | ||
|
|
89ef9fb904 | ||
|
|
bfe8776a1a | ||
|
|
52e31d0b1d | ||
|
|
5a7308fa2a | ||
|
|
b0617887dd | ||
|
|
5b4464a1f2 | ||
|
|
d4f004c558 | ||
|
|
df5e7525c2 | ||
|
|
b56ec7c679 | ||
|
|
cd9ce32c04 | ||
|
|
a65ceaa376 | ||
|
|
2788dfc78e | ||
|
|
888387dfa2 | ||
|
|
9c41fff3d4 | ||
|
|
3acc4dc9d9 | ||
|
|
99f6d8f060 | ||
|
|
efdcbd24c1 | ||
|
|
fa322d73c5 | ||
|
|
cb6f294d35 | ||
|
|
4ac46d4b54 | ||
|
|
48dd47aed3 | ||
|
|
58b3748034 | ||
|
|
fefd651157 | ||
|
|
2b1d598bf3 | ||
|
|
a863e55a01 | ||
|
|
b297692b77 | ||
|
|
a17b43525a | ||
|
|
fd9cd85254 | ||
|
|
aa196b6c0d | ||
|
|
cb38ee9477 | ||
|
|
6faff5d52a | ||
|
|
cabcdcd381 | ||
|
|
e36e9b35e9 | ||
|
|
566df783c8 | ||
|
|
edbd96a7e9 | ||
|
|
72f7d20ce5 | ||
|
|
39147e95be | ||
|
|
7d1d3dd9f6 | ||
|
|
6146aa48c6 | ||
|
|
266261484f | ||
|
|
231e545ca4 | ||
|
|
f18184bded | ||
|
|
7171de0679 | ||
|
|
d360a8ee8b | ||
|
|
5505737b37 | ||
|
|
11bc64f8e6 | ||
|
|
300726ea96 | ||
|
|
186669b9cd | ||
|
|
b4afa8bc28 | ||
|
|
04441590f0 | ||
|
|
f5d9ecfc25 | ||
|
|
f6f4131b53 | ||
|
|
47458daae2 | ||
|
|
0507654d09 | ||
|
|
0ed1cbde2f | ||
|
|
c80c46288c | ||
|
|
0cd1509a38 | ||
|
|
06b8b08d4b | ||
|
|
6fc895a45b | ||
|
|
98bbddc03d | ||
|
|
46d2a86e2f | ||
|
|
5a6407ab14 | ||
|
|
8b38939759 | ||
|
|
4b6587dc3b | ||
|
|
8adbc3b5d2 | ||
|
|
62e565b8c0 | ||
|
|
749a0be86e | ||
|
|
a1de379c5f | ||
|
|
bf00c013c8 | ||
|
|
8c848f9317 | ||
|
|
fa711c18cc | ||
|
|
2d5ea95ee0 | ||
|
|
0cc8b7b705 | ||
|
|
fecd71ef46 | ||
|
|
451262d50b | ||
|
|
88f895b61e | ||
|
|
28f10824fd | ||
|
|
2f6666edfb | ||
|
|
831fbc3319 | ||
|
|
7417fc4180 | ||
|
|
96fcba8391 | ||
|
|
d576752744 | ||
|
|
034a30a283 | ||
|
|
adcacc478d | ||
|
|
e853be2e84 | ||
|
|
c414c9cde7 | ||
|
|
87f9f39179 | ||
|
|
0a18db3142 | ||
|
|
63ba8d4a16 | ||
|
|
017d59f57d | ||
|
|
0f55b074af | ||
|
|
652e8659e4 | ||
|
|
f7d5b9345f | ||
|
|
54480c3776 | ||
|
|
36c39a7e71 | ||
|
|
a837a27dc0 | ||
|
|
8dcec0dfa8 | ||
|
|
85b90b13e4 | ||
|
|
79c173aaff | ||
|
|
f69d393acd | ||
|
|
bebb71b1de | ||
|
|
2f12236ac8 | ||
|
|
a7bfef5ed7 | ||
|
|
4ad65f00b5 | ||
|
|
9993436029 | ||
|
|
1f156170cf | ||
|
|
81247d8cf4 | ||
|
|
5929e81337 | ||
|
|
d8267838ae | ||
|
|
8f450b525b | ||
|
|
681441df7e | ||
|
|
e02c26e703 | ||
|
|
1b9e45b5f9 | ||
|
|
6a0090987c | ||
|
|
02d3f6cc64 | ||
|
|
2e964ba9c2 | ||
|
|
a2c0994bde | ||
|
|
d0e257a364 | ||
|
|
8c5165a06c | ||
|
|
422283a3ac | ||
|
|
24225a3748 | ||
|
|
0b59384130 | ||
|
|
d04822b770 | ||
|
|
84648c67cf | ||
|
|
42112fcca9 | ||
|
|
2bbb7153e7 | ||
|
|
dc0c75964d | ||
|
|
c7c6569c4b | ||
|
|
28d3e981c5 | ||
|
|
3bd9d85ae2 | ||
|
|
b43c66c0bb | ||
|
|
2a002d8e82 | ||
|
|
324d7362c7 | ||
|
|
3dc4b60af1 | ||
|
|
33a885a309 | ||
|
|
4f1cb8abcc | ||
|
|
21e7a75cc9 | ||
|
|
3ef6e4a853 | ||
|
|
bd70c4a24a | ||
|
|
aaf7519e94 | ||
|
|
28cdc3f61b | ||
|
|
4068bc1661 | ||
|
|
1204daa330 | ||
|
|
f8b2b5a759 | ||
|
|
f3e2dfc4de | ||
|
|
a6e0c142ca | ||
|
|
d9e9fea2f7 | ||
|
|
978a16f397 | ||
|
|
d21b984918 | ||
|
|
c308bcb993 | ||
|
|
15103007a5 | ||
|
|
a932b5a483 | ||
|
|
9b96514b91 | ||
|
|
a33142e29e | ||
|
|
ef9df5265b | ||
|
|
bc8a2727c3 | ||
|
|
17a1b823fc | ||
|
|
993f0c22d5 | ||
|
|
bd42aad340 | ||
|
|
fcd3afe29d | ||
|
|
ed62fe91a2 | ||
|
|
aae6cc705f | ||
|
|
eeb2e6cb1a | ||
|
|
6ac18b251c | ||
|
|
11a9ab7b96 | ||
|
|
b87737f55e |
4
.github/workflows/build-app-wxpython.yml
vendored
@@ -30,9 +30,9 @@ jobs:
|
||||
- name: Build Binary
|
||||
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 }}" --key "${{ env.ANALYTICS_KEY }}" --site "${{ env.ANALYTICS_SITE }}"
|
||||
|
||||
# Uncomment when using Github Runners or first run on self-hosted
|
||||
# - name: Import Certificate
|
||||
# uses: apple-actions/import-codesign-certs@v1
|
||||
# if: (!security find-certificate -c "${{ env.MAC_CODESIGN_IDENTITY }}")
|
||||
# uses: apple-actions/import-codesign-certs@v2
|
||||
# with:
|
||||
# p12-file-base64: ${{ secrets.MAC_CODESIGN_CERT }}
|
||||
# p12-password: ${{ secrets.MAC_NOTARIZATION_PASSWORD }}
|
||||
|
||||
23
.github/workflows/validate-external.yml
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
name: CI - Validation (External)
|
||||
|
||||
on:
|
||||
push:
|
||||
workflow_dispatch:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Validate
|
||||
runs-on: macos-latest
|
||||
if: github.repository_owner != 'dortania'
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: '3.10'
|
||||
- name: Install Dependencies
|
||||
run: python3 -m pip install -r requirements.txt
|
||||
- name: Validate
|
||||
run: python3 OpenCore-Patcher-GUI.command --validate
|
||||
4
.github/workflows/validate.yml
vendored
@@ -11,10 +11,6 @@ jobs:
|
||||
name: Validate
|
||||
runs-on: x86_64_monterey
|
||||
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
|
||||
- name: Validate
|
||||
|
||||
4
.gitignore
vendored
@@ -33,3 +33,7 @@ __pycache__/
|
||||
/payloads/OpenCore-Legacy-Patcher-*.plist
|
||||
/payloads/KDK.dmg
|
||||
*.log
|
||||
/Universal-Binaries.dmg
|
||||
/payloads/KDKInfo.plist
|
||||
/payloads/update.sh
|
||||
/payloads/OpenCore-Patcher.app
|
||||
@@ -30,7 +30,7 @@ class CreateBinary:
|
||||
|
||||
def __init__(self):
|
||||
start = time.time()
|
||||
print("- Starting build script")
|
||||
print("Starting build script")
|
||||
|
||||
self.args = self._parse_arguments()
|
||||
|
||||
@@ -39,7 +39,7 @@ class CreateBinary:
|
||||
self._preflight_processes()
|
||||
self._build_binary()
|
||||
self._postflight_processes()
|
||||
print(f"- Build script completed in {str(round(time.time() - start, 2))} seconds")
|
||||
print(f"Build script completed in {str(round(time.time() - start, 2))} seconds")
|
||||
|
||||
|
||||
def _set_cwd(self):
|
||||
@@ -48,7 +48,7 @@ class CreateBinary:
|
||||
"""
|
||||
|
||||
os.chdir(Path(__file__).resolve().parent)
|
||||
print(f"- Current Working Directory: \n\t{os.getcwd()}")
|
||||
print(f"Current Working Directory:\n- {os.getcwd()}")
|
||||
|
||||
|
||||
def _parse_arguments(self):
|
||||
@@ -99,7 +99,7 @@ class CreateBinary:
|
||||
Start preflight processes
|
||||
"""
|
||||
|
||||
print("- Starting preflight processes")
|
||||
print("Starting preflight processes")
|
||||
self._setup_pathing()
|
||||
self._delete_extra_binaries()
|
||||
self._download_resources()
|
||||
@@ -111,7 +111,7 @@ class CreateBinary:
|
||||
Start postflight processes
|
||||
"""
|
||||
|
||||
print("- Starting postflight processes")
|
||||
print("Starting postflight processes")
|
||||
self._patch_load_command()
|
||||
self._add_commit_data()
|
||||
self._post_flight_cleanup()
|
||||
@@ -124,19 +124,19 @@ class CreateBinary:
|
||||
"""
|
||||
|
||||
if Path(f"./dist/OpenCore-Patcher.app").exists():
|
||||
print("- Found OpenCore-Patcher.app, removing...")
|
||||
print("Found OpenCore-Patcher.app, removing...")
|
||||
rm_output = subprocess.run(
|
||||
["rm", "-rf", "./dist/OpenCore-Patcher.app"],
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
||||
)
|
||||
if rm_output.returncode != 0:
|
||||
print("- Remove failed")
|
||||
print("Remove failed")
|
||||
print(rm_output.stderr.decode('utf-8'))
|
||||
raise Exception("Remove failed")
|
||||
|
||||
self._embed_key()
|
||||
|
||||
print("- Building GUI binary...")
|
||||
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)
|
||||
@@ -144,10 +144,18 @@ class CreateBinary:
|
||||
self._strip_key()
|
||||
|
||||
if build_result.returncode != 0:
|
||||
print("- Build failed")
|
||||
print("Build failed")
|
||||
print(build_result.stderr.decode('utf-8'))
|
||||
raise Exception("Build failed")
|
||||
|
||||
# Next embed support icns into ./Resources
|
||||
print("Embedding icns...")
|
||||
for file in Path("payloads/Icon/AppIcons").glob("*.icns"):
|
||||
subprocess.run(
|
||||
["cp", str(file), "./dist/OpenCore-Patcher.app/Contents/Resources/"],
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -157,15 +165,15 @@ class CreateBinary:
|
||||
"""
|
||||
|
||||
if not self.args.key:
|
||||
print("- No developer key provided, skipping...")
|
||||
print("No developer key provided, skipping...")
|
||||
return
|
||||
if not self.args.site:
|
||||
print("- No site provided, skipping...")
|
||||
print("No site provided, skipping...")
|
||||
return
|
||||
|
||||
print("- Embedding developer key...")
|
||||
print("Embedding developer key...")
|
||||
if not Path("./resources/analytics_handler.py").exists():
|
||||
print("- analytics_handler.py not found")
|
||||
print("analytics_handler.py not found")
|
||||
return
|
||||
|
||||
lines = []
|
||||
@@ -188,15 +196,15 @@ class CreateBinary:
|
||||
"""
|
||||
|
||||
if not self.args.key:
|
||||
print("- No developer key provided, skipping...")
|
||||
print("No developer key provided, skipping...")
|
||||
return
|
||||
if not self.args.site:
|
||||
print("- No site provided, skipping...")
|
||||
print("No site provided, skipping...")
|
||||
return
|
||||
|
||||
print("- Stripping developer key...")
|
||||
print("Stripping developer key...")
|
||||
if not Path("./resources/analytics_handler.py").exists():
|
||||
print("- analytics_handler.py not found")
|
||||
print("analytics_handler.py not found")
|
||||
return
|
||||
|
||||
lines = []
|
||||
@@ -236,11 +244,10 @@ class CreateBinary:
|
||||
"launcher.sh",
|
||||
"OC-Patcher-TUI.icns",
|
||||
"OC-Patcher.icns",
|
||||
"Universal-Binaries.zip",
|
||||
]
|
||||
|
||||
|
||||
print("- Deleting extra binaries...")
|
||||
print("Deleting extra binaries...")
|
||||
for file in Path("payloads").glob(pattern="*"):
|
||||
if file.is_dir():
|
||||
if file.name in whitelist_folders:
|
||||
@@ -261,20 +268,20 @@ class CreateBinary:
|
||||
|
||||
patcher_support_pkg_version = constants.Constants().patcher_support_pkg_version
|
||||
required_resources = [
|
||||
"Universal-Binaries.zip"
|
||||
"Universal-Binaries.dmg"
|
||||
]
|
||||
|
||||
print("- Downloading required resources...")
|
||||
print("Downloading required resources...")
|
||||
for resource in required_resources:
|
||||
if Path(f"./payloads/{resource}").exists():
|
||||
if Path(f"./{resource}").exists():
|
||||
if self.args.reset_binaries:
|
||||
print(f"- Removing old {resource}")
|
||||
rm_output = subprocess.run(
|
||||
["rm", "-rf", f"./payloads/{resource}"],
|
||||
["rm", "-rf", f"./{resource}"],
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
||||
)
|
||||
if rm_output.returncode != 0:
|
||||
print("- Remove failed")
|
||||
print("Remove failed")
|
||||
print(rm_output.stderr.decode('utf-8'))
|
||||
raise Exception("Remove failed")
|
||||
else:
|
||||
@@ -298,13 +305,6 @@ class CreateBinary:
|
||||
print(f"- {resource} not found")
|
||||
raise Exception(f"{resource} not found")
|
||||
|
||||
print(" - Moving into payloads")
|
||||
mv_output = subprocess.run(["mv", resource, "./payloads/"], 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 _generate_payloads_dmg(self):
|
||||
"""
|
||||
@@ -324,16 +324,16 @@ class CreateBinary:
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
||||
)
|
||||
if rm_output.returncode != 0:
|
||||
print("- Remove failed")
|
||||
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',
|
||||
'-megabytes', '32000',
|
||||
'-megabytes', '32000', # Overlays can only be as large as the disk image allows
|
||||
'-format', 'UDZO', '-ov',
|
||||
'-volname', 'payloads',
|
||||
'-volname', 'OpenCore Patcher Resources (Base)',
|
||||
'-fs', 'HFS+',
|
||||
'-srcfolder', './payloads',
|
||||
'-passphrase', 'password', '-encryption'
|
||||
|
||||
96
CHANGELOG.md
@@ -1,5 +1,101 @@
|
||||
# OpenCore Legacy Patcher changelog
|
||||
|
||||
## 0.6.7
|
||||
- Resolve partition buttons overlapping in Install OpenCore UI
|
||||
- ex. "EFI" and additional FAT32 partitions on a single drive
|
||||
- Re-enable mediaanalysisd on Ventura
|
||||
- Allows for Live Text support on systems with3802 GPUs
|
||||
- ie. Intel Ivy Bridge and Haswell, Nvidia Kepler
|
||||
- Previously disabled due to high instability in Photos with Face Scanning, now resolved
|
||||
- Work-around crashing after patching with MenuBar2 implementation enabled
|
||||
- Setting must be re-enabled after patching
|
||||
- Update non-Metal Binaries:
|
||||
- Resolve window placement defaulting past top of screen for some apps
|
||||
- ex. OpenCore-Patcher.app during root patching
|
||||
- Resolve indeterminate progress bars not rendering with wxWidgets in Monterey and later
|
||||
- ex. OpenCore-Patcher.app
|
||||
- UI changes:
|
||||
- Add "Show Log File" button to menubar
|
||||
- Avoid listing unsupported installer to download by default
|
||||
- ex. macOS 14 InstallAssistant.pkg
|
||||
- Resolve crash when fetching remote macOS installers offline
|
||||
- Avoid displaying root patches on unsupported macOS versions
|
||||
- ex. macOS 14
|
||||
- Backend changes:
|
||||
- Call `setpgrp()` to prevent app from being killed if parent process is killed (ie. LaunchAgents)
|
||||
- Rework logging handler:
|
||||
- Implement formatted logging
|
||||
- Allowing easier debugging
|
||||
- Implement per-version, per-run file logging
|
||||
- ex. OpenCore-Patcher (0.6.7) (2021-12-31-12-34-56-666903).log
|
||||
- Keep only 10 latest log files
|
||||
- Reveal log file in Finder on main thread crash
|
||||
- Avoid writing username to log file
|
||||
- Resolve SharedSupport.dmg pathing error during macOS Installer Verification
|
||||
- Applicable to systems with 2 (or more) USB Installers with the same name plugged in
|
||||
- Resolve payloads path being mis-routed during CLI calls
|
||||
- Add UI when fetching root patches for host
|
||||
- Remove progress bar work-around for non-Metal in Monterey and later
|
||||
- Requires host to have been patched with PatcherSupportPkg 1.1.2 or newer
|
||||
- Increment Binaries:
|
||||
- PatcherSupportPkg 1.1.2 - release
|
||||
|
||||
## 0.6.6
|
||||
- Implement option to disable ColorSync downgrade on HD 3000 Macs
|
||||
- Allows for Display Profiles support on some units
|
||||
- Note: black box rendering issues will likely appear
|
||||
- Thanks [@jazzzny](https://github.com/Jazzzny)
|
||||
- Rename payloads.dmg volume name to "OpenCore Patcher Resources (Base)"
|
||||
- Allows for better identification when mounted (ex. Disk Utility while app is running)
|
||||
- Implement DMG-based PatcherSupportPkg system
|
||||
- Reduces both app size and root patching time
|
||||
- Resolve incorrect remote KDK matching for macOS betas
|
||||
- ex. Beta 4 KDK being recommended for Beta 3 install
|
||||
- Resolve low power mode on MacPro6,1
|
||||
- Credit to CaseyJ's [PCI Bus Enumeration Patch](https://github.com/AMD-OSX/AMD_Vanilla/pull/196)
|
||||
- Resolve PCI eject menu appearing on unsupported hardware
|
||||
- Resolve kernel panic on wake for AMD TeraScale 1 and Nvidia Tesla 8000 series GPUs
|
||||
- Resolve loss of Ethernet after wake on MacPro3,1 in Ventura
|
||||
- Resolve graphics corruption on wake for TeraScale 1
|
||||
- Patch currently limited to Ventura and newer
|
||||
- Restore Function Keys on MacBook5,2 and MacBook4,1
|
||||
- Implementation by [@jazzzny](https://github.com/Jazzzny)
|
||||
- Update non-Metal Binaries:
|
||||
- Resolves cryptexd and sshd crashes
|
||||
- Resolves screen recording regression
|
||||
- Resolves Photo Booth on macOS Monterey and later
|
||||
- May require tccplus for permissions
|
||||
- Resolve Application alias not being created with AutoPatcher
|
||||
- Backend changes:
|
||||
- Rename OCLP-Helper to OpenCore-Patcher
|
||||
- Allows for better identification when displaying prompts
|
||||
- Reimplement wxPython GUI into modularized system:
|
||||
- Allows for easier maintenance and future expansion
|
||||
- Changes include:
|
||||
- Reworked settings UI
|
||||
- Unified download UI with time remaining
|
||||
- Implement in-app update system
|
||||
- Guides users to update OpenCore and Root Patches once update's installed
|
||||
- Expand app update checks to include nightly users
|
||||
- ex. 0.6.6 nightly -> 0.6.6 release
|
||||
- Implement macOS installer verification after flashing
|
||||
- Implement proper UI call backs on long processes
|
||||
- ex. Root patching
|
||||
- Implement default selections for disks and installers
|
||||
- Set about and quit items
|
||||
- Utilize `py-applescript` for authorization prompts
|
||||
- Avoids displaying prompts with `osascript` in the title
|
||||
- Due to limitations, only used for installer creation and OpenCore installation
|
||||
- Resolve exception handler not logging to file
|
||||
- Display raised exceptions from main thread to users
|
||||
- Increment Binaries:
|
||||
- PatcherSupportPkg 1.1.0 - release
|
||||
- OpenCorePkg 0.9.2 - release
|
||||
- Lilu 1.6.6 - rolling (d8f3782)
|
||||
- RestrictEvents 1.1.1 - release
|
||||
- FeatureUnlock 1.1.4 - release
|
||||
- BlueToolFixup 2.6.6 - release
|
||||
|
||||
## 0.6.5
|
||||
- Update 3802 Patchset Binaries:
|
||||
- Resolves additional 3rd party app crashes on Metal with macOS 13.3+
|
||||
|
||||
14
LICENSE.txt
Normal file
@@ -0,0 +1,14 @@
|
||||
Copyright (c) 2020-2023, Dhinak G
|
||||
Copyright (c) 2020-2023, Mykola Grymalyuk
|
||||
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
||||
3. All advertising materials mentioning features or use of this software must display the following acknowledgement:
|
||||
This product includes software developed by the organization.
|
||||
4. Neither the name of the copyright holder nor the names the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY COPYRIGHT HOLDER "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL COPYRIGHT HOLDER BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
@@ -9,7 +9,7 @@ block_cipher = None
|
||||
a = Analysis(['OpenCore-Patcher-GUI.command'],
|
||||
pathex=[],
|
||||
binaries=[],
|
||||
datas=[('payloads.dmg', '.')],
|
||||
datas=[('payloads.dmg', '.'), ('Universal-Binaries.dmg', '.')],
|
||||
hiddenimports=[],
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
@@ -49,6 +49,7 @@ app = BUNDLE(coll,
|
||||
icon="payloads/OC-Patcher.icns",
|
||||
bundle_identifier="com.dortania.opencore-legacy-patcher",
|
||||
info_plist={
|
||||
"CFBundleName": "OpenCore Legacy Patcher",
|
||||
"CFBundleShortVersionString": constants.Constants().patcher_version,
|
||||
"NSHumanReadableCopyright": constants.Constants().copyright_date,
|
||||
"LSMinimumSystemVersion": "10.10.0",
|
||||
|
||||
@@ -91,6 +91,8 @@ To run the project from source, see here: [Build and run from source](./SOURCE.m
|
||||
* Aid with Nvidia Web Driver research and development
|
||||
* [joevt](https://github.com/joevt)
|
||||
* [FixPCIeLinkrate](https://github.com/joevt/joevtApps)
|
||||
* [Jazzzny](https://github.com/Jazzzny)
|
||||
* Research and various contributions to the project
|
||||
* Amazing users who've graciously donate hardware:
|
||||
* [JohnD](https://forums.macrumors.com/members/johnd.53633/) - 2013 Mac Pro
|
||||
* [SpiGAndromeda](https://github.com/SpiGAndromeda) - AMD Vega 64
|
||||
|
||||
14
SOURCE.md
@@ -2,13 +2,13 @@
|
||||
|
||||
OpenCore Legacy Patcher at its core is a Python-based GUI/CLI-based application. In turn, to run the project from source, you simply need to invoke the OpenCore-Patcher-GUI.command file via Python.
|
||||
|
||||
For developers wishing to validate mainline changes, you may use these nightly links:
|
||||
For developers wishing to validate mainline changes, you may use this link: [GUI (Graphical Based App)](https://nightly.link/dortania/OpenCore-Legacy-Patcher/workflows/build-app-wxpython/main/OpenCore-Patcher.app%20%28GUI%29.zip)
|
||||
|
||||
* [GUI (Graphical Based App)](https://nightly.link/dortania/OpenCore-Legacy-Patcher/workflows/build-app-wxpython/main/OpenCore-Patcher.app%20%28GUI%29.zip)
|
||||
* **Warning**: Nightly builds (untagged builds built from the latest commit) are actively developed OpenCore Legacy Patcher builds. These builds have not been tested, are not guaranteed to work, and are not guaranteed to be safe. Do not use nightlies without a good reason to do so, and do not use them on your main machine. Additionally, these binaries should not be used without first consulting the [CHANGELOG](./CHANGELOG.md).
|
||||
|
||||
**Warning**: These binaries should not be used without first consulting the [CHANGELOG](./CHANGELOG.md). Do not distribute these links in forums, please link to this document instead.
|
||||
|
||||
* Users running new builds of the project without understanding what has changed are at a higher risk of bricking their installation as they do not read any warnings provided in the CHANGELOG. We wish to minimize these situations as much as possible.
|
||||
**Do not share _any_ links to these binaries** in forums; please link to **this document only**.
|
||||
* Additionally, do not reupload these binaries or download binaries from other sites. Using binaries from untrusted sources is a security issue, as they may have been tampered with.
|
||||
* Users running new builds of the project without understanding what has changed and the implications of installing software under active development are at a higher risk of bricking their installation as they do not read any warnings provided in the CHANGELOG. We wish to minimize these situations as much as possible.
|
||||
|
||||
## Getting Started
|
||||
|
||||
@@ -57,10 +57,6 @@ Pass `-h` or `--help` for more information on supported CLI arguments.
|
||||
|
||||
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 Python 3 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 best compatibility, generate binaries on macOS Mojave. These binaries will be compatible with macOS 10.9 to macOS 12.
|
||||
* Currently our build system is a [Macmini8,1 provided by MacStadium](https://www.macstadium.com/opensource) running macOS Mojave (10.14.6).
|
||||
|
||||
```sh
|
||||
# Install PyInstaller
|
||||
pip3 install pyinstaller
|
||||
|
||||
@@ -103,6 +103,9 @@ class SystemPatchDictionary():
|
||||
"GPUSupport.framework": "10.14.3",
|
||||
"SkyLight.framework": f"10.14.6-{self.os_major}",
|
||||
},
|
||||
"/System/Applications": {
|
||||
**({ "Photo Booth.app": "11.7.6"} if self.os_major >= os_data.os_data.monterey else {}),
|
||||
},
|
||||
},
|
||||
"Remove": {
|
||||
"/System/Library/Extensions": [
|
||||
@@ -144,6 +147,8 @@ class SystemPatchDictionary():
|
||||
**({"defaults write /Library/Preferences/.GlobalPreferences.plist ShowDate -int 1": True } if self.os_float >= self.macOS_12_4 else {}),
|
||||
"defaults write /Library/Preferences/.GlobalPreferences.plist InternalDebugUseGPUProcessForCanvasRenderingEnabled -bool false": True,
|
||||
"defaults write /Library/Preferences/.GlobalPreferences.plist WebKitExperimentalUseGPUProcessForCanvasRenderingEnabled -bool false": True,
|
||||
# MenuBar2 breaks macOS if enabled before patching
|
||||
"defaults write /Library/Preferences/.GlobalPreferences.plist Amy.MenuBar2Beta -bool false": True,
|
||||
},
|
||||
},
|
||||
"Non-Metal IOAccelerator Common": {
|
||||
@@ -699,7 +704,7 @@ class SystemPatchDictionary():
|
||||
"AMD3800Controller.kext": "10.13.6",
|
||||
"AMD4600Controller.kext": "10.13.6",
|
||||
"AMD4800Controller.kext": "10.13.6",
|
||||
"ATIRadeonX2000.kext": "10.13.6",
|
||||
"ATIRadeonX2000.kext": "10.13.6" if self.os_major < os_data.os_data.ventura else "10.13.6 TS1",
|
||||
"ATIRadeonX2000GA.plugin": "10.13.6",
|
||||
"ATIRadeonX2000GLDriver.bundle": "10.13.6",
|
||||
"ATIRadeonX2000VADriver.bundle": "10.13.6",
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* [Keyboard Backlight broken](#keyboard-backlight-broken)
|
||||
* [Photos and Maps Apps Heavily Distorted](#photos-and-maps-apps-heavily-distorted)
|
||||
* [Cannot press "Done" when editing a Sidebar Widget](#cannot-press-done-when-editing-a-sidebar-widget)
|
||||
* [Wake from sleep heavily distorted on AMD/ATI in macOS 11.3 and newer](#wake-from-sleep-heavily-distorted-on-amd-ati-in-macos-11-3-and-newer)
|
||||
* [Wake from sleep heavily distorted on AMD/ATI from macOS 11.3 to Monterey](#wake-from-sleep-heavily-distorted-on-amd-ati-from-macos-11-3-to-monterey)
|
||||
* [Unable to switch GPUs on 2011 15" and 17" MacBook Pros](#unable-to-switch-gpus-on-2011-15-and-17-macbook-pros)
|
||||
* [Erratic Colours on ATI TeraScale 2 GPUs (HD5000/HD6000)](#erratic-colours-on-ati-terascale-2-gpus-hd5000-hd6000)
|
||||
* [Unable to allow Safari Extensions](#unable-to-allow-Safari-Extensions)
|
||||
@@ -104,9 +104,11 @@ Due to the Metal Backend, the enhanced color output of these apps seems to heavi
|
||||
|
||||
Workaround: Press some combination of Tab, or Tab and then Shift-Tab, or just Shift-Tab until the "Done" button is highlighted. Then press spacebar to activate the button, the same as in any other dialog with a highlighted button halo.
|
||||
|
||||
## Wake from sleep heavily distorted on AMD/ATI in macOS 11.3 and newer
|
||||
## Wake from sleep heavily distorted on AMD/ATI from macOS 11.3 to Monterey
|
||||
|
||||
Unfortunately, this is a very well known issue that the community is investigating. A currently known solution is to downgrade to macOS 11.2.3 or older until a proper fix can be found. Additionally, logging out and logging back in can resolve the issue without requiring a reboot.
|
||||
**Fixed for macOS Ventura starting from 0.6.6. Big Sur and Monterey will continue to exhibit the issue.**
|
||||
|
||||
For older versions, only known solution is to downgrade to macOS 11.2.3 or older. Additionally, logging out and logging back in can resolve the issue without requiring a reboot.
|
||||
|
||||
* Note, this issue should be exclusive to TeraScale 1 GPUs (ie. HD2000-4000). TeraScale 2 GPUs should not exhibit this issue.
|
||||
|
||||
|
||||
@@ -10,6 +10,8 @@ The easiest way to debug yourself is via Patcher Settings. Here there are many d
|
||||
* "Enable OpenCore DEBUG"
|
||||
* "Enable Kext DEBUG"
|
||||
|
||||

|
||||
|
||||
When you've enabled these 3 options, rebuild OpenCore and install to your drive. This will provide much greater debug information as well as write logs to the EFI Partition.
|
||||
|
||||
## Obtaining OpenCore logs from disk
|
||||
|
||||
@@ -20,4 +20,4 @@ This patcher is made of multiple external applications from different people and
|
||||
* [VMM Patch Set](https://github.com/dortania/OpenCore-Legacy-Patcher/blob/4a8f61a01da72b38a4b2250386cc4b497a31a839/payloads/Config/config.plist#L1222-L1281) - parrotgeek1
|
||||
* Apple Binaries - Apple Inc.
|
||||
|
||||
Remaining files within OpenCore Legacy Patcher are copyrighted 2020-2022 Mykola Grymalyuk & Dhinak G. For integration into other projects, please request written permission.
|
||||
Remaining files within OpenCore Legacy Patcher are distributed under BSD-4-Clause license.
|
||||
|
||||
@@ -298,7 +298,8 @@ Below is an explanation of what Kexts OpenCore Legacy Patcher will inject into m
|
||||
* Reason: Prevents AVXFSCompressionTypeZlib crash on pre AVX1.0 systems in 12.4+
|
||||
* SimpleMSR
|
||||
* Reason: Disables BD PROCHOT to prevent firmware throttling on Nehalem+ MacBooks
|
||||
|
||||
* LegacyKeyboardInjector
|
||||
* Reason: Fixes function keys on MacBook4,1/MacBook5,2
|
||||
|
||||
|
||||
:::
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Post-Installation
|
||||
|
||||
* [Booting without USB drive](#booting-without-usb-drive)
|
||||
* [Booting seamlessly without Verbose or OpenCore Picker](#booting-seamlessly-without-verbose-or-opencore-picker)
|
||||
* [Booting seamlessly without Boot Picker](#booting-seamlessly-without-boot-picker)
|
||||
* [Applying Post Install Volume Patches](#applying-post-install-volume-patches)
|
||||
|
||||
## Booting without USB drive
|
||||
@@ -16,17 +16,13 @@ Once you've installed macOS through OpenCore, you can boot up and go through the
|
||||
|
||||
And voila! No more USB drive required.
|
||||
|
||||
## Booting seamlessly without Verbose or OpenCore Picker
|
||||
## Booting seamlessly without Boot Picker
|
||||
|
||||
To do this, run the OpenCore Patcher and head to Patcher Settings:
|
||||
To do this, run the OpenCore Patcher and head to Patcher Settings, then uncheck "Show OpenCore Bootpicker" on the Build tab:
|
||||
|
||||

|
||||
|
||||
Here you can change different patcher settings, however the main interest is:
|
||||
|
||||
* 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.
|
||||
Once you've toggled it 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.
|
||||
|
||||
## Enabling SIP
|
||||
|
||||
@@ -34,6 +30,8 @@ For many users, SIP will be lowered by default on build. For Intel HD 4000 users
|
||||
|
||||
Note: Machines running macOS Ventura or systems with non-Metal GPUs cannot enable SIP outright, due to having a patched root volume. Enabling it will brick the installation.
|
||||
|
||||
Going forward with 0.6.6, SIP settings can be accessed from the Security tab shown in the images.
|
||||
|
||||
| SIP Enabled | SIP Lowered (Root Patching) | SIP Disabled |
|
||||
| :--- | :--- | :--- |
|
||||
|  |  |  |
|
||||
@@ -48,22 +46,16 @@ If you're unsure whether you should enable SIP, leave it as-is. Systems where yo
|
||||
|
||||
Post Install Volume Patches, sometimes also called root patches, are patches that have to be installed to disk for some older Macs to gain back functionality.
|
||||
|
||||
OCLP v0.4.4 and higher include an autopatcher, which will automatically root patch your system but **only if the USB install media was created within OCLP.**
|
||||
OCLP will automatically root patch your system during a first time install **if the USB install media was created within OCLP.** Users will also be prompted to install these patches after macOS updates or whenever patches are not detected on the system. We recommend rebuilding OpenCore with the latest version of OCLP to take advantage of these new features.
|
||||
|
||||
Users with OCLP v0.4.4 or higher will also be prompted to install these patches after macOS updates or whenever patches are not detected on the system. We recommend rebuilding OpenCore with the latest version of OCLP to take advantage of these new features.
|
||||
Users can also see whether applicable patches have been installed, date and version the system was root patched with in the Post-Install Menu.
|
||||
|
||||
|
||||
|
||||
In OCLP v0.4.5 a new indicator was added to help users to see if, when and on what version the system was root patched. Note that the "Available patches" section above this does not track the status and will always show the patches that are available, whether they're installed or not.
|
||||
|
||||
| Automatic install prompt in 0.4.4+ | Last patched status in 0.4.5+ |
|
||||
| Automatic install prompt | Status |
|
||||
| :--- | :--- |
|
||||
|  |  |
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### Running Post Install patches manually
|
||||
|
||||
If you're using OCLP v0.4.3 or earlier, or need to run the patcher manually, you can do so with the app. There is no harm in trying to run the Patcher, as without compatible hardware, nothing will be done. You can see below on whether your hardware needs root volume patching or not.
|
||||
|
||||
@@ -209,7 +209,7 @@ Ventura has dropped more models which includes all of the blacklisted Macs in qu
|
||||
|
||||
Firstly run the GUI version of OpenCore Legacy Patcher.
|
||||
|
||||
Then go to **Settings**, go to **SMBIOS Settings**, set SMBIOS Spoof Level to **Moderate**. Set SMBIOS Spoof Model **one listed next to your native model in the table for spoofed models below.**
|
||||
Then go to **Settings** and **SMBIOS** tab, set SMBIOS Spoof Level to **Moderate**. Set SMBIOS Spoof Model **one listed next to your native model in the table for spoofed models below.**
|
||||
|
||||
Notice that "Allow native models" and "Allow Native Spoofs" **are NOT** enabled unlike on Monterey, this is on purpose. They are no longer relevant on Ventura and enabling them will cause boot issues.
|
||||
|
||||
@@ -239,14 +239,16 @@ Spoofing to any model with native Ventura support should work, but these are the
|
||||
|
||||
::: details macOS Monterey
|
||||
|
||||
Firstly, run the GUI version of OpenCore Legacy Patcher. Secondly, go to **Settings** and tick **Allow native models**.
|
||||
Firstly, run the GUI version of OpenCore Legacy Patcher. Secondly, go to **Settings** then the **App** tab and tick **Allow native models**.
|
||||
|
||||
Then, go to **SMBIOS Settings**, tick **Allow Native Spoofs**, set SMBIOS Spoof Level to **Moderate**. Set SMBIOS Spoof Model to **one listed next to your native model in the table for spoofed models below.**
|
||||
[](../images/OCLP-App-Allow-Native-Models.png)
|
||||
|
||||
Then, go to **SMBIOS** tab, tick **Allow spoofing native Macs**, set SMBIOS Spoof Level to **Moderate**. Set SMBIOS Spoof Model to **one listed next to your native model in the table for spoofed models below.**
|
||||
|
||||
|
||||
| Main Settings view | SMBIOS settings |
|
||||
| :--- | :--- |
|
||||
|  |  |
|
||||
|  |  |
|
||||
|
||||
|
||||
::: details Table for spoofed models (click to expand)
|
||||
|
||||
@@ -7,7 +7,7 @@ With the release of OpenCore Legacy Patcher v0.5.0 and newer, early support for
|
||||
Ventura's release dropped a large amount of Intel hardware, thus requiring the usage of OpenCore Legacy Patcher on the following models (in addition to previously removed models):
|
||||
|
||||
* iMac16,1 (21.5-inch, Late 2015)
|
||||
* iMac16,2 (21.5-inch 4K, Late 2015)
|
||||
* iMac16,2 (21.5-inch and 21.5-inch 4K, Late 2015)
|
||||
* iMac17,1 (27-inch 5K, Late 2015)
|
||||
* MacBook9,1 (12-inch, Early 2016)
|
||||
* MacBookAir7,1 (11-inch, Early 2015)
|
||||
@@ -78,11 +78,6 @@ With OpenCore Legacy Patcher v0.6.0, basic support has been implemented via Root
|
||||
|
||||
:::
|
||||
|
||||
### Ethernet issue with Early 2008 Mac Pro
|
||||
|
||||
MacPro3,1 suffers from the Ethernet driver dying after returning from sleep, current workaround is to use a USB Ethernet adapter or disable sleep.
|
||||
|
||||
|
||||
::: details Legacy Wireless Support (Resolved in v0.6.0 and newer)
|
||||
|
||||
|
||||
|
||||
BIN
images/OCLP-App-Allow-Native-Models.png
Normal file
|
After Width: | Height: | Size: 542 KiB |
|
Before Width: | Height: | Size: 210 KiB After Width: | Height: | Size: 390 KiB |
|
Before Width: | Height: | Size: 201 KiB After Width: | Height: | Size: 251 KiB |
|
Before Width: | Height: | Size: 252 KiB After Width: | Height: | Size: 452 KiB |
|
Before Width: | Height: | Size: 238 KiB After Width: | Height: | Size: 450 KiB |
|
Before Width: | Height: | Size: 506 KiB After Width: | Height: | Size: 526 KiB |
|
Before Width: | Height: | Size: 496 KiB After Width: | Height: | Size: 530 KiB |
|
Before Width: | Height: | Size: 499 KiB After Width: | Height: | Size: 533 KiB |
|
Before Width: | Height: | Size: 286 KiB After Width: | Height: | Size: 530 KiB |
BIN
images/OCLP-SMBIOS-Allow-Native-Spoof.png
Normal file
|
After Width: | Height: | Size: 451 KiB |
|
Before Width: | Height: | Size: 154 KiB After Width: | Height: | Size: 451 KiB |
BIN
images/ocdebugimage.png
Normal file
|
After Width: | Height: | Size: 316 KiB |
|
Before Width: | Height: | Size: 405 KiB After Width: | Height: | Size: 445 KiB |
|
Before Width: | Height: | Size: 330 KiB After Width: | Height: | Size: 446 KiB |
@@ -1383,6 +1383,24 @@
|
||||
<key>PlistPath</key>
|
||||
<string>Contents/Info.plist</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>Arch</key>
|
||||
<string>x86_64</string>
|
||||
<key>BundlePath</key>
|
||||
<string>LegacyKeyboardInjector.kext</string>
|
||||
<key>Comment</key>
|
||||
<string>Legacy Keyboard support for macOS 11+</string>
|
||||
<key>Enabled</key>
|
||||
<false/>
|
||||
<key>ExecutablePath</key>
|
||||
<string></string>
|
||||
<key>MaxKernel</key>
|
||||
<string></string>
|
||||
<key>MinKernel</key>
|
||||
<string>17.0.0</string>
|
||||
<key>PlistPath</key>
|
||||
<string>Contents/Info.plist</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>Arch</key>
|
||||
<string>x86_64</string>
|
||||
@@ -1943,6 +1961,36 @@
|
||||
<key>Skip</key>
|
||||
<integer>0</integer>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>Arch</key>
|
||||
<string>x86_64</string>
|
||||
<key>Base</key>
|
||||
<string>__ZN17IOPCIConfigurator18IOPCIIsHotplugPortEP16IOPCIConfigEntry</string>
|
||||
<key>Comment</key>
|
||||
<string>CaseySJ - Fix PCI bus enumeration</string>
|
||||
<key>Count</key>
|
||||
<integer>1</integer>
|
||||
<key>Enabled</key>
|
||||
<false/>
|
||||
<key>Find</key>
|
||||
<data>hNt1S0GLVzg=</data>
|
||||
<key>Identifier</key>
|
||||
<string>com.apple.iokit.IOPCIFamily</string>
|
||||
<key>Limit</key>
|
||||
<integer>0</integer>
|
||||
<key>Mask</key>
|
||||
<data></data>
|
||||
<key>MaxKernel</key>
|
||||
<string></string>
|
||||
<key>MinKernel</key>
|
||||
<string>22.0.0</string>
|
||||
<key>Replace</key>
|
||||
<data>hNvrS0GLVzg=</data>
|
||||
<key>ReplaceMask</key>
|
||||
<data></data>
|
||||
<key>Skip</key>
|
||||
<integer>0</integer>
|
||||
</dict>
|
||||
</array>
|
||||
<key>Quirks</key>
|
||||
<dict>
|
||||
@@ -1960,6 +2008,8 @@
|
||||
<false/>
|
||||
<key>DisableIoMapper</key>
|
||||
<false/>
|
||||
<key>DisableIoMapperMapping</key>
|
||||
<false/>
|
||||
<key>DisableLinkeditJettison</key>
|
||||
<false/>
|
||||
<key>DisableRtcChecksum</key>
|
||||
@@ -2617,6 +2667,8 @@
|
||||
<string>Disabled</string>
|
||||
<key>IgnoreTextInGraphics</key>
|
||||
<false/>
|
||||
<key>InitialMode</key>
|
||||
<string>Auto</string>
|
||||
<key>ProvideConsoleGop</key>
|
||||
<true/>
|
||||
<key>ReconnectGraphicsOnConnect</key>
|
||||
|
||||
BIN
payloads/Icon/AppIcons/OC-Build.icns
Normal file
BIN
payloads/Icon/AppIcons/OC-Installer.icns
Normal file
BIN
payloads/Icon/AppIcons/OC-Patch.icns
Normal file
BIN
payloads/Icon/AppIcons/OC-Support.icns
Normal file
@@ -1,4 +1,11 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Create alias for OpenCore-Patcher.app
|
||||
if [ ! -d "/Applications/OpenCore-Patcher.app" ]; then
|
||||
ln -s "/Library/Application Support/Dortania/OpenCore-Patcher.app" "/Applications/OpenCore-Patcher.app"
|
||||
fi
|
||||
|
||||
# Start root patching
|
||||
app_path="/Library/Application Support/Dortania/OpenCore-Patcher.app/Contents/MacOS/OpenCore-Patcher"
|
||||
args="--patch_sys_vol"
|
||||
"$app_path" "$args" &> "/Users/Shared/.OCLP-AutoPatcher-Log-$(date +"%Y_%m_%d_%I_%M_%p").txt"
|
||||
|
||||
BIN
payloads/Kexts/Acidanthera/BlueToolFixup-v2.6.6-DEBUG.zip
Normal file
BIN
payloads/Kexts/Acidanthera/BlueToolFixup-v2.6.6-RELEASE.zip
Normal file
BIN
payloads/Kexts/Acidanthera/Lilu-v1.6.6-DEBUG.zip
Normal file
BIN
payloads/Kexts/Acidanthera/Lilu-v1.6.6-RELEASE.zip
Normal file
BIN
payloads/Kexts/Misc/LegacyKeyboardInjector-v1.0.0.zip
Normal file
@@ -152,6 +152,26 @@ class GenerateKexts:
|
||||
self._get_latest_release(kext_folder, kext_name)
|
||||
|
||||
|
||||
def _is_build_nightly(self, kext: str, version: str) -> bool:
|
||||
# Load CHANGELOG.md
|
||||
changelog_path = Path(f"../../CHANGELOG.md").absolute()
|
||||
with open(changelog_path, "r") as changelog_file:
|
||||
changelog = changelog_file.read()
|
||||
|
||||
# Check if kext is in changelog
|
||||
if kext not in changelog:
|
||||
return False
|
||||
|
||||
# Check if kext is 'rolling' or 'nightly'
|
||||
for line in changelog.split("\n"):
|
||||
if kext in line and version in line:
|
||||
if ("rolling" in line or "nightly" in line):
|
||||
return True
|
||||
break
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def _get_latest_release(self, kext_folder, kext_name, override_kext_zip_name=None):
|
||||
# Get latest release from GitHub API
|
||||
repo_url = KEXT_DICTIONARY[kext_folder][kext_name]["Repository"].replace("https://github.com", "https://api.github.com/repos")
|
||||
@@ -178,6 +198,11 @@ class GenerateKexts:
|
||||
|
||||
if packaging.version.parse(remote_version) <= packaging.version.parse(local_version):
|
||||
print(f" {kext_name} {variant} is up to date: v{local_version}")
|
||||
if remote_version == local_version:
|
||||
if self._is_build_nightly(kext_name, local_version) is False:
|
||||
continue
|
||||
print(f" {kext_name} {variant} is a nightly build, updating...")
|
||||
else:
|
||||
continue
|
||||
|
||||
for asset in latest_release["assets"]:
|
||||
@@ -186,9 +211,9 @@ class GenerateKexts:
|
||||
print(f" Downloading {kext_name} {variant}: v{remote_version}...")
|
||||
zip_name = f"{override_kext_zip_name}-v{remote_version}-{variant}.zip" if override_kext_zip_name else f"{kext_name}-v{remote_version}-{variant}.zip"
|
||||
|
||||
self._download_file(asset["browser_download_url"], f"./{kext_folder}/{zip_name}", f"{kext_name}.kext")
|
||||
if Path(f"./{kext_folder}/{zip_name}").exists():
|
||||
if Path(f"./{kext_folder}/{zip_name.replace(f'v{remote_version}', f'v{local_version}')}").exists():
|
||||
subprocess.run(["rm", "-rf", f"./{kext_folder}/{zip_name.replace(f'v{remote_version}', f'v{local_version}')}"])
|
||||
self._download_file(asset["browser_download_url"], f"./{kext_folder}/{zip_name}", f"{kext_name}.kext")
|
||||
self._update_constants_file(KEXT_DICTIONARY[kext_folder][kext_name]["Constants Variable"], local_version, remote_version)
|
||||
|
||||
if override_kext_zip_name:
|
||||
|
||||
28
payloads/Tools/OpenCore-Patcher.app/Contents/Info.plist
Normal file
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>OpenCore-Patcher</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>OpenCore-Patcher</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>OC-Patcher.icns</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>com.dortania.opencore-legacy-patcher-helper</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>OpenCore-Patcher</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>10.10.0</string>
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<true/>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>Copyright © 2020-2023 Dortania</string>
|
||||
<key>LSUIElement</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -4,3 +4,4 @@ wxpython
|
||||
pyinstaller
|
||||
packaging
|
||||
py_sip_xnu
|
||||
py-applescript
|
||||
@@ -9,8 +9,9 @@ from resources import network_handler, constants, global_settings
|
||||
DATE_FORMAT: str = "%Y-%m-%d %H-%M-%S"
|
||||
ANALYTICS_SERVER: str = ""
|
||||
SITE_KEY: str = ""
|
||||
CRASH_URL: str = ANALYTICS_SERVER + "/crash"
|
||||
|
||||
VALID_ENTRIES: dict = {
|
||||
VALID_ANALYTICS_ENTRIES: dict = {
|
||||
'KEY': str, # Prevent abuse (embedded at compile time)
|
||||
'UNIQUE_IDENTITY': str, # Host's UUID as SHA1 hash
|
||||
'APPLICATION_NAME': str, # ex. OpenCore Legacy Patcher
|
||||
@@ -23,17 +24,63 @@ VALID_ENTRIES: dict = {
|
||||
'TIMESTAMP': datetime.datetime, # ex. 2021-09-01-12-00-00
|
||||
}
|
||||
|
||||
VALID_CRASH_ENTRIES: dict = {
|
||||
'KEY': str, # Prevent abuse (embedded at compile time)
|
||||
'APPLICATION_VERSION': str, # ex. 0.2.0
|
||||
'APPLICATION_COMMIT': str, # ex. 0.2.0 or {commit hash if not a release}
|
||||
'OS_VERSION': str, # ex. 10.15.7
|
||||
'MODEL': str, # ex. MacBookPro11,5
|
||||
'TIMESTAMP': datetime.datetime, # ex. 2021-09-01-12-00-00
|
||||
'CRASH_LOG': str, # ex. "This is a crash log"
|
||||
}
|
||||
|
||||
|
||||
class Analytics:
|
||||
|
||||
def __init__(self, global_constants: constants.Constants) -> None:
|
||||
self.constants: constants.Constants = global_constants
|
||||
self.unique_identity = str(self.constants.computer.uuid_sha1)
|
||||
self.application = str("OpenCore Legacy Patcher")
|
||||
self.version = str(self.constants.patcher_version)
|
||||
self.os = str(self.constants.detected_os_version)
|
||||
self.model = str(self.constants.computer.real_model)
|
||||
self.date = str(datetime.datetime.now().strftime(DATE_FORMAT))
|
||||
|
||||
|
||||
def send_analytics(self) -> None:
|
||||
if global_settings.GlobalEnviromentSettings().read_property("DisableCrashAndAnalyticsReporting") is True:
|
||||
return
|
||||
|
||||
self._generate_base_data()
|
||||
self._post_data()
|
||||
self._post_analytics_data()
|
||||
|
||||
|
||||
def send_crash_report(self, log_file: Path) -> None:
|
||||
if ANALYTICS_SERVER == "":
|
||||
return
|
||||
if SITE_KEY == "":
|
||||
return
|
||||
if global_settings.GlobalEnviromentSettings().read_property("DisableCrashAndAnalyticsReporting") is True:
|
||||
return
|
||||
if not log_file.exists():
|
||||
return
|
||||
if self.constants.commit_info[0].startswith("refs/tags"):
|
||||
# Avoid being overloaded with crash reports
|
||||
return
|
||||
|
||||
commit_info = self.constants.commit_info[0].split("/")[-1] + "_" + self.constants.commit_info[1].split("T")[0] + "_" + self.constants.commit_info[2].split("/")[-1]
|
||||
|
||||
crash_data= {
|
||||
"KEY": SITE_KEY,
|
||||
"APPLICATION_VERSION": self.version,
|
||||
"APPLICATION_COMMIT": commit_info,
|
||||
"OS_VERSION": self.os,
|
||||
"MODEL": self.model,
|
||||
"TIMESTAMP": self.date,
|
||||
"CRASH_LOG": log_file.read_text()
|
||||
}
|
||||
|
||||
network_handler.NetworkUtilities().post(CRASH_URL, json = crash_data)
|
||||
|
||||
|
||||
def _get_country(self) -> str:
|
||||
@@ -54,12 +101,6 @@ class Analytics:
|
||||
|
||||
|
||||
def _generate_base_data(self) -> None:
|
||||
|
||||
self.unique_identity = str(self.constants.computer.uuid_sha1)
|
||||
self.application = str("OpenCore Legacy Patcher")
|
||||
self.version = str(self.constants.patcher_version)
|
||||
self.os = str( self.constants.detected_os_version)
|
||||
self.model = str(self.constants.computer.real_model)
|
||||
self.gpus = []
|
||||
|
||||
self.firmware = str(self.constants.computer.firmware_vendor)
|
||||
@@ -78,14 +119,14 @@ class Analytics:
|
||||
'GPUS': self.gpus,
|
||||
'FIRMWARE': self.firmware,
|
||||
'LOCATION': self.location,
|
||||
'TIMESTAMP': str(datetime.datetime.now().strftime(DATE_FORMAT)),
|
||||
'TIMESTAMP': self.date,
|
||||
}
|
||||
|
||||
# convert to JSON:
|
||||
self.data = json.dumps(self.data)
|
||||
|
||||
|
||||
def _post_data(self) -> None:
|
||||
def _post_analytics_data(self) -> None:
|
||||
# Post data to analytics server
|
||||
if ANALYTICS_SERVER == "":
|
||||
return
|
||||
|
||||
@@ -50,7 +50,7 @@ class arguments:
|
||||
"""
|
||||
Enter validation mode
|
||||
"""
|
||||
|
||||
logging.info("Set Validation Mode")
|
||||
validation.PatcherValidation(self.constants)
|
||||
|
||||
|
||||
@@ -59,9 +59,9 @@ class arguments:
|
||||
Start root volume patching
|
||||
"""
|
||||
|
||||
logging.info("- Set System Volume patching")
|
||||
logging.info("Set System Volume patching")
|
||||
if "Library/InstallerSandboxes/" in str(self.constants.payload_path):
|
||||
logging.info("- Running from Installer Sandbox")
|
||||
logging.info("- Running from Installer Sandbox, blocking OS updaters")
|
||||
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():
|
||||
@@ -75,7 +75,7 @@ class arguments:
|
||||
"""
|
||||
Start root volume unpatching
|
||||
"""
|
||||
logging.info("- Set System 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()
|
||||
|
||||
|
||||
@@ -84,7 +84,7 @@ class arguments:
|
||||
Start root volume auto patching
|
||||
"""
|
||||
|
||||
logging.info("- Set Auto patching")
|
||||
logging.info("Set Auto patching")
|
||||
sys_patch_auto.AutomaticSysPatch(self.constants).start_auto_patch()
|
||||
|
||||
|
||||
@@ -92,6 +92,7 @@ class arguments:
|
||||
"""
|
||||
Start config building process
|
||||
"""
|
||||
logging.info("Set OpenCore Build")
|
||||
|
||||
if self.args.model:
|
||||
if self.args.model:
|
||||
|
||||
@@ -113,7 +113,7 @@ class BuildOpenCore:
|
||||
self.config["#Revision"]["Hardware-Probe"] = pickle.dumps(computer_copy)
|
||||
else:
|
||||
self.config["#Revision"]["Build-Type"] = "OpenCore Built for External Machine"
|
||||
self.config["#Revision"]["OpenCore-Version"] = f"{self.constants.opencore_version} - {self.constants.opencore_build} - {self.constants.opencore_commit}"
|
||||
self.config["#Revision"]["OpenCore-Version"] = f"{self.constants.opencore_version} - {self.constants.opencore_build}"
|
||||
self.config["#Revision"]["Original-Model"] = self.model
|
||||
self.config["NVRAM"]["Add"]["4D1FDA02-38C7-4A6A-9CC6-4BCCA8B30102"]["OCLP-Version"] = f"{self.constants.patcher_version}"
|
||||
self.config["NVRAM"]["Add"]["4D1FDA02-38C7-4A6A-9CC6-4BCCA8B30102"]["OCLP-Model"] = self.model
|
||||
@@ -153,5 +153,3 @@ class BuildOpenCore:
|
||||
logging.info(f"Your OpenCore EFI for {self.model} has been built at:")
|
||||
logging.info(f" {self.constants.opencore_release_folder}")
|
||||
logging.info("")
|
||||
if self.constants.gui_mode is False:
|
||||
input("Press [Enter] to continue\n")
|
||||
|
||||
@@ -73,7 +73,7 @@ class BuildFirmware:
|
||||
support.BuildSupport(self.model, self.constants, self.config).enable_kext("AppleIntelCPUPowerManagement.kext", self.constants.aicpupm_version, self.constants.aicpupm_path)
|
||||
support.BuildSupport(self.model, self.constants, self.config).enable_kext("AppleIntelCPUPowerManagementClient.kext", self.constants.aicpupm_version, self.constants.aicpupm_client_path)
|
||||
|
||||
if smbios_data.smbios_dictionary[self.model]["CPU Generation"] <= cpu_data.cpu_data.sandy_bridge.value or self.constants.disable_xcpm is True:
|
||||
if smbios_data.smbios_dictionary[self.model]["CPU Generation"] <= cpu_data.cpu_data.sandy_bridge.value or self.constants.disable_fw_throttle is True:
|
||||
# With macOS 12.3 Beta 1, Apple dropped the 'plugin-type' check within X86PlatformPlugin
|
||||
# Because of this, X86PP will match onto the CPU instead of ACPI_SMC_PlatformPlugin
|
||||
# This causes power management to break on pre-Ivy Bridge CPUs as they don't have correct
|
||||
@@ -81,11 +81,11 @@ class BuildFirmware:
|
||||
# This patch will simply increase ASPP's 'IOProbeScore' to outmatch X86PP
|
||||
logging.info("- Overriding ACPI SMC matching")
|
||||
support.BuildSupport(self.model, self.constants, self.config).enable_kext("ASPP-Override.kext", self.constants.aspp_override_version, self.constants.aspp_override_path)
|
||||
if self.constants.disable_xcpm is True:
|
||||
if self.constants.disable_fw_throttle is True:
|
||||
# Only inject on older OSes if user requests
|
||||
support.BuildSupport(self.model, self.constants, self.config).get_item_by_kv(self.config["Kernel"]["Add"], "BundlePath", "ASPP-Override.kext")["MinKernel"] = ""
|
||||
|
||||
if self.constants.disable_msr_power_ctl is True and smbios_data.smbios_dictionary[self.model]["CPU Generation"] >= cpu_data.cpu_data.nehalem.value:
|
||||
if self.constants.disable_fw_throttle is True and smbios_data.smbios_dictionary[self.model]["CPU Generation"] >= cpu_data.cpu_data.nehalem.value:
|
||||
logging.info("- Disabling Firmware Throttling")
|
||||
# Nehalem and newer systems force firmware throttling via MSR_POWER_CTL
|
||||
support.BuildSupport(self.model, self.constants, self.config).enable_kext("SimpleMSR.kext", self.constants.simplemsr_version, self.constants.simplemsr_path)
|
||||
@@ -220,17 +220,28 @@ class BuildFirmware:
|
||||
|
||||
self._dual_dp_handling()
|
||||
|
||||
# Force VMM as a temporary solution to getting the MacPro6,1 booting in Ventura
|
||||
# With macOS Ventura, Apple removed AppleIntelCPUPowerManagement.kext and assumed XCPM support across all Macs
|
||||
# This change resulted in broken OS booting as the machine had no power management support
|
||||
# Currently the AICPUPM fix is not fully functional, thus forcing VMM is a temporary solution
|
||||
# Waiting for XNU source to be released to fix this properly
|
||||
# Ref: https://forums.macrumors.com/threads/opencore-on-the-mac-pro.2207814/
|
||||
if self.model in ["MacPro6,1", "iMac7,1", "iMac8,1", "MacBookPro4,1"] or self.constants.set_vmm_cpuid is True:
|
||||
# Patches IOPCIConfigurator.cpp's IOPCIIsHotplugPort to skip configRead16/32 calls
|
||||
# Credit to CaseySJ for original discovery:
|
||||
# - Patch: https://github.com/AMD-OSX/AMD_Vanilla/pull/196
|
||||
# - Source: https://github.com/apple-oss-distributions/IOPCIFamily/blob/main/IOPCIConfigurator.cpp#L968-L1022
|
||||
#
|
||||
# Currently all pre-Sandy Bridge Macs lacking an iGPU benefit from this patch as well as MacPro6,1
|
||||
# Otherwise some graphics hardware will fail to wake, macOS will misreport hardware as ExpressCard-based,
|
||||
# prevents MacPro6,1 from both booting unaccelerated and breaks low power states.
|
||||
if (
|
||||
self.model in ["MacPro6,1", "MacBookPro4,1"] or
|
||||
(
|
||||
smbios_data.smbios_dictionary[self.model]["CPU Generation"] < cpu_data.cpu_data.sandy_bridge.value and \
|
||||
not self.model.startswith("MacBook")
|
||||
)
|
||||
):
|
||||
logging.info("- Adding PCI Bus Enumeration Patch")
|
||||
support.BuildSupport(self.model, self.constants, self.config).get_item_by_kv(self.config["Kernel"]["Patch"], "Comment", "CaseySJ - Fix PCI bus enumeration")["Enabled"] = True
|
||||
|
||||
if self.constants.set_vmm_cpuid is True:
|
||||
logging.info("- Enabling VMM patch")
|
||||
self.config["Kernel"]["Emulate"]["Cpuid1Data"] = binascii.unhexlify("00000000000000000000008000000000")
|
||||
self.config["Kernel"]["Emulate"]["Cpuid1Mask"] = binascii.unhexlify("00000000000000000000008000000000")
|
||||
self.config["Kernel"]["Emulate"]["MinKernel"] = "22.0.0"
|
||||
|
||||
if (
|
||||
self.model.startswith("MacBook")
|
||||
|
||||
@@ -259,7 +259,7 @@ class BuildGraphicsAudio:
|
||||
"CAIL,CAIL_DisableUVDPowerGating": 1,
|
||||
"CAIL,CAIL_DisableVCEPowerGating": 1,
|
||||
})
|
||||
if self.constants.imac_model == "Legacy GCN":
|
||||
if self.constants.imac_model == "GCN":
|
||||
logging.info("- Adding Legacy GCN Power Gate Patches")
|
||||
self.config["DeviceProperties"]["Add"][backlight_path].update({
|
||||
"CAIL,CAIL_DisableDrmdmaPowerGating": 1,
|
||||
@@ -274,7 +274,7 @@ class BuildGraphicsAudio:
|
||||
"CAIL,CAIL_DisableUVDPowerGating": 1,
|
||||
"CAIL,CAIL_DisableVCEPowerGating": 1,
|
||||
})
|
||||
elif self.constants.imac_model == "AMD Lexa":
|
||||
elif self.constants.imac_model == "Lexa":
|
||||
logging.info("- Adding Lexa Spoofing Patches")
|
||||
self.config["DeviceProperties"]["Add"][backlight_path].update({
|
||||
"model": "AMD Radeon Pro WX 3200",
|
||||
@@ -285,7 +285,7 @@ class BuildGraphicsAudio:
|
||||
"model": "AMD Radeon Pro WX 3200",
|
||||
"device-id": binascii.unhexlify("FF67"),
|
||||
})
|
||||
elif self.constants.imac_model == "AMD Navi":
|
||||
elif self.constants.imac_model == "Navi":
|
||||
logging.info("- Adding Navi Spoofing Patches")
|
||||
navi_backlight_path = backlight_path+"/Pci(0x0,0x0)/Pci(0x0,0x0)"
|
||||
self.config["DeviceProperties"]["Add"][navi_backlight_path] = {
|
||||
|
||||
@@ -106,23 +106,6 @@ class BuildMiscellaneous:
|
||||
logging.info("- Disabling memory error reporting")
|
||||
re_block_args.append("pcie")
|
||||
|
||||
# Resolve mediaanalysisd crashing on 3802 GPUs
|
||||
gpu_dict = [] if self.constants.custom_model else self.constants.computer.gpus
|
||||
if gpu_dict == []:
|
||||
gpu_dict = smbios_data.smbios_dictionary[self.model]["Stock GPUs"] if self.model in smbios_data.smbios_dictionary else []
|
||||
|
||||
for gpu in gpu_dict:
|
||||
if not self.constants.custom_model:
|
||||
gpu = gpu.arch
|
||||
if gpu in [
|
||||
device_probe.Intel.Archs.Ivy_Bridge,
|
||||
device_probe.Intel.Archs.Haswell,
|
||||
device_probe.NVIDIA.Archs.Kepler,
|
||||
]:
|
||||
logging.info("- Disabling mediaanalysisd")
|
||||
re_block_args.append("media")
|
||||
break
|
||||
|
||||
return re_block_args
|
||||
|
||||
|
||||
@@ -192,19 +175,20 @@ class BuildMiscellaneous:
|
||||
Trackpad Handler
|
||||
"""
|
||||
|
||||
# Pre-Force Touch trackpad support for macOS Ventura
|
||||
# Pre-Force Touch trackpad & keyboard support for macOS Ventura
|
||||
if smbios_data.smbios_dictionary[self.model]["CPU Generation"] < cpu_data.cpu_data.skylake.value:
|
||||
if self.model.startswith("MacBook"):
|
||||
# These units got force touch early, so ignore them
|
||||
# These units got force touch & the new keyboard mapping early, so ignore them
|
||||
if self.model not in ["MacBookPro11,4", "MacBookPro11,5", "MacBookPro12,1", "MacBook8,1"]:
|
||||
support.BuildSupport(self.model, self.constants, self.config).enable_kext("AppleUSBTopCase.kext", self.constants.topcase_version, self.constants.top_case_path)
|
||||
support.BuildSupport(self.model, self.constants, self.config).get_kext_by_bundle_path("AppleUSBTopCase.kext/Contents/PlugIns/AppleUSBTCButtons.kext")["Enabled"] = True
|
||||
support.BuildSupport(self.model, self.constants, self.config).get_kext_by_bundle_path("AppleUSBTopCase.kext/Contents/PlugIns/AppleUSBTCKeyboard.kext")["Enabled"] = True
|
||||
support.BuildSupport(self.model, self.constants, self.config).get_kext_by_bundle_path("AppleUSBTopCase.kext/Contents/PlugIns/AppleUSBTCKeyEventDriver.kext")["Enabled"] = True
|
||||
support.BuildSupport(self.model, self.constants, self.config).enable_kext("AppleUSBMultitouch.kext", self.constants.multitouch_version, self.constants.multitouch_path)
|
||||
# Legacy Trackpad support
|
||||
# Legacy Trackpad & Keyboard support
|
||||
if self.model in ["MacBook4,1", "MacBook5,2"]:
|
||||
support.BuildSupport(self.model, self.constants, self.config).enable_kext("AppleUSBTrackpad.kext", self.constants.apple_trackpad, self.constants.apple_trackpad_path)
|
||||
support.BuildSupport(self.model, self.constants, self.config).enable_kext("LegacyKeyboardInjector.kext", self.constants.legacy_keyboard, self.constants.legacy_keyboard_path) # Inject legacy personalities into AppleUSBTCKeyboard and AppleUSBTCKeyEventDriver
|
||||
|
||||
|
||||
def _thunderbolt_handling(self) -> None:
|
||||
|
||||
@@ -12,9 +12,10 @@ from data import os_data
|
||||
class Constants:
|
||||
def __init__(self) -> None:
|
||||
# Patcher Versioning
|
||||
self.patcher_version: str = "0.6.5" # OpenCore-Legacy-Patcher
|
||||
self.patcher_support_pkg_version: str = "0.9.7" # PatcherSupportPkg
|
||||
self.patcher_version: str = "0.6.7" # OpenCore-Legacy-Patcher
|
||||
self.patcher_support_pkg_version: str = "1.1.2" # PatcherSupportPkg
|
||||
self.copyright_date: str = "Copyright © 2020-2023 Dortania"
|
||||
self.patcher_name: str = "OpenCore Legacy Patcher"
|
||||
|
||||
# URLs
|
||||
self.url_patcher_support_pkg: str = "https://github.com/dortania/PatcherSupportPkg/releases/download/"
|
||||
@@ -26,13 +27,12 @@ class Constants:
|
||||
|
||||
# OpenCore Versioning
|
||||
# https://github.com/acidanthera/OpenCorePkg
|
||||
self.opencore_commit: str = "41b8aca - 04-03-2023"
|
||||
self.opencore_version: str = "0.9.1"
|
||||
self.opencore_version: str = "0.9.2"
|
||||
|
||||
# Kext Versioning
|
||||
## Acidanthera
|
||||
## https://github.com/acidanthera
|
||||
self.lilu_version: str = "1.6.4" # Lilu
|
||||
self.lilu_version: str = "1.6.6" # Lilu
|
||||
self.whatevergreen_version: str = "1.6.4" # WhateverGreen
|
||||
self.whatevergreen_navi_version: str = "1.6.4-Navi" # WhateverGreen (Navi Patch)
|
||||
self.airportbcrmfixup_version: str = "2.1.7" # AirPortBrcmFixup
|
||||
@@ -42,7 +42,7 @@ class Constants:
|
||||
self.featureunlock_version: str = "1.1.4" # FeatureUnlock
|
||||
self.debugenhancer_version: str = "1.0.7" # DebugEnhancer
|
||||
self.cpufriend_version: str = "1.2.6" # CPUFriend
|
||||
self.bluetool_version: str = "2.6.5" # BlueToolFixup (BrcmPatchRAM)
|
||||
self.bluetool_version: str = "2.6.6" # BlueToolFixup (BrcmPatchRAM)
|
||||
self.cslvfixup_version: str = "2.6.1" # CSLVFixup
|
||||
self.autopkg_version: str = "1.0.2" # AutoPkgInstaller
|
||||
self.cryptexfixup_version: str = "1.0.1" # CryptexFixup
|
||||
@@ -105,15 +105,19 @@ class Constants:
|
||||
## https://github.com/flagersgit/KDKlessWorkaround
|
||||
self.kdkless_version: str = "1.0.0"
|
||||
|
||||
## Jazzzny
|
||||
self.legacy_keyboard: str = "1.0.0" # LegacyKeyboardInjector - Jazzzny
|
||||
|
||||
# Get resource path
|
||||
self.current_path: Path = Path(__file__).parent.parent.resolve()
|
||||
self.original_path: Path = Path(__file__).parent.parent.resolve()
|
||||
self.payload_path: Path = self.current_path / Path("payloads")
|
||||
|
||||
# Patcher Settings
|
||||
## Internal settings
|
||||
self.allow_oc_everywhere: bool = False # Set whether Patcher can be run on unsupported Macs
|
||||
self.gui_mode: bool = False # Determine whether running in a GUI or TUI
|
||||
self.cli_mode: bool = False # Determine if running in CLI mode
|
||||
self.cli_mode: bool = True # Determine if running in CLI mode
|
||||
self.validate: bool = False # Enable validation testing for CI
|
||||
self.recovery_status: bool = False # Detect if booted into RecoveryOS
|
||||
self.ignore_updates: bool = False # Ignore OCLP updates
|
||||
@@ -129,6 +133,8 @@ class Constants:
|
||||
self.launcher_script: str = None # Determine launch file path (None if PyInstaller)
|
||||
self.booted_oc_disk: str = None # Determine current disk OCLP booted from
|
||||
self.unpack_thread = None # Determine if unpack thread finished (threading.Thread)
|
||||
self.update_stage: int = 0 # Determine update stage (see gui_support.py)
|
||||
self.log_filepath: Path = None # Path to log file
|
||||
|
||||
self.commit_info: tuple = (None, None, None) # Commit info (Branch, Commit Date, Commit URL)
|
||||
|
||||
@@ -203,13 +209,13 @@ class Constants:
|
||||
self.dGPU_switch: bool = False # Set Display GPU Switching for Windows
|
||||
self.force_surplus: bool = False # Force SurPlus patch in newer OSes
|
||||
self.force_latest_psp: bool = False # Force latest PatcherSupportPkg
|
||||
self.disable_msr_power_ctl: bool = False # Disable MSR Power Control (missing battery throttling)
|
||||
self.disable_fw_throttle: bool = False # Disable MSR Power Control and XCPM
|
||||
self.software_demux: bool = False # Enable Software Demux patch set
|
||||
self.force_vmm: bool = False # Force VMM patch
|
||||
self.disable_connectdrivers: bool = False # Disable ConnectDrivers (hibernation)
|
||||
self.set_content_caching: bool = False # Set Content Caching
|
||||
self.disable_xcpm: bool = False # Disable XCPM (X86PlatformPlugin.kext)
|
||||
self.set_vmm_cpuid: bool = False # Set VMM bit inside CPUID
|
||||
self.disable_cat_colorsync: bool = False # Disable the ColorSync patch to regain Display Profiles
|
||||
self.set_alc_usage: bool = True # Set AppleALC usage
|
||||
self.allow_3rd_party_drives: bool = True # Allow ThridPartyDrives quirk
|
||||
self.allow_nvme_fixing: bool = True # Allow NVMe Kernel Space Patches
|
||||
@@ -224,6 +230,17 @@ class Constants:
|
||||
]
|
||||
|
||||
# Payload Location
|
||||
|
||||
# Support Disk Images
|
||||
@property
|
||||
def payload_path_dmg(self):
|
||||
return self.original_path / Path("payloads.dmg")
|
||||
|
||||
@property
|
||||
def payload_local_binaries_root_path_dmg(self):
|
||||
return self.original_path / Path("Universal-Binaries.dmg")
|
||||
|
||||
|
||||
# OpenCore
|
||||
@property
|
||||
def opencore_zip_source(self):
|
||||
@@ -485,6 +502,10 @@ class Constants:
|
||||
def apple_isight_path(self):
|
||||
return self.payload_kexts_path / Path(f"Misc/LegacyUSBVideoSupport-v{self.apple_isight_version}.zip")
|
||||
|
||||
@property
|
||||
def legacy_keyboard_path(self):
|
||||
return self.payload_kexts_path / Path(f"Misc/LegacyKeyboardInjector-v{self.legacy_keyboard}.zip")
|
||||
|
||||
@property
|
||||
def apple_raid_path(self):
|
||||
return self.payload_kexts_path / Path(f"Misc/AppleRAIDCard-v{self.apple_raid_version}.zip")
|
||||
@@ -605,7 +626,7 @@ class Constants:
|
||||
|
||||
@property
|
||||
def oclp_helper_path(self):
|
||||
return self.payload_path / Path("Tools/OCLP-Helper")
|
||||
return self.payload_path / Path("Tools/OpenCore-Patcher.app/Contents/MacOS/OpenCore-Patcher")
|
||||
|
||||
@property
|
||||
def rsrrepair_userspace_path(self):
|
||||
@@ -649,14 +670,16 @@ class Constants:
|
||||
def payload_local_binaries_root_path(self):
|
||||
return self.payload_path / Path("Universal-Binaries")
|
||||
|
||||
@property
|
||||
def payload_local_binaries_root_path_zip(self):
|
||||
return self.payload_path / Path("Universal-Binaries.zip")
|
||||
|
||||
@property
|
||||
def kdk_download_path(self):
|
||||
return self.payload_path / Path("KDK.dmg")
|
||||
|
||||
@property
|
||||
def icns_resource_path(self):
|
||||
if self.launcher_script:
|
||||
return self.payload_path / Path("Icon/AppIcons")
|
||||
return Path(self.launcher_binary).parent.parent / Path("Resources")
|
||||
|
||||
|
||||
sbm_values = [
|
||||
"j137ap", # iMacPro1,1
|
||||
|
||||
@@ -71,6 +71,14 @@ class GenerateDefaults:
|
||||
global_settings.GlobalEnviromentSettings().write_property("MacBookPro_TeraScale_2_Accel", False)
|
||||
self.constants.allow_ts2_accel = False
|
||||
|
||||
if self.model in ["MacBookAir4,1","MacBookAir4,2","MacBookPro8,1","MacBookPro8,2","MacBookPro8,3","Macmini5,1"]:
|
||||
colorsync_status = global_settings.GlobalEnviromentSettings().read_property("Disable_ColorSync_Downgrade")
|
||||
if colorsync_status is True:
|
||||
self.constants.disable_cat_colorsync = True
|
||||
else:
|
||||
global_settings.GlobalEnviromentSettings().write_property("Disable_ColorSync_Downgrade", False)
|
||||
self.constants.disable_cat_colorsync = False
|
||||
|
||||
if self.model in smbios_data.smbios_dictionary:
|
||||
if smbios_data.smbios_dictionary[self.model]["CPU Generation"] >= cpu_data.cpu_data.skylake.value:
|
||||
# On 2016-2017 MacBook Pros, 15" devices used a stock Samsung SSD with IONVMeController
|
||||
|
||||
@@ -48,7 +48,7 @@ class GlobalEnviromentSettings:
|
||||
try:
|
||||
plistlib.dump(plist, Path(self.global_settings_plist).open("wb"))
|
||||
except PermissionError:
|
||||
logging.info("- Failed to write to global settings file")
|
||||
logging.info("Failed to write to global settings file")
|
||||
|
||||
|
||||
def _generate_settings_file(self) -> None:
|
||||
@@ -57,7 +57,7 @@ class GlobalEnviromentSettings:
|
||||
try:
|
||||
plistlib.dump({"Developed by Dortania": True,}, Path(self.global_settings_plist).open("wb"))
|
||||
except PermissionError:
|
||||
logging.info("- Permission error: Unable to write to global settings file")
|
||||
logging.info("Permission error: Unable to write to global settings file")
|
||||
|
||||
|
||||
def _convert_defaults_to_global_settings(self) -> None:
|
||||
@@ -76,14 +76,15 @@ class GlobalEnviromentSettings:
|
||||
try:
|
||||
plistlib.dump(global_settings_plist, Path(self.global_settings_plist).open("wb"))
|
||||
except PermissionError:
|
||||
logging.info("- Permission error: Unable to write to global settings file")
|
||||
logging.info("Permission error: Unable to write to global settings file")
|
||||
return
|
||||
|
||||
# delete defaults plist
|
||||
try:
|
||||
Path(defaults_path).unlink()
|
||||
except PermissionError:
|
||||
logging.info("- Permission error: Unable to delete defaults plist")
|
||||
except Exception as e:
|
||||
logging.error("Error: Unable to delete defaults plist")
|
||||
logging.error(e)
|
||||
|
||||
|
||||
def _fix_file_permission(self) -> None:
|
||||
@@ -100,6 +101,6 @@ class GlobalEnviromentSettings:
|
||||
# Set file permission to allow any user to write to log file
|
||||
result = subprocess.run(["chmod", "777", self.global_settings_plist], capture_output=True)
|
||||
if result.returncode != 0:
|
||||
logging.warning("- Failed to fix settings file permissions:")
|
||||
logging.warning("Failed to fix settings file permissions:")
|
||||
if result.stderr:
|
||||
logging.warning(result.stderr.decode("utf-8"))
|
||||
@@ -1,104 +0,0 @@
|
||||
import wx
|
||||
import webbrowser
|
||||
from resources import constants
|
||||
from data import os_data
|
||||
|
||||
class gui_help_menu:
|
||||
def __init__(self, versions, frame, frame_modal):
|
||||
self.constants: constants.Constants = versions
|
||||
self.frame = frame
|
||||
self.frame_modal = frame_modal
|
||||
|
||||
# Define Window Size
|
||||
self.WINDOW_WIDTH_MAIN = 300
|
||||
|
||||
|
||||
def reset_frame_modal(self):
|
||||
if not self.frame_modal:
|
||||
self.frame_modal = wx.Dialog(self.frame)
|
||||
else:
|
||||
self.frame_modal.DestroyChildren()
|
||||
self.frame_modal.Close()
|
||||
if self.constants.detected_os >= os_data.os_data.big_sur:
|
||||
self.frame_modal.ShowWithoutActivating()
|
||||
|
||||
def help_menu(self, event=None):
|
||||
# Define Menu
|
||||
# Header: Get help with OpenCore Legacy Patcher
|
||||
# Subheader: Following resources are available:
|
||||
# Button: Official Guide
|
||||
# Button: Official Discord Server
|
||||
|
||||
self.reset_frame_modal()
|
||||
self.frame_modal.SetSize((self.WINDOW_WIDTH_MAIN, -1))
|
||||
|
||||
# Header
|
||||
self.header = wx.StaticText(self.frame_modal, label="Patcher Resources", pos=(10,10))
|
||||
self.header.SetFont(wx.Font(19, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont"))
|
||||
self.header.Centre(wx.HORIZONTAL)
|
||||
|
||||
# Subheader
|
||||
self.subheader = wx.StaticText(self.frame_modal, label="Following resources are available:")
|
||||
self.subheader.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont"))
|
||||
self.subheader.SetPosition(
|
||||
wx.Point(
|
||||
self.header.GetPosition().x,
|
||||
self.header.GetPosition().y + self.header.GetSize().height + 5
|
||||
)
|
||||
)
|
||||
self.subheader.Centre(wx.HORIZONTAL)
|
||||
|
||||
|
||||
# Official Guide
|
||||
self.guide = wx.Button(self.frame_modal, label="Official Guide", size=(200,30))
|
||||
self.guide.SetPosition(
|
||||
wx.Point(
|
||||
self.subheader.GetPosition().x,
|
||||
self.subheader.GetPosition().y + self.subheader.GetSize().height + 5
|
||||
|
||||
)
|
||||
)
|
||||
self.guide.Bind(wx.EVT_BUTTON, lambda event: webbrowser.open(self.constants.guide_link))
|
||||
self.guide.Centre(wx.HORIZONTAL)
|
||||
|
||||
# Official Discord Server
|
||||
self.discord = wx.Button(self.frame_modal, label="Official Discord Server", size=(200,30))
|
||||
self.discord.SetPosition(
|
||||
wx.Point(
|
||||
self.guide.GetPosition().x,
|
||||
self.guide.GetPosition().y + self.guide.GetSize().height
|
||||
)
|
||||
)
|
||||
self.discord.Bind(wx.EVT_BUTTON, lambda event: webbrowser.open(self.constants.discord_link))
|
||||
self.discord.Centre(wx.HORIZONTAL)
|
||||
|
||||
# Overclock Button
|
||||
self.overclock = wx.Button(self.frame_modal, label="Official Support Phone", size=(200,30))
|
||||
self.overclock.SetPosition(
|
||||
wx.Point(
|
||||
self.discord.GetPosition().x,
|
||||
self.discord.GetPosition().y + self.discord.GetSize().height
|
||||
)
|
||||
)
|
||||
self.overclock.Bind(wx.EVT_BUTTON, lambda event: webbrowser.open("https://www.youtube.com/watch?v=dQw4w9WgXcQ"))
|
||||
self.overclock.Centre(wx.HORIZONTAL)
|
||||
|
||||
|
||||
self.return_to_main = wx.Button(self.frame_modal, label="Return to Main Menu", size=(150,30))
|
||||
self.return_to_main.SetPosition(
|
||||
wx.Point(
|
||||
self.overclock.GetPosition().x,
|
||||
self.overclock.GetPosition().y + self.overclock.GetSize().height + 5
|
||||
)
|
||||
)
|
||||
self.return_to_main.Bind(wx.EVT_BUTTON, lambda event: self.frame_modal.Close())
|
||||
self.return_to_main.Centre(wx.HORIZONTAL)
|
||||
|
||||
# Set Window Size to below Copyright Label
|
||||
self.frame_modal.SetSize(
|
||||
(
|
||||
-1,
|
||||
self.return_to_main.GetPosition().y + self.return_to_main.GetSize().height + 40
|
||||
)
|
||||
)
|
||||
self.frame_modal.ShowWindowModal()
|
||||
@@ -1,46 +0,0 @@
|
||||
import wx
|
||||
import time
|
||||
|
||||
class RedirectText(object):
|
||||
def __init__(self,aWxTextCtrl, sleep):
|
||||
self.out=aWxTextCtrl
|
||||
self.sleep = sleep
|
||||
|
||||
def write(self,string):
|
||||
self.out.WriteText(string)
|
||||
wx.GetApp().Yield()
|
||||
if self.sleep:
|
||||
time.sleep(0.01)
|
||||
|
||||
def fileno(self):
|
||||
return 1
|
||||
|
||||
def flush(self):
|
||||
pass
|
||||
|
||||
class RedirectLabel(object):
|
||||
def __init__(self,aWxTextCtrl):
|
||||
self.out=aWxTextCtrl
|
||||
|
||||
def write(self,string):
|
||||
if "MB/s" in string:
|
||||
self.out.SetLabel(string)
|
||||
self.out.Centre(wx.HORIZONTAL)
|
||||
wx.GetApp().Yield()
|
||||
time.sleep(0.01)
|
||||
|
||||
def fileno(self):
|
||||
return 1
|
||||
|
||||
def flush(self):
|
||||
pass
|
||||
|
||||
class RedirectLabelAll(object):
|
||||
def __init__(self,aWxTextCtrl):
|
||||
self.out=aWxTextCtrl
|
||||
|
||||
def write(self,string):
|
||||
self.out.SetLabel(string)
|
||||
self.out.Centre(wx.HORIZONTAL)
|
||||
wx.GetApp().Yield()
|
||||
time.sleep(0.01)
|
||||
@@ -2,15 +2,17 @@
|
||||
# Usage solely for TUI
|
||||
# Copyright (C) 2020-2022, Dhinak G, Mykola Grymalyuk
|
||||
|
||||
import logging
|
||||
import plistlib
|
||||
import subprocess
|
||||
import shutil
|
||||
import os
|
||||
import logging
|
||||
import applescript
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from resources import utilities, constants
|
||||
from data import os_data
|
||||
|
||||
|
||||
class tui_disk_installation:
|
||||
def __init__(self, versions):
|
||||
self.constants: constants.Constants = versions
|
||||
@@ -75,49 +77,37 @@ class tui_disk_installation:
|
||||
return supported_partitions
|
||||
|
||||
|
||||
def install_opencore(self, full_disk_identifier):
|
||||
def determine_sd_card(media_name):
|
||||
def _determine_sd_card(self, media_name: str):
|
||||
# Array filled with common SD Card names
|
||||
# Note most USB-based SD Card readers generally report as "Storage Device"
|
||||
# Thus no reliable way to detect further without parsing IOService output (kUSBProductString)
|
||||
if (
|
||||
"SD Card" in media_name or \
|
||||
"SD/MMC" in media_name or \
|
||||
"SDXC Reader" in media_name or \
|
||||
"SD Reader" in media_name or \
|
||||
"Card Reader" in media_name
|
||||
):
|
||||
if any(x in media_name for x in ("SD Card", "SD/MMC", "SDXC Reader", "SD Reader", "Card Reader")):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def install_opencore(self, full_disk_identifier: str):
|
||||
# TODO: Apple Script fails in Yosemite(?) and older
|
||||
args = [
|
||||
"osascript",
|
||||
"-e",
|
||||
f'''do shell script "diskutil mount {full_disk_identifier}"'''
|
||||
' with prompt "OpenCore Legacy Patcher needs administrator privileges to mount your EFI."'
|
||||
" with administrator privileges"
|
||||
" without altering line endings",
|
||||
]
|
||||
|
||||
logging.info(f"Mounting partition: {full_disk_identifier}")
|
||||
if self.constants.detected_os >= os_data.os_data.el_capitan and not self.constants.recovery_status:
|
||||
result = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
else:
|
||||
result = subprocess.run(f"diskutil mount {full_disk_identifier}".split(), stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
|
||||
if result.returncode != 0:
|
||||
if "execution error" in result.stderr.decode() and result.stderr.decode().strip()[-5:-1] == "-128":
|
||||
# cancelled prompt
|
||||
try:
|
||||
applescript.AppleScript(f'''do shell script "diskutil mount {full_disk_identifier}" with prompt "OpenCore Legacy Patcher needs administrator privileges to mount this volume." with administrator privileges without altering line endings''').run()
|
||||
except applescript.ScriptError as e:
|
||||
if "User canceled" in str(e):
|
||||
logging.info("Mount cancelled by user")
|
||||
return
|
||||
else:
|
||||
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
|
||||
logging.info(f"An error occurred: {e}")
|
||||
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
|
||||
else:
|
||||
result = subprocess.run(f"diskutil mount {full_disk_identifier}".split(), stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
if result.returncode != 0:
|
||||
logging.info("Mount failed")
|
||||
logging.info(result.stderr.decode())
|
||||
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"]
|
||||
drive_host_info = plistlib.loads(subprocess.run(f"diskutil info -plist {parent_disk}".split(), stdout=subprocess.PIPE).stdout.decode().strip().encode())
|
||||
@@ -128,71 +118,57 @@ class tui_disk_installation:
|
||||
ssd_type = False
|
||||
mount_path = Path(partition_info["MountPoint"])
|
||||
disk_type = partition_info["BusProtocol"]
|
||||
utilities.cls()
|
||||
utilities.header(["Copying OpenCore"])
|
||||
|
||||
if mount_path.exists():
|
||||
if (mount_path / Path("EFI/Microsoft")).exists() and self.constants.gui_mode is False:
|
||||
logging.info("- Found Windows Boot Loader")
|
||||
logging.info("\nWould you like to continue installing OpenCore?")
|
||||
logging.info("Installing OpenCore onto this drive may make Windows unbootable until OpenCore")
|
||||
logging.info("is removed from the partition")
|
||||
logging.info("We highly recommend users partition 200MB off their drive with Disk Utility")
|
||||
logging.info(" Name:\t\t OPENCORE")
|
||||
logging.info(" Format:\t\t FAT32")
|
||||
logging.info(" Size:\t\t 200MB")
|
||||
choice = input("\nWould you like to still install OpenCore to this drive?(y/n): ")
|
||||
if not choice in ["y", "Y", "Yes", "yes"]:
|
||||
subprocess.run(["diskutil", "umount", mount_path], stdout=subprocess.PIPE).stdout.decode().strip().encode()
|
||||
return False
|
||||
if (mount_path / Path("EFI/OC")).exists():
|
||||
logging.info("- Removing preexisting EFI/OC folder")
|
||||
shutil.rmtree(mount_path / Path("EFI/OC"), onerror=rmtree_handler)
|
||||
if (mount_path / Path("System")).exists():
|
||||
logging.info("- Removing preexisting System folder")
|
||||
shutil.rmtree(mount_path / Path("System"), onerror=rmtree_handler)
|
||||
if (mount_path / Path("boot.efi")).exists():
|
||||
logging.info("- Removing preexisting boot.efi")
|
||||
os.remove(mount_path / Path("boot.efi"))
|
||||
logging.info("- Copying OpenCore onto EFI partition")
|
||||
shutil.copytree(self.constants.opencore_release_folder / Path("EFI/OC"), mount_path / Path("EFI/OC"))
|
||||
shutil.copytree(self.constants.opencore_release_folder / Path("System"), mount_path / Path("System"))
|
||||
if Path(self.constants.opencore_release_folder / Path("boot.efi")).exists():
|
||||
shutil.copy(self.constants.opencore_release_folder / Path("boot.efi"), mount_path / Path("boot.efi"))
|
||||
if self.constants.boot_efi is True:
|
||||
logging.info("- Converting Bootstrap to BOOTx64.efi")
|
||||
if (mount_path / Path("EFI/BOOT")).exists():
|
||||
shutil.rmtree(mount_path / Path("EFI/BOOT"), onerror=rmtree_handler)
|
||||
Path(mount_path / Path("EFI/BOOT")).mkdir()
|
||||
shutil.move(mount_path / Path("System/Library/CoreServices/boot.efi"), mount_path / Path("EFI/BOOT/BOOTx64.efi"))
|
||||
shutil.rmtree(mount_path / Path("System"), onerror=rmtree_handler)
|
||||
if determine_sd_card(sd_type) is True:
|
||||
logging.info("- Adding SD Card icon")
|
||||
shutil.copy(self.constants.icon_path_sd, mount_path)
|
||||
elif ssd_type is True:
|
||||
logging.info("- Adding SSD icon")
|
||||
shutil.copy(self.constants.icon_path_ssd, mount_path)
|
||||
elif disk_type == "USB":
|
||||
logging.info("- Adding External USB Drive icon")
|
||||
shutil.copy(self.constants.icon_path_external, mount_path)
|
||||
else:
|
||||
logging.info("- Adding Internal Drive icon")
|
||||
shutil.copy(self.constants.icon_path_internal, mount_path)
|
||||
|
||||
logging.info("- Cleaning install location")
|
||||
if not self.constants.recovery_status:
|
||||
logging.info("- Unmounting EFI partition")
|
||||
subprocess.run(["diskutil", "umount", mount_path], stdout=subprocess.PIPE).stdout.decode().strip().encode()
|
||||
logging.info("- OpenCore transfer complete")
|
||||
if self.constants.gui_mode is False:
|
||||
logging.info("\nPress [Enter] to continue.\n")
|
||||
input()
|
||||
else:
|
||||
if not mount_path.exists():
|
||||
logging.info("EFI failed to mount!")
|
||||
return False
|
||||
return True
|
||||
|
||||
def rmtree_handler(func, path, exc_info):
|
||||
if exc_info[0] == FileNotFoundError:
|
||||
return
|
||||
raise # pylint: disable=misplaced-bare-raise
|
||||
if (mount_path / Path("EFI/OC")).exists():
|
||||
logging.info("Removing preexisting EFI/OC folder")
|
||||
subprocess.run(["rm", "-rf", mount_path / Path("EFI/OC")], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
|
||||
if (mount_path / Path("System")).exists():
|
||||
logging.info("Removing preexisting System folder")
|
||||
subprocess.run(["rm", "-rf", mount_path / Path("System")], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
|
||||
if (mount_path / Path("boot.efi")).exists():
|
||||
logging.info("Removing preexisting boot.efi")
|
||||
subprocess.run(["rm", mount_path / Path("boot.efi")], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
|
||||
logging.info("Copying OpenCore onto EFI partition")
|
||||
subprocess.run(["mkdir", "-p", mount_path / Path("EFI")], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
subprocess.run(["cp", "-r", self.constants.opencore_release_folder / Path("EFI/OC"), mount_path / Path("EFI/OC")], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
subprocess.run(["cp", "-r", self.constants.opencore_release_folder / Path("System"), mount_path / Path("System")], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
|
||||
if Path(self.constants.opencore_release_folder / Path("boot.efi")).exists():
|
||||
subprocess.run(["cp", self.constants.opencore_release_folder / Path("boot.efi"), mount_path / Path("boot.efi")], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
|
||||
if self.constants.boot_efi is True:
|
||||
logging.info("Converting Bootstrap to BOOTx64.efi")
|
||||
if (mount_path / Path("EFI/BOOT")).exists():
|
||||
subprocess.run(["rm", "-rf", mount_path / Path("EFI/BOOT")], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
Path(mount_path / Path("EFI/BOOT")).mkdir()
|
||||
subprocess.run(["mv", mount_path / Path("System/Library/CoreServices/boot.efi"), mount_path / Path("EFI/BOOT/BOOTx64.efi")], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
subprocess.run(["rm", "-rf", mount_path / Path("System")], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
|
||||
if self._determine_sd_card(sd_type) is True:
|
||||
logging.info("Adding SD Card icon")
|
||||
subprocess.run(["cp", self.constants.icon_path_sd, mount_path], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
elif ssd_type is True:
|
||||
logging.info("Adding SSD icon")
|
||||
subprocess.run(["cp", self.constants.icon_path_ssd, mount_path], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
elif disk_type == "USB":
|
||||
logging.info("Adding External USB Drive icon")
|
||||
subprocess.run(["cp", self.constants.icon_path_external, mount_path], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
else:
|
||||
logging.info("Adding Internal Drive icon")
|
||||
subprocess.run(["cp", self.constants.icon_path_internal, mount_path], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
|
||||
logging.info("Cleaning install location")
|
||||
if not self.constants.recovery_status:
|
||||
logging.info("Unmounting EFI partition")
|
||||
subprocess.run(["diskutil", "umount", mount_path], stdout=subprocess.PIPE).stdout.decode().strip().encode()
|
||||
|
||||
logging.info("OpenCore transfer complete")
|
||||
|
||||
return True
|
||||
@@ -100,6 +100,18 @@ class ChunklistVerification:
|
||||
self.status = ChunklistStatus.FAILURE
|
||||
return
|
||||
|
||||
if not Path(self.file_path).exists():
|
||||
self.error_msg = f"File {self.file_path} does not exist"
|
||||
self.status = ChunklistStatus.FAILURE
|
||||
logging.info(self.error_msg)
|
||||
return
|
||||
|
||||
if not Path(self.file_path).is_file():
|
||||
self.error_msg = f"File {self.file_path} is not a file"
|
||||
self.status = ChunklistStatus.FAILURE
|
||||
logging.info(self.error_msg)
|
||||
return
|
||||
|
||||
with self.file_path.open("rb") as f:
|
||||
for chunk in self.chunks:
|
||||
self.current_chunk += 1
|
||||
|
||||
@@ -94,7 +94,7 @@ class KernelDebugKitObject:
|
||||
|
||||
global KDK_ASSET_LIST
|
||||
|
||||
logging.info("- Pulling KDK list from KdkSupportPkg API")
|
||||
logging.info("Pulling KDK list from KdkSupportPkg API")
|
||||
if KDK_ASSET_LIST:
|
||||
return KDK_ASSET_LIST
|
||||
|
||||
@@ -107,11 +107,11 @@ class KernelDebugKitObject:
|
||||
timeout=5
|
||||
)
|
||||
except (requests.exceptions.Timeout, requests.exceptions.TooManyRedirects, requests.exceptions.ConnectionError):
|
||||
logging.info("- Could not contact KDK API")
|
||||
logging.info("Could not contact KDK API")
|
||||
return None
|
||||
|
||||
if results.status_code != 200:
|
||||
logging.info("- Could not fetch KDK list")
|
||||
logging.info("Could not fetch KDK list")
|
||||
return None
|
||||
|
||||
KDK_ASSET_LIST = sorted(results.json(), key=lambda x: (packaging.version.parse(x["version"]), datetime.datetime.fromisoformat(x["date"])), reverse=True)
|
||||
@@ -138,12 +138,12 @@ class KernelDebugKitObject:
|
||||
|
||||
if os_data.os_conversion.os_to_kernel(str(parsed_version.major)) < os_data.os_data.ventura:
|
||||
self.error_msg = "KDKs are not required for macOS Monterey or older"
|
||||
logging.warning(f"- {self.error_msg}")
|
||||
logging.warning(f"{self.error_msg}")
|
||||
return
|
||||
|
||||
self.kdk_installed_path = self._local_kdk_installed()
|
||||
if self.kdk_installed_path:
|
||||
logging.info(f"- KDK already installed ({Path(self.kdk_installed_path).name}), skipping")
|
||||
logging.info(f"KDK already installed ({Path(self.kdk_installed_path).name}), skipping")
|
||||
self.kdk_already_installed = True
|
||||
self.success = True
|
||||
return
|
||||
@@ -151,44 +151,56 @@ class KernelDebugKitObject:
|
||||
remote_kdk_version = self._get_remote_kdks()
|
||||
|
||||
if remote_kdk_version is None:
|
||||
logging.warning("- Failed to fetch KDK list, falling back to local KDK matching")
|
||||
logging.warning("Failed to fetch KDK list, falling back to local KDK matching")
|
||||
|
||||
# First check if a KDK matching the current macOS version is installed
|
||||
# ex. 13.0.1 vs 13.0
|
||||
loose_version = f"{parsed_version.major}.{parsed_version.minor}"
|
||||
logging.info(f"- Checking for KDKs loosely matching {loose_version}")
|
||||
logging.info(f"Checking for KDKs loosely matching {loose_version}")
|
||||
self.kdk_installed_path = self._local_kdk_installed(match=loose_version, check_version=True)
|
||||
if self.kdk_installed_path:
|
||||
logging.info(f"- Found matching KDK: {Path(self.kdk_installed_path).name}")
|
||||
logging.info(f"Found matching KDK: {Path(self.kdk_installed_path).name}")
|
||||
self.kdk_already_installed = True
|
||||
self.success = True
|
||||
return
|
||||
|
||||
older_version = f"{parsed_version.major}.{parsed_version.minor - 1 if parsed_version.minor > 0 else 0}"
|
||||
logging.info(f"- Checking for KDKs matching {older_version}")
|
||||
logging.info(f"Checking for KDKs matching {older_version}")
|
||||
self.kdk_installed_path = self._local_kdk_installed(match=older_version, check_version=True)
|
||||
if self.kdk_installed_path:
|
||||
logging.info(f"- Found matching KDK: {Path(self.kdk_installed_path).name}")
|
||||
logging.info(f"Found matching KDK: {Path(self.kdk_installed_path).name}")
|
||||
self.kdk_already_installed = True
|
||||
self.success = True
|
||||
return
|
||||
|
||||
logging.warning(f"- Couldn't find KDK matching {host_version} or {older_version}, please install one manually")
|
||||
logging.warning(f"Couldn't find KDK matching {host_version} or {older_version}, please install one manually")
|
||||
|
||||
self.error_msg = f"Could not contact KdkSupportPkg API, and no KDK matching {host_version} ({host_build}) or {older_version} was installed.\nPlease ensure you have a network connection or manually install a KDK."
|
||||
|
||||
return
|
||||
|
||||
# First check exact match
|
||||
for kdk in remote_kdk_version:
|
||||
kdk_version = cast(packaging.version.Version, packaging.version.parse(kdk["version"]))
|
||||
if (kdk["build"] == host_build):
|
||||
if (kdk["build"] != host_build):
|
||||
continue
|
||||
self.kdk_url = kdk["url"]
|
||||
self.kdk_url_build = kdk["build"]
|
||||
self.kdk_url_version = kdk["version"]
|
||||
self.kdk_url_expected_size = kdk["fileSize"]
|
||||
self.kdk_url_is_exactly_match = True
|
||||
break
|
||||
if kdk_version <= parsed_version and kdk_version.major == parsed_version.major and (kdk_version.minor in range(parsed_version.minor - 1, parsed_version.minor + 1)):
|
||||
|
||||
# If no exact match, check for closest match
|
||||
if self.kdk_url == "":
|
||||
for kdk in remote_kdk_version:
|
||||
kdk_version = cast(packaging.version.Version, packaging.version.parse(kdk["version"]))
|
||||
if kdk_version > parsed_version:
|
||||
continue
|
||||
if kdk_version.major != parsed_version.major:
|
||||
continue
|
||||
if kdk_version.minor not in range(parsed_version.minor - 1, parsed_version.minor + 1):
|
||||
continue
|
||||
|
||||
# The KDK list is already sorted by version then date, so the first match is the closest
|
||||
self.kdk_closest_match_url = kdk["url"]
|
||||
self.kdk_closest_match_url_build = kdk["build"]
|
||||
@@ -199,29 +211,29 @@ class KernelDebugKitObject:
|
||||
|
||||
if self.kdk_url == "":
|
||||
if self.kdk_closest_match_url == "":
|
||||
logging.warning(f"- No KDKs found for {host_build} ({host_version})")
|
||||
logging.warning(f"No KDKs found for {host_build} ({host_version})")
|
||||
self.error_msg = f"No KDKs found for {host_build} ({host_version})"
|
||||
return
|
||||
logging.info(f"- No direct match found for {host_build}, falling back to closest match")
|
||||
logging.info(f"- Closest Match: {self.kdk_closest_match_url_build} ({self.kdk_closest_match_url_version})")
|
||||
logging.info(f"No direct match found for {host_build}, falling back to closest match")
|
||||
logging.info(f"Closest Match: {self.kdk_closest_match_url_build} ({self.kdk_closest_match_url_version})")
|
||||
|
||||
self.kdk_url = self.kdk_closest_match_url
|
||||
self.kdk_url_build = self.kdk_closest_match_url_build
|
||||
self.kdk_url_version = self.kdk_closest_match_url_version
|
||||
self.kdk_url_expected_size = self.kdk_closest_match_url_expected_size
|
||||
else:
|
||||
logging.info(f"- Direct match found for {host_build} ({host_version})")
|
||||
logging.info(f"Direct match found for {host_build} ({host_version})")
|
||||
|
||||
|
||||
# Check if this KDK is already installed
|
||||
self.kdk_installed_path = self._local_kdk_installed(match=self.kdk_url_build)
|
||||
if self.kdk_installed_path:
|
||||
logging.info(f"- KDK already installed ({Path(self.kdk_installed_path).name}), skipping")
|
||||
logging.info(f"KDK already installed ({Path(self.kdk_installed_path).name}), skipping")
|
||||
self.kdk_already_installed = True
|
||||
self.success = True
|
||||
return
|
||||
|
||||
logging.info("- Following KDK is recommended:")
|
||||
logging.info("Following KDK is recommended:")
|
||||
logging.info(f"- KDK Build: {self.kdk_url_build}")
|
||||
logging.info(f"- KDK Version: {self.kdk_url_version}")
|
||||
logging.info(f"- KDK URL: {self.kdk_url}")
|
||||
@@ -244,7 +256,7 @@ class KernelDebugKitObject:
|
||||
self.error_msg = ""
|
||||
|
||||
if self.kdk_already_installed:
|
||||
logging.info("- No download required, KDK already installed")
|
||||
logging.info("No download required, KDK already installed")
|
||||
self.success = True
|
||||
return None
|
||||
|
||||
@@ -253,7 +265,7 @@ class KernelDebugKitObject:
|
||||
logging.error(self.error_msg)
|
||||
return None
|
||||
|
||||
logging.info(f"- Returning DownloadObject for KDK: {Path(self.kdk_url).name}")
|
||||
logging.info(f"Returning DownloadObject for KDK: {Path(self.kdk_url).name}")
|
||||
self.success = True
|
||||
|
||||
kdk_download_path = self.constants.kdk_download_path if override_path == "" else Path(override_path)
|
||||
@@ -282,7 +294,7 @@ class KernelDebugKitObject:
|
||||
plist_path.touch()
|
||||
plistlib.dump(kdk_dict, plist_path.open("wb"), sort_keys=False)
|
||||
except Exception as e:
|
||||
logging.error(f"- Failed to generate KDK Info.plist: {e}")
|
||||
logging.error(f"Failed to generate KDK Info.plist: {e}")
|
||||
|
||||
|
||||
def _local_kdk_valid(self, kdk_path: Path) -> bool:
|
||||
@@ -302,14 +314,14 @@ class KernelDebugKitObject:
|
||||
"""
|
||||
|
||||
if not Path(f"{kdk_path}/System/Library/CoreServices/SystemVersion.plist").exists():
|
||||
logging.info(f"- Corrupted KDK found ({kdk_path.name}), removing due to missing SystemVersion.plist")
|
||||
logging.info(f"Corrupted KDK found ({kdk_path.name}), removing due to missing SystemVersion.plist")
|
||||
self._remove_kdk(kdk_path)
|
||||
return False
|
||||
|
||||
# Get build from KDK
|
||||
kdk_plist_data = plistlib.load(Path(f"{kdk_path}/System/Library/CoreServices/SystemVersion.plist").open("rb"))
|
||||
if "ProductBuildVersion" not in kdk_plist_data:
|
||||
logging.info(f"- Corrupted KDK found ({kdk_path.name}), removing due to missing ProductBuildVersion")
|
||||
logging.info(f"Corrupted KDK found ({kdk_path.name}), removing due to missing ProductBuildVersion")
|
||||
self._remove_kdk(kdk_path)
|
||||
return False
|
||||
|
||||
@@ -319,7 +331,7 @@ class KernelDebugKitObject:
|
||||
result = subprocess.run(["pkgutil", "--files", f"com.apple.pkg.KDK.{kdk_build}"], capture_output=True)
|
||||
if result.returncode != 0:
|
||||
# If pkg receipt is missing, we'll fallback to legacy validation
|
||||
logging.info(f"- pkg receipt missing for {kdk_path.name}, falling back to legacy validation")
|
||||
logging.info(f"pkg receipt missing for {kdk_path.name}, falling back to legacy validation")
|
||||
return self._local_kdk_valid_legacy(kdk_path)
|
||||
|
||||
# Go through each line of the pkg receipt and ensure it exists
|
||||
@@ -327,7 +339,7 @@ class KernelDebugKitObject:
|
||||
if not line.startswith("System/Library/Extensions"):
|
||||
continue
|
||||
if not Path(f"{kdk_path}/{line}").exists():
|
||||
logging.info(f"- Corrupted KDK found ({kdk_path.name}), removing due to missing file: {line}")
|
||||
logging.info(f"Corrupted KDK found ({kdk_path.name}), removing due to missing file: {line}")
|
||||
self._remove_kdk(kdk_path)
|
||||
return False
|
||||
|
||||
@@ -356,7 +368,7 @@ class KernelDebugKitObject:
|
||||
|
||||
for kext in KEXT_CATALOG:
|
||||
if not Path(f"{kdk_path}/System/Library/Extensions/{kext}").exists():
|
||||
logging.info(f"- Corrupted KDK found, removing due to missing: {kdk_path}/System/Library/Extensions/{kext}")
|
||||
logging.info(f"Corrupted KDK found, removing due to missing: {kdk_path}/System/Library/Extensions/{kext}")
|
||||
self._remove_kdk(kdk_path)
|
||||
return False
|
||||
|
||||
@@ -415,15 +427,15 @@ class KernelDebugKitObject:
|
||||
if not kdk_pkg.name.endswith(f"{match}.pkg"):
|
||||
continue
|
||||
|
||||
logging.info(f"- Found KDK backup: {kdk_pkg.name}")
|
||||
logging.info(f"Found KDK backup: {kdk_pkg.name}")
|
||||
if self.passive is False:
|
||||
logging.info("- Attempting KDK restoration")
|
||||
logging.info("Attempting KDK restoration")
|
||||
if KernelDebugKitUtilities().install_kdk_pkg(kdk_pkg):
|
||||
logging.info("- Successfully restored KDK")
|
||||
logging.info("Successfully restored KDK")
|
||||
return self._local_kdk_installed(match=match, check_version=check_version)
|
||||
else:
|
||||
# When in passive mode, we're just checking if a KDK could be restored
|
||||
logging.info("- KDK restoration skipped, running in passive mode")
|
||||
logging.info("KDK restoration skipped, running in passive mode")
|
||||
return kdk_pkg
|
||||
|
||||
return None
|
||||
@@ -441,22 +453,22 @@ class KernelDebugKitObject:
|
||||
return
|
||||
|
||||
if os.getuid() != 0:
|
||||
logging.warning("- Cannot remove KDK, not running as root")
|
||||
logging.warning("Cannot remove KDK, not running as root")
|
||||
return
|
||||
|
||||
if not Path(kdk_path).exists():
|
||||
logging.warning(f"- KDK does not exist: {kdk_path}")
|
||||
logging.warning(f"KDK does not exist: {kdk_path}")
|
||||
return
|
||||
|
||||
rm_args = ["rm", "-rf" if Path(kdk_path).is_dir() else "-f", kdk_path]
|
||||
|
||||
result = utilities.elevated(rm_args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
if result.returncode != 0:
|
||||
logging.warning(f"- Failed to remove KDK: {kdk_path}")
|
||||
logging.warning(f"- {result.stdout.decode('utf-8')}")
|
||||
logging.warning(f"Failed to remove KDK: {kdk_path}")
|
||||
logging.warning(f"{result.stdout.decode('utf-8')}")
|
||||
return
|
||||
|
||||
logging.info(f"- Successfully removed KDK: {kdk_path}")
|
||||
logging.info(f"Successfully removed KDK: {kdk_path}")
|
||||
|
||||
|
||||
def _remove_unused_kdks(self, exclude_builds: list = None) -> None:
|
||||
@@ -483,7 +495,7 @@ class KernelDebugKitObject:
|
||||
if not Path(KDK_INSTALL_PATH).exists():
|
||||
return
|
||||
|
||||
logging.info("- Cleaning unused KDKs")
|
||||
logging.info("Cleaning unused KDKs")
|
||||
for kdk_folder in Path(KDK_INSTALL_PATH).iterdir():
|
||||
if kdk_folder.name.endswith(".kdk") or kdk_folder.name.endswith(".pkg"):
|
||||
should_remove = True
|
||||
@@ -520,16 +532,18 @@ class KernelDebugKitObject:
|
||||
# TODO: should we use the checksum from the API?
|
||||
result = subprocess.run(["hdiutil", "verify", self.constants.kdk_download_path], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
if result.returncode != 0:
|
||||
logging.info("- Error: Kernel Debug Kit checksum verification failed!")
|
||||
logging.info(f"- Output: {result.stderr.decode('utf-8')}")
|
||||
logging.info("Error: Kernel Debug Kit checksum verification failed!")
|
||||
logging.info(f"Output: {result.stderr.decode('utf-8')}")
|
||||
msg = "Kernel Debug Kit checksum verification failed, please try again.\n\nIf this continues to fail, ensure you're downloading on a stable network connection (ie. Ethernet)"
|
||||
logging.info(f"- {msg}")
|
||||
logging.info(f"{msg}")
|
||||
|
||||
self.error_msg = msg
|
||||
return False
|
||||
|
||||
self._remove_unused_kdks()
|
||||
|
||||
self.success = True
|
||||
logging.info("Kernel Debug Kit checksum verified")
|
||||
return True
|
||||
|
||||
|
||||
class KernelDebugKitUtilities:
|
||||
@@ -554,17 +568,17 @@ class KernelDebugKitUtilities:
|
||||
"""
|
||||
|
||||
if os.getuid() != 0:
|
||||
logging.warning("- Cannot install KDK, not running as root")
|
||||
logging.warning("Cannot install KDK, not running as root")
|
||||
return False
|
||||
|
||||
logging.info(f"- Installing KDK package: {kdk_path.name}")
|
||||
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:")
|
||||
logging.info("Failed to install KDK:")
|
||||
logging.info(result.stdout.decode('utf-8'))
|
||||
if result.stderr:
|
||||
logging.info(result.stderr.decode('utf-8'))
|
||||
@@ -585,21 +599,21 @@ class KernelDebugKitUtilities:
|
||||
"""
|
||||
|
||||
if os.getuid() != 0:
|
||||
logging.warning("- Cannot install KDK, not running as root")
|
||||
logging.warning("Cannot install KDK, not running as root")
|
||||
return False
|
||||
|
||||
logging.info(f"- Extracting downloaded KDK disk image")
|
||||
logging.info(f"Extracting downloaded KDK disk image")
|
||||
with tempfile.TemporaryDirectory() as mount_point:
|
||||
result = subprocess.run(["hdiutil", "attach", kdk_path, "-mountpoint", mount_point, "-nobrowse"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
if result.returncode != 0:
|
||||
logging.info("- Failed to mount KDK:")
|
||||
logging.info("Failed to mount KDK:")
|
||||
logging.info(result.stdout.decode('utf-8'))
|
||||
return False
|
||||
|
||||
kdk_pkg_path = Path(f"{mount_point}/KernelDebugKit.pkg")
|
||||
|
||||
if not kdk_pkg_path.exists():
|
||||
logging.warning("- Failed to find KDK package in DMG, likely corrupted!!!")
|
||||
logging.warning("Failed to find KDK package in DMG, likely corrupted!!!")
|
||||
self._unmount_disk_image(mount_point)
|
||||
return False
|
||||
|
||||
@@ -610,7 +624,7 @@ class KernelDebugKitUtilities:
|
||||
self._create_backup(kdk_pkg_path, Path(f"{kdk_path.parent}/{KDK_INFO_PLIST}"))
|
||||
self._unmount_disk_image(mount_point)
|
||||
|
||||
logging.info("- Successfully installed KDK")
|
||||
logging.info("Successfully installed KDK")
|
||||
return True
|
||||
|
||||
def _unmount_disk_image(self, mount_point) -> None:
|
||||
@@ -633,31 +647,31 @@ class KernelDebugKitUtilities:
|
||||
"""
|
||||
|
||||
if not kdk_path.exists():
|
||||
logging.warning("- KDK does not exist, cannot create backup")
|
||||
logging.warning("KDK does not exist, cannot create backup")
|
||||
return
|
||||
if not kdk_info_plist.exists():
|
||||
logging.warning("- KDK Info.plist does not exist, cannot create backup")
|
||||
logging.warning("KDK Info.plist does not exist, cannot create backup")
|
||||
return
|
||||
|
||||
kdk_info_dict = plistlib.load(kdk_info_plist.open("rb"))
|
||||
|
||||
if 'version' not in kdk_info_dict or 'build' not in kdk_info_dict:
|
||||
logging.warning("- Malformed KDK Info.plist provided, cannot create backup")
|
||||
logging.warning("Malformed KDK Info.plist provided, cannot create backup")
|
||||
return
|
||||
|
||||
if os.getuid() != 0:
|
||||
logging.warning("- Cannot create KDK backup, not running as root")
|
||||
logging.warning("Cannot create KDK backup, not running as root")
|
||||
return
|
||||
|
||||
kdk_dst_name = f"KDK_{kdk_info_dict['version']}_{kdk_info_dict['build']}.pkg"
|
||||
kdk_dst_path = Path(f"{KDK_INSTALL_PATH}/{kdk_dst_name}")
|
||||
|
||||
logging.info(f"- Creating backup: {kdk_dst_name}")
|
||||
logging.info(f"Creating backup: {kdk_dst_name}")
|
||||
if kdk_dst_path.exists():
|
||||
logging.info("- Backup already exists, skipping")
|
||||
logging.info("Backup already exists, skipping")
|
||||
return
|
||||
|
||||
result = utilities.elevated(["cp", "-R", kdk_path, kdk_dst_path], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
if result.returncode != 0:
|
||||
logging.info("- Failed to create KDK backup:")
|
||||
logging.info("Failed to create KDK backup:")
|
||||
logging.info(result.stdout.decode('utf-8'))
|
||||
@@ -1,9 +1,16 @@
|
||||
import logging
|
||||
import sys
|
||||
import threading
|
||||
import os
|
||||
import sys
|
||||
import pprint
|
||||
import logging
|
||||
import threading
|
||||
import traceback
|
||||
import subprocess
|
||||
import applescript
|
||||
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
from resources import constants, analytics_handler, global_settings
|
||||
|
||||
|
||||
class InitializeLoggingSupport:
|
||||
@@ -26,25 +33,26 @@ class InitializeLoggingSupport:
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.log_filename: str = "OpenCore-Patcher.log"
|
||||
def __init__(self, global_constants: constants.Constants) -> None:
|
||||
self.constants: constants.Constants = global_constants
|
||||
|
||||
log_time = datetime.now().strftime("%Y-%m-%d_%H-%M-%S-%f")
|
||||
|
||||
self.log_filename: str = f"OpenCore-Patcher_{self.constants.patcher_version}_{log_time}.log"
|
||||
self.log_filepath: Path = None
|
||||
|
||||
self.original_excepthook: sys = sys.excepthook
|
||||
self.original_thread_excepthook: threading = threading.excepthook
|
||||
|
||||
self.max_file_size: int = 1024 * 1024 * 10 # 10 MB
|
||||
self.file_size_redline: int = 1024 * 1024 * 9 # 9 MB, when to start cleaning log file
|
||||
self.max_file_size: int = 1024 * 1024 # 1 MB
|
||||
self.file_size_redline: int = 1024 * 1024 - 1024 * 100 # 900 KB, when to start cleaning log file
|
||||
|
||||
self._initialize_logging_path()
|
||||
self._clean_log_file()
|
||||
self._attempt_initialize_logging_configuration()
|
||||
self._start_logging()
|
||||
self._implement_custom_traceback_handler()
|
||||
self._fix_file_permission()
|
||||
|
||||
|
||||
def __del__(self) -> None:
|
||||
self._restore_original_excepthook()
|
||||
self._clean_prior_version_logs()
|
||||
|
||||
|
||||
def _initialize_logging_path(self) -> None:
|
||||
@@ -52,39 +60,57 @@ class InitializeLoggingSupport:
|
||||
Initialize logging framework storage path
|
||||
"""
|
||||
|
||||
self.log_filepath = Path(f"~/Library/Logs/{self.log_filename}").expanduser()
|
||||
|
||||
if not self.log_filepath.parent.exists():
|
||||
base_path = Path("~/Library/Logs").expanduser()
|
||||
if not base_path.exists():
|
||||
# Likely in an installer environment, store in /Users/Shared
|
||||
self.log_filepath = Path("/Users/Shared") / self.log_filename
|
||||
|
||||
print("- Initializing logging framework...")
|
||||
print(f" - Log file: {self.log_filepath}")
|
||||
|
||||
|
||||
def _clean_log_file(self) -> None:
|
||||
"""
|
||||
Determine if log file should be cleaned
|
||||
|
||||
We check if we're near the max file size, and if so, we clean the log file
|
||||
"""
|
||||
|
||||
if not self.log_filepath.exists():
|
||||
return
|
||||
|
||||
if self.log_filepath.stat().st_size < self.file_size_redline:
|
||||
return
|
||||
|
||||
# Check if backup log file exists
|
||||
backup_log_filepath = self.log_filepath.with_suffix(".old.log")
|
||||
base_path = Path("/Users/Shared")
|
||||
else:
|
||||
# create Dortania folder if it doesn't exist
|
||||
base_path = base_path / "Dortania"
|
||||
if not base_path.exists():
|
||||
try:
|
||||
if backup_log_filepath.exists():
|
||||
backup_log_filepath.unlink()
|
||||
|
||||
# Rename current log file to backup log file
|
||||
self.log_filepath.rename(backup_log_filepath)
|
||||
base_path.mkdir()
|
||||
except Exception as e:
|
||||
print(f"- Failed to clean log file: {e}")
|
||||
logging.error(f"Failed to create Dortania folder: {e}")
|
||||
base_path = Path("/Users/Shared")
|
||||
|
||||
self.log_filepath = Path(f"{base_path}/{self.log_filename}").expanduser()
|
||||
self.constants.log_filepath = self.log_filepath
|
||||
|
||||
def _clean_prior_version_logs(self) -> None:
|
||||
"""
|
||||
Clean logs from old Patcher versions
|
||||
|
||||
Keep 10 latest logs
|
||||
"""
|
||||
|
||||
paths = [
|
||||
self.log_filepath.parent, # ~/Library/Logs/Dortania
|
||||
self.log_filepath.parent.parent, # ~/Library/Logs (old location)
|
||||
]
|
||||
|
||||
logs = []
|
||||
|
||||
for path in paths:
|
||||
for file in path.glob("OpenCore-Patcher*"):
|
||||
if not file.is_file():
|
||||
continue
|
||||
|
||||
if not file.name.endswith(".log"):
|
||||
continue
|
||||
|
||||
if file.name == self.log_filename:
|
||||
continue
|
||||
|
||||
logs.append(file)
|
||||
|
||||
logs.sort(key=lambda x: x.stat().st_mtime, reverse=True)
|
||||
|
||||
for log in logs[9:]:
|
||||
try:
|
||||
log.unlink()
|
||||
except Exception as e:
|
||||
logging.error(f"Failed to delete log file: {e}")
|
||||
|
||||
|
||||
def _fix_file_permission(self) -> None:
|
||||
@@ -98,11 +124,21 @@ class InitializeLoggingSupport:
|
||||
if os.geteuid() != 0:
|
||||
return
|
||||
|
||||
result = subprocess.run(["chmod", "777", self.log_filepath], capture_output=True)
|
||||
paths = [
|
||||
self.log_filepath, # ~/Library/Logs/Dortania/OpenCore-Patcher_{version}_{date}.log
|
||||
self.log_filepath.parent, # ~/Library/Logs/Dortania
|
||||
]
|
||||
|
||||
for path in paths:
|
||||
result = subprocess.run(["chmod", "777", path], capture_output=True)
|
||||
if result.returncode != 0:
|
||||
print(f"- Failed to fix log file permissions")
|
||||
logging.error(f"Failed to fix log file permissions")
|
||||
if result.stdout:
|
||||
logging.error("STDOUT:")
|
||||
logging.error(result.stdout.decode("utf-8"))
|
||||
if result.stderr:
|
||||
print(result.stderr.decode("utf-8"))
|
||||
logging.error("STDERR:")
|
||||
logging.error(result.stderr.decode("utf-8"))
|
||||
|
||||
|
||||
def _initialize_logging_configuration(self, log_to_file: bool = True) -> None:
|
||||
@@ -119,7 +155,7 @@ class InitializeLoggingSupport:
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.NOTSET,
|
||||
format="%(asctime)s - %(filename)s (%(lineno)d): %(message)s",
|
||||
format="[%(asctime)s] [%(filename)-32s] [%(lineno)-4d]: %(message)s",
|
||||
handlers=[
|
||||
logging.StreamHandler(stream = sys.stdout),
|
||||
logging.FileHandler(self.log_filepath) if log_to_file is True else logging.NullHandler()
|
||||
@@ -140,11 +176,32 @@ class InitializeLoggingSupport:
|
||||
try:
|
||||
self._initialize_logging_configuration()
|
||||
except Exception as e:
|
||||
print(f"- Failed to initialize logging framework: {e}")
|
||||
print("- Retrying without logging to file...")
|
||||
print(f"Failed to initialize logging framework: {e}")
|
||||
print("Retrying without logging to file...")
|
||||
self._initialize_logging_configuration(log_to_file=False)
|
||||
|
||||
|
||||
def _start_logging(self):
|
||||
"""
|
||||
Start logging, used as easily identifiable start point in logs
|
||||
"""
|
||||
|
||||
str_msg = f"# OpenCore Legacy Patcher ({self.constants.patcher_version}) #"
|
||||
str_len = len(str_msg)
|
||||
|
||||
logging.info('#' * str_len)
|
||||
logging.info(str_msg)
|
||||
logging.info('#' * str_len)
|
||||
|
||||
logging.info("Log file set:")
|
||||
# Display relative path to avoid disclosing user's username
|
||||
try:
|
||||
path = self.log_filepath.relative_to(Path.home())
|
||||
logging.info(f"~/{path}")
|
||||
except ValueError:
|
||||
logging.info(self.log_filepath)
|
||||
|
||||
|
||||
def _implement_custom_traceback_handler(self) -> None:
|
||||
"""
|
||||
Reroute traceback to logging module
|
||||
@@ -155,6 +212,49 @@ class InitializeLoggingSupport:
|
||||
Reroute traceback in main thread to logging module
|
||||
"""
|
||||
logging.error("Uncaught exception in main thread", exc_info=(type, value, tb))
|
||||
self._display_debug_properties()
|
||||
|
||||
if "wx/" in "".join(traceback.format_exception(type, value, tb)):
|
||||
# Likely a GUI error, don't display error dialog
|
||||
return
|
||||
|
||||
if self.constants.cli_mode is True:
|
||||
threading.Thread(target=analytics_handler.Analytics(self.constants).send_crash_report, args=(self.log_filepath,)).start()
|
||||
return
|
||||
|
||||
error_msg = f"OpenCore Legacy Patcher encountered the following internal error:\n\n"
|
||||
error_msg += f"{type.__name__}: {value}"
|
||||
if tb:
|
||||
error_msg += f"\n\n{traceback.extract_tb(tb)[-1]}"
|
||||
|
||||
cant_log: bool = global_settings.GlobalEnviromentSettings().read_property("DisableCrashAndAnalyticsReporting")
|
||||
if not isinstance(cant_log, bool):
|
||||
cant_log = False
|
||||
|
||||
if self.constants.commit_info[0].startswith("refs/tags"):
|
||||
cant_log = True
|
||||
|
||||
if cant_log is True:
|
||||
error_msg += "\n\nReveal log file?"
|
||||
else:
|
||||
error_msg += "\n\nSend crash report to Dortania?"
|
||||
|
||||
# Ask user if they want to send crash report
|
||||
try:
|
||||
result = applescript.AppleScript(f'display dialog "{error_msg}" with title "OpenCore Legacy Patcher ({self.constants.patcher_version})" buttons {{"Yes", "No"}} default button "Yes" with icon caution').run()
|
||||
except Exception as e:
|
||||
logging.error(f"Failed to display crash report dialog: {e}")
|
||||
return
|
||||
|
||||
if result[applescript.AEType(b'bhit')] != "Yes":
|
||||
return
|
||||
|
||||
if cant_log is True:
|
||||
subprocess.run(["open", "--reveal", self.log_filepath])
|
||||
return
|
||||
|
||||
threading.Thread(target=analytics_handler.Analytics(self.constants).send_crash_report, args=(self.log_filepath,)).start()
|
||||
|
||||
|
||||
def custom_thread_excepthook(args) -> None:
|
||||
"""
|
||||
@@ -173,3 +273,23 @@ class InitializeLoggingSupport:
|
||||
|
||||
sys.excepthook = self.original_excepthook
|
||||
threading.excepthook = self.original_thread_excepthook
|
||||
|
||||
|
||||
def _display_debug_properties(self) -> None:
|
||||
"""
|
||||
Display debug properties, primarily after main thread crash
|
||||
"""
|
||||
logging.info("Host Properties:")
|
||||
logging.info(f" XNU Version: {self.constants.detected_os}.{self.constants.detected_os_minor}")
|
||||
logging.info(f" XNU Build: {self.constants.detected_os_build}")
|
||||
logging.info(f" macOS Version: {self.constants.detected_os_version}")
|
||||
logging.info("Debug Properties:")
|
||||
logging.info(f" Effective User ID: {os.geteuid()}")
|
||||
logging.info(f" Effective Group ID: {os.getegid()}")
|
||||
logging.info(f" Real User ID: {os.getuid()}")
|
||||
logging.info(f" Real Group ID: {os.getgid()}")
|
||||
logging.info(" Arguments passed to Patcher:")
|
||||
for arg in sys.argv:
|
||||
logging.info(f" {arg}")
|
||||
|
||||
logging.info(f"Host Properties:\n{pprint.pformat(self.constants.computer.__dict__, indent=4)}")
|
||||
|
||||
@@ -6,6 +6,7 @@ import subprocess
|
||||
import tempfile
|
||||
import enum
|
||||
import logging
|
||||
import applescript
|
||||
|
||||
from data import os_data
|
||||
from resources import network_handler, utilities
|
||||
@@ -52,23 +53,20 @@ class InstallerCreation():
|
||||
bool: True if successful, False otherwise
|
||||
"""
|
||||
|
||||
logging.info("- Extracting macOS installer from InstallAssistant.pkg\n This may take some time")
|
||||
args = [
|
||||
"osascript",
|
||||
"-e",
|
||||
logging.info("Extracting macOS installer from InstallAssistant.pkg")
|
||||
try:
|
||||
applescript.AppleScript(
|
||||
f'''do shell script "installer -pkg {Path(download_path)}/InstallAssistant.pkg -target /"'''
|
||||
' with prompt "OpenCore Legacy Patcher needs administrator privileges to add InstallAssistant."'
|
||||
' with prompt "OpenCore Legacy Patcher needs administrator privileges to extract the installer."'
|
||||
" with administrator privileges"
|
||||
" without altering line endings",
|
||||
]
|
||||
|
||||
result = subprocess.run(args,stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
if result.returncode != 0:
|
||||
logging.info("- Failed to install InstallAssistant")
|
||||
logging.info(f" Error Code: {result.returncode}")
|
||||
).run()
|
||||
except Exception as e:
|
||||
logging.info("Failed to install InstallAssistant")
|
||||
logging.info(f" Error Code: {e}")
|
||||
return False
|
||||
|
||||
logging.info("- InstallAssistant installed")
|
||||
logging.info("InstallAssistant installed")
|
||||
return True
|
||||
|
||||
|
||||
@@ -203,7 +201,6 @@ fi
|
||||
if not any(all_disks[disk]['removable'] is False for partition in all_disks[disk]):
|
||||
continue
|
||||
|
||||
logging.info(f"disk {disk}: {all_disks[disk]['name']} ({utilities.human_fmt(all_disks[disk]['size'])})")
|
||||
list_disks.update({
|
||||
disk: {
|
||||
"identifier": all_disks[disk]["identifier"],
|
||||
@@ -400,6 +397,7 @@ class RemoteInstallerCatalog:
|
||||
"integrity": integrity,
|
||||
"Source": "Apple Inc.",
|
||||
"Variant": catalog_url,
|
||||
"OS": os_data.os_conversion.os_to_kernel(version)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -481,7 +479,6 @@ class RemoteInstallerCatalog:
|
||||
|
||||
os_builds.append(newest_apps[ia]["Build"])
|
||||
|
||||
# Final passthrough
|
||||
# Remove Betas if there's a non-beta version available
|
||||
for ia in list(newest_apps):
|
||||
if newest_apps[ia]["Variant"] in ["CustomerSeed", "DeveloperSeed", "PublicSeed"]:
|
||||
@@ -490,6 +487,11 @@ class RemoteInstallerCatalog:
|
||||
newest_apps.pop(ia)
|
||||
break
|
||||
|
||||
# Remove unsupported versions (namely 14)
|
||||
for ia in list(newest_apps):
|
||||
if newest_apps[ia]["Version"].split(".")[0] not in supported_versions:
|
||||
newest_apps.pop(ia)
|
||||
|
||||
return newest_apps
|
||||
|
||||
|
||||
@@ -585,6 +587,7 @@ class LocalInstallerCatalog:
|
||||
"Build": app_sdk,
|
||||
"Path": application,
|
||||
"Minimum Host OS": min_required,
|
||||
"OS": kernel
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
# Copyright (C) 2020-2022, Dhinak G, Mykola Grymalyuk
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import logging
|
||||
import threading
|
||||
from pathlib import Path
|
||||
|
||||
from resources.gui import gui_main
|
||||
from resources.wx_gui import gui_entry
|
||||
from resources import (
|
||||
constants,
|
||||
utilities,
|
||||
@@ -27,18 +28,14 @@ class OpenCoreLegacyPatcher:
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
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}...")
|
||||
logging_handler.InitializeLoggingSupport(self.constants)
|
||||
|
||||
self._generate_base_data()
|
||||
|
||||
if utilities.check_cli_args() is None:
|
||||
gui_main.wx_python_gui(self.constants).main_menu(None)
|
||||
gui_entry.EntryPoint(self.constants).start()
|
||||
|
||||
|
||||
def _generate_base_data(self) -> None:
|
||||
@@ -46,6 +43,11 @@ class OpenCoreLegacyPatcher:
|
||||
Generate base data required for the patcher to run
|
||||
"""
|
||||
|
||||
self.constants.wxpython_variant: bool = True
|
||||
|
||||
# Ensure we live after parent process dies (ie. LaunchAgent)
|
||||
os.setpgrp()
|
||||
|
||||
# Generate OS data
|
||||
os_data = os_probe.OSProbe()
|
||||
self.constants.detected_os = os_data.detect_kernel_major()
|
||||
@@ -90,22 +92,18 @@ class OpenCoreLegacyPatcher:
|
||||
|
||||
# Generate defaults
|
||||
defaults.GenerateDefaults(self.computer.real_model, True, self.constants)
|
||||
threading.Thread(target=analytics_handler.Analytics, args=(self.constants,)).start()
|
||||
threading.Thread(target=analytics_handler.Analytics(self.constants).send_analytics).start()
|
||||
|
||||
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")
|
||||
self.constants.cli_mode = False
|
||||
return
|
||||
|
||||
logging.info("- Detected arguments, switching to CLI mode")
|
||||
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"]
|
||||
ignore_args = ["--auto_patch", "--gui_patch", "--gui_unpatch", "--update_installed"]
|
||||
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):
|
||||
|
||||
@@ -9,6 +9,7 @@ import threading
|
||||
import logging
|
||||
import enum
|
||||
import hashlib
|
||||
import atexit
|
||||
from pathlib import Path
|
||||
|
||||
from resources import utilities
|
||||
@@ -203,10 +204,10 @@ class DownloadObject:
|
||||
|
||||
"""
|
||||
self.status = DownloadStatus.DOWNLOADING
|
||||
logging.info(f"- Starting download: {self.filename}")
|
||||
logging.info(f"Starting download: {self.filename}")
|
||||
if spawn_thread:
|
||||
if self.active_thread:
|
||||
logging.error("- Download already in progress")
|
||||
logging.error("Download already in progress")
|
||||
return
|
||||
self.should_checksum = verify_checksum
|
||||
self.active_thread = threading.Thread(target=self._download, args=(display_progress,))
|
||||
@@ -266,8 +267,8 @@ class DownloadObject:
|
||||
else:
|
||||
raise Exception("Content-Length missing from headers")
|
||||
except Exception as e:
|
||||
logging.error(f"- Error determining file size {self.url}: {str(e)}")
|
||||
logging.error("- Assuming file size is 0")
|
||||
logging.error(f"Error determining file size {self.url}: {str(e)}")
|
||||
logging.error("Assuming file size is 0")
|
||||
self.total_file_size = 0.0
|
||||
|
||||
|
||||
@@ -294,17 +295,17 @@ class DownloadObject:
|
||||
|
||||
try:
|
||||
if Path(path).exists():
|
||||
logging.info(f"- Deleting existing file: {path}")
|
||||
logging.info(f"Deleting existing file: {path}")
|
||||
Path(path).unlink()
|
||||
return True
|
||||
|
||||
if not Path(path).parent.exists():
|
||||
logging.info(f"- Creating directory: {Path(path).parent}")
|
||||
logging.info(f"Creating directory: {Path(path).parent}")
|
||||
Path(path).parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
available_space = utilities.get_free_space(Path(path).parent)
|
||||
if self.total_file_size > available_space:
|
||||
msg = f"- Not enough free space to download {self.filename}, need {utilities.human_fmt(self.total_file_size)}, have {utilities.human_fmt(available_space)}"
|
||||
msg = f"Not enough free space to download {self.filename}, need {utilities.human_fmt(self.total_file_size)}, have {utilities.human_fmt(available_space)}"
|
||||
logging.error(msg)
|
||||
raise Exception(msg)
|
||||
|
||||
@@ -312,9 +313,10 @@ class DownloadObject:
|
||||
self.error = True
|
||||
self.error_msg = str(e)
|
||||
self.status = DownloadStatus.ERROR
|
||||
logging.error(f"- Error preparing working directory {path}: {self.error_msg}")
|
||||
logging.error(f"Error preparing working directory {path}: {self.error_msg}")
|
||||
return False
|
||||
|
||||
logging.info(f"- Directory ready: {path}")
|
||||
return True
|
||||
|
||||
|
||||
@@ -340,6 +342,7 @@ class DownloadObject:
|
||||
response = NetworkUtilities().get(self.url, stream=True, timeout=10)
|
||||
|
||||
with open(self.filepath, 'wb') as file:
|
||||
atexit.register(self.stop)
|
||||
for i, chunk in enumerate(response.iter_content(1024 * 1024 * 4)):
|
||||
if self.should_stop:
|
||||
raise Exception("Download stopped")
|
||||
@@ -351,12 +354,12 @@ class DownloadObject:
|
||||
if display_progress and i % 100:
|
||||
# Don't use logging here, as we'll be spamming the log file
|
||||
if self.total_file_size == 0.0:
|
||||
print(f"- Downloaded {utilities.human_fmt(self.downloaded_file_size)} of {self.filename}")
|
||||
print(f"Downloaded {utilities.human_fmt(self.downloaded_file_size)} of {self.filename}")
|
||||
else:
|
||||
print(f"- Downloaded {self.get_percent():.2f}% of {self.filename} ({utilities.human_fmt(self.get_speed())}/s) ({self.get_time_remaining():.2f} seconds remaining)")
|
||||
print(f"Downloaded {self.get_percent():.2f}% of {self.filename} ({utilities.human_fmt(self.get_speed())}/s) ({self.get_time_remaining():.2f} seconds remaining)")
|
||||
self.download_complete = True
|
||||
logging.info(f"- Download complete: {self.filename}")
|
||||
logging.info("- Stats:")
|
||||
logging.info(f"Download complete: {self.filename}")
|
||||
logging.info("Stats:")
|
||||
logging.info(f"- Downloaded size: {utilities.human_fmt(self.downloaded_file_size)}")
|
||||
logging.info(f"- Time elapsed: {(time.time() - self.start_time):.2f} seconds")
|
||||
logging.info(f"- Speed: {utilities.human_fmt(self.downloaded_file_size / (time.time() - self.start_time))}/s")
|
||||
@@ -365,7 +368,7 @@ class DownloadObject:
|
||||
self.error = True
|
||||
self.error_msg = str(e)
|
||||
self.status = DownloadStatus.ERROR
|
||||
logging.error(f"- Error downloading {self.url}: {self.error_msg}")
|
||||
logging.error(f"Error downloading {self.url}: {self.error_msg}")
|
||||
|
||||
self.status = DownloadStatus.COMPLETE
|
||||
utilities.enable_sleep_after_running()
|
||||
@@ -380,7 +383,6 @@ class DownloadObject:
|
||||
"""
|
||||
|
||||
if self.total_file_size == 0.0:
|
||||
logging.error("- File size is 0, cannot calculate percent")
|
||||
return -1
|
||||
return self.downloaded_file_size / self.total_file_size * 100
|
||||
|
||||
@@ -405,9 +407,11 @@ class DownloadObject:
|
||||
"""
|
||||
|
||||
if self.total_file_size == 0.0:
|
||||
logging.error("- File size is 0, cannot calculate time remaining")
|
||||
return -1
|
||||
return (self.total_file_size - self.downloaded_file_size) / self.get_speed()
|
||||
speed = self.get_speed()
|
||||
if speed <= 0:
|
||||
return -1
|
||||
return (self.total_file_size - self.downloaded_file_size) / speed
|
||||
|
||||
|
||||
def get_file_size(self) -> float:
|
||||
|
||||
@@ -28,15 +28,15 @@ class RoutePayloadDiskImage:
|
||||
"""
|
||||
|
||||
if self.constants.wxpython_variant is True and not self.constants.launcher_script:
|
||||
logging.info("- Running in Binary GUI mode, switching to tmp directory")
|
||||
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")
|
||||
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)
|
||||
output = subprocess.run(
|
||||
[
|
||||
"hdiutil", "attach", "-noverify", f"{self.constants.payload_path}.dmg",
|
||||
"hdiutil", "attach", "-noverify", f"{self.constants.payload_path_dmg}",
|
||||
"-mountpoint", Path(self.temp_dir.name / Path("payloads")),
|
||||
"-nobrowse",
|
||||
"-shadow", Path(self.temp_dir.name / Path("payloads_overlay")),
|
||||
@@ -45,17 +45,17 @@ class RoutePayloadDiskImage:
|
||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT
|
||||
)
|
||||
if output.returncode == 0:
|
||||
logging.info("- Mounted payloads.dmg")
|
||||
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)
|
||||
else:
|
||||
logging.info("- Failed to mount payloads.dmg")
|
||||
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) -> None:
|
||||
def _unmount_active_dmgs(self, unmount_all_active: bool = True) -> None:
|
||||
"""
|
||||
Unmounts disk images associated with OCLP
|
||||
|
||||
@@ -70,19 +70,21 @@ class RoutePayloadDiskImage:
|
||||
dmg_info = subprocess.run(["hdiutil", "info", "-plist"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
dmg_info = plistlib.loads(dmg_info.stdout)
|
||||
|
||||
|
||||
for variant in ["Universal-Binaries.dmg", "payloads.dmg"]:
|
||||
for image in dmg_info["images"]:
|
||||
if image["image-path"].endswith("payloads.dmg"):
|
||||
if image["image-path"].endswith(variant):
|
||||
if unmount_all_active is False:
|
||||
# Check that only our personal payloads.dmg is unmounted
|
||||
if "shadow-path" in image:
|
||||
if self.temp_dir.name in image["shadow-path"]:
|
||||
logging.info("- Unmounting personal payloads.dmg")
|
||||
logging.info(f"Unmounting personal {variant}")
|
||||
subprocess.run(
|
||||
["hdiutil", "detach", image["system-entities"][0]["dev-entry"], "-force"],
|
||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT
|
||||
)
|
||||
else:
|
||||
logging.info(f"- Unmounting payloads.dmg at: {image['system-entities'][0]['dev-entry']}")
|
||||
logging.info(f"Unmounting {variant} at: {image['system-entities'][0]['dev-entry']}")
|
||||
subprocess.run(
|
||||
["hdiutil", "detach", image["system-entities"][0]["dev-entry"], "-force"],
|
||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT
|
||||
|
||||
@@ -67,13 +67,6 @@ class PatchSysVolume:
|
||||
|
||||
self.skip_root_kmutil_requirement = self.hardware_details["Settings: Supports Auxiliary Cache"]
|
||||
|
||||
def __del__(self) -> None:
|
||||
"""
|
||||
Ensures that each time we're patching, we're using a clean PatcherSupportPkg folder
|
||||
"""
|
||||
|
||||
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: Path = None, custom_data_mount_path: Path = None) -> None:
|
||||
"""
|
||||
@@ -287,8 +280,6 @@ class PatchSysVolume:
|
||||
if self.needs_kmutil_exemptions is True:
|
||||
logging.info("Note: Apple will require you to open System Preferences -> Security to allow the new kernel extensions to be loaded")
|
||||
self.constants.root_patcher_succeeded = True
|
||||
if self.constants.gui_mode is False:
|
||||
input("\nPress [ENTER] to continue")
|
||||
|
||||
|
||||
def _rebuild_kernel_collection(self) -> bool:
|
||||
@@ -372,8 +363,6 @@ class PatchSysVolume:
|
||||
logging.info(result.stdout.decode())
|
||||
logging.info("")
|
||||
logging.info("\nPlease reboot the machine to avoid potential issues rerunning the patcher")
|
||||
if self.constants.gui_mode is False:
|
||||
input("Press [ENTER] to continue")
|
||||
return False
|
||||
|
||||
if self.skip_root_kmutil_requirement is True:
|
||||
@@ -385,8 +374,6 @@ class PatchSysVolume:
|
||||
logging.info(result.stdout.decode())
|
||||
logging.info("")
|
||||
logging.info("\nPlease reboot the machine to avoid potential issues rerunning the patcher")
|
||||
if self.constants.gui_mode is False:
|
||||
input("Press [ENTER] to continue")
|
||||
return False
|
||||
|
||||
for file in ["KextPolicy", "KextPolicy-shm", "KextPolicy-wal"]:
|
||||
@@ -505,6 +492,8 @@ class PatchSysVolume:
|
||||
if Path(oclp_path).exists():
|
||||
oclp_plist_data = plistlib.load(Path(oclp_path).open("rb"))
|
||||
for key in oclp_plist_data:
|
||||
if isinstance(oclp_plist_data[key], (bool, int)):
|
||||
continue
|
||||
if "Install" not in oclp_plist_data[key]:
|
||||
continue
|
||||
for location in oclp_plist_data[key]["Install"]:
|
||||
@@ -854,10 +843,27 @@ class PatchSysVolume:
|
||||
logging.info("- Local PatcherSupportPkg resources available, continuing...")
|
||||
return True
|
||||
|
||||
if Path(self.constants.payload_local_binaries_root_path_zip).exists():
|
||||
logging.info("- Local PatcherSupportPkg resources available, unzipping...")
|
||||
logging.info("- Unzipping binaries...")
|
||||
utilities.process_status(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))
|
||||
if Path(self.constants.payload_local_binaries_root_path_dmg).exists():
|
||||
logging.info("- Local PatcherSupportPkg resources available, mounting...")
|
||||
|
||||
output = subprocess.run(
|
||||
[
|
||||
"hdiutil", "attach", "-noverify", f"{self.constants.payload_local_binaries_root_path_dmg}",
|
||||
"-mountpoint", Path(self.constants.payload_path / Path("Universal-Binaries")),
|
||||
"-nobrowse",
|
||||
"-shadow", Path(self.constants.payload_path / Path("Universal-Binaries_overlay")),
|
||||
"-passphrase", "password"
|
||||
],
|
||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT
|
||||
)
|
||||
|
||||
if output.returncode != 0:
|
||||
logging.info("- Failed to mount Universal-Binaries.dmg")
|
||||
logging.info(f"Output: {output.stdout.decode()}")
|
||||
logging.info(f"Return Code: {output.returncode}")
|
||||
return False
|
||||
|
||||
logging.info("- Mounted Universal-Binaries.dmg")
|
||||
return True
|
||||
|
||||
logging.info("- PatcherSupportPkg resources missing, Patcher likely corrupted!!!")
|
||||
@@ -875,33 +881,18 @@ class PatchSysVolume:
|
||||
self.patch_set_dictionary = sys_patch_generate.GenerateRootPatchSets(self.computer.real_model, self.constants, self.hardware_details).patchset
|
||||
|
||||
if self.patch_set_dictionary == {}:
|
||||
change_menu = None
|
||||
logging.info("- No Root Patches required for your machine!")
|
||||
if self.constants.gui_mode is False:
|
||||
input("\nPress [ENTER] to return to the main menu: ")
|
||||
elif self.constants.gui_mode is False:
|
||||
change_menu = input("Would you like to continue with Root Volume Patching?(y/n): ")
|
||||
else:
|
||||
change_menu = "y"
|
||||
logging.info("- Continuing root patching")
|
||||
if change_menu in ["y", "Y"]:
|
||||
return
|
||||
|
||||
logging.info("- Verifying whether Root Patching possible")
|
||||
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.constants.gui_mode is False:
|
||||
input("\nPress [ENTER] to return to the main menu")
|
||||
else:
|
||||
logging.info("- Recommend rebooting the machine and trying to patch again")
|
||||
if self.constants.gui_mode is False:
|
||||
input("- Press [ENTER] to exit: ")
|
||||
elif self.constants.gui_mode is False:
|
||||
input("\nPress [ENTER] to return to the main menu: ")
|
||||
|
||||
else:
|
||||
logging.info("- Returning to main menu")
|
||||
|
||||
def start_unpatch(self) -> None:
|
||||
"""
|
||||
@@ -912,11 +903,5 @@ class PatchSysVolume:
|
||||
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:
|
||||
logging.info("- Recommend rebooting the machine and trying to patch again")
|
||||
if self.constants.gui_mode is False:
|
||||
input("- Press [ENTER] to exit: ")
|
||||
elif self.constants.gui_mode is False:
|
||||
input("\nPress [ENTER] to return to the main menu")
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
# Copyright (C) 2022, Mykola Grymalyuk
|
||||
|
||||
import wx
|
||||
import logging
|
||||
import plistlib
|
||||
import subprocess
|
||||
import webbrowser
|
||||
import logging
|
||||
|
||||
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
|
||||
from resources.wx_gui import gui_entry
|
||||
|
||||
|
||||
class AutomaticSysPatch:
|
||||
@@ -42,6 +45,28 @@ class AutomaticSysPatch:
|
||||
logging.info("- Auto Patch option is not supported on TUI, please use GUI")
|
||||
return
|
||||
|
||||
dict = updates.CheckBinaryUpdates(self.constants).check_binary_updates()
|
||||
if dict:
|
||||
for key in dict:
|
||||
version = dict[key]["Version"]
|
||||
logging.info(f"- Found new version: {version}")
|
||||
|
||||
app = wx.App()
|
||||
frame = wx.Frame(None, -1, "OpenCore Legacy Patcher")
|
||||
dialog = wx.MessageDialog(
|
||||
parent=frame,
|
||||
message=f"Current Version: {self.constants.patcher_version}{' (Nightly)' if not self.constants.commit_info[0].startswith('refs/tags') else ''}\nNew version: {version}\nWould you like to update?",
|
||||
caption="Update Available for OpenCore Legacy Patcher!",
|
||||
style=wx.YES_NO | wx.CANCEL | wx.ICON_QUESTION
|
||||
)
|
||||
dialog.SetYesNoCancelLabels("Download and install", "View on Github", "Ignore")
|
||||
response = dialog.ShowModal()
|
||||
if response == wx.ID_YES:
|
||||
gui_entry.EntryPoint(self.constants).start(entry=gui_entry.SupportedEntryPoints.UPDATE_APP)
|
||||
elif response == wx.ID_NO:
|
||||
webbrowser.open(dict[key]["Github Link"])
|
||||
return
|
||||
|
||||
if utilities.check_seal() is True:
|
||||
logging.info("- Detected Snapshot seal intact, detecting patches")
|
||||
patches = sys_patch_detect.DetectRootPatch(self.constants.computer.real_model, self.constants).detect_patch_set()
|
||||
@@ -58,9 +83,8 @@ class AutomaticSysPatch:
|
||||
for patch in patches:
|
||||
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.CheckBinaryUpdates(self.constants).check_binary_updates()
|
||||
if not dict:
|
||||
|
||||
logging.info("- No new binaries found on Github, proceeding with patching")
|
||||
logging.info("- No new binaries found on Github, proceeding with patching")
|
||||
if self.constants.launcher_script is None:
|
||||
args_string = f"'{self.constants.launcher_binary}' --gui_patch"
|
||||
@@ -97,30 +121,6 @@ class AutomaticSysPatch:
|
||||
stderr=subprocess.STDOUT
|
||||
)
|
||||
return
|
||||
else:
|
||||
for key in dict:
|
||||
version = dict[key]["Version"]
|
||||
github_link = dict[key]["Github Link"]
|
||||
logging.info(f"- Found new version: {version}")
|
||||
|
||||
# launch osascript to ask user if they want to apply the update
|
||||
# if yes, open the link in the default browser
|
||||
# we never want to run the root patcher if there are updates available
|
||||
args = [
|
||||
"osascript",
|
||||
"-e",
|
||||
f"""display dialog "OpenCore Legacy Patcher has detected you're running without Root Patches, and would like to install them.\n\nHowever we've detected a new version of OCLP on Github. Would you like to view this?\n\nCurrent Version: {self.constants.patcher_version}\nLatest Version: {version}\n\nNote: After downloading the latest OCLP version, open the app and run the 'Post Install Root Patcher' from the main menu." """
|
||||
f'with icon POSIX file "{self.constants.app_icon_path}"',
|
||||
]
|
||||
output = subprocess.run(
|
||||
args,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT
|
||||
)
|
||||
if output.returncode == 0:
|
||||
webbrowser.open(github_link)
|
||||
|
||||
return
|
||||
else:
|
||||
logging.info("- No patches detected")
|
||||
else:
|
||||
@@ -170,7 +170,7 @@ class AutomaticSysPatch:
|
||||
if output.returncode == 0:
|
||||
logging.info("- Launching GUI's Build/Install menu")
|
||||
self.constants.start_build_install = True
|
||||
gui_main.wx_python_gui(self.constants).main_menu(None)
|
||||
gui_entry.EntryPoint(self.constants).start(entry=gui_entry.SupportedEntryPoints.BUILD_OC)
|
||||
|
||||
return False
|
||||
|
||||
@@ -245,7 +245,7 @@ class AutomaticSysPatch:
|
||||
if output.returncode == 0:
|
||||
logging.info("- Launching GUI's Build/Install menu")
|
||||
self.constants.start_build_install = True
|
||||
gui_main.wx_python_gui(self.constants).main_menu(None)
|
||||
gui_entry.EntryPoint(self.constants).start(entry=gui_entry.SupportedEntryPoints.BUILD_OC)
|
||||
|
||||
except KeyError:
|
||||
logging.info("- Unable to determine if boot disk is removable, skipping prompt")
|
||||
@@ -350,7 +350,11 @@ class AutomaticSysPatch:
|
||||
for kext in Path("/Library/Extensions").glob("*.kext"):
|
||||
if not Path(f"{kext}/Contents/Info.plist").exists():
|
||||
continue
|
||||
try:
|
||||
kext_plist = plistlib.load(open(f"{kext}/Contents/Info.plist", "rb"))
|
||||
except Exception as e:
|
||||
logging.info(f" - Failed to load plist for {kext.name}: {e}")
|
||||
continue
|
||||
if "GPUCompanionBundles" not in kext_plist:
|
||||
continue
|
||||
logging.info(f" - Found kext with GPUCompanionBundles: {kext.name}")
|
||||
|
||||
@@ -92,7 +92,7 @@ class DetectRootPatch:
|
||||
non_metal_os = os_data.os_data.catalina
|
||||
for i, gpu in enumerate(gpus):
|
||||
if gpu.class_code and gpu.class_code != 0xFFFFFFFF:
|
||||
logging.info(f"- Found GPU ({i}): {utilities.friendly_hex(gpu.vendor_id)}:{utilities.friendly_hex(gpu.device_id)}")
|
||||
logging.info(f"Found GPU ({i}): {utilities.friendly_hex(gpu.vendor_id)}:{utilities.friendly_hex(gpu.device_id)}")
|
||||
if gpu.arch in [device_probe.NVIDIA.Archs.Tesla] and self.constants.force_nv_web is False:
|
||||
if self.constants.detected_os > non_metal_os:
|
||||
self.nvidia_tesla = True
|
||||
@@ -579,6 +579,7 @@ class DetectRootPatch:
|
||||
"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: Unsupported Host OS": True if self.constants.detected_os > os_data.os_data.ventura and not Path("~/.dortania_developer").expanduser().exists() else False,
|
||||
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,
|
||||
|
||||
@@ -40,7 +40,7 @@ class GenerateRootPatchSets:
|
||||
|
||||
utilities.cls()
|
||||
|
||||
logging.info("- The following patches will be applied:")
|
||||
logging.info("The following patches will be applied:")
|
||||
|
||||
if self.hardware_details["Graphics: Intel Ironlake"] is True:
|
||||
required_patches.update({"Non-Metal Common": all_hardware_patchset["Graphics"]["Non-Metal Common"]})
|
||||
@@ -52,8 +52,8 @@ class GenerateRootPatchSets:
|
||||
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"]})
|
||||
# Patchset breaks Display Profiles, don't install if primary GPU is AMD
|
||||
if self.constants.computer.real_model not in ["Macmini5,2", "iMac12,1", "iMac12,2"]:
|
||||
# Patchset breaks Display Profiles, don't install if primary GPU is AMD. Give users option to disable patch in settings to restore Display Profiles
|
||||
if self.constants.computer.real_model not in ["Macmini5,2", "iMac12,1", "iMac12,2"] or self.constants.disable_cat_colorsync is False:
|
||||
required_patches.update({"Non-Metal ColorSync Workaround": all_hardware_patchset["Graphics"]["Non-Metal ColorSync Workaround"]})
|
||||
|
||||
if self.hardware_details["Graphics: Intel Ivy Bridge"] is True:
|
||||
|
||||
@@ -42,10 +42,10 @@ class SysPatchHelpers:
|
||||
if self.constants.computer.reported_board_id in self.constants.sandy_board_id_stock:
|
||||
return
|
||||
|
||||
logging.info(f"- Found unsupported Board ID {self.constants.computer.reported_board_id}, performing AppleIntelSNBGraphicsFB bin patching")
|
||||
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}")
|
||||
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())
|
||||
@@ -54,12 +54,12 @@ class SysPatchHelpers:
|
||||
# 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}")
|
||||
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}")
|
||||
logging.info(f"Error: Could not find {path}")
|
||||
raise Exception("Failed to find AppleIntelSNBGraphicsFB.kext, cannot patch!!!")
|
||||
|
||||
with open(path, 'rb') as f:
|
||||
@@ -97,6 +97,7 @@ class SysPatchHelpers:
|
||||
"Commit URL": f"{self.constants.commit_info[2]}",
|
||||
"Kernel Debug Kit Used": f"{kdk_string}",
|
||||
"OS Version": f"{self.constants.detected_os}.{self.constants.detected_os_minor} ({self.constants.detected_os_build})",
|
||||
"Custom Signature": bool(Path(self.constants.payload_local_binaries_root_path / ".signed").exists()),
|
||||
}
|
||||
|
||||
data.update(patchset)
|
||||
@@ -127,7 +128,7 @@ class SysPatchHelpers:
|
||||
if self.constants.detected_os < os_data.os_data.ventura:
|
||||
return
|
||||
|
||||
logging.info("- Disabling WindowServer Caching")
|
||||
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"])
|
||||
# Disable writing to WindowServer folder
|
||||
@@ -149,7 +150,7 @@ class SysPatchHelpers:
|
||||
if self.constants.detected_os < os_data.os_data.ventura:
|
||||
return
|
||||
|
||||
logging.info("- Parsing Notification Centre Widgets")
|
||||
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()
|
||||
|
||||
@@ -209,7 +210,7 @@ class SysPatchHelpers:
|
||||
if self.constants.detected_os < os_data.os_data.big_sur:
|
||||
return
|
||||
|
||||
logging.info("- Installing Kernel Collection syncing utility")
|
||||
logging.info("Installing Kernel Collection syncing utility")
|
||||
result = utilities.elevated([self.constants.rsrrepair_userspace_path, "--install"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
if result.returncode != 0:
|
||||
logging.info(f"- Failed to install RSRRepair: {result.stdout.decode()}")
|
||||
@@ -261,7 +262,7 @@ class SysPatchHelpers:
|
||||
if not file.name.startswith("31001."):
|
||||
continue
|
||||
|
||||
logging.info(f"- Merging GPUCompiler.framework libraries to match binary")
|
||||
logging.info(f"Merging GPUCompiler.framework libraries to match binary")
|
||||
|
||||
src_dir = f"{LIBRARY_DIR}/{file.name}"
|
||||
if not Path(f"{DEST_DIR}/lib").exists():
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
# Check whether new updates are available for OpenCore Legacy Patcher binary
|
||||
# Call check_binary_updates() to determine if any updates are available
|
||||
# Returns dict with Link and Version of the latest binary update if available
|
||||
import requests
|
||||
import logging
|
||||
|
||||
from resources import network_handler, constants
|
||||
@@ -35,6 +34,12 @@ class CheckBinaryUpdates:
|
||||
if local_version is None:
|
||||
local_version = self.binary_version_array
|
||||
|
||||
|
||||
if local_version == remote_version:
|
||||
if not self.constants.commit_info[0].startswith("refs/tags"):
|
||||
# Check for nightly builds
|
||||
return True
|
||||
|
||||
# Pad version numbers to match length (ie. 0.1.0 vs 0.1.0.1)
|
||||
while len(remote_version) > len(local_version):
|
||||
local_version.append(0)
|
||||
@@ -99,6 +104,9 @@ class CheckBinaryUpdates:
|
||||
response = network_handler.NetworkUtilities().get(REPO_LATEST_RELEASE_URL)
|
||||
data_set = response.json()
|
||||
|
||||
if "tag_name" not in data_set:
|
||||
return None
|
||||
|
||||
self.remote_version = data_set["tag_name"]
|
||||
|
||||
self.remote_version_array = self.remote_version.split(".")
|
||||
@@ -108,7 +116,7 @@ class CheckBinaryUpdates:
|
||||
return None
|
||||
|
||||
for asset in data_set["assets"]:
|
||||
logging.info(f"- Found asset: {asset['name']}")
|
||||
logging.info(f"Found asset: {asset['name']}")
|
||||
if self._determine_remote_type(asset["name"]) == self._determine_local_build_type():
|
||||
available_binaries.update({
|
||||
asset['name']: {
|
||||
|
||||
@@ -48,6 +48,44 @@ def human_fmt(num):
|
||||
return "%.1f %s" % (num, "EB")
|
||||
|
||||
|
||||
def seconds_to_readable_time(seconds) -> str:
|
||||
"""
|
||||
Convert seconds to a readable time format
|
||||
|
||||
Parameters:
|
||||
seconds (int | float | str): Seconds to convert
|
||||
|
||||
Returns:
|
||||
str: Readable time format
|
||||
"""
|
||||
seconds = int(seconds)
|
||||
time = ""
|
||||
|
||||
if seconds == 0:
|
||||
return "Almost done"
|
||||
if seconds < 0:
|
||||
return "Indeterminate"
|
||||
|
||||
years, seconds = divmod(seconds, 31536000)
|
||||
days, seconds = divmod(seconds, 86400)
|
||||
hours, seconds = divmod(seconds, 3600)
|
||||
minutes, seconds = divmod(seconds, 60)
|
||||
|
||||
if years > 0:
|
||||
return "Over a year"
|
||||
if days > 0:
|
||||
if days > 31:
|
||||
return "Over a month"
|
||||
time += f"{days}d "
|
||||
if hours > 0:
|
||||
time += f"{hours}h "
|
||||
if minutes > 0:
|
||||
time += f"{minutes}m "
|
||||
if seconds > 0:
|
||||
time += f"{seconds}s"
|
||||
return time
|
||||
|
||||
|
||||
def header(lines):
|
||||
lines = [i for i in lines if i is not None]
|
||||
total_length = len(max(lines, key=len)) + 4
|
||||
@@ -120,7 +158,7 @@ sleep_process = None
|
||||
|
||||
def disable_sleep_while_running():
|
||||
global sleep_process
|
||||
logging.info("- Disabling Idle Sleep")
|
||||
logging.info("Disabling Idle Sleep")
|
||||
if sleep_process is None:
|
||||
# If sleep_process is active, we'll just keep it running
|
||||
sleep_process = subprocess.Popen(["caffeinate", "-d", "-i", "-s"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
@@ -130,7 +168,7 @@ def disable_sleep_while_running():
|
||||
def enable_sleep_after_running():
|
||||
global sleep_process
|
||||
if sleep_process:
|
||||
logging.info("- Re-enabling Idle Sleep")
|
||||
logging.info("Re-enabling Idle Sleep")
|
||||
sleep_process.kill()
|
||||
sleep_process = None
|
||||
|
||||
@@ -356,9 +394,6 @@ def get_firmware_vendor(*, decode: bool = False):
|
||||
value = value.strip("\0")
|
||||
return value
|
||||
|
||||
def dump_constants(constants):
|
||||
with open(os.path.join(os.path.expanduser('~'), 'Desktop', 'internal_data.txt'), 'w') as f:
|
||||
f.write(str(vars(constants)))
|
||||
|
||||
def find_apfs_physical_volume(device):
|
||||
# ex: disk3s1s1
|
||||
@@ -478,7 +513,7 @@ def block_os_updaters():
|
||||
for bad_process in bad_processes:
|
||||
if bad_process in current_process:
|
||||
if pid != "":
|
||||
logging.info(f"- Killing Process: {pid} - {current_process.split('/')[-1]}")
|
||||
logging.info(f"Killing Process: {pid} - {current_process.split('/')[-1]}")
|
||||
subprocess.run(["kill", "-9", pid])
|
||||
break
|
||||
|
||||
@@ -534,6 +569,7 @@ def check_cli_args():
|
||||
parser.add_argument("--gui_patch", help="Starts GUI in Root Patcher", action="store_true", required=False)
|
||||
parser.add_argument("--gui_unpatch", help="Starts GUI in Root Unpatcher", action="store_true", required=False)
|
||||
parser.add_argument("--auto_patch", help="Check if patches are needed and prompt user", action="store_true", required=False)
|
||||
parser.add_argument("--update_installed", help="Prompt user to finish updating via GUI", action="store_true", required=False)
|
||||
|
||||
args = parser.parse_args()
|
||||
if not (args.build or args.patch_sys_vol or args.unpatch_sys_vol or args.validate or args.auto_patch):
|
||||
|
||||
@@ -4,7 +4,7 @@ from pathlib import Path
|
||||
|
||||
from resources.sys_patch import sys_patch_helpers
|
||||
from resources.build import build
|
||||
from resources import constants
|
||||
from resources import constants, network_handler
|
||||
from data import example_data, model_array, sys_patch_dict, os_data
|
||||
|
||||
|
||||
@@ -120,7 +120,7 @@ class PatcherValidation:
|
||||
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}")
|
||||
logging.info(f"Validating against Darwin {major_kernel}.{minor_kernel}")
|
||||
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")
|
||||
|
||||
@@ -133,18 +133,35 @@ class PatcherValidation:
|
||||
Validates sys_patch modules
|
||||
"""
|
||||
|
||||
if Path(self.constants.payload_local_binaries_root_path_zip).exists():
|
||||
if not Path(self.constants.payload_local_binaries_root_path_dmg).exists():
|
||||
dl_obj = network_handler.DownloadObject(f"https://github.com/dortania/PatcherSupportPkg/releases/download/{self.constants.patcher_support_pkg_version}/Universal-Binaries.dmg", self.constants.payload_local_binaries_root_path_dmg)
|
||||
dl_obj.download(spawn_thread=False)
|
||||
if dl_obj.download_complete is False:
|
||||
logging.info("Failed to download Universal-Binaries.dmg")
|
||||
raise Exception("Failed to download Universal-Binaries.dmg")
|
||||
|
||||
logging.info("Validating Root Patch File integrity")
|
||||
if not Path(self.constants.payload_local_binaries_root_path).exists():
|
||||
subprocess.run(
|
||||
output = subprocess.run(
|
||||
[
|
||||
"ditto", "-V", "-x", "-k", "--sequesterRsrc", "--rsrc",
|
||||
self.constants.payload_local_binaries_root_path_zip,
|
||||
self.constants.payload_path
|
||||
"hdiutil", "attach", "-noverify", f"{self.constants.payload_local_binaries_root_path_dmg}",
|
||||
"-mountpoint", Path(self.constants.payload_path / Path("Universal-Binaries")),
|
||||
"-nobrowse",
|
||||
"-shadow", Path(self.constants.payload_path / Path("Universal-Binaries_overlay")),
|
||||
"-passphrase", "password"
|
||||
],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT
|
||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT
|
||||
)
|
||||
|
||||
if output.returncode != 0:
|
||||
logging.info("Failed to mount Universal-Binaries.dmg")
|
||||
logging.info(f"Output: {output.stdout.decode()}")
|
||||
logging.info(f"Return Code: {output.returncode}")
|
||||
|
||||
raise Exception("Failed to mount Universal-Binaries.dmg")
|
||||
|
||||
logging.info("Mounted Universal-Binaries.dmg")
|
||||
|
||||
|
||||
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):
|
||||
self._validate_root_patch_files(supported_os, i)
|
||||
@@ -152,16 +169,28 @@ class PatcherValidation:
|
||||
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
|
||||
# unmount the dmg
|
||||
output = subprocess.run(
|
||||
[
|
||||
"hdiutil", "detach", Path(self.constants.payload_path / Path("Universal-Binaries")),
|
||||
"-force"
|
||||
],
|
||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT
|
||||
)
|
||||
|
||||
if output.returncode != 0:
|
||||
logging.info("Failed to unmount Universal-Binaries.dmg")
|
||||
logging.info(f"Output: {output.stdout.decode()}")
|
||||
logging.info(f"Return Code: {output.returncode}")
|
||||
|
||||
raise Exception("Failed to unmount Universal-Binaries.dmg")
|
||||
|
||||
subprocess.run(
|
||||
[
|
||||
"rm", "-rf", self.constants.payload_local_binaries_root_path
|
||||
"rm", "-f", Path(self.constants.payload_path / Path("Universal-Binaries_overlay"))
|
||||
],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT
|
||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT
|
||||
)
|
||||
else:
|
||||
logging.info("- Skipping Root Patch File integrity validation")
|
||||
|
||||
|
||||
def _validate_configs(self) -> None:
|
||||
|
||||
67
resources/wx_gui/gui_about.py
Normal file
@@ -0,0 +1,67 @@
|
||||
# About frame, just to sat
|
||||
|
||||
import wx
|
||||
import wx.adv
|
||||
import logging
|
||||
|
||||
from resources.wx_gui import gui_support
|
||||
|
||||
from resources import constants
|
||||
|
||||
|
||||
class AboutFrame(wx.Frame):
|
||||
|
||||
def __init__(self, global_constants: constants.Constants) -> None:
|
||||
if wx.FindWindowByName("About"):
|
||||
return
|
||||
|
||||
logging.info("Generating About frame")
|
||||
super(AboutFrame, self).__init__(None, title="About", size=(350, 350), style=wx.DEFAULT_FRAME_STYLE & ~(wx.RESIZE_BORDER | wx.MAXIMIZE_BOX))
|
||||
self.constants: constants.Constants = global_constants
|
||||
self.Centre()
|
||||
self.hyperlink_colour = (25, 179, 231)
|
||||
|
||||
self._generate_elements(self)
|
||||
|
||||
self.Show()
|
||||
|
||||
|
||||
def _generate_elements(self, frame: wx.Frame) -> None:
|
||||
|
||||
# Set title
|
||||
title = wx.StaticText(frame, label="OpenCore Legacy Patcher", pos=(-1, 5))
|
||||
title.SetFont(wx.Font(19, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont"))
|
||||
title.Centre(wx.HORIZONTAL)
|
||||
|
||||
# Set version
|
||||
version = wx.StaticText(frame, label=f"Version: {self.constants.patcher_version}", pos=(-1, title.GetPosition()[1] + title.GetSize()[1] + 5))
|
||||
version.SetFont(wx.Font(11, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont"))
|
||||
version.Centre(wx.HORIZONTAL)
|
||||
|
||||
# Description
|
||||
description = [
|
||||
"Written by a small group of Mac hobbyists who just",
|
||||
"want to keep old machines out of the landfill!",
|
||||
|
||||
]
|
||||
spacer = 5
|
||||
for line in description:
|
||||
desc = wx.StaticText(frame, label=line, pos=(-1, version.GetPosition()[1] + version.GetSize()[1] + 5 + spacer))
|
||||
desc.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont"))
|
||||
desc.Centre(wx.HORIZONTAL)
|
||||
|
||||
spacer += 20
|
||||
|
||||
# Set icon
|
||||
icon_mac = "/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/com.apple.macbook-unibody-plastic.icns"
|
||||
icon_mac = wx.StaticBitmap(frame, bitmap=wx.Bitmap(icon_mac, wx.BITMAP_TYPE_ICON), pos=(5, desc.GetPosition()[1] - 15))
|
||||
icon_mac.SetSize((160, 160))
|
||||
icon_mac.Centre(wx.HORIZONTAL)
|
||||
|
||||
icon_path = str(self.constants.app_icon_path)
|
||||
icon = wx.StaticBitmap(frame, bitmap=wx.Bitmap(icon_path, wx.BITMAP_TYPE_ICON), pos=(5, desc.GetPosition()[1] + desc.GetSize()[1] + 17))
|
||||
icon.SetSize((64, 64))
|
||||
icon.Centre(wx.HORIZONTAL)
|
||||
|
||||
# Set frame size
|
||||
frame.SetSize((-1, icon.GetPosition()[1] + icon.GetSize()[1] + 60))
|
||||
160
resources/wx_gui/gui_build.py
Normal file
@@ -0,0 +1,160 @@
|
||||
# Generate UI for Building OpenCore
|
||||
import wx
|
||||
import logging
|
||||
import threading
|
||||
import traceback
|
||||
|
||||
from resources import constants
|
||||
from resources.build import build
|
||||
from resources.wx_gui import (
|
||||
gui_main_menu,
|
||||
gui_install_oc,
|
||||
gui_support
|
||||
)
|
||||
|
||||
|
||||
class BuildFrame(wx.Frame):
|
||||
"""
|
||||
Create a frame for building OpenCore
|
||||
Uses a Modal Dialog for smoother transition from other frames
|
||||
"""
|
||||
def __init__(self, parent: wx.Frame, title: str, global_constants: constants.Constants, screen_location: tuple = None) -> None:
|
||||
logging.info("Initializing Build Frame")
|
||||
super(BuildFrame, self).__init__(parent, title=title, size=(350, 200), style=wx.DEFAULT_FRAME_STYLE & ~(wx.RESIZE_BORDER | wx.MAXIMIZE_BOX))
|
||||
gui_support.GenerateMenubar(self, global_constants).generate()
|
||||
|
||||
self.install_button: wx.Button = None
|
||||
self.text_box: wx.TextCtrl = None
|
||||
self.frame_modal: wx.Dialog = None
|
||||
|
||||
self.constants: constants.Constants = global_constants
|
||||
self.title: str = title
|
||||
self.stock_output = logging.getLogger().handlers[0].stream
|
||||
|
||||
self.frame_modal = wx.Dialog(self, title=title, size=(400, 200))
|
||||
|
||||
self._generate_elements(self.frame_modal)
|
||||
|
||||
if self.constants.update_stage != gui_support.AutoUpdateStages.INACTIVE:
|
||||
self.constants.update_stage = gui_support.AutoUpdateStages.BUILDING
|
||||
|
||||
self.Centre()
|
||||
self.frame_modal.ShowWindowModal()
|
||||
|
||||
self._invoke_build()
|
||||
|
||||
|
||||
def _generate_elements(self, frame: wx.Frame = None) -> None:
|
||||
"""
|
||||
Generate UI elements for build frame
|
||||
|
||||
Format:
|
||||
- Title label: Build and Install OpenCore
|
||||
- Text: Model: {Build or Host Model}
|
||||
- Button: Install OpenCore
|
||||
- Read-only text box: {empty}
|
||||
- Button: Return to Main Menu
|
||||
"""
|
||||
frame = self if not frame else frame
|
||||
|
||||
title_label = wx.StaticText(frame, label="Build and Install OpenCore", pos=(-1,5))
|
||||
title_label.SetFont(wx.Font(19, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont"))
|
||||
title_label.Centre(wx.HORIZONTAL)
|
||||
|
||||
model_label = wx.StaticText(frame, label=f"Model: {self.constants.custom_model or self.constants.computer.real_model}", pos=(-1,30))
|
||||
model_label.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont"))
|
||||
model_label.Centre(wx.HORIZONTAL)
|
||||
|
||||
# Button: Install OpenCore
|
||||
install_button = wx.Button(frame, label="🔩 Install OpenCore", pos=(-1, model_label.GetPosition()[1] + model_label.GetSize()[1]), size=(150, 30))
|
||||
install_button.Bind(wx.EVT_BUTTON, self.on_install)
|
||||
install_button.Centre(wx.HORIZONTAL)
|
||||
install_button.Disable()
|
||||
self.install_button = install_button
|
||||
|
||||
# Read-only text box: {empty}
|
||||
text_box = wx.TextCtrl(frame, value="", pos=(-1, install_button.GetPosition()[1] + install_button.GetSize()[1] + 10), size=(400, 350), style=wx.TE_READONLY | wx.TE_MULTILINE | wx.TE_RICH2)
|
||||
text_box.Centre(wx.HORIZONTAL)
|
||||
self.text_box = text_box
|
||||
|
||||
# Button: Return to Main Menu
|
||||
return_button = wx.Button(frame, label="Return to Main Menu", pos=(-1, text_box.GetPosition()[1] + text_box.GetSize()[1] + 5), size=(200, 30))
|
||||
return_button.Bind(wx.EVT_BUTTON, self.on_return_to_main_menu)
|
||||
return_button.Centre(wx.HORIZONTAL)
|
||||
return_button.Disable()
|
||||
self.return_button = return_button
|
||||
|
||||
# Adjust window size to fit all elements
|
||||
frame.SetSize((-1, return_button.GetPosition()[1] + return_button.GetSize()[1] + 40))
|
||||
|
||||
|
||||
def _invoke_build(self) -> None:
|
||||
"""
|
||||
Invokes build function and waits for it to finish
|
||||
"""
|
||||
while gui_support.PayloadMount(self.constants, self).is_unpack_finished() is False:
|
||||
wx.Yield()
|
||||
|
||||
thread = threading.Thread(target=self._build)
|
||||
thread.start()
|
||||
|
||||
while thread.is_alive():
|
||||
wx.Yield()
|
||||
|
||||
self.return_button.Enable()
|
||||
dialog = wx.MessageDialog(
|
||||
parent=self,
|
||||
message=f"Would you like to install OpenCore now?",
|
||||
caption="Finished building your OpenCore configuration!",
|
||||
style=wx.YES_NO | wx.ICON_QUESTION
|
||||
)
|
||||
dialog.SetYesNoLabels("Install to disk", "View build log")
|
||||
|
||||
self.on_install() if dialog.ShowModal() == wx.ID_YES else self.install_button.Enable()
|
||||
|
||||
|
||||
def _build(self) -> None:
|
||||
"""
|
||||
Calls build function and redirects stdout to the text box
|
||||
"""
|
||||
logger = logging.getLogger()
|
||||
logger.addHandler(gui_support.ThreadHandler(self.text_box))
|
||||
try:
|
||||
build.BuildOpenCore(self.constants.custom_model or self.constants.computer.real_model, self.constants)
|
||||
except:
|
||||
logging.error("An internal error occurred while building:\n")
|
||||
logging.error(traceback.format_exc())
|
||||
logger.removeHandler(logger.handlers[2])
|
||||
|
||||
|
||||
def on_return_to_main_menu(self, event: wx.Event = None) -> None:
|
||||
"""
|
||||
Return to main menu
|
||||
"""
|
||||
self.frame_modal.Hide()
|
||||
main_menu_frame = gui_main_menu.MainFrame(
|
||||
None,
|
||||
title=self.title,
|
||||
global_constants=self.constants,
|
||||
screen_location=self.GetScreenPosition()
|
||||
)
|
||||
main_menu_frame.Show()
|
||||
self.frame_modal.Destroy()
|
||||
self.Destroy()
|
||||
|
||||
|
||||
def on_install(self, event: wx.Event = None) -> None:
|
||||
"""
|
||||
Launch install frame
|
||||
"""
|
||||
self.frame_modal.Destroy()
|
||||
self.Destroy()
|
||||
install_oc_frame = gui_install_oc.InstallOCFrame(
|
||||
None,
|
||||
title=self.title,
|
||||
global_constants=self.constants,
|
||||
screen_location=self.GetScreenPosition()
|
||||
)
|
||||
install_oc_frame.Show()
|
||||
|
||||
|
||||
104
resources/wx_gui/gui_download.py
Normal file
@@ -0,0 +1,104 @@
|
||||
# Generate UI for downloading files
|
||||
import wx
|
||||
import logging
|
||||
|
||||
from resources import (
|
||||
constants,
|
||||
network_handler,
|
||||
utilities
|
||||
)
|
||||
|
||||
|
||||
class DownloadFrame(wx.Frame):
|
||||
"""
|
||||
Update provided frame with download stats
|
||||
"""
|
||||
def __init__(self, parent: wx.Frame, title: str, global_constants: constants.Constants, download_obj: network_handler.DownloadObject, item_name: str) -> None:
|
||||
logging.info("Initializing Download Frame")
|
||||
self.constants: constants.Constants = global_constants
|
||||
self.title: str = title
|
||||
self.parent: wx.Frame = parent
|
||||
self.download_obj: network_handler.DownloadObject = download_obj
|
||||
self.item_name: str = item_name
|
||||
|
||||
self.user_cancelled: bool = False
|
||||
|
||||
self.frame_modal = wx.Dialog(parent, title=title, size=(400, 200))
|
||||
|
||||
self._generate_elements(self.frame_modal)
|
||||
|
||||
|
||||
def _generate_elements(self, frame: wx.Dialog = None) -> None:
|
||||
"""
|
||||
Generate elements for download frame
|
||||
"""
|
||||
|
||||
frame = self if not frame else frame
|
||||
|
||||
title_label = wx.StaticText(frame, label=f"Downloading: {self.item_name}", pos=(-1,5))
|
||||
title_label.SetFont(wx.Font(19, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont"))
|
||||
title_label.Centre(wx.HORIZONTAL)
|
||||
|
||||
label_amount = wx.StaticText(frame, label="0.00 B downloaded of 0.00B (0.00%)", pos=(-1, title_label.GetPosition()[1] + title_label.GetSize()[1] + 5))
|
||||
label_amount.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont"))
|
||||
label_amount.Centre(wx.HORIZONTAL)
|
||||
|
||||
label_speed = wx.StaticText(frame, label="Average download speed: Unknown", pos=(-1, label_amount.GetPosition()[1] + label_amount.GetSize()[1] + 5))
|
||||
label_speed.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont"))
|
||||
label_speed.Centre(wx.HORIZONTAL)
|
||||
|
||||
label_est_time = wx.StaticText(frame, label="Estimated time remaining: Unknown", pos=(-1, label_speed.GetPosition()[1] + label_speed.GetSize()[1] + 5))
|
||||
label_est_time.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont"))
|
||||
label_est_time.Centre(wx.HORIZONTAL)
|
||||
|
||||
progress_bar = wx.Gauge(frame, range=100, pos=(-1, label_est_time.GetPosition()[1] + label_est_time.GetSize()[1] + 5), size=(300, 20))
|
||||
progress_bar.Centre(wx.HORIZONTAL)
|
||||
|
||||
return_button = wx.Button(frame, label="Return", pos=(-1, progress_bar.GetPosition()[1] + progress_bar.GetSize()[1] + 5))
|
||||
return_button.Bind(wx.EVT_BUTTON, lambda event: self.terminate_download())
|
||||
return_button.Centre(wx.HORIZONTAL)
|
||||
|
||||
# Set size of frame
|
||||
frame.SetSize((-1, return_button.GetPosition()[1] + return_button.GetSize()[1] + 40))
|
||||
frame.ShowWindowModal()
|
||||
|
||||
self.download_obj.download()
|
||||
while self.download_obj.is_active():
|
||||
percentage: int = self.download_obj.get_percent()
|
||||
|
||||
if percentage == -1:
|
||||
amount_str = f"{utilities.human_fmt(self.download_obj.downloaded_file_size)} downloaded"
|
||||
progress_bar.Pulse()
|
||||
else:
|
||||
amount_str = f"{utilities.human_fmt(self.download_obj.downloaded_file_size)} downloaded of {utilities.human_fmt(self.download_obj.total_file_size)} ({percentage:.2f}%)"
|
||||
progress_bar.SetValue(int(percentage))
|
||||
|
||||
label_amount.SetLabel(amount_str)
|
||||
label_amount.Centre(wx.HORIZONTAL)
|
||||
|
||||
label_speed.SetLabel(
|
||||
f"Average download speed: {utilities.human_fmt(self.download_obj.get_speed())}/s"
|
||||
)
|
||||
|
||||
label_est_time.SetLabel(
|
||||
f"Estimated time remaining: {utilities.seconds_to_readable_time(self.download_obj.get_time_remaining())}"
|
||||
)
|
||||
|
||||
wx.Yield()
|
||||
|
||||
if self.download_obj.download_complete is False and self.user_cancelled is False:
|
||||
wx.MessageBox(f"Download failed: \n{self.download_obj.error_msg}", "Error", wx.OK | wx.ICON_ERROR)
|
||||
|
||||
frame.Destroy()
|
||||
|
||||
|
||||
def terminate_download(self) -> None:
|
||||
"""
|
||||
Terminate download
|
||||
"""
|
||||
if wx.MessageBox("Are you sure you want to cancel the download?", "Cancel Download", wx.YES_NO | wx.ICON_QUESTION | wx.NO_DEFAULT) == wx.YES:
|
||||
logging.info("User cancelled download")
|
||||
self.user_cancelled = True
|
||||
self.download_obj.stop()
|
||||
|
||||
|
||||
92
resources/wx_gui/gui_entry.py
Normal file
@@ -0,0 +1,92 @@
|
||||
# Entry point for the wxPython GUI
|
||||
import wx
|
||||
import sys
|
||||
import atexit
|
||||
import logging
|
||||
|
||||
from resources import constants
|
||||
from resources.wx_gui import (
|
||||
gui_main_menu,
|
||||
gui_build,
|
||||
gui_install_oc,
|
||||
gui_sys_patch_start,
|
||||
gui_update,
|
||||
)
|
||||
from resources.sys_patch import sys_patch_detect
|
||||
|
||||
|
||||
class SupportedEntryPoints:
|
||||
"""
|
||||
Enum for supported entry points
|
||||
"""
|
||||
MAIN_MENU = gui_main_menu.MainFrame
|
||||
BUILD_OC = gui_build.BuildFrame
|
||||
INSTALL_OC = gui_install_oc.InstallOCFrame
|
||||
SYS_PATCH = gui_sys_patch_start.SysPatchStartFrame
|
||||
UPDATE_APP = gui_update.UpdateFrame
|
||||
|
||||
|
||||
class EntryPoint:
|
||||
|
||||
def __init__(self, global_constants: constants.Constants) -> None:
|
||||
self.app: wx.App = None
|
||||
self.main_menu_frame: gui_main_menu.MainFrame = None
|
||||
self.constants: constants.Constants = global_constants
|
||||
|
||||
self.constants.gui_mode = True
|
||||
|
||||
|
||||
def _generate_base_data(self) -> None:
|
||||
self.app = wx.App()
|
||||
self.app.SetAppName(self.constants.patcher_name)
|
||||
|
||||
|
||||
def start(self, entry: SupportedEntryPoints = gui_main_menu.MainFrame) -> None:
|
||||
"""
|
||||
Launches entry point for the wxPython GUI
|
||||
"""
|
||||
self._generate_base_data()
|
||||
|
||||
if "--gui_patch" in sys.argv or "--gui_unpatch" in sys.argv:
|
||||
entry = gui_sys_patch_start.SysPatchStartFrame
|
||||
patches = sys_patch_detect.DetectRootPatch(self.constants.computer.real_model, self.constants).detect_patch_set()
|
||||
|
||||
logging.info(f"Entry point set: {entry.__name__}")
|
||||
|
||||
# Normally set by main.py, but transitions from CLI mode may not have this set
|
||||
self.constants.gui_mode = True
|
||||
|
||||
self.frame: wx.Frame = entry(
|
||||
None,
|
||||
title=f"{self.constants.patcher_name} ({self.constants.patcher_version})",
|
||||
global_constants=self.constants,
|
||||
screen_location=None,
|
||||
**({"patches": patches} if "--gui_patch" in sys.argv or "--gui_unpatch" in sys.argv else {})
|
||||
)
|
||||
|
||||
atexit.register(self.OnCloseFrame)
|
||||
|
||||
if "--gui_patch" in sys.argv:
|
||||
self.frame.start_root_patching()
|
||||
elif "--gui_unpatch" in sys.argv:
|
||||
self.frame.revert_root_patching()
|
||||
|
||||
self.app.MainLoop()
|
||||
|
||||
|
||||
def OnCloseFrame(self, event: wx.Event = None) -> None:
|
||||
"""
|
||||
Closes the wxPython GUI
|
||||
"""
|
||||
|
||||
if not self.frame:
|
||||
return
|
||||
|
||||
logging.info("Cleaning up wxPython GUI")
|
||||
|
||||
self.frame.SetTransparent(0)
|
||||
wx.Yield()
|
||||
|
||||
self.frame.DestroyChildren()
|
||||
self.frame.Destroy()
|
||||
self.app.ExitMainLoop()
|
||||
67
resources/wx_gui/gui_help.py
Normal file
@@ -0,0 +1,67 @@
|
||||
# Generate UI for help menu
|
||||
import wx
|
||||
import logging
|
||||
import webbrowser
|
||||
|
||||
from resources import constants
|
||||
|
||||
|
||||
class HelpFrame(wx.Frame):
|
||||
"""
|
||||
Append to main menu through a modal dialog
|
||||
"""
|
||||
def __init__(self, parent: wx.Frame, title: str, global_constants: constants.Constants, screen_location: tuple = None) -> None:
|
||||
logging.info("Initializing Help Frame")
|
||||
self.dialog = wx.Dialog(parent, title=title, size=(300, 200))
|
||||
|
||||
self.constants: constants.Constants = global_constants
|
||||
self.title: str = title
|
||||
|
||||
self._generate_elements(self.dialog)
|
||||
self.dialog.ShowWindowModal()
|
||||
|
||||
|
||||
def _generate_elements(self, frame: wx.Frame = None) -> None:
|
||||
"""
|
||||
Format:
|
||||
- Title: Patcher Resources
|
||||
- Text: Following resources are available:
|
||||
- Button: Official Guide
|
||||
- Button: Community Discord Server
|
||||
- Button: Official Phone Support
|
||||
- Button: Return to Main Menu
|
||||
"""
|
||||
|
||||
frame = self if not frame else frame
|
||||
|
||||
title_label = wx.StaticText(frame, label="Patcher Resources", pos=(-1,5))
|
||||
title_label.SetFont(wx.Font(19, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont"))
|
||||
title_label.Centre(wx.HORIZONTAL)
|
||||
|
||||
text_label = wx.StaticText(frame, label="Following resources are available:", pos=(-1,30))
|
||||
text_label.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont"))
|
||||
text_label.Centre(wx.HORIZONTAL)
|
||||
|
||||
buttons = {
|
||||
"Official Guide": self.constants.guide_link,
|
||||
"Official Phone Support": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
|
||||
"Community Discord Server": self.constants.discord_link,
|
||||
}
|
||||
|
||||
for button in buttons:
|
||||
help_button = wx.Button(frame, label=button, pos=(-1, text_label.GetPosition()[1] + text_label.GetSize()[1] + (list(buttons.keys()).index(button) * 30)), size=(200, 30))
|
||||
help_button.Bind(wx.EVT_BUTTON, lambda event, temp=buttons[button]: webbrowser.open(temp))
|
||||
help_button.Centre(wx.HORIZONTAL)
|
||||
|
||||
# Button: Return to Main Menu
|
||||
return_button = wx.Button(frame, label="Return to Main Menu", pos=(-1, help_button.GetPosition()[1] + help_button.GetSize()[1]), size=(150, 30))
|
||||
return_button.Bind(wx.EVT_BUTTON, lambda event: frame.Close())
|
||||
return_button.Centre(wx.HORIZONTAL)
|
||||
|
||||
# Set size of frame
|
||||
frame.SetSize((-1, return_button.GetPosition()[1] + return_button.GetSize()[1] + 40))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
355
resources/wx_gui/gui_install_oc.py
Normal file
@@ -0,0 +1,355 @@
|
||||
import wx
|
||||
import threading
|
||||
import logging
|
||||
import traceback
|
||||
|
||||
from resources.wx_gui import gui_main_menu, gui_support, gui_sys_patch_display
|
||||
from resources import constants, install
|
||||
from data import os_data
|
||||
|
||||
|
||||
class InstallOCFrame(wx.Frame):
|
||||
"""
|
||||
Create a frame for installing OpenCore to disk
|
||||
"""
|
||||
def __init__(self, parent: wx.Frame, title: str, global_constants: constants.Constants, screen_location: tuple = None):
|
||||
logging.info("Initializing Install OpenCore Frame")
|
||||
super(InstallOCFrame, self).__init__(parent, title=title, size=(300, 120), style=wx.DEFAULT_FRAME_STYLE & ~(wx.RESIZE_BORDER | wx.MAXIMIZE_BOX))
|
||||
gui_support.GenerateMenubar(self, global_constants).generate()
|
||||
|
||||
self.constants: constants.Constants = global_constants
|
||||
self.title: str = title
|
||||
self.result: bool = False
|
||||
|
||||
self.available_disks: dict = None
|
||||
self.stock_output = logging.getLogger().handlers[0].stream
|
||||
|
||||
self.progress_bar_animation: gui_support.GaugePulseCallback = None
|
||||
|
||||
self.hyperlink_colour = (25, 179, 231)
|
||||
|
||||
self._generate_elements()
|
||||
|
||||
if self.constants.update_stage != gui_support.AutoUpdateStages.INACTIVE:
|
||||
self.constants.update_stage = gui_support.AutoUpdateStages.INSTALLING
|
||||
|
||||
self.Centre()
|
||||
self.Show()
|
||||
|
||||
self._display_disks()
|
||||
|
||||
|
||||
def _generate_elements(self) -> None:
|
||||
"""
|
||||
Display indeterminate progress bar while collecting disk information
|
||||
|
||||
Format:
|
||||
- Title label: Install OpenCore
|
||||
- Text: Fetching information on local disks...
|
||||
- Progress bar: {indeterminate}
|
||||
"""
|
||||
|
||||
# Title label: Install OpenCore
|
||||
title_label = wx.StaticText(self, label="Install OpenCore", pos=(-1,5))
|
||||
title_label.SetFont(wx.Font(19, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont"))
|
||||
title_label.Centre(wx.HORIZONTAL)
|
||||
|
||||
# Text: Parsing local disks...
|
||||
text_label = wx.StaticText(self, label="Fetching information on local disks...", pos=(-1,30))
|
||||
text_label.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont"))
|
||||
text_label.Centre(wx.HORIZONTAL)
|
||||
self.text_label = text_label
|
||||
|
||||
# Progress bar: {indeterminate}
|
||||
progress_bar = wx.Gauge(self, range=100, pos=(-1, text_label.GetPosition()[1] + text_label.GetSize()[1]), size=(150, 30), style=wx.GA_HORIZONTAL | wx.GA_SMOOTH)
|
||||
progress_bar.Centre(wx.HORIZONTAL)
|
||||
|
||||
progress_bar_animation = gui_support.GaugePulseCallback(self.constants, progress_bar)
|
||||
progress_bar_animation.start_pulse()
|
||||
|
||||
self.progress_bar_animation = progress_bar_animation
|
||||
self.progress_bar = progress_bar
|
||||
|
||||
|
||||
def _fetch_disks(self) -> None:
|
||||
"""
|
||||
Fetch information on local disks
|
||||
"""
|
||||
self.available_disks = install.tui_disk_installation(self.constants).list_disks()
|
||||
|
||||
# Need to clean up output on pre-Sierra
|
||||
# Disk images are mixed in with regular disks (ex. payloads.dmg)
|
||||
ignore = ["disk image", "read-only", "virtual"]
|
||||
for disk in self.available_disks.copy():
|
||||
if any(string in self.available_disks[disk]['name'].lower() for string in ignore):
|
||||
del self.available_disks[disk]
|
||||
|
||||
|
||||
def _display_disks(self) -> None:
|
||||
"""
|
||||
Display disk selection dialog
|
||||
"""
|
||||
thread = threading.Thread(target=self._fetch_disks)
|
||||
thread.start()
|
||||
|
||||
while thread.is_alive():
|
||||
wx.Yield()
|
||||
continue
|
||||
|
||||
self.progress_bar_animation.stop_pulse()
|
||||
self.progress_bar.Hide()
|
||||
|
||||
# Create wxDialog for disk selection
|
||||
dialog = wx.Dialog(self, title=self.title, size=(380, -1))
|
||||
|
||||
# Title label: Install OpenCore
|
||||
title_label = wx.StaticText(dialog, label="Install OpenCore", pos=(-1,5))
|
||||
title_label.SetFont(wx.Font(19, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont"))
|
||||
title_label.Centre(wx.HORIZONTAL)
|
||||
|
||||
# Text: select disk to install OpenCore onto
|
||||
text_label = wx.StaticText(dialog, label="Select disk to install OpenCore onto:", pos=(-1, title_label.GetPosition()[1] + title_label.GetSize()[1] + 5))
|
||||
text_label.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont"))
|
||||
text_label.Centre(wx.HORIZONTAL)
|
||||
|
||||
# Add note: "Missing disks? Ensure they're FAT32 or formatted as GUID/GPT"
|
||||
gpt_note = wx.StaticText(dialog, label="Missing disks? Ensure they're FAT32 or formatted as GUID/GPT", pos=(-1, text_label.GetPosition()[1] + text_label.GetSize()[1] + 5))
|
||||
gpt_note.SetFont(wx.Font(10, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont"))
|
||||
gpt_note.Centre(wx.HORIZONTAL)
|
||||
|
||||
# Add buttons for each disk
|
||||
if self.available_disks:
|
||||
# Only show booted disk if building for host
|
||||
disk_root = self.constants.booted_oc_disk if self.constants.custom_model is None else None
|
||||
if disk_root:
|
||||
# disk6s1 -> disk6
|
||||
disk_root = self.constants.booted_oc_disk.strip("disk")
|
||||
disk_root = "disk" + disk_root.split("s")[0]
|
||||
logging.info(f"Checking if booted disk is present: {disk_root}")
|
||||
|
||||
# Add buttons for each disk
|
||||
items = len(self.available_disks)
|
||||
longest_label = max((len(self.available_disks[disk]['disk']) + len(self.available_disks[disk]['name']) + len(str(self.available_disks[disk]['size']))) for disk in self.available_disks)
|
||||
longest_label = longest_label * 9
|
||||
spacer = 0
|
||||
logging.info("Available disks:")
|
||||
for disk in self.available_disks:
|
||||
# Create a button for each disk
|
||||
logging.info(f"- {self.available_disks[disk]['disk']} - {self.available_disks[disk]['name']} - {self.available_disks[disk]['size']}")
|
||||
disk_button = wx.Button(dialog, label=f"{self.available_disks[disk]['disk']} - {self.available_disks[disk]['name']} - {self.available_disks[disk]['size']}", size=(longest_label ,30), pos=(-1, gpt_note.GetPosition()[1] + gpt_note.GetSize()[1] + 5 + spacer))
|
||||
disk_button.Centre(wx.HORIZONTAL)
|
||||
disk_button.Bind(wx.EVT_BUTTON, lambda event, disk=disk: self._display_volumes(disk, self.available_disks))
|
||||
if disk_root == self.available_disks[disk]['disk'] or items == 1:
|
||||
disk_button.SetDefault()
|
||||
spacer += 25
|
||||
|
||||
if disk_root:
|
||||
# Add note: "Note: Blue represent the disk OpenCore is currently booted from"
|
||||
disk_label = wx.StaticText(dialog, label="Note: Blue represent the disk OpenCore is currently booted from", pos=(-1, disk_button.GetPosition()[1] + disk_button.GetSize()[1] + 5))
|
||||
disk_label.SetFont(wx.Font(10, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont"))
|
||||
disk_label.Centre(wx.HORIZONTAL)
|
||||
else:
|
||||
disk_label = wx.StaticText(dialog, label="", pos=(-1, disk_button.GetPosition()[1] + 15))
|
||||
disk_label.SetFont(wx.Font(10, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont"))
|
||||
else:
|
||||
# Text: Failed to find any applicable disks
|
||||
disk_label = wx.StaticText(dialog, label="Failed to find any applicable disks", pos=(-1, gpt_note.GetPosition()[1] + gpt_note.GetSize()[1] + 5))
|
||||
disk_label.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont"))
|
||||
disk_label.Centre(wx.HORIZONTAL)
|
||||
|
||||
# Add button: Search for disks again
|
||||
search_button = wx.Button(dialog, label="Search for disks again", size=(160,30), pos=(-1, disk_label.GetPosition()[1] + disk_label.GetSize()[1] + 5))
|
||||
search_button.Centre(wx.HORIZONTAL)
|
||||
search_button.Bind(wx.EVT_BUTTON, self.on_reload_frame)
|
||||
|
||||
# Add button: Return to main menu
|
||||
return_button = wx.Button(dialog, label="Return to main menu", size=(160,30), pos=(-1, search_button.GetPosition()[1] + 20))
|
||||
return_button.Centre(wx.HORIZONTAL)
|
||||
return_button.Bind(wx.EVT_BUTTON, self.on_return_to_main_menu)
|
||||
|
||||
# Set size
|
||||
dialog.SetSize((-1, return_button.GetPosition()[1] + return_button.GetSize()[1] + 40))
|
||||
dialog.ShowWindowModal()
|
||||
self.dialog = dialog
|
||||
|
||||
|
||||
def _display_volumes(self, disk: str, dataset: dict) -> None:
|
||||
"""
|
||||
List volumes on disk
|
||||
"""
|
||||
|
||||
self.dialog.Close()
|
||||
|
||||
# Create dialog
|
||||
dialog = wx.Dialog(
|
||||
self,
|
||||
title=f"Volumes on {disk}",
|
||||
style=wx.CAPTION | wx.CLOSE_BOX,
|
||||
size=(300, 300)
|
||||
)
|
||||
|
||||
# Add text: "Volumes on {disk}"
|
||||
text_label = wx.StaticText(dialog, label=f"Volumes on {disk}", pos=(-1, 10))
|
||||
text_label.SetFont(wx.Font(19, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont"))
|
||||
text_label.Centre(wx.HORIZONTAL)
|
||||
|
||||
partitions = install.tui_disk_installation(self.constants).list_partitions(disk, dataset)
|
||||
items = len(partitions)
|
||||
longest_label = max((len(partitions[partition]['partition']) + len(partitions[partition]['name']) + len(str(partitions[partition]['size']))) for partition in partitions)
|
||||
longest_label = longest_label * 10
|
||||
spacer = 0
|
||||
logging.info(f"Available partitions for {disk}:")
|
||||
for partition in partitions:
|
||||
logging.info(f"- {partitions[partition]['partition']} - {partitions[partition]['name']} - {partitions[partition]['size']}")
|
||||
disk_button = wx.Button(dialog, label=f"{partitions[partition]['partition']} - {partitions[partition]['name']} - {partitions[partition]['size']}", size=(longest_label,30), pos=(-1, text_label.GetPosition()[1] + text_label.GetSize()[1] + 5 + spacer))
|
||||
disk_button.Centre(wx.HORIZONTAL)
|
||||
disk_button.Bind(wx.EVT_BUTTON, lambda event, partition=partition: self._install_oc_process(partition))
|
||||
if items == 1 or self.constants.booted_oc_disk == partitions[partition]['partition']:
|
||||
disk_button.SetDefault()
|
||||
spacer += 25
|
||||
|
||||
# Add button: Return to main menu
|
||||
return_button = wx.Button(dialog, label="Return to main menu", size=(150,30), pos=(-1, disk_button.GetPosition()[1] + disk_button.GetSize()[1]))
|
||||
return_button.Centre(wx.HORIZONTAL)
|
||||
return_button.Bind(wx.EVT_BUTTON, self.on_return_to_main_menu)
|
||||
|
||||
# Set size
|
||||
dialog.SetSize((-1, return_button.GetPosition()[1] + return_button.GetSize()[1] + 40))
|
||||
|
||||
# Show dialog
|
||||
dialog.ShowWindowModal()
|
||||
self.dialog = dialog
|
||||
|
||||
|
||||
def _install_oc_process(self, partition: dict) -> None:
|
||||
"""
|
||||
Install OpenCore to disk
|
||||
"""
|
||||
self.dialog.Close()
|
||||
|
||||
# Create dialog
|
||||
dialog = wx.Dialog(
|
||||
self,
|
||||
title=f"Installing OpenCore to {partition}",
|
||||
style=wx.CAPTION | wx.CLOSE_BOX,
|
||||
size=(370, 200)
|
||||
)
|
||||
|
||||
# Add text: "Installing OpenCore to {partition}"
|
||||
text_label = wx.StaticText(dialog, label=f"Installing OpenCore to {partition}", pos=(-1, 10))
|
||||
text_label.SetFont(wx.Font(19, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont"))
|
||||
text_label.Centre(wx.HORIZONTAL)
|
||||
|
||||
# Read-only text box: {empty}
|
||||
text_box = wx.TextCtrl(dialog, value="", pos=(-1, text_label.GetPosition()[1] + text_label.GetSize()[1] + 10), size=(370, 200), style=wx.TE_READONLY | wx.TE_MULTILINE | wx.TE_RICH2)
|
||||
text_box.Centre(wx.HORIZONTAL)
|
||||
self.text_box = text_box
|
||||
|
||||
# Add button: Return to main menu
|
||||
return_button = wx.Button(dialog, label="Return to main menu", size=(200,30), pos=(-1, text_box.GetPosition()[1] + text_box.GetSize()[1] + 10))
|
||||
return_button.Centre(wx.HORIZONTAL)
|
||||
return_button.Bind(wx.EVT_BUTTON, self.on_return_to_main_menu)
|
||||
return_button.Disable()
|
||||
|
||||
# Set size
|
||||
dialog.SetSize((370, return_button.GetPosition()[1] + return_button.GetSize()[1] + 40))
|
||||
|
||||
# Show dialog
|
||||
dialog.ShowWindowModal()
|
||||
self.dialog = dialog
|
||||
|
||||
# Install OpenCore
|
||||
self._invoke_install_oc(partition)
|
||||
return_button.Enable()
|
||||
|
||||
|
||||
def _invoke_install_oc(self, partition: dict) -> None:
|
||||
"""
|
||||
Invoke OpenCore installation
|
||||
"""
|
||||
thread = threading.Thread(target=self._install_oc, args=(partition,))
|
||||
thread.start()
|
||||
|
||||
while thread.is_alive():
|
||||
wx.Yield()
|
||||
|
||||
if self.result is True:
|
||||
if self.constants.update_stage != gui_support.AutoUpdateStages.INACTIVE and self.constants.detected_os >= os_data.os_data.big_sur:
|
||||
self.constants.update_stage = gui_support.AutoUpdateStages.ROOT_PATCHING
|
||||
popup_message = wx.MessageDialog(
|
||||
self,
|
||||
f"OpenCore has finished installing to disk.\n\nWould you like to update your root patches next?", "Success",
|
||||
wx.YES_NO | wx.YES_DEFAULT
|
||||
)
|
||||
popup_message.ShowModal()
|
||||
if popup_message.GetReturnCode() == wx.ID_YES:
|
||||
self.Hide()
|
||||
gui_sys_patch_display.SysPatchDisplayFrame(
|
||||
parent=None,
|
||||
title=self.title,
|
||||
global_constants=self.constants,
|
||||
screen_location=self.GetPosition()
|
||||
)
|
||||
self.Destroy()
|
||||
return
|
||||
|
||||
elif not self.constants.custom_model:
|
||||
gui_support.RestartHost(self).restart(message="OpenCore has finished installing to disk.\n\nYou will need to reboot and hold the Option key and select OpenCore/Boot EFI's option.\n\nWould you like to reboot?")
|
||||
else:
|
||||
popup_message = wx.MessageDialog(
|
||||
self,
|
||||
f"OpenCore has finished installing to disk.\n\nYou can eject the drive, insert it into the {self.constants.custom_model}, reboot, hold the Option key and select OpenCore/Boot EFI's option.", "Success",
|
||||
wx.OK
|
||||
)
|
||||
popup_message.ShowModal()
|
||||
else:
|
||||
if self.constants.update_stage != gui_support.AutoUpdateStages.INACTIVE:
|
||||
self.constants.update_stage = gui_support.AutoUpdateStages.FINISHED
|
||||
|
||||
|
||||
def _install_oc(self, partition: dict) -> None:
|
||||
"""
|
||||
Install OpenCore to disk
|
||||
"""
|
||||
logging.info(f"Installing OpenCore to {partition}")
|
||||
|
||||
logger = logging.getLogger()
|
||||
logger.addHandler(gui_support.ThreadHandler(self.text_box))
|
||||
try:
|
||||
self.result = install.tui_disk_installation(self.constants).install_opencore(partition)
|
||||
except:
|
||||
logging.error("An internal error occurred while installing:\n")
|
||||
logging.error(traceback.format_exc())
|
||||
logger.removeHandler(logger.handlers[2])
|
||||
|
||||
|
||||
def on_reload_frame(self, event: wx.Event = None) -> None:
|
||||
"""
|
||||
Reload frame
|
||||
"""
|
||||
self.Destroy()
|
||||
frame = InstallOCFrame(
|
||||
None,
|
||||
title=self.title,
|
||||
global_constants=self.constants,
|
||||
screen_location=self.GetScreenPosition()
|
||||
)
|
||||
frame.Show()
|
||||
|
||||
|
||||
def on_return_to_main_menu(self, event: wx.Event = None) -> None:
|
||||
"""
|
||||
Return to main menu
|
||||
"""
|
||||
main_menu_frame = gui_main_menu.MainFrame(
|
||||
None,
|
||||
title=self.title,
|
||||
global_constants=self.constants,
|
||||
screen_location=self.GetScreenPosition()
|
||||
)
|
||||
main_menu_frame.Show()
|
||||
self.Destroy()
|
||||
|
||||
|
||||
|
||||
|
||||
396
resources/wx_gui/gui_macos_installer_download.py
Normal file
@@ -0,0 +1,396 @@
|
||||
import wx
|
||||
import logging
|
||||
import threading
|
||||
import webbrowser
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from resources.wx_gui import (
|
||||
gui_main_menu,
|
||||
gui_support,
|
||||
gui_download,
|
||||
gui_macos_installer_flash
|
||||
)
|
||||
from resources import (
|
||||
constants,
|
||||
macos_installer_handler,
|
||||
utilities,
|
||||
network_handler,
|
||||
integrity_verification
|
||||
)
|
||||
from data import os_data, smbios_data, cpu_data
|
||||
|
||||
|
||||
class macOSInstallerDownloadFrame(wx.Frame):
|
||||
"""
|
||||
Create a frame for downloading and creating macOS installers
|
||||
Uses a Modal Dialog for smoother transition from other frames
|
||||
Note: Flashing installers is passed to gui_macos_installer_flash.py
|
||||
"""
|
||||
def __init__(self, parent: wx.Frame, title: str, global_constants: constants.Constants, screen_location: tuple = None):
|
||||
logging.info("Initializing macOS Installer Download Frame")
|
||||
self.constants: constants.Constants = global_constants
|
||||
self.title: str = title
|
||||
self.parent: wx.Frame = parent
|
||||
|
||||
self.available_installers = None
|
||||
self.available_installers_latest = None
|
||||
|
||||
self.catalog_seed: macos_installer_handler.SeedType = macos_installer_handler.SeedType.DeveloperSeed
|
||||
|
||||
self.frame_modal = wx.Dialog(parent, title=title, size=(330, 200))
|
||||
|
||||
self._generate_elements(self.frame_modal)
|
||||
self.frame_modal.ShowWindowModal()
|
||||
|
||||
|
||||
def _generate_elements(self, frame: wx.Frame = None) -> None:
|
||||
"""
|
||||
Format:
|
||||
- Title: Create macOS Installer
|
||||
- Button: Download macOS Installer
|
||||
- Button: Use existing macOS Installer
|
||||
- Button: Return to Main Menu
|
||||
"""
|
||||
|
||||
frame = self if not frame else frame
|
||||
|
||||
title_label = wx.StaticText(frame, label="Create macOS Installer", pos=(-1,5))
|
||||
title_label.SetFont(wx.Font(19, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont"))
|
||||
title_label.Centre(wx.HORIZONTAL)
|
||||
|
||||
# Button: Download macOS Installer
|
||||
download_button = wx.Button(frame, label="Download macOS Installer", pos=(-1, title_label.GetPosition()[1] + title_label.GetSize()[1] + 5), size=(200, 30))
|
||||
download_button.Bind(wx.EVT_BUTTON, self.on_download)
|
||||
download_button.Centre(wx.HORIZONTAL)
|
||||
|
||||
# Button: Use existing macOS Installer
|
||||
existing_button = wx.Button(frame, label="Use existing macOS Installer", pos=(-1, download_button.GetPosition()[1] + download_button.GetSize()[1] - 5), size=(200, 30))
|
||||
existing_button.Bind(wx.EVT_BUTTON, self.on_existing)
|
||||
existing_button.Centre(wx.HORIZONTAL)
|
||||
|
||||
# Button: Return to Main Menu
|
||||
return_button = wx.Button(frame, label="Return to Main Menu", pos=(-1, existing_button.GetPosition()[1] + existing_button.GetSize()[1] + 5), size=(150, 30))
|
||||
return_button.Bind(wx.EVT_BUTTON, self.on_return)
|
||||
return_button.Centre(wx.HORIZONTAL)
|
||||
|
||||
# Set size of frame
|
||||
frame.SetSize((-1, return_button.GetPosition()[1] + return_button.GetSize()[1] + 40))
|
||||
|
||||
|
||||
def _generate_catalog_frame(self) -> None:
|
||||
"""
|
||||
Generate frame to display available installers
|
||||
"""
|
||||
super(macOSInstallerDownloadFrame, self).__init__(None, title=self.title, size=(300, 200), style=wx.DEFAULT_FRAME_STYLE & ~(wx.RESIZE_BORDER | wx.MAXIMIZE_BOX))
|
||||
gui_support.GenerateMenubar(self, self.constants).generate()
|
||||
self.Centre()
|
||||
|
||||
# Title: Pulling installer catalog
|
||||
title_label = wx.StaticText(self, label="Pulling installer catalog", pos=(-1,5))
|
||||
title_label.SetFont(wx.Font(19, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont"))
|
||||
title_label.Centre(wx.HORIZONTAL)
|
||||
|
||||
# Progress bar
|
||||
progress_bar = wx.Gauge(self, range=100, pos=(-1, title_label.GetPosition()[1] + title_label.GetSize()[1] + 5), size=(250, 30))
|
||||
progress_bar.Centre(wx.HORIZONTAL)
|
||||
progress_bar_animation = gui_support.GaugePulseCallback(self.constants, progress_bar)
|
||||
progress_bar_animation.start_pulse()
|
||||
|
||||
# Set size of frame
|
||||
self.SetSize((-1, progress_bar.GetPosition()[1] + progress_bar.GetSize()[1] + 40))
|
||||
|
||||
self.Show()
|
||||
|
||||
# Grab installer catalog
|
||||
def _fetch_installers():
|
||||
logging.info(f"Fetching installer catalog: {macos_installer_handler.SeedType(self.catalog_seed).name}")
|
||||
remote_obj = macos_installer_handler.RemoteInstallerCatalog(seed_override=self.catalog_seed)
|
||||
self.available_installers = remote_obj.available_apps
|
||||
self.available_installers_latest = remote_obj.available_apps_latest
|
||||
|
||||
thread = threading.Thread(target=_fetch_installers)
|
||||
thread.start()
|
||||
|
||||
while thread.is_alive():
|
||||
wx.Yield()
|
||||
|
||||
progress_bar_animation.stop_pulse()
|
||||
progress_bar.Hide()
|
||||
self._display_available_installers()
|
||||
|
||||
|
||||
def _display_available_installers(self, event: wx.Event = None, show_full: bool = False) -> None:
|
||||
"""
|
||||
Display available installers in frame
|
||||
"""
|
||||
self.frame_modal.Destroy()
|
||||
dialog = wx.Dialog(self, title="Select macOS Installer", size=(300, 200))
|
||||
|
||||
# Title: Select macOS Installer
|
||||
title_label = wx.StaticText(dialog, label="Select macOS Installer", pos=(-1,5))
|
||||
title_label.SetFont(wx.Font(19, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont"))
|
||||
title_label.Centre(wx.HORIZONTAL)
|
||||
|
||||
# Subtitle: Installers currently available from Apple:
|
||||
subtitle_label = wx.StaticText(dialog, label="Installers currently available from Apple:", pos=(-1, title_label.GetPosition()[1] + title_label.GetSize()[1] + 5))
|
||||
subtitle_label.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont"))
|
||||
subtitle_label.Centre(wx.HORIZONTAL)
|
||||
|
||||
# List of installers
|
||||
installers = self.available_installers_latest if show_full is False else self.available_installers
|
||||
if installers:
|
||||
spacer = 0
|
||||
logging.info(f"Available installers on SUCatalog ({'All entries' if show_full else 'Latest only'}):")
|
||||
for app in installers:
|
||||
logging.info(f"- macOS {installers[app]['Version']} ({installers[app]['Build']}):\n - Size: {utilities.human_fmt(installers[app]['Size'])}\n - Source: {installers[app]['Source']}\n - Variant: {installers[app]['Variant']}\n - Link: {installers[app]['Link']}\n")
|
||||
extra = " Beta" if installers[app]['Variant'] in ["DeveloperSeed" , "PublicSeed"] else ""
|
||||
|
||||
installer_button = wx.Button(dialog, label=f"macOS {installers[app]['Version']}{extra} ({installers[app]['Build']} - {utilities.human_fmt(installers[app]['Size'])})", pos=(-1, subtitle_label.GetPosition()[1] + subtitle_label.GetSize()[1] + 5 + spacer), size=(270, 30))
|
||||
installer_button.Bind(wx.EVT_BUTTON, lambda event, temp=app: self.on_download_installer(installers[temp]))
|
||||
installer_button.Centre(wx.HORIZONTAL)
|
||||
spacer += 25
|
||||
|
||||
# Since installers are sorted by version, set the latest installer as the default button
|
||||
# Note that on full display, the last installer is generally a beta
|
||||
if show_full is False and app == list(installers.keys())[-1]:
|
||||
installer_button.SetDefault()
|
||||
else:
|
||||
logging.error("No installers found on SUCatalog")
|
||||
installer_button = wx.StaticText(dialog, label="Failed to fetch catalog from Apple", pos=(-1, subtitle_label.GetPosition()[1] + subtitle_label.GetSize()[1] + 5))
|
||||
installer_button.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont"))
|
||||
installer_button.Centre(wx.HORIZONTAL)
|
||||
|
||||
# Show all available installers
|
||||
show_all_button = wx.Button(dialog, label="Show all available installers" if show_full is False else "Show only latest installers", pos=(-1, installer_button.GetPosition()[1] + installer_button.GetSize()[1]), size=(200, 30))
|
||||
show_all_button.Bind(wx.EVT_BUTTON, lambda event: self._display_available_installers(event, not show_full))
|
||||
show_all_button.Centre(wx.HORIZONTAL)
|
||||
|
||||
# Return to Main Menu
|
||||
return_button = wx.Button(dialog, label="Return to Main Menu", pos=(-1, show_all_button.GetPosition()[1] + show_all_button.GetSize()[1] - 7), size=(150, 30))
|
||||
return_button.Bind(wx.EVT_BUTTON, self.on_return_to_main_menu)
|
||||
return_button.Centre(wx.HORIZONTAL)
|
||||
|
||||
# Set size of frame
|
||||
dialog.SetSize((-1, return_button.GetPosition()[1] + return_button.GetSize()[1] + 40))
|
||||
dialog.ShowWindowModal()
|
||||
self.frame_modal = dialog
|
||||
|
||||
|
||||
def on_download_installer(self, app: dict) -> None:
|
||||
"""
|
||||
Download macOS installer
|
||||
"""
|
||||
logging.info(f"Selected macOS {app['Version']} ({app['Build']})")
|
||||
|
||||
# Notify user whether their model is compatible with the selected installer
|
||||
problems = []
|
||||
model = self.constants.custom_model or self.constants.computer.real_model
|
||||
if model in smbios_data.smbios_dictionary:
|
||||
if app["OS"] >= os_data.os_data.ventura:
|
||||
if smbios_data.smbios_dictionary[model]["CPU Generation"] <= cpu_data.cpu_data.penryn or model in ["MacPro4,1", "MacPro5,1", "Xserve3,1"]:
|
||||
if model.startswith("MacBook"):
|
||||
problems.append("Lack of internal Keyboard/Trackpad in macOS installer.")
|
||||
else:
|
||||
problems.append("Lack of internal Keyboard/Mouse in macOS installer.")
|
||||
|
||||
if problems:
|
||||
logging.warning(f"Potential issues with {model} and {app['Version']} ({app['Build']}): {problems}")
|
||||
problems = "\n".join(problems)
|
||||
dlg = wx.MessageDialog(self.frame_modal, f"Your model ({model}) may not be fully supported by this installer. You may encounter the following issues:\n\n{problems}\n\nFor more information, see associated page. Otherwise, we recommend using macOS Monterey", "Potential Issues", wx.YES_NO | wx.CANCEL | wx.ICON_WARNING)
|
||||
dlg.SetYesNoCancelLabels("View Github Issue", "Download Anyways", "Cancel")
|
||||
result = dlg.ShowModal()
|
||||
if result == wx.ID_CANCEL:
|
||||
return
|
||||
elif result == wx.ID_YES:
|
||||
webbrowser.open("https://github.com/dortania/OpenCore-Legacy-Patcher/issues/1021")
|
||||
return
|
||||
|
||||
host_space = utilities.get_free_space()
|
||||
needed_space = app['Size'] * 2
|
||||
if host_space < needed_space:
|
||||
logging.error(f"Insufficient space to download and extract: {utilities.human_fmt(host_space)} available vs {utilities.human_fmt(needed_space)} required")
|
||||
dlg = wx.MessageDialog(self.frame_modal, f"You do not have enough free space to download and extract this installer. Please free up some space and try again\n\n{utilities.human_fmt(host_space)} available vs {utilities.human_fmt(needed_space)} required", "Insufficient Space", wx.OK | wx.ICON_WARNING)
|
||||
dlg.ShowModal()
|
||||
return
|
||||
|
||||
self.frame_modal.Close()
|
||||
|
||||
download_obj = network_handler.DownloadObject(app['Link'], self.constants.payload_path / "InstallAssistant.pkg")
|
||||
|
||||
gui_download.DownloadFrame(
|
||||
self,
|
||||
title=self.title,
|
||||
global_constants=self.constants,
|
||||
download_obj=download_obj,
|
||||
item_name=f"macOS {app['Version']} ({app['Build']})",
|
||||
)
|
||||
|
||||
if download_obj.download_complete is False:
|
||||
self.on_return_to_main_menu()
|
||||
return
|
||||
|
||||
self._validate_installer(app['integrity'])
|
||||
|
||||
|
||||
def _validate_installer(self, chunklist_link: str) -> None:
|
||||
"""
|
||||
Validate macOS installer
|
||||
"""
|
||||
self.SetSize((300, 200))
|
||||
for child in self.GetChildren():
|
||||
child.Destroy()
|
||||
|
||||
# Title: Validating macOS Installer
|
||||
title_label = wx.StaticText(self, label="Validating macOS Installer", pos=(-1,5))
|
||||
title_label.SetFont(wx.Font(19, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont"))
|
||||
title_label.Centre(wx.HORIZONTAL)
|
||||
|
||||
# Label: Validating chunk 0 of 0
|
||||
chunk_label = wx.StaticText(self, label="Validating chunk 0 of 0", pos=(-1, title_label.GetPosition()[1] + title_label.GetSize()[1] + 5))
|
||||
chunk_label.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont"))
|
||||
chunk_label.Centre(wx.HORIZONTAL)
|
||||
|
||||
# Progress bar
|
||||
progress_bar = wx.Gauge(self, range=100, pos=(-1, chunk_label.GetPosition()[1] + chunk_label.GetSize()[1] + 5), size=(270, 30))
|
||||
progress_bar.Centre(wx.HORIZONTAL)
|
||||
|
||||
# Set size of frame
|
||||
self.SetSize((-1, progress_bar.GetPosition()[1] + progress_bar.GetSize()[1] + 40))
|
||||
self.Show()
|
||||
|
||||
chunklist_stream = network_handler.NetworkUtilities().get(chunklist_link).content
|
||||
if chunklist_stream:
|
||||
logging.info("Validating macOS installer")
|
||||
utilities.disable_sleep_while_running()
|
||||
chunk_obj = integrity_verification.ChunklistVerification(self.constants.payload_path / Path("InstallAssistant.pkg"), chunklist_stream)
|
||||
if chunk_obj.chunks:
|
||||
progress_bar.SetValue(chunk_obj.current_chunk)
|
||||
progress_bar.SetRange(chunk_obj.total_chunks)
|
||||
|
||||
wx.App.Get().Yield()
|
||||
chunk_obj.validate()
|
||||
|
||||
while chunk_obj.status == integrity_verification.ChunklistStatus.IN_PROGRESS:
|
||||
progress_bar.SetValue(chunk_obj.current_chunk)
|
||||
chunk_label.SetLabel(f"Validating chunk {chunk_obj.current_chunk} of {chunk_obj.total_chunks}")
|
||||
chunk_label.Centre(wx.HORIZONTAL)
|
||||
wx.App.Get().Yield()
|
||||
|
||||
if chunk_obj.status == integrity_verification.ChunklistStatus.FAILURE:
|
||||
logging.error(f"Chunklist validation failed: Hash mismatch on {chunk_obj.current_chunk}")
|
||||
wx.MessageBox(f"Chunklist validation failed: Hash mismatch on {chunk_obj.current_chunk}\n\nThis generally happens when downloading on unstable connections such as WiFi or cellular.\n\nPlease try redownloading again on a stable connection (ie. Ethernet)", "Corrupted Installer!", wx.OK | wx.ICON_ERROR)
|
||||
self.on_return_to_main_menu()
|
||||
return
|
||||
|
||||
logging.info("macOS installer validated")
|
||||
|
||||
# Extract installer
|
||||
title_label.SetLabel("Extracting macOS Installer")
|
||||
title_label.Centre(wx.HORIZONTAL)
|
||||
|
||||
chunk_label.SetLabel("May take a few minutes...")
|
||||
chunk_label.Centre(wx.HORIZONTAL)
|
||||
|
||||
progress_bar_animation = gui_support.GaugePulseCallback(self.constants, progress_bar)
|
||||
progress_bar_animation.start_pulse()
|
||||
|
||||
# Start thread to extract installer
|
||||
self.result = False
|
||||
def extract_installer():
|
||||
self.result = macos_installer_handler.InstallerCreation().install_macOS_installer(self.constants.payload_path)
|
||||
|
||||
thread = threading.Thread(target=extract_installer)
|
||||
thread.start()
|
||||
|
||||
# Show frame
|
||||
self.Show()
|
||||
|
||||
# Wait for thread to finish
|
||||
while thread.is_alive():
|
||||
wx.Yield()
|
||||
|
||||
progress_bar_animation.stop_pulse()
|
||||
progress_bar.Hide()
|
||||
chunk_label.SetLabel("Successfully extracted macOS installer" if self.result is True else "Failed to extract macOS installer")
|
||||
chunk_label.Centre(wx.HORIZONTAL)
|
||||
|
||||
# Create macOS Installer button
|
||||
create_installer_button = wx.Button(self, label="Create macOS Installer", pos=(-1, progress_bar.GetPosition()[1]), size=(170, 30))
|
||||
create_installer_button.Bind(wx.EVT_BUTTON, self.on_existing)
|
||||
create_installer_button.Centre(wx.HORIZONTAL)
|
||||
if self.result is False:
|
||||
create_installer_button.Disable()
|
||||
|
||||
# Return to main menu button
|
||||
return_button = wx.Button(self, label="Return to Main Menu", pos=(-1, create_installer_button.GetPosition()[1] + create_installer_button.GetSize()[1]), size=(150, 30))
|
||||
return_button.Bind(wx.EVT_BUTTON, self.on_return_to_main_menu)
|
||||
return_button.Centre(wx.HORIZONTAL)
|
||||
|
||||
# Set size of frame
|
||||
self.SetSize((-1, return_button.GetPosition()[1] + return_button.GetSize()[1] + 40))
|
||||
|
||||
# Show frame
|
||||
self.Show()
|
||||
|
||||
if self.result is False:
|
||||
wx.MessageBox("An error occurred while extracting the macOS installer. Could be due to a corrupted installer", "Error", wx.OK | wx.ICON_ERROR)
|
||||
return
|
||||
|
||||
user_input = wx.MessageBox("Finished extracting the installer, would you like to continue and create a macOS installer?", "Create macOS Installer?", wx.YES_NO | wx.ICON_QUESTION)
|
||||
if user_input == wx.YES:
|
||||
self.on_existing()
|
||||
|
||||
|
||||
def on_download(self, event: wx.Event) -> None:
|
||||
"""
|
||||
Display available macOS versions to download
|
||||
"""
|
||||
self.frame_modal.Close()
|
||||
self.parent.Hide()
|
||||
self._generate_catalog_frame()
|
||||
self.parent.Close()
|
||||
|
||||
|
||||
def on_existing(self, event: wx.Event = None) -> None:
|
||||
"""
|
||||
Display local macOS installers
|
||||
"""
|
||||
frames = [self, self.frame_modal, self.parent]
|
||||
for frame in frames:
|
||||
if frame:
|
||||
frame.Close()
|
||||
gui_macos_installer_flash.macOSInstallerFlashFrame(
|
||||
None,
|
||||
title=self.title,
|
||||
global_constants=self.constants,
|
||||
**({"screen_location": self.GetScreenPosition()} if self else {})
|
||||
)
|
||||
for frame in frames:
|
||||
if frame:
|
||||
frame.Destroy()
|
||||
|
||||
|
||||
def on_return(self, event: wx.Event) -> None:
|
||||
"""
|
||||
Return to main menu (dismiss frame)
|
||||
"""
|
||||
self.frame_modal.Close()
|
||||
|
||||
|
||||
def on_return_to_main_menu(self, event: wx.Event = None) -> None:
|
||||
"""
|
||||
Return to main menu
|
||||
"""
|
||||
if self.frame_modal:
|
||||
self.frame_modal.Hide()
|
||||
main_menu_frame = gui_main_menu.MainFrame(
|
||||
None,
|
||||
title=self.title,
|
||||
global_constants=self.constants,
|
||||
screen_location=self.GetScreenPosition()
|
||||
)
|
||||
main_menu_frame.Show()
|
||||
if self.frame_modal:
|
||||
self.frame_modal.Destroy()
|
||||
self.Destroy()
|
||||
587
resources/wx_gui/gui_macos_installer_flash.py
Normal file
@@ -0,0 +1,587 @@
|
||||
import wx
|
||||
import time
|
||||
import logging
|
||||
import plistlib
|
||||
import tempfile
|
||||
import threading
|
||||
import subprocess
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from resources.wx_gui import gui_main_menu, gui_build, gui_support
|
||||
from resources import (
|
||||
constants,
|
||||
macos_installer_handler,
|
||||
utilities,
|
||||
network_handler,
|
||||
kdk_handler,
|
||||
)
|
||||
from data import os_data
|
||||
|
||||
|
||||
class macOSInstallerFlashFrame(wx.Frame):
|
||||
|
||||
def __init__(self, parent: wx.Frame, title: str, global_constants: constants.Constants, screen_location: tuple = None):
|
||||
logging.info("Initializing macOS Installer Flash Frame")
|
||||
super(macOSInstallerFlashFrame, self).__init__(parent, title=title, size=(350, 200), style=wx.DEFAULT_FRAME_STYLE & ~(wx.RESIZE_BORDER | wx.MAXIMIZE_BOX))
|
||||
gui_support.GenerateMenubar(self, global_constants).generate()
|
||||
|
||||
self.constants: constants.Constants = global_constants
|
||||
self.title: str = title
|
||||
|
||||
self.available_installers_local: dict = {}
|
||||
self.available_disks: dict = {}
|
||||
self.prepare_result: bool = False
|
||||
|
||||
self.progress_bar_animation: gui_support.GaugePulseCallback = None
|
||||
|
||||
self.frame_modal: wx.Dialog = None
|
||||
|
||||
self._generate_elements()
|
||||
|
||||
self.Centre()
|
||||
self.Show()
|
||||
|
||||
self._populate_installers()
|
||||
|
||||
|
||||
def _generate_elements(self) -> None:
|
||||
"""
|
||||
Fetches local macOS Installers for users to select from
|
||||
"""
|
||||
|
||||
# Title: Fetching local macOS Installers
|
||||
title_label = wx.StaticText(self, label="Fetching local macOS Installers", pos=(-1,1))
|
||||
title_label.SetFont(wx.Font(19, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont"))
|
||||
title_label.Centre(wx.HORIZONTAL)
|
||||
|
||||
# Progress bar
|
||||
progress_bar = wx.Gauge(self, range=100, pos=(-1, 30), size=(200, 30))
|
||||
progress_bar.Centre(wx.HORIZONTAL)
|
||||
|
||||
progress_bar_animation = gui_support.GaugePulseCallback(self.constants, progress_bar)
|
||||
progress_bar_animation.start_pulse()
|
||||
self.progress_bar_animation = progress_bar_animation
|
||||
|
||||
# Set size of frame
|
||||
self.SetSize((-1, progress_bar.GetPosition()[1] + progress_bar.GetSize()[1] + 40))
|
||||
|
||||
|
||||
def _populate_installers(self) -> None:
|
||||
# Grab installer catalog
|
||||
def fetch_installers():
|
||||
self.available_installers_local = macos_installer_handler.LocalInstallerCatalog().available_apps
|
||||
|
||||
thread = threading.Thread(target=fetch_installers)
|
||||
thread.start()
|
||||
|
||||
while thread.is_alive():
|
||||
wx.Yield()
|
||||
|
||||
frame_modal = wx.Dialog(self, title=self.title, size=(350, 200))
|
||||
|
||||
# Title: Select macOS Installer
|
||||
title_label = wx.StaticText(frame_modal, label="Select local macOS Installer", pos=(-1,5))
|
||||
title_label.SetFont(wx.Font(19, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont"))
|
||||
title_label.Centre(wx.HORIZONTAL)
|
||||
|
||||
# List of installers
|
||||
if self.available_installers_local:
|
||||
logging.info("Installer(s) found:")
|
||||
spacer = 10
|
||||
entries = len(self.available_installers_local)
|
||||
for app in self.available_installers_local:
|
||||
logging.info(f"- {self.available_installers_local[app]['Short Name']}: {self.available_installers_local[app]['Version']} ({self.available_installers_local[app]['Build']})")
|
||||
|
||||
app_str = f"{self.available_installers_local[app]['Short Name']}"
|
||||
unsupported: bool = self.available_installers_local[app]['Minimum Host OS'] > self.constants.detected_os
|
||||
|
||||
if unsupported:
|
||||
min_str = os_data.os_conversion.convert_kernel_to_marketing_name(self.available_installers_local[app]['Minimum Host OS'])
|
||||
app_str += f" (Requires {min_str})"
|
||||
else:
|
||||
app_str += f": {self.available_installers_local[app]['Version']} ({self.available_installers_local[app]['Build']})"
|
||||
|
||||
installer_button = wx.Button(frame_modal, label=app_str, pos=(-1, title_label.GetPosition()[1] + title_label.GetSize()[1] + spacer), size=(300, 30))
|
||||
installer_button.Bind(wx.EVT_BUTTON, lambda event, temp=app: self.on_select(self.available_installers_local[temp]))
|
||||
installer_button.Centre(wx.HORIZONTAL)
|
||||
spacer += 25
|
||||
if unsupported:
|
||||
installer_button.Disable()
|
||||
elif entries == 1:
|
||||
installer_button.SetDefault()
|
||||
|
||||
else:
|
||||
installer_button = wx.StaticText(frame_modal, label="No installers found in '/Applications'", pos=(-1, title_label.GetPosition()[1] + title_label.GetSize()[1] + 5))
|
||||
installer_button.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont"))
|
||||
installer_button.Centre(wx.HORIZONTAL)
|
||||
|
||||
# Button: Return to Main Menu
|
||||
cancel_button = wx.Button(frame_modal, label="Return to Main Menu", pos=(-1, installer_button.GetPosition()[1] + installer_button.GetSize()[1]), size=(150, 30))
|
||||
cancel_button.Bind(wx.EVT_BUTTON, self.on_return_to_main_menu)
|
||||
cancel_button.Centre(wx.HORIZONTAL)
|
||||
|
||||
# Set size of frame
|
||||
frame_modal.SetSize((-1, cancel_button.GetPosition()[1] + cancel_button.GetSize()[1] + 40))
|
||||
|
||||
self.progress_bar_animation.stop_pulse()
|
||||
|
||||
frame_modal.ShowWindowModal()
|
||||
self.frame_modal = frame_modal
|
||||
|
||||
|
||||
def on_select(self, installer: dict) -> None:
|
||||
logging.info(f"Selected installer: {installer['Short Name']} ({installer['Version']} ({installer['Build']}))")
|
||||
self.frame_modal.Destroy()
|
||||
|
||||
for child in self.GetChildren():
|
||||
child.Destroy()
|
||||
|
||||
# Fetching information on local disks
|
||||
title_label = wx.StaticText(self, label="Fetching information on local disks", pos=(-1,1))
|
||||
title_label.SetFont(wx.Font(19, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont"))
|
||||
title_label.Centre(wx.HORIZONTAL)
|
||||
|
||||
# Progress bar
|
||||
progress_bar = wx.Gauge(self, range=100, pos=(-1, 30), size=(200, 30))
|
||||
progress_bar.Centre(wx.HORIZONTAL)
|
||||
|
||||
progress_bar_animation = gui_support.GaugePulseCallback(self.constants, progress_bar)
|
||||
progress_bar_animation.start_pulse()
|
||||
|
||||
# Set size of frame
|
||||
self.SetSize((-1, progress_bar.GetPosition()[1] + progress_bar.GetSize()[1] + 40))
|
||||
|
||||
# Fetch local disks
|
||||
def _fetch_disks():
|
||||
self.available_disks = macos_installer_handler.InstallerCreation().list_disk_to_format()
|
||||
|
||||
# Need to clean up output on pre-Sierra
|
||||
# Disk images are mixed in with regular disks (ex. payloads.dmg)
|
||||
ignore = ["disk image", "read-only", "virtual"]
|
||||
for disk in self.available_disks.copy():
|
||||
if any(string in self.available_disks[disk]['name'].lower() for string in ignore):
|
||||
del self.available_disks[disk]
|
||||
|
||||
|
||||
thread = threading.Thread(target=_fetch_disks)
|
||||
thread.start()
|
||||
|
||||
while thread.is_alive():
|
||||
wx.Yield()
|
||||
|
||||
self.frame_modal = wx.Dialog(self, title=self.title, size=(350, 200))
|
||||
|
||||
# Title: Select local disk
|
||||
title_label = wx.StaticText(self.frame_modal, label="Select local disk", pos=(-1,5))
|
||||
title_label.SetFont(wx.Font(19, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont"))
|
||||
title_label.Centre(wx.HORIZONTAL)
|
||||
|
||||
# Label: Selected USB will be erased, please backup any data
|
||||
warning_label = wx.StaticText(self.frame_modal, label="Selected USB will be erased, please backup any data", pos=(-1, title_label.GetPosition()[1] + title_label.GetSize()[1] + 5))
|
||||
warning_label.SetFont(wx.Font(11, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont"))
|
||||
warning_label.Centre(wx.HORIZONTAL)
|
||||
|
||||
# List of disks
|
||||
if self.available_disks:
|
||||
spacer = 5
|
||||
entries = len(self.available_disks)
|
||||
logging.info("Available disks:")
|
||||
for disk in self.available_disks:
|
||||
logging.info(f" - {disk}: {self.available_disks[disk]['name']} - {utilities.human_fmt(self.available_disks[disk]['size'])}")
|
||||
disk_button = wx.Button(self.frame_modal, label=f"{disk}: {self.available_disks[disk]['name']} - {utilities.human_fmt(self.available_disks[disk]['size'])}", pos=(-1, warning_label.GetPosition()[1] + warning_label.GetSize()[1] + spacer), size=(300, 30))
|
||||
disk_button.Bind(wx.EVT_BUTTON, lambda event, temp=disk: self.on_select_disk(self.available_disks[temp], installer))
|
||||
disk_button.Centre(wx.HORIZONTAL)
|
||||
if entries == 1:
|
||||
disk_button.SetDefault()
|
||||
spacer += 25
|
||||
else:
|
||||
disk_button = wx.StaticText(self.frame_modal, label="No disks found", pos=(-1, warning_label.GetPosition()[1] + warning_label.GetSize()[1] + 5))
|
||||
disk_button.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont"))
|
||||
disk_button.Centre(wx.HORIZONTAL)
|
||||
|
||||
# Search for disks again
|
||||
search_button = wx.Button(self.frame_modal, label="Search for disks again", pos=(-1, disk_button.GetPosition()[1] + disk_button.GetSize()[1]), size=(160, 30))
|
||||
search_button.Bind(wx.EVT_BUTTON, lambda event, temp=installer: self.on_select(temp))
|
||||
search_button.Centre(wx.HORIZONTAL)
|
||||
|
||||
# Button: Return to Main Menu
|
||||
cancel_button = wx.Button(self.frame_modal, label="Return to Main Menu", pos=(-1, search_button.GetPosition()[1] + search_button.GetSize()[1] - 10), size=(160, 30))
|
||||
cancel_button.Bind(wx.EVT_BUTTON, self.on_return_to_main_menu)
|
||||
cancel_button.Centre(wx.HORIZONTAL)
|
||||
|
||||
# Set size of frame
|
||||
self.frame_modal.SetSize((-1, cancel_button.GetPosition()[1] + cancel_button.GetSize()[1] + 40))
|
||||
|
||||
progress_bar_animation.stop_pulse()
|
||||
|
||||
self.frame_modal.ShowWindowModal()
|
||||
|
||||
|
||||
def on_select_disk(self, disk: dict, installer: dict) -> None:
|
||||
answer = wx.MessageBox(f"Are you sure you want to erase '{disk['name']}'?\nAll data will be lost, this cannot be undone.", "Confirmation", wx.YES_NO | wx.NO_DEFAULT | wx.ICON_QUESTION)
|
||||
if answer != wx.YES:
|
||||
return
|
||||
|
||||
logging.info(f"Selected disk: {disk['name']}")
|
||||
|
||||
self.frame_modal.Destroy()
|
||||
|
||||
for child in self.GetChildren():
|
||||
child.Destroy()
|
||||
|
||||
self.SetSize((450, -1))
|
||||
|
||||
# Title: Creating Installer: {installer_name}
|
||||
title_label = wx.StaticText(self, label=f"Creating Installer: {installer['Short Name']}", pos=(-1,1))
|
||||
title_label.SetFont(wx.Font(19, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont"))
|
||||
title_label.Centre(wx.HORIZONTAL)
|
||||
|
||||
# Label: Creating macOS installers can take 30min+ on slower USB drives.
|
||||
warning_label = wx.StaticText(self, label="Creating macOS installers can take 30min+ on slower USB drives.", pos=(-1, title_label.GetPosition()[1] + title_label.GetSize()[1] + 5))
|
||||
warning_label.SetFont(wx.Font(11, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont"))
|
||||
warning_label.Centre(wx.HORIZONTAL)
|
||||
|
||||
# Label: We will notify you when the installer is ready.
|
||||
warning_label = wx.StaticText(self, label="We will notify you when the installer is ready.", pos=(-1, warning_label.GetPosition()[1] + warning_label.GetSize()[1] + 5))
|
||||
warning_label.SetFont(wx.Font(11, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont"))
|
||||
warning_label.Centre(wx.HORIZONTAL)
|
||||
|
||||
# Label: Bytes Written: 0 MB
|
||||
bytes_written_label = wx.StaticText(self, label="Bytes Written: 0.00 MB", pos=(-1, warning_label.GetPosition()[1] + warning_label.GetSize()[1] + 5))
|
||||
bytes_written_label.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont"))
|
||||
bytes_written_label.Centre(wx.HORIZONTAL)
|
||||
|
||||
# Progress bar
|
||||
progress_bar = wx.Gauge(self, range=100, pos=(-1, bytes_written_label.GetPosition()[1] + bytes_written_label.GetSize()[1] + 5), size=(300, 30))
|
||||
progress_bar.Centre(wx.HORIZONTAL)
|
||||
|
||||
progress_bar_animation = gui_support.GaugePulseCallback(self.constants, progress_bar)
|
||||
progress_bar_animation.start_pulse()
|
||||
|
||||
# Set size of frame
|
||||
self.SetSize((-1, progress_bar.GetPosition()[1] + progress_bar.GetSize()[1] + 40))
|
||||
self.Show()
|
||||
|
||||
# Prepare resources
|
||||
if self._prepare_resources(installer['Path'], disk['identifier']) is False:
|
||||
logging.error("Failed to prepare resources, cannot continue.")
|
||||
wx.MessageBox("Failed to prepare resources, cannot continue.", "Error", wx.OK | wx.ICON_ERROR)
|
||||
self.on_return_to_main_menu()
|
||||
return
|
||||
|
||||
# Base Size
|
||||
estimated_size = 16000
|
||||
# AutoPkg (700MB~)
|
||||
estimated_size += 700 if installer['OS'] >= os_data.os_data.big_sur else 0
|
||||
# KDK (700MB~, and overhead for copying to installer)
|
||||
estimated_size += 700 * 2 if installer['OS'] >= os_data.os_data.ventura else 0
|
||||
|
||||
progress_bar_animation.stop_pulse()
|
||||
progress_bar.SetRange(estimated_size)
|
||||
|
||||
# /dev/diskX -> diskX
|
||||
root_disk = disk['identifier'][5:]
|
||||
initial_bytes_written = float(utilities.monitor_disk_output(root_disk))
|
||||
self.result = False
|
||||
def _flash():
|
||||
logging.info(f"Flashing {installer['Path']} to {root_disk}")
|
||||
self.result = self._flash_installer(root_disk)
|
||||
|
||||
thread = threading.Thread(target=_flash)
|
||||
thread.start()
|
||||
|
||||
# Wait for installer to be created
|
||||
while thread.is_alive():
|
||||
try:
|
||||
total_bytes_written = float(utilities.monitor_disk_output(root_disk))
|
||||
except:
|
||||
total_bytes_written = initial_bytes_written
|
||||
bytes_written = total_bytes_written - initial_bytes_written
|
||||
wx.CallAfter(bytes_written_label.SetLabel, f"Bytes Written: {bytes_written:.2f} MB")
|
||||
try:
|
||||
bytes_written = int(bytes_written)
|
||||
except:
|
||||
bytes_written = 0
|
||||
wx.CallAfter(progress_bar.SetValue, bytes_written)
|
||||
wx.Yield()
|
||||
|
||||
if self.result is False:
|
||||
logging.error("Failed to flash installer, cannot continue.")
|
||||
self.on_return_to_main_menu()
|
||||
return
|
||||
|
||||
# Next verify the installer
|
||||
progress_bar_animation = gui_support.GaugePulseCallback(self.constants, progress_bar)
|
||||
progress_bar_animation.start_pulse()
|
||||
|
||||
bytes_written_label.SetLabel("Validating Installer Integrity...")
|
||||
error_message = self._validate_installer_pkg(disk['identifier'])
|
||||
|
||||
progress_bar_animation.stop_pulse()
|
||||
|
||||
if error_message != "":
|
||||
progress_bar.SetValue(0)
|
||||
wx.MessageBox(f"Failed to validate installer, cannot continue.\n This can generally happen due to a faulty USB drive, as flashing is an intensive process that can trigger hardware faults not normally seen. \n\n{error_message}", "Corrupted Installer!", wx.OK | wx.ICON_ERROR)
|
||||
self.on_return_to_main_menu()
|
||||
return
|
||||
|
||||
progress_bar.SetValue(estimated_size)
|
||||
|
||||
if gui_support.CheckProperties(self.constants).host_can_build() is False:
|
||||
wx.MessageBox("Installer created successfully! If you want to install OpenCore to this USB, you will need to change the Target Model in settings", "Successfully created the macOS installer!", wx.OK | wx.ICON_INFORMATION)
|
||||
self.on_return_to_main_menu()
|
||||
return
|
||||
|
||||
answer = wx.MessageBox("Installer created successfully, would you like to continue and Install OpenCore to this disk?", "Successfully created the macOS installer!", wx.YES_NO | wx.ICON_QUESTION)
|
||||
if answer != wx.YES:
|
||||
self.on_return_to_main_menu()
|
||||
return
|
||||
|
||||
# Install OpenCore
|
||||
self.Hide()
|
||||
gui_build.BuildFrame(
|
||||
parent=None,
|
||||
title=self.title,
|
||||
global_constants=self.constants,
|
||||
screen_location=self.GetPosition()
|
||||
)
|
||||
self.Destroy()
|
||||
|
||||
|
||||
def _prepare_resources(self, installer_path: str, disk: str) -> None:
|
||||
|
||||
def prepare_script(self, installer_path: str, disk: str, constants: constants.Constants):
|
||||
self.prepare_result = macos_installer_handler.InstallerCreation().generate_installer_creation_script(constants.payload_path, installer_path, disk)
|
||||
|
||||
thread = threading.Thread(target=prepare_script, args=(self, installer_path, disk, self.constants))
|
||||
thread.start()
|
||||
|
||||
while thread.is_alive():
|
||||
wx.Yield()
|
||||
|
||||
return self.prepare_result
|
||||
|
||||
|
||||
def _flash_installer(self, disk) -> bool:
|
||||
utilities.disable_sleep_while_running()
|
||||
logging.info("Creating macOS installer")
|
||||
|
||||
thread = threading.Thread(target=self._auto_package_handler)
|
||||
thread.start()
|
||||
|
||||
# print contents of installer.sh
|
||||
with open(self.constants.installer_sh_path, "r") as f:
|
||||
logging.info(f"installer.sh contents:\n{f.read()}")
|
||||
|
||||
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" not in output:
|
||||
logging.info("Failed to create macOS installer")
|
||||
popup = wx.MessageDialog(self, f"Failed to create macOS installer\n\nOutput: {output}\n\nError: {error}", "Error", wx.OK | wx.ICON_ERROR)
|
||||
popup.ShowModal()
|
||||
return False
|
||||
|
||||
logging.info("Successfully created macOS installer")
|
||||
while thread.is_alive():
|
||||
# wait for download_thread to finish
|
||||
# though highly unlikely this thread is still alive (flashing an Installer will take a while)
|
||||
time.sleep(0.1)
|
||||
logging.info("Installing Root Patcher to drive")
|
||||
self._install_installer_pkg(disk)
|
||||
|
||||
utilities.enable_sleep_after_running()
|
||||
return True
|
||||
|
||||
|
||||
def _auto_package_handler(self):
|
||||
"""
|
||||
Function's main goal is to grab the correct AutoPkg-Assets.pkg and unzip it
|
||||
Note the following:
|
||||
- When running a release build, pull from Github's release page with the same versioning
|
||||
- When running from source/unable to find on Github, use the nightly.link variant
|
||||
- If nightly also fails, fall back to the manually uploaded variant
|
||||
"""
|
||||
link = self.constants.installer_pkg_url
|
||||
if network_handler.NetworkUtilities(link).validate_link() is False:
|
||||
logging.info("Stock Install.pkg is missing on Github, falling back to Nightly")
|
||||
link = self.constants.installer_pkg_url_nightly
|
||||
|
||||
if link.endswith(".zip"):
|
||||
path = self.constants.installer_pkg_zip_path
|
||||
else:
|
||||
path = self.constants.installer_pkg_path
|
||||
|
||||
autopkg_download = network_handler.DownloadObject(link, path)
|
||||
autopkg_download.download(spawn_thread=False)
|
||||
|
||||
if autopkg_download.download_complete is False:
|
||||
logging.warning("Failed to download Install.pkg")
|
||||
logging.warning(autopkg_download.error_msg)
|
||||
return
|
||||
|
||||
# Download thread will re-enable Idle Sleep after downloading
|
||||
utilities.disable_sleep_while_running()
|
||||
if not str(path).endswith(".zip"):
|
||||
return
|
||||
if Path(self.constants.installer_pkg_path).exists():
|
||||
subprocess.run(["rm", self.constants.installer_pkg_path])
|
||||
subprocess.run(["ditto", "-V", "-x", "-k", "--sequesterRsrc", "--rsrc", self.constants.installer_pkg_zip_path, self.constants.payload_path])
|
||||
|
||||
|
||||
def _install_installer_pkg(self, disk):
|
||||
disk = disk + "s2" # ESP sits at 1, and we know macOS will have created the main partition at 2
|
||||
|
||||
if not Path(self.constants.installer_pkg_path).exists():
|
||||
return
|
||||
|
||||
path = utilities.grab_mount_point_from_disk(disk)
|
||||
if not Path(path + "/System/Library/CoreServices/SystemVersion.plist").exists():
|
||||
return
|
||||
|
||||
os_version = plistlib.load(Path(path + "/System/Library/CoreServices/SystemVersion.plist").open("rb"))
|
||||
kernel_version = os_data.os_conversion.os_to_kernel(os_version["ProductVersion"])
|
||||
if int(kernel_version) < os_data.os_data.big_sur:
|
||||
logging.info("Installer unsupported, requires Big Sur or newer")
|
||||
return
|
||||
|
||||
subprocess.run(["mkdir", "-p", f"{path}/Library/Packages/"])
|
||||
subprocess.run(["cp", "-r", self.constants.installer_pkg_path, f"{path}/Library/Packages/"])
|
||||
|
||||
self._kdk_chainload(os_version["ProductBuildVersion"], os_version["ProductVersion"], Path(path + "/Library/Packages/"))
|
||||
|
||||
|
||||
def _kdk_chainload(self, build: str, version: str, download_dir: str):
|
||||
"""
|
||||
Download the correct KDK to be chainloaded in the macOS installer
|
||||
|
||||
Parameters
|
||||
build (str): The build number of the macOS installer (e.g. 20A5343j)
|
||||
version (str): The version of the macOS installer (e.g. 11.0.1)
|
||||
"""
|
||||
|
||||
kdk_dmg_path = Path(download_dir) / "KDK.dmg"
|
||||
kdk_pkg_path = Path(download_dir) / "KDK.pkg"
|
||||
|
||||
if kdk_dmg_path.exists():
|
||||
kdk_dmg_path.unlink()
|
||||
if kdk_pkg_path.exists():
|
||||
kdk_pkg_path.unlink()
|
||||
|
||||
logging.info("Initiating KDK download")
|
||||
logging.info(f"- Build: {build}")
|
||||
logging.info(f"- Version: {version}")
|
||||
logging.info(f"- Working Directory: {download_dir}")
|
||||
|
||||
kdk_obj = kdk_handler.KernelDebugKitObject(self.constants, build, version, ignore_installed=True)
|
||||
if kdk_obj.success is False:
|
||||
logging.info("Failed to retrieve KDK")
|
||||
logging.info(kdk_obj.error_msg)
|
||||
return
|
||||
|
||||
kdk_download_obj = kdk_obj.retrieve_download(override_path=kdk_dmg_path)
|
||||
if kdk_download_obj is None:
|
||||
logging.info("Failed to retrieve KDK")
|
||||
logging.info(kdk_obj.error_msg)
|
||||
|
||||
# Check remaining disk space before downloading
|
||||
space = utilities.get_free_space(download_dir)
|
||||
if space < (kdk_obj.kdk_url_expected_size * 2):
|
||||
logging.info("Not enough disk space to download and install KDK")
|
||||
logging.info(f"Attempting to download locally first")
|
||||
if space < kdk_obj.kdk_url_expected_size:
|
||||
logging.info("Not enough disk space to install KDK, skipping")
|
||||
return
|
||||
# Ideally we'd download the KDK onto the disk to display progress in the UI
|
||||
# However we'll just download to our temp directory and move it to the target disk
|
||||
kdk_dmg_path = self.constants.kdk_download_path
|
||||
|
||||
kdk_download_obj.download(spawn_thread=False)
|
||||
if kdk_download_obj.download_complete is False:
|
||||
logging.info("Failed to download KDK")
|
||||
logging.info(kdk_download_obj.error_msg)
|
||||
return
|
||||
|
||||
if not kdk_dmg_path.exists():
|
||||
logging.info(f"KDK missing: {kdk_dmg_path}")
|
||||
return
|
||||
|
||||
# Now that we have a KDK, extract it to get the pkg
|
||||
with tempfile.TemporaryDirectory() as mount_point:
|
||||
logging.info("Mounting KDK")
|
||||
result = subprocess.run(["hdiutil", "attach", kdk_dmg_path, "-mountpoint", mount_point, "-nobrowse"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
if result.returncode != 0:
|
||||
logging.info("Failed to mount KDK")
|
||||
logging.info(result.stdout.decode("utf-8"))
|
||||
return
|
||||
|
||||
logging.info("Copying KDK")
|
||||
subprocess.run(["cp", "-r", f"{mount_point}/KernelDebugKit.pkg", kdk_pkg_path])
|
||||
|
||||
logging.info("Unmounting KDK")
|
||||
result = subprocess.run(["hdiutil", "detach", mount_point], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
if result.returncode != 0:
|
||||
logging.info("Failed to unmount KDK")
|
||||
logging.info(result.stdout.decode("utf-8"))
|
||||
return
|
||||
|
||||
logging.info("Removing KDK Disk Image")
|
||||
kdk_dmg_path.unlink()
|
||||
|
||||
def _validate_installer_pkg(self, disk: str) -> bool:
|
||||
logging.info("Validating installer pkg")
|
||||
error_message = ""
|
||||
def _integrity_check():
|
||||
nonlocal error_message
|
||||
for folder in Path(utilities.grab_mount_point_from_disk(disk + "s2")).glob("*.app"):
|
||||
if folder.is_dir():
|
||||
dmg_path = folder / "Contents" / "SharedSupport" / "SharedSupport.dmg"
|
||||
break
|
||||
|
||||
if not Path(dmg_path).exists():
|
||||
logging.error(f"Failed to find {dmg_path}")
|
||||
error_message = f"Failed to find {dmg_path}"
|
||||
return error_message
|
||||
result = subprocess.run(["hdiutil", "verify", dmg_path],stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
if result.returncode != 0:
|
||||
if result.stdout:
|
||||
logging.error(result.stdout.decode("utf-8"))
|
||||
error_message = "STDOUT: " + result.stdout.decode("utf-8")
|
||||
if result.stderr:
|
||||
logging.error(result.stderr.decode("utf-8"))
|
||||
error_message += "\n\nSTDERR: " + result.stderr.decode("utf-8")
|
||||
|
||||
|
||||
thread = threading.Thread(target=_integrity_check)
|
||||
thread.start()
|
||||
while thread.is_alive():
|
||||
wx.Yield()
|
||||
|
||||
if error_message == "":
|
||||
logging.info("Installer pkg validated")
|
||||
return error_message
|
||||
|
||||
return error_message
|
||||
|
||||
|
||||
def on_return_to_main_menu(self, event: wx.Event = None):
|
||||
if self.frame_modal:
|
||||
self.frame_modal.Hide()
|
||||
if self:
|
||||
if isinstance(self, wx.Frame):
|
||||
self.Hide()
|
||||
main_menu_frame = gui_main_menu.MainFrame(
|
||||
None,
|
||||
title=self.title,
|
||||
global_constants=self.constants,
|
||||
screen_location=self.GetScreenPosition()
|
||||
)
|
||||
main_menu_frame.Show()
|
||||
if self.frame_modal:
|
||||
self.frame_modal.Destroy()
|
||||
if self:
|
||||
if isinstance(self, wx.Frame):
|
||||
self.Destroy()
|
||||
324
resources/wx_gui/gui_main_menu.py
Normal file
@@ -0,0 +1,324 @@
|
||||
# Generate GUI for main menu
|
||||
import wx
|
||||
import sys
|
||||
import logging
|
||||
import threading
|
||||
import webbrowser
|
||||
|
||||
from resources.wx_gui import (
|
||||
gui_build,
|
||||
gui_macos_installer_download,
|
||||
gui_support,
|
||||
gui_help,
|
||||
gui_settings,
|
||||
gui_sys_patch_start,
|
||||
gui_sys_patch_display,
|
||||
gui_update,
|
||||
)
|
||||
from resources import (
|
||||
constants,
|
||||
global_settings,
|
||||
updates
|
||||
)
|
||||
from data import os_data
|
||||
|
||||
|
||||
class MainFrame(wx.Frame):
|
||||
def __init__(self, parent: wx.Frame, title: str, global_constants: constants.Constants, screen_location: tuple = None):
|
||||
logging.info("Initializing Main Menu Frame")
|
||||
super(MainFrame, self).__init__(parent, title=title, size=(600, 400), style=wx.DEFAULT_FRAME_STYLE & ~(wx.RESIZE_BORDER | wx.MAXIMIZE_BOX))
|
||||
gui_support.GenerateMenubar(self, global_constants).generate()
|
||||
|
||||
self.constants: constants.Constants = global_constants
|
||||
self.title: str = title
|
||||
|
||||
self.model_label: wx.StaticText = None
|
||||
self.build_button: wx.Button = None
|
||||
|
||||
self.constants.update_stage = gui_support.AutoUpdateStages.INACTIVE
|
||||
|
||||
self._generate_elements()
|
||||
|
||||
self.Centre()
|
||||
self.Show()
|
||||
|
||||
|
||||
self._preflight_checks()
|
||||
|
||||
|
||||
def _generate_elements(self) -> None:
|
||||
"""
|
||||
Generate UI elements for the main menu
|
||||
|
||||
Format:
|
||||
- Title label: OpenCore Legacy Patcher v{X.Y.Z}
|
||||
- Text: Model: {Build or Host Model}
|
||||
- Buttons:
|
||||
- Build and Install OpenCore
|
||||
- Post-Install Root Patch
|
||||
- Create macOS Installer
|
||||
- Settings
|
||||
- Help
|
||||
- Text: Copyright
|
||||
"""
|
||||
|
||||
# Title label: OpenCore Legacy Patcher v{X.Y.Z}
|
||||
title_label = wx.StaticText(self, label=f"OpenCore Legacy Patcher v{self.constants.patcher_version}", pos=(-1,10))
|
||||
title_label.SetFont(wx.Font(19, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont"))
|
||||
title_label.Centre(wx.HORIZONTAL)
|
||||
|
||||
# Text: Model: {Build or Host Model}
|
||||
model_label = wx.StaticText(self, label=f"Model: {self.constants.custom_model or self.constants.computer.real_model}", pos=(-1, title_label.GetPosition()[1] + 25
|
||||
))
|
||||
model_label.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont"))
|
||||
model_label.Centre(wx.HORIZONTAL)
|
||||
self.model_label = model_label
|
||||
|
||||
# Buttons:
|
||||
menu_buttons = {
|
||||
"Build and Install OpenCore": {
|
||||
"function": self.on_build_and_install,
|
||||
"description": [
|
||||
"Prepares provided drive to be able",
|
||||
"to boot unsupported OSes.",
|
||||
"Use on installers or internal drives."
|
||||
],
|
||||
"icon": str(self.constants.icns_resource_path / "OC-Build.icns"),
|
||||
},
|
||||
"Create macOS Installer": {
|
||||
"function": self.on_create_macos_installer,
|
||||
"description": [
|
||||
"Download and flash a macOS",
|
||||
"Installer for your system.",
|
||||
],
|
||||
"icon": str(self.constants.icns_resource_path / "OC-Installer.icns"),
|
||||
},
|
||||
"⚙️ Settings": {
|
||||
"function": self.on_settings,
|
||||
"description": [
|
||||
],
|
||||
},
|
||||
"Post-Install Root Patch": {
|
||||
"function": self.on_post_install_root_patch,
|
||||
"description": [
|
||||
"Installs hardware drivers and",
|
||||
"patches for your system after",
|
||||
"installing a new version of macOS.",
|
||||
],
|
||||
"icon": str(self.constants.icns_resource_path / "OC-Patch.icns"),
|
||||
},
|
||||
|
||||
"Support": {
|
||||
"function": self.on_help,
|
||||
"description": [
|
||||
"Resources for OpenCore Legacy",
|
||||
"Patcher.",
|
||||
],
|
||||
"icon": str(self.constants.icns_resource_path / "OC-Support.icns"),
|
||||
},
|
||||
}
|
||||
button_x = 30
|
||||
button_y = model_label.GetPosition()[1] + 30
|
||||
rollover = len(menu_buttons) / 2
|
||||
if rollover % 1 != 0:
|
||||
rollover = int(rollover) + 1
|
||||
index = 0
|
||||
max_height = 0
|
||||
for button_name, button_function in menu_buttons.items():
|
||||
# place icon
|
||||
if "icon" in button_function:
|
||||
icon = wx.StaticBitmap(self, bitmap=wx.Bitmap(button_function["icon"], wx.BITMAP_TYPE_ICON), pos=(button_x - 10, button_y), size=(64, 64))
|
||||
if button_name == "Post-Install Root Patch":
|
||||
icon.SetPosition((-1, button_y + 7))
|
||||
if button_name == "Create macOS Installer":
|
||||
icon.SetPosition((button_x - 5, button_y + 3))
|
||||
if button_name == "Support":
|
||||
# icon_mac.SetSize((80, 80))
|
||||
icon.SetPosition((button_x - 7, button_y + 3))
|
||||
if button_name == "Build and Install OpenCore":
|
||||
icon.SetSize((70, 70))
|
||||
if button_name == "⚙️ Settings":
|
||||
button_y += 5
|
||||
|
||||
button = wx.Button(self, label=button_name, pos=(button_x + 70, button_y), size=(180, 30))
|
||||
button.Bind(wx.EVT_BUTTON, lambda event, function=button_function["function"]: function(event))
|
||||
button_y += 30
|
||||
|
||||
# # Text: Description
|
||||
description_label = wx.StaticText(self, label='\n'.join(button_function["description"]), pos=(button_x + 75, button.GetPosition()[1] + button.GetSize()[1] + 3))
|
||||
description_label.SetFont(wx.Font(10, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont"))
|
||||
# button_y += 15
|
||||
|
||||
for i, line in enumerate(button_function["description"]):
|
||||
if line == "":
|
||||
continue
|
||||
if i == 0:
|
||||
button_y += 11
|
||||
else:
|
||||
button_y += 13
|
||||
|
||||
button_y += 25
|
||||
|
||||
if button_name == "Build and Install OpenCore":
|
||||
self.build_button = button
|
||||
if gui_support.CheckProperties(self.constants).host_can_build() is False:
|
||||
button.Disable()
|
||||
elif button_name == "Post-Install Root Patch":
|
||||
if self.constants.detected_os < os_data.os_data.big_sur:
|
||||
button.Disable()
|
||||
elif button_name == "⚙️ Settings":
|
||||
button.SetSize((100, -1))
|
||||
button.Centre(wx.HORIZONTAL)
|
||||
description_label.Centre(wx.HORIZONTAL)
|
||||
|
||||
index += 1
|
||||
if index == rollover:
|
||||
max_height = button_y
|
||||
button_x = 320
|
||||
button_y = model_label.GetPosition()[1] + 30
|
||||
|
||||
|
||||
# Text: Copyright
|
||||
copy_label = wx.StaticText(self, label=self.constants.copyright_date, pos=(-1, max_height - 15))
|
||||
copy_label.SetFont(wx.Font(10, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont"))
|
||||
copy_label.Centre(wx.HORIZONTAL)
|
||||
|
||||
# Set window size
|
||||
self.SetSize((-1, copy_label.GetPosition()[1] + 50))
|
||||
|
||||
|
||||
def _preflight_checks(self):
|
||||
if (
|
||||
self.constants.computer.build_model != None and
|
||||
self.constants.computer.build_model != self.constants.computer.real_model and
|
||||
self.constants.host_is_hackintosh is False
|
||||
):
|
||||
# Notify user they're booting an unsupported configuration
|
||||
pop_up = wx.MessageDialog(
|
||||
self,
|
||||
f"We found you are currently booting OpenCore built for a different unit: {self.constants.computer.build_model}\n\nWe builds configs to match individual units and cannot be mixed or reused with different Macs.\n\nPlease Build and Install a new OpenCore config, and reboot your Mac.",
|
||||
"Unsupported Configuration Detected!",
|
||||
style=wx.OK | wx.ICON_EXCLAMATION
|
||||
)
|
||||
pop_up.ShowModal()
|
||||
self.on_build_and_install()
|
||||
return
|
||||
|
||||
if "--update_installed" in sys.argv and self.constants.has_checked_updates is False and gui_support.CheckProperties(self.constants).host_can_build():
|
||||
# Notify user that the update has been installed
|
||||
self.constants.has_checked_updates = True
|
||||
pop_up = wx.MessageDialog(
|
||||
self,
|
||||
f"OpenCore Legacy Patcher has been updated to the latest version: {self.constants.patcher_version}\n\nWould you like to update OpenCore and your root volume patches?",
|
||||
"Update successful!",
|
||||
style=wx.YES_NO | wx.YES_DEFAULT | wx.ICON_INFORMATION
|
||||
)
|
||||
pop_up.ShowModal()
|
||||
|
||||
if pop_up.GetReturnCode() != wx.ID_YES:
|
||||
print("Skipping OpenCore and root volume patch update...")
|
||||
return
|
||||
|
||||
|
||||
print("Updating OpenCore and root volume patches...")
|
||||
self.constants.update_stage = gui_support.AutoUpdateStages.CHECKING
|
||||
self.Hide()
|
||||
pos = self.GetPosition()
|
||||
gui_build.BuildFrame(
|
||||
parent=None,
|
||||
title=self.title,
|
||||
global_constants=self.constants,
|
||||
screen_location=pos
|
||||
)
|
||||
self.Close()
|
||||
|
||||
threading.Thread(target=self._check_for_updates).start()
|
||||
|
||||
|
||||
def _check_for_updates(self):
|
||||
if self.constants.has_checked_updates is True:
|
||||
return
|
||||
|
||||
ignore_updates = global_settings.GlobalEnviromentSettings().read_property("IgnoreAppUpdates")
|
||||
if ignore_updates is True:
|
||||
self.constants.ignore_updates = True
|
||||
return
|
||||
|
||||
self.constants.ignore_updates = False
|
||||
self.constants.has_checked_updates = True
|
||||
dict = updates.CheckBinaryUpdates(self.constants).check_binary_updates()
|
||||
if not dict:
|
||||
return
|
||||
|
||||
for entry in dict:
|
||||
version = dict[entry]["Version"]
|
||||
logging.info(f"New version: {version}")
|
||||
dialog = wx.MessageDialog(
|
||||
parent=self,
|
||||
message=f"Current Version: {self.constants.patcher_version}{' (Nightly)' if not self.constants.commit_info[0].startswith('refs/tags') else ''}\nNew version: {version}\nWould you like to update?",
|
||||
caption="Update Available for OpenCore Legacy Patcher!",
|
||||
style=wx.YES_NO | wx.CANCEL | wx.ICON_QUESTION
|
||||
)
|
||||
dialog.SetYesNoCancelLabels("Download and install", "Ignore", "View on Github")
|
||||
response = dialog.ShowModal()
|
||||
|
||||
if response == wx.ID_YES:
|
||||
wx.CallAfter(self.on_update, dict[entry]["Link"], version)
|
||||
elif response == wx.ID_CANCEL:
|
||||
webbrowser.open(dict[entry]["Github Link"])
|
||||
|
||||
|
||||
def on_build_and_install(self, event: wx.Event = None):
|
||||
self.Hide()
|
||||
gui_build.BuildFrame(
|
||||
parent=None,
|
||||
title=self.title,
|
||||
global_constants=self.constants,
|
||||
screen_location=self.GetPosition()
|
||||
)
|
||||
self.Destroy()
|
||||
|
||||
|
||||
def on_post_install_root_patch(self, event: wx.Event = None):
|
||||
gui_sys_patch_display.SysPatchDisplayFrame(
|
||||
parent=self,
|
||||
title=self.title,
|
||||
global_constants=self.constants,
|
||||
screen_location=self.GetPosition()
|
||||
)
|
||||
|
||||
|
||||
def on_create_macos_installer(self, event: wx.Event = None):
|
||||
gui_macos_installer_download.macOSInstallerDownloadFrame(
|
||||
parent=self,
|
||||
title=self.title,
|
||||
global_constants=self.constants,
|
||||
screen_location=self.GetPosition()
|
||||
)
|
||||
|
||||
|
||||
def on_settings(self, event: wx.Event = None):
|
||||
gui_settings.SettingsFrame(
|
||||
parent=self,
|
||||
title=self.title,
|
||||
global_constants=self.constants,
|
||||
screen_location=self.GetPosition()
|
||||
)
|
||||
|
||||
def on_help(self, event: wx.Event = None):
|
||||
gui_help.HelpFrame(
|
||||
parent=self,
|
||||
title=self.title,
|
||||
global_constants=self.constants,
|
||||
screen_location=self.GetPosition()
|
||||
)
|
||||
|
||||
def on_update(self, oclp_url: str, oclp_version: str):
|
||||
gui_update.UpdateFrame(
|
||||
parent=self,
|
||||
title=self.title,
|
||||
global_constants=self.constants,
|
||||
screen_location=self.GetPosition(),
|
||||
url=oclp_url,
|
||||
version_label=oclp_version
|
||||
)
|
||||
1279
resources/wx_gui/gui_settings.py
Normal file
334
resources/wx_gui/gui_support.py
Normal file
@@ -0,0 +1,334 @@
|
||||
import wx
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import logging
|
||||
import plistlib
|
||||
import threading
|
||||
import subprocess
|
||||
import applescript
|
||||
|
||||
import packaging.version
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from resources.wx_gui import gui_about
|
||||
from resources import constants
|
||||
from data import model_array, os_data, smbios_data
|
||||
|
||||
|
||||
class AutoUpdateStages:
|
||||
INACTIVE = 0
|
||||
CHECKING = 1
|
||||
BUILDING = 2
|
||||
INSTALLING = 3
|
||||
ROOT_PATCHING = 4
|
||||
FINISHED = 5
|
||||
|
||||
|
||||
class GenerateMenubar:
|
||||
|
||||
def __init__(self, frame: wx.Frame, global_constants: constants.Constants) -> None:
|
||||
self.frame: wx.Frame = frame
|
||||
self.constants: constants.Constants = global_constants
|
||||
|
||||
|
||||
def generate(self) -> wx.MenuBar:
|
||||
menubar = wx.MenuBar()
|
||||
fileMenu = wx.Menu()
|
||||
|
||||
aboutItem = fileMenu.Append(wx.ID_ABOUT, "&About OpenCore Legacy Patcher")
|
||||
fileMenu.AppendSeparator()
|
||||
relaunchItem = fileMenu.Append(wx.ID_ANY, "&Relaunch as Root")
|
||||
fileMenu.AppendSeparator()
|
||||
revealLogItem = fileMenu.Append(wx.ID_ANY, "&Reveal Log File")
|
||||
|
||||
menubar.Append(fileMenu, "&File")
|
||||
self.frame.SetMenuBar(menubar)
|
||||
|
||||
self.frame.Bind(wx.EVT_MENU, lambda event: gui_about.AboutFrame(self.constants), aboutItem)
|
||||
self.frame.Bind(wx.EVT_MENU, lambda event: RelaunchApplicationAsRoot(self.frame, self.constants).relaunch(None), relaunchItem)
|
||||
self.frame.Bind(wx.EVT_MENU, lambda event: subprocess.run(["open", "-R", self.constants.log_filepath]), revealLogItem)
|
||||
|
||||
if os.geteuid() == 0:
|
||||
relaunchItem.Enable(False)
|
||||
|
||||
|
||||
class GaugePulseCallback:
|
||||
"""
|
||||
Uses an alternative Pulse() method for wx.Gauge() on macOS Monterey+ with non-Metal GPUs
|
||||
Dirty hack, however better to display some form of animation than none at all
|
||||
|
||||
Note: This work-around is no longer needed on hosts using PatcherSupportPkg 1.1.2 or newer
|
||||
"""
|
||||
|
||||
def __init__(self, global_constants: constants.Constants, gauge: wx.Gauge) -> None:
|
||||
self.gauge: wx.Gauge = gauge
|
||||
|
||||
self.pulse_thread: threading.Thread = None
|
||||
self.pulse_thread_active: bool = False
|
||||
|
||||
self.gauge_value: int = 0
|
||||
self.pulse_forward: bool = True
|
||||
|
||||
self.max_value: int = gauge.GetRange()
|
||||
|
||||
self.non_metal_alternative: bool = CheckProperties(global_constants).host_is_non_metal()
|
||||
if self.non_metal_alternative is True:
|
||||
if CheckProperties(global_constants).host_psp_version() >= packaging.version.Version("1.1.2"):
|
||||
self.non_metal_alternative = False
|
||||
|
||||
|
||||
def start_pulse(self) -> None:
|
||||
if self.non_metal_alternative is False:
|
||||
self.gauge.Pulse()
|
||||
return
|
||||
self.pulse_thread_active = True
|
||||
self.pulse_thread = threading.Thread(target=self._pulse)
|
||||
self.pulse_thread.start()
|
||||
|
||||
|
||||
def stop_pulse(self) -> None:
|
||||
if self.non_metal_alternative is False:
|
||||
return
|
||||
self.pulse_thread_active = False
|
||||
self.pulse_thread.join()
|
||||
|
||||
|
||||
def _pulse(self) -> None:
|
||||
while self.pulse_thread_active:
|
||||
if self.gauge_value == 0:
|
||||
self.pulse_forward = True
|
||||
|
||||
elif self.gauge_value == self.max_value:
|
||||
self.pulse_forward = False
|
||||
|
||||
if self.pulse_forward:
|
||||
self.gauge_value += 1
|
||||
else:
|
||||
self.gauge_value -= 1
|
||||
|
||||
wx.CallAfter(self.gauge.SetValue, self.gauge_value)
|
||||
time.sleep(0.005)
|
||||
|
||||
|
||||
class CheckProperties:
|
||||
|
||||
def __init__(self, global_constants: constants.Constants) -> None:
|
||||
self.constants: constants.Constants = global_constants
|
||||
|
||||
|
||||
def host_can_build(self):
|
||||
"""
|
||||
Check if host supports building OpenCore configs
|
||||
"""
|
||||
if self.constants.custom_model:
|
||||
return True
|
||||
if self.constants.host_is_hackintosh is True:
|
||||
return False
|
||||
if self.constants.allow_oc_everywhere is True:
|
||||
return True
|
||||
if self.constants.computer.real_model in model_array.SupportedSMBIOS:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def host_is_non_metal(self, general_check: bool = False):
|
||||
"""
|
||||
Check if host is non-metal
|
||||
Primarily for wx.Gauge().Pulse() workaround (where animation doesn't work on Monterey+)
|
||||
"""
|
||||
|
||||
if self.constants.detected_os < os_data.os_data.monterey and general_check is False:
|
||||
return False
|
||||
if self.constants.detected_os < os_data.os_data.big_sur and general_check is True:
|
||||
return False
|
||||
if not Path("/System/Library/PrivateFrameworks/SkyLight.framework/Versions/A/SkyLightOld.dylib").exists():
|
||||
# SkyLight stubs are only used on non-Metal
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def host_has_cpu_gen(self, gen: int) -> bool:
|
||||
"""
|
||||
Check if host has a CPU generation equal to or greater than the specified generation
|
||||
"""
|
||||
model = self.constants.custom_model if self.constants.custom_model else self.constants.computer.real_model
|
||||
if model in smbios_data.smbios_dictionary:
|
||||
if smbios_data.smbios_dictionary[model]["CPU Generation"] >= gen:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def host_psp_version(self) -> packaging.version.Version:
|
||||
"""
|
||||
Grab PatcherSupportPkg version from OpenCore-Legacy-Patcher.plist
|
||||
"""
|
||||
oclp_plist_path = "/System/Library/CoreServices/OpenCore-Legacy-Patcher.plist"
|
||||
if not Path(oclp_plist_path).exists():
|
||||
return packaging.version.Version("0.0.0")
|
||||
|
||||
oclp_plist = plistlib.load(open(oclp_plist_path, "rb"))
|
||||
if "PatcherSupportPkg" not in oclp_plist:
|
||||
return packaging.version.Version("0.0.0")
|
||||
|
||||
if oclp_plist["PatcherSupportPkg"].startswith("v"):
|
||||
oclp_plist["PatcherSupportPkg"] = oclp_plist["PatcherSupportPkg"][1:]
|
||||
|
||||
return packaging.version.parse(oclp_plist["PatcherSupportPkg"])
|
||||
|
||||
|
||||
class PayloadMount:
|
||||
|
||||
def __init__(self, global_constants: constants.Constants, frame: wx.Frame) -> None:
|
||||
self.constants: constants.Constants = global_constants
|
||||
self.frame: wx.Frame = frame
|
||||
|
||||
|
||||
def is_unpack_finished(self):
|
||||
if self.constants.unpack_thread.is_alive():
|
||||
return False
|
||||
|
||||
if Path(self.constants.payload_kexts_path).exists():
|
||||
return True
|
||||
|
||||
# Raise error to end program
|
||||
popup = wx.MessageDialog(
|
||||
self.frame,
|
||||
f"During unpacking of our internal files, we seemed to have encountered an error.\n\nIf you keep seeing this error, please try rebooting and redownloading the application.",
|
||||
"Internal Error occurred!",
|
||||
style=wx.OK | wx.ICON_EXCLAMATION
|
||||
)
|
||||
popup.ShowModal()
|
||||
self.frame.Freeze()
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
class ThreadHandler(logging.Handler):
|
||||
"""
|
||||
Reroutes logging output to a wx.TextCtrl using UI callbacks
|
||||
"""
|
||||
|
||||
def __init__(self, text_box: wx.TextCtrl):
|
||||
logging.Handler.__init__(self)
|
||||
self.text_box = text_box
|
||||
|
||||
|
||||
def emit(self, record: logging.LogRecord):
|
||||
wx.CallAfter(self.text_box.AppendText, self.format(record) + '\n')
|
||||
|
||||
|
||||
class RestartHost:
|
||||
"""
|
||||
Restarts the host machine
|
||||
"""
|
||||
|
||||
def __init__(self, frame: wx.Frame) -> None:
|
||||
self.frame: wx.Frame = frame
|
||||
|
||||
|
||||
def restart(self, event: wx.Event = None, message: str = ""):
|
||||
self.popup = wx.MessageDialog(
|
||||
self.frame,
|
||||
message,
|
||||
"Reboot to apply?",
|
||||
wx.YES_NO | wx.YES_DEFAULT | wx.ICON_INFORMATION
|
||||
)
|
||||
self.popup.SetYesNoLabels("Reboot", "Ignore")
|
||||
answer = self.popup.ShowModal()
|
||||
if answer == wx.ID_YES:
|
||||
# Reboots with Count Down prompt (user can still dismiss if needed)
|
||||
self.frame.Hide()
|
||||
wx.Yield()
|
||||
try:
|
||||
applescript.AppleScript('tell app "loginwindow" to «event aevtrrst»').run()
|
||||
except applescript.ScriptError as e:
|
||||
logging.error(f"Error while trying to reboot: {e}")
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
class RelaunchApplicationAsRoot:
|
||||
"""
|
||||
Relaunches the application as root
|
||||
"""
|
||||
|
||||
def __init__(self, frame: wx.Frame, global_constants: constants.Constants) -> None:
|
||||
self.constants = global_constants
|
||||
self.frame: wx.Frame = frame
|
||||
|
||||
|
||||
def relaunch(self, event: wx.Event):
|
||||
|
||||
self.dialog = wx.MessageDialog(
|
||||
self.frame,
|
||||
"OpenCore Legacy Patcher needs to relaunch as admin to continue. You will be prompted to enter your password.",
|
||||
"Relaunch as root?",
|
||||
wx.YES_NO | wx.ICON_QUESTION
|
||||
)
|
||||
|
||||
# Show Dialog Box
|
||||
if self.dialog.ShowModal() != wx.ID_YES:
|
||||
logging.info("User cancelled relaunch")
|
||||
return
|
||||
|
||||
timer: int = 5
|
||||
program_arguments: str = ""
|
||||
|
||||
if event:
|
||||
if event.GetEventObject() != wx.Menu:
|
||||
try:
|
||||
if event.GetEventObject().GetLabel() in ["Start Root Patching", "Reinstall Root Patches"]:
|
||||
program_arguments = " --gui_patch"
|
||||
elif event.GetEventObject().GetLabel() == "Revert Root Patches":
|
||||
program_arguments = " --gui_unpatch"
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
if self.constants.launcher_script is None:
|
||||
program_arguments = f"'{self.constants.launcher_binary}'{program_arguments}"
|
||||
else:
|
||||
program_arguments = f"{self.constants.launcher_binary} {self.constants.launcher_script}{program_arguments}"
|
||||
|
||||
# Relaunch as root
|
||||
args = [
|
||||
"osascript",
|
||||
"-e",
|
||||
f'''do shell script "{program_arguments}"'''
|
||||
' with prompt "OpenCore Legacy Patcher needs administrator privileges to relaunch as admin."'
|
||||
" with administrator privileges"
|
||||
" without altering line endings",
|
||||
]
|
||||
|
||||
self.frame.DestroyChildren()
|
||||
self.frame.SetSize(300, 300)
|
||||
self.frame.Centre()
|
||||
|
||||
# Header
|
||||
header = wx.StaticText(self.frame, label="Relaunching as root", pos=(-1, 5))
|
||||
header.SetFont(wx.Font(19, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont"))
|
||||
header.Centre(wx.HORIZONTAL)
|
||||
|
||||
# Add count down label
|
||||
countdown_label = wx.StaticText(self.frame, label=f"Closing old process in {timer} seconds", pos=(0, header.GetPosition().y + header.GetSize().height + 3))
|
||||
countdown_label.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont"))
|
||||
countdown_label.Centre(wx.HORIZONTAL)
|
||||
|
||||
# Set size of frame
|
||||
self.frame.SetSize((-1, countdown_label.GetPosition().y + countdown_label.GetSize().height + 40))
|
||||
|
||||
wx.Yield()
|
||||
|
||||
logging.info(f"Relaunching as root with command: {program_arguments}")
|
||||
subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
|
||||
while True:
|
||||
wx.Yield()
|
||||
countdown_label.SetLabel(f"Closing old process in {timer} seconds")
|
||||
time.sleep(1)
|
||||
timer -= 1
|
||||
if timer == 0:
|
||||
break
|
||||
|
||||
sys.exit(0)
|
||||
338
resources/wx_gui/gui_sys_patch_display.py
Normal file
@@ -0,0 +1,338 @@
|
||||
|
||||
import wx
|
||||
import os
|
||||
import logging
|
||||
import plistlib
|
||||
import threading
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from resources import (
|
||||
constants,
|
||||
)
|
||||
from resources.sys_patch import (
|
||||
sys_patch_detect
|
||||
)
|
||||
from resources.wx_gui import (
|
||||
gui_main_menu,
|
||||
gui_support,
|
||||
gui_sys_patch_start,
|
||||
)
|
||||
|
||||
|
||||
class SysPatchDisplayFrame(wx.Frame):
|
||||
"""
|
||||
Create a modal frame for displaying root patches
|
||||
"""
|
||||
def __init__(self, parent: wx.Frame, title: str, global_constants: constants.Constants, screen_location: tuple = None):
|
||||
logging.info("Initializing Root Patch Display Frame")
|
||||
|
||||
if parent:
|
||||
self.frame = parent
|
||||
else:
|
||||
super().__init__(parent, title=title, size=(360, 200), style=wx.DEFAULT_FRAME_STYLE ^ wx.RESIZE_BORDER ^ wx.MAXIMIZE_BOX)
|
||||
self.frame = self
|
||||
self.frame.Centre()
|
||||
|
||||
self.title = title
|
||||
self.constants: constants.Constants = global_constants
|
||||
self.frame_modal: wx.Dialog = None
|
||||
self.return_button: wx.Button = None
|
||||
self.available_patches: bool = False
|
||||
self.init_with_parent = True if parent else False
|
||||
|
||||
self.frame_modal = wx.Dialog(self.frame, title=title, size=(360, 200))
|
||||
|
||||
self._generate_elements_display_patches(self.frame_modal)
|
||||
|
||||
if self.constants.update_stage != gui_support.AutoUpdateStages.INACTIVE:
|
||||
if self.available_patches is False:
|
||||
gui_support.RestartHost(self.frame).restart(message="No root patch updates needed!\n\nWould you like to reboot to apply the new OpenCore build?")
|
||||
|
||||
|
||||
def _generate_elements_display_patches(self, frame: wx.Frame = None) -> None:
|
||||
"""
|
||||
Generate UI elements for root patching frame
|
||||
|
||||
Format:
|
||||
- Title label: Post-Install Menu
|
||||
- Label: Available patches:
|
||||
- Labels: {patch name}
|
||||
- Button: Start Root Patching
|
||||
- Button: Revert Root Patches
|
||||
- Button: Return to Main Menu
|
||||
"""
|
||||
frame = self if not frame else frame
|
||||
|
||||
title_label = wx.StaticText(frame, label="Post-Install Menu", pos=(-1, 10))
|
||||
title_label.SetFont(wx.Font(19, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont"))
|
||||
title_label.Centre(wx.HORIZONTAL)
|
||||
|
||||
# Label: Fetching patches...
|
||||
available_label = wx.StaticText(frame, label="Fetching patches for host", pos=(-1, title_label.GetPosition()[1] + title_label.GetSize()[1] + 10))
|
||||
available_label.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont"))
|
||||
available_label.Centre(wx.HORIZONTAL)
|
||||
|
||||
# Progress bar
|
||||
progress_bar = wx.Gauge(frame, range=100, pos=(-1, available_label.GetPosition()[1] + available_label.GetSize()[1] + 10), size=(250, 20))
|
||||
progress_bar.Centre(wx.HORIZONTAL)
|
||||
progress_bar_animation = gui_support.GaugePulseCallback(self.constants, progress_bar)
|
||||
progress_bar_animation.start_pulse()
|
||||
|
||||
# Set window height
|
||||
frame.SetSize((-1, progress_bar.GetPosition()[1] + progress_bar.GetSize()[1] + 40))
|
||||
|
||||
# Labels: {patch name}
|
||||
patches: dict = {}
|
||||
def _fetch_patches(self) -> None:
|
||||
nonlocal patches
|
||||
patches = sys_patch_detect.DetectRootPatch(self.constants.computer.real_model, self.constants).detect_patch_set()
|
||||
|
||||
thread = threading.Thread(target=_fetch_patches, args=(self,))
|
||||
thread.start()
|
||||
|
||||
frame.ShowWindowModal()
|
||||
|
||||
while thread.is_alive():
|
||||
wx.Yield()
|
||||
|
||||
|
||||
frame.Close()
|
||||
|
||||
progress_bar.Hide()
|
||||
progress_bar_animation.stop_pulse()
|
||||
|
||||
available_label.SetLabel("Available patches for your system:")
|
||||
available_label.Centre(wx.HORIZONTAL)
|
||||
|
||||
|
||||
can_unpatch: bool = 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):
|
||||
logging.info("No applicable patches available")
|
||||
patches = []
|
||||
|
||||
# Check if OCLP has already applied the same patches
|
||||
no_new_patches = not self._check_if_new_patches_needed(patches) if patches else False
|
||||
|
||||
if not patches:
|
||||
# Prompt user with no patches found
|
||||
patch_label = wx.StaticText(frame, label="No patches required", pos=(-1, available_label.GetPosition()[1] + 20))
|
||||
patch_label.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont"))
|
||||
patch_label.Centre(wx.HORIZONTAL)
|
||||
|
||||
else:
|
||||
# Add Label for each patch
|
||||
i = 0
|
||||
if no_new_patches is True:
|
||||
patch_label = wx.StaticText(frame, label="All applicable patches already installed", pos=(-1, available_label.GetPosition()[1] + 20))
|
||||
patch_label.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont"))
|
||||
patch_label.Centre(wx.HORIZONTAL)
|
||||
i = i + 20
|
||||
else:
|
||||
longest_patch = ""
|
||||
for patch in patches:
|
||||
if (not patch.startswith("Settings") and not patch.startswith("Validation") and patches[patch] is True):
|
||||
if len(patch) > len(longest_patch):
|
||||
longest_patch = patch
|
||||
anchor = wx.StaticText(frame, label=longest_patch, pos=(-1, available_label.GetPosition()[1] + 20))
|
||||
anchor.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont"))
|
||||
anchor.Centre(wx.HORIZONTAL)
|
||||
anchor.Hide()
|
||||
|
||||
logging.info("Available patches:")
|
||||
for patch in patches:
|
||||
if (not patch.startswith("Settings") and not patch.startswith("Validation") and patches[patch] is True):
|
||||
i = i + 20
|
||||
logging.info(f"- {patch}")
|
||||
patch_label = wx.StaticText(frame, label=f"- {patch}", pos=(anchor.GetPosition()[0], available_label.GetPosition()[1] + i))
|
||||
patch_label.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont"))
|
||||
|
||||
if i == 20:
|
||||
patch_label.SetLabel(patch_label.GetLabel().replace("-", ""))
|
||||
patch_label.Centre(wx.HORIZONTAL)
|
||||
|
||||
if patches["Validation: Patching Possible"] is False:
|
||||
# Cannot patch due to the following reasons:
|
||||
patch_label = wx.StaticText(frame, label="Cannot patch due to the following reasons:", pos=(-1, patch_label.GetPosition()[1] + 25))
|
||||
patch_label.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont"))
|
||||
patch_label.Centre(wx.HORIZONTAL)
|
||||
|
||||
longest_patch = ""
|
||||
for patch in patches:
|
||||
if not patch.startswith("Validation"):
|
||||
continue
|
||||
if patches[patch] is False:
|
||||
continue
|
||||
if patch == "Validation: Unpatching Possible":
|
||||
continue
|
||||
|
||||
if len(patch) > len(longest_patch):
|
||||
longest_patch = patch
|
||||
anchor = wx.StaticText(frame, label=longest_patch.split('Validation: ')[1], pos=(-1, patch_label.GetPosition()[1] + 20))
|
||||
anchor.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont"))
|
||||
anchor.Centre(wx.HORIZONTAL)
|
||||
anchor.Hide()
|
||||
|
||||
i = 0
|
||||
for patch in patches:
|
||||
if not patch.startswith("Validation"):
|
||||
continue
|
||||
if patches[patch] is False:
|
||||
continue
|
||||
if patch == "Validation: Unpatching Possible":
|
||||
continue
|
||||
|
||||
patch_label = wx.StaticText(frame, label=f"- {patch.split('Validation: ')[1]}", pos=(anchor.GetPosition()[0], anchor.GetPosition()[1] + i))
|
||||
patch_label.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont"))
|
||||
i = i + 20
|
||||
|
||||
if i == 20:
|
||||
patch_label.SetLabel(patch_label.GetLabel().replace("-", ""))
|
||||
patch_label.Centre(wx.HORIZONTAL)
|
||||
|
||||
else:
|
||||
if self.constants.computer.oclp_sys_version and self.constants.computer.oclp_sys_date:
|
||||
date = self.constants.computer.oclp_sys_date.split(" @")
|
||||
date = date[0] if len(date) == 2 else ""
|
||||
|
||||
patch_text = f"{self.constants.computer.oclp_sys_version}, {date}"
|
||||
|
||||
patch_label = wx.StaticText(frame, label="Root Volume last patched:", pos=(-1, patch_label.GetPosition().y + 25))
|
||||
patch_label.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont"))
|
||||
patch_label.Centre(wx.HORIZONTAL)
|
||||
|
||||
patch_label = wx.StaticText(frame, label=patch_text, pos=(available_label.GetPosition().x - 10, patch_label.GetPosition().y + 20))
|
||||
patch_label.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont"))
|
||||
patch_label.Centre(wx.HORIZONTAL)
|
||||
|
||||
|
||||
# Button: Start Root Patching
|
||||
start_button = wx.Button(frame, label="Start Root Patching", pos=(10, patch_label.GetPosition().y + 25), size=(170, 30))
|
||||
start_button.Bind(wx.EVT_BUTTON, lambda event: self.on_start_root_patching(patches))
|
||||
start_button.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont"))
|
||||
start_button.Centre(wx.HORIZONTAL)
|
||||
|
||||
# Button: Revert Root Patches
|
||||
revert_button = wx.Button(frame, label="Revert Root Patches", pos=(10, start_button.GetPosition().y + start_button.GetSize().height - 5), size=(170, 30))
|
||||
revert_button.Bind(wx.EVT_BUTTON, lambda event: self.on_revert_root_patching(patches))
|
||||
revert_button.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont"))
|
||||
revert_button.Centre(wx.HORIZONTAL)
|
||||
|
||||
# Button: Return to Main Menu
|
||||
return_button = wx.Button(frame, label="Return to Main Menu", pos=(10, revert_button.GetPosition().y + revert_button.GetSize().height), size=(150, 30))
|
||||
return_button.Bind(wx.EVT_BUTTON, self.on_return_dismiss if self.init_with_parent else self.on_return_to_main_menu)
|
||||
return_button.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont"))
|
||||
return_button.Centre(wx.HORIZONTAL)
|
||||
self.return_button = return_button
|
||||
|
||||
# Disable buttons if unsupported
|
||||
if not patches:
|
||||
start_button.Disable()
|
||||
else:
|
||||
self.available_patches = True
|
||||
if patches["Validation: Patching Possible"] is False:
|
||||
start_button.Disable()
|
||||
elif no_new_patches is False:
|
||||
start_button.SetDefault()
|
||||
else:
|
||||
self.available_patches = False
|
||||
if can_unpatch is False:
|
||||
revert_button.Disable()
|
||||
|
||||
# Relaunch as root if not root
|
||||
if os.geteuid() != 0:
|
||||
start_button.Bind(wx.EVT_BUTTON, gui_support.RelaunchApplicationAsRoot(frame, self.constants).relaunch)
|
||||
revert_button.Bind(wx.EVT_BUTTON, gui_support.RelaunchApplicationAsRoot(frame, self.constants).relaunch)
|
||||
|
||||
# Set frame size
|
||||
frame.SetSize((-1, return_button.GetPosition().y + return_button.GetSize().height + 15))
|
||||
frame.ShowWindowModal()
|
||||
|
||||
|
||||
def on_start_root_patching(self, patches: dict):
|
||||
frame = gui_sys_patch_start.SysPatchStartFrame(
|
||||
parent=None,
|
||||
title=self.title,
|
||||
global_constants=self.constants,
|
||||
patches=patches,
|
||||
)
|
||||
frame.start_root_patching()
|
||||
self.on_return_dismiss() if self.init_with_parent else self.on_return_to_main_menu()
|
||||
|
||||
|
||||
def on_revert_root_patching(self, patches: dict):
|
||||
frame = gui_sys_patch_start.SysPatchStartFrame(
|
||||
parent=None,
|
||||
title=self.title,
|
||||
global_constants=self.constants,
|
||||
patches=patches,
|
||||
)
|
||||
frame.revert_root_patching()
|
||||
self.on_return_dismiss() if self.init_with_parent else self.on_return_to_main_menu()
|
||||
|
||||
|
||||
def on_return_to_main_menu(self, event: wx.Event = None):
|
||||
# Get frame from event
|
||||
frame_modal: wx.Dialog = event.GetEventObject().GetParent()
|
||||
frame: wx.Frame = frame_modal.Parent
|
||||
frame_modal.Hide()
|
||||
frame.Hide()
|
||||
|
||||
main_menu_frame = gui_main_menu.MainFrame(
|
||||
None,
|
||||
title=self.title,
|
||||
global_constants=self.constants,
|
||||
)
|
||||
main_menu_frame.Show()
|
||||
frame.Destroy()
|
||||
|
||||
|
||||
def on_return_dismiss(self, event: wx.Event = None):
|
||||
self.frame_modal.Hide()
|
||||
self.frame_modal.Destroy()
|
||||
|
||||
|
||||
def _check_if_new_patches_needed(self, patches: dict) -> bool:
|
||||
"""
|
||||
Checks if any new patches are needed for the user to install
|
||||
Newer users will assume the root patch menu will present missing patches.
|
||||
Thus we'll need to see if the exact same OCLP build was used already
|
||||
"""
|
||||
|
||||
logging.info("Checking if new patches are needed")
|
||||
|
||||
if self.constants.commit_info[0] in ["Running from source", "Built from source"]:
|
||||
return True
|
||||
|
||||
if self.constants.computer.oclp_sys_url != self.constants.commit_info[2]:
|
||||
# If commits are different, assume patches are as well
|
||||
return True
|
||||
|
||||
oclp_plist = "/System/Library/CoreServices/OpenCore-Legacy-Patcher.plist"
|
||||
if not Path(oclp_plist).exists():
|
||||
# If it doesn't exist, no patches were ever installed
|
||||
# ie. all patches applicable
|
||||
return True
|
||||
|
||||
oclp_plist_data = plistlib.load(open(oclp_plist, "rb"))
|
||||
for patch in patches:
|
||||
if (not patch.startswith("Settings") and not patch.startswith("Validation") and patches[patch] is True):
|
||||
# Patches should share the same name as the plist key
|
||||
# See sys_patch_dict.py for more info
|
||||
patch_installed = False
|
||||
for key in oclp_plist_data:
|
||||
if isinstance(oclp_plist_data[key], (bool, int)):
|
||||
continue
|
||||
if "Display Name" not in oclp_plist_data[key]:
|
||||
continue
|
||||
if oclp_plist_data[key]["Display Name"] == patch:
|
||||
patch_installed = True
|
||||
break
|
||||
|
||||
if patch_installed is False:
|
||||
logging.info(f"- Patch {patch} not installed")
|
||||
return True
|
||||
|
||||
logging.info("No new patches detected for system")
|
||||
return False
|
||||
378
resources/wx_gui/gui_sys_patch_start.py
Normal file
@@ -0,0 +1,378 @@
|
||||
|
||||
import wx
|
||||
import sys
|
||||
import time
|
||||
import logging
|
||||
import plistlib
|
||||
import traceback
|
||||
import threading
|
||||
import subprocess
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from resources import (
|
||||
constants,
|
||||
kdk_handler,
|
||||
global_settings,
|
||||
)
|
||||
from resources.sys_patch import (
|
||||
sys_patch,
|
||||
sys_patch_detect
|
||||
)
|
||||
from resources.wx_gui import (
|
||||
gui_main_menu,
|
||||
gui_support,
|
||||
gui_download,
|
||||
)
|
||||
from data import os_data
|
||||
|
||||
|
||||
class SysPatchStartFrame(wx.Frame):
|
||||
"""
|
||||
Create a frame for root patching
|
||||
Uses a Modal Dialog for smoother transition from other frames
|
||||
"""
|
||||
def __init__(self, parent: wx.Frame, title: str, global_constants: constants.Constants, screen_location: tuple = None, patches: dict = {}):
|
||||
logging.info("Initializing Root Patching Frame")
|
||||
|
||||
self.title = title
|
||||
self.constants: constants.Constants = global_constants
|
||||
self.frame_modal: wx.Dialog = None
|
||||
self.return_button: wx.Button = None
|
||||
self.available_patches: bool = False
|
||||
self.patches: dict = patches
|
||||
|
||||
super(SysPatchStartFrame, self).__init__(parent, title=title, size=(350, 200), style=wx.DEFAULT_FRAME_STYLE & ~(wx.RESIZE_BORDER | wx.MAXIMIZE_BOX))
|
||||
gui_support.GenerateMenubar(self, self.constants).generate()
|
||||
self.Centre()
|
||||
|
||||
if self.patches == {}:
|
||||
self.patches = sys_patch_detect.DetectRootPatch(self.constants.computer.real_model, self.constants).detect_patch_set()
|
||||
|
||||
|
||||
def _kdk_download(self, frame: wx.Frame = None) -> bool:
|
||||
frame = self if not frame else frame
|
||||
|
||||
logging.info("KDK missing, generating KDK download frame")
|
||||
|
||||
header = wx.StaticText(frame, label="Downloading Kernel Debug Kit", pos=(-1,5))
|
||||
header.SetFont(wx.Font(19, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont"))
|
||||
header.Centre(wx.HORIZONTAL)
|
||||
|
||||
subheader = wx.StaticText(frame, label="Fetching KDK database...", pos=(-1, header.GetPosition()[1] + header.GetSize()[1] + 5))
|
||||
subheader.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont"))
|
||||
subheader.Centre(wx.HORIZONTAL)
|
||||
|
||||
progress_bar = wx.Gauge(frame, range=100, pos=(-1, subheader.GetPosition()[1] + subheader.GetSize()[1] + 5), size=(250, 20))
|
||||
progress_bar.Centre(wx.HORIZONTAL)
|
||||
|
||||
progress_bar_animation = gui_support.GaugePulseCallback(self.constants, progress_bar)
|
||||
progress_bar_animation.start_pulse()
|
||||
|
||||
# Set size of frame
|
||||
frame.SetSize((-1, progress_bar.GetPosition()[1] + progress_bar.GetSize()[1] + 35))
|
||||
frame.Show()
|
||||
|
||||
# Generate KDK object
|
||||
self.kdk_obj: kdk_handler.KernelDebugKitObject = None
|
||||
def _kdk_thread_spawn():
|
||||
self.kdk_obj = kdk_handler.KernelDebugKitObject(self.constants, self.constants.detected_os_build, self.constants.detected_os_version)
|
||||
|
||||
kdk_thread = threading.Thread(target=_kdk_thread_spawn)
|
||||
kdk_thread.start()
|
||||
|
||||
while kdk_thread.is_alive():
|
||||
wx.Yield()
|
||||
|
||||
if self.kdk_obj.success is False:
|
||||
progress_bar_animation.stop_pulse()
|
||||
progress_bar.SetValue(0)
|
||||
wx.MessageBox(f"KDK download failed: {self.kdk_obj.error_msg}", "Error", wx.OK | wx.ICON_ERROR)
|
||||
return False
|
||||
|
||||
kdk_download_obj = self.kdk_obj.retrieve_download()
|
||||
if not kdk_download_obj:
|
||||
# KDK is already downloaded
|
||||
return True
|
||||
|
||||
gui_download.DownloadFrame(
|
||||
self,
|
||||
title=self.title,
|
||||
global_constants=self.constants,
|
||||
download_obj=kdk_download_obj,
|
||||
item_name=f"KDK Build {self.kdk_obj.kdk_url_build}"
|
||||
)
|
||||
if kdk_download_obj.download_complete is False:
|
||||
return False
|
||||
|
||||
logging.info("KDK download complete, validating with hdiutil")
|
||||
header.SetLabel(f"Validating KDK: {self.kdk_obj.kdk_url_build}")
|
||||
header.Centre(wx.HORIZONTAL)
|
||||
|
||||
subheader.SetLabel("Checking if checksum is valid...")
|
||||
subheader.Centre(wx.HORIZONTAL)
|
||||
wx.Yield()
|
||||
|
||||
progress_bar_animation.stop_pulse()
|
||||
|
||||
if self.kdk_obj.validate_kdk_checksum() is False:
|
||||
progress_bar.SetValue(0)
|
||||
logging.error("KDK checksum validation failed")
|
||||
logging.error(self.kdk_obj.error_msg)
|
||||
msg = wx.MessageDialog(frame, f"KDK checksum validation failed: {self.kdk_obj.error_msg}", "Error", wx.OK | wx.ICON_ERROR)
|
||||
msg.ShowModal()
|
||||
return False
|
||||
|
||||
progress_bar.SetValue(100)
|
||||
|
||||
logging.info("KDK download complete")
|
||||
|
||||
for child in frame.GetChildren():
|
||||
child.Destroy()
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def _generate_modal(self, patches: dict = {}, variant: str = "Root Patching"):
|
||||
"""
|
||||
Create UI for root patching/unpatching
|
||||
"""
|
||||
supported_variants = ["Root Patching", "Revert Root Patches"]
|
||||
if variant not in supported_variants:
|
||||
logging.error(f"Unsupported variant: {variant}")
|
||||
return
|
||||
|
||||
self.frame_modal.Close() if self.frame_modal else None
|
||||
|
||||
dialog = wx.Dialog(self, title=self.title, size=(400, 200))
|
||||
|
||||
# Title
|
||||
title = wx.StaticText(dialog, label=variant, pos=(-1, 10))
|
||||
title.SetFont(wx.Font(19, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont"))
|
||||
title.Centre(wx.HORIZONTAL)
|
||||
|
||||
if variant == "Root Patching":
|
||||
# Label
|
||||
label = wx.StaticText(dialog, label="Root Patching will patch the following:", pos=(-1, title.GetPosition()[1] + 30))
|
||||
label.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont"))
|
||||
label.Centre(wx.HORIZONTAL)
|
||||
|
||||
|
||||
# Get longest patch label, then create anchor for patch labels
|
||||
longest_patch = ""
|
||||
for patch in patches:
|
||||
if (not patch.startswith("Settings") and not patch.startswith("Validation") and patches[patch] is True):
|
||||
if len(patch) > len(longest_patch):
|
||||
longest_patch = patch
|
||||
|
||||
anchor = wx.StaticText(dialog, label=longest_patch, pos=(label.GetPosition()[0], label.GetPosition()[1] + 20))
|
||||
anchor.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont"))
|
||||
anchor.Centre(wx.HORIZONTAL)
|
||||
anchor.Hide()
|
||||
|
||||
# Labels
|
||||
i = 0
|
||||
logging.info("Available patches:")
|
||||
for patch in patches:
|
||||
if (not patch.startswith("Settings") and not patch.startswith("Validation") and patches[patch] is True):
|
||||
logging.info(f"- {patch}")
|
||||
patch_label = wx.StaticText(dialog, label=f"- {patch}", pos=(anchor.GetPosition()[0], label.GetPosition()[1] + 20 + i))
|
||||
patch_label.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont"))
|
||||
i = i + 20
|
||||
|
||||
if i == 20:
|
||||
patch_label.SetLabel(patch_label.GetLabel().replace("-", ""))
|
||||
patch_label.Centre(wx.HORIZONTAL)
|
||||
|
||||
elif i == 0:
|
||||
patch_label = wx.StaticText(dialog, label="No patches to apply", pos=(label.GetPosition()[0], label.GetPosition()[1] + 20))
|
||||
patch_label.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD, False, ".AppleSystemUIFont"))
|
||||
patch_label.Centre(wx.HORIZONTAL)
|
||||
else:
|
||||
patch_label = wx.StaticText(dialog, label="Reverting to last sealed snapshot", pos=(-1, title.GetPosition()[1] + 30))
|
||||
patch_label.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont"))
|
||||
patch_label.Centre(wx.HORIZONTAL)
|
||||
|
||||
|
||||
# Text box
|
||||
text_box = wx.TextCtrl(dialog, pos=(10, patch_label.GetPosition()[1] + 30), size=(400, 400), style=wx.TE_READONLY | wx.TE_MULTILINE | wx.TE_RICH2)
|
||||
text_box.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont"))
|
||||
text_box.Centre(wx.HORIZONTAL)
|
||||
self.text_box = text_box
|
||||
|
||||
# Button: Return to Main Menu
|
||||
return_button = wx.Button(dialog, label="Return to Main Menu", pos=(10, text_box.GetPosition()[1] + text_box.GetSize()[1] + 5), size=(150, 30))
|
||||
return_button.Bind(wx.EVT_BUTTON, self.on_return_to_main_menu)
|
||||
return_button.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, ".AppleSystemUIFont"))
|
||||
return_button.Centre(wx.HORIZONTAL)
|
||||
self.return_button = return_button
|
||||
|
||||
# Set frame size
|
||||
dialog.SetSize((-1, return_button.GetPosition().y + return_button.GetSize().height + 33))
|
||||
self.frame_modal = dialog
|
||||
dialog.ShowWindowModal()
|
||||
|
||||
|
||||
def start_root_patching(self):
|
||||
logging.info("Starting root patching")
|
||||
|
||||
while gui_support.PayloadMount(self.constants, self).is_unpack_finished() is False:
|
||||
wx.Yield()
|
||||
|
||||
if self.patches["Settings: Kernel Debug Kit missing"] is True:
|
||||
if self._kdk_download(self) is False:
|
||||
sys.exit(1)
|
||||
|
||||
self._generate_modal(self.patches, "Root Patching")
|
||||
self.return_button.Disable()
|
||||
|
||||
thread = threading.Thread(target=self._start_root_patching, args=(self.patches,))
|
||||
thread.start()
|
||||
|
||||
while thread.is_alive():
|
||||
wx.Yield()
|
||||
|
||||
self._post_patch()
|
||||
self.return_button.Enable()
|
||||
|
||||
|
||||
def _start_root_patching(self, patches: dict):
|
||||
logger = logging.getLogger()
|
||||
logger.addHandler(gui_support.ThreadHandler(self.text_box))
|
||||
try:
|
||||
sys_patch.PatchSysVolume(self.constants.computer.real_model, self.constants, patches).start_patch()
|
||||
except:
|
||||
logging.error("An internal error occurred while running the Root Patcher:\n")
|
||||
logging.error(traceback.format_exc())
|
||||
logger.removeHandler(logger.handlers[2])
|
||||
|
||||
|
||||
def revert_root_patching(self):
|
||||
logging.info("Reverting root patches")
|
||||
|
||||
self._generate_modal(self.patches, "Revert Root Patches")
|
||||
self.return_button.Disable()
|
||||
|
||||
thread = threading.Thread(target=self._revert_root_patching, args=(self.patches,))
|
||||
thread.start()
|
||||
|
||||
while thread.is_alive():
|
||||
wx.Yield()
|
||||
|
||||
self._post_patch()
|
||||
self.return_button.Enable()
|
||||
|
||||
|
||||
def _revert_root_patching(self, patches: dict):
|
||||
logger = logging.getLogger()
|
||||
logger.addHandler(gui_support.ThreadHandler(self.text_box))
|
||||
try:
|
||||
sys_patch.PatchSysVolume(self.constants.computer.real_model, self.constants, patches).start_unpatch()
|
||||
except:
|
||||
logging.error("An internal error occurred while running the Root Patcher:\n")
|
||||
logging.error(traceback.format_exc())
|
||||
logger.removeHandler(logger.handlers[2])
|
||||
|
||||
|
||||
def on_return_to_main_menu(self, event: wx.Event = None):
|
||||
# Get frame from event
|
||||
frame_modal: wx.Dialog = event.GetEventObject().GetParent()
|
||||
frame: wx.Frame = frame_modal.Parent
|
||||
frame_modal.Hide()
|
||||
frame.Hide()
|
||||
|
||||
main_menu_frame = gui_main_menu.MainFrame(
|
||||
None,
|
||||
title=self.title,
|
||||
global_constants=self.constants,
|
||||
)
|
||||
main_menu_frame.Show()
|
||||
frame.Destroy()
|
||||
|
||||
|
||||
def on_return_dismiss(self, event: wx.Event = None):
|
||||
self.frame_modal.Hide()
|
||||
self.frame_modal.Destroy()
|
||||
|
||||
|
||||
def _post_patch(self):
|
||||
if self.constants.root_patcher_succeeded is False:
|
||||
return
|
||||
|
||||
if self.constants.needs_to_open_preferences is False:
|
||||
gui_support.RestartHost(self.frame_modal).restart(message="Root Patcher finished successfully!\n\nWould you like to reboot now?")
|
||||
return
|
||||
|
||||
if self.constants.detected_os >= os_data.os_data.ventura:
|
||||
gui_support.RestartHost(self.frame_modal).restart(message="Root Patcher finished successfully!\nIf you were prompted to open System Settings to authorize new kexts, this can be ignored. Your system is ready once restarted.\n\nWould you like to reboot now?")
|
||||
return
|
||||
|
||||
# Create dialog box to open System Preferences -> Security and Privacy
|
||||
self.popup = wx.MessageDialog(
|
||||
self.frame_modal,
|
||||
"We just finished installing the patches to your Root Volume!\n\nHowever, Apple requires users to manually approve the kernel extensions installed before they can be used next reboot.\n\nWould you like to open System Preferences?",
|
||||
"Open System Preferences?",
|
||||
wx.YES_NO | wx.ICON_INFORMATION
|
||||
)
|
||||
self.popup.SetYesNoLabels("Open System Preferences", "Ignore")
|
||||
answer = self.popup.ShowModal()
|
||||
if answer == wx.ID_YES:
|
||||
output =subprocess.run(
|
||||
[
|
||||
"osascript", "-e",
|
||||
'tell app "System Preferences" to activate',
|
||||
"-e", 'tell app "System Preferences" to reveal anchor "General" of pane id "com.apple.preference.security"',
|
||||
],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE
|
||||
)
|
||||
if output.returncode != 0:
|
||||
# Some form of fallback if unaccelerated state errors out
|
||||
subprocess.run(["open", "-a", "System Preferences"])
|
||||
time.sleep(5)
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
def _check_if_new_patches_needed(self, patches: dict) -> bool:
|
||||
"""
|
||||
Checks if any new patches are needed for the user to install
|
||||
Newer users will assume the root patch menu will present missing patches.
|
||||
Thus we'll need to see if the exact same OCLP build was used already
|
||||
"""
|
||||
|
||||
logging.info("Checking if new patches are needed")
|
||||
|
||||
if self.constants.commit_info[0] in ["Running from source", "Built from source"]:
|
||||
return True
|
||||
|
||||
if self.constants.computer.oclp_sys_url != self.constants.commit_info[2]:
|
||||
# If commits are different, assume patches are as well
|
||||
return True
|
||||
|
||||
oclp_plist = "/System/Library/CoreServices/OpenCore-Legacy-Patcher.plist"
|
||||
if not Path(oclp_plist).exists():
|
||||
# If it doesn't exist, no patches were ever installed
|
||||
# ie. all patches applicable
|
||||
return True
|
||||
|
||||
oclp_plist_data = plistlib.load(open(oclp_plist, "rb"))
|
||||
for patch in patches:
|
||||
if (not patch.startswith("Settings") and not patch.startswith("Validation") and patches[patch] is True):
|
||||
# Patches should share the same name as the plist key
|
||||
# See sys_patch_dict.py for more info
|
||||
patch_installed = False
|
||||
for key in oclp_plist_data:
|
||||
if isinstance(oclp_plist_data[key], (bool, int)):
|
||||
continue
|
||||
if "Display Name" not in oclp_plist_data[key]:
|
||||
continue
|
||||
if oclp_plist_data[key]["Display Name"] == patch:
|
||||
patch_installed = True
|
||||
break
|
||||
|
||||
if patch_installed is False:
|
||||
logging.info(f"- Patch {patch} not installed")
|
||||
return True
|
||||
|
||||
logging.info("No new patches detected for system")
|
||||
return False
|
||||