Files
ocpm/ocpm/main.py
T
2022-09-03 16:04:08 -04:00

297 lines
9.5 KiB
Python
Executable File

#!/usr/bin/env python3
from __future__ import annotations
import argparse
import os
import plistlib
import shutil
import time
from datetime import datetime
from pathlib import Path
from tempfile import TemporaryDirectory
from zipfile import ZipFile
import requests
import ruamel.yaml
import tqdm as tqdm
from hypy_utils import printc, ensure_dir
from packaging import version
from tqdm.contrib.concurrent import thread_map
from .interaction import print_updates, sizeof_fmt
from .models import Kext, Release
try:
term_len = os.get_terminal_size().columns
bar_len = int(term_len * 0.4)
except Exception:
bar_len = 20
def get_latest_release(name: str, repos: dict, pre: bool):
# Lowercase keys
repos = {k.lower(): v for k, v in repos['Repos'].items()}
name = name.lower()
# Find repo
assert name in repos, f'Kext {name} is not found in our repos. (If it\'s open source, feel free to add it in!)'
repo_info = repos[name]
if isinstance(repo_info, str):
repo = repo_info
else:
repo = repo_info['Repo']
artifact = repo_info.get('Artifact')
assert 'github.com/' in repo, f'For {name}: {repo} is not a github repo, skipping...'
repo = repo.split('github.com/')[1]
# Check latest version
headers = {}
if 'GH_TOKEN' in os.environ:
headers['Authorization'] = f'token {os.environ["GH_TOKEN"]}'
releases = requests.get(f'https://api.github.com/repos/{repo}/releases', headers=headers).json()
if not pre:
releases = [r for r in releases if not r['prerelease']]
latest = releases[0]
return Release.from_github(latest)
def download_file(url: str, file: str | Path):
"""
Helper method handling downloading large files from `url` to `filename`.
Returns a pointer to `filename`.
https://stackoverflow.com/a/42071418/7346633
"""
file = Path(file)
if file.is_file():
return file
chunk_size = 1024
r = requests.get(url, stream=True)
with open(file, 'wb') as f:
pbar = tqdm.tqdm(unit=" MB", total=int(r.headers['Content-Length']) / 1024 / 1024,
bar_format='{desc} {rate_fmt} {remaining} [{bar}] {percentage:.0f}%', ascii=' #', desc=file.name[:bar_len].ljust(bar_len))
for chunk in r.iter_content(chunk_size=chunk_size):
if chunk:
pbar.update(len(chunk) / 1024 / 1024)
f.write(chunk)
return file
def extract_kext(kext_name: str, zip_file: Path, tmp_dir: Path) -> Path | None:
if zip_file.suffix != '.zip':
print(f'Unable to process {zip_file.name}. Currently only zip files are supported.')
return None
with ZipFile(zip_file, 'r') as zipf:
lower = kext_name.lower() + '.kext/'
def find_name():
for n in zipf.namelist():
if n.lower().endswith(lower):
return n
return None
name = find_name()
if not name:
print(f'Unable to find {kext_name}.kext in {zip_file.name}, skipping.')
return None
for to_extract in zipf.namelist():
if to_extract.startswith(name):
zipf.extract(to_extract, tmp_dir)
return tmp_dir / name
def download_updates(efi: Path, updates: list[tuple[Kext, Release]]):
# Create temporary directory
with TemporaryDirectory() as tmp:
start = time.time_ns()
tmp = Path(tmp)
kexts = ensure_dir(tmp / 'extract')
print('Downloading zip files...')
files = [(k, r, download_file(r.artifact.url, tmp / r.artifact.name)) for k, r in updates]
print()
print('Extracting kexts...')
extracted = [(k, r, extract_kext(k.name, f, kexts)) for k, r, f in files]
extracted = [e for e in extracted if e[2]]
# Backup if needed
existing = [t for t in extracted if t[0].path.is_dir()]
if len(existing) > 0:
backup = ensure_dir(efi.parent / f'Backups/{datetime.now().strftime("%m-%d %H-%M")}')
print(f'Backing up original kexts to {backup}...')
for k, r, e in existing:
shutil.move(k.path, backup / k.name)
print(f'Installing new kexts...')
for k, r, e in extracted:
shutil.move(e, k.path)
print()
print(f'✨ All Done in {(time.time_ns() - start) / 1e6:,.0f}s!')
def get_latest_list(names: list[str], repos: dict, args) -> list[Release]:
def get_latest(s: str):
try:
return get_latest_release(s, repos, args.pre)
except AssertionError as e:
print(e)
return None
return thread_map(get_latest, names, desc='Fetching Kexts'.ljust(bar_len), max_workers=32, bar_format='{desc} {rate_fmt} {remaining} [{bar}] {percentage:.0f}%', ascii=' #', unit=' pkg')
def update(args, repos: dict, kexts: list[Kext], efi: Path):
latests = get_latest_list([k.name for k in kexts], repos, args)
# Compare versions
updates: list[tuple[Kext, Release]]
updates = [(k, l) for k, l in zip(kexts, latests) if l and version.parse(l.tag) > version.parse(k.version)]
if len(updates) == 0:
print(f'✨ Everything up-to-date!')
exit(0)
# Print updates
printc(f'\n✨ Found {len(updates)} Updates:')
print_updates(updates)
# Download prompt
print()
print(f'Total download size: {sizeof_fmt(sum(l.artifact.size for k, l in updates))}')
proceed = 'y' if args.y else input(f'🚀 Ready to fly? [y/N] ')
if proceed.lower().strip() != 'y':
print()
print('😕 Huh, okay')
exit(0)
print()
download_updates(efi, updates)
def install(args, repos: dict, kexts: list[Kext], efi: Path):
names = args.install
latests = get_latest_list(names, repos, args)
# Compare versions
updates: list[tuple[Kext, Release]]
updates = [(Kext(efi / 'OC' / 'Kexts' / (n + '.kext'), n, version=l.tag), l) for n, l in zip(names, latests) if l]
if not updates:
printc('&cNo kexts found. Exiting')
return
# Download prompt
print()
print(f'Total download size: {sizeof_fmt(sum(l.artifact.size for k, l in updates))}')
proceed = 'y' if args.y else input(f'🚀 Ready to fly? [y/N] ')
if proceed.lower().strip() != 'y':
print()
print('😕 Huh, okay')
exit(0)
print()
download_updates(efi, updates)
enable(names, kexts, efi)
def enable(names: list[str], kexts: list[Kext], efi: Path):
config_plist: dict = plistlib.loads((efi / 'OC' / 'Config.plist').read_bytes())
config_kexts: list[dict] = config_plist['Kernel']['Add']
def conf_kext_index() -> dict[str, dict]:
return {k['BundlePath'].split('.kext')[0]: k for k in config_kexts}
# Find relevant kexts
index: dict[str: Kext] = {k.path.name.lower().split('.kext')[0]: k for k in kexts}
kexts: list[Kext] = [index.get(n.lower()) for n in names]
for n, orig in zip(kexts, names):
assert n, f'{orig} is not found'
# Modify config.plist
for k in kexts:
# Kext config entry doesn't exist, create one
if k.name not in conf_kext_index():
exec_path = f'Contents/MacOS/{k.name}'
exec_path = exec_path if (k.path / exec_path).is_file() else ''
if not exec_path and (k.path / 'Contents/MacOS').is_dir():
execs = os.listdir(k.path / 'Contents/MacOS')
if execs:
exec_path = f'Contents/MacOS/{execs[0]}'
config_kexts.append({
'Arch': 'x86_64',
'BundlePath': k.path.name,
'Comment': '',
'Enabled': True,
'ExecutablePath': str(exec_path),
'MaxKernel': '',
'MinKernel': '',
'PlistPath': 'Contents/Info.plist'
})
else:
conf_kext_index()[k.name]['Enabled'] = True
# Save config.plist
(efi / 'OC' / 'Config.plist').write_bytes(plistlib.dumps(config_plist))
print('Enabled!')
def run():
parser = argparse.ArgumentParser(description='OpenCore Package Manager by HyDEV')
parser.add_argument('-U', '--update', action='store_true', help='Update')
parser.add_argument('-S', '--install', nargs='+', help='Install packages')
parser.add_argument('-E', '--enable', nargs='+', help='Enable packages in Kexts directory')
# parser.add_argument('-D', '--disable', nargs='+', help='Disable packages')
parser.add_argument('--efi', help='EFI Directory Path', default='.')
parser.add_argument('--pre', action='store_true', help='Use pre-release')
parser.add_argument('-y', action='store_true', help='Say yes')
printc('\n&fOpenCore Package Manager v1.0.0 by HyDEV\n')
args = parser.parse_args()
# Normalize EFI Path
efi = Path(args.efi)
if (efi / 'EFI').is_dir():
efi = efi / 'EFI'
assert (efi / 'OC').is_dir(), 'Open Core directory (OC) not found.'
# Find kexts
kexts_dir = efi / 'OC' / 'Kexts'
kexts = [str(f) for f in os.listdir(kexts_dir)]
kexts = [kexts_dir / f for f in kexts if f.lower().endswith('.kext')]
kexts = [Kext.from_path(k) for k in kexts]
print(f'🔍 Found {len(kexts)} kexts in {kexts_dir}')
# Read Repo
with open(Path(__file__).parent / 'data' / 'OCKextRepos.yml') as f:
repos = ruamel.yaml.safe_load(f)
if args.update:
return update(args, repos, kexts, efi)
if args.install:
return install(args, repos, kexts, efi)
if args.enable:
return enable(args.enable, kexts, efi)
if __name__ == '__main__':
run()