├── .vscode └── extensions.json ├── CHANGELOG.md ├── LICENSE.md ├── README.md └── windows-esd-to-iso /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "mkhl.shfmt", 4 | "timonwong.shellcheck" 5 | ] 6 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changes 2 | 3 | ## 1.1.0 - 2025-11-19 4 | 5 | 1. Additional preflight checks were added to warn if trying to run this script on anything besides macOS (it's only designed and tested there) as well as error if "hdiutil" is missing. 6 | 7 | 2. The `NO_CLEANUP` environment variable was added to prevent the temporary directory from being removed on error or completion. 8 | 9 | ## 1.0.0 - 2024-10-15 10 | 11 | This is the first actual release. Before this point, windows-esd-to-iso was a rolling repository. All changes here are from the original version. 12 | 13 | 1. Replaced status messages with nicer, easier-to-read messages in color. 14 | 15 | 2. Added error checking for presence of wimlib tools and ESD image before starting. 16 | 17 | 3. Added error checking to image count check. 18 | 19 | 4. Various improvements to code quality. 20 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2023, 2024, 2025 Mattie Behrens. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject 9 | to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR 18 | ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 19 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # windows-esd-to-iso 2 | 3 | A tool to convert a Windows 11 electronic software distribution (ESD) to a bootable ISO image. 4 | 5 | Since Microsoft does not distribute Windows ARM ISO images, [a way to download ESDs](#downloading-esds) plus this tool allows you to obtain a Windows ARM ISO image—e.g. for use in a virtual machine, such as [UTM](https://getutm.app) or [the free VMware Fusion Player](https://www.vmware.com/go/getfusionplayer). 6 | 7 | ## Requirements 8 | 9 | - The command-line tools from [wimlib](https://wimlib.net) (available in [Homebrew](https://brew.sh)) 10 | 11 | All remaining requirements are already included in macOS. Patches are welcome for portability. 12 | 13 | ## Usage 14 | 15 | ``` 16 | windows-esd-to-iso ESD_FILE 17 | ``` 18 | 19 | Converts the ESD in ESD_FILE to ISO format. 20 | 21 | On completion or on error, this tool removes its temporary directory (which can be large.) If you don't want this, set `NO_CLEANUP` to any value: 22 | 23 | ``` 24 | NO_CLEANUP=1 windows-esd-to-iso ESD_FILE 25 | ``` 26 | 27 | ## How it works 28 | 29 | [windows-esd-to-iso](./windows-esd-to-iso) will use the wimlib tools to inspect, deconstruct, and assemble into an installation tree the images inside an ESD. 30 | 31 | Specifically, it assumes: 32 | 33 | - Image 1 is the base Windows setup media, which serves as the base of the installation tree 34 | - Image 2 is Windows PE, exported to sources/boot.wim 35 | - Image 3 is Windows Setup, appended to sources/boot.wim, and set bootable 36 | - All remaining images are Windows editions, which will be exported into sources/install.esd 37 | 38 | These steps are performed into a temporary directory. When finished, [hdiutil](https://ss64.com/osx/hdiutil.html) is used to create the ISO image from this temporary tree. 39 | 40 | If the script exits for any reason—successful or otherwise—the temporary directory is cleaned. 41 | 42 | ## Downloading ESDs 43 | 44 | You can use [download-windows-esd](https://github.com/mattieb/download-windows-esd) to get the Windows 11 ESD catalog from Microsoft, then download any ESD you wish that is referenced in that catalog and verify its checksum. 45 | 46 | ## Converting to USB drives 47 | 48 | You can convert an ISO produced with this tool to a USB drive with [windows-iso-to-usb](https://github.com/mattieb/windows-iso-to-usb). 49 | 50 | ## Licensing 51 | 52 | While this tool itself is [already licensed to you](./LICENSE.md), Windows itself may require licensing and activation. 53 | 54 | You must address this; this tool does not. 55 | -------------------------------------------------------------------------------- /windows-esd-to-iso: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Copyright (c) 2023, 2024, 2025 Mattie Behrens. 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining 6 | # a copy of this software and associated documentation files (the 7 | # "Software"), to deal in the Software without restriction, including 8 | # without limitation the rights to use, copy, modify, merge, publish, 9 | # distribute, sublicense, and/or sell copies of the Software, and to 10 | # permit persons to whom the Software is furnished to do so, subject 11 | # to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be 14 | # included in all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR 20 | # ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 21 | # CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | # 24 | 25 | # MARK: color support 26 | # 27 | 28 | dye_detect() { 29 | command -v tput >/dev/null || return 1 30 | test -n "${NO_COLOR-}" && return 1 31 | test -n "${CLICOLOR_FORCE-}" && return 32 | test -t 1 || return 1 33 | test -n "${CLICOLOR-}" && return 34 | test "${1-}" = "default-off" && return 1 35 | return 0 36 | } 37 | 38 | dye_out() ( 39 | _1="$1" 40 | _2="${2-}" 41 | test -n "$1" && shift 42 | test -n "${1-}" && shift 43 | if [ -z "${DYE_COLORS-}" ]; then 44 | printf "%s" "$*" 45 | return 46 | fi 47 | if [ -z "${_2}" ] || [ -z "${1-}" ]; then 48 | eval "tput ${_1}" || true 49 | return 50 | fi 51 | eval "tput ${_1}" || true 52 | printf "%s" "$*" 53 | eval "tput ${_2}" || true 54 | ) 55 | 56 | dye_color() ( 57 | case "$1" in 58 | black) echo 0 ;; 59 | red) echo 1 ;; 60 | green) echo 2 ;; 61 | yellow) echo 3 ;; 62 | blue) echo 4 ;; 63 | magenta) echo 5 ;; 64 | cyan) echo 6 ;; 65 | white | brightgray) echo 7 ;; 66 | gray) echo 8 ;; 67 | bright*) 68 | base="$(dye_color "${1##*bright}")" 69 | echo "$((8 + base))" 70 | ;; 71 | '' | *[!0-9]*) return 1 ;; 72 | *) echo "$1" ;; 73 | esac 74 | ) 75 | 76 | dye_synth() { 77 | if [ "${DYE_COLORS}" = "8" ] && [ "$1" -ge 8 ] && [ "$1" -le 15 ]; then 78 | echo $(($1 - 8)) 79 | return 1 80 | fi 81 | echo "$1" 82 | } 83 | 84 | dye() { 85 | if [ "$1" = "setup" ]; then 86 | shift 87 | dye_detect "$@" && DYE_COLORS="$(tput colors)" 88 | return 0 89 | fi 90 | 91 | ( 92 | _1="$1" 93 | shift 94 | case "${_1}" in 95 | fg) 96 | c="$(dye_color "$1")" || return 97 | c="$(dye_synth "${c}")" || dye_out bold 98 | shift 99 | dye_out "setaf ${c}" "sgr0" "$@" 100 | ;; 101 | bg) 102 | c="$(dye_color "$1")" || return 103 | c="$(dye_synth "${c}")" || true 104 | shift 105 | dye_out "setab ${c}" "sgr0" "$@" 106 | ;; 107 | dim) dye_out "dim" "sgr0" "$@" ;; 108 | bold) dye_out "bold" "sgr0" "$@" ;; 109 | reverse) dye_out "rev" "sgr0" "$@" ;; 110 | reset) dye_out "sgr0" ;; 111 | i | italic) dye_out "sitm" "ritm" "$@" ;; 112 | so | standout) dye_out "smso" "rmso" "$@" ;; 113 | u | ul | underline) dye_out "smul" "rmul" "$@" ;; 114 | begin) dye "$@" ;; 115 | end) 116 | case "$1" in 117 | i | italic) dye_out "ritm" ;; 118 | so | standout) dye_out "rmso" ;; 119 | u | ul | underline) dye_out "rmul" ;; 120 | *) return 1 ;; 121 | esac 122 | ;; 123 | *) 124 | dye fg "${_1}" "$@" 125 | ;; 126 | esac 127 | ) 128 | } 129 | 130 | # MARK: user interface 131 | 132 | print_task() { echo "$(dye green "==>") $(dye bold "$*")"; } 133 | print_subtask() { echo "$(dye blue "==>") $(dye bold "$*")"; } 134 | print_error() { echo "$(dye red "Error:") $*"; } 135 | print_warning() { echo "$(dye yellow "Warning:") $*"; } 136 | 137 | set -eu 138 | 139 | dye setup 140 | 141 | # MARK: preflight checks 142 | 143 | script="$(basename "$0")" 144 | 145 | if [ "$(uname)" != "Darwin" ]; then 146 | print_warning "$(dye bold "${script}") is currently for macOS and may not work here" 147 | fi 148 | 149 | if [ -z "${1-}" ] || [ "$1" = "-h" ]; then 150 | echo "$(dye red "Usage:") ${script} ESD" 151 | exit 0 152 | fi 153 | 154 | check_wimlib_tool() { 155 | if ! which "$1" >/dev/null; then 156 | print_error "wimlib tools not found" 157 | exit 1 158 | fi 159 | } 160 | check_wimlib_tool wiminfo 161 | check_wimlib_tool wimapply 162 | check_wimlib_tool wimexport 163 | 164 | if ! which hdiutil >/dev/null; then 165 | print_error "hdiutil (a standard macOS tool) not found" 166 | exit 1 167 | fi 168 | 169 | esd="$1" 170 | 171 | if [ ! -f "${esd}" ]; then 172 | print_error "$(dye green "${esd}") not found" 173 | exit 2 174 | fi 175 | 176 | # MARK: cleanup exit hook 177 | 178 | tmpdir=$(mktemp -d) 179 | 180 | __cleanup() { 181 | if [ -z "${NO_CLEANUP-}" ]; then 182 | print_task "Cleaning up $(dye green "${tmpdir}")" 183 | rm -rf "${tmpdir}" 184 | else 185 | print_warning "NO_CLEANUP was set; $(dye green "${tmpdir}") will not be removed" 186 | fi 187 | } 188 | trap __cleanup EXIT 189 | 190 | # MARK: export images 191 | 192 | print_task "Exporting images from $(dye green "${esd}")" 193 | 194 | image_count=$(wiminfo "${esd}" --header | grep '^Image Count' | cut -d= -f 2) 195 | if ! [ "${image_count}" -gt 0 ] 2>/dev/null; then 196 | print_error "could not get image count" 197 | exit 3 198 | fi 199 | echo "Found ${image_count} images." 200 | 201 | print_subtask "Exporting image 1" 202 | wimapply "${esd}" 1 "${tmpdir}" 203 | 204 | print_subtask "Exporting image 2" 205 | wimexport "${esd}" 2 \ 206 | "${tmpdir}/sources/boot.wim" \ 207 | --compress=LZX --chunk-size=32K 208 | 209 | print_subtask "Exporting image 3" 210 | wimexport "${esd}" 3 \ 211 | "${tmpdir}/sources/boot.wim" \ 212 | --compress=LZX --chunk-size=32K --boot 213 | 214 | for index in $(seq 4 "${image_count}"); do 215 | print_subtask "Exporting image ${index}" 216 | wimexport "${esd}" "${index}" \ 217 | "${tmpdir}/sources/install.esd" \ 218 | --compress=LZMS --chunk-size 128K --recompress 219 | done 220 | 221 | # MARK: create ISO 222 | 223 | basename="$(basename "${esd}" .esd)" 224 | iso="${basename}.iso" 225 | 226 | rm -f "${iso}" 227 | print_task "Creating $(dye green "${iso}")" 228 | hdiutil makehybrid \ 229 | -o "${iso}" \ 230 | -iso -udf \ 231 | -hard-disk-boot -eltorito-boot "${tmpdir}/efi/microsoft/boot/efisys.bin" \ 232 | -iso-volume-name ESD_ISO \ 233 | -udf-volume-name ESD-ISO \ 234 | "${tmpdir}" 235 | --------------------------------------------------------------------------------