mirror of
https://github.com/sickcodes/Docker-OSX.git
synced 2025-06-21 09:02:48 +02:00
Merge b26a68956c
into e962dce97f
This commit is contained in:
commit
def368fc2a
52
.github/workflows/docker-build.yml
vendored
52
.github/workflows/docker-build.yml
vendored
@ -1,52 +0,0 @@
|
||||
name: Push Docker Image to Docker Hub
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
push_to_docker_hub:
|
||||
name: Push Docker Image to Docker Hub
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
id: checkout_code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Login to Docker Hub
|
||||
id: login_docker_hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USER_NAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
|
||||
|
||||
- name: Echo Docker Hub Username
|
||||
run: echo ${{ secrets.DOCKER_HUB_USER_NAME }}
|
||||
|
||||
- name: Echo GitHub SHA
|
||||
run: echo $GITHUB_SHA
|
||||
|
||||
- name: Build Docker image
|
||||
id: build_image
|
||||
run: |
|
||||
docker build "$GITHUB_WORKSPACE" -t sickcodes/docker-osx:master --label dockerfile-path="Dockerfile"
|
||||
|
||||
- name: Label Master Docker Image as Latest
|
||||
id: label_image
|
||||
run: |
|
||||
docker tag sickcodes/docker-osx:master sickcodes/docker-osx:latest
|
||||
|
||||
- name: Push Docker image master
|
||||
id: push_master
|
||||
run: docker push sickcodes/docker-osx:master
|
||||
|
||||
- name: Push Docker image latest
|
||||
id: push_latest
|
||||
run: docker push sickcodes/docker-osx:latest
|
||||
|
||||
- name: Logout from Docker Hub
|
||||
run: docker logout
|
||||
|
||||
- name: End
|
||||
run: echo "Docker image pushed to Docker Hub successfully"
|
24
Dockerfile
24
Dockerfile
@ -159,6 +159,13 @@ RUN yes | sudo pacman -Syu bc qemu-desktop libvirt dnsmasq virt-manager bridge-u
|
||||
|
||||
WORKDIR /home/arch/OSX-KVM
|
||||
|
||||
# shortname default is catalina, which means :latest is catalina
|
||||
ARG SHORTNAME=catalina
|
||||
|
||||
RUN make \
|
||||
&& qemu-img convert BaseSystem.dmg -O qcow2 -p -c BaseSystem.img \
|
||||
&& rm ./BaseSystem.dmg
|
||||
|
||||
# fix invalid signature on old libguestfs
|
||||
ARG SIGLEVEL=Never
|
||||
|
||||
@ -228,7 +235,7 @@ RUN grep -v InstallMedia ./Launch.sh > ./Launch-nopicker.sh \
|
||||
|
||||
USER arch
|
||||
|
||||
ENV USER=arch
|
||||
ENV USER arch
|
||||
|
||||
# These are hardcoded serials for non-iMessage related research
|
||||
# Overwritten by using GENERATE_UNIQUE=true
|
||||
@ -353,20 +360,7 @@ VOLUME ["/tmp/.X11-unix"]
|
||||
# the default serial numbers are already contained in ./OpenCore/OpenCore.qcow2
|
||||
# And the default serial numbers
|
||||
|
||||
# DMCA compliant download process
|
||||
# If BaseSystem.img does not exist, download ${SHORTNAME}
|
||||
|
||||
# shortname default is below
|
||||
ENV SHORTNAME=sequoia
|
||||
|
||||
ENV BASESYSTEM_IMAGE=BaseSystem.img
|
||||
|
||||
CMD ! [[ -e "${BASESYSTEM_IMAGE:-BaseSystem.img}" ]] \
|
||||
&& printf '%s\n' "No BaseSystem.img available, downloading ${SHORTNAME}" \
|
||||
&& make \
|
||||
&& qemu-img convert BaseSystem.dmg -O qcow2 -p -c ${BASESYSTEM_IMAGE:-BaseSystem.img} \
|
||||
&& rm ./BaseSystem.dmg \
|
||||
; sudo touch /dev/kvm /dev/snd "${IMAGE_PATH}" "${BOOTDISK}" "${ENV}" 2>/dev/null || true \
|
||||
CMD sudo touch /dev/kvm /dev/snd "${IMAGE_PATH}" "${BOOTDISK}" "${ENV}" 2>/dev/null || true \
|
||||
; sudo chown -R $(id -u):$(id -g) /dev/kvm /dev/snd "${IMAGE_PATH}" "${BOOTDISK}" "${ENV}" 2>/dev/null || true \
|
||||
; [[ "${NOPICKER}" == true ]] && { \
|
||||
sed -i '/^.*InstallMedia.*/d' Launch.sh \
|
||||
|
@ -206,20 +206,7 @@ ENV TERMS_OF_USE=i_agree
|
||||
|
||||
ENV BOILERPLATE="By using this Dockerfile, you hereby agree that you are a security reseacher or developer and agree to use this Dockerfile to make the world a safer place. Examples include: making your apps safer, finding your mobile phone, compiling security products, etc. You understand that Docker-OSX is an Open Source project, which is released to the public under the GNU Pulic License version 3 and above. You acknowledge that the Open Source project is absolutely unaffiliated with any third party, in any form whatsoever. Any trademarks or intelectual property which happen to be mentioned anywhere in or around the project are owned by their respective owners. By using this Dockerfile, you agree to agree to the EULA of each piece of upstream or downstream software. The following code is released for the sole purpose of security research, under the GNU Public License version 3. If you are concerned about the licensing, please note that this project is not AGPL. A copy of the license is available online: https://github.com/sickcodes/Docker-OSX/blob/master/LICENSE. In order to use the following Dockerfile you must read and understand the terms. Once you have read the terms, use the -e TERMS_OF_USE=i_agree or -e TERMS_OF_USE=i_disagree"
|
||||
|
||||
# DMCA compliant download process
|
||||
# If BaseSystem.img does not exist, download ${SHORTNAME}
|
||||
|
||||
# shortname default is catalina, which means :latest is catalina
|
||||
ENV SHORTNAME=sonoma
|
||||
|
||||
ENV BASESYSTEM_IMAGE=BaseSystem.img
|
||||
|
||||
CMD ! [[ -e "${BASESYSTEM_IMAGE:-BaseSystem.img}" ]] \
|
||||
&& printf '%s\n' "No BaseSystem.img available, downloading ${SHORTNAME}" \
|
||||
&& make \
|
||||
&& qemu-img convert BaseSystem.dmg -O qcow2 -p -c ${BASESYSTEM_IMAGE:-BaseSystem.img} \
|
||||
&& rm ./BaseSystem.dmg \
|
||||
; echo "${BOILERPLATE}" \
|
||||
CMD echo "${BOILERPLATE}" \
|
||||
; [[ "${TERMS_OF_USE}" = i_agree ]] || exit 1 \
|
||||
; echo "Disk is being copied between layers... Please wait a minute..." \
|
||||
; sudo touch /dev/kvm /dev/snd "${IMAGE_PATH}" "${BOOTDISK}" "${ENV}" 2>/dev/null || true \
|
||||
|
255
Dockerfile.monterey
Normal file
255
Dockerfile.monterey
Normal file
@ -0,0 +1,255 @@
|
||||
#!/usr/bin/docker
|
||||
# ____ __ ____ ______ __
|
||||
# / __ \____ _____/ /_____ _____/ __ \/ ___/ |/ /
|
||||
# / / / / __ \/ ___/ //_/ _ \/ ___/ / / /\__ \| /
|
||||
# / /_/ / /_/ / /__/ ,< / __/ / / /_/ /___/ / |
|
||||
# /_____/\____/\___/_/|_|\___/_/ \____//____/_/|_| [MONTEREY]
|
||||
#
|
||||
# Title: Docker-OSX (Mac on Docker)
|
||||
# Author: Sick.Codes https://twitter.com/sickcodes
|
||||
# Version: 6.0
|
||||
# License: GPLv3+
|
||||
# Repository: https://github.com/sickcodes/Docker-OSX
|
||||
# Website: https://sick.codes
|
||||
#
|
||||
# Notes: Uses a self-hosted BaseSystem.img from a USB installer.
|
||||
# If you want to DIY, use https://github.com/corpnewt/gibMacOS
|
||||
# Set seed as developer, and install the Install Assistant on Big Sur
|
||||
# Burn to a USB, and pull out BaseSystem.img
|
||||
# Or download from https://images.sick.codes/BaseSystem_Monterey.dmg
|
||||
#
|
||||
|
||||
FROM sickcodes/docker-osx
|
||||
|
||||
LABEL maintainer='https://twitter.com/sickcodes <https://sick.codes>'
|
||||
|
||||
SHELL ["/bin/bash", "-c"]
|
||||
|
||||
# change disk size here or add during build, e.g. --build-arg VERSION=10.14.5 --build-arg SIZE=50G
|
||||
ARG SIZE=200G
|
||||
ARG BASE_SYSTEM='https://images.sick.codes/BaseSystem_Monterey.dmg'
|
||||
|
||||
WORKDIR /home/arch/OSX-KVM
|
||||
|
||||
RUN wget -O BaseSystem.dmg "${BASE_SYSTEM}" \
|
||||
&& qemu-img convert BaseSystem.dmg -O qcow2 -p -c BaseSystem.img \
|
||||
&& rm -f BaseSystem.dmg
|
||||
|
||||
RUN qemu-img create -f qcow2 /home/arch/OSX-KVM/mac_hdd_ng.img "${SIZE}"
|
||||
|
||||
WORKDIR /home/arch/OSX-KVM
|
||||
|
||||
#### libguestfs versioning
|
||||
|
||||
# 5.13+ problem resolved by building the qcow2 against 5.12 using libguestfs-1.44.1-6
|
||||
|
||||
ENV SUPERMIN_KERNEL=/boot/vmlinuz-linux
|
||||
ENV SUPERMIN_MODULES=/lib/modules/5.12.14-arch1-1
|
||||
ENV SUPERMIN_KERNEL_VERSION=5.12.14-arch1-1
|
||||
ENV KERNEL_PACKAGE_URL=https://archive.archlinux.org/packages/l/linux/linux-5.12.14.arch1-1-x86_64.pkg.tar.zst
|
||||
ENV KERNEL_HEADERS_PACKAGE_URL=https://archive.archlinux.org/packages/l/linux/linux-headers-5.12.14.arch1-1-x86_64.pkg.tar.zst
|
||||
ENV LIBGUESTFS_PACKAGE_URL=https://archive.archlinux.org/packages/l/libguestfs/libguestfs-1.44.1-6-x86_64.pkg.tar.zst
|
||||
|
||||
ARG LINUX=true
|
||||
|
||||
# required to use libguestfs inside a docker container, to create bootdisks for docker-osx on-the-fly
|
||||
RUN if [[ "${LINUX}" == true ]]; then \
|
||||
sudo pacman -U "${KERNEL_PACKAGE_URL}" --noconfirm \
|
||||
; sudo pacman -U "${LIBGUESTFS_PACKAGE_URL}" --noconfirm \
|
||||
; sudo pacman -U "${KERNEL_HEADERS_PACKAGE_URL}" --noconfirm \
|
||||
; sudo pacman -S mkinitcpio --noconfirm \
|
||||
; sudo libguestfs-test-tool \
|
||||
; sudo rm -rf /var/tmp/.guestfs-* \
|
||||
; fi
|
||||
|
||||
####
|
||||
|
||||
|
||||
# optional --build-arg to change branches for testing
|
||||
ARG BRANCH=master
|
||||
ARG REPO='https://github.com/sickcodes/Docker-OSX.git'
|
||||
# RUN git clone --recurse-submodules --depth 1 --branch "${BRANCH}" "${REPO}"
|
||||
RUN rm -rf ./Docker-OSX \
|
||||
&& git clone --recurse-submodules --depth 1 --branch "${BRANCH}" "${REPO}"
|
||||
|
||||
RUN touch Launch.sh \
|
||||
&& chmod +x ./Launch.sh \
|
||||
&& tee -a Launch.sh <<< '#!/bin/bash' \
|
||||
&& tee -a Launch.sh <<< 'set -eux' \
|
||||
&& tee -a Launch.sh <<< 'sudo chown $(id -u):$(id -g) /dev/kvm 2>/dev/null || true' \
|
||||
&& tee -a Launch.sh <<< 'sudo chown -R $(id -u):$(id -g) /dev/snd 2>/dev/null || true' \
|
||||
&& tee -a Launch.sh <<< '[[ "${RAM}" = max ]] && export RAM="$(("$(head -n1 /proc/meminfo | tr -dc "[:digit:]") / 1000000"))"' \
|
||||
&& tee -a Launch.sh <<< '[[ "${RAM}" = half ]] && export RAM="$(("$(head -n1 /proc/meminfo | tr -dc "[:digit:]") / 2000000"))"' \
|
||||
&& tee -a Launch.sh <<< 'sudo chown -R $(id -u):$(id -g) /dev/snd 2>/dev/null || true' \
|
||||
&& tee -a Launch.sh <<< 'exec qemu-system-x86_64 -m ${RAM:-2}000 \' \
|
||||
&& tee -a Launch.sh <<< '-cpu ${CPU:-Penryn},${CPUID_FLAGS:-vendor=GenuineIntel,+invtsc,vmware-cpuid-freq=on,+ssse3,+sse4.2,+popcnt,+avx,+aes,+xsave,+xsaveopt,check,}${BOOT_ARGS} \' \
|
||||
&& tee -a Launch.sh <<< '-machine q35,${KVM-"accel=kvm:tcg"} \' \
|
||||
&& tee -a Launch.sh <<< '-smp ${CPU_STRING:-${SMP:-4},cores=${CORES:-4}} \' \
|
||||
&& tee -a Launch.sh <<< '-usb -device usb-kbd -device usb-tablet \' \
|
||||
&& tee -a Launch.sh <<< '-device isa-applesmc,osk=ourhardworkbythesewordsguardedpleasedontsteal\(c\)AppleComputerInc \' \
|
||||
&& tee -a Launch.sh <<< '-drive if=pflash,format=raw,readonly=on,file=/home/arch/OSX-KVM/OVMF_CODE.fd \' \
|
||||
&& tee -a Launch.sh <<< '-drive if=pflash,format=raw,file=/home/arch/OSX-KVM/OVMF_VARS-1024x768.fd \' \
|
||||
&& tee -a Launch.sh <<< '-smbios type=2 \' \
|
||||
&& tee -a Launch.sh <<< '-audiodev ${AUDIO_DRIVER:-alsa},id=hda -device ich9-intel-hda -device hda-duplex,audiodev=hda \' \
|
||||
&& tee -a Launch.sh <<< '-device ich9-ahci,id=sata \' \
|
||||
&& tee -a Launch.sh <<< '-drive id=OpenCoreBoot,if=none,snapshot=on,format=qcow2,file=${BOOTDISK:-/home/arch/OSX-KVM/OpenCore/OpenCore.qcow2} \' \
|
||||
&& tee -a Launch.sh <<< '-device ide-hd,bus=sata.2,drive=OpenCoreBoot \' \
|
||||
&& tee -a Launch.sh <<< '-device ide-hd,bus=sata.3,drive=InstallMedia \' \
|
||||
&& tee -a Launch.sh <<< '-drive id=InstallMedia,if=none,file=/home/arch/OSX-KVM/BaseSystem.img,format=qcow2 \' \
|
||||
&& tee -a Launch.sh <<< '-drive id=MacHDD,if=none,file=${IMAGE_PATH:-/home/arch/OSX-KVM/mac_hdd_ng.img},format=${IMAGE_FORMAT:-qcow2} \' \
|
||||
&& tee -a Launch.sh <<< '-device ide-hd,bus=sata.4,drive=MacHDD \' \
|
||||
&& tee -a Launch.sh <<< '-netdev user,id=net0,hostfwd=tcp::${INTERNAL_SSH_PORT:-10022}-:22,hostfwd=tcp::${SCREEN_SHARE_PORT:-5900}-:5900,${ADDITIONAL_PORTS} \' \
|
||||
&& tee -a Launch.sh <<< '-device ${NETWORKING:-vmxnet3},netdev=net0,id=net0,mac=${MAC_ADDRESS:-52:54:00:09:49:17} \' \
|
||||
&& tee -a Launch.sh <<< '-monitor stdio \' \
|
||||
&& tee -a Launch.sh <<< '-boot menu=on \' \
|
||||
&& tee -a Launch.sh <<< '-vga vmware \' \
|
||||
&& tee -a Launch.sh <<< '${EXTRA:-}'
|
||||
|
||||
# docker exec containerid mv ./Launch-nopicker.sh ./Launch.sh
|
||||
# This is now a legacy command.
|
||||
# You can use -e BOOTDISK=/bootdisk with -v ./bootdisk.img:/bootdisk
|
||||
RUN grep -v InstallMedia ./Launch.sh > ./Launch-nopicker.sh \
|
||||
&& chmod +x ./Launch-nopicker.sh \
|
||||
&& sed -i -e s/OpenCore\.qcow2/OpenCore\-nopicker\.qcow2/ ./Launch-nopicker.sh
|
||||
|
||||
USER arch
|
||||
|
||||
ENV USER arch
|
||||
|
||||
|
||||
#### libguestfs versioning
|
||||
|
||||
# 5.13+ problem resolved by building the qcow2 against 5.12 using libguestfs-1.44.1-6
|
||||
|
||||
ENV SUPERMIN_KERNEL=/boot/vmlinuz-linux
|
||||
ENV SUPERMIN_MODULES=/lib/modules/5.12.14-arch1-1
|
||||
ENV SUPERMIN_KERNEL_VERSION=5.12.14-arch1-1
|
||||
ENV KERNEL_PACKAGE_URL=https://archive.archlinux.org/packages/l/linux/linux-5.12.14.arch1-1-x86_64.pkg.tar.zst
|
||||
ENV KERNEL_HEADERS_PACKAGE_URL=https://archive.archlinux.org/packages/l/linux/linux-headers-5.12.14.arch1-1-x86_64.pkg.tar.zst
|
||||
ENV LIBGUESTFS_PACKAGE_URL=https://archive.archlinux.org/packages/l/libguestfs/libguestfs-1.44.1-6-x86_64.pkg.tar.zst
|
||||
|
||||
RUN sudo pacman -Syy \
|
||||
&& sudo pacman -Rns linux --noconfirm \
|
||||
; sudo pacman -S mkinitcpio --noconfirm \
|
||||
&& sudo pacman -U "${KERNEL_PACKAGE_URL}" --noconfirm \
|
||||
&& sudo pacman -U "${LIBGUESTFS_PACKAGE_URL}" --noconfirm \
|
||||
&& rm -rf /var/tmp/.guestfs-* \
|
||||
; libguestfs-test-tool || exit 1
|
||||
|
||||
####
|
||||
|
||||
# symlink the old directory, for redundancy
|
||||
RUN ln -s /home/arch/OSX-KVM/OpenCore /home/arch/OSX-KVM/OpenCore-Catalina || true
|
||||
|
||||
####
|
||||
|
||||
#### SPECIAL RUNTIME ARGUMENTS BELOW
|
||||
|
||||
# env -e ADDITIONAL_PORTS with a comma
|
||||
# for example, -e ADDITIONAL_PORTS=hostfwd=tcp::23-:23,
|
||||
ENV ADDITIONAL_PORTS=
|
||||
|
||||
# add additional QEMU boot arguments
|
||||
ENV BOOT_ARGS=
|
||||
|
||||
ENV BOOTDISK=
|
||||
|
||||
# edit the CPU that is being emulated
|
||||
ENV CPU=Penryn
|
||||
ENV CPUID_FLAGS='vendor=GenuineIntel,+invtsc,vmware-cpuid-freq=on,+ssse3,+sse4.2,+popcnt,+avx,+aes,+xsave,+xsaveopt,check,'
|
||||
|
||||
ENV DISPLAY=:0.0
|
||||
|
||||
# Deprecated
|
||||
ENV ENV=/env
|
||||
|
||||
# Boolean for generating a bootdisk with new random serials.
|
||||
ENV GENERATE_UNIQUE=false
|
||||
|
||||
# Boolean for generating a bootdisk with specific serials.
|
||||
ENV GENERATE_SPECIFIC=false
|
||||
|
||||
ENV IMAGE_PATH=/home/arch/OSX-KVM/mac_hdd_ng.img
|
||||
ENV IMAGE_FORMAT=qcow2
|
||||
|
||||
ENV KVM='accel=kvm:tcg'
|
||||
|
||||
ENV MASTER_PLIST_URL="https://raw.githubusercontent.com/sickcodes/osx-serial-generator/master/config-custom.plist"
|
||||
|
||||
# ENV NETWORKING=e1000-82545em
|
||||
ENV NETWORKING=vmxnet3
|
||||
|
||||
# boolean for skipping the disk selection menu at in the boot process
|
||||
ENV NOPICKER=false
|
||||
|
||||
# dynamic RAM options for runtime
|
||||
ENV RAM=3
|
||||
# ENV RAM=max
|
||||
# ENV RAM=half
|
||||
|
||||
# The x and y coordinates for resolution.
|
||||
# Must be used with either -e GENERATE_UNIQUE=true or -e GENERATE_SPECIFIC=true.
|
||||
ENV WIDTH=1920
|
||||
ENV HEIGHT=1080
|
||||
|
||||
# libguestfs verbose
|
||||
ENV LIBGUESTFS_DEBUG=1
|
||||
ENV LIBGUESTFS_TRACE=1
|
||||
|
||||
VOLUME ["/tmp/.X11-unix"]
|
||||
|
||||
# check if /image is a disk image or a directory. This allows you to optionally use -v disk.img:/image
|
||||
# NOPICKER is used to skip the disk selection screen
|
||||
# GENERATE_UNIQUE is used to generate serial numbers on boot.
|
||||
# /env is a file that you can generate and save using -v source.sh:/env
|
||||
# the env file is a file that you can carry to the next container which will supply the serials numbers.
|
||||
# GENERATE_SPECIFIC is used to either accept the env serial numbers OR you can supply using:
|
||||
# -e DEVICE_MODEL="iMacPro1,1" \
|
||||
# -e SERIAL="C02TW0WAHX87" \
|
||||
# -e BOARD_SERIAL="C027251024NJG36UE" \
|
||||
# -e UUID="5CCB366D-9118-4C61-A00A-E5BAF3BED451" \
|
||||
# -e MAC_ADDRESS="A8:5C:2C:9A:46:2F" \
|
||||
|
||||
# the output will be /bootdisk.
|
||||
# /bootdisk is a useful persistent place to store the 15Mb serial number bootdisk.
|
||||
|
||||
# if you don't set any of the above:
|
||||
# the default serial numbers are already contained in ./OpenCore/OpenCore.qcow2
|
||||
# And the default serial numbers
|
||||
|
||||
CMD sudo touch /dev/kvm /dev/snd "${IMAGE_PATH}" "${BOOTDISK}" "${ENV}" 2>/dev/null || true \
|
||||
; sudo chown -R $(id -u):$(id -g) /dev/kvm /dev/snd "${IMAGE_PATH}" "${BOOTDISK}" "${ENV}" 2>/dev/null || true \
|
||||
; [[ "${NOPICKER}" == true ]] && { \
|
||||
sed -i '/^.*InstallMedia.*/d' Launch.sh \
|
||||
&& export BOOTDISK="${BOOTDISK:=/home/arch/OSX-KVM/OpenCore/OpenCore-nopicker.qcow2}" \
|
||||
; } \
|
||||
|| export BOOTDISK="${BOOTDISK:=/home/arch/OSX-KVM/OpenCore/OpenCore.qcow2}" \
|
||||
; [[ "${GENERATE_UNIQUE}" == true ]] && { \
|
||||
./Docker-OSX/osx-serial-generator/generate-unique-machine-values.sh \
|
||||
--master-plist-url="${MASTER_PLIST_URL}" \
|
||||
--count 1 \
|
||||
--tsv ./serial.tsv \
|
||||
--bootdisks \
|
||||
--width "${WIDTH:-1920}" \
|
||||
--height "${HEIGHT:-1080}" \
|
||||
--output-bootdisk "${BOOTDISK:=/home/arch/OSX-KVM/OpenCore/OpenCore.qcow2}" \
|
||||
--output-env "${ENV:=/env}" \
|
||||
|| exit 1 ; } \
|
||||
; [[ "${GENERATE_SPECIFIC}" == true ]] && { \
|
||||
source "${ENV:=/env}" 2>/dev/null \
|
||||
; ./Docker-OSX/osx-serial-generator/generate-specific-bootdisk.sh \
|
||||
--master-plist-url="${MASTER_PLIST_URL}" \
|
||||
--model "${DEVICE_MODEL}" \
|
||||
--serial "${SERIAL}" \
|
||||
--board-serial "${BOARD_SERIAL}" \
|
||||
--uuid "${UUID}" \
|
||||
--mac-address "${MAC_ADDRESS}" \
|
||||
--width "${WIDTH:-1920}" \
|
||||
--height "${HEIGHT:-1080}" \
|
||||
--output-bootdisk "${BOOTDISK:=/home/arch/OSX-KVM/OpenCore/OpenCore.qcow2}" \
|
||||
|| exit 1 ; } \
|
||||
; ./enable-ssh.sh && /bin/bash -c ./Launch.sh
|
||||
|
||||
# virt-manager mode: eta son
|
||||
# CMD virsh define <(envsubst < Docker-OSX.xml) && virt-manager || virt-manager
|
||||
# CMD virsh define <(envsubst < macOS-libvirt-Catalina.xml) && virt-manager || virt-manager
|
@ -166,20 +166,7 @@ ENV HEIGHT=1080
|
||||
ENV LIBGUESTFS_DEBUG=1
|
||||
ENV LIBGUESTFS_TRACE=1
|
||||
|
||||
# DMCA compliant download process
|
||||
# If BaseSystem.img does not exist, download ${SHORTNAME}
|
||||
|
||||
# shortname default is catalina, which means :latest is catalina
|
||||
ENV SHORTNAME=sonoma
|
||||
|
||||
ENV BASESYSTEM_IMAGE=BaseSystem.img
|
||||
|
||||
CMD ! [[ -e "${BASESYSTEM_IMAGE:-BaseSystem.img}" ]] \
|
||||
&& printf '%s\n' "No BaseSystem.img available, downloading ${SHORTNAME}" \
|
||||
&& make \
|
||||
&& qemu-img convert BaseSystem.dmg -O qcow2 -p -c ${BASESYSTEM_IMAGE:-BaseSystem.img} \
|
||||
&& rm ./BaseSystem.dmg \
|
||||
; sudo touch /dev/kvm /dev/snd "${IMAGE_PATH}" "${BOOTDISK}" "${ENV}" 2>/dev/null || true \
|
||||
CMD sudo touch /dev/kvm /dev/snd "${IMAGE_PATH}" "${BOOTDISK}" "${ENV}" 2>/dev/null || true \
|
||||
; sudo chown -R $(id -u):$(id -g) /dev/kvm /dev/snd "${IMAGE_PATH}" "${BOOTDISK}" "${ENV}" 2>/dev/null || true \
|
||||
; { [[ "${DISPLAY}" = ':99' ]] || [[ "${HEADLESS}" == true ]] ; } && { \
|
||||
nohup Xvfb :99 -screen 0 1920x1080x16 \
|
||||
|
@ -183,20 +183,7 @@ ENV TERMS_OF_USE=i_agree
|
||||
|
||||
ENV BOILERPLATE="By using this Dockerfile, you hereby agree that you are a security reseacher or developer and agree to use this Dockerfile to make the world a safer place. Examples include: making your apps safer, finding your mobile phone, compiling security products, etc. You understand that Docker-OSX is an Open Source project, which is released to the public under the GNU Pulic License version 3 and above. You acknowledge that the Open Source project is absolutely unaffiliated with any third party, in any form whatsoever. Any trademarks or intelectual property which happen to be mentioned anywhere in or around the project are owned by their respective owners. By using this Dockerfile, you agree to agree to the EULA of each piece of upstream or downstream software. The following code is released for the sole purpose of security research, under the GNU Public License version 3. If you are concerned about the licensing, please note that this project is not AGPL. A copy of the license is available online: https://github.com/sickcodes/Docker-OSX/blob/master/LICENSE. In order to use the following Dockerfile you must read and understand the terms. Once you have read the terms, use the -e TERMS_OF_USE=i_agree or -e TERMS_OF_USE=i_disagree"
|
||||
|
||||
# DMCA compliant download process
|
||||
# If BaseSystem.img does not exist, download ${SHORTNAME}
|
||||
|
||||
# shortname default is catalina, which means :latest is catalina
|
||||
ENV SHORTNAME=sonoma
|
||||
|
||||
ENV BASESYSTEM_IMAGE=BaseSystem.img
|
||||
|
||||
CMD ! [[ -e "${BASESYSTEM_IMAGE:-BaseSystem.img}" ]] \
|
||||
&& printf '%s\n' "No BaseSystem.img available, downloading ${SHORTNAME}" \
|
||||
&& make \
|
||||
&& qemu-img convert BaseSystem.dmg -O qcow2 -p -c ${BASESYSTEM_IMAGE:-BaseSystem.img} \
|
||||
&& rm ./BaseSystem.dmg \
|
||||
; echo "${BOILERPLATE}" \
|
||||
CMD echo "${BOILERPLATE}" \
|
||||
; [[ "${TERMS_OF_USE}" = i_agree ]] || exit 1 \
|
||||
; echo "Disk is being copied between layers... Please wait a minute..." \
|
||||
; sudo touch /dev/kvm /dev/snd "${IMAGE_PATH}" "${BOOTDISK}" "${ENV}" 2>/dev/null || true \
|
||||
|
0
EFI_template_installer/EFI/BOOT/BOOTx64.efi
Normal file
0
EFI_template_installer/EFI/BOOT/BOOTx64.efi
Normal file
0
EFI_template_installer/EFI/OC/ACPI/SSDT-AWAC.aml
Normal file
0
EFI_template_installer/EFI/OC/ACPI/SSDT-AWAC.aml
Normal file
0
EFI_template_installer/EFI/OC/ACPI/SSDT-EC-USBX.aml
Normal file
0
EFI_template_installer/EFI/OC/ACPI/SSDT-EC-USBX.aml
Normal file
0
EFI_template_installer/EFI/OC/ACPI/SSDT-RHUB.aml
Normal file
0
EFI_template_installer/EFI/OC/ACPI/SSDT-RHUB.aml
Normal file
0
EFI_template_installer/EFI/OC/Drivers/HfsPlus.efi
Normal file
0
EFI_template_installer/EFI/OC/Drivers/HfsPlus.efi
Normal file
0
EFI_template_installer/EFI/OC/Kexts/AppleALC.kext
Normal file
0
EFI_template_installer/EFI/OC/Kexts/AppleALC.kext
Normal file
0
EFI_template_installer/EFI/OC/Kexts/IntelMausi.kext
Normal file
0
EFI_template_installer/EFI/OC/Kexts/IntelMausi.kext
Normal file
0
EFI_template_installer/EFI/OC/Kexts/Lilu.kext
Normal file
0
EFI_template_installer/EFI/OC/Kexts/Lilu.kext
Normal file
0
EFI_template_installer/EFI/OC/Kexts/VirtualSMC.kext
Normal file
0
EFI_template_installer/EFI/OC/Kexts/VirtualSMC.kext
Normal file
0
EFI_template_installer/EFI/OC/OpenCore.efi
Normal file
0
EFI_template_installer/EFI/OC/OpenCore.efi
Normal file
84
EFI_template_installer/EFI/OC/config-template.plist
Normal file
84
EFI_template_installer/EFI/OC/config-template.plist
Normal file
@ -0,0 +1,84 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>ACPI</key>
|
||||
<dict>
|
||||
<key>Add</key> <array/>
|
||||
<key>Delete</key> <array/>
|
||||
<key>Patch</key> <array/>
|
||||
<key>Quirks</key>
|
||||
<dict>
|
||||
<key>FadtEnableReset</key> <false/>
|
||||
<key>NormalizeHeaders</key> <false/>
|
||||
<key>RebaseRegions</key> <false/>
|
||||
<key>ResetHwSig</key> <false/>
|
||||
<key>ResetLogoStatus</key> <true/>
|
||||
<key>SyncTableIds</key> <false/>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>Booter</key>
|
||||
<dict>
|
||||
<key>MmioWhitelist</key> <array/>
|
||||
<key>Patch</key> <array/>
|
||||
<key>Quirks</key>
|
||||
<dict>
|
||||
<key>AllowRelocationBlock</key> <false/>
|
||||
<key>AvoidRuntimeDefrag</key> <true/>
|
||||
<key>DevirtualiseMmio</key> <false/> <!-- Change to true for Alder Lake B660/Z690 if needed -->
|
||||
<key>DisableSingleUser</key> <false/>
|
||||
<key>DisableVariableWrite</key> <false/>
|
||||
<key>DiscardHibernateMap</key> <false/>
|
||||
<key>EnableSafeModeSlide</key> <true/>
|
||||
<key>EnableWriteUnprotector</key> <false/> <!-- Keep false, OpenRuntime handles this -->
|
||||
<key>ForceBooterSignature</key> <false/>
|
||||
<key>ForceExitBootServices</key> <false/>
|
||||
<key>ProtectMemoryRegions</key> <false/>
|
||||
<key>ProtectSecureBoot</key> <false/>
|
||||
<key>ProtectUefiServices</key> <false/>
|
||||
<key>ProvideCustomSlide</key> <true/>
|
||||
<key>ProvideMaxSlide</key> <integer>0</integer>
|
||||
<key>RebuildAppleMemoryMap</key> <false/> <!-- Change to true for Alder Lake if needed -->
|
||||
<key>ResizeAppleGpuBars</key> <integer>-1</integer>
|
||||
<key>SetupVirtualMap</key> <true/>
|
||||
<key>SignalAppleOS</key> <false/>
|
||||
<key>SyncRuntimePermissions</key> <false/> <!-- Change to true for Alder Lake if needed -->
|
||||
</dict>
|
||||
</dict>
|
||||
<key>DeviceProperties</key> <dict><key>Add</key><dict/><key>Delete</key><dict/></dict>
|
||||
<key>Kernel</key>
|
||||
<dict>
|
||||
<key>Add</key> <array>
|
||||
<!-- Lilu -->
|
||||
<dict><key>Arch</key><string>Any</string><key>BundlePath</key><string>Lilu.kext</string><key>Comment</key><string>Patch engine</string><key>Enabled</key><true/><key>ExecutablePath</key><string>Contents/MacOS/Lilu</string><key>MaxKernel</key><string></string><key>MinKernel</key><string></string><key>PlistPath</key><string>Contents/Info.plist</string></dict>
|
||||
<!-- VirtualSMC -->
|
||||
<dict><key>Arch</key><string>Any</string><key>BundlePath</key><string>VirtualSMC.kext</string><key>Comment</key><string>SMC emulator</string><key>Enabled</key><true/><key>ExecutablePath</key><string>Contents/MacOS/VirtualSMC</string><key>MaxKernel</key><string></string><key>MinKernel</key><string></string><key>PlistPath</key><string>Contents/Info.plist</string></dict>
|
||||
<!-- WhateverGreen -->
|
||||
<dict><key>Arch</key><string>Any</string><key>BundlePath</key><string>WhateverGreen.kext</string><key>Comment</key><string>Video patches</string><key>Enabled</key><true/><key>ExecutablePath</key><string>Contents/MacOS/WhateverGreen</string><key>MaxKernel</key><string></string><key>MinKernel</key><string></string><key>PlistPath</key><string>Contents/Info.plist</string></dict>
|
||||
<!-- AppleALC -->
|
||||
<dict><key>Arch</key><string>Any</string><key>BundlePath</key><string>AppleALC.kext</string><key>Comment</key><string>Audio patches</string><key>Enabled</key><false/><key>ExecutablePath</key><string>Contents/MacOS/AppleALC</string><key>MaxKernel</key><string></string><key>MinKernel</key><string></string><key>PlistPath</key><string>Contents/Info.plist</string></dict>
|
||||
<!-- Ethernet Kexts (disabled by default, enabled by plist_modifier) -->
|
||||
<dict><key>Arch</key><string>Any</string><key>BundlePath</key><string>IntelMausi.kext</string><key>Comment</key><string>Intel Ethernet</string><key>Enabled</key><false/><key>ExecutablePath</key><string>Contents/MacOS/IntelMausi</string><key>MaxKernel</key><string></string><key>MinKernel</key><string></string><key>PlistPath</key><string>Contents/Info.plist</string></dict>
|
||||
<dict><key>Arch</key><string>Any</string><key>BundlePath</key><string>RealtekRTL8111.kext</string><key>Comment</key><string>Realtek RTL8111</string><key>Enabled</key><false/><key>ExecutablePath</key><string>Contents/MacOS/RealtekRTL8111</string><key>MaxKernel</key><string></string><key>MinKernel</key><string></string><key>PlistPath</key><string>Contents/Info.plist</string></dict>
|
||||
<dict><key>Arch</key><string>Any</string><key>BundlePath</key><string>LucyRTL8125Ethernet.kext</string><key>Comment</key><string>Realtek RTL8125</string><key>Enabled</key><false/><key>ExecutablePath</key><string>Contents/MacOS/LucyRTL8125Ethernet</string><key>MaxKernel</key><string></string><key>MinKernel</key><string></string><key>PlistPath</key><string>Contents/Info.plist</string></dict>
|
||||
</array>
|
||||
<key>Block</key> <array/> <key>Emulate</key> <dict/> <key>Force</key> <array/> <key>Patch</key> <array/>
|
||||
<key>Quirks</key>
|
||||
<dict>
|
||||
<key>AppleCpuPmCfgLock</key> <false/> <key>AppleXcpmCfgLock</key> <true/> <key>AppleXcpmExtraMsrs</key> <false/>
|
||||
<key>AppleXcpmForceBoost</key> <false/> <key>CustomPciSerialDevice</key> <false/> <key>CustomSMBIOSGuid</key> <false/>
|
||||
<key>DisableIoMapper</key> <true/> <key>DisableLinkeditJettison</key> <true/> <key>DisableRtcChecksum</key> <false/>
|
||||
<key>ExtendBTFeatureFlags</key> <false/> <key>ExternalDiskIcons</key> <false/> <key>ForceAquantiaEthernet</key> <false/>
|
||||
<key>ForceSecureBootScheme</key> <false/> <key>IncreasePciBarSize</key> <false/> <key>LapicKernelPanic</key> <false/>
|
||||
<key>LegacyCommpage</key> <false/> <key>PanicNoKextDump</key> <true/> <key>PowerTimeoutKernelPanic</key> <true/>
|
||||
<key>ProvideCurrentCpuInfo</key> <false/> <key>SetApfsTrimTimeout</key> <integer>-1</integer>
|
||||
<key>ThirdPartyDrives</key> <false/> <key>XhciPortLimit</key> <false/>
|
||||
</dict>
|
||||
<key>Scheme</key> <dict><key>CustomKernel</key><false/><key>FuzzyMatch</key><true/><key>KernelArch</key><string>Auto</string><key>KernelCache</key><string>Auto</string></dict>
|
||||
</dict>
|
||||
<key>Misc</key> <dict> <key>BlessOverride</key><array/><key>Boot</key><dict><key>ConsoleAttributes</key><integer>0</integer><key>HibernateMode</key><string>None</string><key>HibernateSkipsPicker</key><false/><key>HideAuxiliary</key><false/><key>LauncherOption</key><string>Disabled</string><key>LauncherPath</key><string>Default</string><key>PickerAttributes</key><integer>17</integer><key>PickerAudioAssist</key><false/><key>PickerMode</key><string>External</string><key>PickerVariant</key><string>Auto</string><key>PollAppleHotKeys</key><true/><key>ShowPicker</key><true/><key>TakeoffDelay</key><integer>0</integer><key>Timeout</key><integer>5</integer></dict><key>Debug</key><dict><key>AppleDebug</key><false/><key>ApplePanic</key><false/><key>DisableWatchDog</key><true/><key>DisplayDelay</key><integer>0</integer><key>DisplayLevel</key><integer>2147483650</integer><key>LogModules</key><string>*</string><key>SysReport</key><false/><key>Target</key><integer>3</integer></dict><key>Entries</key><array/><key>Security</key><dict><key>AllowSetDefault</key><true/><key>ApECID</key><integer>0</integer><key>AuthRestart</key><false/><key>BlacklistAppleUpdate</key><true/><key>DmgLoading</key><string>Signed</string><key>EnablePassword</key><false/><key>ExposeSensitiveData</key><integer>6</integer><key>HaltLevel</key><integer>2147483648</integer><key>PasswordHash</key><data></data><key>PasswordSalt</key><data></data><key>ScanPolicy</key><integer>0</integer><key>SecureBootModel</key><string>Disabled</string><key>Vault</key><string>Optional</string></dict><key>Serial</key><dict><key>Init</key><false/><key>Override</key><false/></dict><key>Tools</key><array/></dict>
|
||||
<key>NVRAM</key> <dict><key>Add</key><dict><key>4D1EDE05-38C7-4A6A-9CC6-4BCCA8B38C14</key><dict><key>DefaultBackgroundColor</key><data>AAAAAA==</data><key>UIScale</key><data>AQ==</data></dict><key>7C436110-AB2A-4BBB-A880-FE41995C9F82</key><dict><key>SystemAudioVolume</key><data>Rg==</data><key>boot-args</key><string>-v keepsyms=1 debug=0x100</string><key>csr-active-config</key><data>AAAAAA==</data><key>prev-lang:kbd</key><data>ZW4tVVM6MA==</data><key>run-efi-updater</key><string>No</string></dict></dict><key>Delete</key><dict><key>4D1EDE05-38C7-4A6A-9CC6-4BCCA8B38C14</key><array><string>UIScale</string><string>DefaultBackgroundColor</string></array><key>7C436110-AB2A-4BBB-A880-FE41995C9F82</key><array><string>boot-args</string></dict></dict><key>LegacySchema</key><dict/><key>WriteFlash</key><true/></dict>
|
||||
<key>PlatformInfo</key> <dict><key>Automatic</key><true/><key>CustomMemory</key><false/><key>Generic</key><dict><key>AdviseFeatures</key><false/><key>MLB</key><string>PLEASE_REPLACE_MLB</string><key>MaxBIOSVersion</key><false/><key>ProcessorType</key><integer>0</integer><key>ROM</key><data>AAAAAA==</data><key>SpoofVendor</key><true/><key>SystemMemoryStatus</key><string>Auto</string><key>SystemProductName</key><string>iMacPro1,1</string><key>SystemSerialNumber</key><string>PLEASE_REPLACE_SERIAL</string><key>SystemUUID</key><string>PLEASE_REPLACE_UUID</string></dict><key>UpdateDataHub</key><true/><key>UpdateNVRAM</key><true/><key>UpdateSMBIOS</key><true/><key>UpdateSMBIOSMode</key><string>Create</string><key>UseRawUuidEncoding</key><false/></dict>
|
||||
<key>UEFI</key> <dict><key>APFS</key><dict><key>EnableJumpstart</key><true/><key>GlobalConnect</key><false/><key>HideVerbose</key><true/><key>JumpstartHotPlug</key><false/><key>MinDate</key><integer>0</integer><key>MinVersion</key><integer>0</integer></dict><key>AppleInput</key><dict><key>AppleEvent</key><string>Builtin</string><key>CustomDelays</key><false/><key>GraphicsInputMirroring</key><true/><key>KeyInitialDelay</key><integer>50</integer><key>KeySubsequentDelay</key><integer>5</integer><key>PointerSpeedDiv</key><integer>1</integer><key>PointerSpeedMul</key><integer>1</integer></dict><key>Audio</key><dict><key>AudioCodec</key><integer>0</integer><key>AudioDevice</key><string></string><key>AudioOutMask</key><integer>-1</integer><key>AudioSupport</key><false/><key>DisconnectHda</key><false/><key>MaximumGain</key><integer>-15</integer><key>MinimumAssistGain</key><integer>-30</integer><key>MinimumAudibleGain</key><integer>-55</integer><key>PlayChime</key><string>Auto</string><key>ResetTrafficClass</key><false/><key>SetupDelay</key><integer>0</integer></dict><key>ConnectDrivers</key><true/><key>Drivers</key><array><string>HfsPlus.efi</string><string>OpenRuntime.efi</string><string>OpenCanopy.efi</string></array><key>Input</key><dict><key>KeyFiltering</key><false/><key>KeyForgetThreshold</key><integer>5</integer><key>KeySupport</key><true/><key>KeySupportMode</key><string>Auto</string><key>KeySwap</key><false/><key>PointerSupport</key><false/><key>PointerSupportMode</key><string>ASUS</string><key>TimerResolution</key><integer>50000</integer></dict><key>Output</key><dict><key>ClearScreenOnModeSwitch</key><false/><key>ConsoleMode</key><string></string><key>DirectGopRendering</key><false/><key>ForceResolution</key><false/><key>GopPassThrough</key><string>Disabled</string><key>IgnoreTextInGraphics</key><false/><key>ProvideConsoleGop</key><true/><key>ReconnectGraphicsOnConnect</key><false/><key>ReconnectOnResChange</key><false/><key>ReplaceTabWithSpace</key><false/><key>Resolution</key><string>Max</string><key>SanitiseClearScreen</key><false/><key>TextRenderer</key><string>BuiltinGraphics</string><key>UIScale</key><integer>-1</integer><key>UgaPassThrough</key><false/></dict><key>ProtocolOverrides</key><dict/><key>Quirks</key><dict><key>ActivateHpetSupport</key><false/><key>DisableSecurityPolicy</key><false/><key>EnableVectorAcceleration</key><true/><key>EnableVmx</key><false/><key>ExitBootServicesDelay</key><integer>0</integer><key>ForceOcWriteFlash</key><false/><key>ForgeUefiSupport</key><false/><key>IgnoreInvalidFlexRatio</key><false/><key>ReleaseUsbOwnership</key><false/><key>ReloadOptionRoms</key><false/><key>RequestBootVarRouting</key><true/><key>ResizeGpuBars</key><integer>-1</integer><key>TscSyncTimeout</key><integer>0</integer><key>UnblockFsConnect</key><false/></dict><key>ReservedMemory</key><array/></dict>
|
||||
</dict>
|
||||
</plist>
|
55
constants.py
Normal file
55
constants.py
Normal file
@ -0,0 +1,55 @@
|
||||
# constants.py
|
||||
|
||||
APP_NAME = "Skyscope macOS on PC USB Creator Tool"
|
||||
DEVELOPER_NAME = "Miss Casey Jay Topojani"
|
||||
BUSINESS_NAME = "Skyscope Sentinel Intelligence"
|
||||
|
||||
MACOS_VERSIONS = {
|
||||
"Sonoma": "sonoma",
|
||||
"Ventura": "ventura",
|
||||
"Monterey": "monterey",
|
||||
"Big Sur": "big-sur",
|
||||
"Catalina": "catalina"
|
||||
}
|
||||
|
||||
# Docker image base name
|
||||
DOCKER_IMAGE_BASE = "sickcodes/docker-osx"
|
||||
|
||||
# Default Docker command parameters (some will be overridden)
|
||||
DEFAULT_DOCKER_PARAMS = {
|
||||
"--device": "/dev/kvm",
|
||||
"-p": "50922:10022", # For SSH access to the container
|
||||
"-v": "/tmp/.X11-unix:/tmp/.X11-unix", # For GUI display
|
||||
"-e": "DISPLAY=${DISPLAY:-:0.0}",
|
||||
"-e GENERATE_UNIQUE": "true", # Crucial for unique OpenCore
|
||||
# Sonoma-specific, will need to be conditional or use a base plist
|
||||
# that works for all, or fetch the correct one per version.
|
||||
# For now, let's use a generic one if possible, or the Sonoma one as a placeholder.
|
||||
# The original issue used a Sonoma-specific one.
|
||||
"-e CPU": "'Haswell-noTSX'",
|
||||
"-e CPUID_FLAGS": "'kvm=on,vendor=GenuineIntel,+invtsc,vmware-cpuid-freq=on'",
|
||||
"-e MASTER_PLIST_URL": "'https://raw.githubusercontent.com/sickcodes/osx-serial-generator/master/config-custom-sonoma.plist'"
|
||||
}
|
||||
|
||||
# Parameters that might change per macOS version or user setting
|
||||
VERSION_SPECIFIC_PARAMS = {
|
||||
"Sonoma": {
|
||||
"-e SHORTNAME": "sonoma",
|
||||
"-e MASTER_PLIST_URL": "'https://raw.githubusercontent.com/sickcodes/osx-serial-generator/master/config-custom-sonoma.plist'"
|
||||
},
|
||||
"Ventura": {
|
||||
"-e SHORTNAME": "ventura",
|
||||
"-e MASTER_PLIST_URL": "'https://raw.githubusercontent.com/sickcodes/osx-serial-generator/master/config-custom.plist'" # Needs verification if different for Ventura
|
||||
},
|
||||
"Monterey": {
|
||||
"-e SHORTNAME": "monterey",
|
||||
"-e MASTER_PLIST_URL": "'https://raw.githubusercontent.com/sickcodes/osx-serial-generator/master/config-custom.plist'" # Needs verification
|
||||
},
|
||||
"Big Sur": {
|
||||
"-e SHORTNAME": "big-sur",
|
||||
# Big Sur might not use/need MASTER_PLIST_URL in the same way or has a different default
|
||||
},
|
||||
"Catalina": {
|
||||
# Catalina might not use/need MASTER_PLIST_URL
|
||||
}
|
||||
}
|
176
linux_hardware_info.py
Normal file
176
linux_hardware_info.py
Normal file
@ -0,0 +1,176 @@
|
||||
# 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], 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=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.")
|
||||
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 "", 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 using lspci.
|
||||
"""
|
||||
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 = re.compile(
|
||||
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 stdout.splitlines():
|
||||
match = regex.search(line)
|
||||
if match:
|
||||
class_desc = match.group(1).strip()
|
||||
# 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:
|
||||
device_type = "VGA"
|
||||
elif "Audio device" in class_desc:
|
||||
device_type = "Audio"
|
||||
elif "Ethernet controller" in class_desc:
|
||||
device_type = "Ethernet"
|
||||
elif "Network controller" in class_desc:
|
||||
device_type = "Network (Wi-Fi?)"
|
||||
|
||||
if device_type:
|
||||
cleaned_desc = full_desc
|
||||
# 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() 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.
|
||||
"""
|
||||
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 = 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()
|
||||
value = match.group(2).strip()
|
||||
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("--- 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(" No specific audio codecs detected via /proc/asound.")
|
491
main_app.py
Normal file
491
main_app.py
Normal file
@ -0,0 +1,491 @@
|
||||
# main_app.py
|
||||
import sys
|
||||
import subprocess
|
||||
import os
|
||||
import psutil
|
||||
import platform
|
||||
import ctypes
|
||||
import json
|
||||
import re
|
||||
import traceback # For better error logging
|
||||
import shutil # For shutil.which
|
||||
|
||||
from PyQt6.QtWidgets import (
|
||||
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
|
||||
QLabel, QComboBox, QPushButton, QTextEdit, QMessageBox, QMenuBar,
|
||||
QFileDialog, QGroupBox, QLineEdit, QProgressBar, QCheckBox
|
||||
)
|
||||
from PyQt6.QtGui import QAction, QIcon
|
||||
from PyQt6.QtCore import pyqtSignal, pyqtSlot, QObject, QThread, QTimer, Qt
|
||||
|
||||
from constants import APP_NAME, DEVELOPER_NAME, BUSINESS_NAME, MACOS_VERSIONS
|
||||
# DOCKER_IMAGE_BASE and Docker-related utils are no longer primary for this flow.
|
||||
# utils.py might be refactored or parts removed later.
|
||||
|
||||
# Platform specific USB writers
|
||||
USBWriterLinux = None
|
||||
USBWriterMacOS = None
|
||||
USBWriterWindows = None
|
||||
|
||||
if platform.system() == "Linux":
|
||||
try: from usb_writer_linux import USBWriterLinux
|
||||
except ImportError as e: print(f"Could not import USBWriterLinux: {e}")
|
||||
elif platform.system() == "Darwin":
|
||||
try: from usb_writer_macos import USBWriterMacOS
|
||||
except ImportError as e: print(f"Could not import USBWriterMacOS: {e}")
|
||||
elif platform.system() == "Windows":
|
||||
try: from usb_writer_windows import USBWriterWindows
|
||||
except ImportError as e: print(f"Could not import USBWriterWindows: {e}")
|
||||
|
||||
GIBMACOS_SCRIPT_PATH = os.path.join(os.path.dirname(__file__), "scripts", "gibMacOS", "gibMacOS.py")
|
||||
if not os.path.exists(GIBMACOS_SCRIPT_PATH):
|
||||
GIBMACOS_SCRIPT_PATH = os.path.join(os.path.dirname(__file__), "gibMacOS.py")
|
||||
|
||||
|
||||
class WorkerSignals(QObject):
|
||||
progress = pyqtSignal(str)
|
||||
finished = pyqtSignal(str)
|
||||
error = pyqtSignal(str)
|
||||
progress_value = pyqtSignal(int)
|
||||
|
||||
class GibMacOSWorker(QObject):
|
||||
signals = WorkerSignals()
|
||||
def __init__(self, version_key: str, download_path: str, catalog_key: str = "publicrelease"):
|
||||
super().__init__()
|
||||
self.version_key = version_key
|
||||
self.download_path = download_path
|
||||
self.catalog_key = catalog_key
|
||||
self.process = None
|
||||
self._is_running = True
|
||||
|
||||
@pyqtSlot()
|
||||
def run(self):
|
||||
try:
|
||||
script_to_run = ""
|
||||
if os.path.exists(GIBMACOS_SCRIPT_PATH):
|
||||
script_to_run = GIBMACOS_SCRIPT_PATH
|
||||
elif shutil.which("gibMacOS.py"): # Check if it's in PATH
|
||||
script_to_run = "gibMacOS.py"
|
||||
elif os.path.exists(os.path.join(os.path.dirname(os.path.abspath(sys.argv[0])), "gibMacOS.py")): # Check alongside main_app.py
|
||||
script_to_run = os.path.join(os.path.dirname(os.path.abspath(sys.argv[0])), "gibMacOS.py")
|
||||
else:
|
||||
self.signals.error.emit(f"gibMacOS.py not found at expected locations or in PATH.")
|
||||
return
|
||||
|
||||
version_for_gib = MACOS_VERSIONS.get(self.version_key, self.version_key)
|
||||
os.makedirs(self.download_path, exist_ok=True)
|
||||
|
||||
command = [sys.executable, script_to_run, "-n", "-c", self.catalog_key, "-v", version_for_gib, "-d", self.download_path]
|
||||
self.signals.progress.emit(f"Downloading macOS '{self.version_key}' (as '{version_for_gib}') installer assets...\nCommand: {' '.join(command)}\nOutput will be in: {self.download_path}\n")
|
||||
|
||||
self.process = subprocess.Popen(
|
||||
command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
||||
text=True, bufsize=1, universal_newlines=True,
|
||||
creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == "win32" else 0
|
||||
)
|
||||
|
||||
if self.process.stdout:
|
||||
for line in iter(self.process.stdout.readline, ''):
|
||||
if not self._is_running:
|
||||
self.signals.progress.emit("macOS download process stopping at user request.\n")
|
||||
break
|
||||
line_strip = line.strip()
|
||||
self.signals.progress.emit(line_strip)
|
||||
progress_match = re.search(r"(\d+)%", line_strip)
|
||||
if progress_match:
|
||||
try: self.signals.progress_value.emit(int(progress_match.group(1)))
|
||||
except ValueError: pass
|
||||
self.process.stdout.close()
|
||||
|
||||
return_code = self.process.wait()
|
||||
|
||||
if not self._is_running and return_code != 0:
|
||||
self.signals.finished.emit(f"macOS download cancelled or stopped early (exit code {return_code}).")
|
||||
return
|
||||
|
||||
if return_code == 0:
|
||||
self.signals.finished.emit(f"macOS '{self.version_key}' installer assets downloaded to '{self.download_path}'.")
|
||||
else:
|
||||
self.signals.error.emit(f"Failed to download macOS '{self.version_key}' (gibMacOS exit code {return_code}). Check logs.")
|
||||
except FileNotFoundError:
|
||||
self.signals.error.emit(f"Error: Python or gibMacOS.py script not found. Ensure Python is in PATH and gibMacOS script is correctly located.")
|
||||
except Exception as e:
|
||||
self.signals.error.emit(f"An error occurred during macOS download: {str(e)}\n{traceback.format_exc()}")
|
||||
finally:
|
||||
self._is_running = False
|
||||
|
||||
def stop(self):
|
||||
self._is_running = False
|
||||
if self.process and self.process.poll() is None:
|
||||
self.signals.progress.emit("Attempting to stop macOS download (may not be effective for active downloads)...\n")
|
||||
try:
|
||||
self.process.terminate(); self.process.wait(timeout=2)
|
||||
except subprocess.TimeoutExpired: self.process.kill()
|
||||
self.signals.progress.emit("macOS download process termination requested.\n")
|
||||
|
||||
|
||||
class USBWriterWorker(QObject):
|
||||
signals = WorkerSignals()
|
||||
def __init__(self, device: str, macos_download_path: str,
|
||||
enhance_plist: bool, target_macos_version: str):
|
||||
super().__init__()
|
||||
self.device = device
|
||||
self.macos_download_path = macos_download_path
|
||||
self.enhance_plist = enhance_plist
|
||||
self.target_macos_version = target_macos_version
|
||||
self.writer_instance = None
|
||||
|
||||
@pyqtSlot()
|
||||
def run(self):
|
||||
current_os = platform.system()
|
||||
try:
|
||||
writer_cls = None
|
||||
if current_os == "Linux": writer_cls = USBWriterLinux
|
||||
elif current_os == "Darwin": writer_cls = USBWriterMacOS
|
||||
elif current_os == "Windows": writer_cls = USBWriterWindows
|
||||
|
||||
if writer_cls is None:
|
||||
self.signals.error.emit(f"{current_os} USB writer module not available or OS not supported."); return
|
||||
|
||||
# Platform writers' __init__ will need to be updated for macos_download_path
|
||||
# This assumes usb_writer_*.py __init__ signatures are now:
|
||||
# __init__(self, device, macos_download_path, progress_callback, enhance_plist_enabled, target_macos_version)
|
||||
self.writer_instance = writer_cls(
|
||||
device=self.device,
|
||||
macos_download_path=self.macos_download_path,
|
||||
progress_callback=lambda msg: self.signals.progress.emit(msg),
|
||||
enhance_plist_enabled=self.enhance_plist,
|
||||
target_macos_version=self.target_macos_version
|
||||
)
|
||||
|
||||
if self.writer_instance.format_and_write():
|
||||
self.signals.finished.emit("USB writing process completed successfully.")
|
||||
else:
|
||||
self.signals.error.emit("USB writing process failed. Check output for details.")
|
||||
except Exception as e:
|
||||
self.signals.error.emit(f"USB writing preparation error: {str(e)}\n{traceback.format_exc()}")
|
||||
|
||||
|
||||
class MainWindow(QMainWindow):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.setWindowTitle(APP_NAME)
|
||||
self.setGeometry(100, 100, 800, 700) # Adjusted height
|
||||
|
||||
self.active_worker_thread = None
|
||||
self.macos_download_path = None
|
||||
self.current_worker_instance = None
|
||||
|
||||
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."
|
||||
|
||||
self._setup_ui()
|
||||
self.status_bar = self.statusBar()
|
||||
# self.status_bar.addPermanentWidget(self.progress_bar) # Progress bar now in main layout
|
||||
self.status_bar.showMessage(self.base_status_message, 5000)
|
||||
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)
|
||||
central_widget = QWidget(); self.setCentralWidget(central_widget); main_layout = QVBoxLayout(central_widget)
|
||||
|
||||
# Step 1: Download macOS
|
||||
download_group = QGroupBox("Step 1: Download macOS Installer Assets")
|
||||
download_layout = QVBoxLayout()
|
||||
selection_layout = QHBoxLayout(); self.version_label = QLabel("Select macOS Version:"); self.version_combo = QComboBox()
|
||||
self.version_combo.addItems(MACOS_VERSIONS.keys()); selection_layout.addWidget(self.version_label); selection_layout.addWidget(self.version_combo)
|
||||
download_layout.addLayout(selection_layout)
|
||||
|
||||
self.download_macos_button = QPushButton("Download macOS Installer Assets")
|
||||
self.download_macos_button.clicked.connect(self.start_macos_download_flow)
|
||||
download_layout.addWidget(self.download_macos_button)
|
||||
|
||||
self.cancel_operation_button = QPushButton("Cancel Current Operation")
|
||||
self.cancel_operation_button.clicked.connect(self.stop_current_operation)
|
||||
self.cancel_operation_button.setEnabled(False)
|
||||
download_layout.addWidget(self.cancel_operation_button)
|
||||
download_group.setLayout(download_layout)
|
||||
main_layout.addWidget(download_group)
|
||||
|
||||
# Step 2: USB Drive Selection & Writing
|
||||
usb_group = QGroupBox("Step 2: Create Bootable USB Installer")
|
||||
self.usb_layout = QVBoxLayout()
|
||||
self.usb_drive_label = QLabel("Available USB Drives:"); self.usb_layout.addWidget(self.usb_drive_label)
|
||||
usb_selection_layout = QHBoxLayout(); self.usb_drive_combo = QComboBox(); self.usb_drive_combo.currentIndexChanged.connect(self.update_all_button_states)
|
||||
usb_selection_layout.addWidget(self.usb_drive_combo); self.refresh_usb_button = QPushButton("Refresh List"); self.refresh_usb_button.clicked.connect(self.refresh_usb_drives)
|
||||
usb_selection_layout.addWidget(self.refresh_usb_button); self.usb_layout.addLayout(usb_selection_layout)
|
||||
self.windows_usb_guidance_label = QLabel("For Windows: Select USB disk from dropdown (WMI). Manual input below if empty/unreliable.")
|
||||
self.windows_disk_id_input = QLineEdit(); self.windows_disk_id_input.setPlaceholderText("Disk No. (e.g., 1)"); self.windows_disk_id_input.textChanged.connect(self.update_all_button_states)
|
||||
if platform.system() == "Windows": self.usb_layout.addWidget(self.windows_usb_guidance_label); self.usb_layout.addWidget(self.windows_disk_id_input); self.windows_usb_guidance_label.setVisible(True); self.windows_disk_id_input.setVisible(True)
|
||||
else: self.windows_usb_guidance_label.setVisible(False); self.windows_disk_id_input.setVisible(False)
|
||||
self.enhance_plist_checkbox = QCheckBox("Try to auto-enhance config.plist for this system's hardware (Experimental, Linux Host Only for detection)")
|
||||
self.enhance_plist_checkbox.setChecked(False); self.usb_layout.addWidget(self.enhance_plist_checkbox)
|
||||
warning_label = QLabel("WARNING: USB drive will be ERASED!"); warning_label.setStyleSheet("color: red; font-weight: bold;"); self.usb_layout.addWidget(warning_label)
|
||||
self.write_to_usb_button = QPushButton("Create macOS Installer USB"); self.write_to_usb_button.clicked.connect(self.handle_write_to_usb)
|
||||
self.write_to_usb_button.setEnabled(False); self.usb_layout.addWidget(self.write_to_usb_button); usb_group.setLayout(self.usb_layout); main_layout.addWidget(usb_group)
|
||||
|
||||
self.progress_bar = QProgressBar(self); self.progress_bar.setRange(0, 0); self.progress_bar.setVisible(False); main_layout.addWidget(self.progress_bar)
|
||||
self.output_area = QTextEdit(); self.output_area.setReadOnly(True); main_layout.addWidget(self.output_area)
|
||||
self.update_all_button_states()
|
||||
|
||||
def show_about_dialog(self): QMessageBox.about(self, f"About {APP_NAME}", f"Version: 1.0.0 (Installer Flow)\nDeveloper: {DEVELOPER_NAME}\nBusiness: {BUSINESS_NAME}\n\nThis tool helps create bootable macOS USB drives using gibMacOS and OpenCore.")
|
||||
|
||||
def _set_ui_busy(self, busy_status: bool, message: str = "Processing..."):
|
||||
self.progress_bar.setVisible(busy_status)
|
||||
if busy_status:
|
||||
self.base_status_message = message
|
||||
if not self.spinner_timer.isActive(): self.spinner_timer.start(150)
|
||||
self._update_spinner_status()
|
||||
self.progress_bar.setRange(0,0)
|
||||
else:
|
||||
self.spinner_timer.stop()
|
||||
self.status_bar.showMessage(message or "Ready.", 7000)
|
||||
self.update_all_button_states()
|
||||
|
||||
|
||||
def _update_spinner_status(self):
|
||||
if self.spinner_timer.isActive():
|
||||
char = self.spinner_chars[self.spinner_index % len(self.spinner_chars)]
|
||||
active_worker_provides_progress = False
|
||||
if self.active_worker_thread and self.active_worker_thread.isRunning():
|
||||
active_worker_provides_progress = getattr(self.active_worker_thread, "provides_progress", False)
|
||||
|
||||
if active_worker_provides_progress and self.progress_bar.maximum() == 100: # Determinate
|
||||
self.status_bar.showMessage(f"{char} {self.base_status_message} ({self.progress_bar.value()}%)")
|
||||
else:
|
||||
if self.progress_bar.maximum() != 0: self.progress_bar.setRange(0,0)
|
||||
self.status_bar.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()):
|
||||
self.spinner_timer.stop()
|
||||
|
||||
def update_all_button_states(self):
|
||||
is_worker_active = self.active_worker_thread is not None and self.active_worker_thread.isRunning()
|
||||
|
||||
self.download_macos_button.setEnabled(not is_worker_active)
|
||||
self.version_combo.setEnabled(not is_worker_active)
|
||||
self.cancel_operation_button.setEnabled(is_worker_active and self.current_worker_instance is not None)
|
||||
|
||||
self.refresh_usb_button.setEnabled(not is_worker_active)
|
||||
self.usb_drive_combo.setEnabled(not is_worker_active)
|
||||
if platform.system() == "Windows": self.windows_disk_id_input.setEnabled(not is_worker_active)
|
||||
self.enhance_plist_checkbox.setEnabled(not is_worker_active)
|
||||
|
||||
# Write to USB button logic
|
||||
macos_assets_ready = bool(self.macos_download_path and os.path.isdir(self.macos_download_path))
|
||||
usb_identified = False
|
||||
current_os = platform.system(); writer_module = None
|
||||
if current_os == "Linux": writer_module = USBWriterLinux; usb_identified = bool(self.usb_drive_combo.currentData())
|
||||
elif current_os == "Darwin": writer_module = USBWriterMacOS; usb_identified = bool(self.usb_drive_combo.currentData())
|
||||
elif current_os == "Windows":
|
||||
writer_module = USBWriterWindows
|
||||
usb_identified = bool(self.usb_drive_combo.currentData()) or bool(self.windows_disk_id_input.text().strip())
|
||||
|
||||
self.write_to_usb_button.setEnabled(not is_worker_active and macos_assets_ready and usb_identified and writer_module is not None)
|
||||
tooltip = ""
|
||||
if writer_module is None: tooltip = f"USB Writing not supported on {current_os} or module missing."
|
||||
elif not macos_assets_ready: tooltip = "Download macOS installer assets first (Step 1)."
|
||||
elif not usb_identified: tooltip = "Select or identify a target USB drive."
|
||||
else: tooltip = ""
|
||||
self.write_to_usb_button.setToolTip(tooltip)
|
||||
|
||||
|
||||
def _start_worker(self, worker_instance, on_finished_slot, on_error_slot, worker_name="worker", provides_progress=False):
|
||||
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, f"Starting {worker_name.replace('_', ' ')}...")
|
||||
self.current_worker_instance = worker_instance
|
||||
|
||||
if provides_progress:
|
||||
self.progress_bar.setRange(0,100)
|
||||
worker_instance.signals.progress_value.connect(self.update_progress_bar_value)
|
||||
else:
|
||||
self.progress_bar.setRange(0,0)
|
||||
|
||||
self.active_worker_thread = QThread(); self.active_worker_thread.setObjectName(worker_name + "_thread")
|
||||
setattr(self.active_worker_thread, "provides_progress", provides_progress)
|
||||
|
||||
worker_instance.moveToThread(self.active_worker_thread)
|
||||
worker_instance.signals.progress.connect(self.update_output)
|
||||
worker_instance.signals.finished.connect(lambda msg, wn=worker_name, slot=on_finished_slot: self._handle_worker_finished(msg, wn, slot))
|
||||
worker_instance.signals.error.connect(lambda err, wn=worker_name, slot=on_error_slot: self._handle_worker_error(err, wn, slot))
|
||||
self.active_worker_thread.finished.connect(self.active_worker_thread.deleteLater)
|
||||
self.active_worker_thread.started.connect(worker_instance.run)
|
||||
self.active_worker_thread.start()
|
||||
return True
|
||||
|
||||
@pyqtSlot(int)
|
||||
def update_progress_bar_value(self, value):
|
||||
if self.progress_bar.maximum() == 0: self.progress_bar.setRange(0,100)
|
||||
self.progress_bar.setValue(value)
|
||||
# Spinner update will happen on its timer, it can check progress_bar.value()
|
||||
|
||||
def _handle_worker_finished(self, message, worker_name, specific_finished_slot):
|
||||
final_msg = f"{worker_name.replace('_', ' ').capitalize()} completed."
|
||||
self.current_worker_instance = None # Clear current worker
|
||||
self.active_worker_thread = None
|
||||
if specific_finished_slot: specific_finished_slot(message)
|
||||
self._set_ui_busy(False, final_msg)
|
||||
|
||||
def _handle_worker_error(self, error_message, worker_name, specific_error_slot):
|
||||
final_msg = f"{worker_name.replace('_', ' ').capitalize()} failed."
|
||||
self.current_worker_instance = None # Clear current worker
|
||||
self.active_worker_thread = None
|
||||
if specific_error_slot: specific_error_slot(error_message)
|
||||
self._set_ui_busy(False, final_msg)
|
||||
|
||||
def start_macos_download_flow(self):
|
||||
self.output_area.clear(); selected_version_name = self.version_combo.currentText()
|
||||
gibmacos_version_arg = MACOS_VERSIONS.get(selected_version_name, selected_version_name)
|
||||
|
||||
chosen_path = QFileDialog.getExistingDirectory(self, "Select Directory to Download macOS Installer Assets")
|
||||
if not chosen_path: self.output_area.append("Download directory selection cancelled."); return
|
||||
self.macos_download_path = chosen_path
|
||||
|
||||
worker = GibMacOSWorker(gibmacos_version_arg, self.macos_download_path)
|
||||
if not self._start_worker(worker, self.macos_download_finished, self.macos_download_error,
|
||||
"macos_download",
|
||||
f"Downloading macOS {selected_version_name} assets...",
|
||||
provides_progress=True): # Assuming GibMacOSWorker will emit progress_value
|
||||
self._set_ui_busy(False, "Failed to start macOS download operation.")
|
||||
|
||||
|
||||
@pyqtSlot(str)
|
||||
def macos_download_finished(self, message):
|
||||
QMessageBox.information(self, "Download Complete", message)
|
||||
# self.macos_download_path is set. UI update handled by generic handler.
|
||||
|
||||
@pyqtSlot(str)
|
||||
def macos_download_error(self, error_message):
|
||||
QMessageBox.critical(self, "Download Error", error_message)
|
||||
self.macos_download_path = None
|
||||
# UI reset by generic handler.
|
||||
|
||||
def stop_current_operation(self):
|
||||
if self.current_worker_instance and hasattr(self.current_worker_instance, 'stop'):
|
||||
self.output_area.append(f"
|
||||
--- Attempting to stop {self.active_worker_thread.objectName().replace('_thread','')} ---")
|
||||
self.current_worker_instance.stop()
|
||||
else:
|
||||
self.output_area.append("
|
||||
--- No active stoppable operation or stop method not implemented for current worker. ---")
|
||||
|
||||
def handle_error(self, message):
|
||||
self.output_area.append(f"ERROR: {message}"); QMessageBox.critical(self, "Error", message)
|
||||
self._set_ui_busy(False, "Error occurred.")
|
||||
|
||||
def check_admin_privileges(self) -> bool: # ... (same)
|
||||
try:
|
||||
if platform.system() == "Windows": return ctypes.windll.shell32.IsUserAnAdmin() != 0
|
||||
else: return os.geteuid() == 0
|
||||
except Exception as e: self.output_area.append(f"Could not check admin privileges: {e}"); return False
|
||||
|
||||
def refresh_usb_drives(self): # ... (same logic as before)
|
||||
self.usb_drive_combo.clear(); current_selection_text = getattr(self, '_current_usb_selection_text', None)
|
||||
self.output_area.append("
|
||||
Scanning for disk devices...")
|
||||
if platform.system() == "Windows":
|
||||
self.usb_drive_label.setText("Available USB Disks (Windows - via WMI/PowerShell):")
|
||||
self.windows_usb_guidance_label.setVisible(True); self.windows_disk_id_input.setVisible(False);
|
||||
powershell_command = "Get-WmiObject Win32_DiskDrive | Where-Object {$_.InterfaceType -eq 'USB'} | Select-Object DeviceID, Index, Model, @{Name='SizeGB';Expression={[math]::Round($_.Size / 1GB, 2)}} | ConvertTo-Json"
|
||||
try:
|
||||
process = subprocess.run(["powershell", "-Command", powershell_command], capture_output=True, text=True, check=True, creationflags=subprocess.CREATE_NO_WINDOW)
|
||||
disks_data = json.loads(process.stdout); disks_json = disks_data if isinstance(disks_data, list) else [disks_data] if disks_data else []
|
||||
if disks_json:
|
||||
for disk in disks_json:
|
||||
if disk.get('DeviceID') is None or disk.get('Index') is None: continue
|
||||
disk_text = f"Disk {disk['Index']}: {disk.get('Model','N/A')} ({disk.get('SizeGB','N/A')} GB) - {disk['DeviceID']}"
|
||||
self.usb_drive_combo.addItem(disk_text, userData=str(disk['Index']))
|
||||
self.output_area.append(f"Found {len(disks_json)} USB disk(s) via WMI.");
|
||||
if current_selection_text:
|
||||
for i in range(self.usb_drive_combo.count()):
|
||||
if self.usb_drive_combo.itemText(i) == current_selection_text: self.usb_drive_combo.setCurrentIndex(i); break
|
||||
else: self.output_area.append("No USB disks found via WMI/PowerShell. Manual input field shown as fallback."); self.windows_disk_id_input.setVisible(True)
|
||||
except Exception as e: self.output_area.append(f"Error scanning Windows USBs with PowerShell: {e}"); self.windows_disk_id_input.setVisible(True)
|
||||
else:
|
||||
self.usb_drive_label.setText("Available USB Drives (for Linux/macOS):")
|
||||
self.windows_usb_guidance_label.setVisible(False); self.windows_disk_id_input.setVisible(False)
|
||||
try:
|
||||
partitions = psutil.disk_partitions(all=False); potential_usbs = []
|
||||
for p in partitions:
|
||||
is_removable = 'removable' in p.opts; is_likely_usb = False
|
||||
if platform.system() == "Darwin" and p.device.startswith("/dev/disk") and 'external' in p.opts.lower() and 'physical' in p.opts.lower(): is_likely_usb = True
|
||||
elif platform.system() == "Linux" and ((p.mountpoint and ("/media/" in p.mountpoint or "/run/media/" in p.mountpoint)) or (p.device.startswith("/dev/sd") and not p.device.endswith("da"))): is_likely_usb = True
|
||||
if is_removable or is_likely_usb:
|
||||
try: usage = psutil.disk_usage(p.mountpoint); size_gb = usage.total / (1024**3)
|
||||
except Exception: continue
|
||||
if size_gb < 0.1 : continue
|
||||
drive_text = f"{p.device} @ {p.mountpoint} ({p.fstype}, {size_gb:.2f} GB)"
|
||||
potential_usbs.append((drive_text, p.device))
|
||||
if potential_usbs:
|
||||
idx_to_select = -1
|
||||
for i, (text, device_path) in enumerate(potential_usbs): self.usb_drive_combo.addItem(text, userData=device_path);
|
||||
if text == current_selection_text: idx_to_select = i
|
||||
if idx_to_select != -1: self.usb_drive_combo.setCurrentIndex(idx_to_select)
|
||||
self.output_area.append(f"Found {len(potential_usbs)} potential USB drive(s). Please verify carefully.")
|
||||
else: self.output_area.append("No suitable USB drives found for Linux/macOS.")
|
||||
except ImportError: self.output_area.append("psutil library not found.")
|
||||
except Exception as e: self.output_area.append(f"Error scanning for USB drives: {e}")
|
||||
self.update_all_button_states()
|
||||
|
||||
|
||||
def handle_write_to_usb(self):
|
||||
if not self.check_admin_privileges(): QMessageBox.warning(self, "Privileges Required", "This operation requires Administrator/root privileges."); return
|
||||
if not self.macos_download_path or not os.path.isdir(self.macos_download_path): QMessageBox.warning(self, "Missing macOS Assets", "Download macOS installer assets first."); return
|
||||
current_os = platform.system(); usb_writer_module = None; target_device_id_for_worker = None
|
||||
if current_os == "Windows": target_device_id_for_worker = self.usb_drive_combo.currentData() or self.windows_disk_id_input.text().strip(); usb_writer_module = USBWriterWindows
|
||||
else: target_device_id_for_worker = self.usb_drive_combo.currentData(); usb_writer_module = USBWriterLinux if current_os == "Linux" else USBWriterMacOS if current_os == "Darwin" else None
|
||||
if not usb_writer_module: QMessageBox.warning(self, "Unsupported Platform", f"USB writing not supported for {current_os}."); return
|
||||
if not target_device_id_for_worker: QMessageBox.warning(self, "No USB Selected/Identified", f"Please select/identify target USB."); return
|
||||
if current_os == "Windows" and target_device_id_for_worker.isdigit(): target_device_id_for_worker = f"disk {target_device_id_for_worker}"
|
||||
|
||||
enhance_plist_state = self.enhance_plist_checkbox.isChecked()
|
||||
target_macos_name = self.version_combo.currentText()
|
||||
reply = QMessageBox.warning(self, "Confirm Write Operation", f"WARNING: ALL DATA ON TARGET '{target_device_id_for_worker}' WILL BE ERASED.
|
||||
Proceed?", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.Cancel, QMessageBox.StandardButton.Cancel)
|
||||
if reply == QMessageBox.StandardButton.Cancel: self.output_area.append("
|
||||
USB write cancelled."); return
|
||||
|
||||
# USBWriterWorker now needs different args
|
||||
# The platform specific writers (USBWriterLinux etc) will need to be updated to accept macos_download_path
|
||||
# and use it to find BaseSystem.dmg, EFI/OC etc. instead of opencore_qcow2_path, macos_qcow2_path
|
||||
usb_worker_adapted = USBWriterWorker(
|
||||
device=target_device_id_for_worker,
|
||||
macos_download_path=self.macos_download_path,
|
||||
enhance_plist=enhance_plist_state,
|
||||
target_macos_version=target_macos_name
|
||||
)
|
||||
|
||||
if not self._start_worker(usb_worker_adapted, self.usb_write_finished, self.usb_write_error, "usb_write_worker",
|
||||
busy_message=f"Creating USB for {target_device_id_for_worker}...",
|
||||
provides_progress=False): # USB writing can be long, but progress parsing is per-platform script.
|
||||
self._set_ui_busy(False, "Failed to start USB write operation.")
|
||||
|
||||
@pyqtSlot(str)
|
||||
def usb_write_finished(self, message): QMessageBox.information(self, "USB Write Complete", message)
|
||||
@pyqtSlot(str)
|
||||
def usb_write_error(self, error_message): QMessageBox.critical(self, "USB Write Error", error_message)
|
||||
|
||||
def closeEvent(self, event): # ... (same logic)
|
||||
self._current_usb_selection_text = self.usb_drive_combo.currentText()
|
||||
if self.active_worker_thread and self.active_worker_thread.isRunning():
|
||||
reply = QMessageBox.question(self, 'Confirm Exit', "An operation is running. Exit anyway?", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No)
|
||||
if reply == QMessageBox.StandardButton.Yes:
|
||||
if self.current_worker_instance and hasattr(self.current_worker_instance, 'stop'): self.current_worker_instance.stop()
|
||||
else: self.active_worker_thread.quit()
|
||||
self.active_worker_thread.wait(1000); event.accept()
|
||||
else: event.ignore(); return
|
||||
else: event.accept()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import traceback # Ensure traceback is available for GibMacOSWorker
|
||||
import shutil # Ensure shutil is available for GibMacOSWorker path check
|
||||
app = QApplication(sys.argv)
|
||||
window = MainWindow()
|
||||
window.show()
|
||||
sys.exit(app.exec())
|
295
plist_modifier.py
Normal file
295
plist_modifier.py
Normal file
@ -0,0 +1,295 @@
|
||||
# plist_modifier.py
|
||||
import plistlib
|
||||
import platform
|
||||
import shutil
|
||||
import os
|
||||
import re # For parsing codec names
|
||||
|
||||
if platform.system() == "Linux":
|
||||
try:
|
||||
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: []
|
||||
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)
|
||||
|
||||
INTEL_IGPU_DEFAULTS = {
|
||||
# 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) - 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) - 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 = {
|
||||
# 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)"
|
||||
|
||||
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 enhance_config_plist(plist_path: str, target_macos_version_name: str, progress_callback=None) -> bool:
|
||||
def _report(msg):
|
||||
if progress_callback: progress_callback(f"[PlistModifier] {msg}")
|
||||
else: print(f"[PlistModifier] {msg}")
|
||||
# ... (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: {backup_plist_path}")
|
||||
except Exception as e: _report(f"Error creating backup for {plist_path}: {e}. Proceeding cautiously.")
|
||||
|
||||
config_data = {};
|
||||
try:
|
||||
with open(plist_path, 'rb') as f: config_data = plistlib.load(f)
|
||||
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.")
|
||||
|
||||
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_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.")
|
||||
|
||||
if lookup_key in INTEL_IGPU_DEFAULTS:
|
||||
_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():
|
||||
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 - 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"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 (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_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 (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: {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_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.")
|
||||
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 or older target).")
|
||||
else: # Mojave and newer
|
||||
boot_args.discard('nvda_drv=1')
|
||||
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 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_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_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
|
||||
|
||||
try:
|
||||
with open(plist_path, 'wb') as f:
|
||||
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: # ... (restore backup logic same as before)
|
||||
_report(f"Error saving modified plist file {plist_path}: {e}")
|
||||
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, ensure dummy data for kexts is complete)
|
||||
if __name__ == '__main__':
|
||||
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": [
|
||||
{"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 debug=0x100"}}}
|
||||
}
|
||||
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; 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': '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": "12th Gen Intel(R) Core(TM) i7-12700K", "Flags": "avx avx2"}
|
||||
get_audio_codecs = lambda: ["Realtek ALC1220", "Intel Alder Lake-S HDMI"]
|
||||
|
||||
|
||||
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")
|
||||
print(f"Cleaned up dummy plist and backup.")
|
302
usb_writer_linux.py
Normal file
302
usb_writer_linux.py
Normal file
@ -0,0 +1,302 @@
|
||||
# usb_writer_linux.py (Significant Refactoring for Installer Creation)
|
||||
import subprocess
|
||||
import os
|
||||
import time
|
||||
import shutil
|
||||
import glob
|
||||
import re
|
||||
import plistlib # For plist_modifier call, and potentially for InstallInfo.plist
|
||||
|
||||
try:
|
||||
from plist_modifier import enhance_config_plist
|
||||
except ImportError:
|
||||
enhance_config_plist = None
|
||||
print("Warning: plist_modifier.py not found. Plist enhancement feature will be disabled for USBWriterLinux.")
|
||||
|
||||
# Assume a basic OpenCore EFI template directory exists relative to this script
|
||||
OC_TEMPLATE_DIR = os.path.join(os.path.dirname(__file__), "EFI_template_installer")
|
||||
|
||||
|
||||
class USBWriterLinux:
|
||||
def __init__(self, device: str, macos_download_path: str,
|
||||
progress_callback=None, enhance_plist_enabled: bool = False,
|
||||
target_macos_version: str = ""):
|
||||
self.device = device
|
||||
self.macos_download_path = macos_download_path
|
||||
self.progress_callback = progress_callback
|
||||
self.enhance_plist_enabled = enhance_plist_enabled
|
||||
self.target_macos_version = target_macos_version # String name like "Sonoma"
|
||||
|
||||
pid = os.getpid()
|
||||
self.temp_basesystem_hfs_path = f"temp_basesystem_{pid}.hfs"
|
||||
self.temp_efi_build_dir = f"temp_efi_build_{pid}"
|
||||
self.temp_shared_support_extract_dir = f"temp_shared_support_extract_{pid}"
|
||||
|
||||
|
||||
self.mount_point_usb_esp = f"/mnt/usb_esp_temp_skyscope_{pid}"
|
||||
self.mount_point_usb_macos_target = f"/mnt/usb_macos_target_temp_skyscope_{pid}"
|
||||
|
||||
self.temp_files_to_clean = [self.temp_basesystem_hfs_path]
|
||||
self.temp_dirs_to_clean = [
|
||||
self.temp_efi_build_dir, self.mount_point_usb_esp,
|
||||
self.mount_point_usb_macos_target, self.temp_shared_support_extract_dir
|
||||
]
|
||||
|
||||
def _report_progress(self, message: str):
|
||||
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)}")
|
||||
try:
|
||||
process = subprocess.run(
|
||||
command, check=check, capture_output=capture_output, text=True, timeout=timeout,
|
||||
shell=shell, cwd=working_dir,
|
||||
creationflags=0 # No CREATE_NO_WINDOW on Linux
|
||||
)
|
||||
if capture_output: # Log only if content exists
|
||||
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
|
||||
|
||||
def _cleanup_temp_files_and_dirs(self):
|
||||
self._report_progress("Cleaning up temporary files and directories...")
|
||||
for mp in self.temp_dirs_to_clean: # Unmount first
|
||||
if os.path.ismount(mp):
|
||||
self._run_command(["sudo", "umount", "-lf", mp], check=False, timeout=15)
|
||||
|
||||
for f_path in self.temp_files_to_clean:
|
||||
if os.path.exists(f_path):
|
||||
try: self._run_command(["sudo", "rm", "-f", f_path], check=False)
|
||||
except Exception as e: self._report_progress(f"Error removing temp file {f_path}: {e}")
|
||||
for d_path in self.temp_dirs_to_clean:
|
||||
if os.path.exists(d_path):
|
||||
try: self._run_command(["sudo", "rm", "-rf", d_path], check=False)
|
||||
except Exception as e: self._report_progress(f"Error removing temp dir {d_path}: {e}")
|
||||
|
||||
def check_dependencies(self):
|
||||
self._report_progress("Checking dependencies (sgdisk, mkfs.vfat, mkfs.hfsplus, 7z, rsync, dd)...")
|
||||
dependencies = ["sgdisk", "mkfs.vfat", "mkfs.hfsplus", "7z", "rsync", "dd"]
|
||||
missing_deps = [dep for dep in dependencies if not shutil.which(dep)]
|
||||
if missing_deps:
|
||||
msg = f"Missing dependencies: {', '.join(missing_deps)}. Please install them (e.g., hfsprogs, p7zip-full)."
|
||||
self._report_progress(msg); raise RuntimeError(msg)
|
||||
self._report_progress("All critical dependencies for Linux USB installer creation found.")
|
||||
return True
|
||||
|
||||
def _find_source_file(self, patterns: list[str], description: str) -> str | None:
|
||||
"""Finds the first existing file matching a list of glob patterns within self.macos_download_path."""
|
||||
self._report_progress(f"Searching for {description} in {self.macos_download_path}...")
|
||||
for pattern in patterns:
|
||||
# Using iglob for efficiency if many files, but glob is fine for fewer expected matches
|
||||
found_files = glob.glob(os.path.join(self.macos_download_path, "**", pattern), recursive=True)
|
||||
if found_files:
|
||||
# Prefer files not inside .app bundles if multiple are found, unless it's the app itself.
|
||||
# This is a simple heuristic.
|
||||
non_app_files = [f for f in found_files if ".app/" not in f]
|
||||
target_file = non_app_files[0] if non_app_files else found_files[0]
|
||||
self._report_progress(f"Found {description} at: {target_file}")
|
||||
return target_file
|
||||
self._report_progress(f"Warning: {description} not found with patterns: {patterns}")
|
||||
return None
|
||||
|
||||
def _extract_hfs_from_dmg(self, dmg_path: str, output_hfs_path: str) -> bool:
|
||||
"""Extracts the primary HFS+ partition image (e.g., '4.hfs') from a DMG."""
|
||||
# Assumes BaseSystem.dmg or similar that contains a HFS+ partition image.
|
||||
temp_extract_dir = f"temp_hfs_extract_{os.getpid()}"
|
||||
os.makedirs(temp_extract_dir, exist_ok=True)
|
||||
try:
|
||||
self._report_progress(f"Extracting HFS+ partition image from {dmg_path}...")
|
||||
# 7z e -tdmg <dmg_path> *.hfs -o<output_dir_for_hfs> (usually 4.hfs or similar)
|
||||
self._run_command(["7z", "e", "-tdmg", dmg_path, "*.hfs", f"-o{temp_extract_dir}"], check=True)
|
||||
|
||||
hfs_files = glob.glob(os.path.join(temp_extract_dir, "*.hfs"))
|
||||
if not hfs_files: raise RuntimeError(f"No .hfs file found after extracting {dmg_path}")
|
||||
|
||||
final_hfs_file = max(hfs_files, key=os.path.getsize) # Assume largest is the one
|
||||
self._report_progress(f"Found HFS+ partition image: {final_hfs_file}. Moving to {output_hfs_path}")
|
||||
shutil.move(final_hfs_file, output_hfs_path)
|
||||
return True
|
||||
except Exception as e:
|
||||
self._report_progress(f"Error during HFS extraction from DMG: {e}")
|
||||
return False
|
||||
finally:
|
||||
if os.path.exists(temp_extract_dir): shutil.rmtree(temp_extract_dir, ignore_errors=True)
|
||||
|
||||
def format_and_write(self) -> bool:
|
||||
try:
|
||||
self.check_dependencies()
|
||||
self._cleanup_temp_files_and_dirs()
|
||||
for mp in [self.mount_point_usb_esp, self.mount_point_usb_macos_target, self.temp_efi_build_dir]:
|
||||
self._run_command(["sudo", "mkdir", "-p", mp])
|
||||
|
||||
self._report_progress(f"WARNING: ALL DATA ON {self.device} WILL BE ERASED!")
|
||||
for i in range(1, 10): self._run_command(["sudo", "umount", "-lf", f"{self.device}{i}"], check=False, timeout=5); self._run_command(["sudo", "umount", "-lf", f"{self.device}p{i}"], check=False, timeout=5)
|
||||
|
||||
self._report_progress(f"Partitioning {self.device} with GPT (sgdisk)...")
|
||||
self._run_command(["sudo", "sgdisk", "--zap-all", self.device])
|
||||
self._run_command(["sudo", "sgdisk", "-n", "1:0:+550M", "-t", "1:ef00", "-c", "1:EFI", self.device])
|
||||
self._run_command(["sudo", "sgdisk", "-n", "2:0:0", "-t", "2:af00", "-c", "2:Install macOS", self.device])
|
||||
self._run_command(["sudo", "partprobe", self.device], timeout=10); time.sleep(3)
|
||||
|
||||
esp_partition_dev = next((f"{self.device}{i}" for i in ["1", "p1"] if os.path.exists(f"{self.device}{i}")), None)
|
||||
macos_partition_dev = next((f"{self.device}{i}" for i in ["2", "p2"] if os.path.exists(f"{self.device}{i}")), None)
|
||||
if not (esp_partition_dev and macos_partition_dev): raise RuntimeError(f"Could not reliably determine partition names for {self.device}.")
|
||||
|
||||
self._report_progress(f"Formatting ESP ({esp_partition_dev}) as FAT32...")
|
||||
self._run_command(["sudo", "mkfs.vfat", "-F", "32", "-n", "EFI", esp_partition_dev])
|
||||
self._report_progress(f"Formatting macOS Install partition ({macos_partition_dev}) as HFS+...")
|
||||
self._run_command(["sudo", "mkfs.hfsplus", "-v", f"Install macOS {self.target_macos_version}", macos_partition_dev])
|
||||
|
||||
# --- Prepare macOS Installer Content ---
|
||||
basesystem_dmg_path = self._find_source_file(["BaseSystem.dmg", "InstallAssistant.pkg", "SharedSupport.dmg"], "BaseSystem.dmg or InstallAssistant.pkg or SharedSupport.dmg")
|
||||
if not basesystem_dmg_path: raise RuntimeError("Essential macOS installer DMG/PKG not found in download path.")
|
||||
|
||||
if basesystem_dmg_path.endswith(".pkg") or "SharedSupport.dmg" in os.path.basename(basesystem_dmg_path) :
|
||||
# If we found InstallAssistant.pkg or SharedSupport.dmg, we need to extract BaseSystem.hfs from it.
|
||||
self._report_progress(f"Extracting bootable HFS+ image from {basesystem_dmg_path}...")
|
||||
if not self._extract_hfs_from_dmg(basesystem_dmg_path, self.temp_basesystem_hfs_path):
|
||||
raise RuntimeError("Failed to extract HFS+ image from installer assets.")
|
||||
elif basesystem_dmg_path.endswith("BaseSystem.dmg"): # If it's BaseSystem.dmg directly
|
||||
self._report_progress(f"Extracting bootable HFS+ image from {basesystem_dmg_path}...")
|
||||
if not self._extract_hfs_from_dmg(basesystem_dmg_path, self.temp_basesystem_hfs_path):
|
||||
raise RuntimeError("Failed to extract HFS+ image from BaseSystem.dmg.")
|
||||
else:
|
||||
raise RuntimeError(f"Unsupported file type for BaseSystem extraction: {basesystem_dmg_path}")
|
||||
|
||||
|
||||
self._report_progress(f"Writing BaseSystem HFS+ image to {macos_partition_dev} using dd...")
|
||||
self._run_command(["sudo", "dd", f"if={self.temp_basesystem_hfs_path}", f"of={macos_partition_dev}", "bs=4M", "status=progress", "oflag=sync"])
|
||||
|
||||
self._report_progress("Mounting macOS Install partition on USB...")
|
||||
self._run_command(["sudo", "mount", macos_partition_dev, self.mount_point_usb_macos_target])
|
||||
|
||||
# Copy BaseSystem.dmg & .chunklist to /System/Library/CoreServices/
|
||||
core_services_path_usb = os.path.join(self.mount_point_usb_macos_target, "System", "Library", "CoreServices")
|
||||
self._run_command(["sudo", "mkdir", "-p", core_services_path_usb])
|
||||
|
||||
# Find original BaseSystem.dmg and chunklist in download path to copy them
|
||||
actual_bs_dmg = self._find_source_file(["BaseSystem.dmg"], "original BaseSystem.dmg for copying")
|
||||
if actual_bs_dmg:
|
||||
self._report_progress(f"Copying {actual_bs_dmg} to {core_services_path_usb}/BaseSystem.dmg")
|
||||
self._run_command(["sudo", "cp", actual_bs_dmg, os.path.join(core_services_path_usb, "BaseSystem.dmg")])
|
||||
|
||||
bs_chunklist = actual_bs_dmg.replace(".dmg", ".chunklist")
|
||||
if os.path.exists(bs_chunklist):
|
||||
self._report_progress(f"Copying {bs_chunklist} to {core_services_path_usb}/BaseSystem.chunklist")
|
||||
self._run_command(["sudo", "cp", bs_chunklist, os.path.join(core_services_path_usb, "BaseSystem.chunklist")])
|
||||
else: self._report_progress(f"Warning: BaseSystem.chunklist not found at {bs_chunklist}")
|
||||
else: self._report_progress("Warning: Could not find original BaseSystem.dmg in download path to copy to CoreServices.")
|
||||
|
||||
# Copy InstallInfo.plist
|
||||
install_info_src = self._find_source_file(["InstallInfo.plist"], "InstallInfo.plist")
|
||||
if install_info_src:
|
||||
self._report_progress(f"Copying {install_info_src} to {self.mount_point_usb_macos_target}/InstallInfo.plist")
|
||||
self._run_command(["sudo", "cp", install_info_src, os.path.join(self.mount_point_usb_macos_target, "InstallInfo.plist")])
|
||||
else: self._report_progress("Warning: InstallInfo.plist not found in download path.")
|
||||
|
||||
# Copy Packages (placeholder - needs more specific logic based on gibMacOS output structure)
|
||||
self._report_progress("Placeholder: Copying macOS installation packages to USB (e.g., /System/Installation/Packages)...")
|
||||
# Example: sudo rsync -a /path/to/downloaded_packages_dir/ /mnt/usb_macos_target/System/Installation/Packages/
|
||||
# This needs to correctly identify the source Packages directory from gibMacOS output.
|
||||
# For now, we'll skip actual copying of packages folder, as its location and content can vary.
|
||||
# A proper implementation would require inspecting the gibMacOS download structure.
|
||||
# Create the directory though:
|
||||
self._run_command(["sudo", "mkdir", "-p", os.path.join(self.mount_point_usb_macos_target, "System", "Installation", "Packages")])
|
||||
|
||||
|
||||
# --- OpenCore EFI Setup ---
|
||||
self._report_progress("Setting up OpenCore EFI on ESP...")
|
||||
if not os.path.isdir(OC_TEMPLATE_DIR):
|
||||
self._report_progress(f"FATAL: OpenCore template directory not found at {OC_TEMPLATE_DIR}. Cannot proceed."); return False
|
||||
|
||||
self._report_progress(f"Copying OpenCore EFI template from {OC_TEMPLATE_DIR} to {self.temp_efi_build_dir}")
|
||||
self._run_command(["sudo", "cp", "-a", f"{OC_TEMPLATE_DIR}/.", self.temp_efi_build_dir]) # Copy contents
|
||||
|
||||
temp_config_plist_path = os.path.join(self.temp_efi_build_dir, "EFI", "OC", "config.plist") # Assume template is named config.plist
|
||||
if not os.path.exists(temp_config_plist_path) and os.path.exists(os.path.join(self.temp_efi_build_dir, "EFI", "OC", "config-template.plist")):
|
||||
# If template is config-template.plist, rename it for enhancement
|
||||
shutil.move(os.path.join(self.temp_efi_build_dir, "EFI", "OC", "config-template.plist"), temp_config_plist_path)
|
||||
|
||||
if self.enhance_plist_enabled and enhance_config_plist and os.path.exists(temp_config_plist_path):
|
||||
self._report_progress("Attempting to enhance config.plist...")
|
||||
if enhance_config_plist(temp_config_plist_path, self.target_macos_version, self._report_progress):
|
||||
self._report_progress("config.plist enhancement successful.")
|
||||
else: self._report_progress("config.plist enhancement failed or had issues. Continuing with (potentially original template) plist.")
|
||||
|
||||
self._run_command(["sudo", "mount", esp_partition_dev, self.mount_point_usb_esp])
|
||||
self._report_progress(f"Copying final EFI folder to USB ESP ({self.mount_point_usb_esp})...")
|
||||
self._run_command(["sudo", "rsync", "-avh", "--delete", f"{self.temp_efi_build_dir}/EFI/", f"{self.mount_point_usb_esp}/EFI/"])
|
||||
|
||||
self._report_progress("USB Installer creation process completed successfully.")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self._report_progress(f"An error occurred during USB writing: {e}")
|
||||
import traceback; self._report_progress(traceback.format_exc())
|
||||
return False
|
||||
finally:
|
||||
self._cleanup_temp_files_and_dirs()
|
||||
|
||||
if __name__ == '__main__':
|
||||
if os.geteuid() != 0: print("Please run this script as root (sudo) for testing."); exit(1)
|
||||
print("USB Writer Linux Standalone Test - Installer Method")
|
||||
|
||||
mock_download_dir = f"temp_macos_download_test_{os.getpid()}"
|
||||
os.makedirs(mock_download_dir, exist_ok=True)
|
||||
|
||||
# Create a dummy placeholder for what gibMacOS might download
|
||||
# This is highly simplified. A real gibMacOS download has a complex structure.
|
||||
# For this test, we'll simulate having BaseSystem.dmg and InstallInfo.plist
|
||||
mock_install_data_path = os.path.join(mock_download_dir, "macOS_Install_Data") # Simplified path
|
||||
os.makedirs(mock_install_data_path, exist_ok=True)
|
||||
dummy_bs_dmg_path = os.path.join(mock_install_data_path, "BaseSystem.dmg")
|
||||
dummy_installinfo_path = os.path.join(mock_download_dir, "InstallInfo.plist") # Often at root of a specific product download
|
||||
|
||||
if not os.path.exists(dummy_bs_dmg_path):
|
||||
# Create a tiny dummy file for 7z to "extract" from.
|
||||
# To make _extract_hfs_from_dmg work, it needs a real DMG with a HFS part.
|
||||
# This is hard to mock simply. For now, it will likely fail extraction.
|
||||
# A better mock would be a small, actual DMG with a tiny HFS file.
|
||||
print(f"Creating dummy BaseSystem.dmg at {dummy_bs_dmg_path} (will likely fail HFS extraction in test without a real DMG structure)")
|
||||
with open(dummy_bs_dmg_path, "wb") as f: f.write(os.urandom(1024*10)) # 10KB dummy
|
||||
if not os.path.exists(dummy_installinfo_path):
|
||||
with open(dummy_installinfo_path, "w") as f: f.write("<plist><dict><key>DummyInstallInfo</key><true/></dict></plist>")
|
||||
|
||||
# Create dummy EFI template
|
||||
if not os.path.exists(OC_TEMPLATE_DIR): os.makedirs(OC_TEMPLATE_DIR)
|
||||
if not os.path.exists(os.path.join(OC_TEMPLATE_DIR, "EFI", "OC")): os.makedirs(os.path.join(OC_TEMPLATE_DIR, "EFI", "OC"))
|
||||
dummy_config_template_path = os.path.join(OC_TEMPLATE_DIR, "EFI", "OC", "config.plist") # Name it config.plist directly
|
||||
if not os.path.exists(dummy_config_template_path):
|
||||
with open(dummy_config_template_path, "w") as f: f.write("<plist><dict><key>TestTemplate</key><true/></dict></plist>")
|
||||
|
||||
print("\nAvailable block devices (be careful!):")
|
||||
subprocess.run(["lsblk", "-d", "-o", "NAME,SIZE,MODEL"], check=True)
|
||||
test_device = input("\nEnter target device (e.g., /dev/sdX). THIS DEVICE WILL BE WIPED: ")
|
||||
|
||||
if not test_device or not test_device.startswith("/dev/"):
|
||||
print("Invalid device. Exiting.")
|
||||
else:
|
||||
confirm = input(f"Are you absolutely sure you want to wipe {test_device} and create installer? (yes/NO): ")
|
||||
success = False
|
||||
if confirm.lower() == 'yes':
|
||||
writer = USBWriterLinux(
|
||||
device=test_device,
|
||||
macos_download_path=mock_download_dir,
|
||||
progress_callback=print,
|
||||
enhance_plist_enabled=True,
|
||||
target_macos_version="Sonoma"
|
||||
)
|
||||
success = writer.format_and_write()
|
||||
else: print("Test cancelled by user.")
|
||||
print(f"Test finished. Success: {success}")
|
||||
|
||||
# Cleanup
|
||||
if os.path.exists(mock_download_dir): shutil.rmtree(mock_download_dir, ignore_errors=True)
|
||||
# if os.path.exists(OC_TEMPLATE_DIR) and "EFI_template_installer" in OC_TEMPLATE_DIR :
|
||||
# shutil.rmtree(OC_TEMPLATE_DIR, ignore_errors=True) # Avoid deleting if it's a real shared template
|
||||
print("Mock download dir cleaned up.")
|
||||
print(f"Note: {OC_TEMPLATE_DIR} and its contents might persist if not created by this test run specifically.")
|
316
usb_writer_macos.py
Normal file
316
usb_writer_macos.py
Normal file
@ -0,0 +1,316 @@
|
||||
# usb_writer_macos.py
|
||||
import subprocess
|
||||
import os
|
||||
import time
|
||||
import shutil # For checking command existence
|
||||
import plistlib # For parsing diskutil list -plist output
|
||||
|
||||
class USBWriterMacOS:
|
||||
def __init__(self, device: str, opencore_qcow2_path: str, macos_qcow2_path: str,
|
||||
progress_callback=None, enhance_plist_enabled: bool = False, target_macos_version: str = ""): # New args
|
||||
self.device = device # Should be like /dev/diskX
|
||||
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 # Store
|
||||
self.target_macos_version = target_macos_version # Store
|
||||
|
||||
pid = os.getpid()
|
||||
self.opencore_raw_path = f"opencore_temp_{pid}.raw"
|
||||
self.macos_raw_path = f"macos_main_temp_{pid}.raw"
|
||||
self.temp_opencore_mount = f"/tmp/opencore_efi_temp_skyscope_{pid}"
|
||||
self.temp_usb_esp_mount = f"/tmp/usb_esp_temp_skyscope_{pid}"
|
||||
self.temp_macos_source_mount = f"/tmp/macos_source_temp_skyscope_{pid}"
|
||||
self.temp_usb_macos_target_mount = f"/tmp/usb_macos_target_temp_skyscope_{pid}"
|
||||
|
||||
self.temp_files_to_clean = [self.opencore_raw_path, self.macos_raw_path]
|
||||
self.temp_mount_points_to_clean = [
|
||||
self.temp_opencore_mount, self.temp_usb_esp_mount,
|
||||
self.temp_macos_source_mount, self.temp_usb_macos_target_mount
|
||||
]
|
||||
self.attached_raw_images_devices = [] # Store devices from hdiutil attach
|
||||
|
||||
def _report_progress(self, message: str):
|
||||
print(message) # For standalone testing
|
||||
if self.progress_callback:
|
||||
self.progress_callback(message)
|
||||
|
||||
def _run_command(self, command: list[str], check=True, capture_output=False, timeout=None):
|
||||
self._report_progress(f"Executing: {' '.join(command)}")
|
||||
try:
|
||||
process = subprocess.run(
|
||||
command, check=check, capture_output=capture_output, text=True, timeout=timeout
|
||||
)
|
||||
if capture_output:
|
||||
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 {' '.join(command)} timed out after {timeout} seconds.")
|
||||
raise
|
||||
except subprocess.CalledProcessError as e:
|
||||
self._report_progress(f"Error executing {' '.join(command)} (code {e.returncode}): {e.stderr or e.stdout or str(e)}")
|
||||
raise
|
||||
except FileNotFoundError:
|
||||
self._report_progress(f"Error: Command '{command[0]}' not found. Is it installed and in PATH?")
|
||||
raise
|
||||
|
||||
def _cleanup_temp_files(self):
|
||||
self._report_progress("Cleaning up temporary image files...")
|
||||
for f_path in self.temp_files_to_clean:
|
||||
if os.path.exists(f_path):
|
||||
try:
|
||||
os.remove(f_path)
|
||||
self._report_progress(f"Removed {f_path}")
|
||||
except OSError as e:
|
||||
self._report_progress(f"Error removing {f_path}: {e}")
|
||||
|
||||
def _unmount_path(self, mount_path_or_device, is_device=False, force=False):
|
||||
target = mount_path_or_device
|
||||
cmd_base = ["diskutil"]
|
||||
action = "unmountDisk" if is_device else "unmount"
|
||||
|
||||
if force:
|
||||
cmd = cmd_base + [action, "force", target]
|
||||
else:
|
||||
cmd = cmd_base + [action, target]
|
||||
|
||||
is_target_valid_for_unmount = (os.path.ismount(mount_path_or_device) and not is_device) or \
|
||||
(is_device and os.path.exists(target))
|
||||
|
||||
if is_target_valid_for_unmount:
|
||||
self._report_progress(f"Attempting to unmount {target} (Action: {action}, Force: {force})...")
|
||||
self._run_command(cmd, check=False, timeout=30)
|
||||
|
||||
def _detach_raw_image_device(self, device_path):
|
||||
if device_path and os.path.exists(device_path):
|
||||
self._report_progress(f"Detaching raw image device {device_path}...")
|
||||
try:
|
||||
info_check = subprocess.run(["diskutil", "info", device_path], capture_output=True, text=True, check=False)
|
||||
if info_check.returncode == 0:
|
||||
self._run_command(["hdiutil", "detach", device_path, "-force"], check=False, timeout=30)
|
||||
else:
|
||||
self._report_progress(f"Device {device_path} appears invalid or already detached.")
|
||||
except Exception as e:
|
||||
self._report_progress(f"Exception while checking/detaching {device_path}: {e}")
|
||||
|
||||
def _cleanup_all_mounts_and_mappings(self):
|
||||
self._report_progress("Cleaning up all temporary mounts and attached raw images...")
|
||||
for mp in reversed(self.temp_mount_points_to_clean):
|
||||
self._unmount_path(mp, force=True)
|
||||
if os.path.exists(mp):
|
||||
try: os.rmdir(mp)
|
||||
except OSError as e: self._report_progress(f"Could not rmdir {mp}: {e}")
|
||||
|
||||
devices_to_detach = list(self.attached_raw_images_devices)
|
||||
for dev_path in devices_to_detach:
|
||||
self._detach_raw_image_device(dev_path)
|
||||
self.attached_raw_images_devices = []
|
||||
|
||||
|
||||
def check_dependencies(self):
|
||||
self._report_progress("Checking dependencies (qemu-img, diskutil, hdiutil, rsync)...")
|
||||
dependencies = ["qemu-img", "diskutil", "hdiutil", "rsync"]
|
||||
missing_deps = []
|
||||
for dep in dependencies:
|
||||
if not shutil.which(dep):
|
||||
missing_deps.append(dep)
|
||||
|
||||
if missing_deps:
|
||||
msg = f"Missing dependencies: {', '.join(missing_deps)}. `qemu-img` might need to be installed (e.g., via Homebrew: `brew install qemu`). `diskutil`, `hdiutil`, `rsync` are usually standard on macOS."
|
||||
self._report_progress(msg)
|
||||
raise RuntimeError(msg)
|
||||
|
||||
self._report_progress("All critical dependencies found.")
|
||||
return True
|
||||
|
||||
def _get_partition_device_id(self, parent_disk_id_str: str, partition_label_or_type: str) -> str | None:
|
||||
"""Finds partition device ID by Volume Name or Content Hint."""
|
||||
target_disk_id = parent_disk_id_str.replace("/dev/", "")
|
||||
self._report_progress(f"Searching for partition '{partition_label_or_type}' on disk '{target_disk_id}'")
|
||||
try:
|
||||
result = self._run_command(["diskutil", "list", "-plist", target_disk_id], capture_output=True)
|
||||
if not result.stdout:
|
||||
self._report_progress(f"No stdout from diskutil list for {target_disk_id}")
|
||||
return None
|
||||
|
||||
plist_data = plistlib.loads(result.stdout.encode('utf-8'))
|
||||
|
||||
all_disks_and_partitions = plist_data.get("AllDisksAndPartitions", [])
|
||||
if not isinstance(all_disks_and_partitions, list):
|
||||
if plist_data.get("DeviceIdentifier") == target_disk_id:
|
||||
all_disks_and_partitions = [plist_data]
|
||||
else:
|
||||
all_disks_and_partitions = []
|
||||
|
||||
for disk_info_entry in all_disks_and_partitions:
|
||||
current_disk_id_in_plist = disk_info_entry.get("DeviceIdentifier")
|
||||
if current_disk_id_in_plist == target_disk_id:
|
||||
for part_info in disk_info_entry.get("Partitions", []):
|
||||
vol_name = part_info.get("VolumeName")
|
||||
content_hint = part_info.get("Content")
|
||||
device_id = part_info.get("DeviceIdentifier")
|
||||
|
||||
if device_id:
|
||||
if vol_name and vol_name.strip().lower() == partition_label_or_type.strip().lower():
|
||||
self._report_progress(f"Found partition by VolumeName: {vol_name} -> /dev/{device_id}")
|
||||
return f"/dev/{device_id}"
|
||||
if content_hint and content_hint.strip().lower() == partition_label_or_type.strip().lower():
|
||||
self._report_progress(f"Found partition by Content type: {content_hint} -> /dev/{device_id}")
|
||||
return f"/dev/{device_id}"
|
||||
|
||||
self._report_progress(f"Partition '{partition_label_or_type}' not found on disk '{target_disk_id}'.")
|
||||
return None
|
||||
except Exception as e:
|
||||
self._report_progress(f"Error parsing 'diskutil list -plist {target_disk_id}': {e}")
|
||||
return None
|
||||
|
||||
def format_and_write(self) -> bool:
|
||||
try:
|
||||
self.check_dependencies()
|
||||
self._cleanup_all_mounts_and_mappings()
|
||||
|
||||
for mp in self.temp_mount_points_to_clean:
|
||||
os.makedirs(mp, exist_ok=True)
|
||||
|
||||
self._report_progress(f"WARNING: ALL DATA ON {self.device} WILL BE ERASED!")
|
||||
self._report_progress(f"Unmounting disk {self.device} (force)...")
|
||||
self._unmount_path(self.device, is_device=True, force=True)
|
||||
time.sleep(2)
|
||||
|
||||
self._report_progress(f"Partitioning {self.device} with GPT scheme...")
|
||||
self._run_command([
|
||||
"diskutil", "partitionDisk", self.device, "GPT",
|
||||
"MS-DOS FAT32", "EFI", "551MiB",
|
||||
"JHFS+", "macOS_USB", "0b"
|
||||
], timeout=180)
|
||||
time.sleep(3)
|
||||
|
||||
esp_partition_dev = self._get_partition_device_id(self.device, "EFI")
|
||||
macos_partition_dev = self._get_partition_device_id(self.device, "macOS_USB")
|
||||
|
||||
if not (esp_partition_dev and os.path.exists(esp_partition_dev)):
|
||||
esp_partition_dev = f"{self.device}s1"
|
||||
if not (macos_partition_dev and os.path.exists(macos_partition_dev)):
|
||||
macos_partition_dev = f"{self.device}s2"
|
||||
|
||||
if not (os.path.exists(esp_partition_dev) and os.path.exists(macos_partition_dev)):
|
||||
raise RuntimeError(f"Could not identify partitions on {self.device}. ESP: {esp_partition_dev}, macOS: {macos_partition_dev}")
|
||||
|
||||
self._report_progress(f"Identified ESP: {esp_partition_dev}, macOS Partition: {macos_partition_dev}")
|
||||
|
||||
# --- Write EFI content ---
|
||||
self._report_progress(f"Converting OpenCore QCOW2 ({self.opencore_qcow2_path}) 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(f"Attaching RAW OpenCore image ({self.opencore_raw_path})...")
|
||||
attach_cmd_efi = ["hdiutil", "attach", "-nomount", "-imagekey", "diskimage-class=CRawDiskImage", self.opencore_raw_path]
|
||||
efi_attach_output = self._run_command(attach_cmd_efi, capture_output=True).stdout.strip()
|
||||
raw_efi_disk_id = efi_attach_output.splitlines()[-1].strip().split()[0]
|
||||
if not raw_efi_disk_id.startswith("/dev/disk"):
|
||||
raise RuntimeError(f"Failed to attach raw EFI image: {efi_attach_output}")
|
||||
self.attached_raw_images_devices.append(raw_efi_disk_id)
|
||||
self._report_progress(f"Attached raw OpenCore image as {raw_efi_disk_id}")
|
||||
time.sleep(2)
|
||||
|
||||
source_efi_partition_dev = self._get_partition_device_id(raw_efi_disk_id, "EFI") or f"{raw_efi_disk_id}s1"
|
||||
|
||||
self._report_progress(f"Mounting source EFI partition ({source_efi_partition_dev}) to {self.temp_opencore_mount}...")
|
||||
self._run_command(["diskutil", "mount", "readOnly", "-mountPoint", self.temp_opencore_mount, source_efi_partition_dev], timeout=30)
|
||||
|
||||
self._report_progress(f"Mounting target USB ESP ({esp_partition_dev}) to {self.temp_usb_esp_mount}...")
|
||||
self._run_command(["diskutil", "mount", "-mountPoint", self.temp_usb_esp_mount, esp_partition_dev], timeout=30)
|
||||
|
||||
source_efi_content_path = os.path.join(self.temp_opencore_mount, "EFI")
|
||||
if not os.path.isdir(source_efi_content_path): source_efi_content_path = self.temp_opencore_mount
|
||||
|
||||
target_efi_dir_on_usb = os.path.join(self.temp_usb_esp_mount, "EFI")
|
||||
self._report_progress(f"Copying EFI files from {source_efi_content_path} to {target_efi_dir_on_usb}...")
|
||||
self._run_command(["sudo", "rsync", "-avh", "--delete", f"{source_efi_content_path}/", f"{target_efi_dir_on_usb}/"])
|
||||
|
||||
self._unmount_path(self.temp_opencore_mount, force=True)
|
||||
self._unmount_path(self.temp_usb_esp_mount, force=True)
|
||||
self._detach_raw_image_device(raw_efi_disk_id); raw_efi_disk_id = None
|
||||
|
||||
# --- Write macOS main image (File-level copy) ---
|
||||
self._report_progress(f"Converting macOS QCOW2 ({self.macos_qcow2_path}) to RAW ({self.macos_raw_path})...")
|
||||
self._report_progress("This may take a very long time...")
|
||||
self._run_command(["qemu-img", "convert", "-O", "raw", self.macos_qcow2_path, self.macos_raw_path])
|
||||
|
||||
self._report_progress(f"Attaching RAW macOS image ({self.macos_raw_path})...")
|
||||
attach_cmd_macos = ["hdiutil", "attach", "-nomount", "-imagekey", "diskimage-class=CRawDiskImage", self.macos_raw_path]
|
||||
macos_attach_output = self._run_command(attach_cmd_macos, capture_output=True).stdout.strip()
|
||||
raw_macos_disk_id = macos_attach_output.splitlines()[-1].strip().split()[0]
|
||||
if not raw_macos_disk_id.startswith("/dev/disk"):
|
||||
raise RuntimeError(f"Failed to attach raw macOS image: {macos_attach_output}")
|
||||
self.attached_raw_images_devices.append(raw_macos_disk_id)
|
||||
self._report_progress(f"Attached raw macOS image as {raw_macos_disk_id}")
|
||||
time.sleep(2)
|
||||
|
||||
source_macos_part_dev = self._get_partition_device_id(raw_macos_disk_id, "Apple_APFS_Container") or \
|
||||
self._get_partition_device_id(raw_macos_disk_id, "Apple_APFS") or \
|
||||
self._get_partition_device_id(raw_macos_disk_id, "Apple_HFS") or \
|
||||
f"{raw_macos_disk_id}s2"
|
||||
if not (source_macos_part_dev and os.path.exists(source_macos_part_dev)):
|
||||
raise RuntimeError(f"Could not find source macOS partition on {raw_macos_disk_id}")
|
||||
|
||||
self._report_progress(f"Mounting source macOS partition ({source_macos_part_dev}) to {self.temp_macos_source_mount}...")
|
||||
self._run_command(["diskutil", "mount", "readOnly", "-mountPoint", self.temp_macos_source_mount, source_macos_part_dev], timeout=60)
|
||||
|
||||
self._report_progress(f"Mounting target USB macOS partition ({macos_partition_dev}) to {self.temp_usb_macos_target_mount}...")
|
||||
self._run_command(["diskutil", "mount", "-mountPoint", self.temp_usb_macos_target_mount, macos_partition_dev], timeout=30)
|
||||
|
||||
self._report_progress(f"Copying macOS system files from {self.temp_macos_source_mount} to {self.temp_usb_macos_target_mount} (sudo rsync)...")
|
||||
self._report_progress("This will also take a very long time.")
|
||||
self._run_command([
|
||||
"sudo", "rsync", "-avh", "--delete",
|
||||
"--exclude=.Spotlight-V100", "--exclude=.fseventsd", "--exclude=/.Trashes", "--exclude=/System/Volumes/VM", "--exclude=/private/var/vm",
|
||||
f"{self.temp_macos_source_mount}/", f"{self.temp_usb_macos_target_mount}/"
|
||||
])
|
||||
|
||||
self._report_progress("USB writing process completed successfully.")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self._report_progress(f"An error occurred during USB writing on macOS: {e}")
|
||||
import traceback
|
||||
self._report_progress(traceback.format_exc())
|
||||
return False
|
||||
finally:
|
||||
self._cleanup_all_mounts_and_mappings()
|
||||
self._cleanup_temp_files()
|
||||
|
||||
if __name__ == '__main__':
|
||||
if platform.system() != "Darwin": print("This script is intended for macOS."); exit(1)
|
||||
print("USB Writer macOS Standalone Test - File Copy Method")
|
||||
|
||||
mock_opencore_path = "mock_opencore_macos.qcow2"
|
||||
mock_macos_path = "mock_macos_macos.qcow2"
|
||||
if not os.path.exists(mock_opencore_path): subprocess.run(["qemu-img", "create", "-f", "qcow2", mock_opencore_path, "384M"])
|
||||
if not os.path.exists(mock_macos_path): subprocess.run(["qemu-img", "create", "-f", "qcow2", mock_macos_path, "1G"])
|
||||
|
||||
print("\nAvailable disks (use 'diskutil list external physical' in Terminal to identify your USB):")
|
||||
subprocess.run(["diskutil", "list", "external", "physical"], check=False)
|
||||
test_device = input("\nEnter target disk identifier (e.g., /dev/diskX - NOT /dev/diskXsY). THIS DISK WILL BE WIPED: ")
|
||||
|
||||
if not test_device or not test_device.startswith("/dev/disk"):
|
||||
print("Invalid disk identifier. Exiting.")
|
||||
if os.path.exists(mock_opencore_path): os.remove(mock_opencore_path)
|
||||
if os.path.exists(mock_macos_path): os.remove(mock_macos_path)
|
||||
exit(1)
|
||||
|
||||
confirm = input(f"Are you sure you want to wipe {test_device} and write mock images? (yes/NO): ")
|
||||
success = False
|
||||
if confirm.lower() == 'yes':
|
||||
print("Ensure you have sudo privileges for rsync if needed, or app is run as root.")
|
||||
writer = USBWriterMacOS(test_device, mock_opencore_path, mock_macos_path, print)
|
||||
success = writer.format_and_write()
|
||||
else:
|
||||
print("Test cancelled.")
|
||||
|
||||
print(f"Test finished. Success: {success}")
|
||||
if os.path.exists(mock_opencore_path): os.remove(mock_opencore_path)
|
||||
if os.path.exists(mock_macos_path): os.remove(mock_macos_path)
|
||||
print("Mock files cleaned up.")
|
269
usb_writer_windows.py
Normal file
269
usb_writer_windows.py
Normal file
@ -0,0 +1,269 @@
|
||||
# usb_writer_windows.py
|
||||
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, 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"
|
||||
self.macos_raw_path = f"macos_main_temp_{pid}.raw"
|
||||
self.temp_efi_extract_dir = f"temp_efi_files_{pid}"
|
||||
|
||||
self.temp_files_to_clean = [self.opencore_raw_path, self.macos_raw_path]
|
||||
self.temp_dirs_to_clean = [self.temp_efi_extract_dir]
|
||||
self.assigned_efi_letter = None
|
||||
|
||||
def _report_progress(self, message: str):
|
||||
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)}")
|
||||
try:
|
||||
process = subprocess.run(
|
||||
command, check=check, capture_output=capture_output, text=True, timeout=timeout, shell=shell, cwd=working_dir,
|
||||
creationflags=subprocess.CREATE_NO_WINDOW
|
||||
)
|
||||
if capture_output:
|
||||
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
|
||||
|
||||
|
||||
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)
|
||||
output_text = "" # Initialize to empty string
|
||||
try:
|
||||
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):
|
||||
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):
|
||||
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; 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.")
|
||||
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() # Clean before start
|
||||
os.makedirs(self.temp_efi_extract_dir, exist_ok=True)
|
||||
|
||||
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"Will attempt to assign letter {self.assigned_efi_letter}: to EFI partition.")
|
||||
|
||||
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])
|
||||
|
||||
if shutil.which("7z"):
|
||||
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):
|
||||
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}:\\"):
|
||||
# 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 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])
|
||||
|
||||
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 (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())
|
||||
return False
|
||||
finally:
|
||||
if self.assigned_efi_letter:
|
||||
self._run_diskpart_script(f"select volume {self.assigned_efi_letter}\nremove letter={self.assigned_efi_letter}\nexit")
|
||||
self._cleanup_temp_files_and_dirs()
|
||||
|
||||
if __name__ == '__main__':
|
||||
if platform.system() != "Windows":
|
||||
print("This script is for Windows standalone testing."); exit(1)
|
||||
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 = 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_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.")
|
||||
|
||||
if os.path.exists(mock_oc): os.remove(mock_oc)
|
||||
if os.path.exists(mock_mac): os.remove(mock_mac)
|
||||
print("Mocks cleaned.")
|
126
utils.py
Normal file
126
utils.py
Normal file
@ -0,0 +1,126 @@
|
||||
# utils.py
|
||||
|
||||
import time
|
||||
import uuid
|
||||
from constants import (
|
||||
DOCKER_IMAGE_BASE,
|
||||
DEFAULT_DOCKER_PARAMS,
|
||||
VERSION_SPECIFIC_PARAMS,
|
||||
MACOS_VERSIONS
|
||||
)
|
||||
|
||||
# Path to the generated images inside the Docker container
|
||||
CONTAINER_MACOS_IMG_PATH = "/home/arch/OSX-KVM/mac_hdd_ng.img"
|
||||
# The OpenCore.qcow2 path can vary if BOOTDISK env var is used.
|
||||
# The default generated one by the scripts (if not overridden by BOOTDISK) is:
|
||||
CONTAINER_OPENCORE_QCOW2_PATH = "/home/arch/OSX-KVM/OpenCore/OpenCore.qcow2"
|
||||
|
||||
|
||||
def get_unique_container_name() -> str:
|
||||
"""Generates a unique Docker container name."""
|
||||
return f"skyscope-osx-vm-{uuid.uuid4().hex[:8]}"
|
||||
|
||||
def build_docker_command(macos_version_name: str, container_name: str) -> list[str]:
|
||||
"""
|
||||
Builds the docker run command arguments as a list.
|
||||
|
||||
Args:
|
||||
macos_version_name: The display name of the macOS version (e.g., "Sonoma").
|
||||
container_name: The unique name for the Docker container.
|
||||
|
||||
Returns:
|
||||
A list of strings representing the docker command and its arguments.
|
||||
"""
|
||||
if macos_version_name not in MACOS_VERSIONS:
|
||||
raise ValueError(f"Unsupported macOS version: {macos_version_name}")
|
||||
|
||||
image_tag = MACOS_VERSIONS[macos_version_name]
|
||||
full_image_name = f"{DOCKER_IMAGE_BASE}:{image_tag}"
|
||||
|
||||
# Removed --rm: we need the container to persist for file extraction
|
||||
final_command_args = ["docker", "run", "-it", "--name", container_name]
|
||||
|
||||
# Base parameters for the docker command
|
||||
run_params = DEFAULT_DOCKER_PARAMS.copy()
|
||||
|
||||
# Override/extend with version-specific parameters
|
||||
if macos_version_name in VERSION_SPECIFIC_PARAMS:
|
||||
version_specific = VERSION_SPECIFIC_PARAMS[macos_version_name]
|
||||
|
||||
# More robustly handle environment variables (-e)
|
||||
# Collect all -e keys from defaults and version-specific
|
||||
default_env_vars = {k.split(" ", 1)[1].split("=")[0]: v for k, v in DEFAULT_DOCKER_PARAMS.items() if k.startswith("-e ")}
|
||||
version_env_vars = {k.split(" ", 1)[1].split("=")[0]: v for k, v in version_specific.items() if k.startswith("-e ")}
|
||||
|
||||
merged_env_vars = {**default_env_vars, **version_env_vars}
|
||||
|
||||
# Remove all old -e params from run_params before adding merged ones
|
||||
keys_to_remove_from_run_params = [k_param for k_param in run_params if k_param.startswith("-e ")]
|
||||
for k_rem in keys_to_remove_from_run_params:
|
||||
del run_params[k_rem]
|
||||
|
||||
# Add merged env vars back with the "-e VAR_NAME" format for keys
|
||||
for env_name, env_val_str in merged_env_vars.items():
|
||||
run_params[f"-e {env_name}"] = env_val_str
|
||||
|
||||
# Add other non -e version-specific params
|
||||
for k, v in version_specific.items():
|
||||
if not k.startswith("-e "):
|
||||
run_params[k] = v
|
||||
|
||||
# Construct the command list
|
||||
for key, value in run_params.items():
|
||||
if key.startswith("-e "):
|
||||
# Key is like "-e VARNAME", value is the actual value string like "'data'" or "GENERATE_UNIQUE='true'"
|
||||
env_var_name_from_key = key.split(" ", 1)[1] # e.g. GENERATE_UNIQUE or CPU
|
||||
|
||||
# If value string itself contains '=', it's likely the full 'VAR=val' form
|
||||
if isinstance(value, str) and '=' in value and value.strip("'").upper().startswith(env_var_name_from_key.upper()):
|
||||
# e.g. value is "GENERATE_UNIQUE='true'"
|
||||
final_env_val = value.strip("'")
|
||||
else:
|
||||
# e.g. value is "'true'" for key "-e GENERATE_UNIQUE"
|
||||
final_env_val = f"{env_var_name_from_key}={value.strip("'")}"
|
||||
final_command_args.extend(["-e", final_env_val])
|
||||
else: # for --device, -p, -v
|
||||
final_command_args.extend([key, value.strip("'")]) # Strip quotes for safety
|
||||
|
||||
final_command_args.append(full_image_name)
|
||||
|
||||
return final_command_args
|
||||
|
||||
def build_docker_cp_command(container_name_or_id: str, container_path: str, host_path: str) -> list[str]:
|
||||
"""Builds the 'docker cp' command."""
|
||||
return ["docker", "cp", f"{container_name_or_id}:{container_path}", host_path]
|
||||
|
||||
def build_docker_stop_command(container_name_or_id: str) -> list[str]:
|
||||
"""Builds the 'docker stop' command."""
|
||||
return ["docker", "stop", container_name_or_id]
|
||||
|
||||
def build_docker_rm_command(container_name_or_id: str) -> list[str]:
|
||||
"""Builds the 'docker rm' command."""
|
||||
return ["docker", "rm", container_name_or_id]
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Test the functions
|
||||
container_name = get_unique_container_name()
|
||||
print(f"Generated container name: {container_name}")
|
||||
|
||||
for version_name_key in MACOS_VERSIONS.keys():
|
||||
print(f"Command for {version_name_key}:")
|
||||
cmd_list = build_docker_command(version_name_key, container_name)
|
||||
print(" ".join(cmd_list))
|
||||
print("-" * 20)
|
||||
|
||||
test_container_id = container_name # or an actual ID
|
||||
print(f"CP Main Image: {' '.join(build_docker_cp_command(test_container_id, CONTAINER_MACOS_IMG_PATH, './mac_hdd_ng.img'))}")
|
||||
print(f"CP OpenCore Image: {' '.join(build_docker_cp_command(test_container_id, CONTAINER_OPENCORE_QCOW2_PATH, './OpenCore.qcow2'))}")
|
||||
print(f"Stop Command: {' '.join(build_docker_stop_command(test_container_id))}")
|
||||
print(f"Remove Command: {' '.join(build_docker_rm_command(test_container_id))}")
|
||||
|
||||
# Test with a non-existent version
|
||||
try:
|
||||
build_docker_command("NonExistentVersion", container_name)
|
||||
except ValueError as e:
|
||||
print(e)
|
@ -125,17 +125,4 @@ RUN printf '\n\n\n\n%s\n%s\n\n\n\n' '===========VNC_PASSWORD========== ' "$(<vnc
|
||||
|
||||
WORKDIR /home/arch/OSX-KVM
|
||||
|
||||
# DMCA compliant download process
|
||||
# If BaseSystem.img does not exist, download ${SHORTNAME}
|
||||
|
||||
# shortname default is catalina, which means :latest is catalina
|
||||
ENV SHORTNAME=sonoma
|
||||
|
||||
ENV BASESYSTEM_IMAGE=BaseSystem.img
|
||||
|
||||
CMD ! [[ -e "${BASESYSTEM_IMAGE:-BaseSystem.img}" ]] \
|
||||
&& printf '%s\n' "No BaseSystem.img available, downloading ${SHORTNAME}" \
|
||||
&& make \
|
||||
&& qemu-img convert BaseSystem.dmg -O qcow2 -p -c ${BASESYSTEM_IMAGE:-BaseSystem.img} \
|
||||
&& rm ./BaseSystem.dmg \
|
||||
; ./enable-ssh.sh && envsubst < ./Launch_custom.sh | bash
|
||||
CMD ./enable-ssh.sh && envsubst < ./Launch_custom.sh | bash
|
||||
|
@ -203,18 +203,5 @@ RUN vncpasswd -f < vncpasswd_file > ${HOME}/.vnc/passwd
|
||||
RUN chmod 600 ~/.vnc/passwd
|
||||
RUN printf '\n\n\n\n%s\n%s\n\n\n\n' '===========VNC_PASSWORD========== ' "$(<vncpasswd_file)"
|
||||
|
||||
# DMCA compliant download process
|
||||
# If BaseSystem.img does not exist, download ${SHORTNAME}
|
||||
|
||||
# shortname default is catalina, which means :latest is catalina
|
||||
ENV SHORTNAME=sonoma
|
||||
|
||||
ENV BASESYSTEM_IMAGE=BaseSystem.img
|
||||
|
||||
CMD ! [[ -e "${BASESYSTEM_IMAGE:-BaseSystem.img}" ]] \
|
||||
&& printf '%s\n' "No BaseSystem.img available, downloading ${SHORTNAME}" \
|
||||
&& make \
|
||||
&& qemu-img convert BaseSystem.dmg -O qcow2 -p -c ${BASESYSTEM_IMAGE:-BaseSystem.img} \
|
||||
&& rm ./BaseSystem.dmg \
|
||||
; ./enable-ssh.sh && envsubst < ./Launch_custom.sh | bash
|
||||
CMD ./enable-ssh.sh && envsubst < ./Launch_custom.sh | bash
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user