diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml deleted file mode 100644 index e23f0dc..0000000 --- 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 f3c117a..e8d51f6 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 b150892..432d01b 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 0000000..92cfe5a --- /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 41f4fef..712d059 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 6e8bddd..44f2866 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 0000000..e69de29 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 0000000..e69de29 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 0000000..e69de29 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 0000000..e69de29 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 0000000..e69de29 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 0000000..e69de29 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 0000000..e69de29 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 0000000..e69de29 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 0000000..e69de29 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 0000000..e69de29 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 0000000..e69de29 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 0000000..e69de29 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 0000000..e69de29 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 0000000..e69de29 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 0000000..e69de29 diff --git a/EFI_template_installer/EFI/OC/OpenCore.efi b/EFI_template_installer/EFI/OC/OpenCore.efi new file mode 100644 index 0000000..e69de29 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 0000000..5ee8cf6 --- /dev/null +++ b/EFI_template_installer/EFI/OC/config-template.plist @@ -0,0 +1,84 @@ + + + + + ACPI + + Add + 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 + ProvideMaxSlide 0 + RebuildAppleMemoryMap + ResizeAppleGpuBars -1 + SetupVirtualMap + SignalAppleOS + SyncRuntimePermissions + + + DeviceProperties AddDelete + Kernel + + Add + + ArchAnyBundlePathLilu.kextCommentPatch engineEnabledExecutablePathContents/MacOS/LiluMaxKernelMinKernelPlistPathContents/Info.plist + + ArchAnyBundlePathVirtualSMC.kextCommentSMC emulatorEnabledExecutablePathContents/MacOS/VirtualSMCMaxKernelMinKernelPlistPathContents/Info.plist + + ArchAnyBundlePathWhateverGreen.kextCommentVideo patchesEnabledExecutablePathContents/MacOS/WhateverGreenMaxKernelMinKernelPlistPathContents/Info.plist + + ArchAnyBundlePathAppleALC.kextCommentAudio patchesEnabledExecutablePathContents/MacOS/AppleALCMaxKernelMinKernelPlistPathContents/Info.plist + + ArchAnyBundlePathIntelMausi.kextCommentIntel EthernetEnabledExecutablePathContents/MacOS/IntelMausiMaxKernelMinKernelPlistPathContents/Info.plist + ArchAnyBundlePathRealtekRTL8111.kextCommentRealtek RTL8111EnabledExecutablePathContents/MacOS/RealtekRTL8111MaxKernelMinKernelPlistPathContents/Info.plist + ArchAnyBundlePathLucyRTL8125Ethernet.kextCommentRealtek RTL8125EnabledExecutablePathContents/MacOS/LucyRTL8125EthernetMaxKernelMinKernelPlistPathContents/Info.plist + + Block Emulate 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 + + Scheme CustomKernelFuzzyMatchKernelArchAutoKernelCacheAuto + + Misc BlessOverrideBootConsoleAttributes0HibernateModeNoneHibernateSkipsPickerHideAuxiliaryLauncherOptionDisabledLauncherPathDefaultPickerAttributes17PickerAudioAssistPickerModeExternalPickerVariantAutoPollAppleHotKeysShowPickerTakeoffDelay0Timeout5DebugAppleDebugApplePanicDisableWatchDogDisplayDelay0DisplayLevel2147483650LogModules*SysReportTarget3EntriesSecurityAllowSetDefaultApECID0AuthRestartBlacklistAppleUpdateDmgLoadingSignedEnablePasswordExposeSensitiveData6HaltLevel2147483648PasswordHashPasswordSaltScanPolicy0SecureBootModelDisabledVaultOptionalSerialInitOverrideTools + NVRAM Add4D1EDE05-38C7-4A6A-9CC6-4BCCA8B38C14DefaultBackgroundColorAAAAAA==UIScaleAQ==7C436110-AB2A-4BBB-A880-FE41995C9F82SystemAudioVolumeRg==boot-args-v keepsyms=1 debug=0x100csr-active-configAAAAAA==prev-lang:kbdZW4tVVM6MA==run-efi-updaterNoDelete4D1EDE05-38C7-4A6A-9CC6-4BCCA8B38C14UIScaleDefaultBackgroundColor7C436110-AB2A-4BBB-A880-FE41995C9F82boot-argsLegacySchemaWriteFlash + PlatformInfo AutomaticCustomMemoryGenericAdviseFeaturesMLBPLEASE_REPLACE_MLBMaxBIOSVersionProcessorType0ROMAAAAAA==SpoofVendorSystemMemoryStatusAutoSystemProductNameiMacPro1,1SystemSerialNumberPLEASE_REPLACE_SERIALSystemUUIDPLEASE_REPLACE_UUIDUpdateDataHubUpdateNVRAMUpdateSMBIOSUpdateSMBIOSModeCreateUseRawUuidEncoding + UEFI APFSEnableJumpstartGlobalConnectHideVerboseJumpstartHotPlugMinDate0MinVersion0AppleInputAppleEventBuiltinCustomDelaysGraphicsInputMirroringKeyInitialDelay50KeySubsequentDelay5PointerSpeedDiv1PointerSpeedMul1AudioAudioCodec0AudioDeviceAudioOutMask-1AudioSupportDisconnectHdaMaximumGain-15MinimumAssistGain-30MinimumAudibleGain-55PlayChimeAutoResetTrafficClassSetupDelay0ConnectDriversDriversHfsPlus.efiOpenRuntime.efiOpenCanopy.efiInputKeyFilteringKeyForgetThreshold5KeySupportKeySupportModeAutoKeySwapPointerSupportPointerSupportModeASUSTimerResolution50000OutputClearScreenOnModeSwitchConsoleModeDirectGopRenderingForceResolutionGopPassThroughDisabledIgnoreTextInGraphicsProvideConsoleGopReconnectGraphicsOnConnectReconnectOnResChangeReplaceTabWithSpaceResolutionMaxSanitiseClearScreenTextRendererBuiltinGraphicsUIScale-1UgaPassThroughProtocolOverridesQuirksActivateHpetSupportDisableSecurityPolicyEnableVectorAccelerationEnableVmxExitBootServicesDelay0ForceOcWriteFlashForgeUefiSupportIgnoreInvalidFlexRatioReleaseUsbOwnershipReloadOptionRomsRequestBootVarRoutingResizeGpuBars-1TscSyncTimeout0UnblockFsConnectReservedMemory + + diff --git a/README.md b/README.md index 62d061a..cf81111 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:** 0.8.1 (Alpha) +**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 fully automates the complex process of creating a bootable macOS USB drive for virtually any PC. This tool leverages the power of Docker-OSX and OpenCore, aiming to simplify the Hackintosh journey from start to finish. + +This project is dedicated to creating a seamless experience, from selecting your desired macOS version to generating a USB drive that's ready to boot your PC into macOS, complete with efforts to auto-configure for your hardware. + +## Current Features & Capabilities + +* **Intuitive Graphical User Interface (PyQt6):** Guides you through each step of the process. +* **macOS Version Selection:** Easily choose from popular macOS versions (Sonoma, Ventura, Monterey, Big Sur, Catalina). +* **Automated Docker-OSX Orchestration:** + * **Intelligent Image Pulling:** Automatically pulls the required `sickcodes/docker-osx` image from Docker Hub, with progress displayed. + * **VM Creation & macOS Installation:** Launches the Docker-OSX container where you can interactively install macOS within a QEMU virtual machine. + * **Log Streaming:** View Docker and QEMU logs directly in the application for transparency. +* **VM Image Extraction:** Once macOS is installed in the VM, the tool helps you extract the essential disk images (`mac_hdd_ng.img` and `OpenCore.qcow2`). +* **Container Management:** Stop and remove the Docker-OSX container after use. +* **Cross-Platform USB Drive Preparation:** + * **USB Detection:** Identifies potential USB drives on Linux, macOS, and Windows (using WMI for more accurate detection on Windows). + * **Automated EFI & macOS System Write (Linux & macOS):** + * Partitions the USB drive with a GUID Partition Table (GPT). + * Creates and formats an EFI System Partition (FAT32) and a main macOS partition (HFS+). + * Uses a robust file-level copy (`rsync`) for both EFI content and the main macOS system, ensuring compatibility with various USB sizes and only copying necessary data. + * **Windows USB Writing (Partial Automation):** + * Automates EFI partition creation and EFI file copying. + * **Important:** Writing the main macOS system image currently requires a guided manual step using an external "dd for Windows" utility due to Windows' limitations with direct, scriptable raw partition writing of HFS+/APFS filesystems. The tool prepares the raw image and provides instructions. +* **Experimental `config.plist` Auto-Enhancement:** + * **Linux Host Detection:** If the tool is run on a Linux system, it can gather information about your host computer's hardware (iGPU, audio, Ethernet, CPU). + * **Targeted Modifications:** Optionally attempts to modify the `config.plist` (from the generated `OpenCore.qcow2`) to: + * Add common `DeviceProperties` for Intel iGPUs. + * Set appropriate audio `layout-id`s. + * Ensure necessary Ethernet kexts are enabled. + * Apply boot-args for NVIDIA GTX 970 based on target macOS version (e.g., `nv_disable=1` or `nvda_drv=1`). + * A backup of the original `config.plist` is created before modifications. +* **Privilege Checking:** Warns if administrative/root privileges are needed for USB writing and are not detected. +* **UI Feedback:** Status bar messages and an indeterminate progress bar keep you informed during long operations. + +## Current Status & Known Limitations + +* **Windows Main OS USB Write:** This is the primary limitation, requiring a manual `dd` step. Future work aims to automate this if a reliable, redistributable CLI tool for raw partition writing is identified or developed. +* **`config.plist` Enhancement is Experimental:** + * Hardware detection for this feature is **currently only implemented for Linux hosts.** On macOS/Windows, the plist modification step will run but won't apply hardware-specific changes. + * The applied patches are based on common configurations and may not be optimal or work for all hardware. Always test thoroughly. +* **NVIDIA dGPU Support on Newer macOS:** Modern macOS (Mojave+) does not support NVIDIA Maxwell/Pascal/Turing/Ampere GPUs. The tool attempts to configure systems with these cards for basic display or to use an iGPU if available. Full acceleration is not possible on these macOS versions with these cards. +* **Universal Compatibility:** While the goal is broad PC compatibility, Hackintoshing can be hardware-specific. Success is not guaranteed on all possible PC configurations. +* **Dependency on External Projects:** Relies on Docker-OSX, OpenCore, and various community-sourced kexts and configurations. + +## Prerequisites + +1. **Docker:** Must be installed and running. Your user account needs permission to manage Docker. + * [Install Docker Engine](https://docs.docker.com/engine/install/) +2. **Python:** Version 3.8 or newer. +3. **Python Libraries:** Install with `pip install PyQt6 psutil`. +4. **Platform-Specific CLI Tools for USB Writing:** + + * **Linux (including Debian 13 "Trixie"):** + * `qemu-img` (from `qemu-utils`) + * `parted` + * `kpartx` (from `kpartx` or `multipath-tools`) + * `rsync` + * `mkfs.vfat` (from `dosfstools`) + * `mkfs.hfsplus` (from `hfsprogs`) + * `apfs-fuse`: Often requires manual compilation (e.g., from `sgan81/apfs-fuse` on GitHub). Typical build dependencies: `git g++ cmake libfuse3-dev libicu-dev zlib1g-dev libbz2-dev libssl-dev`. Ensure it's in your PATH. + * `lsblk`, `partprobe` (from `util-linux`) + * Install most via: `sudo apt update && sudo apt install qemu-utils parted kpartx rsync dosfstools hfsprogs util-linux` + * **macOS:** + * `qemu-img` (e.g., via Homebrew: `brew install qemu`) + * `diskutil`, `hdiutil`, `rsync` (standard macOS tools). + * **Windows:** + * `qemu-img` (install and add to PATH). + * `diskpart`, `robocopy` (standard Windows tools). + * `7z.exe` (7-Zip command-line tool, install and add to PATH) - for EFI file extraction. + * A "dd for Windows" utility (e.g., from SUSE, chrysocome.net, or similar). Ensure it's in your PATH and you know how to use it for writing to a physical disk's partition or offset. + +## How to Run + +1. Ensure all prerequisites for your operating system are met. +2. Clone this repository or download the source files. +3. Install Python libraries: `pip install PyQt6 psutil`. +4. Execute `python main_app.py`. +5. **Important for USB Writing:** + * **Linux:** Run with `sudo python main_app.py`. + * **macOS:** The script will use `sudo` internally for `rsync` to USB EFI if needed. You might be prompted for your password. Ensure the main application has Full Disk Access if issues arise with `hdiutil` or `diskutil` not having permissions (System Settings > Privacy & Security). + * **Windows:** Run the application as Administrator. + +## Step-by-Step Usage Guide + +1. **Step 1: Create and Install macOS VM** + * Launch the "Skyscope macOS on PC USB Creator Tool". + * Select your desired macOS version from the dropdown menu. + * Click "Create VM and Start macOS Installation". + * The tool will first pull the necessary Docker image (progress shown). + * Then, a QEMU window will appear. This is your virtual machine. Follow the standard macOS installation procedure within this window (use Disk Utility to erase and format the virtual hard drive, then install macOS). This part is interactive. + * Once macOS is fully installed in QEMU, shut down the macOS VM from within its own interface (Apple Menu > Shut Down). Closing the QEMU window will also terminate the process. +2. **Step 2: Extract VM Images** + * After the Docker process from Step 1 finishes (QEMU window closes), the "Extract Images from Container" button will become active. + * Click it. You'll be prompted to select a directory on your computer. The `mac_hdd_ng.img` (macOS system) and `OpenCore.qcow2` (EFI bootloader) files will be copied here. This may take some time. +3. **Step 3: Container Management (Optional)** + * Once images are extracted, the Docker container used for installation is no longer strictly needed. + * You can "Stop Container" (if it's listed as running by Docker for any reason) and then "Remove Container" to free up disk space. +4. **Step 4: Select Target USB Drive and Write** + * Physically connect your USB flash drive. + * Click "Refresh List". + * **Linux/macOS:** Select your USB drive from the dropdown. Verify size and identifier carefully. + * **Windows:** USB drives detected via WMI will appear in the dropdown. Select the correct one. Ensure it's the `Disk X` number you intend. + * **(Optional, Experimental):** Check the "Try to auto-enhance config.plist..." box if you are on a Linux host and wish to attempt automatic `config.plist` modification for your hardware. A backup of the original `config.plist` will be made. + * **CRITICAL WARNING:** Double-check your selection. The next action will erase the selected USB drive. + * Click "Write Images to USB Drive". Confirm the data erasure warning. + * The process will now: + * (If enhancement enabled) Attempt to modify the `config.plist` within the source OpenCore image. + * Partition and format your USB drive. + * Copy EFI files to the USB's EFI partition. + * Copy macOS system files to the USB's main partition. (On Windows, this step requires manual `dd` operation as guided by the application). + * This is a lengthy process. Monitor the progress in the output area. +5. **Boot!** + * Once complete, safely eject the USB drive. You can now try booting your PC from it. Remember to configure your PC's BIOS/UEFI for booting from USB and for macOS compatibility (e.g., disable Secure Boot, enable AHCI, XHCI Handoff, etc., as per standard Hackintosh guides like Dortania). + +## Future Vision & Enhancements + +* **Fully Automated Windows USB Writing:** Replace the manual `dd` step with a reliable, integrated solution. +* **Advanced `config.plist` Customization:** + * Expand hardware detection to macOS and Windows hosts. + * Provide more granular UI controls for plist enhancements (e.g., preview changes, select specific patches). + * Allow users to load/save `config.plist` modification profiles. +* **Enhanced UI/UX for Progress:** Implement determinate progress bars with percentage completion and more dynamic status updates. +* **Debian 13 "Trixie" (and other distros) Validation:** Continuous compatibility checks and dependency streamlining. +* **"Universal" Config Strategy (Research):** Investigate advanced techniques for more adaptive OpenCore configurations, though true universality is a significant challenge. + +## Contributing + +Your contributions, feedback, and bug reports are highly welcome! Please fork the repository and submit pull requests, or open issues for discussion. + +## License + +(To be decided - e.g., MIT or GPLv3) diff --git a/constants.py b/constants.py new file mode 100644 index 0000000..a8b2114 --- /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 0000000..2e8d9b2 --- /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 0000000..cb6313a --- /dev/null +++ b/main_app.py @@ -0,0 +1,491 @@ +# main_app.py +import sys +import subprocess +import os +import psutil +import platform +import ctypes +import json +import re +import traceback # For better error logging +import shutil # For shutil.which + +from PyQt6.QtWidgets import ( + QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, + QLabel, QComboBox, QPushButton, QTextEdit, QMessageBox, QMenuBar, + QFileDialog, QGroupBox, QLineEdit, QProgressBar, QCheckBox +) +from PyQt6.QtGui import QAction, QIcon +from PyQt6.QtCore import pyqtSignal, pyqtSlot, QObject, QThread, QTimer, Qt + +from constants import APP_NAME, DEVELOPER_NAME, BUSINESS_NAME, MACOS_VERSIONS +# DOCKER_IMAGE_BASE and Docker-related utils are no longer primary for this flow. +# utils.py might be refactored or parts removed later. + +# Platform specific USB writers +USBWriterLinux = None +USBWriterMacOS = None +USBWriterWindows = None + +if platform.system() == "Linux": + try: from usb_writer_linux import USBWriterLinux + except ImportError as e: print(f"Could not import USBWriterLinux: {e}") +elif platform.system() == "Darwin": + try: from usb_writer_macos import USBWriterMacOS + except ImportError as e: print(f"Could not import USBWriterMacOS: {e}") +elif platform.system() == "Windows": + try: from usb_writer_windows import USBWriterWindows + except ImportError as e: print(f"Could not import USBWriterWindows: {e}") + +GIBMACOS_SCRIPT_PATH = os.path.join(os.path.dirname(__file__), "scripts", "gibMacOS", "gibMacOS.py") +if not os.path.exists(GIBMACOS_SCRIPT_PATH): + GIBMACOS_SCRIPT_PATH = os.path.join(os.path.dirname(__file__), "gibMacOS.py") + + +class WorkerSignals(QObject): + progress = pyqtSignal(str) + finished = pyqtSignal(str) + error = pyqtSignal(str) + progress_value = pyqtSignal(int) + +class GibMacOSWorker(QObject): + signals = WorkerSignals() + def __init__(self, version_key: str, download_path: str, catalog_key: str = "publicrelease"): + super().__init__() + self.version_key = version_key + self.download_path = download_path + self.catalog_key = catalog_key + self.process = None + self._is_running = True + + @pyqtSlot() + def run(self): + try: + script_to_run = "" + if os.path.exists(GIBMACOS_SCRIPT_PATH): + script_to_run = GIBMACOS_SCRIPT_PATH + elif shutil.which("gibMacOS.py"): # Check if it's in PATH + script_to_run = "gibMacOS.py" + elif os.path.exists(os.path.join(os.path.dirname(os.path.abspath(sys.argv[0])), "gibMacOS.py")): # Check alongside main_app.py + script_to_run = os.path.join(os.path.dirname(os.path.abspath(sys.argv[0])), "gibMacOS.py") + else: + self.signals.error.emit(f"gibMacOS.py not found at expected locations or in PATH.") + return + + version_for_gib = MACOS_VERSIONS.get(self.version_key, self.version_key) + os.makedirs(self.download_path, exist_ok=True) + + command = [sys.executable, script_to_run, "-n", "-c", self.catalog_key, "-v", version_for_gib, "-d", self.download_path] + self.signals.progress.emit(f"Downloading macOS '{self.version_key}' (as '{version_for_gib}') installer assets...\nCommand: {' '.join(command)}\nOutput will be in: {self.download_path}\n") + + self.process = subprocess.Popen( + command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + text=True, bufsize=1, universal_newlines=True, + creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == "win32" else 0 + ) + + if self.process.stdout: + for line in iter(self.process.stdout.readline, ''): + if not self._is_running: + self.signals.progress.emit("macOS download process stopping at user request.\n") + break + line_strip = line.strip() + self.signals.progress.emit(line_strip) + progress_match = re.search(r"(\d+)%", line_strip) + if progress_match: + try: self.signals.progress_value.emit(int(progress_match.group(1))) + except ValueError: pass + self.process.stdout.close() + + return_code = self.process.wait() + + if not self._is_running and return_code != 0: + self.signals.finished.emit(f"macOS download cancelled or stopped early (exit code {return_code}).") + return + + if return_code == 0: + self.signals.finished.emit(f"macOS '{self.version_key}' installer assets downloaded to '{self.download_path}'.") + else: + self.signals.error.emit(f"Failed to download macOS '{self.version_key}' (gibMacOS exit code {return_code}). Check logs.") + except FileNotFoundError: + self.signals.error.emit(f"Error: Python or gibMacOS.py script not found. Ensure Python is in PATH and gibMacOS script is correctly located.") + except Exception as e: + self.signals.error.emit(f"An error occurred during macOS download: {str(e)}\n{traceback.format_exc()}") + finally: + self._is_running = False + + def stop(self): + self._is_running = False + if self.process and self.process.poll() is None: + self.signals.progress.emit("Attempting to stop macOS download (may not be effective for active downloads)...\n") + try: + self.process.terminate(); self.process.wait(timeout=2) + except subprocess.TimeoutExpired: self.process.kill() + self.signals.progress.emit("macOS download process termination requested.\n") + + +class USBWriterWorker(QObject): + signals = WorkerSignals() + def __init__(self, device: str, macos_download_path: str, + enhance_plist: bool, target_macos_version: str): + super().__init__() + self.device = device + self.macos_download_path = macos_download_path + self.enhance_plist = enhance_plist + self.target_macos_version = target_macos_version + self.writer_instance = None + + @pyqtSlot() + def run(self): + current_os = platform.system() + try: + writer_cls = None + if current_os == "Linux": writer_cls = USBWriterLinux + elif current_os == "Darwin": writer_cls = USBWriterMacOS + elif current_os == "Windows": writer_cls = USBWriterWindows + + if writer_cls is None: + self.signals.error.emit(f"{current_os} USB writer module not available or OS not supported."); return + + # Platform writers' __init__ will need to be updated for macos_download_path + # This assumes usb_writer_*.py __init__ signatures are now: + # __init__(self, device, macos_download_path, progress_callback, enhance_plist_enabled, target_macos_version) + self.writer_instance = writer_cls( + device=self.device, + macos_download_path=self.macos_download_path, + progress_callback=lambda msg: self.signals.progress.emit(msg), + enhance_plist_enabled=self.enhance_plist, + target_macos_version=self.target_macos_version + ) + + if self.writer_instance.format_and_write(): + self.signals.finished.emit("USB writing process completed successfully.") + else: + self.signals.error.emit("USB writing process failed. Check output for details.") + except Exception as e: + self.signals.error.emit(f"USB writing preparation error: {str(e)}\n{traceback.format_exc()}") + + +class MainWindow(QMainWindow): + def __init__(self): + super().__init__() + self.setWindowTitle(APP_NAME) + self.setGeometry(100, 100, 800, 700) # Adjusted height + + self.active_worker_thread = None + self.macos_download_path = None + self.current_worker_instance = None + + self.spinner_chars = ["|", "/", "-", "\\"]; self.spinner_index = 0 + self.spinner_timer = QTimer(self); self.spinner_timer.timeout.connect(self._update_spinner_status) + self.base_status_message = "Ready." + + self._setup_ui() + self.status_bar = self.statusBar() + # self.status_bar.addPermanentWidget(self.progress_bar) # Progress bar now in main layout + self.status_bar.showMessage(self.base_status_message, 5000) + self.refresh_usb_drives() + + def _setup_ui(self): + menubar = self.menuBar(); file_menu = menubar.addMenu("&File"); help_menu = menubar.addMenu("&Help") + exit_action = QAction("&Exit", self); exit_action.triggered.connect(self.close); file_menu.addAction(exit_action) + about_action = QAction("&About", self); about_action.triggered.connect(self.show_about_dialog); help_menu.addAction(about_action) + central_widget = QWidget(); self.setCentralWidget(central_widget); main_layout = QVBoxLayout(central_widget) + + # Step 1: Download macOS + download_group = QGroupBox("Step 1: Download macOS Installer Assets") + download_layout = QVBoxLayout() + selection_layout = QHBoxLayout(); self.version_label = QLabel("Select macOS Version:"); self.version_combo = QComboBox() + self.version_combo.addItems(MACOS_VERSIONS.keys()); selection_layout.addWidget(self.version_label); selection_layout.addWidget(self.version_combo) + download_layout.addLayout(selection_layout) + + self.download_macos_button = QPushButton("Download macOS Installer Assets") + self.download_macos_button.clicked.connect(self.start_macos_download_flow) + download_layout.addWidget(self.download_macos_button) + + self.cancel_operation_button = QPushButton("Cancel Current Operation") + self.cancel_operation_button.clicked.connect(self.stop_current_operation) + self.cancel_operation_button.setEnabled(False) + download_layout.addWidget(self.cancel_operation_button) + download_group.setLayout(download_layout) + main_layout.addWidget(download_group) + + # Step 2: USB Drive Selection & Writing + usb_group = QGroupBox("Step 2: Create Bootable USB Installer") + self.usb_layout = QVBoxLayout() + self.usb_drive_label = QLabel("Available USB Drives:"); self.usb_layout.addWidget(self.usb_drive_label) + usb_selection_layout = QHBoxLayout(); self.usb_drive_combo = QComboBox(); self.usb_drive_combo.currentIndexChanged.connect(self.update_all_button_states) + usb_selection_layout.addWidget(self.usb_drive_combo); self.refresh_usb_button = QPushButton("Refresh List"); self.refresh_usb_button.clicked.connect(self.refresh_usb_drives) + usb_selection_layout.addWidget(self.refresh_usb_button); self.usb_layout.addLayout(usb_selection_layout) + self.windows_usb_guidance_label = QLabel("For Windows: Select USB disk from dropdown (WMI). Manual input below if empty/unreliable.") + self.windows_disk_id_input = QLineEdit(); self.windows_disk_id_input.setPlaceholderText("Disk No. (e.g., 1)"); self.windows_disk_id_input.textChanged.connect(self.update_all_button_states) + if platform.system() == "Windows": self.usb_layout.addWidget(self.windows_usb_guidance_label); self.usb_layout.addWidget(self.windows_disk_id_input); self.windows_usb_guidance_label.setVisible(True); self.windows_disk_id_input.setVisible(True) + else: self.windows_usb_guidance_label.setVisible(False); self.windows_disk_id_input.setVisible(False) + self.enhance_plist_checkbox = QCheckBox("Try to auto-enhance config.plist for this system's hardware (Experimental, Linux Host Only for detection)") + self.enhance_plist_checkbox.setChecked(False); self.usb_layout.addWidget(self.enhance_plist_checkbox) + warning_label = QLabel("WARNING: USB drive will be ERASED!"); warning_label.setStyleSheet("color: red; font-weight: bold;"); self.usb_layout.addWidget(warning_label) + self.write_to_usb_button = QPushButton("Create macOS Installer USB"); self.write_to_usb_button.clicked.connect(self.handle_write_to_usb) + self.write_to_usb_button.setEnabled(False); self.usb_layout.addWidget(self.write_to_usb_button); usb_group.setLayout(self.usb_layout); main_layout.addWidget(usb_group) + + self.progress_bar = QProgressBar(self); self.progress_bar.setRange(0, 0); self.progress_bar.setVisible(False); main_layout.addWidget(self.progress_bar) + self.output_area = QTextEdit(); self.output_area.setReadOnly(True); main_layout.addWidget(self.output_area) + self.update_all_button_states() + + def show_about_dialog(self): QMessageBox.about(self, f"About {APP_NAME}", f"Version: 1.0.0 (Installer Flow)\nDeveloper: {DEVELOPER_NAME}\nBusiness: {BUSINESS_NAME}\n\nThis tool helps create bootable macOS USB drives using gibMacOS and OpenCore.") + + def _set_ui_busy(self, busy_status: bool, message: str = "Processing..."): + self.progress_bar.setVisible(busy_status) + if busy_status: + self.base_status_message = message + if not self.spinner_timer.isActive(): self.spinner_timer.start(150) + self._update_spinner_status() + self.progress_bar.setRange(0,0) + else: + self.spinner_timer.stop() + self.status_bar.showMessage(message or "Ready.", 7000) + self.update_all_button_states() + + + def _update_spinner_status(self): + if self.spinner_timer.isActive(): + char = self.spinner_chars[self.spinner_index % len(self.spinner_chars)] + active_worker_provides_progress = False + if self.active_worker_thread and self.active_worker_thread.isRunning(): + active_worker_provides_progress = getattr(self.active_worker_thread, "provides_progress", False) + + if active_worker_provides_progress and self.progress_bar.maximum() == 100: # Determinate + self.status_bar.showMessage(f"{char} {self.base_status_message} ({self.progress_bar.value()}%)") + else: + if self.progress_bar.maximum() != 0: self.progress_bar.setRange(0,0) + self.status_bar.showMessage(f"{char} {self.base_status_message}") + self.spinner_index = (self.spinner_index + 1) % len(self.spinner_chars) + elif not (self.active_worker_thread and self.active_worker_thread.isRunning()): + self.spinner_timer.stop() + + def update_all_button_states(self): + is_worker_active = self.active_worker_thread is not None and self.active_worker_thread.isRunning() + + self.download_macos_button.setEnabled(not is_worker_active) + self.version_combo.setEnabled(not is_worker_active) + self.cancel_operation_button.setEnabled(is_worker_active and self.current_worker_instance is not None) + + self.refresh_usb_button.setEnabled(not is_worker_active) + self.usb_drive_combo.setEnabled(not is_worker_active) + if platform.system() == "Windows": self.windows_disk_id_input.setEnabled(not is_worker_active) + self.enhance_plist_checkbox.setEnabled(not is_worker_active) + + # Write to USB button logic + macos_assets_ready = bool(self.macos_download_path and os.path.isdir(self.macos_download_path)) + usb_identified = False + current_os = platform.system(); writer_module = None + if current_os == "Linux": writer_module = USBWriterLinux; usb_identified = bool(self.usb_drive_combo.currentData()) + elif current_os == "Darwin": writer_module = USBWriterMacOS; usb_identified = bool(self.usb_drive_combo.currentData()) + elif current_os == "Windows": + writer_module = USBWriterWindows + usb_identified = bool(self.usb_drive_combo.currentData()) or bool(self.windows_disk_id_input.text().strip()) + + self.write_to_usb_button.setEnabled(not is_worker_active and macos_assets_ready and usb_identified and writer_module is not None) + tooltip = "" + if writer_module is None: tooltip = f"USB Writing not supported on {current_os} or module missing." + elif not macos_assets_ready: tooltip = "Download macOS installer assets first (Step 1)." + elif not usb_identified: tooltip = "Select or identify a target USB drive." + else: tooltip = "" + self.write_to_usb_button.setToolTip(tooltip) + + + def _start_worker(self, worker_instance, on_finished_slot, on_error_slot, worker_name="worker", provides_progress=False): + if self.active_worker_thread and self.active_worker_thread.isRunning(): + QMessageBox.warning(self, "Busy", "Another operation is in progress."); return False + + self._set_ui_busy(True, f"Starting {worker_name.replace('_', ' ')}...") + self.current_worker_instance = worker_instance + + if provides_progress: + self.progress_bar.setRange(0,100) + worker_instance.signals.progress_value.connect(self.update_progress_bar_value) + else: + self.progress_bar.setRange(0,0) + + self.active_worker_thread = QThread(); self.active_worker_thread.setObjectName(worker_name + "_thread") + setattr(self.active_worker_thread, "provides_progress", provides_progress) + + worker_instance.moveToThread(self.active_worker_thread) + worker_instance.signals.progress.connect(self.update_output) + worker_instance.signals.finished.connect(lambda msg, wn=worker_name, slot=on_finished_slot: self._handle_worker_finished(msg, wn, slot)) + worker_instance.signals.error.connect(lambda err, wn=worker_name, slot=on_error_slot: self._handle_worker_error(err, wn, slot)) + self.active_worker_thread.finished.connect(self.active_worker_thread.deleteLater) + self.active_worker_thread.started.connect(worker_instance.run) + self.active_worker_thread.start() + return True + + @pyqtSlot(int) + def update_progress_bar_value(self, value): + if self.progress_bar.maximum() == 0: self.progress_bar.setRange(0,100) + self.progress_bar.setValue(value) + # Spinner update will happen on its timer, it can check progress_bar.value() + + def _handle_worker_finished(self, message, worker_name, specific_finished_slot): + final_msg = f"{worker_name.replace('_', ' ').capitalize()} completed." + self.current_worker_instance = None # Clear current worker + self.active_worker_thread = None + if specific_finished_slot: specific_finished_slot(message) + self._set_ui_busy(False, final_msg) + + def _handle_worker_error(self, error_message, worker_name, specific_error_slot): + final_msg = f"{worker_name.replace('_', ' ').capitalize()} failed." + self.current_worker_instance = None # Clear current worker + self.active_worker_thread = None + if specific_error_slot: specific_error_slot(error_message) + self._set_ui_busy(False, final_msg) + + def start_macos_download_flow(self): + self.output_area.clear(); selected_version_name = self.version_combo.currentText() + gibmacos_version_arg = MACOS_VERSIONS.get(selected_version_name, selected_version_name) + + chosen_path = QFileDialog.getExistingDirectory(self, "Select Directory to Download macOS Installer Assets") + if not chosen_path: self.output_area.append("Download directory selection cancelled."); return + self.macos_download_path = chosen_path + + worker = GibMacOSWorker(gibmacos_version_arg, self.macos_download_path) + if not self._start_worker(worker, self.macos_download_finished, self.macos_download_error, + "macos_download", + f"Downloading macOS {selected_version_name} assets...", + provides_progress=True): # Assuming GibMacOSWorker will emit progress_value + self._set_ui_busy(False, "Failed to start macOS download operation.") + + + @pyqtSlot(str) + def macos_download_finished(self, message): + QMessageBox.information(self, "Download Complete", message) + # self.macos_download_path is set. UI update handled by generic handler. + + @pyqtSlot(str) + def macos_download_error(self, error_message): + QMessageBox.critical(self, "Download Error", error_message) + self.macos_download_path = None + # UI reset by generic handler. + + def stop_current_operation(self): + if self.current_worker_instance and hasattr(self.current_worker_instance, 'stop'): + self.output_area.append(f" +--- Attempting to stop {self.active_worker_thread.objectName().replace('_thread','')} ---") + self.current_worker_instance.stop() + else: + self.output_area.append(" +--- No active stoppable operation or stop method not implemented for current worker. ---") + + def handle_error(self, message): + self.output_area.append(f"ERROR: {message}"); QMessageBox.critical(self, "Error", message) + self._set_ui_busy(False, "Error occurred.") + + def check_admin_privileges(self) -> bool: # ... (same) + try: + if platform.system() == "Windows": return ctypes.windll.shell32.IsUserAnAdmin() != 0 + else: return os.geteuid() == 0 + except Exception as e: self.output_area.append(f"Could not check admin privileges: {e}"); return False + + def refresh_usb_drives(self): # ... (same logic as before) + self.usb_drive_combo.clear(); current_selection_text = getattr(self, '_current_usb_selection_text', None) + self.output_area.append(" +Scanning for disk devices...") + if platform.system() == "Windows": + self.usb_drive_label.setText("Available USB Disks (Windows - via WMI/PowerShell):") + self.windows_usb_guidance_label.setVisible(True); self.windows_disk_id_input.setVisible(False); + powershell_command = "Get-WmiObject Win32_DiskDrive | Where-Object {$_.InterfaceType -eq 'USB'} | Select-Object DeviceID, Index, Model, @{Name='SizeGB';Expression={[math]::Round($_.Size / 1GB, 2)}} | ConvertTo-Json" + try: + process = subprocess.run(["powershell", "-Command", powershell_command], capture_output=True, text=True, check=True, creationflags=subprocess.CREATE_NO_WINDOW) + disks_data = json.loads(process.stdout); disks_json = disks_data if isinstance(disks_data, list) else [disks_data] if disks_data else [] + if disks_json: + for disk in disks_json: + if disk.get('DeviceID') is None or disk.get('Index') is None: continue + disk_text = f"Disk {disk['Index']}: {disk.get('Model','N/A')} ({disk.get('SizeGB','N/A')} GB) - {disk['DeviceID']}" + self.usb_drive_combo.addItem(disk_text, userData=str(disk['Index'])) + self.output_area.append(f"Found {len(disks_json)} USB disk(s) via WMI."); + if current_selection_text: + for i in range(self.usb_drive_combo.count()): + if self.usb_drive_combo.itemText(i) == current_selection_text: self.usb_drive_combo.setCurrentIndex(i); break + else: self.output_area.append("No USB disks found via WMI/PowerShell. Manual input field shown as fallback."); self.windows_disk_id_input.setVisible(True) + except Exception as e: self.output_area.append(f"Error scanning Windows USBs with PowerShell: {e}"); self.windows_disk_id_input.setVisible(True) + else: + self.usb_drive_label.setText("Available USB Drives (for Linux/macOS):") + self.windows_usb_guidance_label.setVisible(False); self.windows_disk_id_input.setVisible(False) + try: + partitions = psutil.disk_partitions(all=False); potential_usbs = [] + for p in partitions: + is_removable = 'removable' in p.opts; is_likely_usb = False + if platform.system() == "Darwin" and p.device.startswith("/dev/disk") and 'external' in p.opts.lower() and 'physical' in p.opts.lower(): is_likely_usb = True + elif platform.system() == "Linux" and ((p.mountpoint and ("/media/" in p.mountpoint or "/run/media/" in p.mountpoint)) or (p.device.startswith("/dev/sd") and not p.device.endswith("da"))): is_likely_usb = True + if is_removable or is_likely_usb: + try: usage = psutil.disk_usage(p.mountpoint); size_gb = usage.total / (1024**3) + except Exception: continue + if size_gb < 0.1 : continue + drive_text = f"{p.device} @ {p.mountpoint} ({p.fstype}, {size_gb:.2f} GB)" + potential_usbs.append((drive_text, p.device)) + if potential_usbs: + idx_to_select = -1 + for i, (text, device_path) in enumerate(potential_usbs): self.usb_drive_combo.addItem(text, userData=device_path); + if text == current_selection_text: idx_to_select = i + if idx_to_select != -1: self.usb_drive_combo.setCurrentIndex(idx_to_select) + self.output_area.append(f"Found {len(potential_usbs)} potential USB drive(s). Please verify carefully.") + else: self.output_area.append("No suitable USB drives found for Linux/macOS.") + except ImportError: self.output_area.append("psutil library not found.") + except Exception as e: self.output_area.append(f"Error scanning for USB drives: {e}") + self.update_all_button_states() + + + def handle_write_to_usb(self): + if not self.check_admin_privileges(): QMessageBox.warning(self, "Privileges Required", "This operation requires Administrator/root privileges."); return + if not self.macos_download_path or not os.path.isdir(self.macos_download_path): QMessageBox.warning(self, "Missing macOS Assets", "Download macOS installer assets first."); return + current_os = platform.system(); usb_writer_module = None; target_device_id_for_worker = None + if current_os == "Windows": target_device_id_for_worker = self.usb_drive_combo.currentData() or self.windows_disk_id_input.text().strip(); usb_writer_module = USBWriterWindows + else: target_device_id_for_worker = self.usb_drive_combo.currentData(); usb_writer_module = USBWriterLinux if current_os == "Linux" else USBWriterMacOS if current_os == "Darwin" else None + if not usb_writer_module: QMessageBox.warning(self, "Unsupported Platform", f"USB writing not supported for {current_os}."); return + if not target_device_id_for_worker: QMessageBox.warning(self, "No USB Selected/Identified", f"Please select/identify target USB."); return + if current_os == "Windows" and target_device_id_for_worker.isdigit(): target_device_id_for_worker = f"disk {target_device_id_for_worker}" + + enhance_plist_state = self.enhance_plist_checkbox.isChecked() + target_macos_name = self.version_combo.currentText() + reply = QMessageBox.warning(self, "Confirm Write Operation", f"WARNING: ALL DATA ON TARGET '{target_device_id_for_worker}' WILL BE ERASED. +Proceed?", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.Cancel, QMessageBox.StandardButton.Cancel) + if reply == QMessageBox.StandardButton.Cancel: self.output_area.append(" +USB write cancelled."); return + + # USBWriterWorker now needs different args + # The platform specific writers (USBWriterLinux etc) will need to be updated to accept macos_download_path + # and use it to find BaseSystem.dmg, EFI/OC etc. instead of opencore_qcow2_path, macos_qcow2_path + usb_worker_adapted = USBWriterWorker( + device=target_device_id_for_worker, + macos_download_path=self.macos_download_path, + enhance_plist=enhance_plist_state, + target_macos_version=target_macos_name + ) + + if not self._start_worker(usb_worker_adapted, self.usb_write_finished, self.usb_write_error, "usb_write_worker", + busy_message=f"Creating USB for {target_device_id_for_worker}...", + provides_progress=False): # USB writing can be long, but progress parsing is per-platform script. + self._set_ui_busy(False, "Failed to start USB write operation.") + + @pyqtSlot(str) + def usb_write_finished(self, message): QMessageBox.information(self, "USB Write Complete", message) + @pyqtSlot(str) + def usb_write_error(self, error_message): QMessageBox.critical(self, "USB Write Error", error_message) + + def closeEvent(self, event): # ... (same logic) + self._current_usb_selection_text = self.usb_drive_combo.currentText() + if self.active_worker_thread and self.active_worker_thread.isRunning(): + reply = QMessageBox.question(self, 'Confirm Exit', "An operation is running. Exit anyway?", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No) + if reply == QMessageBox.StandardButton.Yes: + if self.current_worker_instance and hasattr(self.current_worker_instance, 'stop'): self.current_worker_instance.stop() + else: self.active_worker_thread.quit() + self.active_worker_thread.wait(1000); event.accept() + else: event.ignore(); return + else: event.accept() + + +if __name__ == "__main__": + import traceback # Ensure traceback is available for GibMacOSWorker + import shutil # Ensure shutil is available for GibMacOSWorker path check + app = QApplication(sys.argv) + window = MainWindow() + window.show() + sys.exit(app.exec()) diff --git a/plist_modifier.py b/plist_modifier.py new file mode 100644 index 0000000..00f48b7 --- /dev/null +++ b/plist_modifier.py @@ -0,0 +1,295 @@ +# plist_modifier.py +import plistlib +import platform +import shutil +import os +import re # For parsing codec names + +if platform.system() == "Linux": + try: + from linux_hardware_info import get_pci_devices_info, get_cpu_info, get_audio_codecs + except ImportError: + print("Warning: linux_hardware_info.py not found. Plist enhancement will be limited.") + get_pci_devices_info = lambda: [] + get_cpu_info = lambda: {} + get_audio_codecs = lambda: [] +else: + print(f"Warning: Hardware info gathering not implemented for {platform.system()} in plist_modifier.") + get_pci_devices_info = lambda: [] + get_cpu_info = lambda: {} + get_audio_codecs = lambda: [] # Dummy function for non-Linux + +# --- Mappings --- +# Values are typically byte-swapped for device-id and some ig-platform-id representations in OpenCore +# For AAPL,ig-platform-id, the first two bytes are often the device-id (swapped), last two are platform related. +# Example: UHD 630 (Desktop Coffee Lake) device-id 0x3E9B -> data <9B3E0000> +# ig-platform-id commonly 0x3E9B0007 -> data <07009B3E> (or other variants) + +INTEL_IGPU_DEFAULTS = { + # Coffee Lake Desktop (UHD 630) - Common + "8086:3e9b": {"AAPL,ig-platform-id": b"\x07\x00\x9B\x3E", "device-id": b"\x9B\x3E\x00\x00", "framebuffer-patch-enable": b"\x01\x00\x00\x00"}, + # Kaby Lake Desktop (HD 630) - Common + "8086:5912": {"AAPL,ig-platform-id": b"\x05\x00\x12\x59", "device-id": b"\x12\x59\x00\x00", "framebuffer-patch-enable": b"\x01\x00\x00\x00"}, + # Skylake Desktop (HD 530) - Common + "8086:1912": {"AAPL,ig-platform-id": b"\x00\x00\x12\x19", "device-id": b"\x12\x19\x00\x00", "framebuffer-patch-enable": b"\x01\x00\x00\x00"}, + + # Alder Lake-S Desktop (UHD 730/750/770) - device-id often needs to be accurate + "8086:4680": {"AAPL,ig-platform-id": b"\x0A\x00\x9B\x46", "device-id": b"\x80\x46\x00\x00", "enable-hdmi20": b"\x01\x00\x00\x00"}, # e.g. i9-12900K UHD 770 (0x4680) -> common platform ID for iGPU only + "8086:4690": {"AAPL,ig-platform-id": b"\x0A\x00\x9B\x46", "device-id": b"\x90\x46\x00\x00", "enable-hdmi20": b"\x01\x00\x00\x00"}, # e.g. i5-12600K UHD 770 (0x4690) + "8086:4692": {"AAPL,ig-platform-id": b"\x0A\x00\x9B\x46", "device-id": b"\x92\x46\x00\x00", "enable-hdmi20": b"\x01\x00\x00\x00"}, # e.g. i5-12400 UHD 730 (0x4692) + # Alternative Alder Lake platform-id (often when dGPU is primary) + "8086:4680_dgpu": {"AAPL,ig-platform-id": b"\x04\x00\x12\x40", "device-id": b"\x80\x46\x00\x00", "enable-hdmi20": b"\x01\x00\x00\x00"}, # Using a suffix for internal logic, not a real PCI ID + "8086:4690_dgpu": {"AAPL,ig-platform-id": b"\x04\x00\x12\x40", "device-id": b"\x90\x46\x00\x00", "enable-hdmi20": b"\x01\x00\x00\x00"}, + "8086:4692_dgpu": {"AAPL,ig-platform-id": b"\x04\x00\x12\x40", "device-id": b"\x92\x46\x00\x00", "enable-hdmi20": b"\x01\x00\x00\x00"}, +} +INTEL_IGPU_PCI_PATH = "PciRoot(0x0)/Pci(0x2,0x0)" + +# Primary keys are now Codec Names. PCI IDs are secondary/fallback. +AUDIO_LAYOUTS = { + # Codec Names (Prefer these) - Extracted from "Codec: Realtek ALCXXX" or similar + "Realtek ALC221": 11, "Realtek ALC233": 11, "Realtek ALC235": 28, + "Realtek ALC255": 11, "Realtek ALC256": 11, "Realtek ALC257": 11, + "Realtek ALC269": 11, "Realtek ALC271": 11, "Realtek ALC282": 11, + "Realtek ALC283": 11, "Realtek ALC285": 11, "Realtek ALC289": 11, + "Realtek ALC295": 11, + "Realtek ALC662": 5, "Realtek ALC671": 11, + "Realtek ALC887": 7, "Realtek ALC888": 7, + "Realtek ALC892": 1, "Realtek ALC897": 11, # Common, 11 often works + "Realtek ALC1150": 1, + "Realtek ALC1200": 7, + "Realtek ALC1220": 7, "Realtek ALC1220-VB": 7, # VB variant often uses same layouts + "Conexant CX20756": 3, # Example Conexant + # Fallback PCI IDs for generic Intel HDA controllers if codec name not matched + "pci_8086:a170": 1, # Sunrise Point-H HD Audio + "pci_8086:a2f0": 1, # Series 200 HD Audio (Kaby Lake) + "pci_8086:a348": 3, # Cannon Point-LP HD Audio + "pci_8086:f0c8": 3, # Comet Lake HD Audio (Series 400) + "pci_8086:43c8": 11,# Tiger Lake-H HD Audio (Series 500) + "pci_8086:7ad0": 11,# Alder Lake PCH-P HD Audio +} +AUDIO_PCI_PATH_FALLBACK = "PciRoot(0x0)/Pci(0x1f,0x3)" + +ETHERNET_KEXT_MAP = { # vendor_id:device_id -> kext_name + "8086:15b8": "IntelMausi.kext", "8086:153a": "IntelMausi.kext", "8086:10f0": "IntelMausi.kext", + "8086:15be": "IntelMausi.kext", "8086:0d4f": "IntelMausi.kext", "8086:15b7": "IntelMausi.kext", # I219-V(3) + "8086:1a1c": "IntelMausi.kext", # Comet Lake-S vPro (I219-LM) + "10ec:8168": "RealtekRTL8111.kext", "10ec:8111": "RealtekRTL8111.kext", + "10ec:2502": "LucyRTL8125Ethernet.kext", # Realtek RTL8125 2.5GbE + "10ec:2600": "LucyRTL8125Ethernet.kext", # Realtek RTL8125B 2.5GbE + "8086:15ec": "AppleIntelI210Ethernet.kext", # I225-V (Often needs AppleIGB.kext or specific patches) + "8086:15f3": "AppleIntelI210Ethernet.kext", # I225-V / I226-V + "14e4:1686": "AirportBrcmFixup.kext", # Placeholder for Broadcom Wi-Fi, actual kext depends on model +} + + +def enhance_config_plist(plist_path: str, target_macos_version_name: str, progress_callback=None) -> bool: + def _report(msg): + if progress_callback: progress_callback(f"[PlistModifier] {msg}") + else: print(f"[PlistModifier] {msg}") + # ... (backup logic same as before) ... + _report(f"Starting config.plist enhancement for: {plist_path}"); _report(f"Target macOS version: {target_macos_version_name.lower()}") + if not os.path.exists(plist_path): _report(f"Error: Plist file not found at {plist_path}"); return False + backup_plist_path = plist_path + ".backup" + try: shutil.copy2(plist_path, backup_plist_path); _report(f"Created backup: {backup_plist_path}") + except Exception as e: _report(f"Error creating backup for {plist_path}: {e}. Proceeding cautiously.") + + config_data = {}; + try: + with open(plist_path, 'rb') as f: config_data = plistlib.load(f) + except Exception as e: _report(f"Error loading plist {plist_path}: {e}"); return False + + pci_devices = []; cpu_info = {}; audio_codecs_detected = [] + if platform.system() == "Linux": + pci_devices = get_pci_devices_info(); cpu_info = get_cpu_info(); audio_codecs_detected = get_audio_codecs() + if not pci_devices: _report("Warning: Could not retrieve PCI hardware info on Linux.") + if not audio_codecs_detected: _report("Warning: Could not detect specific audio codecs on Linux.") + else: _report("Hardware detection for plist enhancement Linux-host only. Skipping hardware-specific mods.") + + dev_props = config_data.setdefault("DeviceProperties", {}).setdefault("Add", {}) + kernel_add = config_data.setdefault("Kernel", {}).setdefault("Add", []) + nvram_add = config_data.setdefault("NVRAM", {}).setdefault("Add", {}) + boot_args_uuid = "7C436110-AB2A-4BBB-A880-FE41995C9F82" + boot_args_section = nvram_add.setdefault(boot_args_uuid, {}) + current_boot_args_str = boot_args_section.get("boot-args", ""); boot_args = set(current_boot_args_str.split()) + modified_plist = False + + # 1. Intel iGPU + intel_igpu_on_host = next((dev for dev in pci_devices if dev['type'] == 'VGA' and dev['vendor_id'] == '8086'), None) + dgpu_present = any(dev['type'] == 'VGA' and dev['vendor_id'] != '8086' for dev in pci_devices) + + if intel_igpu_on_host: + lookup_key = f"{intel_igpu_on_host['vendor_id']}:{intel_igpu_on_host['device_id']}" + # For Alder Lake, if a dGPU is also present, a different platform-id might be preferred. + if lookup_key.startswith("8086:46") and dgpu_present: # Basic check for Alder Lake iGPU + dGPU + lookup_key_dgpu = f"{lookup_key}_dgpu" + if lookup_key_dgpu in INTEL_IGPU_DEFAULTS: + lookup_key = lookup_key_dgpu + _report(f"Intel Alder Lake iGPU ({intel_igpu_on_host['description']}) detected with a dGPU. Using dGPU-specific properties if available.") + + if lookup_key in INTEL_IGPU_DEFAULTS: + _report(f"Applying properties for Intel iGPU: {intel_igpu_on_host['description']} ({lookup_key}).") + igpu_path_properties = dev_props.setdefault(INTEL_IGPU_PCI_PATH, {}) + for key, value in INTEL_IGPU_DEFAULTS[lookup_key].items(): + if igpu_path_properties.get(key) != value: igpu_path_properties[key] = value; _report(f" Set {INTEL_IGPU_PCI_PATH} -> {key}"); modified_plist = True + else: _report(f"Found Intel iGPU: {intel_igpu_on_host['description']} ({lookup_key}) but no default properties in map.") + + # 2. Audio Enhancement - Prioritize detected codec name + audio_device_pci_path_to_patch = AUDIO_PCI_PATH_FALLBACK # Default + audio_layout_set = False + if audio_codecs_detected: + _report(f"Detected audio codecs: {audio_codecs_detected}") + for codec_name_full in audio_codecs_detected: + # Try to match known parts of codec names, e.g. "Realtek ALC897" from "Codec: Realtek ALC897" + # Or "ALC897" if that's how it's stored in AUDIO_LAYOUTS keys + for known_codec_key, layout_id in AUDIO_LAYOUTS.items(): + if not known_codec_key.startswith("pci_"): # Ensure we are checking codec names, not PCI IDs + # Simple substring match or more specific regex + # Example: "Realtek ALC255" should match "ALC255" if key is "ALC255" + # Or if key is "Realtek ALC255" it matches directly + # For "Codec: Realtek ALC255" we might want to extract "Realtek ALC255" + + # Attempt to extract the core codec part (e.g., "ALC255", "CX20756") + simple_codec_name_match = re.search(r"(ALC\d{3,4}(?:-VB)?|CX\d{4,})", codec_name_full, re.IGNORECASE) + simple_codec_name = simple_codec_name_match.group(1) if simple_codec_name_match else None + + if (known_codec_key in codec_name_full) or \ + (simple_codec_name and known_codec_key in simple_codec_name) or \ + (known_codec_key.replace("Realtek ", "") in codec_name_full.replace("Realtek ", "")): # Try matching without "Realtek " + + _report(f"Matched Audio Codec: '{codec_name_full}' (using key '{known_codec_key}'). Setting layout-id to {layout_id}.") + audio_path_properties = dev_props.setdefault(audio_device_pci_path_to_patch, {}) + new_layout_data = plistlib.Data(layout_id.to_bytes(1, 'little')) + if audio_path_properties.get("layout-id") != new_layout_data: + audio_path_properties["layout-id"] = new_layout_data; _report(f" Set {audio_device_pci_path_to_patch} -> layout-id = {layout_id}"); modified_plist = True + audio_layout_set = True; break + if audio_layout_set: break + + if not audio_layout_set: # Fallback to PCI ID of audio controller + _report("No specific audio codec match found or no codecs detected. Falling back to PCI ID for audio controller.") + for dev in pci_devices: + if dev['type'] == 'Audio': + lookup_key = f"pci_{dev['vendor_id']}:{dev['device_id']}" # PCI ID keys are prefixed + if lookup_key in AUDIO_LAYOUTS: + layout_id = AUDIO_LAYOUTS[lookup_key] + _report(f"Found Audio device (PCI): {dev['description']}. Setting layout-id to {layout_id} via PCI ID map.") + audio_path_properties = dev_props.setdefault(audio_device_pci_path_to_patch, {}) + new_layout_data = plistlib.Data(layout_id.to_bytes(1, 'little')) + if audio_path_properties.get("layout-id") != new_layout_data: + audio_path_properties["layout-id"] = new_layout_data; _report(f" Set {audio_device_pci_path_to_patch} -> layout-id = {layout_id}"); modified_plist = True + audio_layout_set = True; break + + if audio_layout_set: # Common action if any layout was set + for kext_entry in kernel_add: + if isinstance(kext_entry, dict) and kext_entry.get("BundlePath") == "AppleALC.kext": + if not kext_entry.get("Enabled", False): kext_entry["Enabled"] = True; _report(" Ensured AppleALC.kext is enabled."); modified_plist = True + break + + # 3. Ethernet Kext Enablement (same logic as before) + for dev in pci_devices: + if dev['type'] == 'Ethernet': + lookup_key = f"{dev['vendor_id']}:{dev['device_id']}" + if lookup_key in ETHERNET_KEXT_MAP: + kext_name = ETHERNET_KEXT_MAP[lookup_key]; _report(f"Found Ethernet: {dev['description']}. Ensuring {kext_name} is enabled.") + kext_modified_in_plist = False + for kext_entry in kernel_add: + if isinstance(kext_entry, dict) and kext_entry.get("BundlePath") == kext_name: + if not kext_entry.get("Enabled", False): kext_entry["Enabled"] = True; _report(f" Enabled {kext_name}."); modified_plist = True + else: _report(f" {kext_name} already enabled.") + kext_modified_in_plist = True; break + if not kext_modified_in_plist: _report(f" Warning: {kext_name} for {dev['description']} not in Kernel->Add list of config.plist.") + break + + # 4. NVIDIA GTX 970 Specific Adjustments + gtx_970_present = any(dev['vendor_id'] == '10de' and dev['device_id'] == '13c2' for dev in pci_devices) + if gtx_970_present: + _report("NVIDIA GTX 970 detected.") + high_sierra_and_older_versions = ["high sierra", "sierra", "el capitan"] + is_high_sierra_or_older_target = target_macos_version_name.lower() in high_sierra_and_older_versions + + original_boot_args_set = set(boot_args) + + if is_high_sierra_or_older_target: + boot_args.add('nvda_drv=1'); boot_args.discard('nv_disable=1') + _report(" Configured for NVIDIA Web Drivers (High Sierra or older target).") + else: # Mojave and newer + boot_args.discard('nvda_drv=1') + if intel_igpu_on_host: + boot_args.add('nv_disable=1') + _report(f" Added nv_disable=1 for {target_macos_version_name} to prioritize detected host iGPU over GTX 970.") + else: + boot_args.discard('nv_disable=1') + _report(f" GTX 970 is likely only GPU. `nv_disable=1` not forced for {target_macos_version_name}. Basic display expected.") + if boot_args != original_boot_args_set: modified_plist = True + + final_boot_args_str = ' '.join(sorted(list(boot_args))) + if boot_args_section.get('boot-args') != final_boot_args_str: + boot_args_section['boot-args'] = final_boot_args_str + _report(f"Updated boot-args to: '{final_boot_args_str}'") + modified_plist = True + + if not modified_plist: + _report("No changes made to config.plist based on detected hardware or existing settings were different from defaults.") + # If no hardware changes on non-Linux, this is expected. + if platform.system() != "Linux" and not pci_devices : return True # No error, just no action + + try: + with open(plist_path, 'wb') as f: + plistlib.dump(config_data, f, sort_keys=True, fmt=plistlib.PlistFormat.XML) # Ensure XML format + _report(f"Successfully saved config.plist to {plist_path}") + return True + except Exception as e: # ... (restore backup logic same as before) + _report(f"Error saving modified plist file {plist_path}: {e}") + try: shutil.copy2(backup_plist_path, plist_path); _report("Restored backup successfully.") + except Exception as backup_error: _report(f"CRITICAL: FAILED TO RESTORE BACKUP. {plist_path} may be corrupt. Backup is at {backup_plist_path}. Error: {backup_error}") + return False + +# if __name__ == '__main__': (Keep the same test block as before, ensure dummy data for kexts is complete) +if __name__ == '__main__': + print("Plist Modifier Standalone Test") # ... (rest of test block as in previous version) + dummy_plist_path = "test_config.plist" + dummy_data = { + "DeviceProperties": {"Add": {}}, + "Kernel": {"Add": [ + {"Arch": "Any", "BundlePath": "Lilu.kext", "Comment": "Lilu", "Enabled": True, "ExecutablePath": "Contents/MacOS/Lilu", "MaxKernel": "", "MinKernel": "", "PlistPath": "Contents/Info.plist"}, + {"Arch": "Any", "BundlePath": "WhateverGreen.kext", "Comment": "WG", "Enabled": True, "ExecutablePath": "Contents/MacOS/WhateverGreen", "MaxKernel": "", "MinKernel": "", "PlistPath": "Contents/Info.plist"}, + {"Arch": "Any", "BundlePath": "AppleALC.kext", "Comment": "AppleALC", "Enabled": False, "ExecutablePath": "Contents/MacOS/AppleALC", "MaxKernel": "", "MinKernel": "", "PlistPath": "Contents/Info.plist"}, + {"Arch": "Any", "BundlePath": "IntelMausi.kext", "Comment": "IntelMausi", "Enabled": False, "ExecutablePath": "Contents/MacOS/IntelMausi", "MaxKernel": "", "MinKernel": "", "PlistPath": "Contents/Info.plist"}, + {"Arch": "Any", "BundlePath": "RealtekRTL8111.kext", "Comment": "Realtek", "Enabled": False, "ExecutablePath": "Contents/MacOS/RealtekRTL8111", "MaxKernel": "", "MinKernel": "", "PlistPath": "Contents/Info.plist"}, + {"Arch": "Any", "BundlePath": "LucyRTL8125Ethernet.kext", "Comment": "LucyRealtek", "Enabled": False, "ExecutablePath": "Contents/MacOS/LucyRTL8125Ethernet", "MaxKernel": "", "MinKernel": "", "PlistPath": "Contents/Info.plist"}, + ]}, + "NVRAM": {"Add": {"7C436110-AB2A-4BBB-A880-FE41995C9F82": {"boot-args": "-v debug=0x100"}}} + } + with open(dummy_plist_path, 'wb') as f: plistlib.dump(dummy_data, f) + print(f"Created dummy {dummy_plist_path} for testing.") + + original_get_pci = get_pci_devices_info; original_get_cpu = get_cpu_info; original_get_audio_codecs = get_audio_codecs + if platform.system() != "Linux": + print("Mocking hardware info for non-Linux.") + get_pci_devices_info = lambda: [ + {'type': 'VGA', 'vendor_id': '8086', 'device_id': '4680', 'description': 'Alder Lake UHD 770', 'full_lspci_line':''}, + {'type': 'Audio', 'vendor_id': '8086', 'device_id': '7ad0', 'description': 'Alder Lake PCH-P HD Audio', 'full_lspci_line':''}, + {'type': 'Ethernet', 'vendor_id': '10ec', 'device_id': '2502', 'description': 'Realtek RTL8125', 'full_lspci_line':''}, + ] + get_cpu_info = lambda: {"Model name": "12th Gen Intel(R) Core(TM) i7-12700K", "Flags": "avx avx2"} + get_audio_codecs = lambda: ["Realtek ALC1220", "Intel Alder Lake-S HDMI"] + + + print("\n--- Testing with Sonoma (should enable iGPU, audio [ALC1220 layout 7], ethernet [LucyRTL8125]) ---") + success_sonoma = enhance_config_plist(dummy_plist_path, "Sonoma", print) + print(f"Plist enhancement for Sonoma {'succeeded' if success_sonoma else 'failed'}.") + if success_sonoma: + with open(dummy_plist_path, 'rb') as f: modified_data = plistlib.load(f) + print(f" Sonoma boot-args: {modified_data.get('NVRAM',{}).get('Add',{}).get(boot_args_uuid,{}).get('boot-args')}") + print(f" Sonoma iGPU props: {modified_data.get('DeviceProperties',{}).get('Add',{}).get(INTEL_IGPU_PCI_PATH)}") + print(f" Sonoma Audio props: {modified_data.get('DeviceProperties',{}).get('Add',{}).get(AUDIO_PCI_PATH_FALLBACK)}") + for kext in modified_data.get("Kernel",{}).get("Add",[]): + if "LucyRTL8125Ethernet.kext" in kext.get("BundlePath",""): print(f" LucyRTL8125Ethernet.kext Enabled: {kext.get('Enabled')}") + if "AppleALC.kext" in kext.get("BundlePath",""): print(f" AppleALC.kext Enabled: {kext.get('Enabled')}") + + + if platform.system() != "Linux": + get_pci_devices_info = original_get_pci; get_cpu_info = original_get_cpu; get_audio_codecs = original_get_audio_codecs + + if os.path.exists(dummy_plist_path): os.remove(dummy_plist_path) + if os.path.exists(dummy_plist_path + ".backup"): os.remove(dummy_plist_path + ".backup") + print(f"Cleaned up dummy plist and backup.") diff --git a/usb_writer_linux.py b/usb_writer_linux.py new file mode 100644 index 0000000..f10eb5c --- /dev/null +++ b/usb_writer_linux.py @@ -0,0 +1,302 @@ +# usb_writer_linux.py (Significant Refactoring for Installer Creation) +import subprocess +import os +import time +import shutil +import glob +import re +import plistlib # For plist_modifier call, and potentially for InstallInfo.plist + +try: + from plist_modifier import enhance_config_plist +except ImportError: + enhance_config_plist = None + print("Warning: plist_modifier.py not found. Plist enhancement feature will be disabled for USBWriterLinux.") + +# Assume a basic OpenCore EFI template directory exists relative to this script +OC_TEMPLATE_DIR = os.path.join(os.path.dirname(__file__), "EFI_template_installer") + + +class USBWriterLinux: + def __init__(self, device: str, macos_download_path: str, + progress_callback=None, enhance_plist_enabled: bool = False, + target_macos_version: str = ""): + self.device = device + self.macos_download_path = macos_download_path + self.progress_callback = progress_callback + self.enhance_plist_enabled = enhance_plist_enabled + self.target_macos_version = target_macos_version # String name like "Sonoma" + + pid = os.getpid() + self.temp_basesystem_hfs_path = f"temp_basesystem_{pid}.hfs" + self.temp_efi_build_dir = f"temp_efi_build_{pid}" + self.temp_shared_support_extract_dir = f"temp_shared_support_extract_{pid}" + + + self.mount_point_usb_esp = f"/mnt/usb_esp_temp_skyscope_{pid}" + self.mount_point_usb_macos_target = f"/mnt/usb_macos_target_temp_skyscope_{pid}" + + self.temp_files_to_clean = [self.temp_basesystem_hfs_path] + self.temp_dirs_to_clean = [ + self.temp_efi_build_dir, self.mount_point_usb_esp, + self.mount_point_usb_macos_target, self.temp_shared_support_extract_dir + ] + + def _report_progress(self, message: str): + if self.progress_callback: self.progress_callback(message) + else: print(message) + + def _run_command(self, command: list[str] | str, check=True, capture_output=False, timeout=None, shell=False, working_dir=None): + self._report_progress(f"Executing: {command if isinstance(command, str) else ' '.join(command)}") + try: + process = subprocess.run( + command, check=check, capture_output=capture_output, text=True, timeout=timeout, + shell=shell, cwd=working_dir, + creationflags=0 # No CREATE_NO_WINDOW on Linux + ) + if capture_output: # Log only if content exists + if process.stdout and process.stdout.strip(): self._report_progress(f"STDOUT: {process.stdout.strip()}") + if process.stderr and process.stderr.strip(): self._report_progress(f"STDERR: {process.stderr.strip()}") + return process + except subprocess.TimeoutExpired: self._report_progress(f"Command timed out after {timeout} seconds."); raise + except subprocess.CalledProcessError as e: self._report_progress(f"Error executing (code {e.returncode}): {e.stderr or e.stdout or str(e)}"); raise + except FileNotFoundError: self._report_progress(f"Error: Command '{command[0] if isinstance(command, list) else command.split()[0]}' not found."); raise + + def _cleanup_temp_files_and_dirs(self): + self._report_progress("Cleaning up temporary files and directories...") + for mp in self.temp_dirs_to_clean: # Unmount first + if os.path.ismount(mp): + self._run_command(["sudo", "umount", "-lf", mp], check=False, timeout=15) + + for f_path in self.temp_files_to_clean: + if os.path.exists(f_path): + try: self._run_command(["sudo", "rm", "-f", f_path], check=False) + except Exception as e: self._report_progress(f"Error removing temp file {f_path}: {e}") + for d_path in self.temp_dirs_to_clean: + if os.path.exists(d_path): + try: self._run_command(["sudo", "rm", "-rf", d_path], check=False) + except Exception as e: self._report_progress(f"Error removing temp dir {d_path}: {e}") + + def check_dependencies(self): + self._report_progress("Checking dependencies (sgdisk, mkfs.vfat, mkfs.hfsplus, 7z, rsync, dd)...") + dependencies = ["sgdisk", "mkfs.vfat", "mkfs.hfsplus", "7z", "rsync", "dd"] + missing_deps = [dep for dep in dependencies if not shutil.which(dep)] + if missing_deps: + msg = f"Missing dependencies: {', '.join(missing_deps)}. Please install them (e.g., hfsprogs, p7zip-full)." + self._report_progress(msg); raise RuntimeError(msg) + self._report_progress("All critical dependencies for Linux USB installer creation found.") + return True + + def _find_source_file(self, patterns: list[str], description: str) -> str | None: + """Finds the first existing file matching a list of glob patterns within self.macos_download_path.""" + self._report_progress(f"Searching for {description} in {self.macos_download_path}...") + for pattern in patterns: + # Using iglob for efficiency if many files, but glob is fine for fewer expected matches + found_files = glob.glob(os.path.join(self.macos_download_path, "**", pattern), recursive=True) + if found_files: + # Prefer files not inside .app bundles if multiple are found, unless it's the app itself. + # This is a simple heuristic. + non_app_files = [f for f in found_files if ".app/" not in f] + target_file = non_app_files[0] if non_app_files else found_files[0] + self._report_progress(f"Found {description} at: {target_file}") + return target_file + self._report_progress(f"Warning: {description} not found with patterns: {patterns}") + return None + + def _extract_hfs_from_dmg(self, dmg_path: str, output_hfs_path: str) -> bool: + """Extracts the primary HFS+ partition image (e.g., '4.hfs') from a DMG.""" + # Assumes BaseSystem.dmg or similar that contains a HFS+ partition image. + temp_extract_dir = f"temp_hfs_extract_{os.getpid()}" + os.makedirs(temp_extract_dir, exist_ok=True) + try: + self._report_progress(f"Extracting HFS+ partition image from {dmg_path}...") + # 7z e -tdmg *.hfs -o (usually 4.hfs or similar) + self._run_command(["7z", "e", "-tdmg", dmg_path, "*.hfs", f"-o{temp_extract_dir}"], check=True) + + hfs_files = glob.glob(os.path.join(temp_extract_dir, "*.hfs")) + if not hfs_files: raise RuntimeError(f"No .hfs file found after extracting {dmg_path}") + + final_hfs_file = max(hfs_files, key=os.path.getsize) # Assume largest is the one + self._report_progress(f"Found HFS+ partition image: {final_hfs_file}. Moving to {output_hfs_path}") + shutil.move(final_hfs_file, output_hfs_path) + return True + except Exception as e: + self._report_progress(f"Error during HFS extraction from DMG: {e}") + return False + finally: + if os.path.exists(temp_extract_dir): shutil.rmtree(temp_extract_dir, ignore_errors=True) + + def format_and_write(self) -> bool: + try: + self.check_dependencies() + self._cleanup_temp_files_and_dirs() + for mp in [self.mount_point_usb_esp, self.mount_point_usb_macos_target, self.temp_efi_build_dir]: + self._run_command(["sudo", "mkdir", "-p", mp]) + + self._report_progress(f"WARNING: ALL DATA ON {self.device} WILL BE ERASED!") + for i in range(1, 10): self._run_command(["sudo", "umount", "-lf", f"{self.device}{i}"], check=False, timeout=5); self._run_command(["sudo", "umount", "-lf", f"{self.device}p{i}"], check=False, timeout=5) + + self._report_progress(f"Partitioning {self.device} with GPT (sgdisk)...") + self._run_command(["sudo", "sgdisk", "--zap-all", self.device]) + self._run_command(["sudo", "sgdisk", "-n", "1:0:+550M", "-t", "1:ef00", "-c", "1:EFI", self.device]) + self._run_command(["sudo", "sgdisk", "-n", "2:0:0", "-t", "2:af00", "-c", "2:Install macOS", self.device]) + self._run_command(["sudo", "partprobe", self.device], timeout=10); time.sleep(3) + + esp_partition_dev = next((f"{self.device}{i}" for i in ["1", "p1"] if os.path.exists(f"{self.device}{i}")), None) + macos_partition_dev = next((f"{self.device}{i}" for i in ["2", "p2"] if os.path.exists(f"{self.device}{i}")), None) + if not (esp_partition_dev and macos_partition_dev): raise RuntimeError(f"Could not reliably determine partition names for {self.device}.") + + self._report_progress(f"Formatting ESP ({esp_partition_dev}) as FAT32...") + self._run_command(["sudo", "mkfs.vfat", "-F", "32", "-n", "EFI", esp_partition_dev]) + self._report_progress(f"Formatting macOS Install partition ({macos_partition_dev}) as HFS+...") + self._run_command(["sudo", "mkfs.hfsplus", "-v", f"Install macOS {self.target_macos_version}", macos_partition_dev]) + + # --- Prepare macOS Installer Content --- + basesystem_dmg_path = self._find_source_file(["BaseSystem.dmg", "InstallAssistant.pkg", "SharedSupport.dmg"], "BaseSystem.dmg or InstallAssistant.pkg or SharedSupport.dmg") + if not basesystem_dmg_path: raise RuntimeError("Essential macOS installer DMG/PKG not found in download path.") + + if basesystem_dmg_path.endswith(".pkg") or "SharedSupport.dmg" in os.path.basename(basesystem_dmg_path) : + # If we found InstallAssistant.pkg or SharedSupport.dmg, we need to extract BaseSystem.hfs from it. + self._report_progress(f"Extracting bootable HFS+ image from {basesystem_dmg_path}...") + if not self._extract_hfs_from_dmg(basesystem_dmg_path, self.temp_basesystem_hfs_path): + raise RuntimeError("Failed to extract HFS+ image from installer assets.") + elif basesystem_dmg_path.endswith("BaseSystem.dmg"): # If it's BaseSystem.dmg directly + self._report_progress(f"Extracting bootable HFS+ image from {basesystem_dmg_path}...") + if not self._extract_hfs_from_dmg(basesystem_dmg_path, self.temp_basesystem_hfs_path): + raise RuntimeError("Failed to extract HFS+ image from BaseSystem.dmg.") + else: + raise RuntimeError(f"Unsupported file type for BaseSystem extraction: {basesystem_dmg_path}") + + + self._report_progress(f"Writing BaseSystem HFS+ image to {macos_partition_dev} using dd...") + self._run_command(["sudo", "dd", f"if={self.temp_basesystem_hfs_path}", f"of={macos_partition_dev}", "bs=4M", "status=progress", "oflag=sync"]) + + self._report_progress("Mounting macOS Install partition on USB...") + self._run_command(["sudo", "mount", macos_partition_dev, self.mount_point_usb_macos_target]) + + # Copy BaseSystem.dmg & .chunklist to /System/Library/CoreServices/ + core_services_path_usb = os.path.join(self.mount_point_usb_macos_target, "System", "Library", "CoreServices") + self._run_command(["sudo", "mkdir", "-p", core_services_path_usb]) + + # Find original BaseSystem.dmg and chunklist in download path to copy them + actual_bs_dmg = self._find_source_file(["BaseSystem.dmg"], "original BaseSystem.dmg for copying") + if actual_bs_dmg: + self._report_progress(f"Copying {actual_bs_dmg} to {core_services_path_usb}/BaseSystem.dmg") + self._run_command(["sudo", "cp", actual_bs_dmg, os.path.join(core_services_path_usb, "BaseSystem.dmg")]) + + bs_chunklist = actual_bs_dmg.replace(".dmg", ".chunklist") + if os.path.exists(bs_chunklist): + self._report_progress(f"Copying {bs_chunklist} to {core_services_path_usb}/BaseSystem.chunklist") + self._run_command(["sudo", "cp", bs_chunklist, os.path.join(core_services_path_usb, "BaseSystem.chunklist")]) + else: self._report_progress(f"Warning: BaseSystem.chunklist not found at {bs_chunklist}") + else: self._report_progress("Warning: Could not find original BaseSystem.dmg in download path to copy to CoreServices.") + + # Copy InstallInfo.plist + install_info_src = self._find_source_file(["InstallInfo.plist"], "InstallInfo.plist") + if install_info_src: + self._report_progress(f"Copying {install_info_src} to {self.mount_point_usb_macos_target}/InstallInfo.plist") + self._run_command(["sudo", "cp", install_info_src, os.path.join(self.mount_point_usb_macos_target, "InstallInfo.plist")]) + else: self._report_progress("Warning: InstallInfo.plist not found in download path.") + + # Copy Packages (placeholder - needs more specific logic based on gibMacOS output structure) + self._report_progress("Placeholder: Copying macOS installation packages to USB (e.g., /System/Installation/Packages)...") + # Example: sudo rsync -a /path/to/downloaded_packages_dir/ /mnt/usb_macos_target/System/Installation/Packages/ + # This needs to correctly identify the source Packages directory from gibMacOS output. + # For now, we'll skip actual copying of packages folder, as its location and content can vary. + # A proper implementation would require inspecting the gibMacOS download structure. + # Create the directory though: + self._run_command(["sudo", "mkdir", "-p", os.path.join(self.mount_point_usb_macos_target, "System", "Installation", "Packages")]) + + + # --- OpenCore EFI Setup --- + self._report_progress("Setting up OpenCore EFI on ESP...") + if not os.path.isdir(OC_TEMPLATE_DIR): + self._report_progress(f"FATAL: OpenCore template directory not found at {OC_TEMPLATE_DIR}. Cannot proceed."); return False + + self._report_progress(f"Copying OpenCore EFI template from {OC_TEMPLATE_DIR} to {self.temp_efi_build_dir}") + self._run_command(["sudo", "cp", "-a", f"{OC_TEMPLATE_DIR}/.", self.temp_efi_build_dir]) # Copy contents + + temp_config_plist_path = os.path.join(self.temp_efi_build_dir, "EFI", "OC", "config.plist") # Assume template is named config.plist + if not os.path.exists(temp_config_plist_path) and os.path.exists(os.path.join(self.temp_efi_build_dir, "EFI", "OC", "config-template.plist")): + # If template is config-template.plist, rename it for enhancement + shutil.move(os.path.join(self.temp_efi_build_dir, "EFI", "OC", "config-template.plist"), temp_config_plist_path) + + if self.enhance_plist_enabled and enhance_config_plist and os.path.exists(temp_config_plist_path): + self._report_progress("Attempting to enhance config.plist...") + if enhance_config_plist(temp_config_plist_path, self.target_macos_version, self._report_progress): + self._report_progress("config.plist enhancement successful.") + else: self._report_progress("config.plist enhancement failed or had issues. Continuing with (potentially original template) plist.") + + self._run_command(["sudo", "mount", esp_partition_dev, self.mount_point_usb_esp]) + self._report_progress(f"Copying final EFI folder to USB ESP ({self.mount_point_usb_esp})...") + self._run_command(["sudo", "rsync", "-avh", "--delete", f"{self.temp_efi_build_dir}/EFI/", f"{self.mount_point_usb_esp}/EFI/"]) + + self._report_progress("USB Installer creation process completed successfully.") + return True + + except Exception as e: + self._report_progress(f"An error occurred during USB writing: {e}") + import traceback; self._report_progress(traceback.format_exc()) + return False + finally: + self._cleanup_temp_files_and_dirs() + +if __name__ == '__main__': + if os.geteuid() != 0: print("Please run this script as root (sudo) for testing."); exit(1) + print("USB Writer Linux Standalone Test - Installer Method") + + mock_download_dir = f"temp_macos_download_test_{os.getpid()}" + os.makedirs(mock_download_dir, exist_ok=True) + + # Create a dummy placeholder for what gibMacOS might download + # This is highly simplified. A real gibMacOS download has a complex structure. + # For this test, we'll simulate having BaseSystem.dmg and InstallInfo.plist + mock_install_data_path = os.path.join(mock_download_dir, "macOS_Install_Data") # Simplified path + os.makedirs(mock_install_data_path, exist_ok=True) + dummy_bs_dmg_path = os.path.join(mock_install_data_path, "BaseSystem.dmg") + dummy_installinfo_path = os.path.join(mock_download_dir, "InstallInfo.plist") # Often at root of a specific product download + + if not os.path.exists(dummy_bs_dmg_path): + # Create a tiny dummy file for 7z to "extract" from. + # To make _extract_hfs_from_dmg work, it needs a real DMG with a HFS part. + # This is hard to mock simply. For now, it will likely fail extraction. + # A better mock would be a small, actual DMG with a tiny HFS file. + print(f"Creating dummy BaseSystem.dmg at {dummy_bs_dmg_path} (will likely fail HFS extraction in test without a real DMG structure)") + with open(dummy_bs_dmg_path, "wb") as f: f.write(os.urandom(1024*10)) # 10KB dummy + if not os.path.exists(dummy_installinfo_path): + with open(dummy_installinfo_path, "w") as f: f.write("DummyInstallInfo") + + # Create dummy EFI template + if not os.path.exists(OC_TEMPLATE_DIR): os.makedirs(OC_TEMPLATE_DIR) + if not os.path.exists(os.path.join(OC_TEMPLATE_DIR, "EFI", "OC")): os.makedirs(os.path.join(OC_TEMPLATE_DIR, "EFI", "OC")) + dummy_config_template_path = os.path.join(OC_TEMPLATE_DIR, "EFI", "OC", "config.plist") # Name it config.plist directly + if not os.path.exists(dummy_config_template_path): + with open(dummy_config_template_path, "w") as f: f.write("TestTemplate") + + print("\nAvailable block devices (be careful!):") + subprocess.run(["lsblk", "-d", "-o", "NAME,SIZE,MODEL"], check=True) + test_device = input("\nEnter target device (e.g., /dev/sdX). THIS DEVICE WILL BE WIPED: ") + + if not test_device or not test_device.startswith("/dev/"): + print("Invalid device. Exiting.") + else: + confirm = input(f"Are you absolutely sure you want to wipe {test_device} and create installer? (yes/NO): ") + success = False + if confirm.lower() == 'yes': + writer = USBWriterLinux( + device=test_device, + macos_download_path=mock_download_dir, + progress_callback=print, + enhance_plist_enabled=True, + target_macos_version="Sonoma" + ) + success = writer.format_and_write() + else: print("Test cancelled by user.") + print(f"Test finished. Success: {success}") + + # Cleanup + if os.path.exists(mock_download_dir): shutil.rmtree(mock_download_dir, ignore_errors=True) + # if os.path.exists(OC_TEMPLATE_DIR) and "EFI_template_installer" in OC_TEMPLATE_DIR : + # shutil.rmtree(OC_TEMPLATE_DIR, ignore_errors=True) # Avoid deleting if it's a real shared template + print("Mock download dir cleaned up.") + print(f"Note: {OC_TEMPLATE_DIR} and its contents might persist if not created by this test run specifically.") diff --git a/usb_writer_macos.py b/usb_writer_macos.py new file mode 100644 index 0000000..aaaaeff --- /dev/null +++ b/usb_writer_macos.py @@ -0,0 +1,316 @@ +# usb_writer_macos.py +import subprocess +import os +import time +import shutil # For checking command existence +import plistlib # For parsing diskutil list -plist output + +class USBWriterMacOS: + def __init__(self, device: str, opencore_qcow2_path: str, macos_qcow2_path: str, + progress_callback=None, enhance_plist_enabled: bool = False, target_macos_version: str = ""): # New args + self.device = device # Should be like /dev/diskX + self.opencore_qcow2_path = opencore_qcow2_path + self.macos_qcow2_path = macos_qcow2_path + self.progress_callback = progress_callback + self.enhance_plist_enabled = enhance_plist_enabled # Store + self.target_macos_version = target_macos_version # Store + + pid = os.getpid() + self.opencore_raw_path = f"opencore_temp_{pid}.raw" + self.macos_raw_path = f"macos_main_temp_{pid}.raw" + self.temp_opencore_mount = f"/tmp/opencore_efi_temp_skyscope_{pid}" + self.temp_usb_esp_mount = f"/tmp/usb_esp_temp_skyscope_{pid}" + self.temp_macos_source_mount = f"/tmp/macos_source_temp_skyscope_{pid}" + self.temp_usb_macos_target_mount = f"/tmp/usb_macos_target_temp_skyscope_{pid}" + + self.temp_files_to_clean = [self.opencore_raw_path, self.macos_raw_path] + self.temp_mount_points_to_clean = [ + self.temp_opencore_mount, self.temp_usb_esp_mount, + self.temp_macos_source_mount, self.temp_usb_macos_target_mount + ] + self.attached_raw_images_devices = [] # Store devices from hdiutil attach + + def _report_progress(self, message: str): + print(message) # For standalone testing + if self.progress_callback: + self.progress_callback(message) + + def _run_command(self, command: list[str], check=True, capture_output=False, timeout=None): + self._report_progress(f"Executing: {' '.join(command)}") + try: + process = subprocess.run( + command, check=check, capture_output=capture_output, text=True, timeout=timeout + ) + if capture_output: + if process.stdout and process.stdout.strip(): + self._report_progress(f"STDOUT: {process.stdout.strip()}") + if process.stderr and process.stderr.strip(): + self._report_progress(f"STDERR: {process.stderr.strip()}") + return process + except subprocess.TimeoutExpired: + self._report_progress(f"Command {' '.join(command)} timed out after {timeout} seconds.") + raise + except subprocess.CalledProcessError as e: + self._report_progress(f"Error executing {' '.join(command)} (code {e.returncode}): {e.stderr or e.stdout or str(e)}") + raise + except FileNotFoundError: + self._report_progress(f"Error: Command '{command[0]}' not found. Is it installed and in PATH?") + raise + + def _cleanup_temp_files(self): + self._report_progress("Cleaning up temporary image files...") + for f_path in self.temp_files_to_clean: + if os.path.exists(f_path): + try: + os.remove(f_path) + self._report_progress(f"Removed {f_path}") + except OSError as e: + self._report_progress(f"Error removing {f_path}: {e}") + + def _unmount_path(self, mount_path_or_device, is_device=False, force=False): + target = mount_path_or_device + cmd_base = ["diskutil"] + action = "unmountDisk" if is_device else "unmount" + + if force: + cmd = cmd_base + [action, "force", target] + else: + cmd = cmd_base + [action, target] + + is_target_valid_for_unmount = (os.path.ismount(mount_path_or_device) and not is_device) or \ + (is_device and os.path.exists(target)) + + if is_target_valid_for_unmount: + self._report_progress(f"Attempting to unmount {target} (Action: {action}, Force: {force})...") + self._run_command(cmd, check=False, timeout=30) + + def _detach_raw_image_device(self, device_path): + if device_path and os.path.exists(device_path): + self._report_progress(f"Detaching raw image device {device_path}...") + try: + info_check = subprocess.run(["diskutil", "info", device_path], capture_output=True, text=True, check=False) + if info_check.returncode == 0: + self._run_command(["hdiutil", "detach", device_path, "-force"], check=False, timeout=30) + else: + self._report_progress(f"Device {device_path} appears invalid or already detached.") + except Exception as e: + self._report_progress(f"Exception while checking/detaching {device_path}: {e}") + + def _cleanup_all_mounts_and_mappings(self): + self._report_progress("Cleaning up all temporary mounts and attached raw images...") + for mp in reversed(self.temp_mount_points_to_clean): + self._unmount_path(mp, force=True) + if os.path.exists(mp): + try: os.rmdir(mp) + except OSError as e: self._report_progress(f"Could not rmdir {mp}: {e}") + + devices_to_detach = list(self.attached_raw_images_devices) + for dev_path in devices_to_detach: + self._detach_raw_image_device(dev_path) + self.attached_raw_images_devices = [] + + + def check_dependencies(self): + self._report_progress("Checking dependencies (qemu-img, diskutil, hdiutil, rsync)...") + dependencies = ["qemu-img", "diskutil", "hdiutil", "rsync"] + missing_deps = [] + for dep in dependencies: + if not shutil.which(dep): + missing_deps.append(dep) + + if missing_deps: + msg = f"Missing dependencies: {', '.join(missing_deps)}. `qemu-img` might need to be installed (e.g., via Homebrew: `brew install qemu`). `diskutil`, `hdiutil`, `rsync` are usually standard on macOS." + self._report_progress(msg) + raise RuntimeError(msg) + + self._report_progress("All critical dependencies found.") + return True + + def _get_partition_device_id(self, parent_disk_id_str: str, partition_label_or_type: str) -> str | None: + """Finds partition device ID by Volume Name or Content Hint.""" + target_disk_id = parent_disk_id_str.replace("/dev/", "") + self._report_progress(f"Searching for partition '{partition_label_or_type}' on disk '{target_disk_id}'") + try: + result = self._run_command(["diskutil", "list", "-plist", target_disk_id], capture_output=True) + if not result.stdout: + self._report_progress(f"No stdout from diskutil list for {target_disk_id}") + return None + + plist_data = plistlib.loads(result.stdout.encode('utf-8')) + + all_disks_and_partitions = plist_data.get("AllDisksAndPartitions", []) + if not isinstance(all_disks_and_partitions, list): + if plist_data.get("DeviceIdentifier") == target_disk_id: + all_disks_and_partitions = [plist_data] + else: + all_disks_and_partitions = [] + + for disk_info_entry in all_disks_and_partitions: + current_disk_id_in_plist = disk_info_entry.get("DeviceIdentifier") + if current_disk_id_in_plist == target_disk_id: + for part_info in disk_info_entry.get("Partitions", []): + vol_name = part_info.get("VolumeName") + content_hint = part_info.get("Content") + device_id = part_info.get("DeviceIdentifier") + + if device_id: + if vol_name and vol_name.strip().lower() == partition_label_or_type.strip().lower(): + self._report_progress(f"Found partition by VolumeName: {vol_name} -> /dev/{device_id}") + return f"/dev/{device_id}" + if content_hint and content_hint.strip().lower() == partition_label_or_type.strip().lower(): + self._report_progress(f"Found partition by Content type: {content_hint} -> /dev/{device_id}") + return f"/dev/{device_id}" + + self._report_progress(f"Partition '{partition_label_or_type}' not found on disk '{target_disk_id}'.") + return None + except Exception as e: + self._report_progress(f"Error parsing 'diskutil list -plist {target_disk_id}': {e}") + return None + + def format_and_write(self) -> bool: + try: + self.check_dependencies() + self._cleanup_all_mounts_and_mappings() + + for mp in self.temp_mount_points_to_clean: + os.makedirs(mp, exist_ok=True) + + self._report_progress(f"WARNING: ALL DATA ON {self.device} WILL BE ERASED!") + self._report_progress(f"Unmounting disk {self.device} (force)...") + self._unmount_path(self.device, is_device=True, force=True) + time.sleep(2) + + self._report_progress(f"Partitioning {self.device} with GPT scheme...") + self._run_command([ + "diskutil", "partitionDisk", self.device, "GPT", + "MS-DOS FAT32", "EFI", "551MiB", + "JHFS+", "macOS_USB", "0b" + ], timeout=180) + time.sleep(3) + + esp_partition_dev = self._get_partition_device_id(self.device, "EFI") + macos_partition_dev = self._get_partition_device_id(self.device, "macOS_USB") + + if not (esp_partition_dev and os.path.exists(esp_partition_dev)): + esp_partition_dev = f"{self.device}s1" + if not (macos_partition_dev and os.path.exists(macos_partition_dev)): + macos_partition_dev = f"{self.device}s2" + + if not (os.path.exists(esp_partition_dev) and os.path.exists(macos_partition_dev)): + raise RuntimeError(f"Could not identify partitions on {self.device}. ESP: {esp_partition_dev}, macOS: {macos_partition_dev}") + + self._report_progress(f"Identified ESP: {esp_partition_dev}, macOS Partition: {macos_partition_dev}") + + # --- Write EFI content --- + self._report_progress(f"Converting OpenCore QCOW2 ({self.opencore_qcow2_path}) to RAW ({self.opencore_raw_path})...") + self._run_command(["qemu-img", "convert", "-O", "raw", self.opencore_qcow2_path, self.opencore_raw_path]) + + self._report_progress(f"Attaching RAW OpenCore image ({self.opencore_raw_path})...") + attach_cmd_efi = ["hdiutil", "attach", "-nomount", "-imagekey", "diskimage-class=CRawDiskImage", self.opencore_raw_path] + efi_attach_output = self._run_command(attach_cmd_efi, capture_output=True).stdout.strip() + raw_efi_disk_id = efi_attach_output.splitlines()[-1].strip().split()[0] + if not raw_efi_disk_id.startswith("/dev/disk"): + raise RuntimeError(f"Failed to attach raw EFI image: {efi_attach_output}") + self.attached_raw_images_devices.append(raw_efi_disk_id) + self._report_progress(f"Attached raw OpenCore image as {raw_efi_disk_id}") + time.sleep(2) + + source_efi_partition_dev = self._get_partition_device_id(raw_efi_disk_id, "EFI") or f"{raw_efi_disk_id}s1" + + self._report_progress(f"Mounting source EFI partition ({source_efi_partition_dev}) to {self.temp_opencore_mount}...") + self._run_command(["diskutil", "mount", "readOnly", "-mountPoint", self.temp_opencore_mount, source_efi_partition_dev], timeout=30) + + self._report_progress(f"Mounting target USB ESP ({esp_partition_dev}) to {self.temp_usb_esp_mount}...") + self._run_command(["diskutil", "mount", "-mountPoint", self.temp_usb_esp_mount, esp_partition_dev], timeout=30) + + source_efi_content_path = os.path.join(self.temp_opencore_mount, "EFI") + if not os.path.isdir(source_efi_content_path): source_efi_content_path = self.temp_opencore_mount + + target_efi_dir_on_usb = os.path.join(self.temp_usb_esp_mount, "EFI") + self._report_progress(f"Copying EFI files from {source_efi_content_path} to {target_efi_dir_on_usb}...") + self._run_command(["sudo", "rsync", "-avh", "--delete", f"{source_efi_content_path}/", f"{target_efi_dir_on_usb}/"]) + + self._unmount_path(self.temp_opencore_mount, force=True) + self._unmount_path(self.temp_usb_esp_mount, force=True) + self._detach_raw_image_device(raw_efi_disk_id); raw_efi_disk_id = None + + # --- Write macOS main image (File-level copy) --- + self._report_progress(f"Converting macOS QCOW2 ({self.macos_qcow2_path}) to RAW ({self.macos_raw_path})...") + self._report_progress("This may take a very long time...") + self._run_command(["qemu-img", "convert", "-O", "raw", self.macos_qcow2_path, self.macos_raw_path]) + + self._report_progress(f"Attaching RAW macOS image ({self.macos_raw_path})...") + attach_cmd_macos = ["hdiutil", "attach", "-nomount", "-imagekey", "diskimage-class=CRawDiskImage", self.macos_raw_path] + macos_attach_output = self._run_command(attach_cmd_macos, capture_output=True).stdout.strip() + raw_macos_disk_id = macos_attach_output.splitlines()[-1].strip().split()[0] + if not raw_macos_disk_id.startswith("/dev/disk"): + raise RuntimeError(f"Failed to attach raw macOS image: {macos_attach_output}") + self.attached_raw_images_devices.append(raw_macos_disk_id) + self._report_progress(f"Attached raw macOS image as {raw_macos_disk_id}") + time.sleep(2) + + source_macos_part_dev = self._get_partition_device_id(raw_macos_disk_id, "Apple_APFS_Container") or \ + self._get_partition_device_id(raw_macos_disk_id, "Apple_APFS") or \ + self._get_partition_device_id(raw_macos_disk_id, "Apple_HFS") or \ + f"{raw_macos_disk_id}s2" + if not (source_macos_part_dev and os.path.exists(source_macos_part_dev)): + raise RuntimeError(f"Could not find source macOS partition on {raw_macos_disk_id}") + + self._report_progress(f"Mounting source macOS partition ({source_macos_part_dev}) to {self.temp_macos_source_mount}...") + self._run_command(["diskutil", "mount", "readOnly", "-mountPoint", self.temp_macos_source_mount, source_macos_part_dev], timeout=60) + + self._report_progress(f"Mounting target USB macOS partition ({macos_partition_dev}) to {self.temp_usb_macos_target_mount}...") + self._run_command(["diskutil", "mount", "-mountPoint", self.temp_usb_macos_target_mount, macos_partition_dev], timeout=30) + + self._report_progress(f"Copying macOS system files from {self.temp_macos_source_mount} to {self.temp_usb_macos_target_mount} (sudo rsync)...") + self._report_progress("This will also take a very long time.") + self._run_command([ + "sudo", "rsync", "-avh", "--delete", + "--exclude=.Spotlight-V100", "--exclude=.fseventsd", "--exclude=/.Trashes", "--exclude=/System/Volumes/VM", "--exclude=/private/var/vm", + f"{self.temp_macos_source_mount}/", f"{self.temp_usb_macos_target_mount}/" + ]) + + self._report_progress("USB writing process completed successfully.") + return True + + except Exception as e: + self._report_progress(f"An error occurred during USB writing on macOS: {e}") + import traceback + self._report_progress(traceback.format_exc()) + return False + finally: + self._cleanup_all_mounts_and_mappings() + self._cleanup_temp_files() + +if __name__ == '__main__': + if platform.system() != "Darwin": print("This script is intended for macOS."); exit(1) + print("USB Writer macOS Standalone Test - File Copy Method") + + mock_opencore_path = "mock_opencore_macos.qcow2" + mock_macos_path = "mock_macos_macos.qcow2" + if not os.path.exists(mock_opencore_path): subprocess.run(["qemu-img", "create", "-f", "qcow2", mock_opencore_path, "384M"]) + if not os.path.exists(mock_macos_path): subprocess.run(["qemu-img", "create", "-f", "qcow2", mock_macos_path, "1G"]) + + print("\nAvailable disks (use 'diskutil list external physical' in Terminal to identify your USB):") + subprocess.run(["diskutil", "list", "external", "physical"], check=False) + test_device = input("\nEnter target disk identifier (e.g., /dev/diskX - NOT /dev/diskXsY). THIS DISK WILL BE WIPED: ") + + if not test_device or not test_device.startswith("/dev/disk"): + print("Invalid disk identifier. Exiting.") + if os.path.exists(mock_opencore_path): os.remove(mock_opencore_path) + if os.path.exists(mock_macos_path): os.remove(mock_macos_path) + exit(1) + + confirm = input(f"Are you sure you want to wipe {test_device} and write mock images? (yes/NO): ") + success = False + if confirm.lower() == 'yes': + print("Ensure you have sudo privileges for rsync if needed, or app is run as root.") + writer = USBWriterMacOS(test_device, mock_opencore_path, mock_macos_path, print) + success = writer.format_and_write() + else: + print("Test cancelled.") + + print(f"Test finished. Success: {success}") + if os.path.exists(mock_opencore_path): os.remove(mock_opencore_path) + if os.path.exists(mock_macos_path): os.remove(mock_macos_path) + print("Mock files cleaned up.") diff --git a/usb_writer_windows.py b/usb_writer_windows.py new file mode 100644 index 0000000..8008d60 --- /dev/null +++ b/usb_writer_windows.py @@ -0,0 +1,269 @@ +# usb_writer_windows.py +import subprocess +import os +import time +import shutil +import re # For parsing diskpart output +import sys # For checking psutil import + +# Try to import QMessageBox for the placeholder, otherwise use a mock for standalone test +try: + from PyQt6.QtWidgets import QMessageBox +except ImportError: + class QMessageBox: # Mock for standalone testing + @staticmethod + def information(*args): print(f"INFO (QMessageBox mock): Title='{args[1]}', Message='{args[2]}'") + @staticmethod + def warning(*args): print(f"WARNING (QMessageBox mock): Title='{args[1]}', Message='{args[2]}'"); return QMessageBox # Mock button press + Yes = 1 # Mock value + No = 0 # Mock value + Cancel = 0 # Mock value + + +class USBWriterWindows: + def __init__(self, device_id: str, opencore_qcow2_path: str, macos_qcow2_path: str, + progress_callback=None, enhance_plist_enabled: bool = False, target_macos_version: str = ""): + # device_id is expected to be the disk number string, e.g., "1", "2" or "disk 1", "disk 2" + self.disk_number = "".join(filter(str.isdigit, device_id)) + if not self.disk_number: + raise ValueError(f"Invalid device_id format: '{device_id}'. Must contain a disk number.") + + self.physical_drive_path = f"\\\\.\\PhysicalDrive{self.disk_number}" + + self.opencore_qcow2_path = opencore_qcow2_path + self.macos_qcow2_path = macos_qcow2_path + self.progress_callback = progress_callback + self.enhance_plist_enabled = enhance_plist_enabled # Not used in Windows writer yet + self.target_macos_version = target_macos_version # Not used in Windows writer yet + + pid = os.getpid() + self.opencore_raw_path = f"opencore_temp_{pid}.raw" + self.macos_raw_path = f"macos_main_temp_{pid}.raw" + self.temp_efi_extract_dir = f"temp_efi_files_{pid}" + + self.temp_files_to_clean = [self.opencore_raw_path, self.macos_raw_path] + self.temp_dirs_to_clean = [self.temp_efi_extract_dir] + self.assigned_efi_letter = None + + def _report_progress(self, message: str): + if self.progress_callback: self.progress_callback(message) + else: print(message) + + def _run_command(self, command: list[str] | str, check=True, capture_output=False, timeout=None, shell=False, working_dir=None): + self._report_progress(f"Executing: {command if isinstance(command, str) else ' '.join(command)}") + try: + process = subprocess.run( + command, check=check, capture_output=capture_output, text=True, timeout=timeout, shell=shell, cwd=working_dir, + creationflags=subprocess.CREATE_NO_WINDOW + ) + if capture_output: + if process.stdout and process.stdout.strip(): self._report_progress(f"STDOUT: {process.stdout.strip()}") + if process.stderr and process.stderr.strip(): self._report_progress(f"STDERR: {process.stderr.strip()}") + return process + except subprocess.TimeoutExpired: self._report_progress(f"Command timed out after {timeout} seconds."); raise + except subprocess.CalledProcessError as e: self._report_progress(f"Error executing (code {e.returncode}): {e.stderr or e.stdout or str(e)}"); raise + except FileNotFoundError: self._report_progress(f"Error: Command '{command[0] if isinstance(command, list) else command.split()[0]}' not found."); raise + + + def _run_diskpart_script(self, script_content: str, capture_output_for_parse=False) -> str | None: + script_file_path = f"diskpart_script_{os.getpid()}.txt" + with open(script_file_path, "w") as f: f.write(script_content) + output_text = "" # Initialize to empty string + try: + self._report_progress(f"Running diskpart script:\n{script_content}") + process = self._run_command(["diskpart", "/s", script_file_path], capture_output=True, check=False) + output_text = (process.stdout or "") + "\n" + (process.stderr or "") # Combine, as diskpart output can be inconsistent + + # Check for known success messages, otherwise assume potential issue or log output for manual check. + # This is not a perfect error check for diskpart. + success_indicators = [ + "DiskPart successfully", "successfully completed", "succeeded in creating", + "successfully formatted", "successfully assigned" + ] + has_success_indicator = any(indicator in output_text for indicator in success_indicators) + has_error_indicator = "Virtual Disk Service error" in output_text or "DiskPart has encountered an error" in output_text + + if has_error_indicator: + self._report_progress(f"Diskpart script may have failed. Output:\n{output_text}") + # Optionally raise an error here if script is critical + # raise subprocess.CalledProcessError(1, "diskpart", output=output_text) + elif not has_success_indicator and "There are no partitions on this disk to show" not in output_text: # Allow benign message + self._report_progress(f"Diskpart script output does not clearly indicate success. Output:\n{output_text}") + + + if capture_output_for_parse: + return output_text + finally: + if os.path.exists(script_file_path): os.remove(script_file_path) + return output_text if capture_output_for_parse else None # Return None if not capturing for parse + + + def _cleanup_temp_files_and_dirs(self): + self._report_progress("Cleaning up...") + for f_path in self.temp_files_to_clean: + if os.path.exists(f_path): + try: os.remove(f_path) + except Exception as e: self._report_progress(f"Could not remove temp file {f_path}: {e}") + for d_path in self.temp_dirs_to_clean: + if os.path.exists(d_path): + try: shutil.rmtree(d_path, ignore_errors=True) + except Exception as e: self._report_progress(f"Could not remove temp dir {d_path}: {e}") + + + def _find_available_drive_letter(self) -> str | None: + import string; used_letters = set() + try: + # Check if psutil was imported by the main application + if 'psutil' in sys.modules: + partitions = sys.modules['psutil'].disk_partitions(all=True) + for p in partitions: + if p.mountpoint and len(p.mountpoint) >= 2 and p.mountpoint[1] == ':': # Check for "X:" + used_letters.add(p.mountpoint[0].upper()) + except Exception as e: + self._report_progress(f"Could not list used drive letters with psutil: {e}. Will try common letters.") + + for letter in "STUVWXYZGHIJKLMNOPQR": + if letter not in used_letters and letter > 'D': # Avoid A, B, C, D + # Further check if letter is truly available (e.g. subst) - more complex, skip for now + return letter + return None + + def check_dependencies(self): + self._report_progress("Checking dependencies (qemu-img, diskpart, robocopy)... DD for Win & 7z are manual checks.") + dependencies = ["qemu-img", "diskpart", "robocopy"]; missing = [dep for dep in dependencies if not shutil.which(dep)] + if missing: raise RuntimeError(f"Missing dependencies: {', '.join(missing)}. qemu-img needs install & PATH.") + self._report_progress("Base dependencies found. Ensure 'dd for Windows' and '7z.exe' are in PATH if needed.") + return True + + def format_and_write(self) -> bool: + try: + self.check_dependencies() + self._cleanup_temp_files_and_dirs() # Clean before start + os.makedirs(self.temp_efi_extract_dir, exist_ok=True) + + self._report_progress(f"WARNING: ALL DATA ON DISK {self.disk_number} ({self.physical_drive_path}) WILL BE ERASED!") + + self.assigned_efi_letter = self._find_available_drive_letter() + if not self.assigned_efi_letter: raise RuntimeError("Could not find an available drive letter for EFI.") + self._report_progress(f"Will attempt to assign letter {self.assigned_efi_letter}: to EFI partition.") + + diskpart_script_part1 = f"select disk {self.disk_number}\nclean\nconvert gpt\n" + diskpart_script_part1 += f"create partition efi size=550\nformat fs=fat32 quick label=EFI\nassign letter={self.assigned_efi_letter}\n" + diskpart_script_part1 += "create partition primary label=macOS_USB\nexit\n" + self._run_diskpart_script(diskpart_script_part1) + time.sleep(5) + + macos_partition_offset_str = "Offset not determined" + macos_partition_number_str = "2 (assumed)" + + diskpart_script_detail = f"select disk {self.disk_number}\nselect partition 2\ndetail partition\nexit\n" + detail_output = self._run_diskpart_script(diskpart_script_detail, capture_output_for_parse=True) + + if detail_output: + self._report_progress(f"Detail Partition Output:\n{detail_output}") + offset_match = re.search(r"Offset in Bytes\s*:\s*(\d+)", detail_output, re.IGNORECASE) + if offset_match: macos_partition_offset_str = f"{offset_match.group(1)} bytes ({int(offset_match.group(1)) // (1024*1024)} MiB)" + + # Try to find the line "Partition X" where X is the number we want + part_num_search = re.search(r"Partition\s+(\d+)\s*\n\s*Type", detail_output, re.IGNORECASE | re.MULTILINE) + if part_num_search: + macos_partition_number_str = part_num_search.group(1) + self._report_progress(f"Determined macOS partition number: {macos_partition_number_str}") + else: # Fallback if the above specific regex fails + # Look for lines like "Partition 2", "Type : xxxxx" + # This is brittle if diskpart output format changes + partition_lines = [line for line in detail_output.splitlines() if "Partition " in line and "Type :" in line] + if len(partition_lines) > 0 : # Assuming the one we want is the last "Partition X" before other details + last_part_match = re.search(r"Partition\s*(\d+)", partition_lines[-1]) + if last_part_match: macos_partition_number_str = last_part_match.group(1) + + + self._report_progress(f"Converting OpenCore QCOW2 to RAW: {self.opencore_raw_path}") + self._run_command(["qemu-img", "convert", "-O", "raw", self.opencore_qcow2_path, self.opencore_raw_path]) + + if shutil.which("7z"): + self._report_progress("Attempting EFI extraction using 7-Zip...") + self._run_command(["7z", "x", self.opencore_raw_path, f"-o{self.temp_efi_extract_dir}", "EFI", "-r", "-y"], check=False) + source_efi_folder = os.path.join(self.temp_efi_extract_dir, "EFI") + if not os.path.isdir(source_efi_folder): + if os.path.exists(os.path.join(self.temp_efi_extract_dir, "BOOTX64.EFI")): source_efi_folder = self.temp_efi_extract_dir + else: raise RuntimeError("Could not extract EFI folder using 7-Zip from OpenCore image.") + + target_efi_on_usb = f"{self.assigned_efi_letter}:\\EFI" + if not os.path.exists(f"{self.assigned_efi_letter}:\\"): # Check if drive letter is mounted + time.sleep(3) # Wait a bit more + if not os.path.exists(f"{self.assigned_efi_letter}:\\"): + # Attempt to re-assign just in case + self._report_progress(f"Re-assigning drive letter {self.assigned_efi_letter} to EFI partition...") + reassign_script = f"select disk {self.disk_number}\nselect partition 1\nassign letter={self.assigned_efi_letter}\nexit\n" + self._run_diskpart_script(reassign_script) + time.sleep(3) + if not os.path.exists(f"{self.assigned_efi_letter}:\\"): + raise RuntimeError(f"EFI partition {self.assigned_efi_letter}: not accessible after assign/re-assign.") + + if not os.path.exists(target_efi_on_usb): os.makedirs(target_efi_on_usb, exist_ok=True) + self._report_progress(f"Copying EFI files from '{source_efi_folder}' to '{target_efi_on_usb}'") + self._run_command(["robocopy", source_efi_folder, target_efi_on_usb, "/E", "/S", "/NFL", "/NDL", "/NJH", "/NJS", "/NC", "/NS", "/NP", "/XO"], check=True) # Added /XO to exclude older + else: raise RuntimeError("7-Zip CLI (7z.exe) not found in PATH for EFI extraction.") + + self._report_progress(f"Converting macOS QCOW2 to RAW: {self.macos_raw_path}") + self._run_command(["qemu-img", "convert", "-O", "raw", self.macos_qcow2_path, self.macos_raw_path]) + + abs_macos_raw_path = os.path.abspath(self.macos_raw_path) + guidance_message = ( + f"RAW macOS image conversion complete:\n'{abs_macos_raw_path}'\n\n" + f"Target USB: Disk {self.disk_number} (Path: {self.physical_drive_path})\n" + f"The target macOS partition is: Partition {macos_partition_number_str}\n" + f"Calculated Offset (approx): {macos_partition_offset_str}\n\n" + "MANUAL STEP REQUIRED using a 'dd for Windows' utility:\n" + "1. Open Command Prompt or PowerShell AS ADMINISTRATOR.\n" + "2. Carefully identify your 'dd for Windows' utility and its exact syntax.\n" + " Common utilities: dd from SUSE (recommended), dd by chrysocome.net.\n" + "3. Example 'dd' command (SYNTAX VARIES GREATLY BETWEEN DD TOOLS!):\n" + f" `dd if=\"{abs_macos_raw_path}\" of={self.physical_drive_path} bs=4M --progress`\n" + " (This example writes to the whole disk, which might be okay if your macOS partition is the first primary after EFI and occupies the rest). \n" + " A SAFER (but more complex) approach if your 'dd' supports it, is to write directly to the partition's OFFSET (requires dd that handles PhysicalDrive offsets correctly):\n" + f" `dd if=\"{abs_macos_raw_path}\" of={self.physical_drive_path} seek= bs= ...`\n" + " (The 'seek' parameter and its units depend on your dd tool. The offset from diskpart is in bytes.)\n\n" + "VERIFY YOUR DD COMMAND AND TARGETS BEFORE EXECUTION. DATA LOSS IS LIKELY IF INCORRECT.\n" + "This tool cannot automate this step due to the variability and risks of 'dd' utilities on Windows." + ) + self._report_progress(f"GUIDANCE:\n{guidance_message}") + QMessageBox.information(None, "Manual macOS Image Write Required", guidance_message) + + self._report_progress("Windows USB writing (EFI part automated, macOS part manual guidance provided) process initiated.") + return True + + except Exception as e: + self._report_progress(f"Error during Windows USB writing: {e}") + import traceback; self._report_progress(traceback.format_exc()) + return False + finally: + if self.assigned_efi_letter: + self._run_diskpart_script(f"select volume {self.assigned_efi_letter}\nremove letter={self.assigned_efi_letter}\nexit") + self._cleanup_temp_files_and_dirs() + +if __name__ == '__main__': + if platform.system() != "Windows": + print("This script is for Windows standalone testing."); exit(1) + print("USB Writer Windows Standalone Test - Improved Guidance") + mock_oc = "mock_oc_win.qcow2"; mock_mac = "mock_mac_win.qcow2" + # Ensure qemu-img is available for mock file creation + if not shutil.which("qemu-img"): + print("qemu-img not found, cannot create mock files for test. Exiting.") + exit(1) + if not os.path.exists(mock_oc): subprocess.run(["qemu-img", "create", "-f", "qcow2", mock_oc, "384M"]) + if not os.path.exists(mock_mac): subprocess.run(["qemu-img", "create", "-f", "qcow2", mock_mac, "1G"]) + + disk_id_input = input("Enter target disk NUMBER (e.g., '1' for 'disk 1'). THIS DISK WILL BE WIPES: ") + if not disk_id_input.isdigit(): print("Invalid disk number."); exit(1) + + if input(f"Sure to wipe disk {disk_id_input}? (yes/NO): ").lower() == 'yes': + # USBWriterWindows expects just the disk number string (e.g., "1") + writer = USBWriterWindows(disk_id_input, mock_oc, mock_mac, print) + writer.format_and_write() + else: print("Cancelled.") + + if os.path.exists(mock_oc): os.remove(mock_oc) + if os.path.exists(mock_mac): os.remove(mock_mac) + print("Mocks cleaned.") diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..6395aab --- /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 42ebe93..d4ef8b7 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========== ' "$(