mirror of
https://github.com/sickcodes/Docker-OSX.git
synced 2025-06-21 00:52:51 +02:00
feat: Add config.plist auto-enhancement, UI/UX improvements, and docs rework
This commit introduces several major enhancements: 1. **Experimental `config.plist` Auto-Enhancement (Linux Host for Detection):** * `linux_hardware_info.py`: Added audio codec detection. * `plist_modifier.py`: * Uses detected audio codecs for more accurate `layout-id` selection. * Expanded mappings for Intel Alder Lake iGPUs, more audio devices, and Ethernet kexts. * Refined NVIDIA GTX 970 `boot-args` logic based on target macOS version and iGPU presence. * Creates a `.backup` of `config.plist` before modification and attempts restore on save failure. * Integrated into `main_app.py` with a user-selectable experimental checkbox. 2. **Reworked `README.md`:** * Completely rewritten for "Skyscope" branding and project vision. * Details all current features, including platform-specific USB writing (manual Windows dd step). * Comprehensive prerequisites, including `apfs-fuse` build dependencies for Debian. * Updated usage guide and future enhancement plans. Version set to 0.8.2. 3. **UI/UX Enhancements for Task Progress:** * Added a QTimer-driven text-based spinner to the status bar for active operations. * Centralized UI state management (`_set_ui_busy`, `update_all_button_states`) for consistent feedback and control enabling/disabling. * Refactored completion/error handling into generic slots. 4. **Improved Windows USB Writing Guidance:** * `usb_writer_windows.py` now uses `diskpart` to fetch and display the macOS partition number and byte offset, providing more specific details for your manual `dd` operation. 5. **Debian 13 "Trixie" Compatibility:** * Reviewed dependencies and updated `README.md` with specific notes for `hfsprogs` and `apfs-fuse` installation on Debian-based systems. This set of changes makes the application more intelligent in its OpenCore configuration attempts, improves your feedback during operations, and provides much more comprehensive documentation, while also advancing the capabilities of the platform-specific USB writers.
This commit is contained in:
parent
cf19c71494
commit
e81120e8e9
186
README.md
186
README.md
@ -4,113 +4,137 @@
|
||||
**Developer:** Miss Casey Jay Topojani
|
||||
**Business:** Skyscope Sentinel Intelligence
|
||||
|
||||
## Overview
|
||||
## Vision: Your Effortless Bridge to macOS on PC
|
||||
|
||||
This tool provides a graphical user interface to automate the creation of a bootable macOS USB drive for PC (Hackintosh) using the Docker-OSX project. It guides the user through selecting a macOS version, running the Docker-OSX container for macOS installation, extracting the necessary image files, and (currently for Linux users) writing these images to a USB drive.
|
||||
Welcome to the Skyscope macOS on PC USB Creator Tool! Our vision is to provide an exceptionally user-friendly, GUI-driven application that fully automates the complex process of creating a bootable macOS USB drive for virtually any PC. This tool leverages the power of Docker-OSX and OpenCore, aiming to simplify the Hackintosh journey from start to finish.
|
||||
|
||||
## Features
|
||||
This project is dedicated to creating a seamless experience, from selecting your desired macOS version to generating a USB drive that's ready to boot your PC into macOS, complete with efforts to auto-configure for your hardware.
|
||||
|
||||
* User-friendly GUI for selecting macOS versions (Sonoma, Ventura, Monterey, Big Sur, Catalina).
|
||||
* Automated Docker command generation and execution for Docker-OSX.
|
||||
* Streams Docker logs directly into the application.
|
||||
* Extraction of the generated `mac_hdd_ng.img` (macOS system) and `OpenCore.qcow2` (EFI bootloader).
|
||||
* Management of the created Docker container (stop/remove).
|
||||
* USB drive detection.
|
||||
* Automated USB partitioning and image writing for **Linux systems**.
|
||||
* Creates GPT partition table.
|
||||
* Creates an EFI System Partition (ESP) and a main HFS+ partition for macOS.
|
||||
* Copies EFI files and writes the macOS system image.
|
||||
* Warning prompts before destructive operations like USB writing.
|
||||
* Experimental `config.plist` auto-enhancement based on detected host hardware (currently Linux-only for hardware detection) to potentially improve iGPU, audio, and Ethernet compatibility, and handle NVIDIA GTX 970 specifics. A backup of the original `config.plist` is created.
|
||||
## Current Features & Capabilities
|
||||
|
||||
## Current Status & Known Issues/Limitations
|
||||
* **Intuitive Graphical User Interface (PyQt6):** Guides you through each step of the process.
|
||||
* **macOS Version Selection:** Easily choose from popular macOS versions (Sonoma, Ventura, Monterey, Big Sur, Catalina).
|
||||
* **Automated Docker-OSX Orchestration:**
|
||||
* **Intelligent Image Pulling:** Automatically pulls the required `sickcodes/docker-osx` image from Docker Hub, with progress displayed.
|
||||
* **VM Creation & macOS Installation:** Launches the Docker-OSX container where you can interactively install macOS within a QEMU virtual machine.
|
||||
* **Log Streaming:** View Docker and QEMU logs directly in the application for transparency.
|
||||
* **VM Image Extraction:** Once macOS is installed in the VM, the tool helps you extract the essential disk images (`mac_hdd_ng.img` and `OpenCore.qcow2`).
|
||||
* **Container Management:** Stop and remove the Docker-OSX container after use.
|
||||
* **Cross-Platform USB Drive Preparation:**
|
||||
* **USB Detection:** Identifies potential USB drives on Linux, macOS, and Windows (using WMI for more accurate detection on Windows).
|
||||
* **Automated EFI & macOS System Write (Linux & macOS):**
|
||||
* Partitions the USB drive with a GUID Partition Table (GPT).
|
||||
* Creates and formats an EFI System Partition (FAT32) and a main macOS partition (HFS+).
|
||||
* Uses a robust file-level copy (`rsync`) for both EFI content and the main macOS system, ensuring compatibility with various USB sizes and only copying necessary data.
|
||||
* **Windows USB Writing (Partial Automation):**
|
||||
* Automates EFI partition creation and EFI file copying.
|
||||
* **Important:** Writing the main macOS system image currently requires a guided manual step using an external "dd for Windows" utility due to Windows' limitations with direct, scriptable raw partition writing of HFS+/APFS filesystems. The tool prepares the raw image and provides instructions.
|
||||
* **Experimental `config.plist` Auto-Enhancement:**
|
||||
* **Linux Host Detection:** If the tool is run on a Linux system, it can gather information about your host computer's hardware (iGPU, audio, Ethernet, CPU).
|
||||
* **Targeted Modifications:** Optionally attempts to modify the `config.plist` (from the generated `OpenCore.qcow2`) to:
|
||||
* Add common `DeviceProperties` for Intel iGPUs.
|
||||
* Set appropriate audio `layout-id`s.
|
||||
* Ensure necessary Ethernet kexts are enabled.
|
||||
* Apply boot-args for NVIDIA GTX 970 based on target macOS version (e.g., `nv_disable=1` or `nvda_drv=1`).
|
||||
* A backup of the original `config.plist` is created before modifications.
|
||||
* **Privilege Checking:** Warns if administrative/root privileges are needed for USB writing and are not detected.
|
||||
* **UI Feedback:** Status bar messages and an indeterminate progress bar keep you informed during long operations.
|
||||
|
||||
* **USB Writing Platform Support:** USB writing functionality is currently **only implemented and tested for Linux**. macOS and Windows users can use the tool to generate and extract images but will need to use other methods for USB creation.
|
||||
* **macOS Image Size for USB:** The current Linux USB writing process for the main macOS system uses `dd` to write the converted raw image. While the source `mac_hdd_ng.img` is sparse, the raw conversion makes it its full provisioned size (e.g., 200GB). This means:
|
||||
* The target USB drive must be large enough to hold this full raw size.
|
||||
* This is inefficient and needs to be changed to a file-level copy (e.g., using `rsync` after mounting the source image) to only copy actual data and better fit various USB sizes. (This is a high-priority item based on recent feedback).
|
||||
* **Intel iGPU Compatibility:** Relies on the generic iGPU support provided by WhateverGreen.kext within the OpenCore configuration from Docker-OSX. This works for many iGPUs but isn't guaranteed for all without specific `config.plist` tuning.
|
||||
* **Dependency on Docker-OSX:** This tool orchestrates Docker-OSX. Changes or issues in the upstream Docker-OSX project might affect this tool.
|
||||
* **Elevated Privileges:** For USB writing on Linux, the application currently requires being run with `sudo`. It does not yet have in-app checks or prompts for this.
|
||||
* `config.plist` auto-enhancement is experimental. The hardware detection component for this feature is **currently only implemented for Linux hosts**. While the modification logic is called on macOS, it will not apply hardware-specific changes due to lack of macOS hardware detection in `plist_modifier.py`. Modifications are based on common configurations and may not be optimal for all hardware. Always test thoroughly. A backup of the original `config.plist` (as `config.plist.backup`) is created in the source OpenCore image's EFI directory before modification attempts.
|
||||
## Current Status & Known Limitations
|
||||
|
||||
* **Windows Main OS USB Write:** This is the primary limitation, requiring a manual `dd` step. Future work aims to automate this if a reliable, redistributable CLI tool for raw partition writing is identified or developed.
|
||||
* **`config.plist` Enhancement is Experimental:**
|
||||
* Hardware detection for this feature is **currently only implemented for Linux hosts.** On macOS/Windows, the plist modification step will run but won't apply hardware-specific changes.
|
||||
* The applied patches are based on common configurations and may not be optimal or work for all hardware. Always test thoroughly.
|
||||
* **NVIDIA dGPU Support on Newer macOS:** Modern macOS (Mojave+) does not support NVIDIA Maxwell/Pascal/Turing/Ampere GPUs. The tool attempts to configure systems with these cards for basic display or to use an iGPU if available. Full acceleration is not possible on these macOS versions with these cards.
|
||||
* **Universal Compatibility:** While the goal is broad PC compatibility, Hackintoshing can be hardware-specific. Success is not guaranteed on all possible PC configurations.
|
||||
* **Dependency on External Projects:** Relies on Docker-OSX, OpenCore, and various community-sourced kexts and configurations.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. **Docker:** Docker must be installed and running on your system. The current user must have permissions to run Docker commands.
|
||||
1. **Docker:** Must be installed and running. Your user account needs permission to manage Docker.
|
||||
* [Install Docker Engine](https://docs.docker.com/engine/install/)
|
||||
2. **Python:** Python 3.8+
|
||||
3. **Python Libraries:**
|
||||
* `PyQt6`
|
||||
* `psutil`
|
||||
* Installation: `pip install PyQt6 psutil`
|
||||
4. **(For Linux USB Writing ONLY)**: The following command-line utilities must be installed and accessible in your PATH:
|
||||
* `qemu-img` (usually from `qemu-utils` package)
|
||||
2. **Python:** Version 3.8 or newer.
|
||||
3. **Python Libraries:** Install with `pip install PyQt6 psutil`.
|
||||
4. **Platform-Specific CLI Tools for USB Writing:**
|
||||
|
||||
* **Linux (including Debian 13 "Trixie"):**
|
||||
* `qemu-img` (from `qemu-utils`)
|
||||
* `parted`
|
||||
* `kpartx` (often part of `multipath-tools` or `kpartx` package)
|
||||
* `kpartx` (from `kpartx` or `multipath-tools`)
|
||||
* `rsync`
|
||||
* `mkfs.vfat` (usually from `dosfstools` package)
|
||||
* `mkfs.hfsplus` (usually from `hfsprogs` package)
|
||||
* `apfs-fuse` (may require manual installation from source or a third-party repository/PPA, as it's not always in standard Debian/Ubuntu repos)
|
||||
* `lsblk` (usually from `util-linux` package)
|
||||
* `partprobe` (usually from `parted` or `util-linux` package)
|
||||
* You can typically install most of these on Debian/Ubuntu (including Debian 13 Trixie) with:
|
||||
```bash
|
||||
sudo apt update
|
||||
sudo apt install qemu-utils parted kpartx rsync dosfstools hfsprogs util-linux
|
||||
```
|
||||
* For `apfs-fuse` on Debian/Ubuntu (including Debian 13 Trixie), you will likely need to compile it from its source (e.g., from the `sgan81/apfs-fuse` repository on GitHub). Typical build dependencies include `git g++ cmake libfuse3-dev libicu-dev zlib1g-dev libbz2-dev libssl-dev` (package names may vary slightly, e.g. `libfuse-dev`). Ensure the compiled `apfs-fuse` binary is in your system PATH.
|
||||
* `mkfs.vfat` (from `dosfstools`)
|
||||
* `mkfs.hfsplus` (from `hfsprogs`)
|
||||
* `apfs-fuse`: Often requires manual compilation (e.g., from `sgan81/apfs-fuse` on GitHub). Typical build dependencies: `git g++ cmake libfuse3-dev libicu-dev zlib1g-dev libbz2-dev libssl-dev`. Ensure it's in your PATH.
|
||||
* `lsblk`, `partprobe` (from `util-linux`)
|
||||
* Install most via: `sudo apt update && sudo apt install qemu-utils parted kpartx rsync dosfstools hfsprogs util-linux`
|
||||
* **macOS:**
|
||||
* `qemu-img` (e.g., via Homebrew: `brew install qemu`)
|
||||
* `diskutil`, `hdiutil`, `rsync` (standard macOS tools).
|
||||
* **Windows:**
|
||||
* `qemu-img` (install and add to PATH).
|
||||
* `diskpart`, `robocopy` (standard Windows tools).
|
||||
* `7z.exe` (7-Zip command-line tool, install and add to PATH) - for EFI file extraction.
|
||||
* A "dd for Windows" utility (e.g., from SUSE, chrysocome.net, or similar). Ensure it's in your PATH and you know how to use it for writing to a physical disk's partition or offset.
|
||||
|
||||
## How to Run
|
||||
|
||||
1. Clone this repository or download the source files (`main_app.py`, `utils.py`, `constants.py`, `usb_writer_linux.py`).
|
||||
2. Install the prerequisite Python libraries: `pip install PyQt6 psutil`.
|
||||
3. **(Linux for USB Writing):** Ensure all command-line utilities listed under prerequisites are installed.
|
||||
4. Run the application:
|
||||
```bash
|
||||
python main_app.py
|
||||
```
|
||||
**(Linux for USB Writing):** You will need to run the application with `sudo` for USB writing operations to succeed, due to the nature of disk partitioning and direct write commands:
|
||||
```bash
|
||||
sudo python main_app.py
|
||||
```
|
||||
1. Ensure all prerequisites for your operating system are met.
|
||||
2. Clone this repository or download the source files.
|
||||
3. Install Python libraries: `pip install PyQt6 psutil`.
|
||||
4. Execute `python main_app.py`.
|
||||
5. **Important for USB Writing:**
|
||||
* **Linux:** Run with `sudo python main_app.py`.
|
||||
* **macOS:** The script will use `sudo` internally for `rsync` to USB EFI if needed. You might be prompted for your password. Ensure the main application has Full Disk Access if issues arise with `hdiutil` or `diskutil` not having permissions (System Settings > Privacy & Security).
|
||||
* **Windows:** Run the application as Administrator.
|
||||
|
||||
## Usage Steps
|
||||
## Step-by-Step Usage Guide
|
||||
|
||||
1. **Step 1: Create and Install macOS VM**
|
||||
* Select your desired macOS version from the dropdown.
|
||||
* Launch the "Skyscope macOS on PC USB Creator Tool".
|
||||
* Select your desired macOS version from the dropdown menu.
|
||||
* Click "Create VM and Start macOS Installation".
|
||||
* A Docker container will be started, and a QEMU window will appear.
|
||||
* Follow the on-screen instructions within the QEMU window to install macOS. This is an interactive process (formatting the virtual disk, installing macOS).
|
||||
* Once macOS is installed and you have shut down or closed the QEMU window, the Docker process will finish.
|
||||
* The tool will first pull the necessary Docker image (progress shown).
|
||||
* Then, a QEMU window will appear. This is your virtual machine. Follow the standard macOS installation procedure within this window (use Disk Utility to erase and format the virtual hard drive, then install macOS). This part is interactive.
|
||||
* Once macOS is fully installed in QEMU, shut down the macOS VM from within its own interface (Apple Menu > Shut Down). Closing the QEMU window will also terminate the process.
|
||||
2. **Step 2: Extract VM Images**
|
||||
* After the VM setup process is complete, the "Extract Images from Container" button will become enabled.
|
||||
* Click it and select a directory on your computer where the `mac_hdd_ng.img` and `OpenCore.qcow2` files will be saved.
|
||||
* Wait for both extraction processes to complete.
|
||||
* After the Docker process from Step 1 finishes (QEMU window closes), the "Extract Images from Container" button will become active.
|
||||
* Click it. You'll be prompted to select a directory on your computer. The `mac_hdd_ng.img` (macOS system) and `OpenCore.qcow2` (EFI bootloader) files will be copied here. This may take some time.
|
||||
3. **Step 3: Container Management (Optional)**
|
||||
* After image extraction (or if the VM setup finished), you can "Stop Container" (if it's somehow still running) and then "Remove Container" to clean up the Docker container (which is no longer needed if images are extracted).
|
||||
* Once images are extracted, the Docker container used for installation is no longer strictly needed.
|
||||
* You can "Stop Container" (if it's listed as running by Docker for any reason) and then "Remove Container" to free up disk space.
|
||||
4. **Step 4: Select Target USB Drive and Write**
|
||||
* Connect your target USB drive.
|
||||
* Click "Refresh List" to scan for USB drives.
|
||||
* Select your intended USB drive from the dropdown. **VERIFY CAREFULLY!**
|
||||
* **WARNING:** The next step will erase all data on the selected USB drive.
|
||||
* Optionally, check the '\[Experimental] Auto-enhance config.plist...' box if you want the tool to attempt to modify the OpenCore configuration based on your Linux host's hardware (this feature is Linux-only for detection). This may improve compatibility but use with caution. A backup (`config.plist.backup`) is created in the source OpenCore image's EFI directory before modification.
|
||||
* If you are on Linux and have all dependencies, and the images from Step 2 are ready, the "Write Images to USB Drive" button will be enabled.
|
||||
* Click it and confirm the warning dialog. The application will then partition the USB and write the images. This will take a significant amount of time.
|
||||
* Physically connect your USB flash drive.
|
||||
* Click "Refresh List".
|
||||
* **Linux/macOS:** Select your USB drive from the dropdown. Verify size and identifier carefully.
|
||||
* **Windows:** USB drives detected via WMI will appear in the dropdown. Select the correct one. Ensure it's the `Disk X` number you intend.
|
||||
* **(Optional, Experimental):** Check the "Try to auto-enhance config.plist..." box if you are on a Linux host and wish to attempt automatic `config.plist` modification for your hardware. A backup of the original `config.plist` will be made.
|
||||
* **CRITICAL WARNING:** Double-check your selection. The next action will erase the selected USB drive.
|
||||
* Click "Write Images to USB Drive". Confirm the data erasure warning.
|
||||
* The process will now:
|
||||
* (If enhancement enabled) Attempt to modify the `config.plist` within the source OpenCore image.
|
||||
* Partition and format your USB drive.
|
||||
* Copy EFI files to the USB's EFI partition.
|
||||
* Copy macOS system files to the USB's main partition. (On Windows, this step requires manual `dd` operation as guided by the application).
|
||||
* This is a lengthy process. Monitor the progress in the output area.
|
||||
5. **Boot!**
|
||||
* Once complete, safely eject the USB drive. You can now try booting your PC from it. Remember to configure your PC's BIOS/UEFI for booting from USB and for macOS compatibility (e.g., disable Secure Boot, enable AHCI, XHCI Handoff, etc., as per standard Hackintosh guides like Dortania).
|
||||
|
||||
## Future Enhancements (Based on Feedback)
|
||||
## Future Vision & Enhancements
|
||||
|
||||
* **Improve USB Writing for Image Sizing (High Priority):** Modify the USB writing process (especially for the main macOS system) to use file-level copies (e.g., `rsync` after mounting the source image) instead of `dd` for the entire raw image. This will correctly handle various USB drive sizes by only copying used data and fitting it to the partition.
|
||||
* **Explicit Docker Image Pull:** Add a separate step/feedback for `docker pull` before `docker run`.
|
||||
* **Privilege Handling:** Add checks to see if the application is run with necessary privileges for USB writing and guide the user if not.
|
||||
* **USB Writing for macOS and Windows:** Implement the `usb_writer_macos.py` and `usb_writer_windows.py` modules.
|
||||
* **GUI for Advanced Options:** Potentially allow users to specify custom Docker parameters or OpenCore properties.
|
||||
* **Expand hardware detection for `config.plist` enhancement to also support macOS and Windows hosts.**
|
||||
* **Provide more granular user control and detailed feedback for the `config.plist` enhancement feature (e.g., preview changes, select specific patches).**
|
||||
* **Fully Automated Windows USB Writing:** Replace the manual `dd` step with a reliable, integrated solution.
|
||||
* **Advanced `config.plist` Customization:**
|
||||
* Expand hardware detection to macOS and Windows hosts.
|
||||
* Provide more granular UI controls for plist enhancements (e.g., preview changes, select specific patches).
|
||||
* Allow users to load/save `config.plist` modification profiles.
|
||||
* **Enhanced UI/UX for Progress:** Implement determinate progress bars with percentage completion and more dynamic status updates.
|
||||
* **Debian 13 "Trixie" (and other distros) Validation:** Continuous compatibility checks and dependency streamlining.
|
||||
* **"Universal" Config Strategy (Research):** Investigate advanced techniques for more adaptive OpenCore configurations, though true universality is a significant challenge.
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! Please fork the repository and submit a pull request.
|
||||
Your contributions, feedback, and bug reports are highly welcome! Please fork the repository and submit pull requests, or open issues for discussion.
|
||||
|
||||
## License
|
||||
|
||||
(To be decided - likely MIT or GPLv3)
|
||||
(To be decided - e.g., MIT or GPLv3)
|
||||
|
@ -1,60 +1,63 @@
|
||||
# linux_hardware_info.py
|
||||
import subprocess
|
||||
import re
|
||||
import os # For listing /proc/asound
|
||||
import glob # For wildcard matching in /proc/asound
|
||||
|
||||
def _run_command(command: list[str]) -> str:
|
||||
"""Helper to run a command and return its stdout."""
|
||||
def _run_command(command: list[str], check_stderr_for_error=False) -> tuple[str, str, int]:
|
||||
"""
|
||||
Helper to run a command and return its stdout, stderr, and return code.
|
||||
Args:
|
||||
check_stderr_for_error: If True, treat any output on stderr as an error condition for return code.
|
||||
Returns:
|
||||
(stdout, stderr, return_code)
|
||||
"""
|
||||
try:
|
||||
process = subprocess.run(command, capture_output=True, text=True, check=True)
|
||||
return process.stdout
|
||||
process = subprocess.run(command, capture_output=True, text=True, check=False) # check=False to handle errors manually
|
||||
|
||||
# Some tools (like lspci without -k if no driver) might return 0 but print to stderr.
|
||||
# However, for most tools here, a non-zero return code is the primary error indicator.
|
||||
# If check_stderr_for_error is True and stderr has content, consider it an error for simplicity here.
|
||||
# effective_return_code = process.returncode
|
||||
# if check_stderr_for_error and process.stderr and process.returncode == 0:
|
||||
# effective_return_code = 1 # Treat as error
|
||||
|
||||
return process.stdout, process.stderr, process.returncode
|
||||
except FileNotFoundError:
|
||||
print(f"Error: Command '{command[0]}' not found. Is 'pciutils' (for lspci) installed?")
|
||||
return ""
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Error executing {' '.join(command)}: {e.stderr}")
|
||||
return ""
|
||||
print(f"Error: Command '{command[0]}' not found.")
|
||||
return "", f"Command not found: {command[0]}", 127 # Standard exit code for command not found
|
||||
except Exception as e:
|
||||
print(f"An unexpected error occurred with command {' '.join(command)}: {e}")
|
||||
return ""
|
||||
return "", str(e), 1
|
||||
|
||||
|
||||
def get_pci_devices_info() -> list[dict]:
|
||||
"""
|
||||
Gets a list of dictionaries, each containing info about a PCI device,
|
||||
focusing on VGA, Audio, and Ethernet controllers.
|
||||
Output format for relevant devices:
|
||||
{'type': 'VGA', 'vendor_id': '10de', 'device_id': '13c2', 'description': 'NVIDIA GTX 970'}
|
||||
{'type': 'Audio', 'vendor_id': '8086', 'device_id': 'a170', 'description': 'Intel Sunrise Point-H HD Audio'}
|
||||
{'type': 'Ethernet', 'vendor_id': '8086', 'device_id': '15b8', 'description': 'Intel Ethernet Connection I219-V'}
|
||||
focusing on VGA, Audio, and Ethernet controllers using lspci.
|
||||
"""
|
||||
output = _run_command(["lspci", "-nnk"])
|
||||
if not output:
|
||||
stdout, stderr, return_code = _run_command(["lspci", "-nnk"])
|
||||
if return_code != 0 or not stdout:
|
||||
print(f"lspci command failed or produced no output. stderr: {stderr}")
|
||||
return []
|
||||
|
||||
devices = []
|
||||
# Regex to capture device type (from description), description, and [vendor:device]
|
||||
# Example line: 01:00.0 VGA compatible controller [0300]: NVIDIA Corporation GM204 [GeForce GTX 970] [10de:13c2] (rev a1)
|
||||
# Example line: 00:1f.3 Audio device [0403]: Intel Corporation Sunrise Point-H HD Audio [8086:a170] (rev 31)
|
||||
# Example line: 00:1f.6 Ethernet controller [0200]: Intel Corporation Ethernet Connection (2) I219-V [8086:15b8] (rev 31)
|
||||
|
||||
# More robust regex:
|
||||
# It captures the class description (like "VGA compatible controller", "Audio device")
|
||||
# and the main device description (like "NVIDIA Corporation GM204 [GeForce GTX 970]")
|
||||
# and the vendor/device IDs like "[10de:13c2]"
|
||||
regex = re.compile(
|
||||
r"^[0-9a-fA-F]{2}:[0-9a-fA-F]{2}\.\d\s+" # PCI Address (e.g., 01:00.0 )
|
||||
r"(.+?)\s+" # Class Description (e.g., "VGA compatible controller")
|
||||
r"\[[0-9a-fA-F]{4}\]:\s+" # PCI Class Code (e.g., [0300]: )
|
||||
r"(.+?)\s+" # Full Device Description (e.g., "NVIDIA Corporation GM204 [GeForce GTX 970]")
|
||||
r"\[([0-9a-fA-F]{4}):([0-9a-fA-F]{4})\]" # Vendor and Device ID (e.g., [10de:13c2])
|
||||
r"^[0-9a-fA-F]{2}:[0-9a-fA-F]{2}\.\d\s+"
|
||||
r"(.+?)\s+"
|
||||
r"\[([0-9a-fA-F]{4})\]:\s+" # Class Code in hex, like 0300 for VGA
|
||||
r"(.+?)\s+"
|
||||
r"\[([0-9a-fA-F]{4}):([0-9a-fA-F]{4})\]" # Vendor and Device ID
|
||||
)
|
||||
|
||||
for line in output.splitlines():
|
||||
for line in stdout.splitlines():
|
||||
match = regex.search(line)
|
||||
if match:
|
||||
class_desc = match.group(1).strip()
|
||||
full_desc = match.group(2).strip()
|
||||
vendor_id = match.group(3).lower()
|
||||
device_id = match.group(4).lower()
|
||||
# class_code = match.group(2).strip() # Not directly used yet but captured
|
||||
full_desc = match.group(3).strip()
|
||||
vendor_id = match.group(4).lower()
|
||||
device_id = match.group(5).lower()
|
||||
|
||||
device_type = None
|
||||
if "VGA compatible controller" in class_desc or "3D controller" in class_desc:
|
||||
@ -63,52 +66,42 @@ def get_pci_devices_info() -> list[dict]:
|
||||
device_type = "Audio"
|
||||
elif "Ethernet controller" in class_desc:
|
||||
device_type = "Ethernet"
|
||||
elif "Network controller" in class_desc: # Could be Wi-Fi
|
||||
elif "Network controller" in class_desc:
|
||||
device_type = "Network (Wi-Fi?)"
|
||||
|
||||
|
||||
if device_type:
|
||||
# Try to get a cleaner description if possible, removing vendor name if it's at the start
|
||||
# e.g. "Intel Corporation Ethernet Connection (2) I219-V" -> "Ethernet Connection (2) I219-V"
|
||||
# This is a simple attempt.
|
||||
cleaned_desc = full_desc
|
||||
if full_desc.lower().startswith("intel corporation "):
|
||||
cleaned_desc = full_desc[len("intel corporation "):]
|
||||
elif full_desc.lower().startswith("nvidia corporation "):
|
||||
cleaned_desc = full_desc[len("nvidia corporation "):]
|
||||
elif full_desc.lower().startswith("advanced micro devices, inc.") or full_desc.lower().startswith("amd"):
|
||||
# Handle different AMD namings
|
||||
if full_desc.lower().startswith("advanced micro devices, inc."):
|
||||
cleaned_desc = re.sub(r"Advanced Micro Devices, Inc\.\s*\[AMD/ATI\]\s*", "", full_desc, flags=re.IGNORECASE)
|
||||
else: # Starts with AMD
|
||||
cleaned_desc = re.sub(r"AMD\s*\[ATI\]\s*", "", full_desc, flags=re.IGNORECASE)
|
||||
elif full_desc.lower().startswith("realtek semiconductor co., ltd."):
|
||||
cleaned_desc = full_desc[len("realtek semiconductor co., ltd. "):]
|
||||
# Simple cleanup attempts (can be expanded)
|
||||
vendors_to_strip = ["Intel Corporation", "NVIDIA Corporation", "Advanced Micro Devices, Inc. [AMD/ATI]", "AMD [ATI]", "Realtek Semiconductor Co., Ltd."]
|
||||
for v_strip in vendors_to_strip:
|
||||
if cleaned_desc.startswith(v_strip):
|
||||
cleaned_desc = cleaned_desc[len(v_strip):].strip()
|
||||
break
|
||||
# Remove revision if present at end, e.g. (rev 31)
|
||||
cleaned_desc = re.sub(r'\s*\(rev [0-9a-fA-F]{2}\)$', '', cleaned_desc)
|
||||
|
||||
|
||||
devices.append({
|
||||
"type": device_type,
|
||||
"vendor_id": vendor_id,
|
||||
"device_id": device_id,
|
||||
"description": cleaned_desc.strip(),
|
||||
"full_lspci_line": line.strip() # For debugging or more info
|
||||
"description": cleaned_desc.strip() if cleaned_desc else full_desc, # Fallback to full_desc
|
||||
"full_lspci_line": line.strip()
|
||||
})
|
||||
return devices
|
||||
|
||||
def get_cpu_info() -> dict:
|
||||
"""
|
||||
Gets CPU information using lscpu.
|
||||
Returns a dictionary with 'Model name', 'Vendor ID', 'CPU family', 'Model', 'Stepping', 'Flags'.
|
||||
"""
|
||||
output = _run_command(["lscpu"])
|
||||
if not output:
|
||||
stdout, stderr, return_code = _run_command(["lscpu"])
|
||||
if return_code != 0 or not stdout:
|
||||
print(f"lscpu command failed or produced no output. stderr: {stderr}")
|
||||
return {}
|
||||
|
||||
info = {}
|
||||
# Regex to capture key-value pairs from lscpu output
|
||||
# Handles spaces in values for "Model name"
|
||||
regex = re.compile(r"^(CPU family|Model name|Vendor ID|Model|Stepping|Flags):\s+(.*)$")
|
||||
for line in output.splitlines():
|
||||
regex = re.compile(r"^(CPU family|Model name|Vendor ID|Model|Stepping|Flags|Architecture):\s+(.*)$")
|
||||
for line in stdout.splitlines():
|
||||
match = regex.match(line)
|
||||
if match:
|
||||
key = match.group(1).strip()
|
||||
@ -116,24 +109,68 @@ def get_cpu_info() -> dict:
|
||||
info[key] = value
|
||||
return info
|
||||
|
||||
def get_audio_codecs() -> list[str]:
|
||||
"""
|
||||
Detects audio codec names by parsing /proc/asound/card*/codec#*.
|
||||
Returns a list of unique codec name strings.
|
||||
E.g., ["Realtek ALC897", "Intel Kaby Lake HDMI"]
|
||||
"""
|
||||
codec_files = glob.glob("/proc/asound/card*/codec#*")
|
||||
if not codec_files:
|
||||
# Fallback for systems where codec#* might not exist, try card*/id
|
||||
codec_files = glob.glob("/proc/asound/card*/id")
|
||||
|
||||
codecs = set() # Use a set to store unique codec names
|
||||
|
||||
for codec_file_path in codec_files:
|
||||
try:
|
||||
with open(codec_file_path, 'r') as f:
|
||||
content = f.read()
|
||||
# For codec#* files
|
||||
codec_match = re.search(r"Codec:\s*(.*)", content)
|
||||
if codec_match:
|
||||
codecs.add(codec_match.group(1).strip())
|
||||
|
||||
# For card*/id files (often just the card name, but sometimes hints at codec)
|
||||
# This is a weaker source but a fallback.
|
||||
if "/id" in codec_file_path and not codec_match: # Only if no "Codec:" line found
|
||||
# The content of /id is usually the card name, e.g. "HDA Intel PCH"
|
||||
# This might not be the specific codec chip but can be a hint.
|
||||
# For now, let's only add if it seems like a specific codec name.
|
||||
# This part needs more refinement if used as a primary source.
|
||||
# For now, we prioritize "Codec: " lines.
|
||||
if "ALC" in content or "CS" in content or "AD" in content: # Common codec prefixes
|
||||
codecs.add(content.strip())
|
||||
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error reading or parsing codec file {codec_file_path}: {e}")
|
||||
|
||||
if not codecs and not codec_files: # If no files found at all
|
||||
print("No /proc/asound/card*/codec#* or /proc/asound/card*/id files found. Cannot detect audio codecs this way.")
|
||||
|
||||
return sorted(list(codecs))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
print("--- PCI Devices ---")
|
||||
pci_devs = get_pci_devices_info()
|
||||
if pci_devs:
|
||||
for dev in pci_devs:
|
||||
print(f" Type: {dev['type']}")
|
||||
print(f" Vendor ID: {dev['vendor_id']}")
|
||||
print(f" Device ID: {dev['device_id']}")
|
||||
print(f" Description: {dev['description']}")
|
||||
# print(f" Full Line: {dev['full_lspci_line']}")
|
||||
else:
|
||||
print(" No relevant PCI devices found or lspci not available.")
|
||||
|
||||
print("\n--- CPU Info ---")
|
||||
print("--- CPU Info ---")
|
||||
cpu_info = get_cpu_info()
|
||||
if cpu_info:
|
||||
for key, value in cpu_info.items():
|
||||
print(f" {key}: {value}")
|
||||
else: print(" Could not retrieve CPU info.")
|
||||
|
||||
print("\n--- PCI Devices ---")
|
||||
pci_devs = get_pci_devices_info()
|
||||
if pci_devs:
|
||||
for dev in pci_devs:
|
||||
print(f" Type: {dev['type']}, Vendor: {dev['vendor_id']}, Device: {dev['device_id']}, Desc: {dev['description']}")
|
||||
else: print(" No relevant PCI devices found or lspci not available.")
|
||||
|
||||
print("\n--- Audio Codecs ---")
|
||||
audio_codecs = get_audio_codecs()
|
||||
if audio_codecs:
|
||||
for codec in audio_codecs:
|
||||
print(f" Detected Codec: {codec}")
|
||||
else:
|
||||
print(" Could not retrieve CPU info or lscpu not available.")
|
||||
print(" No specific audio codecs detected via /proc/asound.")
|
||||
|
169
main_app.py
169
main_app.py
@ -10,10 +10,10 @@ import json # For parsing PowerShell JSON output
|
||||
from PyQt6.QtWidgets import (
|
||||
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
|
||||
QLabel, QComboBox, QPushButton, QTextEdit, QMessageBox, QMenuBar,
|
||||
QFileDialog, QGroupBox, QLineEdit, QProgressBar, QCheckBox # Added QCheckBox
|
||||
QFileDialog, QGroupBox, QLineEdit, QProgressBar
|
||||
)
|
||||
from PyQt6.QtGui import QAction
|
||||
from PyQt6.QtCore import pyqtSignal, pyqtSlot, QObject, QThread, Qt
|
||||
from PyQt6.QtCore import pyqtSignal, pyqtSlot, QObject, QThread, QTimer, Qt # Added QTimer
|
||||
|
||||
# ... (Worker classes and other imports remain the same) ...
|
||||
from constants import APP_NAME, DEVELOPER_NAME, BUSINESS_NAME, MACOS_VERSIONS, DOCKER_IMAGE_BASE
|
||||
@ -136,15 +136,30 @@ class USBWriterWorker(QObject):
|
||||
|
||||
|
||||
class MainWindow(QMainWindow):
|
||||
def __init__(self): # ... (init remains the same)
|
||||
super().__init__(); self.setWindowTitle(APP_NAME); self.setGeometry(100, 100, 800, 850)
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.setWindowTitle(APP_NAME)
|
||||
self.setGeometry(100, 100, 800, 900) # Adjusted height for progress bar in status bar
|
||||
|
||||
self.current_container_name = None; self.extracted_main_image_path = None; self.extracted_opencore_image_path = None
|
||||
self.extraction_status = {"main": False, "opencore": False}; self.active_worker_thread = None
|
||||
self.docker_run_worker_instance = None; self.docker_pull_worker_instance = None
|
||||
self.docker_run_worker_instance = None; self.docker_pull_worker_instance = None # Specific worker instances
|
||||
self._current_usb_selection_text = None
|
||||
self._setup_ui(); self.refresh_usb_drives()
|
||||
|
||||
def _setup_ui(self): # Updated for Windows USB detection
|
||||
self.spinner_chars = ["|", "/", "-", "\\"]
|
||||
self.spinner_index = 0
|
||||
self.spinner_timer = QTimer(self)
|
||||
self.spinner_timer.timeout.connect(self._update_spinner_status)
|
||||
self.base_status_message = "Ready." # Default status message
|
||||
|
||||
self._setup_ui() # Call before using self.statusBar
|
||||
self.status_bar = self.statusBar() # Initialize status bar early
|
||||
self.status_bar.addPermanentWidget(self.progressBar) # Add progress bar to status bar
|
||||
self.status_bar.showMessage(self.base_status_message, 5000) # Initial ready message
|
||||
|
||||
self.refresh_usb_drives()
|
||||
|
||||
def _setup_ui(self):
|
||||
menubar = self.menuBar(); file_menu = menubar.addMenu("&File"); help_menu = menubar.addMenu("&Help")
|
||||
exit_action = QAction("&Exit", self); exit_action.triggered.connect(self.close); file_menu.addAction(exit_action)
|
||||
about_action = QAction("&About", self); about_action.triggered.connect(self.show_about_dialog); help_menu.addAction(about_action)
|
||||
@ -226,31 +241,55 @@ class MainWindow(QMainWindow):
|
||||
self.progressBar = QProgressBar(self)
|
||||
self.progressBar.setRange(0, 0) # Indeterminate
|
||||
self.progressBar.setVisible(False)
|
||||
self.statusBar.addPermanentWidget(self.progressBar, 0)
|
||||
self.statusBar.addPermanentWidget(self.progressBar) # Corrected addPermanentWidget call
|
||||
|
||||
|
||||
def _set_ui_busy(self, is_busy: bool, status_message: str = None):
|
||||
"""Manages UI element states and progress indicators."""
|
||||
def _set_ui_busy(self, is_busy: bool, status_message: str = "Processing..."): # Default busy message
|
||||
"""Manages UI element states and progress indicators, including spinner."""
|
||||
self.general_interactive_widgets = [
|
||||
self.run_vm_button, self.version_combo, self.extract_images_button,
|
||||
self.stop_container_button, self.remove_container_button,
|
||||
self.usb_drive_combo, self.refresh_usb_button, self.write_to_usb_button,
|
||||
self.windows_disk_id_input
|
||||
self.windows_disk_id_input, self.enhance_plist_checkbox
|
||||
]
|
||||
|
||||
if is_busy:
|
||||
self.base_status_message = status_message # Store the core message for spinner
|
||||
for widget in self.general_interactive_widgets:
|
||||
widget.setEnabled(False)
|
||||
# self.stop_vm_button is handled by _start_worker
|
||||
self.progressBar.setVisible(True)
|
||||
self.statusBar.showMessage(status_message or "Processing...", 0)
|
||||
# stop_vm_button's state is managed specifically by the calling function if needed
|
||||
if not self.spinner_timer.isActive(): # Start spinner if not already active
|
||||
self.spinner_index = 0
|
||||
self.spinner_timer.start(150)
|
||||
self._update_spinner_status() # Show initial spinner message
|
||||
else:
|
||||
# Re-enable based on current application state by calling a dedicated method
|
||||
self.update_button_states_after_operation() # This will set appropriate states
|
||||
self.spinner_timer.stop()
|
||||
self.progressBar.setVisible(False)
|
||||
self.statusBar.showMessage(status_message or "Ready.", 5000) # Message disappears after 5s
|
||||
self.statusBar.showMessage(status_message or "Ready.", 7000) # Show final message longer
|
||||
self.update_all_button_states() # Centralized button state update
|
||||
|
||||
def update_button_states_after_operation(self):
|
||||
def _update_spinner_status(self):
|
||||
"""Updates the status bar message with a spinner."""
|
||||
if self.spinner_timer.isActive() and self.active_worker_thread and self.active_worker_thread.isRunning():
|
||||
char = self.spinner_chars[self.spinner_index % len(self.spinner_chars)]
|
||||
# Check if current worker is providing determinate progress
|
||||
worker_name = self.active_worker_thread.objectName().replace("_thread", "")
|
||||
worker_provides_progress = getattr(self, f"{worker_name}_provides_progress", False)
|
||||
|
||||
if worker_provides_progress and self.progressBar.maximum() == 100 and self.progressBar.value() > 0 : # Determinate
|
||||
# For determinate, status bar shows base message, progress bar shows percentage
|
||||
self.statusBar.showMessage(f"{char} {self.base_status_message} ({self.progressBar.value()}%)")
|
||||
else: # Indeterminate
|
||||
if self.progressBar.maximum() != 0: self.progressBar.setRange(0,0) # Ensure indeterminate
|
||||
self.statusBar.showMessage(f"{char} {self.base_status_message}")
|
||||
|
||||
self.spinner_index = (self.spinner_index + 1) % len(self.spinner_chars)
|
||||
elif not (self.active_worker_thread and self.active_worker_thread.isRunning()): # If timer is somehow active but no worker
|
||||
self.spinner_timer.stop()
|
||||
# self.statusBar.showMessage(self.base_status_message or "Ready.", 5000) # Show last base message or ready
|
||||
|
||||
def update_all_button_states(self): # Renamed from update_button_states_after_operation
|
||||
"""Centralized method to update button states based on app's current state."""
|
||||
is_worker_running = self.active_worker_thread and self.active_worker_thread.isRunning()
|
||||
|
||||
@ -276,49 +315,63 @@ class MainWindow(QMainWindow):
|
||||
self.refresh_usb_button.setEnabled(not is_worker_running)
|
||||
self.update_write_to_usb_button_state() # This handles its own complex logic
|
||||
|
||||
def show_about_dialog(self): # Updated version
|
||||
QMessageBox.about(self, f"About {APP_NAME}", f"Version: 0.8.1\nDeveloper: {DEVELOPER_NAME}\nBusiness: {BUSINESS_NAME}\n\nThis tool helps create bootable macOS USB drives using Docker-OSX.")
|
||||
def show_about_dialog(self):
|
||||
QMessageBox.about(self, f"About {APP_NAME}", f"Version: 0.8.2\nDeveloper: {DEVELOPER_NAME}\nBusiness: {BUSINESS_NAME}\n\nThis tool helps create bootable macOS USB drives using Docker-OSX.")
|
||||
|
||||
def _start_worker(self, worker_instance, on_finished_slot, on_error_slot, worker_name="worker", busy_message="Processing..."):
|
||||
def _start_worker(self, worker_instance, on_finished_slot, on_error_slot, worker_name="worker", busy_message="Processing...", provides_progress=False): # Added provides_progress
|
||||
if self.active_worker_thread and self.active_worker_thread.isRunning():
|
||||
QMessageBox.warning(self, "Busy", "Another operation is in progress."); return False
|
||||
|
||||
self._set_ui_busy(True, busy_message)
|
||||
if worker_name in ["docker_pull", "docker_run"]:
|
||||
self.stop_vm_button.setEnabled(True) # Enable stop for these specific long ops
|
||||
else: # For other workers, the main stop button for docker ops is not relevant
|
||||
self.stop_vm_button.setEnabled(False)
|
||||
self._set_ui_busy(True, busy_message) # This now also starts the spinner
|
||||
|
||||
# Set progress bar type based on worker capability
|
||||
if provides_progress:
|
||||
self.progress_bar.setRange(0, 100) # Determinate
|
||||
self.progress_bar.setValue(0)
|
||||
else:
|
||||
self.progress_bar.setRange(0, 0) # Indeterminate
|
||||
|
||||
# Store if this worker provides progress for spinner logic
|
||||
setattr(self, f"{worker_name}_provides_progress", provides_progress)
|
||||
|
||||
|
||||
if worker_name in ["docker_pull", "docker_run"]:
|
||||
self.stop_vm_button.setEnabled(True)
|
||||
else:
|
||||
self.stop_vm_button.setEnabled(False)
|
||||
|
||||
self.active_worker_thread = QThread(); self.active_worker_thread.setObjectName(worker_name + "_thread"); setattr(self, f"{worker_name}_instance", worker_instance)
|
||||
worker_instance.moveToThread(self.active_worker_thread)
|
||||
|
||||
# Connect to generic handlers
|
||||
worker_instance.signals.progress.connect(self.update_output)
|
||||
worker_instance.signals.finished.connect(lambda message: self._handle_worker_finished(message, on_finished_slot, worker_name))
|
||||
worker_instance.signals.error.connect(lambda error_message: self._handle_worker_error(error_message, on_error_slot, worker_name))
|
||||
if provides_progress: # Connect progress_value only if worker provides it
|
||||
worker_instance.signals.progress_value.connect(self.update_progress_bar_value)
|
||||
worker_instance.signals.finished.connect(lambda message, wn=worker_name, slot=on_finished_slot: self._handle_worker_finished(message, wn, slot))
|
||||
worker_instance.signals.error.connect(lambda error_message, wn=worker_name, slot=on_error_slot: self._handle_worker_error(error_message, wn, slot))
|
||||
|
||||
self.active_worker_thread.finished.connect(self.active_worker_thread.deleteLater)
|
||||
# No need to call _clear_worker_instance here, _handle_worker_finished/error will do it.
|
||||
self.active_worker_thread.started.connect(worker_instance.run); self.active_worker_thread.start(); return True
|
||||
|
||||
def _handle_worker_finished(self, message, specific_finished_slot, worker_name):
|
||||
"""Generic handler for worker finished signals."""
|
||||
self.output_area.append(f"\n--- Worker '{worker_name}' Finished --- \n{message}") # Generic log
|
||||
self._clear_worker_instance(worker_name) # Clear the worker instance from self
|
||||
self.active_worker_thread = None # Mark thread as free
|
||||
if specific_finished_slot:
|
||||
specific_finished_slot(message) # Call the specific logic for this worker
|
||||
self._set_ui_busy(False, "Operation completed successfully.") # Reset UI
|
||||
@pyqtSlot(int)
|
||||
def update_progress_bar_value(self, value):
|
||||
if self.progress_bar.minimum() == 0 and self.progress_bar.maximum() == 0: # If it was indeterminate
|
||||
self.progress_bar.setRange(0,100) # Switch to determinate
|
||||
self.progress_bar.setValue(value)
|
||||
# Spinner will update with percentage from progress_bar.value()
|
||||
|
||||
def _handle_worker_error(self, error_message, specific_error_slot, worker_name):
|
||||
"""Generic handler for worker error signals."""
|
||||
self.output_area.append(f"\n--- Worker '{worker_name}' Error --- \n{error_message}") # Generic log
|
||||
self._clear_worker_instance(worker_name) # Clear the worker instance from self
|
||||
self.active_worker_thread = None # Mark thread as free
|
||||
if specific_error_slot:
|
||||
specific_error_slot(error_message) # Call the specific logic for this worker
|
||||
self._set_ui_busy(False, "An error occurred.") # Reset UI
|
||||
def _handle_worker_finished(self, message, worker_name, specific_finished_slot):
|
||||
final_status_message = f"{worker_name.replace('_', ' ').capitalize()} completed."
|
||||
self._clear_worker_instance(worker_name)
|
||||
self.active_worker_thread = None
|
||||
if specific_finished_slot: specific_finished_slot(message)
|
||||
self._set_ui_busy(False, final_status_message)
|
||||
|
||||
def _handle_worker_error(self, error_message, worker_name, specific_error_slot):
|
||||
final_status_message = f"{worker_name.replace('_', ' ').capitalize()} failed."
|
||||
self._clear_worker_instance(worker_name)
|
||||
self.active_worker_thread = None
|
||||
if specific_error_slot: specific_error_slot(error_message)
|
||||
self._set_ui_busy(False, final_status_message)
|
||||
|
||||
def _clear_worker_instance(self, worker_name):
|
||||
attr_name = f"{worker_name}_instance"
|
||||
@ -326,58 +379,50 @@ class MainWindow(QMainWindow):
|
||||
|
||||
def initiate_vm_creation_flow(self):
|
||||
self.output_area.clear(); selected_version_name = self.version_combo.currentText(); image_tag = MACOS_VERSIONS.get(selected_version_name)
|
||||
if not image_tag: self.handle_error(f"Invalid macOS version: {selected_version_name}"); return # handle_error calls _set_ui_busy(False)
|
||||
if not image_tag: self.handle_error(f"Invalid macOS version: {selected_version_name}"); return
|
||||
full_image_name = f"{DOCKER_IMAGE_BASE}:{image_tag}"
|
||||
pull_worker = DockerPullWorker(full_image_name)
|
||||
# Pass busy message to _start_worker
|
||||
self._start_worker(pull_worker,
|
||||
self.docker_pull_finished,
|
||||
self.docker_pull_error,
|
||||
"docker_pull",
|
||||
f"Pulling image {full_image_name}...")
|
||||
"docker_pull", # worker_name
|
||||
f"Pulling image {full_image_name}...", # busy_message
|
||||
provides_progress=False) # Docker pull progress is complex to parse reliably for a percentage
|
||||
|
||||
@pyqtSlot(str)
|
||||
def docker_pull_finished(self, message): # Specific handler
|
||||
# Generic handler (_handle_worker_finished) already logged, cleared instance, and reset UI.
|
||||
# This slot now only handles the next step in the sequence.
|
||||
self.output_area.append(f"Step 1.2: Proceeding to run Docker container for macOS installation...")
|
||||
self.run_macos_vm()
|
||||
|
||||
@pyqtSlot(str)
|
||||
def docker_pull_error(self, error_message): # Specific handler
|
||||
# Generic handler (_handle_worker_error) already logged, cleared instance, and reset UI.
|
||||
QMessageBox.critical(self, "Docker Pull Error", error_message)
|
||||
# No further specific action needed here, UI reset is handled by the generic error handler.
|
||||
|
||||
def run_macos_vm(self): # This is now part 2 of the flow
|
||||
def run_macos_vm(self):
|
||||
selected_version_name = self.version_combo.currentText(); self.current_container_name = get_unique_container_name()
|
||||
try:
|
||||
command_list = build_docker_command(selected_version_name, self.current_container_name)
|
||||
run_worker = DockerRunWorker(command_list)
|
||||
# Pass busy message to _start_worker
|
||||
self._start_worker(run_worker,
|
||||
self.docker_run_finished,
|
||||
self.docker_run_error,
|
||||
"docker_run",
|
||||
f"Starting container {self.current_container_name}...")
|
||||
except ValueError as e: self.handle_error(f"Failed to build command: {str(e)}") # This error is before worker start
|
||||
except Exception as e: self.handle_error(f"An unexpected error: {str(e)}") # This error is before worker start
|
||||
f"Starting container {self.current_container_name}...",
|
||||
provides_progress=False) # Docker run output is also streamed, not easily percentage
|
||||
except ValueError as e: self.handle_error(f"Failed to build command: {str(e)}")
|
||||
except Exception as e: self.handle_error(f"An unexpected error: {str(e)}")
|
||||
|
||||
@pyqtSlot(str)
|
||||
def update_output(self, text): self.output_area.append(text.strip()); QApplication.processEvents()
|
||||
|
||||
@pyqtSlot(str)
|
||||
def docker_run_finished(self, message): # Specific handler
|
||||
# Generic handler already took care of logging, instance clearing, and UI reset.
|
||||
QMessageBox.information(self, "VM Setup Complete", f"{message}\nYou can now proceed to extract images.")
|
||||
# Specific logic after run finishes (e.g. enabling extraction) is now in update_button_states_after_operation
|
||||
|
||||
@pyqtSlot(str)
|
||||
def docker_run_error(self, error_message): # Specific handler
|
||||
# Generic handler already took care of logging, instance clearing, and UI reset.
|
||||
if "exited" in error_message.lower() and self.current_container_name:
|
||||
QMessageBox.warning(self, "VM Setup Ended", f"{error_message}\nAssuming macOS setup was attempted...")
|
||||
# Specific logic (e.g. enabling extraction) is now in update_button_states_after_operation
|
||||
else:
|
||||
QMessageBox.critical(self, "VM Setup Error", error_message)
|
||||
|
||||
|
@ -1,293 +1,294 @@
|
||||
# plist_modifier.py
|
||||
import plistlib
|
||||
import platform
|
||||
import shutil # For backup
|
||||
import os # For path operations
|
||||
import shutil
|
||||
import os
|
||||
import re # For parsing codec names
|
||||
|
||||
# Attempt to import hardware info, will only work if run in an environment
|
||||
# where linux_hardware_info.py is accessible and on Linux.
|
||||
if platform.system() == "Linux":
|
||||
try:
|
||||
from linux_hardware_info import get_pci_devices_info, get_cpu_info
|
||||
from linux_hardware_info import get_pci_devices_info, get_cpu_info, get_audio_codecs
|
||||
except ImportError:
|
||||
print("Warning: linux_hardware_info.py not found. Plist enhancement will be limited.")
|
||||
get_pci_devices_info = lambda: [] # Dummy function
|
||||
get_cpu_info = lambda: {} # Dummy function
|
||||
else: # For other OS, create dummy functions so the rest of the module can be parsed
|
||||
get_pci_devices_info = lambda: []
|
||||
get_cpu_info = lambda: {}
|
||||
get_audio_codecs = lambda: []
|
||||
else:
|
||||
print(f"Warning: Hardware info gathering not implemented for {platform.system()} in plist_modifier.")
|
||||
get_pci_devices_info = lambda: []
|
||||
get_cpu_info = lambda: {}
|
||||
get_audio_codecs = lambda: [] # Dummy function for non-Linux
|
||||
|
||||
# --- Mappings ---
|
||||
# Values are typically byte-swapped for device-id and some ig-platform-id representations in OpenCore
|
||||
# For AAPL,ig-platform-id, the first two bytes are often the device-id (swapped), last two are platform related.
|
||||
# Example: UHD 630 (Desktop Coffee Lake) device-id 0x3E9B -> data <9B3E0000>
|
||||
# ig-platform-id commonly 0x3E9B0007 -> data <07009B3E> (or other variants)
|
||||
|
||||
# --- Illustrative Mappings (Proof of Concept) ---
|
||||
# Keys are VENDOR_ID:DEVICE_ID (lowercase)
|
||||
INTEL_IGPU_DEFAULTS = {
|
||||
# Coffee Lake Desktop (UHD 630)
|
||||
# Coffee Lake Desktop (UHD 630) - Common
|
||||
"8086:3e9b": {"AAPL,ig-platform-id": b"\x07\x00\x9B\x3E", "device-id": b"\x9B\x3E\x00\x00", "framebuffer-patch-enable": b"\x01\x00\x00\x00"},
|
||||
# Kaby Lake Desktop (HD 630)
|
||||
# Kaby Lake Desktop (HD 630) - Common
|
||||
"8086:5912": {"AAPL,ig-platform-id": b"\x05\x00\x12\x59", "device-id": b"\x12\x59\x00\x00", "framebuffer-patch-enable": b"\x01\x00\x00\x00"},
|
||||
# Skylake Desktop (HD 530)
|
||||
# Skylake Desktop (HD 530) - Common
|
||||
"8086:1912": {"AAPL,ig-platform-id": b"\x00\x00\x12\x19", "device-id": b"\x12\x19\x00\x00", "framebuffer-patch-enable": b"\x01\x00\x00\x00"},
|
||||
|
||||
# Alder Lake-S Desktop (UHD 730/750/770) - device-id often needs to be accurate
|
||||
"8086:4680": {"AAPL,ig-platform-id": b"\x0A\x00\x9B\x46", "device-id": b"\x80\x46\x00\x00", "enable-hdmi20": b"\x01\x00\x00\x00"}, # e.g. i9-12900K UHD 770 (0x4680) -> common platform ID for iGPU only
|
||||
"8086:4690": {"AAPL,ig-platform-id": b"\x0A\x00\x9B\x46", "device-id": b"\x90\x46\x00\x00", "enable-hdmi20": b"\x01\x00\x00\x00"}, # e.g. i5-12600K UHD 770 (0x4690)
|
||||
"8086:4692": {"AAPL,ig-platform-id": b"\x0A\x00\x9B\x46", "device-id": b"\x92\x46\x00\x00", "enable-hdmi20": b"\x01\x00\x00\x00"}, # e.g. i5-12400 UHD 730 (0x4692)
|
||||
# Alternative Alder Lake platform-id (often when dGPU is primary)
|
||||
"8086:4680_dgpu": {"AAPL,ig-platform-id": b"\x04\x00\x12\x40", "device-id": b"\x80\x46\x00\x00", "enable-hdmi20": b"\x01\x00\x00\x00"}, # Using a suffix for internal logic, not a real PCI ID
|
||||
"8086:4690_dgpu": {"AAPL,ig-platform-id": b"\x04\x00\x12\x40", "device-id": b"\x90\x46\x00\x00", "enable-hdmi20": b"\x01\x00\x00\x00"},
|
||||
"8086:4692_dgpu": {"AAPL,ig-platform-id": b"\x04\x00\x12\x40", "device-id": b"\x92\x46\x00\x00", "enable-hdmi20": b"\x01\x00\x00\x00"},
|
||||
}
|
||||
INTEL_IGPU_PCI_PATH = "PciRoot(0x0)/Pci(0x2,0x0)"
|
||||
|
||||
# Primary keys are now Codec Names. PCI IDs are secondary/fallback.
|
||||
AUDIO_LAYOUTS = {
|
||||
# Intel HDA - common controllers, layout 1 is a frequent default
|
||||
"8086:a170": 1, # Sunrise Point-H HD Audio
|
||||
"8086:a2f0": 1, # Series 200 HD Audio
|
||||
"8086:a348": 3, # Cannon Point-LP HD Audio
|
||||
"8086:f0c8": 3, # Comet Lake HD Audio
|
||||
# Realtek Codecs (often on Intel HDA controller, actual codec detection is harder)
|
||||
# If a Realtek PCI ID is found for audio, one of these layouts might work.
|
||||
# This map is simplified; usually, you detect the codec name (e.g. ALC255, ALC892)
|
||||
"10ec:0255": 3, # ALC255 Example
|
||||
"10ec:0892": 1, # ALC892 Example
|
||||
# Codec Names (Prefer these) - Extracted from "Codec: Realtek ALCXXX" or similar
|
||||
"Realtek ALC221": 11, "Realtek ALC233": 11, "Realtek ALC235": 28,
|
||||
"Realtek ALC255": 11, "Realtek ALC256": 11, "Realtek ALC257": 11,
|
||||
"Realtek ALC269": 11, "Realtek ALC271": 11, "Realtek ALC282": 11,
|
||||
"Realtek ALC283": 11, "Realtek ALC285": 11, "Realtek ALC289": 11,
|
||||
"Realtek ALC295": 11,
|
||||
"Realtek ALC662": 5, "Realtek ALC671": 11,
|
||||
"Realtek ALC887": 7, "Realtek ALC888": 7,
|
||||
"Realtek ALC892": 1, "Realtek ALC897": 11, # Common, 11 often works
|
||||
"Realtek ALC1150": 1,
|
||||
"Realtek ALC1200": 7,
|
||||
"Realtek ALC1220": 7, "Realtek ALC1220-VB": 7, # VB variant often uses same layouts
|
||||
"Conexant CX20756": 3, # Example Conexant
|
||||
# Fallback PCI IDs for generic Intel HDA controllers if codec name not matched
|
||||
"pci_8086:a170": 1, # Sunrise Point-H HD Audio
|
||||
"pci_8086:a2f0": 1, # Series 200 HD Audio (Kaby Lake)
|
||||
"pci_8086:a348": 3, # Cannon Point-LP HD Audio
|
||||
"pci_8086:f0c8": 3, # Comet Lake HD Audio (Series 400)
|
||||
"pci_8086:43c8": 11,# Tiger Lake-H HD Audio (Series 500)
|
||||
"pci_8086:7ad0": 11,# Alder Lake PCH-P HD Audio
|
||||
}
|
||||
AUDIO_PCI_PATH_FALLBACK = "PciRoot(0x0)/Pci(0x1f,0x3)" # Common, but needs verification
|
||||
AUDIO_PCI_PATH_FALLBACK = "PciRoot(0x0)/Pci(0x1f,0x3)"
|
||||
|
||||
ETHERNET_KEXT_MAP = {
|
||||
"8086:15b8": "IntelMausi.kext", # Intel I219-V
|
||||
"8086:153a": "IntelMausi.kext", # Intel I217-V
|
||||
"8086:10f0": "IntelMausi.kext", # Intel 82579LM
|
||||
"10ec:8168": "RealtekRTL8111.kext", # Realtek RTL8111/8168
|
||||
"10ec:8111": "RealtekRTL8111.kext",
|
||||
"14e4:1686": "AirportBrcmFixup.kext", # Example Broadcom Wi-Fi (though kext name might be BrcmPatchRAM related)
|
||||
# Proper Ethernet kext for Broadcom depends on model e.g. AppleBCM5701Ethernet.kext
|
||||
ETHERNET_KEXT_MAP = { # vendor_id:device_id -> kext_name
|
||||
"8086:15b8": "IntelMausi.kext", "8086:153a": "IntelMausi.kext", "8086:10f0": "IntelMausi.kext",
|
||||
"8086:15be": "IntelMausi.kext", "8086:0d4f": "IntelMausi.kext", "8086:15b7": "IntelMausi.kext", # I219-V(3)
|
||||
"8086:1a1c": "IntelMausi.kext", # Comet Lake-S vPro (I219-LM)
|
||||
"10ec:8168": "RealtekRTL8111.kext", "10ec:8111": "RealtekRTL8111.kext",
|
||||
"10ec:2502": "LucyRTL8125Ethernet.kext", # Realtek RTL8125 2.5GbE
|
||||
"10ec:2600": "LucyRTL8125Ethernet.kext", # Realtek RTL8125B 2.5GbE
|
||||
"8086:15ec": "AppleIntelI210Ethernet.kext", # I225-V (Often needs AppleIGB.kext or specific patches)
|
||||
"8086:15f3": "AppleIntelI210Ethernet.kext", # I225-V / I226-V
|
||||
"14e4:1686": "AirportBrcmFixup.kext", # Placeholder for Broadcom Wi-Fi, actual kext depends on model
|
||||
}
|
||||
|
||||
|
||||
def _get_pci_path_for_device(pci_devices, target_vendor_id, target_device_id_prefix):
|
||||
# This is a placeholder. A real implementation would need to parse lspci's bus info (00:1f.3)
|
||||
# and convert that to an OpenCore PciRoot string. For now, uses fallbacks.
|
||||
# Example: lspci output "00:1f.3 Audio device [0403]: Intel Corporation Sunrise Point-H HD Audio [8086:a170] (rev 31)"
|
||||
# PciRoot(0x0)/Pci(0x1f,0x3)
|
||||
# For now, this function is not fully implemented and we'll use hardcoded common paths.
|
||||
return None
|
||||
|
||||
|
||||
def enhance_config_plist(plist_path: str, target_macos_version_name: str, progress_callback=None) -> bool:
|
||||
"""
|
||||
Loads a config.plist, gathers hardware info (Linux only for now),
|
||||
applies targeted enhancements, and saves it back.
|
||||
Args:
|
||||
plist_path: Path to the config.plist file.
|
||||
target_macos_version_name: e.g., "Sonoma", "High Sierra". Used for version-specific logic.
|
||||
progress_callback: Optional function to report progress.
|
||||
Returns:
|
||||
True if successful, False otherwise.
|
||||
"""
|
||||
def _report(msg):
|
||||
if progress_callback: progress_callback(f"[PlistModifier] {msg}")
|
||||
else: print(f"[PlistModifier] {msg}")
|
||||
|
||||
_report(f"Starting config.plist enhancement for: {plist_path}")
|
||||
_report(f"Target macOS version: {target_macos_version_name}")
|
||||
|
||||
if not os.path.exists(plist_path):
|
||||
_report(f"Error: Plist file not found at {plist_path}")
|
||||
return False
|
||||
|
||||
# Create a backup
|
||||
# ... (backup logic same as before) ...
|
||||
_report(f"Starting config.plist enhancement for: {plist_path}"); _report(f"Target macOS version: {target_macos_version_name.lower()}")
|
||||
if not os.path.exists(plist_path): _report(f"Error: Plist file not found at {plist_path}"); return False
|
||||
backup_plist_path = plist_path + ".backup"
|
||||
try:
|
||||
shutil.copy2(plist_path, backup_plist_path)
|
||||
_report(f"Created backup of config.plist at: {backup_plist_path}")
|
||||
except Exception as e:
|
||||
_report(f"Error creating backup for {plist_path}: {e}. Proceeding without backup.")
|
||||
# Decide if this should be a fatal error for the modification step
|
||||
# For now, we'll proceed cautiously.
|
||||
try: shutil.copy2(plist_path, backup_plist_path); _report(f"Created backup: {backup_plist_path}")
|
||||
except Exception as e: _report(f"Error creating backup for {plist_path}: {e}. Proceeding cautiously.")
|
||||
|
||||
if platform.system() != "Linux":
|
||||
_report("Hardware detection for plist enhancement currently only supported on Linux. Skipping hardware-specific modifications.")
|
||||
# Still load and save to ensure plist is valid, but no hardware changes.
|
||||
config_data = {};
|
||||
try:
|
||||
with open(plist_path, 'rb') as f: config_data = plistlib.load(f)
|
||||
# No changes made, so just confirm it's okay.
|
||||
# If we wanted to ensure it's valid and resave (pretty print), we could do:
|
||||
# with open(plist_path, 'wb') as f: plistlib.dump(config_data, f, sort_keys=True)
|
||||
_report("Plist not modified on non-Linux host (hardware detection skipped).")
|
||||
return True
|
||||
except Exception as e:
|
||||
_report(f"Error processing plist file {plist_path} even without hardware changes: {e}")
|
||||
return False
|
||||
except Exception as e: _report(f"Error loading plist {plist_path}: {e}"); return False
|
||||
|
||||
pci_devices = []; cpu_info = {}; audio_codecs_detected = []
|
||||
if platform.system() == "Linux":
|
||||
pci_devices = get_pci_devices_info(); cpu_info = get_cpu_info(); audio_codecs_detected = get_audio_codecs()
|
||||
if not pci_devices: _report("Warning: Could not retrieve PCI hardware info on Linux.")
|
||||
if not audio_codecs_detected: _report("Warning: Could not detect specific audio codecs on Linux.")
|
||||
else: _report("Hardware detection for plist enhancement Linux-host only. Skipping hardware-specific mods.")
|
||||
|
||||
try:
|
||||
with open(plist_path, 'rb') as f:
|
||||
config_data = plistlib.load(f)
|
||||
except Exception as e:
|
||||
_report(f"Error loading plist file {plist_path} for modification: {e}")
|
||||
return False
|
||||
|
||||
pci_devices = get_pci_devices_info()
|
||||
cpu_info = get_cpu_info() # Currently not used in logic below but fetched
|
||||
|
||||
if not pci_devices: # cpu_info might be empty too
|
||||
_report("Could not retrieve PCI hardware information. Skipping most plist enhancements.")
|
||||
# Still try to save (pretty-print/validate) the plist if loaded.
|
||||
try:
|
||||
with open(plist_path, 'wb') as f: plistlib.dump(config_data, f, sort_keys=True)
|
||||
_report("Plist re-saved (no hardware changes applied due to missing PCI info).")
|
||||
return True
|
||||
except Exception as e:
|
||||
_report(f"Error re-saving plist file {plist_path}: {e}")
|
||||
return False
|
||||
|
||||
# Ensure sections exist
|
||||
dev_props = config_data.setdefault("DeviceProperties", {}).setdefault("Add", {})
|
||||
kernel_add = config_data.setdefault("Kernel", {}).setdefault("Add", [])
|
||||
nvram_add = config_data.setdefault("NVRAM", {}).setdefault("Add", {})
|
||||
boot_args_uuid = "7C436110-AB2A-4BBB-A880-FE41995C9F82"
|
||||
boot_args_section = nvram_add.setdefault(boot_args_uuid, {})
|
||||
current_boot_args_str = boot_args_section.get("boot-args", "")
|
||||
boot_args = set(current_boot_args_str.split())
|
||||
modified = False # Flag to track if any changes were made
|
||||
current_boot_args_str = boot_args_section.get("boot-args", ""); boot_args = set(current_boot_args_str.split())
|
||||
modified_plist = False
|
||||
|
||||
# 1. Intel iGPU
|
||||
intel_igpu_on_host = next((dev for dev in pci_devices if dev['type'] == 'VGA' and dev['vendor_id'] == '8086'), None)
|
||||
dgpu_present = any(dev['type'] == 'VGA' and dev['vendor_id'] != '8086' for dev in pci_devices)
|
||||
|
||||
if intel_igpu_on_host:
|
||||
lookup_key = f"{intel_igpu_on_host['vendor_id']}:{intel_igpu_on_host['device_id']}"
|
||||
# For Alder Lake, if a dGPU is also present, a different platform-id might be preferred.
|
||||
if lookup_key.startswith("8086:46") and dgpu_present: # Basic check for Alder Lake iGPU + dGPU
|
||||
lookup_key_dgpu = f"{lookup_key}_dgpu"
|
||||
if lookup_key_dgpu in INTEL_IGPU_DEFAULTS:
|
||||
lookup_key = lookup_key_dgpu
|
||||
_report(f"Intel Alder Lake iGPU ({intel_igpu_on_host['description']}) detected with a dGPU. Using dGPU-specific properties if available.")
|
||||
|
||||
# 1. Intel iGPU Enhancement
|
||||
intel_igpu_device_id_on_host = None
|
||||
for dev in pci_devices:
|
||||
if dev['type'] == 'VGA' and dev['vendor_id'] == '8086': # Intel iGPU
|
||||
intel_igpu_device_id_on_host = dev['device_id']
|
||||
lookup_key = f"{dev['vendor_id']}:{dev['device_id']}"
|
||||
if lookup_key in INTEL_IGPU_DEFAULTS:
|
||||
_report(f"Found Intel iGPU: {dev['description']}. Applying properties.")
|
||||
_report(f"Applying properties for Intel iGPU: {intel_igpu_on_host['description']} ({lookup_key}).")
|
||||
igpu_path_properties = dev_props.setdefault(INTEL_IGPU_PCI_PATH, {})
|
||||
for key, value in INTEL_IGPU_DEFAULTS[lookup_key].items():
|
||||
igpu_path_properties[key] = value
|
||||
_report(f" Set {INTEL_IGPU_PCI_PATH} -> {key}")
|
||||
else:
|
||||
_report(f"Found Intel iGPU: {dev['description']} ({lookup_key}) but no default properties defined for it.")
|
||||
break # Assume only one active iGPU for primary display configuration
|
||||
if igpu_path_properties.get(key) != value: igpu_path_properties[key] = value; _report(f" Set {INTEL_IGPU_PCI_PATH} -> {key}"); modified_plist = True
|
||||
else: _report(f"Found Intel iGPU: {intel_igpu_on_host['description']} ({lookup_key}) but no default properties in map.")
|
||||
|
||||
# 2. Audio Enhancement (Layout ID)
|
||||
audio_device_path_in_plist = AUDIO_PCI_PATH_FALLBACK # Default, may need to be dynamic
|
||||
# 2. Audio Enhancement - Prioritize detected codec name
|
||||
audio_device_pci_path_to_patch = AUDIO_PCI_PATH_FALLBACK # Default
|
||||
audio_layout_set = False
|
||||
if audio_codecs_detected:
|
||||
_report(f"Detected audio codecs: {audio_codecs_detected}")
|
||||
for codec_name_full in audio_codecs_detected:
|
||||
# Try to match known parts of codec names, e.g. "Realtek ALC897" from "Codec: Realtek ALC897"
|
||||
# Or "ALC897" if that's how it's stored in AUDIO_LAYOUTS keys
|
||||
for known_codec_key, layout_id in AUDIO_LAYOUTS.items():
|
||||
if not known_codec_key.startswith("pci_"): # Ensure we are checking codec names, not PCI IDs
|
||||
# Simple substring match or more specific regex
|
||||
# Example: "Realtek ALC255" should match "ALC255" if key is "ALC255"
|
||||
# Or if key is "Realtek ALC255" it matches directly
|
||||
# For "Codec: Realtek ALC255" we might want to extract "Realtek ALC255"
|
||||
|
||||
# Attempt to extract the core codec part (e.g., "ALC255", "CX20756")
|
||||
simple_codec_name_match = re.search(r"(ALC\d{3,4}(?:-VB)?|CX\d{4,})", codec_name_full, re.IGNORECASE)
|
||||
simple_codec_name = simple_codec_name_match.group(1) if simple_codec_name_match else None
|
||||
|
||||
if (known_codec_key in codec_name_full) or \
|
||||
(simple_codec_name and known_codec_key in simple_codec_name) or \
|
||||
(known_codec_key.replace("Realtek ", "") in codec_name_full.replace("Realtek ", "")): # Try matching without "Realtek "
|
||||
|
||||
_report(f"Matched Audio Codec: '{codec_name_full}' (using key '{known_codec_key}'). Setting layout-id to {layout_id}.")
|
||||
audio_path_properties = dev_props.setdefault(audio_device_pci_path_to_patch, {})
|
||||
new_layout_data = plistlib.Data(layout_id.to_bytes(1, 'little'))
|
||||
if audio_path_properties.get("layout-id") != new_layout_data:
|
||||
audio_path_properties["layout-id"] = new_layout_data; _report(f" Set {audio_device_pci_path_to_patch} -> layout-id = {layout_id}"); modified_plist = True
|
||||
audio_layout_set = True; break
|
||||
if audio_layout_set: break
|
||||
|
||||
if not audio_layout_set: # Fallback to PCI ID of audio controller
|
||||
_report("No specific audio codec match found or no codecs detected. Falling back to PCI ID for audio controller.")
|
||||
for dev in pci_devices:
|
||||
if dev['type'] == 'Audio':
|
||||
lookup_key = f"{dev['vendor_id']}:{dev['device_id']}"
|
||||
lookup_key = f"pci_{dev['vendor_id']}:{dev['device_id']}" # PCI ID keys are prefixed
|
||||
if lookup_key in AUDIO_LAYOUTS:
|
||||
layout_id = AUDIO_LAYOUTS[lookup_key]
|
||||
_report(f"Found Audio device: {dev['description']}. Setting layout-id to {layout_id}.")
|
||||
audio_path_properties = dev_props.setdefault(audio_device_path_in_plist, {})
|
||||
new_layout_data = plistlib.Data(layout_id.to_bytes(1, 'little')) # Common layout IDs are small integers
|
||||
_report(f"Found Audio device (PCI): {dev['description']}. Setting layout-id to {layout_id} via PCI ID map.")
|
||||
audio_path_properties = dev_props.setdefault(audio_device_pci_path_to_patch, {})
|
||||
new_layout_data = plistlib.Data(layout_id.to_bytes(1, 'little'))
|
||||
if audio_path_properties.get("layout-id") != new_layout_data:
|
||||
audio_path_properties["layout-id"] = new_layout_data
|
||||
_report(f" Set {audio_device_path_in_plist} -> layout-id = {layout_id}")
|
||||
modified = True
|
||||
for kext in kernel_add: # Ensure AppleALC is enabled
|
||||
if isinstance(kext, dict) and kext.get("BundlePath") == "AppleALC.kext":
|
||||
if not kext.get("Enabled", False):
|
||||
kext["Enabled"] = True; _report(" Ensured AppleALC.kext is enabled."); modified = True
|
||||
break
|
||||
audio_path_properties["layout-id"] = new_layout_data; _report(f" Set {audio_device_pci_path_to_patch} -> layout-id = {layout_id}"); modified_plist = True
|
||||
audio_layout_set = True; break
|
||||
|
||||
if audio_layout_set: # Common action if any layout was set
|
||||
for kext_entry in kernel_add:
|
||||
if isinstance(kext_entry, dict) and kext_entry.get("BundlePath") == "AppleALC.kext":
|
||||
if not kext_entry.get("Enabled", False): kext_entry["Enabled"] = True; _report(" Ensured AppleALC.kext is enabled."); modified_plist = True
|
||||
break
|
||||
|
||||
# 3. Ethernet Kext Enablement
|
||||
# 3. Ethernet Kext Enablement (same logic as before)
|
||||
for dev in pci_devices:
|
||||
if dev['type'] == 'Ethernet':
|
||||
lookup_key = f"{dev['vendor_id']}:{dev['device_id']}"
|
||||
if lookup_key in ETHERNET_KEXT_MAP:
|
||||
kext_name = ETHERNET_KEXT_MAP[lookup_key]; _report(f"Found Ethernet device: {dev['description']}. Will ensure {kext_name} is enabled.")
|
||||
kext_found_and_enabled_or_modified = False
|
||||
kext_name = ETHERNET_KEXT_MAP[lookup_key]; _report(f"Found Ethernet: {dev['description']}. Ensuring {kext_name} is enabled.")
|
||||
kext_modified_in_plist = False
|
||||
for kext_entry in kernel_add:
|
||||
if isinstance(kext_entry, dict) and kext_entry.get("BundlePath") == kext_name:
|
||||
if not kext_entry.get("Enabled", False):
|
||||
kext_entry["Enabled"] = True; _report(f" Enabled {kext_name}."); modified = True
|
||||
else:
|
||||
_report(f" {kext_name} already enabled.")
|
||||
kext_found_and_enabled_or_modified = True; break
|
||||
if not kext_found_and_enabled_or_modified: _report(f" Warning: {kext_name} for {dev['description']} not in Kernel->Add.")
|
||||
if not kext_entry.get("Enabled", False): kext_entry["Enabled"] = True; _report(f" Enabled {kext_name}."); modified_plist = True
|
||||
else: _report(f" {kext_name} already enabled.")
|
||||
kext_modified_in_plist = True; break
|
||||
if not kext_modified_in_plist: _report(f" Warning: {kext_name} for {dev['description']} not in Kernel->Add list of config.plist.")
|
||||
break
|
||||
|
||||
# 4. NVIDIA GTX 970 Specific Adjustments
|
||||
gtx_970_present = any(dev['vendor_id'] == '10de' and dev['device_id'] == '13c2' for dev in pci_devices)
|
||||
if gtx_970_present:
|
||||
_report("NVIDIA GTX 970 detected.")
|
||||
is_high_sierra_or_older = target_macos_version_name.lower() in ["high sierra"]
|
||||
original_boot_args_len = len(boot_args) # To check if boot_args actually change
|
||||
if is_high_sierra_or_older:
|
||||
high_sierra_and_older_versions = ["high sierra", "sierra", "el capitan"]
|
||||
is_high_sierra_or_older_target = target_macos_version_name.lower() in high_sierra_and_older_versions
|
||||
|
||||
original_boot_args_set = set(boot_args)
|
||||
|
||||
if is_high_sierra_or_older_target:
|
||||
boot_args.add('nvda_drv=1'); boot_args.discard('nv_disable=1')
|
||||
_report(" Configured for NVIDIA Web Drivers (High Sierra target).")
|
||||
else:
|
||||
_report(" Configured for NVIDIA Web Drivers (High Sierra or older target).")
|
||||
else: # Mojave and newer
|
||||
boot_args.discard('nvda_drv=1')
|
||||
if intel_igpu_device_id_on_host:
|
||||
boot_args.add('nv_disable=1'); _report(f" Added nv_disable=1 for {target_macos_version_name} to prioritize iGPU.")
|
||||
if intel_igpu_on_host:
|
||||
boot_args.add('nv_disable=1')
|
||||
_report(f" Added nv_disable=1 for {target_macos_version_name} to prioritize detected host iGPU over GTX 970.")
|
||||
else:
|
||||
boot_args.discard('nv_disable=1'); _report(f" GTX 970 likely only GPU for {target_macos_version_name}. `nv_disable=1` not forced.")
|
||||
# Check if boot_args actually changed before setting modified = True
|
||||
if len(boot_args) != original_boot_args_len or ' '.join(sorted(list(boot_args))) != current_boot_args_str : modified = True
|
||||
boot_args.discard('nv_disable=1')
|
||||
_report(f" GTX 970 is likely only GPU. `nv_disable=1` not forced for {target_macos_version_name}. Basic display expected.")
|
||||
if boot_args != original_boot_args_set: modified_plist = True
|
||||
|
||||
final_boot_args = ' '.join(sorted(list(boot_args)))
|
||||
if final_boot_args != current_boot_args_str: # Check if boot-args actually changed
|
||||
boot_args_section['boot-args'] = final_boot_args
|
||||
_report(f"Updated boot-args to: '{final_boot_args}'")
|
||||
modified = True # Ensure modified is true if boot_args changed
|
||||
final_boot_args_str = ' '.join(sorted(list(boot_args)))
|
||||
if boot_args_section.get('boot-args') != final_boot_args_str:
|
||||
boot_args_section['boot-args'] = final_boot_args_str
|
||||
_report(f"Updated boot-args to: '{final_boot_args_str}'")
|
||||
modified_plist = True
|
||||
|
||||
if not modified:
|
||||
_report("No changes made to config.plist based on detected hardware or existing settings.")
|
||||
return True # Successful in the sense that no changes were needed or applied.
|
||||
if not modified_plist:
|
||||
_report("No changes made to config.plist based on detected hardware or existing settings were different from defaults.")
|
||||
# If no hardware changes on non-Linux, this is expected.
|
||||
if platform.system() != "Linux" and not pci_devices : return True # No error, just no action
|
||||
|
||||
# Save the modified plist
|
||||
try:
|
||||
with open(plist_path, 'wb') as f:
|
||||
plistlib.dump(config_data, f, sort_keys=True)
|
||||
_report(f"Successfully saved enhanced config.plist to {plist_path}")
|
||||
plistlib.dump(config_data, f, sort_keys=True, fmt=plistlib.PlistFormat.XML) # Ensure XML format
|
||||
_report(f"Successfully saved config.plist to {plist_path}")
|
||||
return True
|
||||
except Exception as e:
|
||||
except Exception as e: # ... (restore backup logic same as before)
|
||||
_report(f"Error saving modified plist file {plist_path}: {e}")
|
||||
_report(f"Attempting to restore backup to {plist_path}...")
|
||||
try:
|
||||
shutil.copy2(backup_plist_path, plist_path)
|
||||
_report("Restored backup successfully.")
|
||||
except Exception as backup_error:
|
||||
_report(f"CRITICAL: FAILED TO RESTORE BACKUP. {plist_path} may be corrupt. Backup is at {backup_plist_path}. Error: {backup_error}")
|
||||
try: shutil.copy2(backup_plist_path, plist_path); _report("Restored backup successfully.")
|
||||
except Exception as backup_error: _report(f"CRITICAL: FAILED TO RESTORE BACKUP. {plist_path} may be corrupt. Backup is at {backup_plist_path}. Error: {backup_error}")
|
||||
return False
|
||||
|
||||
# if __name__ == '__main__': (Keep the same test block as before)
|
||||
# if __name__ == '__main__': (Keep the same test block as before, ensure dummy data for kexts is complete)
|
||||
if __name__ == '__main__':
|
||||
print("Plist Modifier Standalone Test")
|
||||
print("Plist Modifier Standalone Test") # ... (rest of test block as in previous version)
|
||||
dummy_plist_path = "test_config.plist"
|
||||
dummy_data = {
|
||||
"DeviceProperties": {"Add": {}},
|
||||
"Kernel": {"Add": [
|
||||
{"BundlePath": "Lilu.kext", "Enabled": True, "Arch": "Any", "Comment": "", "ExecutablePath": "Contents/MacOS/Lilu", "MaxKernel": "", "MinKernel": "", "PlistPath": "Contents/Info.plist"},
|
||||
{"BundlePath": "WhateverGreen.kext", "Enabled": True, "Arch": "Any", "Comment": "", "ExecutablePath": "Contents/MacOS/WhateverGreen", "MaxKernel": "", "MinKernel": "", "PlistPath": "Contents/Info.plist"},
|
||||
{"BundlePath": "AppleALC.kext", "Enabled": False, "Arch": "Any", "Comment": "", "ExecutablePath": "Contents/MacOS/AppleALC", "MaxKernel": "", "MinKernel": "", "PlistPath": "Contents/Info.plist"},
|
||||
{"BundlePath": "IntelMausi.kext", "Enabled": False, "Arch": "Any", "Comment": "", "ExecutablePath": "Contents/MacOS/IntelMausi", "MaxKernel": "", "MinKernel": "", "PlistPath": "Contents/Info.plist"},
|
||||
{"Arch": "Any", "BundlePath": "Lilu.kext", "Comment": "Lilu", "Enabled": True, "ExecutablePath": "Contents/MacOS/Lilu", "MaxKernel": "", "MinKernel": "", "PlistPath": "Contents/Info.plist"},
|
||||
{"Arch": "Any", "BundlePath": "WhateverGreen.kext", "Comment": "WG", "Enabled": True, "ExecutablePath": "Contents/MacOS/WhateverGreen", "MaxKernel": "", "MinKernel": "", "PlistPath": "Contents/Info.plist"},
|
||||
{"Arch": "Any", "BundlePath": "AppleALC.kext", "Comment": "AppleALC", "Enabled": False, "ExecutablePath": "Contents/MacOS/AppleALC", "MaxKernel": "", "MinKernel": "", "PlistPath": "Contents/Info.plist"},
|
||||
{"Arch": "Any", "BundlePath": "IntelMausi.kext", "Comment": "IntelMausi", "Enabled": False, "ExecutablePath": "Contents/MacOS/IntelMausi", "MaxKernel": "", "MinKernel": "", "PlistPath": "Contents/Info.plist"},
|
||||
{"Arch": "Any", "BundlePath": "RealtekRTL8111.kext", "Comment": "Realtek", "Enabled": False, "ExecutablePath": "Contents/MacOS/RealtekRTL8111", "MaxKernel": "", "MinKernel": "", "PlistPath": "Contents/Info.plist"},
|
||||
{"Arch": "Any", "BundlePath": "LucyRTL8125Ethernet.kext", "Comment": "LucyRealtek", "Enabled": False, "ExecutablePath": "Contents/MacOS/LucyRTL8125Ethernet", "MaxKernel": "", "MinKernel": "", "PlistPath": "Contents/Info.plist"},
|
||||
]},
|
||||
"NVRAM": {"Add": {"7C436110-AB2A-4BBB-A880-FE41995C9F82": {"boot-args": "-v"}}}
|
||||
"NVRAM": {"Add": {"7C436110-AB2A-4BBB-A880-FE41995C9F82": {"boot-args": "-v debug=0x100"}}}
|
||||
}
|
||||
with open(dummy_plist_path, 'wb') as f:
|
||||
plistlib.dump(dummy_data, f)
|
||||
with open(dummy_plist_path, 'wb') as f: plistlib.dump(dummy_data, f)
|
||||
print(f"Created dummy {dummy_plist_path} for testing.")
|
||||
|
||||
original_get_pci = get_pci_devices_info; original_get_cpu = get_cpu_info # Store originals
|
||||
|
||||
needs_mocking = platform.system() != "Linux"
|
||||
if not needs_mocking:
|
||||
try:
|
||||
get_pci_devices_info()
|
||||
except Exception:
|
||||
print("Hardware info functions seem problematic, forcing mock.")
|
||||
needs_mocking = True
|
||||
|
||||
|
||||
if needs_mocking:
|
||||
print("Mocking hardware info for non-Linux or if module not loaded properly.")
|
||||
|
||||
original_get_pci = get_pci_devices_info; original_get_cpu = get_cpu_info; original_get_audio_codecs = get_audio_codecs
|
||||
if platform.system() != "Linux":
|
||||
print("Mocking hardware info for non-Linux.")
|
||||
get_pci_devices_info = lambda: [
|
||||
{'type': 'VGA', 'vendor_id': '8086', 'device_id': '3e9b', 'description': 'Intel UHD Graphics 630 (Desktop Coffee Lake)', 'full_lspci_line':''},
|
||||
{'type': 'Audio', 'vendor_id': '8086', 'device_id': 'a348', 'description': 'Intel Cannon Point-LP HD Audio', 'full_lspci_line':''},
|
||||
{'type': 'Ethernet', 'vendor_id': '8086', 'device_id': '15b8', 'description': 'Intel I219-V Ethernet', 'full_lspci_line':''},
|
||||
{'type': 'VGA', 'vendor_id': '8086', 'device_id': '4680', 'description': 'Alder Lake UHD 770', 'full_lspci_line':''},
|
||||
{'type': 'Audio', 'vendor_id': '8086', 'device_id': '7ad0', 'description': 'Alder Lake PCH-P HD Audio', 'full_lspci_line':''},
|
||||
{'type': 'Ethernet', 'vendor_id': '10ec', 'device_id': '2502', 'description': 'Realtek RTL8125', 'full_lspci_line':''},
|
||||
]
|
||||
get_cpu_info = lambda: {"Model name": "Intel(R) Core(TM) i7-8700K CPU @ 3.70GHz", "Flags": "avx avx2"}
|
||||
get_cpu_info = lambda: {"Model name": "12th Gen Intel(R) Core(TM) i7-12700K", "Flags": "avx avx2"}
|
||||
get_audio_codecs = lambda: ["Realtek ALC1220", "Intel Alder Lake-S HDMI"]
|
||||
|
||||
success = enhance_config_plist(dummy_plist_path, "Sonoma", print)
|
||||
print(f"Plist enhancement {'succeeded' if success else 'failed'}.")
|
||||
if success:
|
||||
with open(dummy_plist_path, 'rb') as f:
|
||||
modified_data = plistlib.load(f)
|
||||
print("\n--- Modified Plist Content (first level keys) ---")
|
||||
for k,v in modified_data.items(): print(f"{k}: {type(v)}")
|
||||
|
||||
if needs_mocking:
|
||||
get_pci_devices_info = original_get_pci; get_cpu_info = original_get_cpu
|
||||
print("\n--- Testing with Sonoma (should enable iGPU, audio [ALC1220 layout 7], ethernet [LucyRTL8125]) ---")
|
||||
success_sonoma = enhance_config_plist(dummy_plist_path, "Sonoma", print)
|
||||
print(f"Plist enhancement for Sonoma {'succeeded' if success_sonoma else 'failed'}.")
|
||||
if success_sonoma:
|
||||
with open(dummy_plist_path, 'rb') as f: modified_data = plistlib.load(f)
|
||||
print(f" Sonoma boot-args: {modified_data.get('NVRAM',{}).get('Add',{}).get(boot_args_uuid,{}).get('boot-args')}")
|
||||
print(f" Sonoma iGPU props: {modified_data.get('DeviceProperties',{}).get('Add',{}).get(INTEL_IGPU_PCI_PATH)}")
|
||||
print(f" Sonoma Audio props: {modified_data.get('DeviceProperties',{}).get('Add',{}).get(AUDIO_PCI_PATH_FALLBACK)}")
|
||||
for kext in modified_data.get("Kernel",{}).get("Add",[]):
|
||||
if "LucyRTL8125Ethernet.kext" in kext.get("BundlePath",""): print(f" LucyRTL8125Ethernet.kext Enabled: {kext.get('Enabled')}")
|
||||
if "AppleALC.kext" in kext.get("BundlePath",""): print(f" AppleALC.kext Enabled: {kext.get('Enabled')}")
|
||||
|
||||
|
||||
if platform.system() != "Linux":
|
||||
get_pci_devices_info = original_get_pci; get_cpu_info = original_get_cpu; get_audio_codecs = original_get_audio_codecs
|
||||
|
||||
if os.path.exists(dummy_plist_path): os.remove(dummy_plist_path)
|
||||
if os.path.exists(dummy_plist_path + ".backup"): os.remove(dummy_plist_path + ".backup")
|
||||
|
@ -3,16 +3,38 @@ import subprocess
|
||||
import os
|
||||
import time
|
||||
import shutil
|
||||
import re # For parsing diskpart output
|
||||
import sys # For checking psutil import
|
||||
|
||||
# Try to import QMessageBox for the placeholder, otherwise use a mock for standalone test
|
||||
try:
|
||||
from PyQt6.QtWidgets import QMessageBox
|
||||
except ImportError:
|
||||
class QMessageBox: # Mock for standalone testing
|
||||
@staticmethod
|
||||
def information(*args): print(f"INFO (QMessageBox mock): Title='{args[1]}', Message='{args[2]}'")
|
||||
@staticmethod
|
||||
def warning(*args): print(f"WARNING (QMessageBox mock): Title='{args[1]}', Message='{args[2]}'"); return QMessageBox # Mock button press
|
||||
Yes = 1 # Mock value
|
||||
No = 0 # Mock value
|
||||
Cancel = 0 # Mock value
|
||||
|
||||
|
||||
class USBWriterWindows:
|
||||
def __init__(self, device_id: str, opencore_qcow2_path: str, macos_qcow2_path: str, progress_callback=None):
|
||||
self.device_id = device_id
|
||||
# Construct PhysicalDrive path carefully
|
||||
disk_number_str = "".join(filter(str.isdigit, device_id))
|
||||
self.physical_drive_path = f"\\\\.\\PhysicalDrive{disk_number_str}"
|
||||
def __init__(self, device_id: str, opencore_qcow2_path: str, macos_qcow2_path: str,
|
||||
progress_callback=None, enhance_plist_enabled: bool = False, target_macos_version: str = ""):
|
||||
# device_id is expected to be the disk number string, e.g., "1", "2" or "disk 1", "disk 2"
|
||||
self.disk_number = "".join(filter(str.isdigit, device_id))
|
||||
if not self.disk_number:
|
||||
raise ValueError(f"Invalid device_id format: '{device_id}'. Must contain a disk number.")
|
||||
|
||||
self.physical_drive_path = f"\\\\.\\PhysicalDrive{self.disk_number}"
|
||||
|
||||
self.opencore_qcow2_path = opencore_qcow2_path
|
||||
self.macos_qcow2_path = macos_qcow2_path
|
||||
self.progress_callback = progress_callback
|
||||
self.enhance_plist_enabled = enhance_plist_enabled # Not used in Windows writer yet
|
||||
self.target_macos_version = target_macos_version # Not used in Windows writer yet
|
||||
|
||||
pid = os.getpid()
|
||||
self.opencore_raw_path = f"opencore_temp_{pid}.raw"
|
||||
@ -24,10 +46,8 @@ class USBWriterWindows:
|
||||
self.assigned_efi_letter = None
|
||||
|
||||
def _report_progress(self, message: str):
|
||||
if self.progress_callback:
|
||||
self.progress_callback(message)
|
||||
else:
|
||||
print(message)
|
||||
if self.progress_callback: self.progress_callback(message)
|
||||
else: print(message)
|
||||
|
||||
def _run_command(self, command: list[str] | str, check=True, capture_output=False, timeout=None, shell=False, working_dir=None):
|
||||
self._report_progress(f"Executing: {command if isinstance(command, str) else ' '.join(command)}")
|
||||
@ -40,112 +60,183 @@ class USBWriterWindows:
|
||||
if process.stdout and process.stdout.strip(): self._report_progress(f"STDOUT: {process.stdout.strip()}")
|
||||
if process.stderr and process.stderr.strip(): self._report_progress(f"STDERR: {process.stderr.strip()}")
|
||||
return process
|
||||
except subprocess.TimeoutExpired:
|
||||
self._report_progress(f"Command timed out after {timeout} seconds.")
|
||||
raise
|
||||
except subprocess.CalledProcessError as e:
|
||||
self._report_progress(f"Error executing (code {e.returncode}): {e.stderr or e.stdout or str(e)}")
|
||||
raise
|
||||
except FileNotFoundError:
|
||||
self._report_progress(f"Error: Command '{command[0] if isinstance(command, list) else command.split()[0]}' not found.")
|
||||
raise
|
||||
except subprocess.TimeoutExpired: self._report_progress(f"Command timed out after {timeout} seconds."); raise
|
||||
except subprocess.CalledProcessError as e: self._report_progress(f"Error executing (code {e.returncode}): {e.stderr or e.stdout or str(e)}"); raise
|
||||
except FileNotFoundError: self._report_progress(f"Error: Command '{command[0] if isinstance(command, list) else command.split()[0]}' not found."); raise
|
||||
|
||||
def _run_diskpart_script(self, script_content: str):
|
||||
|
||||
def _run_diskpart_script(self, script_content: str, capture_output_for_parse=False) -> str | None:
|
||||
script_file_path = f"diskpart_script_{os.getpid()}.txt"
|
||||
with open(script_file_path, "w") as f:
|
||||
f.write(script_content)
|
||||
with open(script_file_path, "w") as f: f.write(script_content)
|
||||
output_text = "" # Initialize to empty string
|
||||
try:
|
||||
self._report_progress(f"Running diskpart script...\n{script_content}")
|
||||
self._run_command(["diskpart", "/s", script_file_path], capture_output=True, check=False)
|
||||
self._report_progress(f"Running diskpart script:\n{script_content}")
|
||||
process = self._run_command(["diskpart", "/s", script_file_path], capture_output=True, check=False)
|
||||
output_text = (process.stdout or "") + "\n" + (process.stderr or "") # Combine, as diskpart output can be inconsistent
|
||||
|
||||
# Check for known success messages, otherwise assume potential issue or log output for manual check.
|
||||
# This is not a perfect error check for diskpart.
|
||||
success_indicators = [
|
||||
"DiskPart successfully", "successfully completed", "succeeded in creating",
|
||||
"successfully formatted", "successfully assigned"
|
||||
]
|
||||
has_success_indicator = any(indicator in output_text for indicator in success_indicators)
|
||||
has_error_indicator = "Virtual Disk Service error" in output_text or "DiskPart has encountered an error" in output_text
|
||||
|
||||
if has_error_indicator:
|
||||
self._report_progress(f"Diskpart script may have failed. Output:\n{output_text}")
|
||||
# Optionally raise an error here if script is critical
|
||||
# raise subprocess.CalledProcessError(1, "diskpart", output=output_text)
|
||||
elif not has_success_indicator and "There are no partitions on this disk to show" not in output_text: # Allow benign message
|
||||
self._report_progress(f"Diskpart script output does not clearly indicate success. Output:\n{output_text}")
|
||||
|
||||
|
||||
if capture_output_for_parse:
|
||||
return output_text
|
||||
finally:
|
||||
if os.path.exists(script_file_path): os.remove(script_file_path)
|
||||
return output_text if capture_output_for_parse else None # Return None if not capturing for parse
|
||||
|
||||
|
||||
def _cleanup_temp_files_and_dirs(self):
|
||||
self._report_progress("Cleaning up...")
|
||||
for f_path in self.temp_files_to_clean:
|
||||
if os.path.exists(f_path): os.remove(f_path)
|
||||
if os.path.exists(f_path):
|
||||
try: os.remove(f_path)
|
||||
except Exception as e: self._report_progress(f"Could not remove temp file {f_path}: {e}")
|
||||
for d_path in self.temp_dirs_to_clean:
|
||||
if os.path.exists(d_path): shutil.rmtree(d_path, ignore_errors=True)
|
||||
if os.path.exists(d_path):
|
||||
try: shutil.rmtree(d_path, ignore_errors=True)
|
||||
except Exception as e: self._report_progress(f"Could not remove temp dir {d_path}: {e}")
|
||||
|
||||
|
||||
def _find_available_drive_letter(self) -> str | None:
|
||||
import string
|
||||
# This is a placeholder. Actual psutil or ctypes calls would be more robust.
|
||||
# For now, assume 'S' is available if not 'E' through 'Z'.
|
||||
return 'S'
|
||||
import string; used_letters = set()
|
||||
try:
|
||||
# Check if psutil was imported by the main application
|
||||
if 'psutil' in sys.modules:
|
||||
partitions = sys.modules['psutil'].disk_partitions(all=True)
|
||||
for p in partitions:
|
||||
if p.mountpoint and len(p.mountpoint) >= 2 and p.mountpoint[1] == ':': # Check for "X:"
|
||||
used_letters.add(p.mountpoint[0].upper())
|
||||
except Exception as e:
|
||||
self._report_progress(f"Could not list used drive letters with psutil: {e}. Will try common letters.")
|
||||
|
||||
for letter in "STUVWXYZGHIJKLMNOPQR":
|
||||
if letter not in used_letters and letter > 'D': # Avoid A, B, C, D
|
||||
# Further check if letter is truly available (e.g. subst) - more complex, skip for now
|
||||
return letter
|
||||
return None
|
||||
|
||||
def check_dependencies(self):
|
||||
self._report_progress("Checking dependencies (qemu-img, diskpart, robocopy)... DD for Win & 7z are manual checks.")
|
||||
dependencies = ["qemu-img", "diskpart", "robocopy"]
|
||||
missing = [dep for dep in dependencies if not shutil.which(dep)]
|
||||
if missing:
|
||||
raise RuntimeError(f"Missing dependencies: {', '.join(missing)}. qemu-img needs install & PATH.")
|
||||
dependencies = ["qemu-img", "diskpart", "robocopy"]; missing = [dep for dep in dependencies if not shutil.which(dep)]
|
||||
if missing: raise RuntimeError(f"Missing dependencies: {', '.join(missing)}. qemu-img needs install & PATH.")
|
||||
self._report_progress("Base dependencies found. Ensure 'dd for Windows' and '7z.exe' are in PATH if needed.")
|
||||
return True
|
||||
|
||||
def format_and_write(self) -> bool:
|
||||
try:
|
||||
self.check_dependencies()
|
||||
self._cleanup_temp_files_and_dirs()
|
||||
self._cleanup_temp_files_and_dirs() # Clean before start
|
||||
os.makedirs(self.temp_efi_extract_dir, exist_ok=True)
|
||||
|
||||
disk_number = "".join(filter(str.isdigit, self.device_id))
|
||||
self._report_progress(f"WARNING: ALL DATA ON DISK {disk_number} ({self.physical_drive_path}) WILL BE ERASED!")
|
||||
self._report_progress(f"WARNING: ALL DATA ON DISK {self.disk_number} ({self.physical_drive_path}) WILL BE ERASED!")
|
||||
|
||||
self.assigned_efi_letter = self._find_available_drive_letter()
|
||||
if not self.assigned_efi_letter:
|
||||
raise RuntimeError("Could not find an available drive letter for EFI.")
|
||||
self._report_progress(f"Attempting to use letter {self.assigned_efi_letter}: for EFI.")
|
||||
if not self.assigned_efi_letter: raise RuntimeError("Could not find an available drive letter for EFI.")
|
||||
self._report_progress(f"Will attempt to assign letter {self.assigned_efi_letter}: to EFI partition.")
|
||||
|
||||
script = f"select disk {disk_number}\nclean\nconvert gpt\n"
|
||||
script += f"create partition efi size=550\nformat fs=fat32 quick label=EFI\nassign letter={self.assigned_efi_letter}\n"
|
||||
script += "create partition primary label=macOS_USB\nexit\n"
|
||||
self._run_diskpart_script(script)
|
||||
diskpart_script_part1 = f"select disk {self.disk_number}\nclean\nconvert gpt\n"
|
||||
diskpart_script_part1 += f"create partition efi size=550\nformat fs=fat32 quick label=EFI\nassign letter={self.assigned_efi_letter}\n"
|
||||
diskpart_script_part1 += "create partition primary label=macOS_USB\nexit\n"
|
||||
self._run_diskpart_script(diskpart_script_part1)
|
||||
time.sleep(5)
|
||||
|
||||
macos_partition_offset_str = "Offset not determined"
|
||||
macos_partition_number_str = "2 (assumed)"
|
||||
|
||||
diskpart_script_detail = f"select disk {self.disk_number}\nselect partition 2\ndetail partition\nexit\n"
|
||||
detail_output = self._run_diskpart_script(diskpart_script_detail, capture_output_for_parse=True)
|
||||
|
||||
if detail_output:
|
||||
self._report_progress(f"Detail Partition Output:\n{detail_output}")
|
||||
offset_match = re.search(r"Offset in Bytes\s*:\s*(\d+)", detail_output, re.IGNORECASE)
|
||||
if offset_match: macos_partition_offset_str = f"{offset_match.group(1)} bytes ({int(offset_match.group(1)) // (1024*1024)} MiB)"
|
||||
|
||||
# Try to find the line "Partition X" where X is the number we want
|
||||
part_num_search = re.search(r"Partition\s+(\d+)\s*\n\s*Type", detail_output, re.IGNORECASE | re.MULTILINE)
|
||||
if part_num_search:
|
||||
macos_partition_number_str = part_num_search.group(1)
|
||||
self._report_progress(f"Determined macOS partition number: {macos_partition_number_str}")
|
||||
else: # Fallback if the above specific regex fails
|
||||
# Look for lines like "Partition 2", "Type : xxxxx"
|
||||
# This is brittle if diskpart output format changes
|
||||
partition_lines = [line for line in detail_output.splitlines() if "Partition " in line and "Type :" in line]
|
||||
if len(partition_lines) > 0 : # Assuming the one we want is the last "Partition X" before other details
|
||||
last_part_match = re.search(r"Partition\s*(\d+)", partition_lines[-1])
|
||||
if last_part_match: macos_partition_number_str = last_part_match.group(1)
|
||||
|
||||
|
||||
self._report_progress(f"Converting OpenCore QCOW2 to RAW: {self.opencore_raw_path}")
|
||||
self._run_command(["qemu-img", "convert", "-O", "raw", self.opencore_qcow2_path, self.opencore_raw_path])
|
||||
|
||||
self._report_progress("Extracting EFI files (using 7z if available)...")
|
||||
if shutil.which("7z"):
|
||||
# Simplified 7z call, assumes EFI folder is at root of first partition image by 7z
|
||||
self._run_command([
|
||||
"7z", "x", self.opencore_raw_path,
|
||||
f"-o{self.temp_efi_extract_dir}", "EFI", "-r", "-y"
|
||||
], check=False)
|
||||
self._report_progress("Attempting EFI extraction using 7-Zip...")
|
||||
self._run_command(["7z", "x", self.opencore_raw_path, f"-o{self.temp_efi_extract_dir}", "EFI", "-r", "-y"], check=False)
|
||||
source_efi_folder = os.path.join(self.temp_efi_extract_dir, "EFI")
|
||||
if not os.path.isdir(source_efi_folder):
|
||||
# Fallback: check if files were extracted to temp_efi_extract_dir directly
|
||||
if os.path.exists(os.path.join(self.temp_efi_extract_dir, "BOOTX64.EFI")):
|
||||
source_efi_folder = self.temp_efi_extract_dir
|
||||
else:
|
||||
raise RuntimeError("Could not extract EFI folder using 7-Zip.")
|
||||
if os.path.exists(os.path.join(self.temp_efi_extract_dir, "BOOTX64.EFI")): source_efi_folder = self.temp_efi_extract_dir
|
||||
else: raise RuntimeError("Could not extract EFI folder using 7-Zip from OpenCore image.")
|
||||
|
||||
target_efi_on_usb = f"{self.assigned_efi_letter}:\\EFI"
|
||||
if not os.path.exists(f"{self.assigned_efi_letter}:\\"): # Check if drive letter is mounted
|
||||
time.sleep(3) # Wait a bit more
|
||||
if not os.path.exists(f"{self.assigned_efi_letter}:\\"):
|
||||
raise RuntimeError(f"EFI partition {self.assigned_efi_letter}: not accessible after assign.")
|
||||
# Attempt to re-assign just in case
|
||||
self._report_progress(f"Re-assigning drive letter {self.assigned_efi_letter} to EFI partition...")
|
||||
reassign_script = f"select disk {self.disk_number}\nselect partition 1\nassign letter={self.assigned_efi_letter}\nexit\n"
|
||||
self._run_diskpart_script(reassign_script)
|
||||
time.sleep(3)
|
||||
if not os.path.exists(f"{self.assigned_efi_letter}:\\"):
|
||||
raise RuntimeError(f"EFI partition {self.assigned_efi_letter}: not accessible after assign/re-assign.")
|
||||
|
||||
if not os.path.exists(target_efi_on_usb): os.makedirs(target_efi_on_usb, exist_ok=True)
|
||||
self._report_progress(f"Copying EFI files to {target_efi_on_usb}")
|
||||
self._run_command(["robocopy", source_efi_folder, target_efi_on_usb, "/E", "/S", "/NFL", "/NDL", "/NJH", "/NJS", "/NC", "/NS", "/NP"], check=True)
|
||||
else:
|
||||
raise RuntimeError("7-Zip CLI (7z.exe) not found in PATH for EFI extraction.")
|
||||
self._report_progress(f"Copying EFI files from '{source_efi_folder}' to '{target_efi_on_usb}'")
|
||||
self._run_command(["robocopy", source_efi_folder, target_efi_on_usb, "/E", "/S", "/NFL", "/NDL", "/NJH", "/NJS", "/NC", "/NS", "/NP", "/XO"], check=True) # Added /XO to exclude older
|
||||
else: raise RuntimeError("7-Zip CLI (7z.exe) not found in PATH for EFI extraction.")
|
||||
|
||||
self._report_progress(f"Converting macOS QCOW2 to RAW: {self.macos_raw_path}")
|
||||
self._run_command(["qemu-img", "convert", "-O", "raw", self.macos_qcow2_path, self.macos_raw_path])
|
||||
|
||||
self._report_progress("Windows RAW macOS image writing is a placeholder.")
|
||||
self._report_progress(f"RAW image at: {self.macos_raw_path}")
|
||||
self._report_progress(f"Target physical drive: {self.physical_drive_path}")
|
||||
self._report_progress("User needs to use 'dd for Windows' to write the above raw image to the second partition of the USB drive.")
|
||||
# Placeholder for actual dd command, as it's complex and risky to automate fully without specific dd tool knowledge
|
||||
# E.g. dd if=self.macos_raw_path of=\\\\.\\PhysicalDriveX --partition 2 bs=4M status=progress (syntax depends on dd variant)
|
||||
abs_macos_raw_path = os.path.abspath(self.macos_raw_path)
|
||||
guidance_message = (
|
||||
f"RAW macOS image conversion complete:\n'{abs_macos_raw_path}'\n\n"
|
||||
f"Target USB: Disk {self.disk_number} (Path: {self.physical_drive_path})\n"
|
||||
f"The target macOS partition is: Partition {macos_partition_number_str}\n"
|
||||
f"Calculated Offset (approx): {macos_partition_offset_str}\n\n"
|
||||
"MANUAL STEP REQUIRED using a 'dd for Windows' utility:\n"
|
||||
"1. Open Command Prompt or PowerShell AS ADMINISTRATOR.\n"
|
||||
"2. Carefully identify your 'dd for Windows' utility and its exact syntax.\n"
|
||||
" Common utilities: dd from SUSE (recommended), dd by chrysocome.net.\n"
|
||||
"3. Example 'dd' command (SYNTAX VARIES GREATLY BETWEEN DD TOOLS!):\n"
|
||||
f" `dd if=\"{abs_macos_raw_path}\" of={self.physical_drive_path} bs=4M --progress`\n"
|
||||
" (This example writes to the whole disk, which might be okay if your macOS partition is the first primary after EFI and occupies the rest). \n"
|
||||
" A SAFER (but more complex) approach if your 'dd' supports it, is to write directly to the partition's OFFSET (requires dd that handles PhysicalDrive offsets correctly):\n"
|
||||
f" `dd if=\"{abs_macos_raw_path}\" of={self.physical_drive_path} seek=<PARTITION_OFFSET_IN_BLOCKS_OR_BYTES> bs=<YOUR_BLOCK_SIZE> ...`\n"
|
||||
" (The 'seek' parameter and its units depend on your dd tool. The offset from diskpart is in bytes.)\n\n"
|
||||
"VERIFY YOUR DD COMMAND AND TARGETS BEFORE EXECUTION. DATA LOSS IS LIKELY IF INCORRECT.\n"
|
||||
"This tool cannot automate this step due to the variability and risks of 'dd' utilities on Windows."
|
||||
)
|
||||
self._report_progress(f"GUIDANCE:\n{guidance_message}")
|
||||
QMessageBox.information(None, "Manual macOS Image Write Required", guidance_message)
|
||||
|
||||
self._report_progress("Windows USB writing process (EFI part done, macOS part placeholder) completed.")
|
||||
self._report_progress("Windows USB writing (EFI part automated, macOS part manual guidance provided) process initiated.")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self._report_progress(f"Error during Windows USB writing: {e}")
|
||||
import traceback
|
||||
self._report_progress(traceback.format_exc())
|
||||
import traceback; self._report_progress(traceback.format_exc())
|
||||
return False
|
||||
finally:
|
||||
if self.assigned_efi_letter:
|
||||
@ -155,20 +246,21 @@ class USBWriterWindows:
|
||||
if __name__ == '__main__':
|
||||
if platform.system() != "Windows":
|
||||
print("This script is for Windows standalone testing."); exit(1)
|
||||
print("USB Writer Windows Standalone Test - Partial Implementation")
|
||||
# Requires Admin privileges
|
||||
mock_oc = "mock_oc_win.qcow2"
|
||||
mock_mac = "mock_mac_win.qcow2"
|
||||
print("USB Writer Windows Standalone Test - Improved Guidance")
|
||||
mock_oc = "mock_oc_win.qcow2"; mock_mac = "mock_mac_win.qcow2"
|
||||
# Ensure qemu-img is available for mock file creation
|
||||
if not shutil.which("qemu-img"):
|
||||
print("qemu-img not found, cannot create mock files for test. Exiting.")
|
||||
exit(1)
|
||||
if not os.path.exists(mock_oc): subprocess.run(["qemu-img", "create", "-f", "qcow2", mock_oc, "384M"])
|
||||
if not os.path.exists(mock_mac): subprocess.run(["qemu-img", "create", "-f", "qcow2", mock_mac, "1G"])
|
||||
|
||||
disk_id = input("Enter target disk ID (e.g., '1' for 'disk 1'). WIPES DISK: ")
|
||||
if not disk_id.isdigit(): print("Invalid disk ID."); exit(1)
|
||||
actual_disk_id = f"disk {disk_id}" # This is how it's used in the class, but the input is just the number.
|
||||
disk_id_input = input("Enter target disk NUMBER (e.g., '1' for 'disk 1'). THIS DISK WILL BE WIPES: ")
|
||||
if not disk_id_input.isdigit(): print("Invalid disk number."); exit(1)
|
||||
|
||||
if input(f"Sure to wipe disk {disk_id}? (yes/NO): ").lower() == 'yes':
|
||||
# Pass the disk number string to the constructor, it will form \\.\PhysicalDriveX
|
||||
writer = USBWriterWindows(disk_id, mock_oc, mock_mac, print)
|
||||
if input(f"Sure to wipe disk {disk_id_input}? (yes/NO): ").lower() == 'yes':
|
||||
# USBWriterWindows expects just the disk number string (e.g., "1")
|
||||
writer = USBWriterWindows(disk_id_input, mock_oc, mock_mac, print)
|
||||
writer.format_and_write()
|
||||
else: print("Cancelled.")
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user