diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml deleted file mode 100644 index e23f0dc6..00000000 --- a/.github/workflows/docker-build.yml +++ /dev/null @@ -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" \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index f3c117a0..e8d51f6b 100644 --- a/Dockerfile +++ b/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 \ diff --git a/Dockerfile.auto b/Dockerfile.auto index b1508922..432d01ba 100644 --- a/Dockerfile.auto +++ b/Dockerfile.auto @@ -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 \ diff --git a/Dockerfile.monterey b/Dockerfile.monterey new file mode 100644 index 00000000..92cfe5a3 --- /dev/null +++ b/Dockerfile.monterey @@ -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 ' + +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 diff --git a/Dockerfile.naked b/Dockerfile.naked index 41f4fefc..712d0592 100644 --- a/Dockerfile.naked +++ b/Dockerfile.naked @@ -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 \ diff --git a/Dockerfile.naked-auto b/Dockerfile.naked-auto index 6e8bddd7..44f2866d 100644 --- a/Dockerfile.naked-auto +++ b/Dockerfile.naked-auto @@ -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 \ diff --git a/EFI_template_installer/EFI/BOOT/BOOTx64.efi b/EFI_template_installer/EFI/BOOT/BOOTx64.efi new file mode 100644 index 00000000..e69de29b diff --git a/EFI_template_installer/EFI/OC/ACPI/SSDT-AWAC.aml b/EFI_template_installer/EFI/OC/ACPI/SSDT-AWAC.aml new file mode 100644 index 00000000..e69de29b diff --git a/EFI_template_installer/EFI/OC/ACPI/SSDT-EC-USBX.aml b/EFI_template_installer/EFI/OC/ACPI/SSDT-EC-USBX.aml new file mode 100644 index 00000000..e69de29b diff --git a/EFI_template_installer/EFI/OC/ACPI/SSDT-PLUG-ALT.aml b/EFI_template_installer/EFI/OC/ACPI/SSDT-PLUG-ALT.aml new file mode 100644 index 00000000..e69de29b diff --git a/EFI_template_installer/EFI/OC/ACPI/SSDT-RHUB.aml b/EFI_template_installer/EFI/OC/ACPI/SSDT-RHUB.aml new file mode 100644 index 00000000..e69de29b diff --git a/EFI_template_installer/EFI/OC/Drivers/HfsPlus.efi b/EFI_template_installer/EFI/OC/Drivers/HfsPlus.efi new file mode 100644 index 00000000..e69de29b diff --git a/EFI_template_installer/EFI/OC/Drivers/OpenCanopy.efi b/EFI_template_installer/EFI/OC/Drivers/OpenCanopy.efi new file mode 100644 index 00000000..e69de29b diff --git a/EFI_template_installer/EFI/OC/Drivers/OpenRuntime.efi b/EFI_template_installer/EFI/OC/Drivers/OpenRuntime.efi new file mode 100644 index 00000000..e69de29b diff --git a/EFI_template_installer/EFI/OC/Kexts/AppleALC.kext b/EFI_template_installer/EFI/OC/Kexts/AppleALC.kext new file mode 100644 index 00000000..e69de29b diff --git a/EFI_template_installer/EFI/OC/Kexts/IntelMausi.kext b/EFI_template_installer/EFI/OC/Kexts/IntelMausi.kext new file mode 100644 index 00000000..e69de29b diff --git a/EFI_template_installer/EFI/OC/Kexts/Lilu.kext b/EFI_template_installer/EFI/OC/Kexts/Lilu.kext new file mode 100644 index 00000000..e69de29b diff --git a/EFI_template_installer/EFI/OC/Kexts/LucyRTL8125Ethernet.kext b/EFI_template_installer/EFI/OC/Kexts/LucyRTL8125Ethernet.kext new file mode 100644 index 00000000..e69de29b diff --git a/EFI_template_installer/EFI/OC/Kexts/RealtekRTL8111.kext b/EFI_template_installer/EFI/OC/Kexts/RealtekRTL8111.kext new file mode 100644 index 00000000..e69de29b diff --git a/EFI_template_installer/EFI/OC/Kexts/VirtualSMC.kext b/EFI_template_installer/EFI/OC/Kexts/VirtualSMC.kext new file mode 100644 index 00000000..e69de29b diff --git a/EFI_template_installer/EFI/OC/Kexts/WhateverGreen.kext b/EFI_template_installer/EFI/OC/Kexts/WhateverGreen.kext new file mode 100644 index 00000000..e69de29b diff --git a/EFI_template_installer/EFI/OC/OpenCore.efi b/EFI_template_installer/EFI/OC/OpenCore.efi new file mode 100644 index 00000000..e69de29b diff --git a/EFI_template_installer/EFI/OC/config-template.plist b/EFI_template_installer/EFI/OC/config-template.plist new file mode 100644 index 00000000..bf1ea734 --- /dev/null +++ b/EFI_template_installer/EFI/OC/config-template.plist @@ -0,0 +1,103 @@ + + + + + ACPI + + Add + + CommentSSDT-PLUG-ALT: CPU Power ManagementEnabledPathSSDT-PLUG-ALT.aml + CommentSSDT-EC-USBX: Embedded Controller and USB PowerEnabledPathSSDT-EC-USBX.aml + CommentSSDT-AWAC: Realtime Clock FixEnabledPathSSDT-AWAC.aml + CommentSSDT-RHUB: USB ResetEnabledPathSSDT-RHUB.aml + + Delete + Patch + Quirks + + FadtEnableReset + NormalizeHeaders + RebaseRegions + ResetHwSig + ResetLogoStatus + SyncTableIds + + + Booter + + MmioWhitelist + Patch + Quirks + + AllowRelocationBlock + AvoidRuntimeDefrag + DevirtualiseMmio + DisableSingleUser + DisableVariableWrite + DiscardHibernateMap + EnableSafeModeSlide + EnableWriteUnprotector + ForceBooterSignature + ForceExitBootServices + ProtectMemoryRegions + ProtectSecureBoot + ProtectUefiServices + ProvideCustomSlide + ProvideMaxSlide0 + RebuildAppleMemoryMap + ResizeAppleGpuBars-1 + SetupVirtualMap + SignalAppleOS + SyncRuntimePermissions + + + DevicePropertiesAddDelete + Kernel + + Add + + ArchAnyBundlePathLilu.kextCommentLiluEnabledExecutablePathContents/MacOS/LiluMaxKernelMinKernelPlistPathContents/Info.plist + ArchAnyBundlePathVirtualSMC.kextCommentVirtualSMCEnabledExecutablePathContents/MacOS/VirtualSMCMaxKernelMinKernelPlistPathContents/Info.plist + ArchAnyBundlePathWhateverGreen.kextCommentWhateverGreen for GraphicsEnabledExecutablePathContents/MacOS/WhateverGreenMaxKernelMinKernelPlistPathContents/Info.plist + ArchAnyBundlePathAppleALC.kextCommentAppleALC for AudioEnabledExecutablePathContents/MacOS/AppleALCMaxKernelMinKernelPlistPathContents/Info.plist + ArchAnyBundlePathIntelMausi.kextCommentIntel EthernetEnabledExecutablePathContents/MacOS/IntelMausiMaxKernelMinKernelPlistPathContents/Info.plist + ArchAnyBundlePathRealtekRTL8111.kextCommentRealtek RTL8111EnabledExecutablePathContents/MacOS/RealtekRTL8111MaxKernelMinKernelPlistPathContents/Info.plist + ArchAnyBundlePathLucyRTL8125Ethernet.kextCommentRealtek RTL8125 2.5GbEEnabledExecutablePathContents/MacOS/LucyRTL8125EthernetMaxKernelMinKernelPlistPathContents/Info.plist + + Block + EmulateCpuid1DataCpuid1MaskDummyPowerManagementMaxKernelMinKernel + Force + Patch + Quirks + + AppleCpuPmCfgLock + AppleXcpmCfgLock + AppleXcpmExtraMsrs + AppleXcpmForceBoost + CustomPciSerialDevice + CustomSMBIOSGuid + DisableIoMapper + DisableLinkeditJettison + DisableRtcChecksum + ExtendBTFeatureFlags + ExternalDiskIcons + ForceAquantiaEthernet + ForceSecureBootScheme + IncreasePciBarSize + LapicKernelPanic + LegacyCommpage + PanicNoKextDump + PowerTimeoutKernelPanic + ProvideCurrentCpuInfo + SetApfsTrimTimeout-1 + ThirdPartyDrives + XhciPortLimit + + SchemeCustomKernelFuzzyMatchKernelArchAutoKernelCacheAuto + + PickerAudioAssistPickerModeExternalPickerVariantAutoPollAppleHotKeysShowPickerTakeoffDelay0Timeout5DebugAppleDebugApplePanicDisableWatchDogDisplayDelay0DisplayLevel2147483650LogModules*SysReportTarget0EntriesSecurityAllowSetDefaultApECID0AuthRestartBlacklistAppleUpdateDmgLoadingSignedEnablePasswordExposeSensitiveData6HaltLevel2147483648PasswordHashPasswordSaltScanPolicy0SecureBootModelDisabledVaultOptionalSerialInitOverrideTools + NVRAMAdd4D1EDE05-38C7-4A6A-9CC6-4BCCA8B38C14DefaultBackgroundColorAAAAAA==UIScaleAQ==7C436110-AB2A-4BBB-A880-FE41995C9F82SystemAudioVolumeRg==boot-args-v keepsyms=1 debug=0x100 alcid=1csr-active-configAAAAAA==prev-lang:kbden-US:0run-efi-updaterNoDelete4D1EDE05-38C7-4A6A-9CC6-4BCCA8B38C14UIScaleDefaultBackgroundColor7C436110-AB2A-4BBB-A880-FE41995C9F82boot-argscsr-active-configLegacyOverwriteLegacySchemaWriteFlash + SystemProductNameiMacPro1,1SystemSerialNumberCHANGEMESystemUUIDCHANGEMEUpdateDataHubUpdateNVRAMUpdateSMBIOSUpdateSMBIOSModeCreateUseRawUuidEncoding + UEFIAPFSEnableJumpstartGlobalConnectHideVerboseJumpstartHotPlugMinDate-1MinVersion-1AppleInputAppleEventBuiltinCustomDelaysGraphicsInputMirroringKeyInitialDelay50KeySubsequentDelay5PointerSpeedDiv1PointerSpeedMul1AudioAudioCodec0AudioDevicePciRoot(0x0)/Pci(0x1b,0x0)AudioOutMask1AudioSupportDisconnectHdaMaximumGain-15MinimumAssistGain-30MinimumAudibleGain-55PlayChimeAutoResetTrafficClassSetupDelay0ConnectDriversDriversHfsPlus.efiOpenRuntime.efiOpenCanopy.efiInputKeyFilteringKeyForgetThreshold5KeySupportKeySupportModeAutoKeySwapPointerSupportPointerSupportModeASUSTimerResolution50000OutputClearScreenOnModeSwitchConsoleModeDirectGopRenderingForceResolutionGopPassThroughDisabledIgnoreTextInGraphicsProvideConsoleGopReconnectGraphicsOnConnectReconnectOnResChangeReplaceTabWithSpaceResolutionMaxSanitiseClearScreenTextRendererBuiltinGraphicsUIScale0UgaPassThroughProtocolOverridesQuirksActivateHpetSupportDisableSecurityPolicyEnableVectorAccelerationEnableVmxExitBootServicesDelay0ForceOcWriteFlashForgeUefiSupportIgnoreInvalidFlexRatioReleaseUsbOwnershipReloadOptionRomsRequestBootVarRoutingResizeGpuBars-1TscSyncTimeout0UnblockFsConnectReservedMemory + + diff --git a/README.md b/README.md index 62d061a9..7e598f63 100644 --- a/README.md +++ b/README.md @@ -1,1956 +1,140 @@ -# Docker-OSX · [Follow @sickcodes on Twitter](https://twitter.com/sickcodes) - -![Running Mac OS X in a Docker container](/running-mac-inside-docker-qemu.png?raw=true "OSX KVM DOCKER") - -Run Mac OS X in Docker with near-native performance! X11 Forwarding! iMessage security research! iPhone USB working! macOS in a Docker container! - -Conduct Security Research on macOS using both Linux & Windows! - -# Docker-OSX now has a Discord server & Telegram! - -The Discord is active on #docker-osx and anyone is welcome to come and ask questions, ideas, etc. - -

- -

- - -### Click to join the Discord server [https://discord.gg/sickchat](https://discord.gg/sickchat) - -### Click to join the Telegram server [https://t.me/sickcodeschat](https://t.me/sickcodeschat) - -Or reach out via Linkedin if it's private: [https://www.linkedin.com/in/sickcodes](https://www.linkedin.com/in/sickcodes) - -Or via [https://sick.codes/contact/](https://sick.codes/contact/) - -## Author - -This project is maintained by [Sick.Codes](https://sick.codes/). [(Twitter)](https://twitter.com/sickcodes) - -Additional credits can be found here: https://github.com/sickcodes/Docker-OSX/blob/master/CREDITS.md - -Additionally, comprehensive list of all contributors can be found here: https://github.com/sickcodes/Docker-OSX/graphs/contributors - -Big thanks to [@kholia](https://twitter.com/kholia) for maintaining the upstream project, which Docker-OSX is built on top of: [OSX-KVM](https://github.com/kholia/OSX-KVM). - -Also special thanks to [@thenickdude](https://github.com/thenickdude) who maintains the valuable fork [KVM-OpenCore](https://github.com/thenickdude/KVM-Opencore), which was started by [@Leoyzen](https://github.com/Leoyzen/)! - -Extra special thanks to the OpenCore team over at: https://github.com/acidanthera/OpenCorePkg. Their well-maintained bootloader provides much of the great functionality that Docker-OSX users enjoy :) - -If you like this project, consider contributing here or upstream! - -## Quick Start Docker-OSX - -Video setup tutorial is also available here: https://www.youtube.com/watch?v=wLezYl77Ll8 - -**Windows users:** [click here to see the notes below](#id-like-to-run-docker-osx-on-windows)! - -

- -

- -First time here? try [initial setup](#initial-setup), otherwise try the instructions below to use either Catalina or Big Sur. - -## Any questions, ideas, or just want to hang out? -# [https://discord.gg/sickchat](https://discord.gg/sickchat) - -Release names and their version: - -### Catalina (10.15) [![https://img.shields.io/docker/image-size/sickcodes/docker-osx/latest?label=sickcodes%2Fdocker-osx%3Alatest](https://img.shields.io/docker/image-size/sickcodes/docker-osx/latest?label=sickcodes%2Fdocker-osx%3Alatest)](https://hub.docker.com/r/sickcodes/docker-osx/tags?page=1&ordering=last_updated) - -```bash -docker run -it \ - --device /dev/kvm \ - -p 50922:10022 \ - -v /tmp/.X11-unix:/tmp/.X11-unix \ - -e "DISPLAY=${DISPLAY:-:0.0}" \ - -e SHORTNAME=catalina \ - sickcodes/docker-osx:latest - -# docker build -t docker-osx . -``` -### Big Sur (11) [![https://img.shields.io/docker/image-size/sickcodes/docker-osx/latest?label=sickcodes%2Fdocker-osx%3Alatest](https://img.shields.io/docker/image-size/sickcodes/docker-osx/latest?label=sickcodes%2Fdocker-osx%3Alatest)](https://hub.docker.com/r/sickcodes/docker-osx/tags?page=1&ordering=last_updated) - -```bash -docker run -it \ - --device /dev/kvm \ - -p 50922:10022 \ - -v /tmp/.X11-unix:/tmp/.X11-unix \ - -e "DISPLAY=${DISPLAY:-:0.0}" \ - -e SHORTNAME=big-sur \ - sickcodes/docker-osx:latest - -# docker build -t docker-osx . -``` - -### Monterey (12) [![https://img.shields.io/docker/image-size/sickcodes/docker-osx/latest?label=sickcodes%2Fdocker-osx%3Alatest](https://img.shields.io/docker/image-size/sickcodes/docker-osx/latest?label=sickcodes%2Fdocker-osx%3Alatest)](https://hub.docker.com/r/sickcodes/docker-osx/tags?page=1&ordering=last_updated) - -```bash - -docker run -it \ - --device /dev/kvm \ - -p 50922:10022 \ - -v /tmp/.X11-unix:/tmp/.X11-unix \ - -e "DISPLAY=${DISPLAY:-:0.0}" \ - -e GENERATE_UNIQUE=true \ - -e MASTER_PLIST_URL='https://raw.githubusercontent.com/sickcodes/osx-serial-generator/master/config-custom.plist' \ - -e SHORTNAME=monterey \ - sickcodes/docker-osx:latest - -# docker build -t docker-osx . -``` - -### Ventura (13) [![https://img.shields.io/docker/image-size/sickcodes/docker-osx/latest?label=sickcodes%2Fdocker-osx%3Alatest](https://img.shields.io/docker/image-size/sickcodes/docker-osx/latest?label=sickcodes%2Fdocker-osx%3Alatest)](https://hub.docker.com/r/sickcodes/docker-osx/tags?page=1&ordering=last_updated) - -```bash - -docker run -it \ - --device /dev/kvm \ - -p 50922:10022 \ - -v /tmp/.X11-unix:/tmp/.X11-unix \ - -e "DISPLAY=${DISPLAY:-:0.0}" \ - -e GENERATE_UNIQUE=true \ - -e MASTER_PLIST_URL='https://raw.githubusercontent.com/sickcodes/osx-serial-generator/master/config-custom.plist' \ - -e SHORTNAME=ventura \ - sickcodes/docker-osx:latest - -# docker build -t docker-osx . -``` - -### Sonoma (14) [![https://img.shields.io/docker/image-size/sickcodes/docker-osx/latest?label=sickcodes%2Fdocker-osx%3Alatest](https://img.shields.io/docker/image-size/sickcodes/docker-osx/latest?label=sickcodes%2Fdocker-osx%3Alatest)](https://hub.docker.com/r/sickcodes/docker-osx/tags?page=1&ordering=last_updated) - -```bash - -docker run -it \ - --device /dev/kvm \ - -p 50922:10022 \ - -v /tmp/.X11-unix:/tmp/.X11-unix \ - -e "DISPLAY=${DISPLAY:-:0.0}" \ - -e GENERATE_UNIQUE=true \ - -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' \ - -e SHORTNAME=sonoma \ - sickcodes/docker-osx:latest - -# docker build -t docker-osx . -``` - -### Sequoia (15) [![https://img.shields.io/docker/image-size/sickcodes/docker-osx/latest?label=sickcodes%2Fdocker-osx%3Alatest](https://img.shields.io/docker/image-size/sickcodes/docker-osx/latest?label=sickcodes%2Fdocker-osx%3Alatest)](https://hub.docker.com/r/sickcodes/docker-osx/tags?page=1&ordering=last_updated) - -```bash - -docker run -it \ - --device /dev/kvm \ - -p 50922:10022 \ - -v /tmp/.X11-unix:/tmp/.X11-unix \ - -e "DISPLAY=${DISPLAY:-:0.0}" \ - -e GENERATE_UNIQUE=true \ - -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' \ - -e SHORTNAME=sequoia \ - sickcodes/docker-osx:latest - -# docker build -t docker-osx . -``` - - - -### Older Systems - -### High Sierra [![https://img.shields.io/docker/image-size/sickcodes/docker-osx/latest?label=sickcodes%2Fdocker-osx%3Alatest](https://img.shields.io/docker/image-size/sickcodes/docker-osx/latest?label=sickcodes%2Fdocker-osx%3Alatest)](https://hub.docker.com/r/sickcodes/docker-osx/tags?page=1&ordering=last_updated) - -```bash - -docker run -it \ - --device /dev/kvm \ - -p 50922:10022 \ - -v /tmp/.X11-unix:/tmp/.X11-unix \ - -e "DISPLAY=${DISPLAY:-:0.0}" \ - -e SHORTNAME=high-sierra \ - sickcodes/docker-osx:latest - -# docker build -t docker-osx . -``` - -### Mojave [![https://img.shields.io/docker/image-size/sickcodes/docker-osx/latest?label=sickcodes%2Fdocker-osx%3Alatest](https://img.shields.io/docker/image-size/sickcodes/docker-osx/latest?label=sickcodes%2Fdocker-osx%3Alatest)](https://hub.docker.com/r/sickcodes/docker-osx/tags?page=1&ordering=last_updated) - -```bash - -docker run -it \ - --device /dev/kvm \ - -p 50922:10022 \ - -v /tmp/.X11-unix:/tmp/.X11-unix \ - -e "DISPLAY=${DISPLAY:-:0.0}" \ - -e SHORTNAME=mojave \ - sickcodes/docker-osx:latest - -# docker build -t docker-osx . -``` - - - -#### Download the image manually and use it in Docker - -[![https://img.shields.io/docker/image-size/sickcodes/docker-osx/naked?label=sickcodes%2Fdocker-osx%3Anaked](https://img.shields.io/docker/image-size/sickcodes/docker-osx/naked?label=sickcodes%2Fdocker-osx%3Anaked)](https://hub.docker.com/r/sickcodes/docker-osx/tags?page=1&ordering=last_updated) - - -This is a particularly good way for downloading the container, in case Docker's CDN (or your connection) happens to be slow. - -```bash -wget https://images2.sick.codes/mac_hdd_ng_auto.img - -docker run -it \ - --device /dev/kvm \ - -p 50922:10022 \ - -v "${PWD}/mac_hdd_ng_auto.img:/image" \ - -v /tmp/.X11-unix:/tmp/.X11-unix \ - -e "DISPLAY=${DISPLAY:-:0.0}" \ - -e GENERATE_UNIQUE=true \ - -e MASTER_PLIST_URL=https://raw.githubusercontent.com/sickcodes/Docker-OSX/master/custom/config-nopicker-custom.plist \ - -e SHORTNAME=catalina \ - sickcodes/docker-osx:naked -``` - - - - -# Share directories, sharing files, shared folder, mount folder -The easiest and most secure way is `sshfs` -```bash -# on Linux/Windows -mkdir ~/mnt/osx -sshfs user@localhost:/ -p 50922 ~/mnt/osx -# wait a few seconds, and ~/mnt/osx will have full rootfs mounted over ssh, and in userspace -# automated: sshpass -p sshfs user@localhost:/ -p 50922 ~/mnt/osx -``` - - -# (VFIO) iPhone USB passthrough (VFIO) - -If you have a laptop see the next usbfluxd section. - -If you have a desktop PC, you can use [@Silfalion](https://github.com/Silfalion)'s instructions: [https://github.com/Silfalion/Iphone_docker_osx_passthrough](https://github.com/Silfalion/Iphone_docker_osx_passthrough) - -# (USBFLUXD) iPhone USB -> Network style passthrough OSX-KVM Docker-OSX - -Video setup tutorial for usbfluxd is also available here: https://www.youtube.com/watch?v=kTk5fGjK_PM - -

- iPhone USB passthrough on macOS virtual machine Linux & Windows -

- - -This method WORKS on laptop, PC, anything! - -Thank you [@nikias](https://github.com/nikias) for [usbfluxd](https://github.com/corellium/usbfluxd) via [https://github.com/corellium](https://github.com/corellium)! - -**This is done inside Linux.** - -Open 3 terminals on Linux - -Connecting your device over USB on Linux allows you to expose `usbmuxd` on port `5000` using [https://github.com/corellium/usbfluxd](https://github.com/corellium/usbfluxd) to another system on the same network. - -Ensure `usbmuxd`, `socat` and `usbfluxd` are installed. - -`sudo pacman -S libusbmuxd usbmuxd avahi socat` - -Available on the AUR: [https://aur.archlinux.org/packages/usbfluxd/](https://aur.archlinux.org/packages/usbfluxd/) - -`yay usbfluxd` - -Plug in your iPhone or iPad. - -Terminal 1 -```bash -sudo systemctl start usbmuxd -sudo avahi-daemon -``` - -Terminal 2: -```bash -# on host -sudo systemctl restart usbmuxd -sudo socat tcp-listen:5000,fork unix-connect:/var/run/usbmuxd -``` - -Terminal 3: -```bash -sudo usbfluxd -f -n -``` - -### Connect to a host running usbfluxd - -**This is done inside macOS.** - -Install homebrew. - -`172.17.0.1` is usually the Docker bridge IP, which is your PC, but you can use any IP from `ip addr`... - -macOS Terminal: -```zsh -# on the guest -brew install make automake autoconf libtool pkg-config gcc libimobiledevice usbmuxd - -git clone https://github.com/corellium/usbfluxd.git -cd usbfluxd - -./autogen.sh -make -sudo make install -``` - -Accept the USB over TCP connection, and appear as local: - -(you may need to change `172.17.0.1` to the IP address of the host. e.g. check `ip addr`) - -```bash -# on the guest -sudo launchctl start usbmuxd -export PATH=/usr/local/sbin:${PATH} -sudo usbfluxd -f -r 172.17.0.1:5000 -``` - -Close apps such as Xcode and reopen them and your device should appear! - -*If you need to start again on Linux, wipe the current usbfluxd, usbmuxd, and socat:* -```bash -sudo killall usbfluxd -sudo systemctl restart usbmuxd -sudo killall socat -``` - -## Make container FASTER using [https://github.com/sickcodes/osx-optimizer](https://github.com/sickcodes/osx-optimizer) - -SEE commands in [https://github.com/sickcodes/osx-optimizer](https://github.com/sickcodes/osx-optimizer)! - -- Skip the GUI login screen (at your own risk!) -- Disable spotlight indexing on macOS to heavily speed up Virtual Instances. -- Disable heavy login screen wallpaper -- Disable updates (at your own risk!) - -## Increase disk space by moving /var/lib/docker to external drive, block storage, NFS, or any other location conceivable. - -Move /var/lib/docker, following the tutorial below - -- Cheap large physical disk storage instead using your server's disk, or SSD. -- Block Storage, NFS, etc. - -Tutorial here: https://sick.codes/how-to-run-docker-from-block-storage/ - -Only follow the above tutorial if you are happy with wiping all your current Docker images/layers. - -Safe mode: Disable docker temporarily so you can move the Docker folder temporarily. - -- Do NOT do this until you have moved your image out already [https://github.com/dulatello08/Docker-OSX/#quick-start-your-own-image-naked-container-image](https://github.com/dulatello08/Docker-OSX/#quick-start-your-own-image-naked-container-image) - -```bash -killall dockerd -systemctl disable --now docker -systemctl disable --now docker.socket -systemctl stop docker -systemctl stop docker.socket -``` -Now, that Docker daemon is off, move /var/lib/docker somewhere - -Then, symbolicly link /var/lib/docker somewhere: - -```bash -mv /var/lib/docker /run/media/user/some_drive/docker -ln -s /run/media/user/some_drive/docker /var/lib/docker - -# now check if /var/lib/docker is working still -ls /var/lib/docker -``` -If you see folders, then it worked. You can restart Docker, or just reboot if you want to be sure. - -## Important notices: - -**2021-11-14** - Added High Sierra, Mojave - -Pick one of these while **building**, irrelevant when using docker pull: -``` ---build-arg SHORTNAME=high-sierra ---build-arg SHORTNAME=mojave ---build-arg SHORTNAME=catalina ---build-arg SHORTNAME=big-sur ---build-arg SHORTNAME=monterey ---build-arg SHORTNAME=ventura ---build-arg SHORTNAME=sonoma -``` - - -## Technical details - -There are currently multiple images, each with different use cases (explained [below](#container-images)): - -- High Sierra (10.13) -- Mojave (10.14) -- Catalina (10.15) -- Big Sur (11) -- Monterey (12) -- Ventura (13) -- Sonoma (14) -- Auto (pre-made Catalina) -- Naked (use your own .img) -- Naked-Auto (user your own .img and SSH in) - -High Sierra: - -[![https://img.shields.io/docker/image-size/sickcodes/docker-osx/high-sierra?label=sickcodes%2Fdocker-osx%3Ahigh-sierra](https://img.shields.io/docker/image-size/sickcodes/docker-osx/high-sierra?label=sickcodes%2Fdocker-osx%3Ahigh-sierra)](https://hub.docker.com/r/sickcodes/docker-osx/tags?page=1&ordering=last_updated) - -Mojave: - -[![https://img.shields.io/docker/image-size/sickcodes/docker-osx/mojave?label=sickcodes%2Fdocker-osx%3Amojave](https://img.shields.io/docker/image-size/sickcodes/docker-osx/mojave?label=sickcodes%2Fdocker-osx%3Amojave)](https://hub.docker.com/r/sickcodes/docker-osx/tags?page=1&ordering=last_updated) - -Catalina: - -[![https://img.shields.io/docker/image-size/sickcodes/docker-osx/latest?label=sickcodes%2Fdocker-osx%3Alatest](https://img.shields.io/docker/image-size/sickcodes/docker-osx/latest?label=sickcodes%2Fdocker-osx%3Alatest)](https://hub.docker.com/r/sickcodes/docker-osx/tags?page=1&ordering=last_updated) - -Big-Sur: - -[![https://img.shields.io/docker/image-size/sickcodes/docker-osx/big-sur?label=sickcodes%2Fdocker-osx%3Abig-sur](https://img.shields.io/docker/image-size/sickcodes/docker-osx/big-sur?label=sickcodes%2Fdocker-osx%3Abig-sur)](https://hub.docker.com/r/sickcodes/docker-osx/tags?page=1&ordering=last_updated) - -Monterey make your own image: - -[![https://img.shields.io/docker/image-size/sickcodes/docker-osx/monterey?label=sickcodes%2Fdocker-osx%3Amonterey](https://img.shields.io/docker/image-size/sickcodes/docker-osx/monterey?label=sickcodes%2Fdocker-osx%3Amonterey)](https://hub.docker.com/r/sickcodes/docker-osx/tags?page=1&ordering=last_updated) - -Ventura make your own image: - -[![https://img.shields.io/docker/image-size/sickcodes/docker-osx/ventura?label=sickcodes%2Fdocker-osx%3Aventura](https://img.shields.io/docker/image-size/sickcodes/docker-osx/ventura?label=sickcodes%2Fdocker-osx%3Aventura)](https://hub.docker.com/r/sickcodes/docker-osx/tags?page=1&ordering=last_updated) - -Sonoma make your own image: - -[![https://img.shields.io/docker/image-size/sickcodes/docker-osx/sonoma?label=sickcodes%2Fdocker-osx%3Asonoma](https://img.shields.io/docker/image-size/sickcodes/docker-osx/sonoma?label=sickcodes%2Fdocker-osx%3Asonoma)](https://hub.docker.com/r/sickcodes/docker-osx/tags?page=1&ordering=last_updated) - -Pre-made **Catalina** system by [Sick.Codes](https://sick.codes): username: `user`, password: `alpine` - -[![https://img.shields.io/docker/image-size/sickcodes/docker-osx/auto?label=sickcodes%2Fdocker-osx%3Aauto](https://img.shields.io/docker/image-size/sickcodes/docker-osx/auto?label=sickcodes%2Fdocker-osx%3Aauto)](https://hub.docker.com/r/sickcodes/docker-osx/tags?page=1&ordering=last_updated) - -Naked: Bring-your-own-image setup (use any of the above first): - -[![https://img.shields.io/docker/image-size/sickcodes/docker-osx/naked?label=sickcodes%2Fdocker-osx%3Anaked](https://img.shields.io/docker/image-size/sickcodes/docker-osx/naked?label=sickcodes%2Fdocker-osx%3Anaked)](https://hub.docker.com/r/sickcodes/docker-osx/tags?page=1&ordering=last_updated) - -Naked Auto: same as above but with `-e USERNAME` & `-e PASSWORD` and `-e OSX_COMMANDS="put your commands here"` - -[![https://img.shields.io/docker/image-size/sickcodes/docker-osx/naked-auto?label=sickcodes%2Fdocker-osx%3Anaked-auto](https://img.shields.io/docker/image-size/sickcodes/docker-osx/naked-auto?label=sickcodes%2Fdocker-osx%3Anaked-auto)](https://hub.docker.com/r/sickcodes/docker-osx/tags?page=1&ordering=last_updated) - -## Capabilities -- use iPhone OSX KVM on Linux using [usbfluxd](https://github.com/corellium/usbfluxd)! -- macOS Monterey VM on Linux! -- Folder sharing- -- USB passthrough (hotplug too) -- SSH enabled (`localhost:50922`) -- VNC enabled (`localhost:8888`) if using ./vnc version -- iMessage security research via [serial number generator!](https://github.com/sickcodes/osx-serial-generator) -- X11 forwarding is enabled -- runs on top of QEMU + KVM -- supports Big Sur, custom images, Xvfb headless mode -- you can clone your container with `docker commit` - -### Requirements - -- 20GB+++ disk space for bare minimum installation (50GB if using Xcode) -- virtualization should be enabled in your BIOS settings -- a x86_64 kvm-capable host -- at least 50 GBs for `:auto` (half for the base image, half for your runtime image - -### TODO - -- documentation for security researchers -- gpu acceleration -- support for virt-manager - -## Docker - -Images built on top of the contents of this repository are also available on **Docker Hub** for convenience: https://hub.docker.com/r/sickcodes/docker-osx - -A comprehensive list of the available Docker images and their intended purpose can be found in the [Instructions](#instructions). - -## Kubernetes - -Docker-OSX supports Kubernetes. - -Kubernetes Helm Chart & Documentation can be found under the [helm directory](helm/README.md). - -Thanks [cephasara](https://github.com/cephasara) for contributing this major contribution. - -[![Artifact HUB](https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/docker-osx)](https://artifacthub.io/packages/search?repo=docker-osx) - -## Support - -### Small questions & issues - -Feel free to open an [issue](https://github.com/sickcodes/Docker-OSX/issues/new/choose), should you come across minor issues with running Docker-OSX or have any questions. - -#### Resolved issues - -Before you open an issue, however, please check the [closed issues](https://github.com/sickcodes/Docker-OSX/issues?q=is%3Aissue+is%3Aclosed) and confirm that you're using the latest version of this repository — your issues may have already been resolved! You might also see your answer in our questions and answers section [below](#more-questions-and-answers). - -### Feature requests and updates - -Follow [@sickcodes](https://twitter.com/sickcodes)! - -### Professional support - -For more sophisticated endeavours, we offer the following support services: - -- Enterprise support, business support, or casual support. -- Custom images, custom scripts, consulting (per hour available!) -- One-on-one conversations with you or your development team. - -In case you're interested, contact [@sickcodes on Twitter](https://twitter.com/sickcodes) or click [here](https://sick.codes/contact). - -## License/Contributing - -Docker-OSX is licensed under the [GPL v3+](LICENSE). Contributions are welcomed and immensely appreciated. You are in fact permitted to use Docker-OSX as a tool to create proprietary software. - -### Other cool Docker/QEMU based projects -- [Run Android in a Docker Container with Dock Droid](https://github.com/sickcodes/dock-droid) -- [Run Android fully native on the host!](https://github.com/sickcodes/droid-native) -- [Run iOS 12 in a Docker container with Docker-eyeOS](https://github.com/sickcodes/Docker-eyeOS) - [https://github.com/sickcodes/Docker-eyeOS](https://github.com/sickcodes/Docker-eyeOS) -- [Run iMessage relayer in Docker with Bluebubbles.app](https://bluebubbles.app/) - [Getting started wiki](https://github.com/BlueBubblesApp/BlueBubbles-Server/wiki/Running-via-Docker) - -## Disclaimer - -If you are serious about Apple Security, and possibly finding 6-figure bug bounties within the Apple Bug Bounty Program, then you're in the right place! Further notes: [Is Hackintosh, OSX-KVM, or Docker-OSX legal?](https://sick.codes/is-hackintosh-osx-kvm-or-docker-osx-legal/) - -Product names, logos, brands and other trademarks referred to within this project are the property of their respective trademark holders. These trademark holders are not affiliated with our repository in any capacity. They do not sponsor or endorse this project in any way. - -# Instructions - -## Container images - -### Already set up or just looking to make a container quickly? Check out our [quick start](#quick-start-docker-osx) or see a bunch more use cases under our [container creation examples](#container-creation-examples) section. - -There are several different Docker-OSX images available that are suitable for different purposes. - -- `sickcodes/docker-osx:latest` - [I just want to try it out.](#quick-start-docker-osx) -- `sickcodes/docker-osx:latest` - [I want to use Docker-OSX to develop/secure apps in Xcode (sign into Xcode, Transporter)](#quick-start-your-own-image-naked-container-image) -- `sickcodes/docker-osx:naked` - [I want to use Docker-OSX for CI/CD-related purposes (sign into Xcode, Transporter)](#building-a-headless-container-from-a-custom-image) - -Create your personal image using `:latest` or `big-sur`. Then, pull the image out the image. Afterwards, you will be able to duplicate that image and import it to the `:naked` container, in order to revert the container to a previous state repeatedly. - -- `sickcodes/docker-osx:auto` - [I'm only interested in using the command line (useful for compiling software or using Homebrew headlessly).](#prebuilt-image-with-arbitrary-command-line-arguments) -- `sickcodes/docker-osx:naked` - [I need iMessage/iCloud for security research.](#generating-serial-numbers) -- `sickcodes/docker-osx:big-sur` - [I want to run Big Sur.](#quick-start-docker-osx) -- `sickcodes/docker-osx:monterey` - [I want to run Monterey.](#quick-start-docker-osx) -- `sickcodes/docker-osx:ventura` - [I want to run Ventura.](#quick-start-docker-osx) -- `sickcodes/docker-osx:sonoma` - [I want to run Sonoma.](#quick-start-docker-osx) - -- `sickcodes/docker-osx:high-sierra` - I want to run High Sierra. -- `sickcodes/docker-osx:mojave` - I want to run Mojave. - -## Initial setup -Before you do anything else, you will need to turn on hardware virtualization in your BIOS. Precisely how will depend on your particular machine (and BIOS), but it should be straightforward. - -Then, you'll need QEMU and some other dependencies on your host: - -```bash -# ARCH -sudo pacman -S qemu libvirt dnsmasq virt-manager bridge-utils flex bison iptables-nft edk2-ovmf - -# UBUNTU DEBIAN -sudo apt install qemu qemu-kvm libvirt-clients libvirt-daemon-system bridge-utils virt-manager libguestfs-tools - -# CENTOS RHEL FEDORA -sudo yum install libvirt qemu-kvm -``` - -Then, enable libvirt and load the KVM kernel module: - -```bash -sudo systemctl enable --now libvirtd -sudo systemctl enable --now virtlogd - -echo 1 | sudo tee /sys/module/kvm/parameters/ignore_msrs - -sudo modprobe kvm -``` - -### I'd like to run Docker-OSX on Windows - -Running Docker-OSX on Windows is possible using WSL2 (Windows 11 + Windows Subsystem for Linux). - -You must have Windows 11 installed with build 22000+ (21H2 or higher). - -First, install WSL on your computer by running this command in an administrator powershell. For more info, look [here](https://docs.microsoft.com/en-us/windows/wsl/install). - -This will install Ubuntu by default. -``` -wsl --install -``` - - You can confirm WSL2 is enabled using `wsl -l -v` in PowerShell. To see other distributions that are available, use `wsl -l -o`. - -If you have previously installed WSL1, upgrade to WSL 2. Check [this link to upgrade from WSL1 to WSL2](https://docs.microsoft.com/en-us/windows/wsl/install#upgrade-version-from-wsl-1-to-wsl-2). - -After WSL installation, go to `C:/Users//.wslconfig` and add `nestedVirtualization=true` to the end of the file (If the file doesn't exist, create it). For more information about the `.wslconfig` file check [this link](https://docs.microsoft.com/en-us/windows/wsl/wsl-config#wslconfig). Verify that you have selected "Show Hidden Files" and "Show File Extensions" in File Explorer options. -The result should be like this: -``` -[wsl2] -nestedVirtualization=true -``` - -Go into your WSL distro (Run `wsl` in powershell) and check if KVM is enabled by using the `kvm-ok` command. The output should look like this: - -``` -INFO: /dev/kvm exists -KVM acceleration can be used -``` - -Use the command `sudo apt -y install bridge-utils cpu-checker libvirt-clients libvirt-daemon qemu qemu-kvm` to install it if it isn't. - -Now download and install [Docker for Windows](https://docs.docker.com/desktop/windows/install/) if it is not already installed. - -After installation, go into Settings and check these 2 boxes: - -``` -General -> "Use the WSL2 based engine"; -Resources -> WSL Integration -> "Enable integration with my default WSL distro", -``` - -Ensure `x11-apps` is installed. Use the command `sudo apt install x11-apps -y` to install it if it isn't. - -Finally, there are 3 ways to get video output: - -- WSLg: This is the simplest and easiest option to use. There may be some issues such as the keyboard not being fully passed through or seeing a second mouse on the desktop - [Issue on WSLg](https://github.com/microsoft/wslg/issues/376) - but this option is recommended. - -To use WSLg's built-in X-11 server, change these two lines in the docker run command to point Docker-OSX to WSLg. - -``` --e "DISPLAY=${DISPLAY:-:0.0}" \ --v /mnt/wslg/.X11-unix:/tmp/.X11-unix \ -``` -Or try: - -``` --e "DISPLAY=${DISPLAY:-:0}" \ --v /mnt/wslg/.X11-unix:/tmp/.X11-unix \ -``` - -For Ubuntu 20.x on Windows, see [https://github.com/sickcodes/Docker-OSX/discussions/458](https://github.com/sickcodes/Docker-OSX/discussions/458) - -- VNC: See the [VNC section](#building-a-headless-container-which-allows-insecure-vnc-on-localhost-for-local-use-only) for more information. You could also add -vnc argument to qemu. Connect to your mac VM via a VNC Client. [Here is a how to](https://wiki.archlinux.org/title/QEMU#VNC) -- Desktop Environment: This will give you a full desktop linux experience but it will use a bit more of the computer's resources. Here is an example guide, but there are other guides that help set up a desktop environment. [DE Example](https://www.makeuseof.com/tag/linux-desktop-windows-subsystem/) - -## Additional boot instructions for when you are [creating your container](#container-creation-examples) - -- Boot the macOS Base System (Press Enter) - -- Click `Disk Utility` - -- Erase the BIGGEST disk (around 200gb default), DO NOT MODIFY THE SMALLER DISKS. --- if you can't click `erase`, you may need to reduce the disk size by 1kb - -- (optional) Create a partition using the unused space to house the OS and your files if you want to limit the capacity. (For Xcode 12 partition at least 60gb.) - -- Click `Reinstall macOS` - -- The system may require multiple reboots during installation - -## Troubleshooting - -### Routine checks - -This is a great place to start if you are having trouble getting going, especially if you're not that familiar with Docker just yet. - -Just looking to make a container quickly? Check out our [container creation examples](#container-creation-examples) section. - -More specific/advanced troubleshooting questions and answers may be found in [More Questions and Answers](#more-questions-and-answers). You should also check out the [closed issues](https://github.com/sickcodes/Docker-OSX/issues?q=is%3Aissue+is%3Aclosed). Someone else might have gotten a question like yours answered already even if you can't find it in this document! - -#### Confirm that your CPU supports virtualization - -See [initial setup](#initial-setup). - - - -#### Docker Unknown Server OS error - -```console -docker: unknown server OS: . -See 'docker run --help'. -``` - -This means your docker daemon is not running. - -`pgrep dockerd` should return nothing - -Therefore, you have a few choices. - -`sudo dockerd` for foreground Docker usage. I use this. - -Or - -`sudo systemctl --start dockerd` to start dockerd this now. - -Or - -`sudo systemctl --enable --now dockerd` for start dockerd on every reboot, and now. - - -#### Use more CPU Cores/SMP - -Examples: - -`-e EXTRA='-smp 6,sockets=3,cores=2'` - -`-e EXTRA='-smp 8,sockets=4,cores=2'` - -`-e EXTRA='-smp 16,sockets=8,cores=2'` - -Note, unlike memory, CPU usage is shared. so you can allocate all of your CPU's to the container. - -### Confirm your user is part of the Docker group, KVM group, libvirt group - -#### Add yourself to the Docker group - -If you use `sudo dockerd` or dockerd is controlled by systemd/systemctl, then you must be in the Docker group. -If you are not in the Docker group: - -```bash -sudo usermod -aG docker "${USER}" -``` -and also add yourself to the kvm and libvirt groups if needed: - -```bash -sudo usermod -aG libvirt "${USER}" -sudo usermod -aG kvm "${USER}" -``` - -See also: [initial setup](#initial-setup). - -#### Is the docker daemon enabled? - -```bash -# run ad hoc -sudo dockerd - -# or daemonize it -sudo nohup dockerd & - -# enable it in systemd (it will persist across reboots this way) -sudo systemctl enable --now docker - -# or just start it as your user with systemd instead of enabling it -systemctl start docker -``` - -## More Questions and Answers - -Big thank you to our contributors who have worked out almost every conceivable issue so far! - -[https://github.com/sickcodes/Docker-OSX/blob/master/CREDITS.md](https://github.com/sickcodes/Docker-OSX/blob/master/CREDITS.md) - - -### Start the same container later (persistent disk) - -Created a container with `docker run` and want to reuse the underlying image again later? - -NB: see [container creation examples](#container-creation-examples) first for how to get to the point where this is applicable. - -This is for when you want to run the SAME container again later. You may need to use `docker commit` to save your container before you can reuse it. Check if your container is persisted with `docker ps --all`. - -If you don't run this you will have a new image every time. - -```bash -# look at your recent containers and copy the CONTAINER ID -docker ps --all - -# docker start the container ID -docker start -ai abc123xyz567 - -# if you have many containers, you can try automate it with filters like this -# docker ps --all --filter "ancestor=sickcodes/docker-osx" -# for locally tagged/built containers -# docker ps --all --filter "ancestor=docker-osx" - -``` - -You can also pull the `.img` file out of the container, which is stored in `/var/lib/docker`, and supply it as a runtime argument to the `:naked` Docker image. - -See also: [here](https://github.com/sickcodes/Docker-OSX/issues/197). - -### I have used Docker-OSX before and want to restart a container that starts automatically - -Containers that use `sickcodes/docker-osx:auto` can be stopped while being started. - -```bash -# find last container -docker ps -a - -# docker start old container with -i for interactive, -a for attach STDIN/STDOUT -docker start -ai -i -``` - -### LibGTK errors "connection refused" - -You may see one or more libgtk-related errors if you do not have everything set up for hardware virtualisation yet. If you have not yet done so, check out the [initial setup](#initial-setup) section and the [routine checks](#routine-checks) section as you may have missed a setup step or may not have all the needed Docker dependencies ready to go. - -See also: [here](https://github.com/sickcodes/Docker-OSX/issues/174). - -#### Permissions denied error - -If you have not yet set up xhost, try the following: - -```bash -echo $DISPLAY - -# ARCH -sudo pacman -S xorg-xhost - -# UBUNTU DEBIAN -sudo apt install x11-xserver-utils - -# CENTOS RHEL FEDORA -sudo yum install xorg-x11-server-utils - -# then run -xhost + - -``` - -### RAM over-allocation -You cannot allocate more RAM than your machine has. The default is 3 Gigabytes: `-e RAM=3`. - -If you are trying to allocate more RAM to the container than you currently have available, you may see an error like the following: `cannot set up guest memory 'pc.ram': Cannot allocate memory`. See also: [here](https://github.com/sickcodes/Docker-OSX/issues/188), [here](https://github.com/sickcodes/Docker-OSX/pull/189). - -For example (below) the `buff/cache` already contains 20 Gigabytes of allocated RAM: - -```console -[user@hostname ~]$ free -mh - total used free shared buff/cache available -Mem: 30Gi 3.5Gi 7.0Gi 728Mi 20Gi 26Gi -Swap: 11Gi 0B 11Gi -``` - -Clear the buffer and the cache: - -```bash -sudo tee /proc/sys/vm/drop_caches <<< 3 -``` - -Now check the RAM again: - -```console -[user@hostname ~]$ free -mh - total used free shared buff/cache available -Mem: 30Gi 3.3Gi 26Gi 697Mi 1.5Gi 26Gi -Swap: 11Gi 0B 11Gi -``` - -### PulseAudio - -#### Use PulseAudio for sound - -Note: [AppleALC](https://github.com/acidanthera/AppleALC), [`alcid`](https://dortania.github.io/OpenCore-Post-Install/universal/audio.html) and [VoodooHDA-OC](https://github.com/chris1111/VoodooHDA-OC) do not have [codec support](https://osy.gitbook.io/hac-mini-guide/details/hda-fix#hda-codec). However, [IORegistryExplorer](https://github.com/vulgo/IORegistryExplorer) does show the controller component working. - -```bash -docker run \ - --device /dev/kvm \ - -e AUDIO_DRIVER=pa,server=unix:/tmp/pulseaudio.socket \ - -v "/run/user/$(id -u)/pulse/native:/tmp/pulseaudio.socket" \ - -v /tmp/.X11-unix:/tmp/.X11-unix \ - sickcodes/docker-osx -``` - -#### PulseAudio debugging - -```bash -docker run \ - --device /dev/kvm \ - -e AUDIO_DRIVER=pa,server=unix:/tmp/pulseaudio.socket \ - -v "/run/user/$(id -u)/pulse/native:/tmp/pulseaudio.socket" \ - -v /tmp/.X11-unix:/tmp/.X11-unix \ - -e PULSE_SERVER=unix:/tmp/pulseaudio.socket \ - sickcodes/docker-osx pactl list -``` - -#### PulseAudio with WSLg - -```bash -docker run \ - --device /dev/kvm \ - -e AUDIO_DRIVER=pa,server=unix:/tmp/pulseaudio.socket \ - -v /mnt/wslg/runtime-dir/pulse/native:/tmp/pulseaudio.socket \ - -v /mnt/wslg/.X11-unix:/tmp/.X11-unix \ - sickcodes/docker-osx -``` - -### Forward additional ports (nginx hosting example) - -It's possible to forward additional ports depending on your needs. In this example, we'll use Mac OSX to host nginx: - -``` -host:10023 <-> 10023:container:10023 <-> 80:guest -``` - -On the host machine, run: - -```bash -docker run -it \ - --device /dev/kvm \ - -p 50922:10022 \ - -e ADDITIONAL_PORTS='hostfwd=tcp::10023-:80,' \ - -p 10023:10023 \ - sickcodes/docker-osx:auto -``` - -In a Terminal session running the container, run: - -```bash -/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" - -brew install nginx -sudo sed -i -e 's/8080/80/' /usr/local/etc/nginx/nginx.confcd -# sudo nginx -s stop -sudo nginx -``` - -**nginx should now be reachable on port 10023.** - -Additionally, you can string multiple statements together, for example: - -```bash - -e ADDITIONAL_PORTS='hostfwd=tcp::10023-:80,hostfwd=tcp::10043-:443,' - -p 10023:10023 \ - -p 10043:10043 \ -``` - -### Bridged networking - -You might not need to do anything with the default setup to enable internet connectivity from inside the container. Additionally, `curl` may work even if `ping` doesn't. - -See discussion [here](https://github.com/sickcodes/Docker-OSX/issues/177) and [here](https://github.com/sickcodes/Docker-OSX/issues/72) and [here](https://github.com/sickcodes/Docker-OSX/issues/88). - -### Enable IPv4 forwarding for bridged network connections for remote installations - -This is not required for LOCAL installations. - -Additionally note it may [cause the host to leak your IP, even if you're using a VPN in the container](https://sick.codes/cve-2020-15590/). - -However, if you're trying to connect to an instance of Docker-OSX remotely (e.g. an instance of Docker-OSX hosted in a datacenter), this may improve your performance: - -```bash -# enable for current session -sudo sysctl -w net.ipv4.ip_forward=1 - -# OR -# sudo tee /proc/sys/net/ipv4/ip_forward <<< 1 - -# enable permanently -sudo touch /etc/sysctl.conf -sudo tee -a /etc/sysctl.conf <`. For example, to kill everything, `docker ps | xargs docker kill`.** - -Native QEMU VNC example - -```bash -docker run -i \ - --device /dev/kvm \ - -p 50922:10022 \ - -p 5999:5999 \ - -v /tmp/.X11-unix:/tmp/.X11-unix \ - -e "DISPLAY=${DISPLAY:-:0.0}" \ - -e EXTRA="-display none -vnc 0.0.0.0:99,password=on" \ - sickcodes/docker-osx:big-sur - -# type `change vnc password myvncusername` into the docker terminal and set a password -# connect to localhost:5999 using VNC -# qemu 6 seems to require a username for vnc now -``` - -**NOT TLS/HTTPS Encrypted at all!** - -Or `ssh -N root@1.1.1.1 -L 5999:127.0.0.1:5999`, where `1.1.1.1` is your remote server IP. - -(Note: if you close port 5999 and use the SSH tunnel, this becomes secure.) - -### Building a headless container to run remotely with secure VNC - -Add the following line: - -`-e EXTRA="-display none -vnc 0.0.0.0:99,password=on"` - -In the Docker terminal, press `enter` until you see `(qemu)`. - -Type `change vnc password someusername` - -Enter a password for your new vnc username^. - -You also need the container IP: `docker inspect | jq -r '.[0].NetworkSettings.IPAddress'` - -Or `ip n` will usually show the container IP first. - -Now VNC connects using the Docker container IP, for example `172.17.0.2:5999` - -Remote VNC over SSH: `ssh -N root@1.1.1.1 -L 5999:172.17.0.2:5999`, where `1.1.1.1` is your remote server IP and `172.17.0.2` is your LAN container IP. - -Now you can direct connect VNC to any container built with this command! - -### I'd like to use SPICE instead of VNC - -Optionally, you can enable the SPICE protocol, which allows use of `remote-viewer` to access your OSX container rather than VNC. - -Note: `-disable-ticketing` will allow unauthenticated access to the VM. See the [spice manual](https://www.spice-space.org/spice-user-manual.html) for help setting up authenticated access ("Ticketing"). - -```bash - docker run \ - --device /dev/kvm \ - -p 3001:3001 \ - -p 50922:10022 \ - -e "DISPLAY=${DISPLAY:-:0.0}" \ - -e EXTRA="-monitor telnet::45454,server,nowait -nographic -serial null -spice disable-ticketing,port=3001" \ - mycustomimage -``` - -Then simply do `remote-viewer spice://localhost:3001` and add `--spice-debug` for debugging. - -#### Creating images based on an already configured and set up container -```bash -# You can create an image of an already configured and setup container. -# This allows you to effectively duplicate a system. -# To do this, run the following commands - -# make note of your container id -docker ps --all -docker commit containerid newImageName - -# To run this image do the following -docker run \ - --device /dev/kvm \ - --device /dev/snd \ - -v /tmp/.X11-unix:/tmp/.X11-unix \ - newImageName -``` - -```bash -docker pull sickcodes/docker-osx:auto - -# boot directly into a real OS X shell with no display (Xvfb) [HEADLESS] -docker run -it \ - --device /dev/kvm \ - -p 50922:10022 \ - sickcodes/docker-osx:auto - -# username is user -# password is alpine -# Wait 2-3 minutes until you drop into the shell. -``` - -#### Run the original version of Docker-OSX - -```bash - -docker pull sickcodes/docker-osx:latest - -docker run -it \ - --device /dev/kvm \ - --device /dev/snd \ - -v /tmp/.X11-unix:/tmp/.X11-unix \ - -e "DISPLAY=${DISPLAY:-:0.0}" \ - sickcodes/docker-osx:latest - -# press CTRL + G if your mouse gets stuck -# scroll down to troubleshooting if you have problems -# need more RAM and SSH on localhost -p 50922? -``` - -#### Run but enable SSH in OS X (Original Version)! - -```bash -docker run -it \ - --device /dev/kvm \ - --device /dev/snd \ - -p 50922:10022 \ - -v /tmp/.X11-unix:/tmp/.X11-unix \ - -e "DISPLAY=${DISPLAY:-:0.0}" \ - sickcodes/docker-osx:latest - -# turn on SSH after you've installed OS X in the "Sharing" settings. -ssh user@localhost -p 50922 -``` - -#### Autoboot into OS X after you've installed everything - -Add the extra option `-e NOPICKER=true`. - -Old machines: - -```bash -# find your containerID -docker ps - -# move the no picker script on top of the Launch script -# NEW CONTAINERS -docker exec containerID mv ./Launch-nopicker.sh ./Launch.sh - -# VNC-VERSION-CONTAINER -docker exec containerID mv ./Launch-nopicker.sh ./Launch_custom.sh - -# LEGACY CONTAINERS -docker exec containerID bash -c "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 -" -``` - - - -### The big-sur image starts slowly after installation. Is this expected? - -Automatic updates are still on in the container's settings. You may wish to turn them off. [We have future plans for development around this.](https://github.com/sickcodes/Docker-OSX/issues/227) - -### What is `${DISPLAY:-:0.0}`? - -`$DISPLAY` is the shell variable that refers to your X11 display server. - -`${DISPLAY}` is the same, but allows you to join variables like this: - -- e.g. `${DISPLAY}_${DISPLAY}` would print `:0.0_:0.0` -- e.g. `$DISPLAY_$DISPLAY` would print `:0.0` - -...because `$DISPLAY_` is not `$DISPLAY` - -`${variable:-fallback}` allows you to set a "fallback" variable to be substituted if `$variable` is not set. - -You can also use `${variable:=fallback}` to set that variable (in your current terminal). - -In Docker-OSX, we assume, `:0.0` is your default `$DISPLAY` variable. - -You can see what yours is - -```bash -echo $DISPLAY -``` - -That way, `${DISPLAY:-:0.0}` will use whatever variable your X11 server has set for you, else `:0.0` - -### What is `-v /tmp/.X11-unix:/tmp/.X11-unix`? - -`-v` is a Docker command-line option that lets you pass a volume to the container. - -The directory that we are letting the Docker container use is a X server display socket. - -`/tmp/.X11-unix` - -If we let the Docker container use the same display socket as our own environment, then any applications you run inside the Docker container will show up on your screen too! [https://www.x.org/archive/X11R6.8.0/doc/RELNOTES5.html](https://www.x.org/archive/X11R6.8.0/doc/RELNOTES5.html) - -### ALSA errors on startup or container creation - -You may when initialising or booting into a container see errors from the `(qemu)` console of the following form: -`ALSA lib blahblahblah: (function name) returned error: no such file or directory`. These are more or less expected. As long as you are able to boot into the container and everything is working, no reason to worry about these. - -See also: [here](https://github.com/sickcodes/Docker-OSX/issues/174). +# Skyscope macOS on PC USB Creator Tool + +**Version:** 1.1.0 (Alpha - Installer Workflow with NVIDIA/OCLP Guidance) +**Developer:** Miss Casey Jay Topojani +**Business:** Skyscope Sentinel Intelligence + +## Vision: Your Effortless Bridge to macOS on PC + +Welcome to the Skyscope macOS on PC USB Creator Tool! Our vision is to provide an exceptionally user-friendly, GUI-driven application that automates the complex process of creating a bootable macOS USB **Installer** for a wide range of PCs. This tool aims to be your comprehensive solution, simplifying the Hackintosh journey from start to finish by leveraging direct macOS downloads from Apple and intelligent OpenCore EFI configuration. + +This project is dedicated to creating a seamless experience, from selecting your desired macOS version (defaulting to the latest like Sequoia where possible) to generating a USB drive that's ready to boot your PC and guide you through installing macOS. We strive to incorporate advanced options for tech-savvy users while maintaining an intuitive interface for all, with a clear path for enabling currently unsupported hardware like specific NVIDIA GPUs on newer macOS versions through community-standard methods. + +## Core Features + +* **Intuitive Graphical User Interface (PyQt6):** + * Dark-themed by default (planned UI enhancement). + * Rounded window design (platform permitting). + * Clear, step-by-step workflow. + * Enhanced progress indicators (filling bars, spinners, percentage updates - planned). +* **Automated macOS Installer Acquisition:** + * Directly downloads official macOS installer assets from Apple's servers using `gibMacOS.py` principles. + * Supports user selection of macOS versions (e.g., Sequoia, Sonoma, Ventura, Monterey, Big Sur, etc.). +* **Automated USB Installer Creation:** + * **Cross-Platform USB Detection:** Identifies suitable USB drives on Linux, macOS, and Windows (using WMI for more accurate detection on Windows). + * **Automated Partitioning:** Creates GUID Partition Table (GPT), an EFI System Partition (FAT32, ~300-550MB), and a main macOS Installer partition (HFS+). + * **macOS Installer Layout (Linux & macOS):** Automatically extracts and lays out downloaded macOS assets (BaseSystem, key support files, and installer packages) onto the USB to create a bootable macOS installer volume. + * **Windows USB Writing (Partial Automation):** Automates EFI partition setup and EFI file copying. Writing the BaseSystem HFS+ image to the main USB partition requires a guided manual `dd` step by the user. Copying further HFS+ installer content from Windows is not automated. +* **Intelligent OpenCore EFI Setup:** + * Assembles a complete OpenCore EFI folder on the USB's EFI partition using a robust template. + * **Experimental `config.plist` Auto-Enhancement:** + * If enabled by the user (and running the tool on a Linux host for hardware detection): + * Gathers host hardware information (iGPU, dGPU, Audio, Ethernet, CPU). + * Applies targeted modifications to the `config.plist` for iGPU, audio, Ethernet, and specific NVIDIA GPU considerations. + * Creates a backup of the original `config.plist` before modification. +* **NVIDIA GPU Strategy (for newer macOS like Sonoma/Sequoia):** + * The tool configures the `config.plist` to ensure bootability with NVIDIA Maxwell/Pascal GPUs (like GTX 970). + * If an Intel iGPU is present and usable, it will be prioritized for display, and `nv_disable=1` will be set for the NVIDIA card. + * Includes necessary boot-args (e.g., `amfi_get_out_of_my_way=0x1`) to prepare the system for **post-install patching with OpenCore Legacy Patcher (OCLP)**, which is required for graphics acceleration. +* **Privilege Checking:** Warns if administrative/root privileges are needed for USB writing and are not detected. + +## NVIDIA GPU Support on Newer macOS (Mojave+): The OCLP Path + +Modern macOS versions (Mojave and newer, including Ventura, Sonoma, and Sequoia) do not natively support NVIDIA Maxwell (e.g., GTX 970) or Pascal GPUs with graphics acceleration. + +**How Skyscope Tool Helps:** + +1. **Bootable Installer:** This tool will help you create a macOS USB installer with an OpenCore EFI configured to allow your system to boot with your NVIDIA card (either using an available Intel iGPU with the NVIDIA card disabled by `nv_disable=1`, or with the NVIDIA card providing basic, unaccelerated display if it's the only option). +2. **OCLP Preparation:** The `config.plist` generated by this tool will include essential boot arguments (like `amfi_get_out_of_my_way=0x1`) and settings (`SecureBootModel=Disabled`) that are prerequisites for using the OpenCore Legacy Patcher (OCLP). + +**User Action Required for NVIDIA Acceleration (Post-Install):** + +* After you have installed macOS onto your PC's internal drive using the USB created by this tool, you **must run the OpenCore Legacy Patcher application from within your new macOS installation.** +* OCLP will then apply the necessary system patches to the installed macOS system to enable graphics acceleration for your unsupported NVIDIA card. +* This tool **does not** perform these system patches itself. It prepares your installer and EFI to be compatible with the OCLP process. +* **CUDA:** CUDA support is tied to NVIDIA's official drivers, which are not available for newer macOS. OCLP primarily restores graphics (Metal/OpenGL/CL) acceleration, not the CUDA compute environment. + +For macOS High Sierra or older, this tool can set `nvda_drv=1` if you intend to install NVIDIA Web Drivers (which you must source and install separately). + +## Current Status & Known Limitations + +* **Workflow Transition:** The project is currently transitioning from a Docker-OSX based method to a `gibMacOS`-based installer creation method. Not all platform-specific USB writers are fully refactored for this new approach yet. +* **Windows USB Writing:** Creating the HFS+ macOS installer partition and copying files to it from Windows is complex without native HFS+ write support. The EFI part is automated; the main partition might initially require manual steps or use of `dd` for BaseSystem, with file copying being a challenge. +* **`config.plist` Enhancement is Experimental:** Hardware detection for this feature is currently Linux-host only. The range of hardware automatically configured is limited to common setups. +* **Universal Compatibility:** While striving for broad compatibility, Hackintoshing is hardware-dependent. Success on every PC configuration cannot be guaranteed. +* **Dependency on External Projects:** Relies on OpenCore and various community-sourced kexts and configurations. The `gibMacOS.py` script (or its underlying principles) is key for downloading assets. + +## Prerequisites + +1. **Python:** Version 3.8 or newer. +2. **Python Libraries:** `PyQt6`, `psutil`. Install via `pip install PyQt6 psutil`. +3. **Core Utilities (All Platforms, in PATH):** + * `git` (for `gibMacOS.py`). + * `7z` or `7za` (7-Zip CLI for archive extraction). +4. **`gibMacOS.py` Script:** + * Clone `corpnewt/gibMacOS` (`git clone https://github.com/corpnewt/gibMacOS.git`) into a `scripts/gibMacOS` subdirectory within this project, or ensure `gibMacOS.py` is in the project root or system PATH and adjust `GIBMACOS_SCRIPT_PATH` in `main_app.py` if necessary. +5. **Platform-Specific CLI Tools for USB Writing:** + * **Linux (e.g., Debian 13 "Trixie"):** + * `sgdisk` (from `gdisk`), `parted`, `partprobe` (from `util-linux`) + * `mkfs.vfat` (from `dosfstools`), `mkfs.hfsplus` (from `hfsprogs`) + * `rsync`, `dd` + * `apfs-fuse`: Requires manual compilation (e.g., from `sgan81/apfs-fuse` on GitHub). Typical build dependencies: `git g++ cmake libfuse3-dev libicu-dev zlib1g-dev libbz2-dev libssl-dev`. + * Install most via: `sudo apt update && sudo apt install gdisk parted dosfstools hfsprogs rsync util-linux p7zip-full` (or `p7zip`) + * **macOS:** `diskutil`, `hdiutil`, `rsync`, `cp`, `dd`, `bless`. `7z` (e.g., `brew install p7zip`). + * **Windows:** `diskpart`, `robocopy`. `7z.exe`. A "dd for Windows" utility. + +## How to Run (Development Phase) + +1. Meet all prerequisites for your OS, including `gibMacOS.py` setup. +2. Clone this repository. Install Python libs: `pip install PyQt6 psutil`. +3. Execute `python main_app.py`. +4. **For USB Writing Operations:** + * **Linux:** Run with `sudo python main_app.py`. + * **macOS:** Run normally. May prompt for password for `sudo rsync` or `diskutil`. Ensure the app has Full Disk Access if needed. + * **Windows:** Run as Administrator. + +## Step-by-Step Usage Guide (New Workflow) + +1. **Step 1: Download macOS Installer Assets** + * Launch the "Skyscope macOS on PC USB Creator Tool". + * Select your desired macOS version (e.g., Sequoia, Sonoma). + * Choose a directory on your computer to save the downloaded assets. + * Click "Download macOS Installer Assets". The tool will use `gibMacOS` to fetch the official installer files from Apple. This may take time. Progress will be shown. +2. **Step 2: Create Bootable USB Installer** + * Once downloads are complete, connect your target USB flash drive (16GB+ recommended). + * Click "Refresh List" to detect USB drives. + * **Linux/macOS:** Select your USB drive from the dropdown. Verify size and identifier carefully. + * **Windows:** USB drives detected via WMI will appear in the dropdown. Select the correct one. Ensure it's the `Disk X` number you intend. + * **(Optional, Experimental):** Check the "Try to auto-enhance config.plist..." box if you are on a Linux host and wish the tool to attempt automatic `config.plist` modification for your hardware. A backup of the original `config.plist` will be made. + * **CRITICAL WARNING:** Double-check your USB selection. The next action will erase the entire USB drive. + * Click "Create macOS Installer USB". Confirm the data erasure warning. + * The tool will: + * Partition and format the USB drive. + * Extract and write the macOS BaseSystem to make the USB bootable. + * Copy necessary macOS installer packages and files to the USB. + * Assemble an OpenCore EFI folder (potentially with your hardware-specific enhancements if enabled) onto the USB's EFI partition. + * This is a lengthy process. Monitor progress in the output area and status bar. +3. **Boot Your PC from the USB!** + * Safely eject the USB. Configure your PC's BIOS/UEFI for macOS booting (disable Secure Boot, enable AHCI, XHCI Handoff, etc. - see Dortania guides). + * Boot from the USB and proceed with macOS installation onto your PC's internal drive. +4. **(For Unsupported NVIDIA on newer macOS): Post-Install Patching** + * After installing macOS, if you have an unsupported NVIDIA card (like GTX 970 on Sonoma/Sequoia) and want graphics acceleration, you will need to run the **OpenCore Legacy Patcher (OCLP)** application from within your new macOS installation. This tool has prepared the EFI to be generally compatible with OCLP. + +## Future Vision & Advanced Capabilities + +* **Fully Automated Windows USB Writing:** Replace the manual `dd` step with a reliable, integrated solution. +* **Advanced `config.plist` Customization:** + * Expand hardware detection for plist enhancement to macOS and Windows hosts. + * Provide more granular UI controls for plist enhancements (e.g., preview changes, select specific patches). + * Allow users to load/save `config.plist` modification profiles. +* **Enhanced UI/UX for Progress:** Implement determinate progress bars with percentage completion and more dynamic status updates. +* **Debian 13 "Trixie" (and other distros) Validation:** Continuous compatibility checks and dependency streamlining. +* **"Universal" Config Strategy (Research):** Investigate advanced techniques for more adaptive OpenCore configurations, though true universality is a significant challenge. + +## Contributing + +We are passionate about making Hackintoshing more accessible! Contributions, feedback, and bug reports are highly encouraged. + +## License + +(To be decided - e.g., MIT or GPLv3) diff --git a/constants.py b/constants.py new file mode 100644 index 00000000..a8b21149 --- /dev/null +++ b/constants.py @@ -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 + } +} diff --git a/linux_hardware_info.py b/linux_hardware_info.py new file mode 100644 index 00000000..2e8d9b25 --- /dev/null +++ b/linux_hardware_info.py @@ -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.") diff --git a/main_app.py b/main_app.py new file mode 100644 index 00000000..90051e16 --- /dev/null +++ b/main_app.py @@ -0,0 +1,542 @@ +# main_app.py +import sys +import subprocess +import os +import psutil +import platform +import ctypes +import json +import re # For progress parsing +import traceback # For error reporting +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 # Added QTimer + +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}") + +# Path to gibMacOS.py script. Assumed to be in a 'scripts' subdirectory. +# The application startup or a setup step should ensure gibMacOS is cloned/present here. +GIBMACOS_SCRIPT_PATH = os.path.join(os.path.dirname(__file__), "scripts", "gibMacOS", "gibMacOS.py") +if not os.path.exists(GIBMACOS_SCRIPT_PATH): + # Fallback if not in relative scripts dir, try to find it in current dir (e.g. if user placed it there) + GIBMACOS_SCRIPT_PATH = os.path.join(os.path.dirname(__file__), "gibMacOS.py") + + +class WorkerSignals(QObject): + progress = pyqtSignal(str) + finished = pyqtSignal(str) # Can carry a success message or final status + error = pyqtSignal(str) + # New signal for determinate progress + progress_value = pyqtSignal(int) # Percentage 0-100 + + +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 = GIBMACOS_SCRIPT_PATH + if not os.path.exists(script_to_run): + alt_script_path = os.path.join(os.path.dirname(os.path.dirname(GIBMACOS_SCRIPT_PATH)), "gibMacOS.py") # if main_app is in src/ + script_to_run = alt_script_path if os.path.exists(alt_script_path) else "gibMacOS.py" + if not os.path.exists(script_to_run) and not shutil.which(script_to_run): # Check if it's in PATH + self.signals.error.emit(f"gibMacOS.py not found at expected locations ({GIBMACOS_SCRIPT_PATH}, {alt_script_path}) or in PATH.") + return + else: + script_to_run = GIBMACOS_SCRIPT_PATH + + 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"\(?\s*(\d{1,3}\.?\d*)\s*%\s*\)?", line_strip) + if progress_match: + try: + percent = int(float(progress_match.group(1))) + self.signals.progress_value.emit(percent) + except ValueError: + pass # Ignore if not a valid int + elif "downloaded 100.00%" in line_strip.lower(): + self.signals.progress_value.emit(100) + 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 (tried: {GIBMACOS_SCRIPT_PATH}).") + 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 + + 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 + ) + + # Check if writer_instance has 'signals' attribute for progress_value (for rsync progress later) + # This is more for future-proofing if USB writers implement determinate progress. + if hasattr(self.writer_instance, 'signals') and hasattr(self.writer_instance.signals, 'progress_value'): + self.writer_instance.signals.progress_value.connect(self.signals.progress_value.emit) + + 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, 750) + + 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 added to 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) + + 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) + + 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.statusBar.addPermanentWidget(self.progress_bar) # Removed from here, progress bar now in main layout + self.update_all_button_states() + + + def show_about_dialog(self): QMessageBox.about(self, f"About {APP_NAME}", f"Version: 1.0.1\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..."): + # Disable/Enable general interactive widgets + general_widgets_to_manage = [ + self.download_macos_button, self.version_combo, + self.refresh_usb_button, self.usb_drive_combo, + self.windows_disk_id_input, self.enhance_plist_checkbox, + self.write_to_usb_button # Write button is also general now + ] + for widget in general_widgets_to_manage: + widget.setEnabled(not busy_status) + + # Specific button for ongoing operation + self.cancel_operation_button.setEnabled(busy_status and self.current_worker_instance is not None) + + 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() + # Progress bar range set by _start_worker based on provides_progress + else: + self.spinner_timer.stop() + self.status_bar.showMessage(message or "Ready.", 7000) + + if not busy_status: # After an operation, always update all button states + 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)] + current_message = self.base_status_message + + # Check if current worker is providing determinate progress + 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 + current_message = f"{self.base_status_message} ({self.progress_bar.value()}%)" + else: # Indeterminate + if self.progress_bar.maximum() != 0: self.progress_bar.setRange(0,0) + + self.status_bar.showMessage(f"{char} {current_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); self.progress_bar.setValue(0) + # Ensure signal exists on worker before connecting + if hasattr(worker_instance.signals, 'progress_value'): + worker_instance.signals.progress_value.connect(self.update_progress_bar_value) + else: + self._report_progress(f"Warning: Worker '{worker_name}' set to provides_progress=True but has no 'progress_value' signal.") + self.progress_bar.setRange(0,0) # Fallback to indeterminate + provides_progress = False # Correct the flag + 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) + + # Store specific instance type for stop_current_operation if needed + if worker_name == "macos_download": self.gibmacos_worker_instance = worker_instance + + 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) + # Update base_status_message for spinner to include percentage + if self.active_worker_thread and self.active_worker_thread.isRunning(): + worker_name_display = self.active_worker_thread.objectName().replace("_thread","").replace("_"," ").capitalize() + self.base_status_message = f"{worker_name_display} in progress..." # Keep it generic or pass specific msg + # The spinner timer will pick up self.progress_bar.value() + + def _handle_worker_finished(self, message, worker_name, specific_finished_slot): + final_msg = f"{worker_name.replace('_', ' ').capitalize()} completed." + if worker_name == "macos_download": self.gibmacos_worker_instance = None # Clear specific instance + self.current_worker_instance = None + 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." + if worker_name == "macos_download": self.gibmacos_worker_instance = None # Clear specific instance + self.current_worker_instance = None + 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 + + # self.output_area.append(f"Starting macOS {selected_version_name} download to: {self.macos_download_path}...") # Message handled by _set_ui_busy + + 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", # worker_name + f"Downloading macOS {selected_version_name} assets...", # busy_message + provides_progress=True): # GibMacOSWorker now attempts to provide progress + self._set_ui_busy(False, "Failed to start macOS download operation.") + + + @pyqtSlot(str) + def macos_download_finished(self, message): + # self.output_area.append(f"macOS Download Finished: {message}") # Logged by generic handler + 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): + # self.output_area.append(f"macOS Download Error: {error_message}") # Logged by generic handler + 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'): + worker_name_display = "Operation" + if self.active_worker_thread: # Get worker name if possible + worker_name_display = self.active_worker_thread.objectName().replace('_thread','').replace('_',' ').capitalize() + self.output_area.append(f"\n--- Attempting to stop {worker_name_display} ---") + self.current_worker_instance.stop() + else: + self.output_area.append("\n--- No active stoppable operation or stop method not implemented for current worker. ---") + # UI state will be updated when the worker actually finishes or errors out due to stop. + # We can disable the cancel button here to prevent multiple clicks if desired, + # but update_all_button_states will also handle it. + self.cancel_operation_button.setEnabled(False) + + + 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("\nScanning for disk devices...") + current_os = platform.system() + self.windows_usb_guidance_label.setVisible(current_os == "Windows") + # Show/hide manual input field based on whether WMI found drives or failed + # This logic is now more refined within the Windows block + self.usb_drive_combo.setVisible(True) + + if current_os == "Windows": + self.usb_drive_label.setText("Available USB Disks (Windows - via WMI/PowerShell):") + self.windows_disk_id_input.setVisible(False) # Hide initially, show on WMI error/no results + self.windows_usb_input_label.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 Disk Number input enabled below."); + self.windows_usb_input_label.setVisible(True); self.windows_disk_id_input.setVisible(True) + except Exception as e: + self.output_area.append(f"Error scanning Windows USBs with PowerShell: {e}. Manual Disk Number input enabled below.") + self.windows_usb_input_label.setVisible(True); 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); self.windows_usb_input_label.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 current_os == "Darwin" and p.device.startswith("/dev/disk") and 'external' in p.opts.lower() and 'physical' in p.opts.lower(): is_likely_usb = True + elif current_os == "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() + if not target_device_id_for_worker and self.windows_disk_id_input.isVisible(): # Fallback to manual input IF VISIBLE + target_device_id_for_worker = self.windows_disk_id_input.text().strip() + if not target_device_id_for_worker or not target_device_id_for_worker.isdigit(): # Must be a digit (disk index) + QMessageBox.warning(self, "Input Required", "Please select a valid USB disk from dropdown or enter its Disk Number if WMI failed."); return + 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 + # For Windows, USBWriterWindows expects just the number string. + # For Linux/macOS, it's the device path like /dev/sdx or /dev/diskX. + + 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 + + usb_worker = 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, 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 write progress is complex, indeterminate for now + 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): + 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; import shutil + app = QApplication(sys.argv); window = MainWindow(); window.show(); sys.exit(app.exec()) diff --git a/plist_modifier.py b/plist_modifier.py new file mode 100644 index 00000000..9da60cdb --- /dev/null +++ b/plist_modifier.py @@ -0,0 +1,280 @@ +# 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 --- +# For AAPL,ig-platform-id, byte order in can be direct or swapped depending on source. +# OpenCore usually expects direct byte order for data values (e.g. 0A009B46 for 0x469B000A). +# The values below are what should be written as data (hex bytes). +INTEL_IGPU_DEFAULTS = { + # Coffee Lake Desktop (UHD 630) + "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) + "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) + "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 iGPUs (e.g., UHD 730, UHD 770) + # For driving a display (Desktop): AAPL,ig-platform-id = 0x469B000A (Data: 0A009B46) or 0x4692000A (Data: 0A009246) + # device-id is often the PCI device ID itself, byte-swapped. e.g., 0x4690 -> <90460000> + "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"}, # For i5-12600K UHD 770 + "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"}, # For i7/i9 UHD 770 + "8086:4692": {"AAPL,ig-platform-id": b"\x0A\x00\x92\x46", "device-id": b"\x92\x46\x00\x00", "enable-hdmi20": b"\x01\x00\x00\x00"}, # For i5 (non-K) UHD 730/770 + # Headless mode (if dGPU is primary) for Alder Lake: AAPL,ig-platform-id = 0x04001240 (Data: 04001240) + "8086:4690_headless": {"AAPL,ig-platform-id": b"\x04\x00\x12\x40", "device-id": b"\x90\x46\x00\x00"}, + "8086:4680_headless": {"AAPL,ig-platform-id": b"\x04\x00\x12\x40", "device-id": b"\x80\x46\x00\x00"}, + "8086:4692_headless": {"AAPL,ig-platform-id": b"\x04\x00\x12\x40", "device-id": b"\x92\x46\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 (from /proc/asound or lshw) + "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 for B660/B760, layout 11 or 66 often suggested + "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 + "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 variants + "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 +} + + +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}") + _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) + # Check for any discrete GPU (non-Intel VGA) + 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']}" + # If a dGPU is also present, prefer headless iGPU setup if available. + final_lookup_key = lookup_key + if dgpu_present and f"{lookup_key}_headless" in INTEL_IGPU_DEFAULTS: + final_lookup_key = f"{lookup_key}_headless" + _report(f"Intel iGPU ({intel_igpu_on_host['description']}) detected with a dGPU. Applying headless properties: {final_lookup_key}") + elif lookup_key in INTEL_IGPU_DEFAULTS: + _report(f"Intel iGPU ({intel_igpu_on_host['description']}) detected. Applying display properties: {lookup_key}") + else: + _report(f"Found Intel iGPU: {intel_igpu_on_host['description']} ({lookup_key}) but no default properties in map for key '{final_lookup_key}'.") + final_lookup_key = None # Ensure we don't use a key that's not in the map + + if final_lookup_key and final_lookup_key in INTEL_IGPU_DEFAULTS: + igpu_path_properties = dev_props.setdefault(INTEL_IGPU_PCI_PATH, {}) + for key, value in INTEL_IGPU_DEFAULTS[final_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: already reported no properties found + + # 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: + for known_codec_key, layout_id in AUDIO_LAYOUTS.items(): + if not known_codec_key.startswith("pci_"): + # Try to match the core part of the codec name + # e.g. "Realtek ALC897" should match a key like "ALC897" or "Realtek ALC897" + if known_codec_key.lower() in codec_name_full.lower(): + _report(f"Matched Audio Codec: '{codec_name_full}' (using key '{known_codec_key}'). Setting layout-id {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: + _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 (PCI): {dev['description']}. Setting layout-id {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 + nvidia_gtx_970_present = any(dev['vendor_id'] == '10de' and dev['device_id'] == '13c2' for dev in pci_devices) + if nvidia_gtx_970_present: + _report("NVIDIA GTX 970 detected.") + high_sierra_versions = ["high sierra", "sierra"]; + is_legacy_nvidia_target = target_macos_version_name.lower() in high_sierra_versions + original_boot_args_set = set(boot_args) + + if is_legacy_nvidia_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') + boot_args.add('amfi_get_out_of_my_way=0x1') # For OCLP compatibility + _report(f" Added amfi_get_out_of_my_way=0x1 for {target_macos_version_name} (OCLP prep).") + 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 primary GPU. `nv_disable=1` not forced for {target_macos_version_name}. Basic display expected. OCLP recommended post-install for acceleration.") + 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 new modifications made to config.plist based on detected hardware or existing settings were different from defaults.") + if platform.system() != "Linux" and not pci_devices : return True + + try: # Save logic (same as before) + with open(plist_path, 'wb') as f: plistlib.dump(config_data, f, sort_keys=True, fmt=plistlib.PlistFormat.XML) + _report(f"Successfully saved config.plist to {plist_path}"); return True + except Exception as e: + _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: {backup_error}") + return False + +# if __name__ == '__main__': (Keep comprehensive test block) +if __name__ == '__main__': + import traceback # Ensure traceback is imported for standalone test + print("Plist Modifier Standalone Test") # ... (rest of test block as in previous version, ensure dummy data for kexts is complete) + dummy_plist_path = "test_config.plist" + # Ensure kext entries in dummy_data have all required fields for the modifier logic to not error out + # when trying to access keys like "Enabled", "Arch", etc. + 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': '4690', 'description': 'Alder Lake UHD 770 (i5-12600K)', '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 2.5GbE', 'full_lspci_line':''}, + {'type': 'VGA', 'vendor_id': '10de', 'device_id': '13c2', 'description': 'NVIDIA GTX 970', 'full_lspci_line':''} # Test GTX 970 present + ] + get_cpu_info = lambda: {"Model name": "12th Gen Intel(R) Core(TM) i7-12700K", "Flags": "avx avx2"} + get_audio_codecs = lambda: ["Realtek ALC897", "Intel Alder Lake-S HDMI"] # Mock ALC897 for B760M + + print("\n--- Testing with Sonoma (GTX 970 + iGPU present) ---") + 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')}") # Should have nv_disable=1, amfi + print(f" Sonoma iGPU props: {modified_data.get('DeviceProperties',{}).get('Add',{}).get(INTEL_IGPU_PCI_PATH)}") # Should have Alder Lake props (headless if dGPU active) + print(f" Sonoma Audio props: {modified_data.get('DeviceProperties',{}).get('Add',{}).get(AUDIO_PCI_PATH_FALLBACK)}") # Should have ALC897 layout + 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.") diff --git a/usb_writer_linux.py b/usb_writer_linux.py new file mode 100644 index 00000000..e0d1f08a --- /dev/null +++ b/usb_writer_linux.py @@ -0,0 +1,308 @@ +# usb_writer_linux.py (Finalizing installer asset copying - refined) +import subprocess +import os +import time +import shutil +import glob +import re +import plistlib +import traceback + +try: + from plist_modifier import enhance_config_plist +except ImportError: + enhance_config_plist = None +# from constants import MACOS_VERSIONS # Imported in _get_gibmacos_product_folder + +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; pid = os.getpid() + self.temp_basesystem_hfs_path = f"temp_basesystem_{pid}.hfs" + self.temp_efi_build_dir = f"temp_efi_build_{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_shared_support_mount = f"/mnt/shared_support_temp_{pid}" + self.temp_dmg_extract_dir = f"temp_dmg_extract_{pid}" # Added for _extract_hfs_from_dmg_or_pkg + + 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_mount, + self.temp_dmg_extract_dir # Ensure this is cleaned + ] + + def _report_progress(self, message: str, is_rsync_line: bool = False): + if is_rsync_line: + match = re.search(r"(\d+)%\s+", message) + if match: + try: percentage = int(match.group(1)); self.progress_callback(f"PROGRESS_VALUE:{percentage}") + except ValueError: pass + if self.progress_callback: self.progress_callback(message) + else: print(message) + else: + 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, stream_rsync_progress=False): + cmd_list = command if isinstance(command, list) else command.split() + is_rsync_progress_command = stream_rsync_progress and "rsync" in cmd_list[0 if cmd_list[0] != "sudo" else (1 if len(cmd_list) > 1 else 0)] + + if is_rsync_progress_command: + effective_cmd_list = list(cmd_list) + rsync_idx = -1 + for i, arg in enumerate(effective_cmd_list): + if "rsync" in arg: rsync_idx = i; break + if rsync_idx != -1: + conflicting_flags = ["-P", "--progress"]; effective_cmd_list = [arg for arg in effective_cmd_list if arg not in conflicting_flags] + actual_rsync_cmd_index_in_list = -1 + for i, arg_part in enumerate(effective_cmd_list): + if "rsync" in os.path.basename(arg_part): actual_rsync_cmd_index_in_list = i; break + if actual_rsync_cmd_index_in_list != -1: + if "--info=progress2" not in effective_cmd_list: effective_cmd_list.insert(actual_rsync_cmd_index_in_list + 1, "--info=progress2") + if "--no-inc-recursive" not in effective_cmd_list : effective_cmd_list.insert(actual_rsync_cmd_index_in_list + 1, "--no-inc-recursive") + else: self._report_progress("Warning: rsync command part not found for progress flag insertion.") + self._report_progress(f"Executing (with progress streaming): {' '.join(effective_cmd_list)}") + process = subprocess.Popen(effective_cmd_list, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, bufsize=1, universal_newlines=True, cwd=working_dir) + stdout_lines, stderr_lines = [], [] + if process.stdout: + for line in iter(process.stdout.readline, ''): line_strip = line.strip(); self._report_progress(line_strip, is_rsync_line=True); stdout_lines.append(line_strip) + process.stdout.close() + if process.stderr: + for line in iter(process.stderr.readline, ''): line_strip = line.strip(); self._report_progress(f"STDERR: {line_strip}"); stderr_lines.append(line_strip) + process.stderr.close() + return_code = process.wait(timeout=timeout); + if check and return_code != 0: raise subprocess.CalledProcessError(return_code, effective_cmd_list, output="\n".join(stdout_lines), stderr="\n".join(stderr_lines)) + return subprocess.CompletedProcess(args=effective_cmd_list, returncode=return_code, stdout="\n".join(stdout_lines), stderr="\n".join(stderr_lines)) + else: + self._report_progress(f"Executing: {' '.join(cmd_list)}") + try: + process = subprocess.run(cmd_list, check=check, capture_output=capture_output, text=True, timeout=timeout, shell=shell, cwd=working_dir, creationflags=0) + 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 '{cmd_list[0]}' not found."); raise + + def _cleanup_temp_files_and_dirs(self): + self._report_progress("Cleaning up...") + for mp in self.temp_dirs_to_clean: + 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 deps...");deps=["sgdisk","parted","mkfs.vfat","mkfs.hfsplus","7z","rsync","dd"];m=[d for d in deps if not shutil.which(d)]; assert not m, f"Missing: {', '.join(m)}. Install hfsprogs for mkfs.hfsplus, p7zip for 7z."; return True + + def _find_gibmacos_asset(self, asset_patterns: list[str] | str, product_folder_path: str | None = None, search_deep=True) -> str | None: + if isinstance(asset_patterns, str): asset_patterns = [asset_patterns] + search_base = product_folder_path or self.macos_download_path + self._report_progress(f"Searching for {asset_patterns} in {search_base} and subdirectories...") + for pattern in asset_patterns: + common_subdirs_for_pattern = ["", "SharedSupport", f"Install macOS {self.target_macos_version}.app/Contents/SharedSupport", f"Install macOS {self.target_macos_version}.app/Contents/Resources"] + for sub_dir_pattern in common_subdirs_for_pattern: + current_search_base = os.path.join(search_base, sub_dir_pattern) + # Escape special characters for glob, but allow wildcards in pattern itself + # This simple escape might not be perfect for all glob patterns. + glob_pattern = os.path.join(glob.escape(current_search_base), pattern) + + found_files = glob.glob(glob_pattern, recursive=False) + if found_files: + found_files.sort(key=os.path.getsize, reverse=True) + self._report_progress(f"Found '{pattern}' at: {found_files[0]} (in {current_search_base})") + return found_files[0] + + if search_deep: + deep_search_pattern = os.path.join(glob.escape(search_base), "**", pattern) + found_files_deep = sorted(glob.glob(deep_search_pattern, recursive=True), key=len) + if found_files_deep: + self._report_progress(f"Found '{pattern}' via deep search at: {found_files_deep[0]}") + return found_files_deep[0] + + self._report_progress(f"Warning: Asset matching patterns '{asset_patterns}' not found in {search_base}.") + return None + + def _get_gibmacos_product_folder(self) -> str | None: + from constants import MACOS_VERSIONS + base_path = os.path.join(self.macos_download_path, "macOS Downloads", "publicrelease") + if not os.path.isdir(base_path): base_path = self.macos_download_path + if os.path.isdir(base_path): + for item in os.listdir(base_path): + item_path = os.path.join(base_path, item) + version_tag_from_constants = MACOS_VERSIONS.get(self.target_macos_version, self.target_macos_version).lower() + if os.path.isdir(item_path) and (self.target_macos_version.lower() in item.lower() or version_tag_from_constants in item.lower()): + self._report_progress(f"Identified gibMacOS product folder: {item_path}"); return item_path + self._report_progress(f"Could not identify a specific product folder for '{self.target_macos_version}'. Using general download path: {self.macos_download_path}"); return self.macos_download_path + + def _extract_hfs_from_dmg_or_pkg(self, dmg_or_pkg_path: str, output_hfs_path: str) -> bool: + os.makedirs(self.temp_dmg_extract_dir, exist_ok=True); current_target = dmg_or_pkg_path + try: + if dmg_or_pkg_path.endswith(".pkg"): self._report_progress(f"Extracting DMG from PKG {current_target}..."); self._run_command(["7z", "e", "-txar", current_target, "*.dmg", f"-o{self.temp_dmg_extract_dir}"], check=True); dmgs_in_pkg = glob.glob(os.path.join(self.temp_dmg_extract_dir, "*.dmg")); assert dmgs_in_pkg, "No DMG in PKG."; current_target = max(dmgs_in_pkg, key=os.path.getsize, default=dmgs_in_pkg[0]); assert current_target, "No primary DMG in PKG."; self._report_progress(f"Using DMG from PKG: {current_target}") + assert current_target and current_target.endswith(".dmg"), f"Not a valid DMG: {current_target}" + basesystem_dmg_to_process = current_target + if "basesystem.dmg" not in os.path.basename(current_target).lower(): self._report_progress(f"Extracting BaseSystem.dmg from {current_target}..."); self._run_command(["7z", "e", current_target, "*/BaseSystem.dmg", "-r", f"-o{self.temp_dmg_extract_dir}"], check=True); found_bs_dmg = glob.glob(os.path.join(self.temp_dmg_extract_dir, "**", "*BaseSystem.dmg"), recursive=True); assert found_bs_dmg, f"No BaseSystem.dmg from {current_target}"; basesystem_dmg_to_process = found_bs_dmg[0] + self._report_progress(f"Extracting HFS+ partition image from {basesystem_dmg_to_process}..."); self._run_command(["7z", "e", "-tdmg", basesystem_dmg_to_process, "*.hfs", f"-o{self.temp_dmg_extract_dir}"], check=True); hfs_files = glob.glob(os.path.join(self.temp_dmg_extract_dir, "*.hfs")); + if not hfs_files: self._run_command(["7z", "e", "-tdmg", basesystem_dmg_to_process, "*", f"-o{self.temp_dmg_extract_dir}"], check=True); hfs_files = [os.path.join(self.temp_dmg_extract_dir, f) for f in os.listdir(self.temp_dmg_extract_dir) if not f.lower().endswith((".xml",".chunklist",".plist")) and os.path.isfile(os.path.join(self.temp_dmg_extract_dir,f)) and os.path.getsize(os.path.join(self.temp_dmg_extract_dir,f)) > 2*1024*1024*1024] # Min 2GB HFS for BaseSystem + assert hfs_files, f"No suitable HFS+ image file found after extracting {basesystem_dmg_to_process}" + final_hfs_file = max(hfs_files, key=os.path.getsize); self._report_progress(f"Found HFS+ 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: {e}\n{traceback.format_exc()}"); return False + finally: + if os.path.exists(self.temp_dmg_extract_dir): shutil.rmtree(self.temp_dmg_extract_dir, ignore_errors=True) + + def _create_minimal_efi_template(self, efi_dir_path): + self._report_progress(f"Minimal EFI template directory not found or empty. Creating basic structure at {efi_dir_path}"); oc_dir=os.path.join(efi_dir_path,"EFI","OC");os.makedirs(os.path.join(efi_dir_path,"EFI","BOOT"),exist_ok=True);os.makedirs(oc_dir,exist_ok=True);[os.makedirs(os.path.join(oc_dir,s),exist_ok=True) for s in ["Drivers","Kexts","ACPI","Tools","Resources"]];open(os.path.join(efi_dir_path,"EFI","BOOT","BOOTx64.efi"),"w").close();open(os.path.join(oc_dir,"OpenCore.efi"),"w").close();bc={"#Comment":"Basic config","Misc":{"Security":{"ScanPolicy":0,"SecureBootModel":"Disabled"}},"PlatformInfo":{"Generic":{"MLB":"CHANGE_ME_MLB","SystemSerialNumber":"CHANGE_ME_SERIAL","SystemUUID":"CHANGE_ME_UUID","ROM":b"\0"*6}}};plistlib.dump(bc,open(os.path.join(oc_dir,"config.plist"),'wb'),fmt=plistlib.PlistFormat.XML) + + def format_and_write(self) -> bool: + try: + self.check_dependencies(); self._cleanup_temp_files_and_dirs(); + for mp_dir in [self.mount_point_usb_esp, self.mount_point_usb_macos_target, self.temp_efi_build_dir]: self._run_command(["sudo", "mkdir", "-p", mp_dir]) + 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", "0:0:+551MiB", "-t", "0:ef00", "-c", "0:EFI", self.device]) + usb_vol_name = f"Install macOS {self.target_macos_version}" + self._run_command(["sudo", "sgdisk", "-n", "0:0:0", "-t", "0:af00", "-c", f"0:{usb_vol_name[:11]}" , self.device]) + self._run_command(["sudo", "partprobe", self.device], timeout=10); time.sleep(3) + esp_dev=f"{self.device}1" if os.path.exists(f"{self.device}1") else f"{self.device}p1"; macos_part=f"{self.device}2" if os.path.exists(f"{self.device}2") else f"{self.device}p2"; assert os.path.exists(esp_dev) and os.path.exists(macos_part), "Partitions not found." + self._report_progress(f"Formatting ESP {esp_dev}..."); self._run_command(["sudo", "mkfs.vfat", "-F", "32", "-n", "EFI", esp_dev]) + self._report_progress(f"Formatting macOS partition {macos_part}..."); self._run_command(["sudo", "mkfs.hfsplus", "-v", usb_vol_name, macos_part]) + + product_folder_path = self._get_gibmacos_product_folder() + basesystem_source_dmg_or_pkg = self._find_gibmacos_asset(["BaseSystem.dmg", "InstallESD.dmg", "SharedSupport.dmg", "InstallAssistant.pkg"], product_folder_path, "BaseSystem.dmg (or source like InstallESD.dmg/SharedSupport.dmg/InstallAssistant.pkg)") + if not basesystem_source_dmg_or_pkg: raise RuntimeError("Essential macOS DMG/PKG for BaseSystem extraction not found in download path.") + if not self._extract_hfs_from_dmg_or_pkg(basesystem_source_dmg_or_pkg, self.temp_basesystem_hfs_path): + raise RuntimeError("Failed to extract HFS+ image from BaseSystem assets.") + self._report_progress(f"Writing BaseSystem to {macos_part}..."); self._run_command(["sudo","dd",f"if={self.temp_basesystem_hfs_path}",f"of={macos_part}","bs=4M","status=progress","oflag=sync"]) + self._report_progress("Mounting macOS USB partition..."); self._run_command(["sudo","mount",macos_part,self.mount_point_usb_macos_target]) + + # --- Finalizing macOS Installer Content on USB's HFS+ partition --- + self._report_progress("Finalizing macOS installer content on USB...") + usb_target_root = self.mount_point_usb_macos_target + + app_bundle_name = f"Install macOS {self.target_macos_version}.app" + app_bundle_path_usb = os.path.join(usb_target_root, app_bundle_name) + contents_path_usb = os.path.join(app_bundle_path_usb, "Contents") + shared_support_path_usb_app = os.path.join(contents_path_usb, "SharedSupport") + resources_path_usb_app = os.path.join(contents_path_usb, "Resources") # For createinstallmedia structure + sys_install_pkgs_usb = os.path.join(usb_target_root, "System", "Installation", "Packages") + coreservices_path_usb = os.path.join(usb_target_root, "System", "Library", "CoreServices") + + for p in [shared_support_path_usb_app, resources_path_usb_app, coreservices_path_usb, sys_install_pkgs_usb]: + self._run_command(["sudo", "mkdir", "-p", p]) + + # Copy BaseSystem.dmg & BaseSystem.chunklist + bs_dmg_src = self._find_gibmacos_asset("BaseSystem.dmg", product_folder_path, search_deep=True) + bs_chunklist_src = self._find_gibmacos_asset("BaseSystem.chunklist", product_folder_path, search_deep=True) + if bs_dmg_src: + self._report_progress(f"Copying BaseSystem.dmg to USB CoreServices and App SharedSupport...") + self._run_command(["sudo", "cp", bs_dmg_src, os.path.join(coreservices_path_usb, "BaseSystem.dmg")]) + self._run_command(["sudo", "cp", bs_dmg_src, os.path.join(shared_support_path_usb_app, "BaseSystem.dmg")]) + if bs_chunklist_src: + self._report_progress(f"Copying BaseSystem.chunklist to USB CoreServices and App SharedSupport...") + self._run_command(["sudo", "cp", bs_chunklist_src, os.path.join(coreservices_path_usb, "BaseSystem.chunklist")]) + self._run_command(["sudo", "cp", bs_chunklist_src, os.path.join(shared_support_path_usb_app, "BaseSystem.chunklist")]) + if not bs_dmg_src or not bs_chunklist_src: self._report_progress("Warning: BaseSystem.dmg or .chunklist not found in product folder.") + + # Copy InstallInfo.plist + installinfo_src = self._find_gibmacos_asset("InstallInfo.plist", product_folder_path, search_deep=True) + if installinfo_src: + self._report_progress(f"Copying InstallInfo.plist...") + self._run_command(["sudo", "cp", installinfo_src, os.path.join(contents_path_usb, "Info.plist")]) + self._run_command(["sudo", "cp", installinfo_src, os.path.join(usb_target_root, "InstallInfo.plist")]) + else: self._report_progress("Warning: InstallInfo.plist (source) not found.") + + # Copy main installer package(s) + main_pkg_src = self._find_gibmacos_asset("InstallAssistant.pkg", product_folder_path, search_deep=True) or self._find_gibmacos_asset("InstallESD.dmg", product_folder_path, search_deep=True) + if main_pkg_src: + pkg_basename = os.path.basename(main_pkg_src) + self._report_progress(f"Copying main payload '{pkg_basename}' to App SharedSupport and System Packages...") + self._run_command(["sudo", "cp", main_pkg_src, os.path.join(shared_support_path_usb_app, pkg_basename)]) + self._run_command(["sudo", "cp", main_pkg_src, os.path.join(sys_install_pkgs_usb, pkg_basename)]) + else: self._report_progress("Warning: Main installer package (InstallAssistant.pkg/InstallESD.dmg) not found.") + + diag_src = self._find_gibmacos_asset("AppleDiagnostics.dmg", product_folder_path, search_deep=True) + if diag_src: self._run_command(["sudo", "cp", diag_src, os.path.join(shared_support_path_usb_app, "AppleDiagnostics.dmg")]) + + template_boot_efi = os.path.join(OC_TEMPLATE_DIR, "EFI", "BOOT", "BOOTx64.efi") + if os.path.exists(template_boot_efi) and os.path.getsize(template_boot_efi) > 0: + self._run_command(["sudo", "cp", template_boot_efi, os.path.join(coreservices_path_usb, "boot.efi")]) + else: self._report_progress(f"Warning: Template BOOTx64.efi for installer's boot.efi not found or empty.") + + # Create .IAProductInfo (Simplified XML string to avoid f-string issues in tool call) + ia_product_info_path = os.path.join(usb_target_root, ".IAProductInfo") + ia_content_xml = "Product IDcom.apple.pkg.InstallAssistantProduct Path" + app_bundle_name + "/Contents/SharedSupport/InstallAssistant.pkg" + temp_ia_path = f"temp_iaproductinfo_{pid}.plist" + with open(temp_ia_path, "w") as f: f.write(ia_content_xml) + self._run_command(["sudo", "cp", temp_ia_path, ia_product_info_path]) + if os.path.exists(temp_ia_path): os.remove(temp_ia_path) + self._report_progress("Created .IAProductInfo.") + self._report_progress("macOS installer assets fully copied to USB.") + + # --- OpenCore EFI Setup --- + self._report_progress("Setting up OpenCore EFI on ESP..."); self._run_command(["sudo", "mount", esp_dev, self.mount_point_usb_esp]) + if not os.path.isdir(OC_TEMPLATE_DIR) or not os.listdir(OC_TEMPLATE_DIR): self._create_minimal_efi_template(self.temp_efi_build_dir) + else: self._run_command(["sudo", "cp", "-a", f"{OC_TEMPLATE_DIR}/.", self.temp_efi_build_dir]) + temp_config_plist_path = os.path.join(self.temp_efi_build_dir, "EFI", "OC", "config.plist") + if not os.path.exists(temp_config_plist_path): + template_plist = os.path.join(self.temp_efi_build_dir, "EFI", "OC", "config-template.plist") + if os.path.exists(template_plist): shutil.copy2(template_plist, temp_config_plist_path) + else: plistlib.dump({"#Comment": "Basic config by Skyscope"}, open(temp_config_plist_path, 'wb'), fmt=plistlib.PlistFormat.XML) + if self.enhance_plist_enabled and enhance_config_plist: + 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 processing complete.") + else: self._report_progress("config.plist enhancement call failed or had issues.") + 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/"], stream_rsync_progress=True) + + 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}"); self._report_progress(traceback.format_exc()) + return False + finally: + self._cleanup_temp_files_and_dirs() + +if __name__ == '__main__': + import traceback; from constants import MACOS_VERSIONS + if os.geteuid() != 0: print("Please run this script as root (sudo) for testing."); exit(1) + print("USB Writer Linux Standalone Test - Installer Method (Fuller Asset Copying Logic)") + mock_download_dir = f"temp_macos_download_skyscope_{os.getpid()}"; os.makedirs(mock_download_dir, exist_ok=True) + target_version_cli = sys.argv[1] if len(sys.argv) > 1 else "Sonoma" + mock_product_name_segment = MACOS_VERSIONS.get(target_version_cli, target_version_cli).lower() + mock_product_name = f"012-34567 - macOS {target_version_cli} {mock_product_name_segment}.x.x" + specific_product_folder = os.path.join(mock_download_dir, "macOS Downloads", "publicrelease", mock_product_name) + os.makedirs(os.path.join(specific_product_folder, "SharedSupport"), exist_ok=True); os.makedirs(specific_product_folder, exist_ok=True) + with open(os.path.join(specific_product_folder, "SharedSupport", "BaseSystem.dmg"), "wb") as f: f.write(os.urandom(10*1024*1024)) + with open(os.path.join(specific_product_folder, "SharedSupport", "BaseSystem.chunklist"), "w") as f: f.write("dummy chunklist") + with open(os.path.join(specific_product_folder, "InstallInfo.plist"), "wb") as f: plistlib.dump({"DisplayName":f"macOS {target_version_cli}"},f) + with open(os.path.join(specific_product_folder, "InstallAssistant.pkg"), "wb") as f: f.write(os.urandom(1024)) + with open(os.path.join(specific_product_folder, "SharedSupport", "AppleDiagnostics.dmg"), "wb") as f: f.write(os.urandom(1024)) + if not os.path.exists(OC_TEMPLATE_DIR): os.makedirs(OC_TEMPLATE_DIR, exist_ok=True) + if not os.path.exists(os.path.join(OC_TEMPLATE_DIR, "EFI", "OC")): os.makedirs(os.path.join(OC_TEMPLATE_DIR, "EFI", "OC"), exist_ok=True) + if not os.path.exists(os.path.join(OC_TEMPLATE_DIR, "EFI", "BOOT")): os.makedirs(os.path.join(OC_TEMPLATE_DIR, "EFI", "BOOT"), exist_ok=True) + with open(os.path.join(OC_TEMPLATE_DIR, "EFI", "OC", "config-template.plist"), "wb") as f: plistlib.dump({"Test":True}, f, fmt=plistlib.PlistFormat.XML) + with open(os.path.join(OC_TEMPLATE_DIR, "EFI", "BOOT", "BOOTx64.efi"), "w") as f: f.write("dummy bootx64.efi") + 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."); shutil.rmtree(mock_download_dir); shutil.rmtree(OC_TEMPLATE_DIR, ignore_errors=True); exit(1) + if input(f"Sure to wipe {test_device}? (yes/NO): ").lower() == 'yes': + writer = USBWriterLinux(test_device, mock_download_dir, print, True, target_version_cli) + writer.format_and_write() + else: print("Test cancelled.") + shutil.rmtree(mock_download_dir, ignore_errors=True); + # shutil.rmtree(OC_TEMPLATE_DIR, ignore_errors=True) # Usually keep template dir for other tests + print("Mock download dir cleaned up.") diff --git a/usb_writer_macos.py b/usb_writer_macos.py new file mode 100644 index 00000000..aa5353a8 --- /dev/null +++ b/usb_writer_macos.py @@ -0,0 +1,362 @@ +# usb_writer_macos.py (Refactoring for Installer Workflow) +import subprocess +import os +import time +import shutil +import glob +import plistlib +import traceback + +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.") + +OC_TEMPLATE_DIR = os.path.join(os.path.dirname(__file__), "EFI_template_installer") + +try: + from constants import MACOS_VERSIONS +except ImportError: + MACOS_VERSIONS = {"Sonoma": "14", "Ventura": "13", "Monterey": "12"} + print("Warning: constants.py not found, using fallback MACOS_VERSIONS for _get_gibmacos_product_folder.") + + +class USBWriterMacOS: + 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 + + pid = os.getpid() + self.temp_basesystem_hfs_path = f"/tmp/temp_basesystem_{pid}.hfs" + self.temp_efi_build_dir = f"/tmp/temp_efi_build_{pid}" + self.temp_dmg_extract_dir = f"/tmp/temp_dmg_extract_{pid}" + + self.mounted_usb_esp_path = None # Will be like /Volumes/EFI + self.mounted_usb_macos_path = None # Will be like /Volumes/Install macOS ... + self.mounted_source_basesystem_path = f"/tmp/source_basesystem_mount_{pid}" + + self.temp_files_to_clean = [self.temp_basesystem_hfs_path] + self.temp_dirs_to_clean = [ + self.temp_efi_build_dir, self.temp_dmg_extract_dir, + self.mounted_source_basesystem_path + # Actual USB mount points (/Volumes/EFI, /Volumes/Install macOS...) are unmounted, not rmdir'd from here + ] + self.attached_dmg_devices = [] # Store device paths from hdiutil attach + + def _report_progress(self, message: str, is_rsync_line: bool = False): + # Simplified progress for macOS writer for now, can add rsync parsing later if needed + if self.progress_callback: self.progress_callback(message) + else: print(message) + + def _run_command(self, command: list[str], check=True, capture_output=False, timeout=None, shell=False): + self._report_progress(f"Executing: {' '.join(command)}") + try: + process = subprocess.run(command, check=check, capture_output=capture_output, text=True, timeout=timeout, shell=shell) + 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]}' not found."); raise + + def _cleanup_temp_files_and_dirs(self): + self._report_progress("Cleaning up temporary files, directories, and mounts on macOS...") + + # Unmount our specific /tmp mount points first + if self.mounted_source_basesystem_path and os.path.ismount(self.mounted_source_basesystem_path): + self._unmount_path(self.mounted_source_basesystem_path, force=True) + # System mount points like /Volumes/EFI or /Volumes/Install macOS... are unmounted by diskutil unmountDisk or unmount + # We also add them to temp_dirs_to_clean if we used their dynamic path for rmdir later (but only if they were /tmp based) + + for dev_path in list(self.attached_dmg_devices): + self._detach_dmg(dev_path) + self.attached_dmg_devices = [] + + for f_path in self.temp_files_to_clean: + if os.path.exists(f_path): + try: os.remove(f_path) + except OSError 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) and d_path.startswith("/tmp/"): # Only remove /tmp dirs we created + try: shutil.rmtree(d_path, ignore_errors=True) + except OSError as e: self._report_progress(f"Error removing temp dir {d_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" + cmd = cmd_base + ([action, "force", target] if force else [action, target]) + + # Check if it's a valid target for unmount/unmountDisk + # For mount paths, check os.path.ismount. For devices, check if base device exists. + can_unmount = False + if is_device: + # Extract base disk identifier like /dev/diskX from /dev/diskXsY + base_device = re.match(r"(/dev/disk\d+)", target) + if base_device and os.path.exists(base_device.group(1)): + can_unmount = True + elif os.path.ismount(target): + can_unmount = True + + if can_unmount: + self._report_progress(f"Attempting to {action} {'forcefully ' if force else ''}{target}...") + self._run_command(cmd, check=False, timeout=60) # Increased timeout for diskutil + else: + self._report_progress(f"Skipping unmount for {target}, not a valid mount point or device for this action.") + + + def _detach_dmg(self, device_path): + if not device_path or not device_path.startswith("/dev/disk"): return + self._report_progress(f"Attempting to detach DMG device {device_path}...") + try: + # Ensure it's actually a virtual disk from hdiutil + is_virtual_disk = False + try: + info_result = self._run_command(["diskutil", "info", "-plist", device_path], capture_output=True) + if info_result.returncode == 0 and info_result.stdout: + disk_info = plistlib.loads(info_result.stdout.encode('utf-8')) + if disk_info.get("VirtualOrPhysical") == "Virtual": + is_virtual_disk = True + except Exception: pass # Ignore parsing errors, proceed to detach attempt + + if is_virtual_disk: + self._run_command(["hdiutil", "detach", device_path, "-force"], check=False, timeout=30) + else: + self._report_progress(f"{device_path} is not a virtual disk, or info check failed. Skipping direct hdiutil detach.") + + if device_path in self.attached_dmg_devices: + self.attached_dmg_devices.remove(device_path) + except Exception as e: + self._report_progress(f"Could not detach {device_path}: {e}") + + + def check_dependencies(self): + self._report_progress("Checking dependencies (diskutil, hdiutil, 7z, rsync, dd, bless)...") + dependencies = ["diskutil", "hdiutil", "7z", "rsync", "dd", "bless"] + missing_deps = [dep for dep in dependencies if not shutil.which(dep)] + if missing_deps: + msg = f"Missing dependencies: {', '.join(missing_deps)}. `7z` (p7zip) might need to be installed (e.g., via Homebrew: `brew install p7zip`). Others are standard." + self._report_progress(msg); raise RuntimeError(msg) + self._report_progress("All critical dependencies for macOS USB installer creation found.") + return True + + def _get_gibmacos_product_folder(self) -> str | None: + base_path = os.path.join(self.macos_download_path, "macOS Downloads", "publicrelease") + if not os.path.isdir(base_path): base_path = self.macos_download_path + if os.path.isdir(base_path): + for item in os.listdir(base_path): + item_path = os.path.join(base_path, item) + version_tag = MACOS_VERSIONS.get(self.target_macos_version, self.target_macos_version).lower() + if os.path.isdir(item_path) and (self.target_macos_version.lower() in item.lower() or version_tag in item.lower()): + self._report_progress(f"Identified gibMacOS product folder: {item_path}"); return item_path + self._report_progress(f"Could not identify a specific product folder for '{self.target_macos_version}'. Using general download path: {self.macos_download_path}"); return self.macos_download_path + + def _find_gibmacos_asset(self, asset_patterns: list[str] | str, product_folder_path: str | None = None, search_deep=True) -> str | None: + if isinstance(asset_patterns, str): asset_patterns = [asset_patterns] + search_base = product_folder_path or self.macos_download_path + self._report_progress(f"Searching for {asset_patterns} in {search_base} and subdirectories...") + for pattern in asset_patterns: + common_subdirs_for_pattern = ["", "SharedSupport", f"Install macOS {self.target_macos_version}.app/Contents/SharedSupport", f"Install macOS {self.target_macos_version}.app/Contents/Resources"] + for sub_dir_pattern in common_subdirs_for_pattern: + current_search_base = os.path.join(search_base, sub_dir_pattern) + glob_pattern = os.path.join(glob.escape(current_search_base), pattern) + found_files = glob.glob(glob_pattern, recursive=False) + if found_files: + found_files.sort(key=os.path.getsize, reverse=True) + self._report_progress(f"Found '{pattern}' at: {found_files[0]} (in {current_search_base})") + return found_files[0] + if search_deep: + deep_search_pattern = os.path.join(glob.escape(search_base), "**", pattern) + found_files_deep = sorted(glob.glob(deep_search_pattern, recursive=True), key=len) + if found_files_deep: + self._report_progress(f"Found '{pattern}' via deep search at: {found_files_deep[0]}") + return found_files_deep[0] + self._report_progress(f"Warning: Asset matching patterns '{asset_patterns}' not found in {search_base}.") + return None + + def _extract_hfs_from_dmg_or_pkg(self, dmg_or_pkg_path: str, output_hfs_path: str) -> bool: + os.makedirs(self.temp_dmg_extract_dir, exist_ok=True); current_target = dmg_or_pkg_path + try: + if dmg_or_pkg_path.endswith(".pkg"): + self._report_progress(f"Extracting DMG from PKG {current_target}..."); self._run_command(["7z", "e", "-txar", current_target, "*.dmg", f"-o{self.temp_dmg_extract_dir}"], check=True); dmgs_in_pkg = glob.glob(os.path.join(self.temp_dmg_extract_dir, "*.dmg")); assert dmgs_in_pkg, "No DMG in PKG."; current_target = max(dmgs_in_pkg, key=os.path.getsize, default=dmgs_in_pkg[0]); assert current_target, "No primary DMG in PKG."; self._report_progress(f"Using DMG from PKG: {current_target}") + assert current_target and current_target.endswith(".dmg"), f"Not a valid DMG: {current_target}" + basesystem_dmg_to_process = current_target + if "basesystem.dmg" not in os.path.basename(current_target).lower(): + self._report_progress(f"Extracting BaseSystem.dmg from {current_target}..."); self._run_command(["7z", "e", current_target, "*/BaseSystem.dmg", "-r", f"-o{self.temp_dmg_extract_dir}"], check=True); found_bs_dmg = glob.glob(os.path.join(self.temp_dmg_extract_dir, "**", "*BaseSystem.dmg"), recursive=True); assert found_bs_dmg, f"No BaseSystem.dmg from {current_target}"; basesystem_dmg_to_process = found_bs_dmg[0] + self._report_progress(f"Extracting HFS+ partition image from {basesystem_dmg_to_process}..."); self._run_command(["7z", "e", "-tdmg", basesystem_dmg_to_process, "*.hfs", f"-o{self.temp_dmg_extract_dir}"], check=True); hfs_files = glob.glob(os.path.join(self.temp_dmg_extract_dir, "*.hfs")); + if not hfs_files: self._run_command(["7z", "e", "-tdmg", basesystem_dmg_to_process, "*", f"-o{self.temp_dmg_extract_dir}"], check=True); hfs_files = [os.path.join(self.temp_dmg_extract_dir, f) for f in os.listdir(self.temp_dmg_extract_dir) if not f.lower().endswith((".xml",".chunklist",".plist")) and os.path.isfile(os.path.join(self.temp_dmg_extract_dir,f)) and os.path.getsize(os.path.join(self.temp_dmg_extract_dir,f)) > 2*1024*1024*1024] + assert hfs_files, f"No suitable HFS+ image file found after extracting {basesystem_dmg_to_process}" + final_hfs_file = max(hfs_files, key=os.path.getsize); self._report_progress(f"Found HFS+ 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: {e}\n{traceback.format_exc()}"); return False + finally: + if os.path.exists(self.temp_dmg_extract_dir): shutil.rmtree(self.temp_dmg_extract_dir, ignore_errors=True) + + def _create_minimal_efi_template(self, efi_dir_path): + self._report_progress(f"Minimal EFI template directory not found or empty. Creating basic structure at {efi_dir_path}"); oc_dir=os.path.join(efi_dir_path,"EFI","OC");os.makedirs(os.path.join(efi_dir_path,"EFI","BOOT"),exist_ok=True);os.makedirs(oc_dir,exist_ok=True);[os.makedirs(os.path.join(oc_dir,s),exist_ok=True) for s in ["Drivers","Kexts","ACPI","Tools","Resources"]];open(os.path.join(efi_dir_path,"EFI","BOOT","BOOTx64.efi"),"w").close();open(os.path.join(oc_dir,"OpenCore.efi"),"w").close();bc={"#Comment":"Basic config","Misc":{"Security":{"ScanPolicy":0,"SecureBootModel":"Disabled"}},"PlatformInfo":{"Generic":{"MLB":"CHANGE_ME_MLB","SystemSerialNumber":"CHANGE_ME_SERIAL","SystemUUID":"CHANGE_ME_UUID","ROM":b"\0"*6}}};plistlib.dump(bc,open(os.path.join(oc_dir,"config.plist"),'wb'),fmt=plistlib.PlistFormat.XML) + + def format_and_write(self) -> bool: + try: + self.check_dependencies() + self._cleanup_temp_files_and_dirs() + for mp_dir in self.temp_dirs_to_clean: + if not os.path.exists(mp_dir): os.makedirs(mp_dir, exist_ok=True) + + self._report_progress(f"WARNING: ALL DATA ON {self.device} WILL BE ERASED!") + self._run_command(["diskutil", "unmountDisk", "force", self.device], check=False, timeout=60); time.sleep(2) + + installer_vol_name = f"Install macOS {self.target_macos_version}" + self._report_progress(f"Partitioning {self.device} for '{installer_vol_name}'...") + self._run_command(["diskutil", "partitionDisk", self.device, "GPT", "FAT32", "EFI", "551MiB", "JHFS+", installer_vol_name, "0b"], timeout=180); time.sleep(3) + + disk_info_plist_str = self._run_command(["diskutil", "list", "-plist", self.device], capture_output=True).stdout + if not disk_info_plist_str: raise RuntimeError("Failed to get disk info after partitioning.") + disk_info = plistlib.loads(disk_info_plist_str.encode('utf-8')) + + esp_partition_dev = None; macos_partition_dev = None + main_disk_entry = next((d for d in disk_info.get("AllDisksAndPartitions", []) if d.get("DeviceIdentifier") == self.device.replace("/dev/", "")), None) + if main_disk_entry: + for part in main_disk_entry.get("Partitions", []): + if part.get("Content") == "EFI": esp_partition_dev = f"/dev/{part.get('DeviceIdentifier')}" + elif part.get("VolumeName") == installer_vol_name: macos_partition_dev = f"/dev/{part.get('DeviceIdentifier')}" + + if not (esp_partition_dev and macos_partition_dev): raise RuntimeError(f"Could not identify partitions on {self.device} (EFI: {esp_partition_dev}, macOS: {macos_partition_dev}). Check diskutil list output.") + self._report_progress(f"Identified ESP: {esp_partition_dev}, macOS Partition: {macos_partition_dev}") + + product_folder_path = self._get_gibmacos_product_folder() + source_for_hfs_extraction = self._find_gibmacos_asset(["BaseSystem.dmg", "InstallESD.dmg", "SharedSupport.dmg", "InstallAssistant.pkg"], product_folder_path, "BaseSystem.dmg (or source like InstallESD.dmg/SharedSupport.dmg/InstallAssistant.pkg)") + if not source_for_hfs_extraction: raise RuntimeError("Essential macOS DMG/PKG for BaseSystem extraction not found in download path.") + + if not self._extract_hfs_from_dmg_or_pkg(source_for_hfs_extraction, self.temp_basesystem_hfs_path): + raise RuntimeError("Failed to extract HFS+ image from BaseSystem assets.") + + raw_macos_partition_dev = macos_partition_dev.replace("/dev/disk", "/dev/rdisk") + self._report_progress(f"Writing BaseSystem HFS+ image to {raw_macos_partition_dev} using dd...") + self._run_command(["sudo", "dd", f"if={self.temp_basesystem_hfs_path}", f"of={raw_macos_partition_dev}", "bs=1m"], timeout=1800) + + self.mounted_usb_macos_path = f"/Volumes/{installer_vol_name}" + if not os.path.ismount(self.mounted_usb_macos_path): + self._run_command(["diskutil", "mount", "-mountPoint", self.temp_usb_macos_target_mount, macos_partition_dev]) + self.mounted_usb_macos_path = self.temp_usb_macos_target_mount + + self._report_progress(f"macOS partition mounted at {self.mounted_usb_macos_path}") + + usb_target_root = self.mounted_usb_macos_path + app_bundle_name = f"Install macOS {self.target_macos_version}.app" + app_bundle_path_usb = os.path.join(usb_target_root, app_bundle_name) + contents_path_usb = os.path.join(app_bundle_path_usb, "Contents") + shared_support_path_usb_app = os.path.join(contents_path_usb, "SharedSupport") + resources_path_usb_app = os.path.join(contents_path_usb, "Resources") + sys_install_pkgs_usb = os.path.join(usb_target_root, "System", "Installation", "Packages") + coreservices_path_usb = os.path.join(usb_target_root, "System", "Library", "CoreServices") + + for p in [shared_support_path_usb_app, resources_path_usb_app, coreservices_path_usb, sys_install_pkgs_usb]: + self._run_command(["sudo", "mkdir", "-p", p]) + + for f_name in ["BaseSystem.dmg", "BaseSystem.chunklist"]: + src_file = self._find_gibmacos_asset(f_name, product_folder_path, search_deep=True) + if src_file: self._run_command(["sudo", "cp", src_file, os.path.join(shared_support_path_usb_app, os.path.basename(src_file))]); self._run_command(["sudo", "cp", src_file, os.path.join(coreservices_path_usb, os.path.basename(src_file))]) + else: self._report_progress(f"Warning: {f_name} not found.") + + installinfo_src = self._find_gibmacos_asset("InstallInfo.plist", product_folder_path, search_deep=True) + if installinfo_src: self._run_command(["sudo", "cp", installinfo_src, os.path.join(contents_path_usb, "Info.plist")]); self._run_command(["sudo", "cp", installinfo_src, os.path.join(usb_target_root, "InstallInfo.plist")]) + else: self._report_progress("Warning: InstallInfo.plist not found.") + + main_pkg_src = self._find_gibmacos_asset(["InstallAssistant.pkg", "InstallESD.dmg"], product_folder_path, search_deep=True) + if main_pkg_src: pkg_basename = os.path.basename(main_pkg_src); self._run_command(["sudo", "cp", main_pkg_src, os.path.join(shared_support_path_usb_app, pkg_basename)]); self._run_command(["sudo", "cp", main_pkg_src, os.path.join(sys_install_pkgs_usb, pkg_basename)]) + else: self._report_progress("Warning: Main installer PKG/DMG not found.") + + diag_src = self._find_gibmacos_asset("AppleDiagnostics.dmg", product_folder_path, search_deep=True) + if diag_src: self._run_command(["sudo", "cp", diag_src, os.path.join(shared_support_path_usb_app, "AppleDiagnostics.dmg")]) + + template_boot_efi = os.path.join(OC_TEMPLATE_DIR, "EFI", "BOOT", "BOOTx64.efi") + if os.path.exists(template_boot_efi) and os.path.getsize(template_boot_efi) > 0: self._run_command(["sudo", "cp", template_boot_efi, os.path.join(coreservices_path_usb, "boot.efi")]) + else: self._report_progress(f"Warning: Template BOOTx64.efi for installer's boot.efi not found or empty.") + + ia_product_info_path = os.path.join(usb_target_root, ".IAProductInfo") + ia_content_xml = "Product IDcom.apple.pkg.InstallAssistantProduct Path" + app_bundle_name + "/Contents/SharedSupport/InstallAssistant.pkg" + temp_ia_path = f"/tmp/temp_iaproductinfo_{pid}.plist" + with open(temp_ia_path, "w") as f: f.write(ia_content_xml) + self._run_command(["sudo", "cp", temp_ia_path, ia_product_info_path]) + if os.path.exists(temp_ia_path): os.remove(temp_ia_path) + + self._report_progress("macOS installer assets copied.") + + self._report_progress("Setting up OpenCore EFI on ESP...") + self.mounted_usb_esp_path = f"/Volumes/EFI" # Default mount path for ESP + if not os.path.ismount(self.mounted_usb_esp_path): + self._run_command(["diskutil", "mount", "-mountPoint", self.temp_usb_esp_mount, esp_partition_dev]) + self.mounted_usb_esp_path = self.temp_usb_esp_mount + + if not os.path.isdir(OC_TEMPLATE_DIR) or not os.listdir(OC_TEMPLATE_DIR): self._create_minimal_efi_template(self.temp_efi_build_dir) + else: self._run_command(["cp", "-a", f"{OC_TEMPLATE_DIR}/.", self.temp_efi_build_dir]) + + temp_config_plist_path = os.path.join(self.temp_efi_build_dir, "EFI", "OC", "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")): + shutil.copy2(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 (note: hardware detection is Linux-only)...") + if enhance_config_plist(temp_config_plist_path, self.target_macos_version, self._report_progress): self._report_progress("config.plist enhancement complete.") + else: self._report_progress("config.plist enhancement call failed or had issues.") + + self._report_progress(f"Copying final EFI folder to USB ESP ({self.mounted_usb_esp_path})...") + self._run_command(["sudo", "rsync", "-avh", "--delete", f"{self.temp_efi_build_dir}/EFI/", f"{self.mounted_usb_esp_path}/EFI/"]) + + self._report_progress(f"Blessing the installer volume: {self.mounted_usb_macos_path}") + bless_target_folder = os.path.join(self.mounted_usb_macos_path, "System", "Library", "CoreServices") + self._run_command(["sudo", "bless", "--folder", bless_target_folder, "--label", installer_vol_name, "--setBoot"], check=False) + + 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 on macOS: {e}"); self._report_progress(traceback.format_exc()) + return False + finally: + self._cleanup_temp_files_and_dirs() + +if __name__ == '__main__': + import traceback + from constants import MACOS_VERSIONS + if platform.system() != "Darwin": print("This script is intended for macOS for standalone testing."); exit(1) + print("USB Writer macOS Standalone Test - Installer Method") + mock_download_dir = f"/tmp/temp_macos_download_skyscope_{os.getpid()}"; os.makedirs(mock_download_dir, exist_ok=True) + target_version_cli = sys.argv[1] if len(sys.argv) > 1 else "Sonoma" + mock_product_name_segment = MACOS_VERSIONS.get(target_version_cli, target_version_cli).lower() + mock_product_name = f"012-34567 - macOS {target_version_cli} {mock_product_name_segment}.x.x" + mock_product_folder_path = os.path.join(mock_download_dir, "macOS Downloads", "publicrelease", mock_product_name) + os.makedirs(os.path.join(mock_product_folder_path, "SharedSupport"), exist_ok=True) + with open(os.path.join(mock_product_folder_path, "SharedSupport", "BaseSystem.dmg"), "wb") as f: f.write(os.urandom(10*1024*1024)) + with open(os.path.join(mock_product_folder_path, "SharedSupport", "BaseSystem.chunklist"), "w") as f: f.write("dummy chunklist") + with open(os.path.join(mock_product_folder_path, "InstallInfo.plist"), "wb") as f: plistlib.dump({"DisplayName":f"macOS {target_version_cli}"},f) + with open(os.path.join(mock_product_folder_path, "InstallAssistant.pkg"), "wb") as f: f.write(os.urandom(1024)) + with open(os.path.join(mock_product_folder_path, "SharedSupport", "AppleDiagnostics.dmg"), "wb") as f: f.write(os.urandom(1024)) + + if not os.path.exists(OC_TEMPLATE_DIR): os.makedirs(OC_TEMPLATE_DIR, exist_ok=True) + if not os.path.exists(os.path.join(OC_TEMPLATE_DIR, "EFI", "OC")): os.makedirs(os.path.join(OC_TEMPLATE_DIR, "EFI", "OC"), exist_ok=True) + if not os.path.exists(os.path.join(OC_TEMPLATE_DIR, "EFI", "BOOT")): os.makedirs(os.path.join(OC_TEMPLATE_DIR, "EFI", "BOOT"), exist_ok=True) + dummy_config_template_path = os.path.join(OC_TEMPLATE_DIR, "EFI", "OC", "config.plist") + if not os.path.exists(dummy_config_template_path): + with open(dummy_config_template_path, "wb") as f: plistlib.dump({"TestTemplate":True}, f, fmt=plistlib.PlistFormat.XML) + dummy_bootx64_efi_path = os.path.join(OC_TEMPLATE_DIR, "EFI", "BOOT", "BOOTx64.efi") + if not os.path.exists(dummy_bootx64_efi_path): + with open(dummy_bootx64_efi_path, "w") as f: f.write("dummy bootx64.efi content") + + + print("\nAvailable external physical disks (use 'diskutil list external physical'):"); subprocess.run(["diskutil", "list", "external", "physical"], check=False) + test_device = input("\nEnter target disk identifier (e.g., /dev/diskX). THIS DISK WILL BE WIPED: ") + if not test_device or not test_device.startswith("/dev/disk"): print("Invalid disk."); shutil.rmtree(mock_download_dir, ignore_errors=True); exit(1) + if input(f"Sure to wipe {test_device}? (yes/NO): ").lower() == 'yes': + writer = USBWriterMacOS(test_device, mock_download_dir, print, True, target_version_cli) + writer.format_and_write() + else: print("Test cancelled.") + shutil.rmtree(mock_download_dir, ignore_errors=True) + # Deliberately not cleaning OC_TEMPLATE_DIR in test, as it might be shared or pre-existing. + print("Mock download dir cleaned up.") diff --git a/usb_writer_windows.py b/usb_writer_windows.py new file mode 100644 index 00000000..ecf34500 --- /dev/null +++ b/usb_writer_windows.py @@ -0,0 +1,623 @@ +# usb_writer_windows.py (Refining EFI setup and manual step guidance) +import subprocess +import os +import time +import shutil +import re +import glob +import plistlib +import traceback +import sys # Added for psutil check + +try: + from PyQt6.QtWidgets import QMessageBox +except ImportError: + # Mock QMessageBox for standalone testing or if PyQt6 is not available + class QMessageBox: + Information = 1 # Dummy enum value + Warning = 2 # Dummy enum value + Question = 3 # Dummy enum value + YesRole = 0 # Dummy role + NoRole = 1 # Dummy role + + @staticmethod + def information(parent, title, message, buttons=None, defaultButton=None): + print(f"INFO (QMessageBox mock): Title='{title}', Message='{message}'") + return QMessageBox.Yes # Simulate a positive action if needed + @staticmethod + def warning(parent, title, message, buttons=None, defaultButton=None): + print(f"WARNING (QMessageBox mock): Title='{title}', Message='{message}'") + return QMessageBox.Yes # Simulate a positive action + @staticmethod + def critical(parent, title, message, buttons=None, defaultButton=None): + print(f"CRITICAL (QMessageBox mock): Title='{title}', Message='{message}'") + return QMessageBox.Yes # Simulate a positive action + # Add other static methods if your code uses them, e.g. question + @staticmethod + def question(parent, title, message, buttons=None, defaultButton=None): + print(f"QUESTION (QMessageBox mock): Title='{title}', Message='{message}'") + return QMessageBox.Yes # Simulate 'Yes' for testing + + # Dummy button values if your code checks for specific button results + Yes = 0x00004000 + No = 0x00010000 + Cancel = 0x00400000 + + +try: + from plist_modifier import enhance_config_plist +except ImportError: + print("Warning: plist_modifier not found. Enhancement will be skipped.") + def enhance_config_plist(plist_path, macos_version, progress_callback): + if progress_callback: + progress_callback("Skipping plist enhancement: plist_modifier not available.") + return False # Indicate failure or no action + +# This path needs to be correct relative to where usb_writer_windows.py is, or use an absolute path strategy +OC_TEMPLATE_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "EFI_template_installer") + +class USBWriterWindows: + def __init__(self, device_id_str: str, macos_download_path: str, + progress_callback=None, enhance_plist_enabled: bool = False, + target_macos_version: str = ""): + self.device_id_str = device_id_str + self.disk_number = "".join(filter(str.isdigit, device_id_str)) + self.physical_drive_path = f"\\\\.\\PhysicalDrive{self.disk_number}" + 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 + + pid = os.getpid() + # Use system temp for Windows more reliably + self.temp_dir_base = os.path.join(os.environ.get("TEMP", "C:\\Temp"), f"skyscope_usb_temp_{pid}") + self.temp_basesystem_hfs_path = os.path.join(self.temp_dir_base, f"temp_basesystem_{pid}.hfs") + self.temp_efi_build_dir = os.path.join(self.temp_dir_base, f"temp_efi_build_{pid}") + self.temp_dmg_extract_dir = os.path.join(self.temp_dir_base, f"temp_dmg_extract_{pid}") + + self.temp_files_to_clean = [self.temp_basesystem_hfs_path] # Specific files outside temp_dir_base (if any) + self.temp_dirs_to_clean = [self.temp_dir_base] # Base temp dir for this instance + 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, creationflags=0): + 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=creationflags) + 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 = os.path.join(self.temp_dir_base, f"diskpart_script_{os.getpid()}.txt") + os.makedirs(self.temp_dir_base, exist_ok=True) + output_text = None + try: + self._report_progress(f"Running diskpart script:\n{script_content}") + with open(script_file_path, "w") as f: f.write(script_content) + # Use CREATE_NO_WINDOW for subprocess.run with diskpart + process = self._run_command(["diskpart", "/s", script_file_path], capture_output=True, check=False, creationflags=subprocess.CREATE_NO_WINDOW) + output_text = (process.stdout or "") + "\n" + (process.stderr or "") + if capture_output_for_parse: return output_text + finally: + if os.path.exists(script_file_path): + try: os.remove(script_file_path) + except OSError as e: self._report_progress(f"Warning: Could not remove temp diskpart script {script_file_path}: {e}") + return None # Explicitly return None if not capturing for parse or if it fails before return + + def _cleanup_temp_files_and_dirs(self): + self._report_progress("Cleaning up temporary files and directories on Windows...") + for f_path in self.temp_files_to_clean: + if os.path.exists(f_path): + try: os.remove(f_path) + except OSError as e: self._report_progress(f"Error removing file {f_path}: {e}") + + for d_path in self.temp_dirs_to_clean: # self.temp_dir_base is the main one + if os.path.exists(d_path): + try: shutil.rmtree(d_path, ignore_errors=False) # Try with ignore_errors=False first + except OSError as e: + self._report_progress(f"Error removing dir {d_path}: {e}. Attempting force remove.") + try: shutil.rmtree(d_path, ignore_errors=True) # Fallback to ignore_errors=True + except OSError as e_force: self._report_progress(f"Force remove for dir {d_path} also failed: {e_force}") + + + def _find_available_drive_letter(self) -> str | None: + import string + used_letters = set() + try: + # Try to use psutil if available (e.g., when run from main_app.py) + if 'psutil' in sys.modules: + import psutil # Ensure it's imported here if check passes + partitions = psutil.disk_partitions(all=True) + for p in partitions: + if p.mountpoint and len(p.mountpoint) == 2 and p.mountpoint[1] == ':': + used_letters.add(p.mountpoint[0].upper()) + else: # Fallback if psutil is not available (e.g. pure standalone script) + self._report_progress("psutil not available, using limited drive letter detection.") + # Basic check, might not be exhaustive + for letter in string.ascii_uppercase[3:]: # D onwards + if os.path.exists(f"{letter}:\\"): + used_letters.add(letter) + + except Exception as e: + self._report_progress(f"Error detecting used drive letters: {e}. Proceeding with caution.") + + # Prefer letters from S onwards, less likely to conflict with user drives + for letter in "STUVWXYZGHIJKLMNOPQR": + if letter not in used_letters and letter > 'C': # Ensure it's not A, B, C + return letter + return None + + def check_dependencies(self): + self._report_progress("Checking dependencies (diskpart, robocopy, 7z, dd for Windows [manual check])...") + dependencies = ["diskpart", "robocopy", "7z"] + missing = [dep for dep in dependencies if not shutil.which(dep)] + if missing: + msg = f"Missing dependencies: {', '.join(missing)}. `diskpart` & `robocopy` should be standard. `7z.exe` (7-Zip) needs to be installed and its directory added to the system PATH." + self._report_progress(msg) + raise RuntimeError(msg) + self._report_progress("Please ensure a 'dd for Windows' utility (e.g., from SUSE, Cygwin, or http://www.chrysocome.net/dd) is installed and accessible from your PATH for writing the main macOS BaseSystem image.") + return True + + def _find_gibmacos_asset(self, asset_name: str, product_folder_path: str | None = None, search_deep=True) -> str | None: + search_locations = [] + if product_folder_path and os.path.isdir(product_folder_path): + search_locations.extend([product_folder_path, os.path.join(product_folder_path, "SharedSupport")]) + + # Also search directly in macos_download_path and a potential "macOS Install Data" subdirectory + search_locations.extend([self.macos_download_path, os.path.join(self.macos_download_path, "macOS Install Data")]) + + # If a version-specific folder exists at the root of macos_download_path (less common for gibMacOS structure) + if os.path.isdir(self.macos_download_path): + for item in os.listdir(self.macos_download_path): + item_path = os.path.join(self.macos_download_path, item) + if os.path.isdir(item_path) and self.target_macos_version.lower() in item.lower(): + search_locations.append(item_path) + search_locations.append(os.path.join(item_path, "SharedSupport")) + # Assuming first match is good enough for this heuristic + break + + # Deduplicate search locations while preserving order (Python 3.7+) + search_locations = list(dict.fromkeys(search_locations)) + + for loc in search_locations: + if not os.path.isdir(loc): continue + + path = os.path.join(loc, asset_name) + if os.path.exists(path): + self._report_progress(f"Found '{asset_name}' at: {path}") + return path + + # Case-insensitive glob as fallback for direct name match + # Create a pattern like "[bB][aA][sS][eE][sS][yY][sS][tT][eE][mM].[dD][mM][gG]" + pattern_parts = [f"[{c.lower()}{c.upper()}]" if c.isalpha() else re.escape(c) for c in asset_name] + insensitive_glob_pattern = "".join(pattern_parts) + + found_files = glob.glob(os.path.join(loc, insensitive_glob_pattern), recursive=False) + if found_files: + self._report_progress(f"Found '{asset_name}' via case-insensitive glob at: {found_files[0]}") + return found_files[0] + + if search_deep: + self._report_progress(f"Asset '{asset_name}' not found in primary locations, starting deep search in {self.macos_download_path}...") + deep_search_pattern = os.path.join(self.macos_download_path, "**", asset_name) + # Sort by length to prefer shallower paths, then alphabetically + found_files_deep = sorted(glob.glob(deep_search_pattern, recursive=True), key=lambda p: (len(os.path.dirname(p)), p)) + if found_files_deep: + self._report_progress(f"Found '{asset_name}' via deep search at: {found_files_deep[0]}") + return found_files_deep[0] + + self._report_progress(f"Warning: Asset '{asset_name}' not found.") + return None + + def _get_gibmacos_product_folder(self) -> str | None: + # constants.py should be in the same directory or Python path + try: from constants import MACOS_VERSIONS + except ImportError: MACOS_VERSIONS = {} ; self._report_progress("Warning: MACOS_VERSIONS from constants.py not loaded.") + + # Standard gibMacOS download structure: macOS Downloads/publicrelease/012-34567 - macOS Sonoma 14.0 + base_path = os.path.join(self.macos_download_path, "macOS Downloads", "publicrelease") + if not os.path.isdir(base_path): + # Fallback if "macOS Downloads/publicrelease" is not present, use macos_download_path directly + base_path = self.macos_download_path + + if os.path.isdir(base_path): + potential_folders = [] + for item in os.listdir(base_path): + item_path = os.path.join(base_path, item) + # Check if it's a directory and matches target_macos_version (name or tag) + version_tag_from_constants = MACOS_VERSIONS.get(self.target_macos_version, self.target_macos_version.lower().replace(" ", "")) + if os.path.isdir(item_path) and \ + (self.target_macos_version.lower() in item.lower() or \ + version_tag_from_constants.lower() in item.lower().replace(" ", "")): + potential_folders.append(item_path) + + if potential_folders: + # Sort by length (prefer shorter, more direct matches) or other heuristics if needed + best_match = min(potential_folders, key=len) + self._report_progress(f"Identified gibMacOS product folder: {best_match}") + return best_match + + self._report_progress(f"Could not identify a specific product folder for '{self.target_macos_version}'. Using general download path: {self.macos_download_path}") + return self.macos_download_path # Fallback to the root download path + + def _extract_hfs_from_dmg_or_pkg(self, dmg_or_pkg_path: str, output_hfs_path: str) -> bool: + temp_extract_dir = self.temp_dmg_extract_dir + os.makedirs(temp_extract_dir, exist_ok=True) + current_target = dmg_or_pkg_path + try: + if not os.path.exists(current_target): + self._report_progress(f"Error: Input file for HFS extraction does not exist: {current_target}"); return False + + # Step 1: If it's a PKG, extract DMGs from it. + if dmg_or_pkg_path.lower().endswith(".pkg"): + self._report_progress(f"Extracting DMG(s) from PKG: {current_target} using 7z...") + # Using 'e' to extract flat, '-txar' for PKG/XAR format. + self._run_command(["7z", "e", "-txar", current_target, "*.dmg", f"-o{temp_extract_dir}", "-y"], check=True) + dmgs_in_pkg = glob.glob(os.path.join(temp_extract_dir, "*.dmg")) + if not dmgs_in_pkg: self._report_progress(f"No DMG files found after extracting PKG: {current_target}"); return False + # Select the largest DMG, assuming it's the main one. + current_target = max(dmgs_in_pkg, key=os.path.getsize, default=None) + if not current_target: self._report_progress("Failed to select a DMG from PKG contents."); return False + self._report_progress(f"Using DMG from PKG: {current_target}") + + # Step 2: Ensure we have a DMG file. + if not current_target or not current_target.lower().endswith(".dmg"): + self._report_progress(f"Not a valid DMG file for HFS extraction: {current_target}"); return False + + basesystem_dmg_to_process = current_target + # Step 3: If the DMG is not BaseSystem.dmg, try to extract BaseSystem.dmg from it. + # This handles cases like SharedSupport.dmg containing BaseSystem.dmg. + if "basesystem.dmg" not in os.path.basename(current_target).lower(): + self._report_progress(f"Extracting BaseSystem.dmg from container DMG: {current_target} using 7z...") + # Extract recursively, looking for any path that includes BaseSystem.dmg + self._run_command(["7z", "e", current_target, "*/BaseSystem.dmg", "-r", f"-o{temp_extract_dir}", "-y"], check=True) + found_bs_dmg_list = glob.glob(os.path.join(temp_extract_dir, "**", "*BaseSystem.dmg"), recursive=True) + if not found_bs_dmg_list: self._report_progress(f"No BaseSystem.dmg found within {current_target}"); return False + basesystem_dmg_to_process = max(found_bs_dmg_list, key=os.path.getsize, default=None) # Largest if multiple + if not basesystem_dmg_to_process: self._report_progress("Failed to select BaseSystem.dmg from container."); return False + self._report_progress(f"Processing extracted BaseSystem.dmg: {basesystem_dmg_to_process}") + + # Step 4: Extract HFS partition image from BaseSystem.dmg. + self._report_progress(f"Extracting HFS+ partition image from {basesystem_dmg_to_process} using 7z...") + # Using 'e' to extract flat, '-tdmg' for DMG format. Looking for '*.hfs' or specific partition files. + # Common HFS file names inside BaseSystem.dmg are like '2.hfs' or similar. + # Sometimes they don't have .hfs extension, 7z might list them by index. + # We will try to extract any .hfs file. + self._run_command(["7z", "e", "-tdmg", basesystem_dmg_to_process, "*.hfs", f"-o{temp_extract_dir}", "-y"], check=True) + hfs_files = glob.glob(os.path.join(temp_extract_dir, "*.hfs")) + + if not hfs_files: # If no .hfs, try extracting by common partition indices if 7z supports listing them for DMG + self._report_progress("No direct '*.hfs' found. Attempting extraction of common HFS partition by index (e.g., '2', '3')...") + # This is more complex as 7z CLI might not easily allow extracting by index directly without listing first. + # For now, we rely on .hfs existing. If this fails, user might need to extract manually with 7z GUI. + # A more robust solution would involve listing contents and then extracting the correct file. + self._report_progress("Extraction by index is not implemented. Please ensure BaseSystem.dmg contains a directly extractable .hfs file.") + return False + + if not hfs_files: self._report_progress(f"No HFS files found after extracting DMG: {basesystem_dmg_to_process}"); return False + + final_hfs_file = max(hfs_files, key=os.path.getsize, default=None) # Largest HFS file + if not final_hfs_file: self._report_progress("Failed to select HFS file."); return False + + self._report_progress(f"Found HFS+ 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: {e}\n{traceback.format_exc()}"); return False + + def _create_minimal_efi_template_content(self, efi_dir_path_root): + self._report_progress(f"Minimal EFI template directory '{OC_TEMPLATE_DIR}' not found or is empty. Creating basic structure at {efi_dir_path_root}") + efi_path = os.path.join(efi_dir_path_root, "EFI") + oc_dir = os.path.join(efi_path, "OC") + os.makedirs(os.path.join(efi_path, "BOOT"), exist_ok=True) + os.makedirs(oc_dir, exist_ok=True) + for sub_dir in ["Drivers", "Kexts", "ACPI", "Tools", "Resources"]: + os.makedirs(os.path.join(oc_dir, sub_dir), exist_ok=True) + + # Create dummy BOOTx64.efi and OpenCore.efi + with open(os.path.join(efi_path, "BOOT", "BOOTx64.efi"), "w") as f: f.write("Minimal Boot") + with open(os.path.join(oc_dir, "OpenCore.efi"), "w") as f: f.write("Minimal OC") + + # Create a very basic config.plist + basic_config = { + "#WARNING": "This is a minimal config.plist. Replace with a full one for booting macOS!", + "Misc": {"Security": {"ScanPolicy": 0, "SecureBootModel": "Disabled"}}, + "PlatformInfo": {"Generic": {"MLB": "CHANGE_ME_MLB", "SystemSerialNumber": "CHANGE_ME_SERIAL", "SystemUUID": "CHANGE_ME_UUID", "ROM": b"\x00\x00\x00\x00\x00\x00"}}, + "NVRAM": {"Add": {"4D1EDE05-38C7-4A6A-9CC6-4BCCA8B38C14": {"DefaultBackgroundColor": "00000000", "UIScale": "01"}}}, # Basic NVRAM + "UEFI": {"Drivers": ["OpenRuntime.efi"], "Input": {"KeySupport": True}} # Example + } + config_plist_path = os.path.join(oc_dir, "config.plist") + try: + with open(config_plist_path, 'wb') as fp: + plistlib.dump(basic_config, fp, fmt=plistlib.PlistFormat.XML) + self._report_progress(f"Created minimal config.plist at {config_plist_path}") + except Exception as e: + self._report_progress(f"Error creating minimal config.plist: {e}") + + + def format_and_write(self) -> bool: + try: + self.check_dependencies() + if os.path.exists(self.temp_dir_base): + self._report_progress(f"Cleaning up existing temp base directory: {self.temp_dir_base}") + shutil.rmtree(self.temp_dir_base, ignore_errors=True) + os.makedirs(self.temp_dir_base, exist_ok=True) + os.makedirs(self.temp_efi_build_dir, exist_ok=True) # For building EFI contents before copy + os.makedirs(self.temp_dmg_extract_dir, exist_ok=True) # For 7z extractions + + self._report_progress(f"WARNING: ALL DATA ON DISK {self.disk_number} ({self.physical_drive_path}) WILL BE ERASED!") + # Optional: Add a QMessageBox.question here for final confirmation in GUI mode + + 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.") + + installer_vol_label = f"Install macOS {self.target_macos_version}" + # Ensure label for diskpart is max 32 chars for FAT32. "Install macOS Monterey" is 23 chars. + diskpart_script_part1 = f"select disk {self.disk_number}\nclean\nconvert gpt\n" + # Create EFI (ESP) partition, 550MB is generous and common + diskpart_script_part1 += f"create partition efi size=550\nformat fs=fat32 quick label=EFI\nassign letter={self.assigned_efi_letter}\n" + # Create main macOS partition (HFS+). Let diskpart use remaining space. + # AF00 is Apple HFS+ type GUID. For APFS, it's 7C3457EF-0000-11AA-AA11-00306543ECAC + # We create as HFS+ because BaseSystem is HFS+. Installer will convert if needed. + diskpart_script_part1 += f"create partition primary label=\"{installer_vol_label[:31]}\" id=AF00\nexit\n" + + self._run_diskpart_script(diskpart_script_part1) + self._report_progress("Disk partitioning complete. Waiting for volumes to settle...") + time.sleep(5) # Give Windows time to recognize new partitions + + macos_partition_number_str = "2 (typically)"; macos_partition_offset_str = "Offset not automatically determined for Windows dd" + try: + # Attempt to get partition details. This is informational. + diskpart_script_detail = f"select disk {self.disk_number}\nlist partition\nexit\n" + detail_output = self._run_diskpart_script(diskpart_script_detail, capture_output_for_parse=True) + if detail_output: + # Try to find Partition 2, assuming it's our target HFS+ partition + part_match = re.search(r"Partition 2\s+Primary\s+\d+\s+[GMK]B\s+(\d+)\s+[GMK]B", detail_output, re.IGNORECASE) + if part_match: + macos_partition_offset_str = f"{part_match.group(1)} MB (approx. from start of disk for Partition 2)" + else: # Fallback if specific regex fails + self._report_progress("Could not parse partition 2 offset, using generic message.") + except Exception as e: + self._report_progress(f"Could not get detailed partition info from diskpart: {e}") + + + # --- OpenCore EFI Setup --- + self._report_progress("Setting up OpenCore EFI on ESP...") + if not os.path.isdir(OC_TEMPLATE_DIR) or not os.listdir(OC_TEMPLATE_DIR): + self._report_progress(f"EFI_template_installer at '{OC_TEMPLATE_DIR}' is missing or empty.") + self._create_minimal_efi_template_content(self.temp_efi_build_dir) # Create in temp_efi_build_dir + else: + self._report_progress(f"Copying EFI template from {OC_TEMPLATE_DIR} to {self.temp_efi_build_dir}") + shutil.copytree(OC_TEMPLATE_DIR, self.temp_efi_build_dir, dirs_exist_ok=True) + + temp_config_plist_path = os.path.join(self.temp_efi_build_dir, "EFI", "OC", "config.plist") + if not os.path.exists(temp_config_plist_path): + template_plist_path = os.path.join(self.temp_efi_build_dir, "EFI", "OC", "config-template.plist") + if os.path.exists(template_plist_path): + self._report_progress(f"Using template config: {template_plist_path}") + shutil.copy2(template_plist_path, temp_config_plist_path) + else: + self._report_progress("No config.plist or config-template.plist found in EFI template. Creating a minimal one.") + plistlib.dump({"#Comment": "Minimal config by Skyscope - REPLACE ME", "PlatformInfo": {"Generic": {"MLB": "CHANGE_ME"}}}, + open(temp_config_plist_path, 'wb'), fmt=plistlib.PlistFormat.XML) + + if self.enhance_plist_enabled and enhance_config_plist: # Check if function exists + self._report_progress("Attempting to enhance config.plist (note: hardware detection for enhancement is primarily Linux-based)...") + if enhance_config_plist(temp_config_plist_path, self.target_macos_version, self._report_progress): + self._report_progress("config.plist enhancement process complete.") + else: + self._report_progress("config.plist enhancement process failed or had issues (this is expected on Windows for hardware-specifics).") + + target_efi_on_usb_root = f"{self.assigned_efi_letter}:\\" + # Ensure the assigned drive letter is actually available before robocopy + if not os.path.exists(target_efi_on_usb_root): + time.sleep(3) # Extra wait + if not os.path.exists(target_efi_on_usb_root): + raise RuntimeError(f"EFI partition {target_efi_on_usb_root} not accessible after formatting and assignment.") + + self._report_progress(f"Copying final EFI folder from {os.path.join(self.temp_efi_build_dir, 'EFI')} to USB ESP ({target_efi_on_usb_root}EFI)...") + # Using robocopy: /E for subdirs (incl. empty), /S for non-empty, /NFL no file list, /NDL no dir list, /NJH no job header, /NJS no job summary, /NC no class, /NS no size, /NP no progress + # /MT:8 for multithreading (default is 8, can be 1-128) + self._run_command(["robocopy", os.path.join(self.temp_efi_build_dir, "EFI"), os.path.join(target_efi_on_usb_root, "EFI"), "/E", "/NFL", "/NDL", "/NJH", "/NJS", "/NC", "/NS", "/NP", "/MT:8", "/R:3", "/W:5"], check=True) + self._report_progress(f"EFI setup complete on {target_efi_on_usb_root}") + + # --- Prepare BaseSystem HFS Image --- + self._report_progress("Locating BaseSystem image (DMG or PKG containing it) from downloaded assets...") + product_folder_path = self._get_gibmacos_product_folder() + basesystem_source_dmg_or_pkg = ( + self._find_gibmacos_asset("BaseSystem.dmg", product_folder_path) or + self._find_gibmacos_asset("InstallAssistant.pkg", product_folder_path) or # Common for newer macOS + self._find_gibmacos_asset("SharedSupport.dmg", product_folder_path) # Older fallback + ) + if not basesystem_source_dmg_or_pkg: + # Last resort: search for any large PKG file as it might be the installer + if product_folder_path: + pkgs = glob.glob(os.path.join(product_folder_path, "*.pkg")) + glob.glob(os.path.join(product_folder_path, "SharedSupport", "*.pkg")) + if pkgs: basesystem_source_dmg_or_pkg = max(pkgs, key=os.path.getsize, default=None) + if not basesystem_source_dmg_or_pkg: + raise RuntimeError("Could not find BaseSystem.dmg, InstallAssistant.pkg, or SharedSupport.dmg in expected locations.") + + self._report_progress(f"Selected source for HFS extraction: {basesystem_source_dmg_or_pkg}") + if not self._extract_hfs_from_dmg_or_pkg(basesystem_source_dmg_or_pkg, self.temp_basesystem_hfs_path): + raise RuntimeError(f"Failed to extract HFS+ image from '{basesystem_source_dmg_or_pkg}'. Check 7z output above.") + + # --- Guidance for Manual Steps --- + abs_hfs_path_win = os.path.abspath(self.temp_basesystem_hfs_path).replace("/", "\\") + abs_download_path_win = os.path.abspath(self.macos_download_path).replace("/", "\\") + physical_drive_path_win = self.physical_drive_path # Already has escaped backslashes for \\.\ + + # Try to find specific assets for better guidance + install_info_plist_src = self._find_gibmacos_asset("InstallInfo.plist", product_folder_path, search_deep=False) or "InstallInfo.plist (find in product folder)" + basesystem_dmg_src = self._find_gibmacos_asset("BaseSystem.dmg", product_folder_path, search_deep=False) or "BaseSystem.dmg" + basesystem_chunklist_src = self._find_gibmacos_asset("BaseSystem.chunklist", product_folder_path, search_deep=False) or "BaseSystem.chunklist" + main_installer_pkg_src = self._find_gibmacos_asset("InstallAssistant.pkg", product_folder_path, search_deep=False) or \ + self._find_gibmacos_asset("InstallESD.dmg", product_folder_path, search_deep=False) or \ + "InstallAssistant.pkg OR InstallESD.dmg (main installer package)" + apple_diag_src = self._find_gibmacos_asset("AppleDiagnostics.dmg", product_folder_path, search_deep=False) or "AppleDiagnostics.dmg (if present)" + + + guidance_message = ( + f"AUTOMATED EFI SETUP COMPLETE on drive {self.assigned_efi_letter}: (USB partition 1).\n" + f"TEMPORARY BaseSystem HFS image prepared at: '{abs_hfs_path_win}'.\n\n" + f"MANUAL STEPS REQUIRED FOR MAIN macOS PARTITION (USB partition {macos_partition_number_str} - '{installer_vol_label}'):\n" + f"TARGET DISK: Disk {self.disk_number} ({physical_drive_path_win})\n" + f"TARGET PARTITION FOR HFS+ CONTENT: Partition {macos_partition_number_str} (Offset from disk start: {macos_partition_offset_str}).\n\n" + + f"1. WRITE BaseSystem IMAGE:\n" + f" You MUST use a 'dd for Windows' utility. Open Command Prompt or PowerShell AS ADMINISTRATOR.\n" + f" Example command (VERIFY SYNTAX & TARGETS for YOUR dd tool! Incorrect use can WIPE OTHER DRIVES!):\n" + f" `dd if=\"{abs_hfs_path_win}\" of={physical_drive_path_win} bs=8M --progress` (if targeting whole disk with offset for partition 2)\n" + f" OR (if your dd supports writing directly to a partition by its number/offset, less common for \\\\.\\PhysicalDrive targets):\n" + f" `dd if=\"{abs_hfs_path_win}\" of=\\\\?\\Volume{{GUID_OF_PARTITION_2}}\ bs=8M --progress` (more complex to get GUID)\n" + f" It's often SAFER to write to the whole physical drive path ({physical_drive_path_win}) if your `dd` version calculates offsets correctly or if you specify the exact starting sector/byte offset for partition 2.\n" + f" The BaseSystem HFS image is approx. {os.path.getsize(self.temp_basesystem_hfs_path)/(1024*1024):.2f} MB.\n\n" + + f"2. COPY OTHER INSTALLER FILES (CRITICAL FOR OFFLINE INSTALLER):\n" + f" After `dd`-ing BaseSystem.hfs, the '{installer_vol_label}' partition on the USB needs more files from your download path: '{abs_download_path_win}'.\n" + f" This requires a tool that can WRITE to HFS+ partitions from Windows (e.g., TransMac, Paragon HFS+ for Windows, HFSExplorer with write capabilities if any), OR perform this step on macOS/Linux.\n\n" + f" KEY FILES/FOLDERS TO COPY from '{abs_download_path_win}' (likely within a subfolder named like '{os.path.basename(product_folder_path if product_folder_path else '')}') to the ROOT of the '{installer_vol_label}' USB partition:\n" + f" a. Create folder: `Install macOS {self.target_macos_version}.app` (this is a directory)\n" + f" b. Copy '{os.path.basename(install_info_plist_src)}' to the root of '{installer_vol_label}' partition.\n" + f" c. Copy '{os.path.basename(basesystem_dmg_src)}' AND '{os.path.basename(basesystem_chunklist_src)}' into: `System/Library/CoreServices/` (on '{installer_vol_label}')\n" + f" d. Copy '{os.path.basename(main_installer_pkg_src)}' into: `Install macOS {self.target_macos_version}.app/Contents/SharedSupport/`\n" + f" (Alternatively, for older macOS, sometimes into: `System/Installation/Packages/`)\n" + f" e. Copy '{os.path.basename(apple_diag_src)}' (if found) into: `Install macOS {self.target_macos_version}.app/Contents/SharedSupport/` (or a similar recovery/diagnostics path if known for your version).\n" + f" f. Ensure `boot.efi` (from the OpenCore EFI, often copied from `usr/standalone/i386/boot.efi` inside BaseSystem.dmg or similar) is placed at `System/Library/CoreServices/boot.efi` on the '{installer_vol_label}' partition. (Your EFI setup on partition 1 handles OpenCore booting, this is for the macOS installer itself).\n\n" + + f"3. (Optional but Recommended) Create `.IAProductInfo` file at the root of the '{installer_vol_label}' partition. This file is a symlink to `Install macOS {self.target_macos_version}.app/Contents/SharedSupport/InstallInfo.plist` in real installers. On Windows, you may need to copy the `InstallInfo.plist` to this location as well if symlinks are hard.\n\n" + + "IMPORTANT:\n" + "- Without step 2 (copying additional assets), the USB will likely NOT work as a full offline installer and may only offer Internet Recovery (if OpenCore is correctly configured for network access).\n" + "- The temporary BaseSystem HFS image at '{abs_hfs_path_win}' will be DELETED when you close this program or this message.\n" + ) + self._report_progress(f"GUIDANCE FOR MANUAL STEPS:\n{guidance_message}") + # Use the QMessageBox mock or actual if available + QMessageBox.information(None, f"Manual Steps Required for Windows USB - {self.target_macos_version}", guidance_message) + + self._report_progress("Windows USB installer preparation (EFI automated, macOS content requires manual steps as detailed).") + return True + + except Exception as e: + self._report_progress(f"FATAL ERROR during Windows USB writing: {e}"); self._report_progress(traceback.format_exc()) + # Show error in QMessageBox as well if possible + QMessageBox.critical(None, "USB Writing Failed", f"An error occurred: {e}\n\n{traceback.format_exc()}") + return False + finally: + if self.assigned_efi_letter: + self._report_progress(f"Attempting to remove drive letter assignment for {self.assigned_efi_letter}:") + # Run silently, don't check for errors as it's cleanup + self._run_diskpart_script(f"select volume {self.assigned_efi_letter}\nremove letter={self.assigned_efi_letter}\nexit", capture_output_for_parse=False) + + # Cleanup of self.temp_dir_base will handle all sub-temp-dirs and files within it. + self._cleanup_temp_files_and_dirs() + self._report_progress("Temporary files cleanup attempted.") + +# Standalone test block +if __name__ == '__main__': + import platform + if platform.system() != "Windows": + print("This script's standalone test mode is intended for Windows.") + # sys.exit(1) # Use sys.exit for proper exit codes + + print("USB Writer Windows Standalone Test - Installer Method Guidance") + + # Mock constants if not available (e.g. running totally standalone) + try: from constants import MACOS_VERSIONS + except ImportError: MACOS_VERSIONS = {"Sonoma": "sonoma", "Ventura": "ventura"} ; print("Mocked MACOS_VERSIONS") + + pid_test = os.getpid() + # Create a unique temp directory for this test run to avoid conflicts + # Place it in user's Temp for better behavior on Windows + test_run_temp_dir = os.path.join(os.environ.get("TEMP", "C:\\Temp"), f"skyscope_test_run_{pid_test}") + os.makedirs(test_run_temp_dir, exist_ok=True) + + # Mock download directory structure within the test_run_temp_dir + mock_download_dir = os.path.join(test_run_temp_dir, "mock_macos_downloads") + os.makedirs(mock_download_dir, exist_ok=True) + + # Example: Sonoma. More versions could be added for thorough testing. + target_version_test = "Sonoma" + version_tag_test = MACOS_VERSIONS.get(target_version_test, target_version_test.lower()) + + mock_product_name = f"012-34567 - macOS {target_version_test} 14.1" # Example name + mock_product_folder = os.path.join(mock_download_dir, "macOS Downloads", "publicrelease", mock_product_name) + mock_shared_support = os.path.join(mock_product_folder, "SharedSupport") + os.makedirs(mock_shared_support, exist_ok=True) + + # Create dummy files that would be found by _find_gibmacos_asset and _extract_hfs_from_dmg_or_pkg + # 1. Dummy InstallAssistant.pkg (which contains BaseSystem.dmg) + dummy_pkg_path = os.path.join(mock_product_folder, "InstallAssistant.pkg") + with open(dummy_pkg_path, "wb") as f: f.write(os.urandom(10*1024*1024)) # 10MB dummy PKG + # For the _extract_hfs_from_dmg_or_pkg to work with 7z, it needs a real archive. + # This test won't actually run 7z unless 7z is installed and the dummy files are valid archives. + # The focus here is testing the script logic, not 7z itself. + # So, we'll also create a dummy extracted BaseSystem.hfs for the guidance part. + + # 2. Dummy files for the guidance message (these would normally be in mock_product_folder or mock_shared_support) + with open(os.path.join(mock_product_folder, "InstallInfo.plist"), "w") as f: f.write("") + with open(os.path.join(mock_shared_support, "BaseSystem.dmg"), "wb") as f: f.write(os.urandom(5*1024*1024)) # Dummy DMG + with open(os.path.join(mock_shared_support, "BaseSystem.chunklist"), "w") as f: f.write("chunklist content") + # AppleDiagnostics.dmg is optional + with open(os.path.join(mock_shared_support, "AppleDiagnostics.dmg"), "wb") as f: f.write(os.urandom(1*1024*1024)) + + + # Ensure OC_TEMPLATE_DIR (EFI_template_installer) exists for the test or use the minimal creation. + # Relative path from usb_writer_windows.py to EFI_template_installer + abs_oc_template_dir = OC_TEMPLATE_DIR + if not os.path.exists(abs_oc_template_dir): + print(f"Warning: Test OC_TEMPLATE_DIR '{abs_oc_template_dir}' not found. Minimal EFI will be created by script if needed.") + # Optionally, create a dummy one for test if you want to test the copy logic: + # os.makedirs(os.path.join(abs_oc_template_dir, "EFI", "OC"), exist_ok=True) + # with open(os.path.join(abs_oc_template_dir, "EFI", "OC", "config-template.plist"), "wb") as f: plistlib.dump({"TestTemplate":True}, f) + else: + print(f"Using existing OC_TEMPLATE_DIR for test: {abs_oc_template_dir}") + + + disk_id_input = input("Enter target PHYSICAL DISK NUMBER for test (e.g., '1' for PhysicalDrive1). WARNING: THIS DISK WILL BE MODIFIED/WIPED by diskpart. BE ABSOLUTELY SURE. Enter 'skip' to not run diskpart stage: ") + + if disk_id_input.lower() == 'skip': + print("Skipping disk operations. Guidance message will be shown with placeholder disk info.") + # Create a writer instance with a dummy disk ID for logic testing without diskpart + writer = USBWriterWindows("disk 0", mock_download_dir, print, True, target_version_test) + # We need to manually create a dummy temp_basesystem.hfs for the guidance message part + os.makedirs(writer.temp_dir_base, exist_ok=True) + with open(writer.temp_basesystem_hfs_path, "wb") as f: f.write(os.urandom(1024*1024)) # 1MB dummy HFS + # Manually call parts of format_and_write that don't involve diskpart + writer.check_dependencies() # Still check other deps + # Simulate EFI setup success for guidance + writer.assigned_efi_letter = "X" + # ... then generate and show guidance (this part is inside format_and_write) + # This is a bit clunky for 'skip' mode. Full format_and_write is better if safe. + print("Test in 'skip' mode is limited. Full test requires a dedicated test disk.") + + elif not disk_id_input.isdigit(): + print("Invalid disk number.") + else: + actual_disk_id_str = f"\\\\.\\PhysicalDrive{disk_id_input}" # Match format used by class + confirm = input(f"ARE YOU ABSOLUTELY SURE you want to test on {actual_disk_id_str}? This involves running 'diskpart clean'. Type 'YESIDO' to confirm: ") + if confirm == 'YESIDO': + writer = USBWriterWindows(actual_disk_id_str, mock_download_dir, print, True, target_version_test) + try: + writer.format_and_write() + print(f"Test run completed. Check disk {disk_id_input} and console output.") + except Exception as e: + print(f"Test run failed: {e}") + traceback.print_exc() + else: + print("Test cancelled by user.") + + # Cleanup the test run's unique temp directory + print(f"Cleaning up test run temp directory: {test_run_temp_dir}") + shutil.rmtree(test_run_temp_dir, ignore_errors=True) + + print("Standalone test finished.") +``` diff --git a/utils.py b/utils.py new file mode 100644 index 00000000..6395aab5 --- /dev/null +++ b/utils.py @@ -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) diff --git a/vnc-version/Dockerfile b/vnc-version/Dockerfile index 42ebe930..d4ef8b7b 100644 --- a/vnc-version/Dockerfile +++ b/vnc-version/Dockerfile @@ -125,17 +125,4 @@ RUN printf '\n\n\n\n%s\n%s\n\n\n\n' '===========VNC_PASSWORD========== ' "$( ${HOME}/.vnc/passwd RUN chmod 600 ~/.vnc/passwd RUN printf '\n\n\n\n%s\n%s\n\n\n\n' '===========VNC_PASSWORD========== ' "$(