install.sh 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110
  1. #!/usr/bin/env bash
  2. set -Eeuo pipefail
  3. TMP="$STORAGE/tmp"
  4. DIR="$TMP/unpack"
  5. FB="falling back to manual installation!"
  6. ETFS="boot/etfsboot.com"
  7. EFISYS="efi/microsoft/boot/efisys_noprompt.bin"
  8. skipInstall() {
  9. local iso="$1"
  10. local method=""
  11. local magic byte
  12. local boot="$STORAGE/windows.boot"
  13. local previous="$STORAGE/windows.base"
  14. if [ -f "$previous" ]; then
  15. previous=$(<"$previous")
  16. previous="${previous//[![:print:]]/}"
  17. if [ -n "$previous" ]; then
  18. if [[ "${STORAGE,,}/${previous,,}" != "${iso,,}" ]]; then
  19. if [ -f "$boot" ] && hasDisk; then
  20. if [[ "${iso,,}" == "${STORAGE,,}/windows."* ]]; then
  21. method="your custom .iso file"
  22. else
  23. if [[ "${previous,,}" != "windows."* ]]; then
  24. method="the VERSION variable"
  25. fi
  26. fi
  27. if [ -n "$method" ]; then
  28. info "Detected that $method was changed, but ignoring this because Windows is already installed."
  29. info "Please start with an empty /storage folder, if you want to install a different version of Windows."
  30. fi
  31. return 0
  32. fi
  33. rm -f "$STORAGE/$previous"
  34. return 1
  35. fi
  36. fi
  37. fi
  38. [ -f "$boot" ] && hasDisk && return 0
  39. [ ! -f "$iso" ] && return 1
  40. [ ! -s "$iso" ] && return 1
  41. # Check if the ISO was already processed by our script
  42. magic=$(dd if="$iso" seek=0 bs=1 count=1 status=none | tr -d '\000')
  43. magic="$(printf '%s' "$magic" | od -A n -t x1 -v | tr -d ' \n')"
  44. byte="16" && [[ "$MANUAL" == [Yy1]* ]] && byte="17"
  45. if [[ "$magic" != "$byte" ]]; then
  46. info "The ISO will be processed again because the configuration was changed..."
  47. return 1
  48. fi
  49. return 0
  50. }
  51. startInstall() {
  52. html "Starting $APP..."
  53. if [ -z "$CUSTOM" ]; then
  54. local file="${VERSION//\//}.iso"
  55. if [[ "${VERSION,,}" == "http"* ]]; then
  56. file=$(basename "${VERSION%%\?*}")
  57. : "${file//+/ }"; printf -v file '%b' "${_//%/\\x}"
  58. file=$(echo "$file" | sed -e 's/[^A-Za-z0-9._-]/_/g')
  59. else
  60. local language
  61. language=$(getLanguage "$LANGUAGE" "culture")
  62. language="${language%%-*}"
  63. if [ -n "$language" ] && [[ "${language,,}" != "en" ]]; then
  64. file="${VERSION//\//}_${language,,}.iso"
  65. fi
  66. fi
  67. BOOT="$STORAGE/$file"
  68. fi
  69. skipInstall "$BOOT" && return 1
  70. rm -rf "$TMP"
  71. mkdir -p "$TMP"
  72. if [ -z "$CUSTOM" ]; then
  73. ISO=$(basename "$BOOT")
  74. ISO="$TMP/$ISO"
  75. if [ -f "$BOOT" ] && [ -s "$BOOT" ]; then
  76. mv -f "$BOOT" "$ISO"
  77. fi
  78. fi
  79. rm -f "$BOOT"
  80. return 0
  81. }
  82. finishInstall() {
  83. local iso="$1"
  84. local aborted="$2"
  85. local base byte
  86. if [ ! -s "$iso" ] || [ ! -f "$iso" ]; then
  87. error "Failed to find ISO file: $iso" && return 1
  88. fi
  89. if [[ "$aborted" != [Yy1]* ]]; then
  90. # Mark ISO as prepared via magic byte
  91. byte="16" && [[ "$MANUAL" == [Yy1]* ]] && byte="17"
  92. if ! printf '%b' "\x$byte" | dd of="$iso" bs=1 seek=0 count=1 conv=notrunc status=none; then
  93. warn "failed to set magic byte in ISO file: $iso"
  94. fi
  95. fi
  96. rm -f "$STORAGE/windows.old"
  97. rm -f "$STORAGE/windows.vga"
  98. rm -f "$STORAGE/windows.args"
  99. rm -f "$STORAGE/windows.base"
  100. rm -f "$STORAGE/windows.boot"
  101. rm -f "$STORAGE/windows.mode"
  102. rm -f "$STORAGE/windows.type"
  103. cp -f /run/version "$STORAGE/windows.ver"
  104. if [[ "$iso" == "$STORAGE/"* ]]; then
  105. if [[ "$aborted" != [Yy1]* ]] || [ -z "$CUSTOM" ]; then
  106. base=$(basename "$iso")
  107. echo "$base" > "$STORAGE/windows.base"
  108. fi
  109. fi
  110. if [[ "${PLATFORM,,}" == "x64" ]]; then
  111. if [[ "${BOOT_MODE,,}" == "windows_legacy" ]]; then
  112. echo "$BOOT_MODE" > "$STORAGE/windows.mode"
  113. if [[ "${MACHINE,,}" != "q35" ]]; then
  114. echo "$MACHINE" > "$STORAGE/windows.old"
  115. fi
  116. else
  117. # Enable secure boot + TPM on manual installs as Win11 requires
  118. if [[ "$MANUAL" == [Yy1]* ]] || [[ "$aborted" == [Yy1]* ]]; then
  119. if [[ "${DETECTED,,}" == "win11"* ]]; then
  120. BOOT_MODE="windows_secure"
  121. echo "$BOOT_MODE" > "$STORAGE/windows.mode"
  122. fi
  123. fi
  124. # Enable secure boot on multi-socket systems to workaround freeze
  125. if [ -n "$SOCKETS" ] && [[ "$SOCKETS" != "1" ]]; then
  126. BOOT_MODE="windows_secure"
  127. echo "$BOOT_MODE" > "$STORAGE/windows.mode"
  128. fi
  129. fi
  130. fi
  131. if [ -n "${ARGS:-}" ]; then
  132. ARGUMENTS="$ARGS ${ARGUMENTS:-}"
  133. echo "$ARGS" > "$STORAGE/windows.args"
  134. fi
  135. if [ -n "${DISK_TYPE:-}" ] && [[ "${DISK_TYPE:-}" != "scsi" ]]; then
  136. echo "$DISK_TYPE" > "$STORAGE/windows.type"
  137. fi
  138. rm -rf "$TMP"
  139. return 0
  140. }
  141. abortInstall() {
  142. local dir="$1"
  143. local iso="$2"
  144. local efi
  145. [[ "${iso,,}" == *".esd" ]] && exit 60
  146. efi=$(find "$dir" -maxdepth 1 -type d -iname efi | head -n 1)
  147. if [ -z "$efi" ]; then
  148. [[ "${PLATFORM,,}" == "x64" ]] && BOOT_MODE="windows_legacy"
  149. fi
  150. if [ -n "$CUSTOM" ]; then
  151. BOOT="$iso"
  152. REMOVE="N"
  153. else
  154. if [[ "$iso" != "$BOOT" ]]; then
  155. if ! mv -f "$iso" "$BOOT"; then
  156. error "Failed to move ISO file: $iso" && return 1
  157. fi
  158. fi
  159. fi
  160. finishInstall "$BOOT" "Y" && return 0
  161. return 1
  162. }
  163. detectCustom() {
  164. local dir file base
  165. local fname="custom.iso"
  166. local boot="$STORAGE/windows.boot"
  167. CUSTOM=""
  168. dir=$(find / -maxdepth 1 -type d -iname "$fname" | head -n 1)
  169. [ ! -d "$dir" ] && dir=$(find "$STORAGE" -maxdepth 1 -type d -iname "$fname" | head -n 1)
  170. if [ -d "$dir" ]; then
  171. if ! hasDisk || [ ! -f "$boot" ]; then
  172. error "The bind $dir maps to a file that does not exist!" && return 1
  173. fi
  174. fi
  175. file=$(find / -maxdepth 1 -type f -iname "$fname" | head -n 1)
  176. [ ! -s "$file" ] && file=$(find "$STORAGE" -maxdepth 1 -type f -iname "$fname" | head -n 1)
  177. if [ ! -s "$file" ] && [[ "${VERSION,,}" != "http"* ]]; then
  178. base=$(basename "$VERSION")
  179. file="$STORAGE/$base"
  180. fi
  181. if [ ! -f "$file" ] || [ ! -s "$file" ]; then
  182. return 0
  183. fi
  184. local size
  185. size="$(stat -c%s "$file")"
  186. [ -z "$size" ] || [[ "$size" == "0" ]] && return 0
  187. ISO="$file"
  188. CUSTOM="$ISO"
  189. BOOT="$STORAGE/windows.$size.iso"
  190. return 0
  191. }
  192. extractESD() {
  193. local iso="$1"
  194. local dir="$2"
  195. local version="$3"
  196. local desc="$4"
  197. local size size_gb space space_gb desc
  198. local msg="Extracting $desc bootdisk..."
  199. info "$msg" && html "$msg"
  200. if [ "$(stat -c%s "$iso")" -lt 100000000 ]; then
  201. error "Invalid ESD file: Size is smaller than 100 MB" && return 1
  202. fi
  203. rm -rf "$dir"
  204. mkdir -p "$dir"
  205. size=16106127360
  206. size_gb=$(formatBytes "$size")
  207. space=$(df --output=avail -B 1 "$dir" | tail -n 1)
  208. space_gb=$(formatBytes "$space")
  209. if (( size > space )); then
  210. error "Not enough free space in $STORAGE, have $space_gb available but need at least $size_gb." && return 1
  211. fi
  212. local esdImageCount
  213. esdImageCount=$(wimlib-imagex info "$iso" | awk '/Image Count:/ {print $3}')
  214. wimlib-imagex apply "$iso" 1 "$dir" --quiet 2>/dev/null || {
  215. retVal=$?
  216. error "Extracting $desc bootdisk failed" && return $retVal
  217. }
  218. local bootWimFile="$dir/sources/boot.wim"
  219. local installWimFile="$dir/sources/install.wim"
  220. local msg="Extracting $desc environment..."
  221. info "$msg" && html "$msg"
  222. wimlib-imagex export "$iso" 2 "$bootWimFile" --compress=none --quiet || {
  223. retVal=$?
  224. error "Adding WinPE failed" && return ${retVal}
  225. }
  226. local msg="Extracting $desc setup..."
  227. info "$msg" && html "$msg"
  228. wimlib-imagex export "$iso" 3 "$bootWimFile" --compress=none --boot --quiet || {
  229. retVal=$?
  230. error "Adding Windows Setup failed" && return ${retVal}
  231. }
  232. if [[ "${PLATFORM,,}" == "x64" ]]; then
  233. LABEL="CCCOMA_X64FRE_EN-US_DV9"
  234. else
  235. LABEL="CPBA_A64FRE_EN-US_DV9"
  236. fi
  237. local msg="Extracting $desc image..."
  238. info "$msg" && html "$msg"
  239. local edition imageIndex imageEdition
  240. edition=$(getCatalog "$version" "name")
  241. if [ -z "$edition" ]; then
  242. error "Invalid VERSION specified, value \"$version\" is not recognized!" && return 1
  243. fi
  244. for (( imageIndex=4; imageIndex<=esdImageCount; imageIndex++ )); do
  245. imageEdition=$(wimlib-imagex info "$iso" ${imageIndex} | grep '^Description:' | sed 's/Description:[ \t]*//')
  246. [[ "${imageEdition,,}" != "${edition,,}" ]] && continue
  247. wimlib-imagex export "$iso" ${imageIndex} "$installWimFile" --compress=LZMS --chunk-size 128K --quiet || {
  248. retVal=$?
  249. error "Addition of $imageIndex to the $desc image failed" && return $retVal
  250. }
  251. return 0
  252. done
  253. error "Failed to find product '$edition' in install.wim!" && return 1
  254. }
  255. extractImage() {
  256. local iso="$1"
  257. local dir="$2"
  258. local version="$3"
  259. local desc="local ISO"
  260. local size size_gb space space_gb
  261. if [ -z "$CUSTOM" ]; then
  262. desc="downloaded ISO"
  263. if [[ "$version" != "http"* ]]; then
  264. desc=$(printVersion "$version" "$desc")
  265. fi
  266. fi
  267. if [[ "${iso,,}" == *".esd" ]]; then
  268. extractESD "$iso" "$dir" "$version" "$desc" && return 0
  269. return 1
  270. fi
  271. local msg="Extracting $desc image..."
  272. info "$msg" && html "$msg"
  273. rm -rf "$dir"
  274. mkdir -p "$dir"
  275. size=$(stat -c%s "$iso")
  276. size_gb=$(formatBytes "$size")
  277. space=$(df --output=avail -B 1 "$dir" | tail -n 1)
  278. space_gb=$(formatBytes "$space")
  279. if ((size<100000000)); then
  280. error "Invalid ISO file: Size is smaller than 100 MB" && return 1
  281. fi
  282. if (( size > space )); then
  283. error "Not enough free space in $STORAGE, have $space_gb available but need at least $size_gb." && return 1
  284. fi
  285. rm -rf "$dir"
  286. if ! 7z x "$iso" -o"$dir" > /dev/null; then
  287. error "Failed to extract ISO file: $iso" && return 1
  288. fi
  289. LABEL=$(isoinfo -d -i "$iso" | sed -n 's/Volume id: //p')
  290. return 0
  291. }
  292. getPlatform() {
  293. local xml="$1"
  294. local tag="ARCH"
  295. local platform="x64"
  296. local arch
  297. arch=$(sed -n "/$tag/{s/.*<$tag>\(.*\)<\/$tag>.*/\1/;p}" <<< "$xml")
  298. case "${arch,,}" in
  299. "0" ) platform="x86" ;;
  300. "9" ) platform="x64" ;;
  301. "12" )platform="arm64" ;;
  302. esac
  303. echo "$platform"
  304. return 0
  305. }
  306. checkPlatform() {
  307. local xml="$1"
  308. local platform compat
  309. platform=$(getPlatform "$xml")
  310. case "${platform,,}" in
  311. "x86" ) compat="x64" ;;
  312. "x64" ) compat="$platform" ;;
  313. "arm64" ) compat="$platform" ;;
  314. * ) compat="${PLATFORM,,}" ;;
  315. esac
  316. [[ "${compat,,}" == "${PLATFORM,,}" ]] && return 0
  317. error "You cannot boot ${platform^^} images on a $PLATFORM CPU!"
  318. return 1
  319. }
  320. hasVersion() {
  321. local id="$1"
  322. local tag="$2"
  323. local xml="$3"
  324. local edition
  325. [ ! -f "/run/assets/$id.xml" ] && return 1
  326. edition=$(printEdition "$id" "")
  327. [ -z "$edition" ] && return 1
  328. [[ "${xml,,}" != *"<${tag,,}>${edition,,}</${tag,,}>"* ]] && return 1
  329. return 0
  330. }
  331. selectVersion() {
  332. local tag="$1"
  333. local xml="$2"
  334. local platform="$3"
  335. local id name prefer
  336. name=$(sed -n "/$tag/{s/.*<$tag>\(.*\)<\/$tag>.*/\1/;p}" <<< "$xml")
  337. [[ "$name" == *"Operating System"* ]] && name=""
  338. [ -z "$name" ] && return 0
  339. id=$(fromName "$name" "$platform")
  340. [ -z "$id" ] && warn "Unknown ${tag,,}: '$name'" && return 0
  341. prefer="$id-enterprise"
  342. hasVersion "$prefer" "$tag" "$xml" && echo "$prefer" && return 0
  343. prefer="$id-ultimate"
  344. hasVersion "$prefer" "$tag" "$xml" && echo "$prefer" && return 0
  345. prefer="$id"
  346. hasVersion "$prefer" "$tag" "$xml" && echo "$prefer" && return 0
  347. prefer=$(getVersion "$name" "$platform")
  348. echo "$prefer"
  349. return 0
  350. }
  351. detectVersion() {
  352. local xml="$1"
  353. local id platform
  354. platform=$(getPlatform "$xml")
  355. id=$(selectVersion "DISPLAYNAME" "$xml" "$platform")
  356. [ -z "$id" ] && id=$(selectVersion "PRODUCTNAME" "$xml" "$platform")
  357. [ -z "$id" ] && id=$(selectVersion "NAME" "$xml" "$platform")
  358. echo "$id"
  359. return 0
  360. }
  361. detectLanguage() {
  362. local xml="$1"
  363. local lang=""
  364. if [[ "$xml" == *"LANGUAGE><DEFAULT>"* ]]; then
  365. lang="${xml#*LANGUAGE><DEFAULT>}"
  366. lang="${lang%%<*}"
  367. else
  368. if [[ "$xml" == *"FALLBACK><DEFAULT>"* ]]; then
  369. lang="${xml#*FALLBACK><DEFAULT>}"
  370. lang="${lang%%<*}"
  371. fi
  372. fi
  373. if [ -z "$lang" ]; then
  374. warn "Language could not be detected from ISO!" && return 0
  375. fi
  376. local culture
  377. culture=$(getLanguage "$lang" "culture")
  378. [ -n "$culture" ] && LANGUAGE="$lang" && return 0
  379. warn "Invalid language detected: \"$lang\""
  380. return 0
  381. }
  382. setXML() {
  383. local file="/custom.xml"
  384. if [ -d "$file" ]; then
  385. error "The bind $file maps to a file that does not exist!" && exit 67
  386. fi
  387. [ ! -f "$file" ] || [ ! -s "$file" ] && file="$STORAGE/custom.xml"
  388. [ ! -f "$file" ] || [ ! -s "$file" ] && file="/run/assets/custom.xml"
  389. [ ! -f "$file" ] || [ ! -s "$file" ] && file="$1"
  390. [ ! -f "$file" ] || [ ! -s "$file" ] && file="/run/assets/$DETECTED.xml"
  391. [ ! -f "$file" ] || [ ! -s "$file" ] && return 1
  392. XML="$file"
  393. return 0
  394. }
  395. detectImage() {
  396. local dir="$1"
  397. local version="$2"
  398. local desc msg find language
  399. XML=""
  400. if [ -z "$DETECTED" ] && [ -z "$CUSTOM" ]; then
  401. [[ "${version,,}" != "http"* ]] && DETECTED="$version"
  402. fi
  403. if [ -n "$DETECTED" ]; then
  404. skipVersion "${DETECTED,,}" && return 0
  405. if ! setXML "" && [[ "$MANUAL" != [Yy1]* ]]; then
  406. MANUAL="Y"
  407. desc=$(printEdition "$DETECTED" "this version")
  408. warn "the answer file for $desc was not found ($DETECTED.xml), $FB."
  409. fi
  410. return 0
  411. fi
  412. info "Detecting version from ISO image..."
  413. if detectLegacy "$dir"; then
  414. desc=$(printEdition "$DETECTED" "$DETECTED")
  415. info "Detected: $desc"
  416. return 0
  417. fi
  418. local src wim info
  419. src=$(find "$dir" -maxdepth 1 -type d -iname sources | head -n 1)
  420. if [ ! -d "$src" ]; then
  421. warn "failed to locate 'sources' folder in ISO image, $FB" && return 1
  422. fi
  423. wim=$(find "$src" -maxdepth 1 -type f -iname install.wim | head -n 1)
  424. [ ! -f "$wim" ] && wim=$(find "$src" -maxdepth 1 -type f -iname install.esd | head -n 1)
  425. if [ ! -f "$wim" ]; then
  426. warn "failed to locate 'install.wim' or 'install.esd' in ISO image, $FB" && return 1
  427. fi
  428. info=$(wimlib-imagex info -xml "$wim" | tr -d '\000')
  429. checkPlatform "$info" || exit 67
  430. DETECTED=$(detectVersion "$info")
  431. if [ -z "$DETECTED" ]; then
  432. msg="Failed to determine Windows version from image"
  433. if setXML "" || [[ "$MANUAL" == [Yy1]* ]]; then
  434. info "${msg}!"
  435. else
  436. MANUAL="Y"
  437. warn "${msg}, $FB."
  438. fi
  439. return 0
  440. fi
  441. desc=$(printEdition "$DETECTED" "$DETECTED")
  442. detectLanguage "$info"
  443. if [[ "${LANGUAGE,,}" != "en" ]] && [[ "${LANGUAGE,,}" != "en-"* ]]; then
  444. language=$(getLanguage "$LANGUAGE" "desc")
  445. desc+=" ($language)"
  446. fi
  447. info "Detected: $desc"
  448. setXML "" && return 0
  449. if [[ "$DETECTED" == "win81x86"* ]] || [[ "$DETECTED" == "win10x86"* ]]; then
  450. error "The 32-bit version of $desc is not supported!" && return 1
  451. fi
  452. msg="the answer file for $desc was not found ($DETECTED.xml)"
  453. local fallback="/run/assets/${DETECTED%%-*}.xml"
  454. if setXML "$fallback" || [[ "$MANUAL" == [Yy1]* ]]; then
  455. [[ "$MANUAL" != [Yy1]* ]] && warn "${msg}."
  456. else
  457. MANUAL="Y"
  458. warn "${msg}, $FB."
  459. fi
  460. return 0
  461. }
  462. prepareImage() {
  463. local iso="$1"
  464. local dir="$2"
  465. local desc missing
  466. desc=$(printVersion "$DETECTED" "$DETECTED")
  467. setMachine "$DETECTED" "$iso" "$dir" "$desc" || return 1
  468. skipVersion "$DETECTED" && return 0
  469. if [[ "${BOOT_MODE,,}" != "windows_legacy" ]]; then
  470. [ -f "$dir/$ETFS" ] && [ -f "$dir/$EFISYS" ] && return 0
  471. missing=$(basename "$dir/$EFISYS")
  472. [ ! -f "$dir/$ETFS" ] && missing=$(basename "$dir/$ETFS")
  473. error "Failed to locate file \"${missing,,}\" in ISO image!"
  474. return 1
  475. fi
  476. prepareLegacy "$iso" "$dir" "$desc" && return 0
  477. error "Failed to extract boot image from ISO image!"
  478. return 1
  479. }
  480. updateXML() {
  481. local asset="$1"
  482. local language="$2"
  483. local culture region user admin pass keyboard
  484. if [ -n "${VM_NET_IP:-}" ]; then
  485. sed -i "s/ 20.20.20.1 / ${VM_NET_IP%.*}.1 /g" "$asset"
  486. fi
  487. [ -z "$HEIGHT" ] && HEIGHT="720"
  488. [ -z "$WIDTH" ] && WIDTH="1280"
  489. sed -i "s/<VerticalResolution>1080<\/VerticalResolution>/<VerticalResolution>$HEIGHT<\/VerticalResolution>/g" "$asset"
  490. sed -i "s/<HorizontalResolution>1920<\/HorizontalResolution>/<HorizontalResolution>$WIDTH<\/HorizontalResolution>/g" "$asset"
  491. culture=$(getLanguage "$language" "culture")
  492. if [ -n "$culture" ] && [[ "${culture,,}" != "en-us" ]]; then
  493. sed -i "s/<UILanguage>en-US<\/UILanguage>/<UILanguage>$culture<\/UILanguage>/g" "$asset"
  494. fi
  495. region="$REGION"
  496. [ -z "$region" ] && region="$culture"
  497. if [ -n "$region" ] && [[ "${region,,}" != "en-us" ]]; then
  498. sed -i "s/<UserLocale>en-US<\/UserLocale>/<UserLocale>$region<\/UserLocale>/g" "$asset"
  499. sed -i "s/<SystemLocale>en-US<\/SystemLocale>/<SystemLocale>$region<\/SystemLocale>/g" "$asset"
  500. fi
  501. keyboard="$KEYBOARD"
  502. [ -z "$keyboard" ] && keyboard="$culture"
  503. if [ -n "$keyboard" ] && [[ "${keyboard,,}" != "en-us" ]]; then
  504. sed -i "s/<InputLocale>en-US<\/InputLocale>/<InputLocale>$keyboard<\/InputLocale>/g" "$asset"
  505. sed -i "s/<InputLocale>0409:00000409<\/InputLocale>/<InputLocale>$keyboard<\/InputLocale>/g" "$asset"
  506. fi
  507. user=$(echo "$USERNAME" | sed 's/[^[:alnum:]@!._-]//g')
  508. if [ -n "$user" ]; then
  509. sed -i "s/<Name>Docker<\/Name>/<Name>$user<\/Name>/g" "$asset"
  510. sed -i "s/where name=\"Docker\"/where name=\"$user\"/g" "$asset"
  511. sed -i "s/<FullName>Docker<\/FullName>/<FullName>$user<\/FullName>/g" "$asset"
  512. sed -i "s/<Username>Docker<\/Username>/<Username>$user<\/Username>/g" "$asset"
  513. fi
  514. if [ -n "$PASSWORD" ]; then
  515. pass=$(printf '%s' "${PASSWORD}Password" | iconv -f utf-8 -t utf-16le | base64 -w 0)
  516. admin=$(printf '%s' "${PASSWORD}AdministratorPassword" | iconv -f utf-8 -t utf-16le | base64 -w 0)
  517. sed -i "s/<Value>password<\/Value>/<Value>$admin<\/Value>/g" "$asset"
  518. sed -i "s/<PlainText>true<\/PlainText>/<PlainText>false<\/PlainText>/g" "$asset"
  519. sed -z "s/<Password>...........<Value \/>/<Password>\n <Value>$pass<\/Value>/g" -i "$asset"
  520. sed -z "s/<Password>...............<Value \/>/<Password>\n <Value>$pass<\/Value>/g" -i "$asset"
  521. sed -z "s/<AdministratorPassword>...........<Value \/>/<AdministratorPassword>\n <Value>$admin<\/Value>/g" -i "$asset"
  522. sed -z "s/<AdministratorPassword>...............<Value \/>/<AdministratorPassword>\n <Value>$admin<\/Value>/g" -i "$asset"
  523. fi
  524. if [ -n "$EDITION" ]; then
  525. [[ "${EDITION^^}" == "CORE" ]] && EDITION="STANDARDCORE"
  526. sed -i "s/SERVERSTANDARD<\/Value>/SERVER${EDITION^^}<\/Value>/g" "$asset"
  527. fi
  528. if [ -n "$KEY" ]; then
  529. sed -i '/<ProductKey>/,/<\/ProductKey>/d' "$asset"
  530. sed -i "s/<\/UserData>/ <ProductKey>\n <Key>${KEY}<\/Key>\n <WillShowUI>OnError<\/WillShowUI>\n <\/ProductKey>\n <\/UserData>/g" "$asset"
  531. fi
  532. return 0
  533. }
  534. addDriver() {
  535. local id="$1"
  536. local path="$2"
  537. local target="$3"
  538. local driver="$4"
  539. local desc=""
  540. local folder=""
  541. if [ -z "$id" ]; then
  542. warn "no Windows version specified for \"$driver\" driver!" && return 0
  543. fi
  544. case "${id,,}" in
  545. "win7x86"* ) folder="w7/x86" ;;
  546. "win7x64"* ) folder="w7/amd64" ;;
  547. "win81x64"* ) folder="w8.1/amd64" ;;
  548. "win10x64"* ) folder="w10/amd64" ;;
  549. "win11x64"* ) folder="w11/amd64" ;;
  550. "win2025"* ) folder="2k25/amd64" ;;
  551. "win2022"* ) folder="2k22/amd64" ;;
  552. "win2019"* ) folder="2k19/amd64" ;;
  553. "win2016"* ) folder="2k16/amd64" ;;
  554. "win2012"* ) folder="2k12R2/amd64" ;;
  555. "win2008"* ) folder="2k8R2/amd64" ;;
  556. "win10arm64"* ) folder="w10/ARM64" ;;
  557. "win11arm64"* ) folder="w11/ARM64" ;;
  558. "winvistax86"* ) folder="2k8/x86" ;;
  559. "winvistax64"* ) folder="2k8/amd64" ;;
  560. esac
  561. if [ -z "$folder" ]; then
  562. desc=$(printVersion "$id" "$id")
  563. if [[ "${id,,}" != *"x86"* ]]; then
  564. warn "no \"$driver\" driver available for \"$desc\" !" && return 0
  565. else
  566. warn "no \"$driver\" driver available for the 32-bit version of \"$desc\" !" && return 0
  567. fi
  568. fi
  569. [ ! -d "$path/$driver/$folder" ] && return 0
  570. case "${id,,}" in
  571. "winvista"* )
  572. [[ "${driver,,}" == "viorng" ]] && return 0
  573. ;;
  574. esac
  575. local dest="$path/$target/$driver"
  576. mkdir -p "$dest" || return 1
  577. cp -Lr "$path/$driver/$folder/." "$dest" || return 1
  578. return 0
  579. }
  580. addDrivers() {
  581. local src="$1"
  582. local tmp="$2"
  583. local file="$3"
  584. local index="$4"
  585. local version="$5"
  586. local drivers="$tmp/drivers"
  587. rm -rf "$drivers"
  588. mkdir -p "$drivers"
  589. local msg="Adding drivers to image..."
  590. info "$msg" && html "$msg"
  591. if [ -z "$version" ]; then
  592. version="win11x64"
  593. warn "Windows version unknown, falling back to Windows 11 drivers..."
  594. fi
  595. if ! bsdtar -xf /drivers.txz -C "$drivers"; then
  596. error "Failed to extract drivers from archive!" && return 1
  597. fi
  598. local target="\$WinPEDriver\$"
  599. local dest="$drivers/$target"
  600. mkdir -p "$dest" || return 1
  601. wimlib-imagex update "$file" "$index" --command "delete --force --recursive /$target" >/dev/null || true
  602. addDriver "$version" "$drivers" "$target" "qxl" || return 1
  603. addDriver "$version" "$drivers" "$target" "viofs" || return 1
  604. addDriver "$version" "$drivers" "$target" "sriov" || return 1
  605. addDriver "$version" "$drivers" "$target" "smbus" || return 1
  606. addDriver "$version" "$drivers" "$target" "qxldod" || return 1
  607. addDriver "$version" "$drivers" "$target" "viorng" || return 1
  608. addDriver "$version" "$drivers" "$target" "viostor" || return 1
  609. addDriver "$version" "$drivers" "$target" "viomem" || return 1
  610. addDriver "$version" "$drivers" "$target" "NetKVM" || return 1
  611. addDriver "$version" "$drivers" "$target" "Balloon" || return 1
  612. addDriver "$version" "$drivers" "$target" "vioscsi" || return 1
  613. addDriver "$version" "$drivers" "$target" "pvpanic" || return 1
  614. addDriver "$version" "$drivers" "$target" "vioinput" || return 1
  615. addDriver "$version" "$drivers" "$target" "viogpudo" || return 1
  616. addDriver "$version" "$drivers" "$target" "vioserial" || return 1
  617. addDriver "$version" "$drivers" "$target" "qemupciserial" || return 1
  618. case "${version,,}" in
  619. "win11x64"* | "win2025"* )
  620. # Workaround Virtio GPU driver bug
  621. local dst="$src/\$OEM\$/\$\$/Drivers"
  622. mkdir -p "$dst" || return 1
  623. cp -Lr "$dest/." "$dst" || return 1
  624. rm -rf "$dest/viogpudo"
  625. ;;
  626. esac
  627. if ! wimlib-imagex update "$file" "$index" --command "add $dest /$target" >/dev/null; then
  628. return 1
  629. fi
  630. rm -rf "$drivers"
  631. return 0
  632. }
  633. updateImage() {
  634. local dir="$1"
  635. local asset="$2"
  636. local language="$3"
  637. local tmp="/tmp/install"
  638. local file="autounattend.xml"
  639. local org="${file//.xml/.org}"
  640. local dat="${file//.xml/.dat}"
  641. local desc path src wim xml index result
  642. skipVersion "${DETECTED,,}" && return 0
  643. if [ ! -s "$asset" ] || [ ! -f "$asset" ]; then
  644. asset=""
  645. if [[ "$MANUAL" != [Yy1]* ]]; then
  646. MANUAL="Y"
  647. warn "no answer file provided, $FB."
  648. fi
  649. fi
  650. rm -rf "$tmp"
  651. mkdir -p "$tmp"
  652. src=$(find "$dir" -maxdepth 1 -type d -iname sources | head -n 1)
  653. if [ ! -d "$src" ]; then
  654. error "failed to locate 'sources' folder in ISO image, $FB" && return 1
  655. fi
  656. wim=$(find "$src" -maxdepth 1 -type f -iname boot.wim | head -n 1)
  657. [ ! -f "$wim" ] && wim=$(find "$src" -maxdepth 1 -type f -iname boot.esd | head -n 1)
  658. if [ ! -f "$wim" ]; then
  659. error "failed to locate 'boot.wim' or 'boot.esd' in ISO image, $FB" && return 1
  660. fi
  661. index="1"
  662. result=$(wimlib-imagex info -xml "$wim" | tr -d '\000')
  663. if [[ "${result^^}" == *"<IMAGE INDEX=\"2\">"* ]]; then
  664. index="2"
  665. fi
  666. if ! addDrivers "$src" "$tmp" "$wim" "$index" "$DETECTED"; then
  667. error "Failed to add drivers to image!"
  668. fi
  669. if ! addFolder "$src"; then
  670. error "Failed to add OEM folder to image!"
  671. fi
  672. if wimlib-imagex extract "$wim" "$index" "/$file" "--dest-dir=$tmp" >/dev/null 2>&1; then
  673. if ! wimlib-imagex extract "$wim" "$index" "/$dat" "--dest-dir=$tmp" >/dev/null 2>&1; then
  674. if ! wimlib-imagex extract "$wim" "$index" "/$org" "--dest-dir=$tmp" >/dev/null 2>&1; then
  675. if ! wimlib-imagex update "$wim" "$index" --command "rename /$file /$org" > /dev/null; then
  676. warn "failed to backup original answer file ($file)."
  677. fi
  678. fi
  679. fi
  680. fi
  681. if [[ "$MANUAL" != [Yy1]* ]]; then
  682. xml=$(basename "$asset")
  683. info "Adding $xml for automatic installation..."
  684. local answer="$tmp/$xml"
  685. cp "$asset" "$answer"
  686. updateXML "$answer" "$language"
  687. if ! wimlib-imagex update "$wim" "$index" --command "add $answer /$file" > /dev/null; then
  688. MANUAL="Y"
  689. warn "failed to add answer file ($xml) to ISO image, $FB"
  690. else
  691. wimlib-imagex update "$wim" "$index" --command "add $answer /$dat" > /dev/null || true
  692. fi
  693. fi
  694. if [[ "$MANUAL" == [Yy1]* ]]; then
  695. wimlib-imagex update "$wim" "$index" --command "delete --force /$file" > /dev/null || true
  696. if wimlib-imagex extract "$wim" "$index" "/$org" "--dest-dir=$tmp" >/dev/null 2>&1; then
  697. if ! wimlib-imagex update "$wim" "$index" --command "add $tmp/$org /$file" > /dev/null; then
  698. warn "failed to restore original answer file ($org)."
  699. fi
  700. fi
  701. fi
  702. local find="$file"
  703. [[ "$MANUAL" == [Yy1]* ]] && find="$org"
  704. path=$(find "$dir" -maxdepth 1 -type f -iname "$find" | head -n 1)
  705. if [ -f "$path" ]; then
  706. if [[ "$MANUAL" != [Yy1]* ]]; then
  707. mv -f "$path" "${path%.*}.org"
  708. else
  709. mv -f "$path" "${path%.*}.xml"
  710. fi
  711. fi
  712. rm -rf "$tmp"
  713. return 0
  714. }
  715. removeImage() {
  716. local iso="$1"
  717. [ ! -f "$iso" ] && return 0
  718. [ -n "$CUSTOM" ] && return 0
  719. rm -f "$iso" 2> /dev/null || warn "failed to remove $iso !"
  720. return 0
  721. }
  722. buildImage() {
  723. local dir="$1"
  724. local failed=""
  725. local cat="BOOT.CAT"
  726. local log="/run/shm/iso.log"
  727. local base size size_gb space space_gb desc
  728. if [ -f "$BOOT" ]; then
  729. error "File $BOOT does already exist?!" && return 1
  730. fi
  731. base=$(basename "$BOOT")
  732. local out="$TMP/${base%.*}.tmp"
  733. rm -f "$out"
  734. desc=$(printVersion "$DETECTED" "ISO")
  735. local msg="Building $desc image..."
  736. info "$msg" && html "$msg"
  737. [ -z "$LABEL" ] && LABEL="Windows"
  738. if [ ! -f "$dir/$ETFS" ]; then
  739. error "Failed to locate file \"$ETFS\" in ISO image!" && return 1
  740. fi
  741. size=$(du -h -b --max-depth=0 "$dir" | cut -f1)
  742. size_gb=$(formatBytes "$size")
  743. space=$(df --output=avail -B 1 "$TMP" | tail -n 1)
  744. space_gb=$(formatBytes "$space")
  745. if (( size > space )); then
  746. error "Not enough free space in $STORAGE, have $space_gb available but need at least $size_gb." && return 1
  747. fi
  748. if [[ "${BOOT_MODE,,}" != "windows_legacy" ]]; then
  749. genisoimage -o "$out" -b "$ETFS" -no-emul-boot -c "$cat" -iso-level 4 -J -l -D -N -joliet-long -relaxed-filenames -V "${LABEL::30}" \
  750. -udf -boot-info-table -eltorito-alt-boot -eltorito-boot "$EFISYS" -no-emul-boot -allow-limited-size -quiet "$dir" 2> "$log" || failed="y"
  751. else
  752. case "${DETECTED,,}" in
  753. "win2k"* | "winxp"* | "win2003"* )
  754. genisoimage -o "$out" -b "$ETFS" -no-emul-boot -boot-load-seg 1984 -boot-load-size 4 -c "$cat" -iso-level 2 -J -l -D -N -joliet-long \
  755. -relaxed-filenames -V "${LABEL::30}" -quiet "$dir" 2> "$log" || failed="y" ;;
  756. "win9"* )
  757. genisoimage -o "$out" -b "$ETFS" -J -r -V "${LABEL::30}" -quiet "$dir" 2> "$log" || failed="y" ;;
  758. * )
  759. genisoimage -o "$out" -b "$ETFS" -no-emul-boot -c "$cat" -iso-level 2 -J -l -D -N -joliet-long -relaxed-filenames -V "${LABEL::30}" \
  760. -udf -allow-limited-size -quiet "$dir" 2> "$log" || failed="y" ;;
  761. esac
  762. fi
  763. if [ -n "$failed" ]; then
  764. [ -s "$log" ] && echo "$(<"$log")"
  765. error "Failed to build image!" && return 1
  766. fi
  767. local error=""
  768. local hide="Warning: creating filesystem that does not conform to ISO-9660."
  769. [ -s "$log" ] && error="$(<"$log")"
  770. [[ "$error" != "$hide" ]] && echo "$error"
  771. mv -f "$out" "$BOOT" || return 1
  772. return 0
  773. }
  774. bootWindows() {
  775. rm -rf "$TMP"
  776. if [ -f "$STORAGE/windows.args" ]; then
  777. ARGS=$(<"$STORAGE/windows.args")
  778. ARGS="${ARGS//[![:print:]]/}"
  779. ARGUMENTS="$ARGS ${ARGUMENTS:-}"
  780. fi
  781. if [ -s "$STORAGE/windows.type" ] && [ -f "$STORAGE/windows.type" ]; then
  782. if [ -z "${DISK_TYPE:-}" ]; then
  783. DISK_TYPE=$(<"$STORAGE/windows.type")
  784. DISK_TYPE="${DISK_TYPE//[![:print:]]/}"
  785. fi
  786. fi
  787. if [ -s "$STORAGE/windows.mode" ] && [ -f "$STORAGE/windows.mode" ]; then
  788. BOOT_MODE=$(<"$STORAGE/windows.mode")
  789. BOOT_MODE="${BOOT_MODE//[![:print:]]/}"
  790. fi
  791. if [ -s "$STORAGE/windows.old" ] && [ -f "$STORAGE/windows.old" ]; then
  792. if [[ "${PLATFORM,,}" == "x64" ]]; then
  793. MACHINE=$(<"$STORAGE/windows.old")
  794. MACHINE="${MACHINE//[![:print:]]/}"
  795. fi
  796. fi
  797. return 0
  798. }
  799. ######################################
  800. ! parseVersion && exit 58
  801. ! parseLanguage && exit 56
  802. ! detectCustom && exit 59
  803. if ! startInstall; then
  804. bootWindows && return 0
  805. exit 68
  806. fi
  807. if [ ! -s "$ISO" ] || [ ! -f "$ISO" ]; then
  808. if ! downloadImage "$ISO" "$VERSION" "$LANGUAGE"; then
  809. rm -f "$ISO" 2> /dev/null || true
  810. exit 61
  811. fi
  812. fi
  813. if ! extractImage "$ISO" "$DIR" "$VERSION"; then
  814. rm -f "$ISO" 2> /dev/null || true
  815. exit 62
  816. fi
  817. if ! detectImage "$DIR" "$VERSION"; then
  818. abortInstall "$DIR" "$ISO" && return 0
  819. exit 60
  820. fi
  821. if ! prepareImage "$ISO" "$DIR"; then
  822. abortInstall "$DIR" "$ISO" && return 0
  823. exit 66
  824. fi
  825. if ! updateImage "$DIR" "$XML" "$LANGUAGE"; then
  826. abortInstall "$DIR" "$ISO" && return 0
  827. exit 63
  828. fi
  829. if ! removeImage "$ISO"; then
  830. exit 64
  831. fi
  832. if ! buildImage "$DIR"; then
  833. exit 65
  834. fi
  835. if ! finishInstall "$BOOT" "N"; then
  836. exit 69
  837. fi
  838. html "Successfully prepared image for installation..."
  839. return 0