From eb1befd90eb04b40c1b79169d73010c6ea566518 Mon Sep 17 00:00:00 2001 From: Kroese Date: Thu, 20 Aug 2026 04:50:57 +0200 Subject: [PATCH] feat: Implement Windows 95 support (#2165) --- .github/workflows/install.yml | 100 ++- .github/workflows/settings.yml | 2 +- src/answer.sh | 58 +- src/batch.sh | 1128 +++++++++++++++++++++++++++++--- src/define.sh | 20 +- src/install.sh | 81 ++- src/legacy.sh | 39 +- src/power.sh | 33 +- src/sif.sh | 36 +- 9 files changed, 1316 insertions(+), 181 deletions(-) diff --git a/.github/workflows/install.yml b/.github/workflows/install.yml index a0572ff2..e62fa2d4 100644 --- a/.github/workflows/install.yml +++ b/.github/workflows/install.yml @@ -369,6 +369,12 @@ jobs: } } + if (-not (Test-Path -LiteralPath "Z:\" -PathType Container)) { + throw "Mapped Z: drive is not available." + } + + $driveZ = "ok" + $share = ( Get-Content ` -LiteralPath "\\host.lan\Data\validation.token" ` @@ -431,6 +437,7 @@ jobs: version = [string]$windows.Version build = [string]$windows.BuildNumber platform = $platform + drive_z = $driveZ oem_file = $oemFile share = $share share_write = "ok" @@ -590,6 +597,15 @@ jobs: TestSharedFolder = "ok" End Function + Function TestMappedDrive(filesystem) + If Not filesystem.DriveExists("Z:") Then + Err.Raise vbObjectError + 4, "TestMappedDrive", _ + "Mapped Z: drive is not available." + End If + + TestMappedDrive = "ok" + End Function + Function TestInternet() Dim request Dim response @@ -647,6 +663,7 @@ jobs: Dim oemFile Dim share Dim shareWrite + Dim driveZ Dim internet Dim json @@ -685,6 +702,7 @@ jobs: oemFile = ReadTextFile(filesystem, "C:\OEM\validation.token") share = ReadTextFile(filesystem, "\\host.lan\Data\validation.token") shareWrite = TestSharedFolder(filesystem, token) + driveZ = TestMappedDrive(filesystem) internet = TestInternet() End If @@ -700,6 +718,7 @@ jobs: """oem_file"":""" & EscapeJson(oemFile) & """," & _ """share"":""" & EscapeJson(share) & """," & _ """share_write"":""" & EscapeJson(shareWrite) & """," & _ + """drive_z"":""" & EscapeJson(driveZ) & """," & _ """internet"":""" & EscapeJson(internet) & """" & _ "}" @@ -779,11 +798,14 @@ jobs: :retry if not exist \\host.lan\Data\validation.token goto wait + if not exist Z:\NUL goto wait echo TOKEN> C:\OEM\validation.result type C:\OEM\validation.token >> C:\OEM\validation.result echo SHARE>> C:\OEM\validation.result type \\host.lan\Data\validation.token >> C:\OEM\validation.result + echo DRIVE_Z>> C:\OEM\validation.result + echo ok>> C:\OEM\validation.result echo VERSION>> C:\OEM\validation.result ver >> C:\OEM\validation.result @@ -940,6 +962,7 @@ jobs: EXPECTED_PLATFORM: ${{ inputs.platform }} MINIMUM_BUILD: ${{ inputs.minimum_build }} DISPLAY_NAME: ${{ inputs.name }} + VERSION: ${{ inputs.version }} run: | set -Eeuo pipefail @@ -1094,12 +1117,62 @@ jobs: return 0 } + check_rdp_port() { + local container_ip + local attempt + + container_ip="$( + docker inspect \ + --format '{{range .NetworkSettings.Networks}}{{println .IPAddress}}{{end}}' \ + "$CONTAINER" 2>/dev/null | + head -n 1 || true + )" + + if [[ -z "$container_ip" ]]; then + echo "Could not determine the Windows container IP address." + return 1 + fi + + echo + echo "Checking TCP port 3389 at $container_ip..." + + for attempt in {1..30}; do + if timeout 2 bash -c \ + "exec 3<>/dev/tcp/$container_ip/3389; exec 3>&-; exec 3<&-" \ + 2>/dev/null; then + echo "TCP port 3389 is open." + return 0 + fi + + sleep 2 + done + + case "${VERSION,,}" in + 95 | 98 | me) + echo "::warning::TCP port 3389 is not open for $DISPLAY_NAME; this is allowed." + return 0 + ;; + esac + + echo "TCP port 3389 is not open for $DISPLAY_NAME." + return 1 + } + deadline=$((SECONDS + INSTALL_TIMEOUT)) reboot_timeout="$REBOOT_TIMEOUT" minimum_reboots="$MINIMUM_REBOOTS" boot_loop_limit="$BOOT_LOOP_LIMIT" first_bios_start=-1 + version_lower="${VERSION,,}" + + case "$version_lower" in + 95 | 98 | me) + reboot_timeout=$((REBOOT_TIMEOUT * 2)) + echo "Extended first reboot timeout to $((reboot_timeout / 60)) minutes for $DISPLAY_NAME." + ;; + esac + while (( SECONDS < deadline )); do state="$( docker inspect \ @@ -1259,6 +1332,10 @@ jobs: awk '$0 == "SHARE" { getline; print; exit }' <<< "$basic_response" )" + drive_z="$( + awk '$0 == "DRIVE_Z" { getline; print; exit }' <<< "$basic_response" + )" + version="$( awk ' $0 == "VERSION" { found = 1; next } @@ -1266,7 +1343,7 @@ jobs: ' <<< "$basic_response" )" - if [[ -z "$oem_file" || -z "$share" || -z "$version" ]] || + if [[ -z "$oem_file" || -z "$share" || -z "$drive_z" || -z "$version" ]] || ! grep -Eq '[0-9]+\.[0-9]+\.[0-9]+' <<< "$version"; then sleep 1 continue @@ -1298,6 +1375,11 @@ jobs: exit 1 fi + if [[ "$drive_z" != "ok" ]]; then + echo "The mapped Z: drive test did not succeed." + exit 1 + fi + normalized_caption="${version//\(R\)/}" normalized_expected_caption="${EXPECTED_CAPTION//\(R\)/}" @@ -1332,12 +1414,17 @@ jobs: exit 1 fi + if ! check_rdp_port; then + exit 1 + fi + echo echo "$DISPLAY_NAME installed successfully." echo "Version: $version_number" echo "Build: $build" echo "OEM files: copied successfully" echo "Shared folder: readable and writable" + echo "Mapped Z: drive: available" exit 0 fi @@ -1371,6 +1458,7 @@ jobs: oem_file="$(jq -r '.oem_file // empty' <<< "$response")" share="$(jq -r '.share // empty' <<< "$response")" share_write="$(jq -r '.share_write // empty' <<< "$response")" + drive_z="$(jq -r '.drive_z // empty' <<< "$response")" internet="$(jq -r '.internet // empty' <<< "$response")" if [[ "$token" != "$EXPECTED_TOKEN" ]]; then @@ -1401,6 +1489,11 @@ jobs: exit 1 fi + if [[ "$drive_z" != "ok" ]]; then + echo "The mapped Z: drive test did not succeed." + exit 1 + fi + if [[ "$internet" != "ok" ]]; then echo "The guest internet connection test did not succeed." exit 1 @@ -1451,6 +1544,10 @@ jobs: exit 1 fi + if ! check_rdp_port; then + exit 1 + fi + echo echo "$DISPLAY_NAME installed successfully." echo "Version: $version" @@ -1458,6 +1555,7 @@ jobs: echo "Platform: $platform" echo "OEM files: copied successfully" echo "Shared folder: readable and writable" + echo "Mapped Z: drive: available" echo "Internet connection: accessible" exit 0 fi diff --git a/.github/workflows/settings.yml b/.github/workflows/settings.yml index f9e8131a..4d4fda21 100644 --- a/.github/workflows/settings.yml +++ b/.github/workflows/settings.yml @@ -55,7 +55,7 @@ jobs: tunnel_delay: 1800 kill_on_failure: true install_timeout: 9000 - reboot_timeout: 1800 + reboot_timeout: 18000 minimum_reboots: 1 boot_loop_limit: 10 expected_caption: ${{ inputs.expected_caption }} diff --git a/src/answer.sh b/src/answer.sh index 65c46978..9d915c04 100644 --- a/src/answer.sh +++ b/src/answer.sh @@ -526,17 +526,28 @@ updateAutologinXML() { return 0 } +usesWscriptLogonLauncher() { + + case "${DETECTED,,}" in + "winvista"* | "win7"* | "win2008r2"* ) return 0 ;; + esac + + return 1 +} + updateLogonCommandXML() { local asset="$1" - case "${DETECTED,,}" in - "winvista"* ) return 0 ;; - esac - local command="$XML_COMPONENT_SHELL_OOBE/u:FirstLogonCommands/u:SynchronousCommand/u:CommandLine" local expected='cmd.exe /d /c call "%WINDIR%\Setup\Scripts\SetupComplete.cmd" logon' - local hidden="powershell.exe -NoLogo -NoProfile -NonInteractive -WindowStyle Hidden -Command \"& \$env:ComSpec /d /c ('call ' + [char]34 + \$env:WINDIR + '\Setup\Scripts\SetupComplete.cmd' + [char]34 + ' logon'); exit \$LASTEXITCODE\"" + local hidden + + if usesWscriptLogonLauncher; then + hidden='wscript.exe //B //NoLogo C:\Windows\Setup\Scripts\RunHidden.vbs' + else + hidden="powershell.exe -NoLogo -NoProfile -NonInteractive -WindowStyle Hidden -Command \"\$cmd = 'call ' + [char]34 + \$env:WINDIR + '\Setup\Scripts\SetupComplete.cmd' + [char]34 + ' logon'; & \$env:ComSpec /d /c \$cmd; exit \$LASTEXITCODE\"" + fi local count value count=$(getXMLNodeCount "$asset" "$command") || return 1 @@ -1957,12 +1968,49 @@ prepareSetupScript() { [ -n "$staged" ] || return 0 + stageHiddenLogonLauncher "$stage" || return 1 updateSetupScript "$staged" "$asset" || return 1 finalizeSetupScript "$staged" || return 1 return 0 } +stageHiddenLogonLauncher() { + + local stage="$1" + + usesWscriptLogonLauncher || return 0 + + local target="$stage/\$OEM\$/\$\$/Setup/Scripts/RunHidden.vbs" + + if ! mkdir -p "$(dirname "$target")"; then + error "Failed to create hidden logon launcher directory!" + return 1 + fi + + if ! cat > "$target" <<'EOF' +Option Explicit + +Dim shell, command, result + +Set shell = CreateObject("WScript.Shell") +command = shell.ExpandEnvironmentStrings("%ComSpec% /d /c call " & Chr(34) & "%WINDIR%\Setup\Scripts\SetupComplete.cmd" & Chr(34) & " logon") +result = shell.Run(command, 0, True) +WScript.Quit result +EOF + then + error "Failed to create hidden logon launcher!" + return 1 + fi + + if ! unix2dos -q "$target"; then + error "Failed to convert hidden logon launcher to DOS format!" + return 1 + fi + + return 0 +} + updateSetupScript() { local script="$1" diff --git a/src/batch.sh b/src/batch.sh index fc73569e..8a0d943c 100644 --- a/src/batch.sh +++ b/src/batch.sh @@ -7,9 +7,15 @@ Win9xInstall() { local dir="$3" local desc="$4" - local folder monitor="Plug and Play Monitor" options="/IS /IQ /IT" target - local setup="SETUP" local shortcut="Y" + local setup="SETUP" + local share='\\host.lan\Data' + local options="/ID /IS /IQ /IT" + local monitor="Plug and Play Monitor" + local target folder + + local msg="Preparing $desc installation..." + info "$msg" && html "$msg" if disabled "$SHORTCUT" || disabled "${SAMBA:-Y}"; then shortcut="N" @@ -24,7 +30,9 @@ Win9xInstall() { case "${id,,}" in "win95"* ) - folder="WIN95" ;; + folder="WIN95" + options="/IW $options" + share='\\Host\Data' ;; "win98"* ) folder="WIN98" options="/P J /IE /NF $options" ;; @@ -43,8 +51,12 @@ Win9xInstall() { return 1 fi - [ -z "$WIDTH" ] && WIDTH="1280" - [ -z "$HEIGHT" ] && HEIGHT="720" + if [[ "${id,,}" == "win95"* ]]; then + validateWin95OSR2 "$target" "$desc" || return 1 + fi + + [ -z "$WIDTH" ] && WIDTH="1024" + [ -z "$HEIGHT" ] && HEIGHT="768" validateResolution "WIDTH" "$WIDTH" 320 || return 1 validateResolution "HEIGHT" "$HEIGHT" 200 || return 1 @@ -101,7 +113,7 @@ Win9xInstall() { stageWin9xPasswordList "$target" "$desc" || return 1 fi - if enabled "$shortcut" || [ -n "$install" ] || [[ "${id,,}" == "win9x"* ]]; then + if enabled "$shortcut" || [ -n "$install" ] || [[ "${id,,}" == "win95"* || "${id,,}" == "win9x"* ]]; then stageWin9xHide "$target" "$desc" || return 1 fi @@ -109,11 +121,12 @@ Win9xInstall() { stageWin9xWait "$target" "$desc" || return 1 fi - if [ -n "$install" ] || [[ "${id,,}" == "win9x"* ]]; then + if [ -n "$install" ] || [[ "${id,,}" == "win95"* || "${id,,}" == "win9x"* ]]; then stageWin9xPostSetup "$target" "$desc" "$install" "$id" || return 1 fi stageWin9xDMA "$target" "$desc" || return 1 + stageWin9xScandiskConfig "$target" "$desc" || return 1 # Reuse the same driver archive extraction used by the XP/2003 path. All # Windows 9x support files live together under win9x/ in that archive. @@ -122,10 +135,30 @@ Win9xInstall() { local patcher="$win9x/patcher9x/patcher9x" local patcher_dos="$patcher.img" local qemouse="$win9x/qemouse" - local display="$win9x/boxv9x" + local display="$win9x/vmdisp9x" + local audio95="$win9x/alcx95" + local audiowdm="$win9x/alcxwdm" extractDrivers "$drivers" || return 1 + if [ -z "${BIOS:-}" ]; then + local bios="$win9x/bios/win9x.bin" + + if [ ! -s "$bios" ]; then + rm -rf "$drivers" || : + error "Failed to locate the Windows 9x BIOS!" + return 1 + fi + + BIOS="$TMP/win9x.bin" + + if ! cp -f -- "$bios" "$BIOS"; then + rm -rf "$drivers" || : + error "Failed to prepare the Windows 9x BIOS!" + return 1 + fi + fi + if [ ! -f "$patcher" ] || [ ! -f "$patcher_dos" ]; then rm -rf "$drivers" || : error "Failed to locate Patcher9x!" @@ -143,12 +176,38 @@ Win9xInstall() { return 1 fi + if [[ "${id,,}" == "win95"* ]]; then + # QEMU AC97 is a normal PCI PnP device, so keep its Win95 VxD driver beside + # the retained C:\SETUP source. + if ! stageWin95AC97Driver "$target" "$audio95" "$desc"; then + rm -rf "$drivers" || : + return 1 + fi + elif [[ "${id,,}" == "win98"* || "${id,,}" == "win9x"* ]]; then + # Win98/Me use the WDM package from the same driver archive. Stage it beside + # C:\SETUP and add a QEMU-specific INF match for the emulated Intel AC97 PCI ID. + if ! stageWin98MeAC97Driver "$target" "$audiowdm" "$desc"; then + rm -rf "$drivers" || : + return 1 + fi + fi + + # Keep Win95's product-type and online-service changes together in the same + # loose SETUPPP.INF. This prevents MSN/Online Services from being installed + # at all, and also handles the no-key OEM product type when needed. + if [[ "${id,,}" == "win95"* ]]; then + patchWin95SetupComponents "$target" "$desc" "$batchKey" || { + rm -rf "$drivers" || : + return 1 + } + fi + if ! stageWin9xDosPatcher "$target" "$desc" "$patcher_dos"; then rm -rf "$drivers" || : return 1 fi - if ! stageWin9xMouseFiles "$target" "$desc" "$qemouse"; then + if ! stageWin9xSetupOverrides "$target" "$desc" "$qemouse"; then rm -rf "$drivers" || : return 1 fi @@ -158,6 +217,12 @@ Win9xInstall() { return 1 fi + if enabled "$shortcut" && [[ "${id,,}" == "win95"* ]] && + ! stageWin9xSharedShortcut "$target" "$desc" "$share"; then + rm -rf "$drivers" || : + return 1 + fi + if [[ "${id,,}" == "win9x"* ]]; then if ! stageWinMeFinalBootFiles "$dir" "$target" "$desc" || ! stageWinMeBootActivation "$target" "$desc" || @@ -176,7 +241,7 @@ Win9xInstall() { writeWin9xAnswerFile \ "$target" "$id" "$setup" "$monitor" \ "$batchHost" "$batchUsername" "$batchOrganization" "$batchWorkgroup" \ - "$batchKey" "$shortcut" "$install" "$timezone" || return 1 + "$batchKey" "$shortcut" "$install" "$timezone" "$share" || return 1 # Build the hard-disk system image from the setup files themselves. Do not rely # on a particular El Torito floppy-image filename: some perfectly valid Win9x @@ -193,6 +258,31 @@ Win9xInstall() { return 0 } +validateWin95OSR2() { + + local dir="$1" + local desc="$2" + local format + + format=$(find "$dir" -maxdepth 1 -type f -iname 'FORMAT.COM' -print -quit) || return 1 + + if [ -z "$format" ]; then + error "Failed to locate FORMAT.COM in $desc setup files!" + return 1 + fi + + # FAT32 support was introduced with Windows 95 OSR2. Require the setup + # FORMAT.COM itself to carry the FAT32 marker so pre-OSR2 media fail before + # any setup files are modified. The existing image builder performs the full + # structural validation of that FAT32 boot template later. + if ! LC_ALL=C grep -aFq 'FAT32 ' "$format"; then + error "Unsupported $desc ISO image: Windows 95 OSR2 or newer is required!" + return 1 + fi + + return 0 +} + stageWin9xPasswordList() { local dir="$1" @@ -280,6 +370,37 @@ stageWin9xPostSetup() { local install="$3" local id="$4" local target="$dir/POST9X.BAT" + + # Windows 95 can reach its first real desktop without the extra Setup reboot + # used by the established 98/Me post-setup path. Run FirstLogon and finish its + # pending file operations in a hidden housekeeping shell so the next normal + # reboot does not re-enter WININIT. Launch the OEM install in its own visible + # command window and wait for it so install.bat keeps its debugging behavior. + if [[ "${id,,}" == "win95"* ]]; then + { + printf '%s\n' \ + '@ECHO OFF' \ + 'C:\WINDOWS\RUNDLL.EXE SETUPX.DLL,InstallHinfSection Win9x.FirstLogon 4 C:\WINDOWS\MSBATCH.INF' \ + 'COPY /Y C:\SETUP\W9XAUTO.BAT C:\AUTOEXEC.BAT >NUL' \ + 'DEL C:\SETUP\PATCH9X.RUN >NUL' \ + 'IF EXIST C:\WINDOWS\DESKTOP\ONLINE~1 C:\WINDOWS\COMMAND\DELTREE.EXE /Y C:\WINDOWS\DESKTOP\ONLINE~1 >NUL' + + if [ -n "$install" ]; then + if enabled "${LOG:-}"; then + printf '%s\n' 'START /W C:\WINDOWS\COMMAND.COM /C C:\OEM\install.bat > C:\OEM\install.log' + else + printf '%s\n' 'START /W C:\WINDOWS\COMMAND.COM /C C:\OEM\install.bat' + fi + fi + + } | unix2dos > "$target" || { + error "Failed to create post-desktop setup script for $desc!" + return 1 + } + + return 0 + fi + local marker="$dir/POST9X.NEW" local cleanup="$dir/POST9X.REG" @@ -381,6 +502,52 @@ EOF return 0 } +stageWin9xScandiskConfig() { + + local dir="$1" + local desc="$2" + local target="$dir/W9XSCAN.INI" + + # Boot-time ScanDisk is started in /CUSTOM mode after an unclean shutdown. + # Keep filesystem repair enabled while removing every normal interactive + # decision and summary screen. A surface scan is intentionally excluded: it + # is unnecessary for routine forced-poweroff recovery and would make booting + # a large virtual disk take an excessive amount of time. + { + printf '%s\n' \ + '[Environment]' \ + 'LfnCheck=On' \ + '' \ + '[Custom]' \ + 'DriveSummary=Off' \ + 'AllSummary=Off' \ + 'Surface=Never' \ + 'CheckHost=Never' \ + 'SaveLog=Append' \ + 'Undo=Never' \ + 'DS_Header=Fix' \ + 'FAT_Media=Fix' \ + 'Okay_Entries=Fix' \ + 'Bad_Chain=Fix' \ + 'Crosslinks=Fix' \ + 'Boot_Sector=Fix' \ + 'Invalid_MDFAT=Fix' \ + 'DS_Crosslinks=Fix' \ + 'DS_LostClust=Fix' \ + 'DS_Signatures=Fix' \ + 'Mismatch_FAT=Fix' \ + 'Bad_Clusters=Fix' \ + 'Bad_Entries=Delete' \ + 'LostClust=Save' \ + '' + } | unix2dos > "$target" || { + error "Failed to create SCANDISK.INI in $desc setup files!" + return 1 + } + + return 0 +} + patchWin9xSetupFiles() { local id="$1" @@ -413,16 +580,18 @@ patchWin9xSetupFiles() { stageWin9xDisplayDriver "$target" "$display" "$desc" || return 1 if ! mv -f -- \ - "$target/BOXV9X/boxv9x.inf" \ - "$target/BOXV9X/boxvmini.drv" \ - "$target/BOXV9X/boxvmini.vxd" \ + "$target/VMDISP9X/vmdisp9x.inf" \ + "$target/VMDISP9X/qemumini.drv" \ + "$target/VMDISP9X/qemumini.vxd" \ + "$target/VMDISP9X/vmhal9x.dll" \ + "$target/VMDISP9X/vmhal486.dll" \ + "$target/VMDISP9X/vmdisp9x.dll" \ "$target/"; then error "Failed to stage the Windows 9x display driver in the setup source!" return 1 fi - rm -rf -- "$target/BOXV9X" || return 1 - : > "$target/BOXV9X" || return 1 + rm -rf -- "$target/VMDISP9X" || return 1 # Use QEMouse directly in the MINI.CAB GUI Setup environment for every Win9x # release, so the setup mouse path does not depend on a DOS INT 33h TSR. @@ -431,6 +600,414 @@ patchWin9xSetupFiles() { return 0 } +stageWin95AC97Driver() { + + local dir="$1" + local audio="$2" + local desc="$3" + local source name + + if [ ! -d "$audio" ] || [ -z "$(find "$audio" -maxdepth 1 -type f -print -quit)" ]; then + error "Failed to locate the Windows 95 AC97 driver payload!" + return 1 + fi + + # Keep the driver payload self-contained: drivers.tar.xz owns the exact files + # and this path simply stages everything present in win9x/alcx95/. This avoids + # having to update an installer-side filename list when the payload changes. + if ! cp -a -- "$audio"/. "$dir"/; then + error "Failed to stage the Windows 95 AC97 driver payload!" + return 1 + fi + + # Verify every top-level file copied byte-for-byte without hard-coding the + # payload contents. Subdirectories, if ever added, are copied by cp -a above. + while IFS= read -r -d '' source; do + name="${source##*/}" + if [ ! -f "$dir/$name" ] || ! cmp -s -- "$source" "$dir/$name"; then + error "Failed to verify the staged Windows 95 AC97 driver file $name!" + return 1 + fi + done < <(find "$audio" -maxdepth 1 -type f -print0) + + # Validate the critical invariants of the Win95 VxD package without rewriting + # vendor files: QEMU's PCI ID, PnP/SB-emulation registration, and every file + # referenced by the INF must be present and non-empty. + if ! python3 - "$dir" <<'PY' +from pathlib import Path +import sys + +root = Path(sys.argv[1]) +required_files = ( + 'VALCX95.INF', + 'VALCX95.VXD', + 'ALCX95.DRV', + 'ALCX95.INI', + 'ALSWWT.DRV', + 'ALSWWT16.DLL', + 'SWWTAC97.DAT', + 'SWWTAC97.TON', +) + +for name in required_files: + path = root / name + if not path.is_file() or path.stat().st_size == 0: + raise SystemExit(f'AC97 payload file missing or empty: {name}') + +data = (root / 'VALCX95.INF').read_bytes().replace(b'\r\n', b'\n').lower() + +required_inf = ( + b'signature="$chicago$"', + b'class=media', + b'%alcich.devicedesc%=alcaud, pci\\ven_8086&dev_2415', + b'%alcaud_sb.devicedesc%=alcsb, virtual\\alc_sbemulation', + b'hkr,drivers,sbemulation,,"yes"', + b'copyfiles=alcaud.copylist, alcwtb.copylist, alcini.copylist', +) +for item in required_inf: + if item not in data: + raise SystemExit(f'AC97 INF verification failed for {item!r}.') + +def section(name: bytes) -> bytes: + marker = b'[' + name + b']' + try: + body = data.split(marker, 1)[1] + except IndexError: + raise SystemExit(f'AC97 INF section missing: {name.decode()}') + return body.split(b'\n[', 1)[0] + +destinations = section(b'destinationdirs') +for item in ( + b'defaultdestdir=11', + b'alcaud.copylist=11', + b'alcwtb.copylist=10', + b'alcini.copylist=10', +): + if item not in destinations: + raise SystemExit(f'AC97 destination verification failed for {item!r}.') + +copy_sections = { + b'alcaud.copylist': ( + b'valcx95.vxd', + b'alcx95.drv', + b'alswwt.drv', + b'alswwt16.dll', + b'swwtac97.ton', + ), + b'alcwtb.copylist': (b'swwtac97.dat',), + b'alcini.copylist': (b'alcx95.ini',), +} +for name, items in copy_sections.items(): + body = section(name) + for item in items: + if item not in body: + raise SystemExit( + f'AC97 copy-list verification failed in {name!r} for {item!r}.' + ) + +source_files = section(b'sourcedisksfiles') +for name in required_files[1:]: + if name.lower().encode() not in source_files: + raise SystemExit(f'AC97 source-file verification failed for {name}.') +PY + then + error "Failed to verify the Windows 95 AC97 PCI driver payload!" + return 1 + fi + + return 0 +} + +stageWin98MeAC97Driver() { + + local dir="$1" + local audio="$2" + local desc="$3" + local source name + + if [ ! -d "$audio" ] || [ -z "$(find "$audio" -maxdepth 1 -type f -print -quit)" ]; then + error "Failed to locate the Windows 98/Me AC97 WDM driver payload!" + return 1 + fi + + # Keep both vendor INFs and every referenced payload file together in the + # retained setup source. OtherDevicePath already makes C:\SETUP an OEM PnP + # search path while Windows is enumerating the AC97 controller. + if ! cp -a -- "$audio"/. "$dir"/; then + error "Failed to stage the Windows 98/Me AC97 WDM driver payload!" + return 1 + fi + + # Verify every top-level archive file before creating the QEMU compatibility + # INF below, so the vendor payload itself remains byte-for-byte unchanged. + while IFS= read -r -d '' source; do + name="${source##*/}" + if [ ! -f "$dir/$name" ] || ! cmp -s -- "$source" "$dir/$name"; then + error "Failed to verify the staged Windows 98/Me AC97 driver file $name!" + return 1 + fi + done < <(find "$audio" -maxdepth 1 -type f -print0) + + # The supplied Realtek WDM INFs contain Intel ICH matches but no generic + # PCI\VEN_8086&DEV_2415 entry used by QEMU's AC97 device. Derive an unsigned + # 8.3-compatible OEM INF from Alcxwdm0.inf, preserving the vendor INF itself. + # Removing CatalogFile avoids claiming the generated INF is covered by the + # vendor catalog after adding the QEMU hardware ID. + if ! python3 - "$dir" <<'PY' +from pathlib import Path +import re +import sys + +root = Path(sys.argv[1]) +required_files = ( + 'Alcxwdm0.inf', + 'Alcxwdm1.inf', + 'Alcxwdm.cat', + 'ALCXWDM.SYS', + 'Soundman.exe', + 'ALSndMgr.cpl', + 'alsndmgr.wav', +) + +for name in required_files: + path = root / name + if not path.is_file() or path.stat().st_size == 0: + raise SystemExit(f'AC97 WDM payload file missing or empty: {name}') + +vendor = root / 'Alcxwdm0.inf' +raw = vendor.read_bytes() +normalized = raw.replace(b'\r\n', b'\n').lower() + +for item in ( + b'signature="$chicago$"', + b'class=media', + b'[avance]', + b'[ac97aud]', + b'alcxwdm.sys', + b'alsoinstall=ks.registration(ks.inf), wdmaudio.registration(wdmaudio.inf)', + b'hkr,drivers\\wave\\wdmaud.drv,driver,,wdmaud.drv', + b'alcxwdm.sys=222', + b'soundman.exe=222', + b'alsndmgr.cpl=222', + b'alsndmgr.wav=222', + b'hklm,%autorun%,soundman,,"soundman.exe"', +): + if item not in normalized: + raise SystemExit(f'AC97 WDM installation invariant missing: {item!r}.') + +newline = b'\r\n' if b'\r\n' in raw else b'\n' +clone = raw +clone, catalog_count = re.subn( + br'(?im)^[ \t]*CatalogFile[ \t]*=[^\r\n]*(?:\r?\n)', + b'', + clone, + count=1, +) +if catalog_count != 1: + raise SystemExit(f'Unexpected AC97 WDM CatalogFile count: {catalog_count}.') + +soundman = re.compile( + br'(?im)^[ \t]*HKLM[ \t]*,[ \t]*%AUTORUN%[ \t]*,[ \t]*SoundMan[ \t]*,' + br'[ \t]*,[ \t]*"SOUNDMAN\.EXE"[ \t]*(?:\r?\n|$)' +) +clone, soundman_count = soundman.subn(b'', clone, count=1) +if soundman_count != 1: + raise SystemExit(f'Unexpected AC97 SoundMan autorun count: {soundman_count}.') + +preferred = re.compile( + br'(?im)^[ \t]*HKR[ \t]*,[ \t]*,[ \t]*SetupPreferredAudioDevices[ \t]*,' +) +if preferred.search(clone): + raise SystemExit('AC97 WDM INF already configures SetupPreferredAudioDevices.') + +addreg = re.search(br'(?im)^\[AC97AUD\.AddReg\][ \t]*(?:\r?\n)', clone) +if addreg is None: + raise SystemExit('AC97 WDM [AC97AUD.AddReg] section missing.') +preference = b'HKR,,SetupPreferredAudioDevices,3,01,00,00,00' + newline +clone = clone[:addreg.end()] + preference + clone[addreg.end():] + +generic = re.compile( + br'(?im)^[ \t]*%ALCAUD\.Desc%[ \t]*=[ \t]*AC97AUD[ \t]*,[ \t]*' + br'PCI\\VEN_8086&DEV_2415[ \t]*(?:\r?$)' +) + +if not generic.search(clone): + section = re.search(br'(?im)^\[Avance\][ \t]*(?:\r?\n)', clone) + if section is None: + raise SystemExit('AC97 WDM [Avance] section missing.') + entry = b'%ALCAUD.Desc%=AC97AUD,\tPCI\\VEN_8086&DEV_2415' + newline + clone = clone[:section.end()] + entry + clone[section.end():] + +matches = generic.findall(clone) +if len(matches) != 1: + raise SystemExit( + f'Unexpected QEMU AC97 generic hardware-ID count: {len(matches)} (expected 1).' + ) + +if re.search(br'(?im)^[ \t]*CatalogFile[ \t]*=', clone): + raise SystemExit('Generated QEMU AC97 INF still references the vendor catalog.') +if soundman.search(clone): + raise SystemExit('Generated QEMU AC97 INF still enables the SoundMan autorun entry.') +if len(preferred.findall(clone)) != 1: + raise SystemExit('Generated QEMU AC97 INF preferred-audio registration is invalid.') + +output = root / 'QEMUAC97.INF' +output.write_bytes(clone) +if not output.is_file() or output.stat().st_size == 0: + raise SystemExit('Failed to create QEMUAC97.INF.') +PY + then + error "Failed to prepare the Windows 98/Me AC97 WDM PCI driver!" + return 1 + fi + + return 0 +} + +patchWin95SetupComponents() { + + local target="$1" + local desc="$2" + local batchKey="$3" + local setuppp="$target/SETUPPP.INF" + local layout="$target/LAYOUT.INF" + + # Win95 OSR2 normally keeps SETUPPP.INF in PRECOPY*.CAB, while some later + # media exposes it directly. A loose copy selected by LAYOUT.INF lets us + # suppress MSN/Online Services before Setup creates their desktop objects. + if [ ! -s "$setuppp" ]; then + extractWin9xCabFile "$target" 'SETUPPP.INF' "$setuppp" "$desc" || return 1 + fi + + if [ ! -s "$layout" ]; then + extractWin9xCabFile "$target" 'LAYOUT.INF' "$layout" "$desc" || return 1 + fi + + if ! python3 - "$setuppp" "$layout" "$batchKey" <<'PY' +from pathlib import Path +import re +import sys + +setuppp = Path(sys.argv[1]) +layout = Path(sys.argv[2]) +has_key = bool(sys.argv[3]) + +setup_data = setuppp.read_bytes() +setup_lines = setup_data.splitlines(keepends=True) + +if not has_key: + product_re = re.compile( + br'^([ \t]*ProductType[ \t]*=[ \t]*)9([ \t]*)(\r?\n)?$', + re.IGNORECASE, + ) + matches = [] + + for index, line in enumerate(setup_lines): + match = product_re.match(line) + if match: + matches.append((index, match)) + + if len(matches) != 1: + print( + f"Unexpected Windows 95 ProductType=9 count: {len(matches)} (expected 1).", + file=sys.stderr, + ) + raise SystemExit(1) + + index, match = matches[0] + setup_lines[index] = ( + match.group(1) + + b'1' + + match.group(2) + + (match.group(3) or b'') + ) + +# MOS.INF installs The Microsoft Network and online registration. MSINFO.INF +# installs the OSR2 Online Services material. Remove every active reference but +# preserve comments and every unrelated byte in SETUPPP.INF. +blocked = (b'MOS.INF', b'MSINFO.INF') +filtered = [] + +for line in setup_lines: + stripped = line.lstrip(b' \t') + upper = stripped.upper() + + if stripped.startswith(b';') or not any(name in upper for name in blocked): + filtered.append(line) + +setup_data = b''.join(filtered) + +for line in setup_data.splitlines(): + stripped = line.lstrip(b' \t') + upper = stripped.upper() + + if not stripped.startswith(b';') and any(name in upper for name in blocked): + print("Failed to remove a Windows 95 online-service setup reference.", file=sys.stderr) + raise SystemExit(1) + +if not has_key: + product_one = re.compile( + br'^[ \t]*ProductType[ \t]*=[ \t]*1[ \t]*$', + re.IGNORECASE, + ) + if sum(bool(product_one.match(line.rstrip(b'\r'))) for line in setup_data.splitlines()) != 1: + print("Failed to verify the Windows 95 ProductType=1 change.", file=sys.stderr) + raise SystemExit(1) + +layout_data = layout.read_bytes() +layout_lines = layout_data.splitlines(keepends=True) +layout_re = re.compile( + br'^([ \t]*SETUPPP\.INF[ \t]*=[ \t]*)[0-9]+,([^,\r\n]*,)[0-9]+([^\r\n]*)(\r?\n)?$', + re.IGNORECASE, +) +layout_matches = [] + +for index, line in enumerate(layout_lines): + match = layout_re.match(line) + if match: + layout_matches.append((index, match)) + +if len(layout_matches) != 1: + print( + f"Unexpected Windows 95 SETUPPP.INF layout-entry count: {len(layout_matches)} (expected 1).", + file=sys.stderr, + ) + raise SystemExit(1) + +setuppp_size = len(setup_data) +index, match = layout_matches[0] +layout_lines[index] = ( + match.group(1) + + b'0,' + + match.group(2) + + str(setuppp_size).encode('ascii') + + match.group(3) + + (match.group(4) or b'') +) + +setuppp.write_bytes(setup_data) +layout.write_bytes(b''.join(layout_lines)) + +verify_layout = layout.read_bytes().splitlines() +expected = re.compile( + br'^[ \t]*SETUPPP\.INF[ \t]*=[ \t]*0,[^,\r\n]*,' + + str(setuppp_size).encode('ascii') + + br'(?:,|[ \t]*$)', + re.IGNORECASE, +) + +if sum(bool(expected.match(line)) for line in verify_layout) != 1: + print("Failed to verify the Windows 95 SETUPPP.INF layout entry.", file=sys.stderr) + raise SystemExit(1) +PY + then + error "Failed to patch the Windows 95 setup components!" + return 1 + fi + + return 0 +} + patchWinMeBaseComponents() { local target="$1" @@ -503,10 +1080,16 @@ stageWin9xDisplayDriver() { local source="$2" local desc="$3" - local dest="$target/BOXV9X" + local dest="$target/VMDISP9X" local file - for file in boxv9x.inf boxvmini.drv boxvmini.vxd; do + for file in \ + vmdisp9x.inf \ + qemumini.drv \ + qemumini.vxd \ + vmhal9x.dll \ + vmhal486.dll \ + vmdisp9x.dll; do if [ ! -f "$source/$file" ]; then error "Failed to locate required Windows 9x display driver file: $file" @@ -519,35 +1102,33 @@ stageWin9xDisplayDriver() { mkdir -p "$dest" || return 1 if ! cp -f -- \ - "$source/boxv9x.inf" \ - "$source/boxvmini.drv" \ - "$source/boxvmini.vxd" \ + "$source/vmdisp9x.inf" \ + "$source/qemumini.drv" \ + "$source/qemumini.vxd" \ + "$source/vmhal9x.dll" \ + "$source/vmhal486.dll" \ + "$source/vmdisp9x.dll" \ "$dest/"; then error "Failed to add the display driver to $desc setup files!" return 1 fi - # The virtual display driver's DDC flag makes Win9x enumerate a Plug and - # Play monitor after the display driver starts. The unattended setup already - # selects the monitor and display mode, and BOXV9X carries a fixed mode list, - # so disable DDC in our staged copy to avoid a second monitor PnP pass. - if ! grep -Eq '^[[:space:]]*HKR,DEFAULT,DDC,,1[[:space:]]*$' "$dest/boxv9x.inf"; then + # VMDisp9x's DDC flag makes Win9x enumerate a Plug and Play monitor after + # the display driver starts. The unattended setup already selects the monitor + # and display mode, and VMDisp9x carries a fixed mode list, so disable DDC in + # our staged copy to avoid a second monitor PnP pass. + if ! grep -Eq '^[[:space:]]*HKR,DEFAULT,DDC,,1[[:space:]]*$' "$dest/vmdisp9x.inf"; then error "Failed to locate the DDC setting in the Windows 9x display driver!" return 1 fi - if ! sed -i 's/HKR,DEFAULT,DDC,,1/HKR,DEFAULT,DDC,,0/' "$dest/boxv9x.inf" || - ! grep -Eq '^[[:space:]]*HKR,DEFAULT,DDC,,0[[:space:]]*$' "$dest/boxv9x.inf"; then + if ! sed -i 's/HKR,DEFAULT,DDC,,1/HKR,DEFAULT,DDC,,0/' "$dest/vmdisp9x.inf" || + ! grep -Eq '^[[:space:]]*HKR,DEFAULT,DDC,,0[[:space:]]*$' "$dest/vmdisp9x.inf"; then error "Failed to disable DDC in the Windows 9x display driver!" return 1 fi - # BOXV9X is the source-media tag named by the upstream INF. Provide that tag - # beside the driver files so Setup never asks for a separate driver disk while - # installing the exact QEMU PCI match. - : > "$dest/BOXV9X" || return 1 - return 0 } @@ -689,28 +1270,205 @@ stageWin9xDosPatcher() { return 0 } -stageWin9xMouseFiles() { +stageWin9xSetupOverrides() { local dir="$1" local desc="$2" local qemouse="$3" - local file="qemouse.drv" - local source="$qemouse/$file" - local target="$dir/${file^^}" + local layout="$dir/LAYOUT.INF" + local mouse="$qemouse/qemouse.drv" + local scandisk="$dir/W9XSCAN.INI" + local mouse_target="$dir/MOUSE.DRV" + local scandisk_target="$dir/SCANDISK.INI" + local layout_temp="$TMP/win9x-layout-files" + local cab extracted existing layout_name + local mouse_size scandisk_size + local -a precopy_cabs=() other_cabs=() extracted_layouts=() layouts=() - # QEMouse is used both by MINI.CAB during GUI Setup and by the installed OS. - # Keep one source copy in C:\SETUP; MSBATCH.INF installs the same binary as - # MOUSE.DRV so Windows can retain its stock mouse configuration and VxD stack. - if [ ! -f "$source" ]; then - error "Failed to locate $file!" + if [ ! -f "$mouse" ]; then + error "Failed to locate qemouse.drv!" return 1 fi - if ! cp -f -- "$source" "$target" || ! cmp -s -- "$source" "$target"; then - error "Failed to stage $file in $desc setup files!" + if [ ! -f "$scandisk" ]; then + error "Failed to locate the staged SCANDISK.INI configuration!" return 1 fi + # Make Setup's own source filenames contain our desired payloads. This avoids + # competing CopyFiles destinations entirely: every normal Setup copy or later + # reinstall of MOUSE.DRV/SCANDISK.INI resolves to these same loose files. + if [ ! -s "$layout" ]; then + extractWin9xCabFile "$dir" 'LAYOUT.INF' "$layout" "$desc" || return 1 + fi + + # Windows 98 splits its setup source map across LAYOUT.INF, LAYOUT1.INF and + # LAYOUT2.INF. Stage every numbered LAYOUT file available in the cabinets, but + # never overwrite an already-loose copy because earlier setup patches may have + # modified it. Win95/Me simply keep whichever LAYOUT files their media carries. + rm -rf -- "$layout_temp" || return 1 + mkdir -p "$layout_temp/files" || return 1 + + mapfile -d '' precopy_cabs < <( + find "$dir" -maxdepth 1 -type f -iname 'PRECOPY*.CAB' -print0 | sort -z + ) + mapfile -d '' other_cabs < <( + find "$dir" -maxdepth 1 -type f -iname '*.CAB' ! -iname 'PRECOPY*.CAB' -print0 | sort -z + ) + + for cab in "${precopy_cabs[@]}" "${other_cabs[@]}"; do + + rm -rf -- "$layout_temp/files" || return 1 + mkdir -p "$layout_temp/files" || return 1 + + cabextract -q \ + -F 'LAYOUT*.INF' \ + -F 'layout*.inf' \ + -d "$layout_temp/files" "$cab" >/dev/null 2>&1 || : + + mapfile -d '' extracted_layouts < <( + find "$layout_temp/files" -type f -iname 'LAYOUT*.INF' -print0 | sort -z + ) + + for extracted in "${extracted_layouts[@]}"; do + + layout_name=$(basename "$extracted") + layout_name="${layout_name^^}" + + if [[ ! "$layout_name" =~ ^LAYOUT[0-9]*\.INF$ ]]; then + continue + fi + + existing=$(find "$dir" -maxdepth 1 -type f -iname "$layout_name" -print -quit) || return 1 + + if [ -n "$existing" ] && [ -s "$existing" ]; then + continue + fi + + if [ -n "$existing" ]; then + rm -f -- "$existing" || return 1 + fi + + if ! cp -f -- "$extracted" "$dir/$layout_name"; then + rm -rf -- "$layout_temp" || : + error "Failed to stage $layout_name from $desc setup files!" + return 1 + fi + + done + + done + + rm -rf -- "$layout_temp" || return 1 + + mapfile -d '' layouts < <( + find "$dir" -maxdepth 1 -type f -iregex '.*/LAYOUT[0-9]*\.INF' -print0 | sort -z + ) + + if (( ${#layouts[@]} == 0 )); then + error "Failed to locate the $desc setup source layout files!" + return 1 + fi + + if ! cp -f -- "$mouse" "$mouse_target" || + ! cmp -s -- "$mouse" "$mouse_target"; then + error "Failed to replace the $desc MOUSE.DRV setup source!" + return 1 + fi + + if ! cp -f -- "$scandisk" "$scandisk_target" || + ! cmp -s -- "$scandisk" "$scandisk_target"; then + error "Failed to replace the $desc SCANDISK.INI setup source!" + return 1 + fi + + mouse_size=$(stat -c %s -- "$mouse_target") || return 1 + scandisk_size=$(stat -c %s -- "$scandisk_target") || return 1 + + # Search the complete LAYOUT*.INF set for each stock source entry, then patch + # the one file that owns it. Point that entry at disk 0 with no source + # subdirectory and record the loose replacement's uncompressed size. + if ! python3 - "$mouse_size" "$scandisk_size" "${layouts[@]}" <<'PY' +from pathlib import Path +import re +import sys + +replacements = { + b'MOUSE.DRV': int(sys.argv[1]), + b'SCANDISK.INI': int(sys.argv[2]), +} +layouts = [Path(value) for value in sys.argv[3:]] +contents = { + layout: layout.read_bytes().splitlines(keepends=True) + for layout in layouts +} +changed = set() + +for name, size in replacements.items(): + pattern = re.compile( + br'^([ \t]*' + re.escape(name) + br'[ \t]*=[ \t]*)' + br'[0-9]+,[^,\r\n]*,[0-9]+([^\r\n]*)(\r?\n)?$', + re.IGNORECASE, + ) + matches = [] + + for layout, lines in contents.items(): + for index, line in enumerate(lines): + match = pattern.match(line) + if match: + matches.append((layout, index, match)) + + if len(matches) != 1: + print( + f"Unexpected {name.decode('ascii')} layout-entry count across " + f"LAYOUT*.INF: {len(matches)} (expected 1).", + file=sys.stderr, + ) + raise SystemExit(1) + + layout, index, match = matches[0] + contents[layout][index] = ( + match.group(1) + + b'0,,' + + str(size).encode('ascii') + + match.group(2) + + (match.group(3) or b'') + ) + changed.add(layout) + +for layout in changed: + layout.write_bytes(b''.join(contents[layout])) + +verify = { + layout: layout.read_bytes().splitlines() + for layout in layouts +} +for name, size in replacements.items(): + expected = re.compile( + br'^[ \t]*' + re.escape(name) + br'[ \t]*=[ \t]*0,,' + + str(size).encode('ascii') + + br'(?:,|[ \t]*$)', + re.IGNORECASE, + ) + count = sum( + bool(expected.match(line)) + for lines in verify.values() + for line in lines + ) + if count != 1: + print( + f"Failed to verify the {name.decode('ascii')} layout override " + f"across LAYOUT*.INF.", + file=sys.stderr, + ) + raise SystemExit(1) +PY + then + error "Failed to update the $desc setup source layout!" + return 1 + fi + + rm -f -- "$scandisk" || return 1 return 0 } @@ -744,6 +1502,91 @@ stageWin9xFinalAutoexec() { return 0 } +stageWin9xSharedShortcut() { + + local dir="$1" + local desc="$2" + local share="$3" + local target="$dir/Shared.lnk" + + # Use the native network PIDL layout from a Shell-generated Windows 98 link. + # Keep that known-good structure byte-for-byte for the normal host.lan target, + # and resize only its server/share PIDL items when Win95 needs the NetBIOS name. + if ! python3 - "$target" "$share" <<'PY' +from pathlib import Path +import hashlib +import struct +import sys + +target = Path(sys.argv[1]) +share = sys.argv[2].encode('ascii') + +template = bytes.fromhex( + '4c0000000114020000000000c000000000000046410000001000000000000000' + '0000000000000000000000000000000000000000000000000300000000000000' + '0000000000000000000000003b0014001f2d602c8d20ea3a6910a2d708002b30' + '309d10004000005c5c686f73742e6c616e001500c000005c5c686f73742e6c61' + '6e5c446174610000001d00433a5c57494e444f57535c53595354454d5c534845' + '4c4c33322e444c4c00000000' +) + +if len(template) != 172: + raise SystemExit('Native Shared link template size verification failed.') +if hashlib.sha256(template).hexdigest() != '5e5bbcc3e32b1dea7a7a742dc648f85d9aca801b874d60066899d770aa7700a5': + raise SystemExit('Native Shared link template hash verification failed.') +if not share.startswith(b'\\\\'): + raise SystemExit('Shared link target is not a UNC path.') + +server, separator, name = share[2:].partition(b'\\') +if not server or not separator or not name or b'\\' in name: + raise SystemExit('Shared link target must contain one server and one share name.') + +# The working Win98 link contains a 20-byte Network Neighborhood root PIDL, +# followed by a server item (type 0x40) and a share item (type 0xC0). +header = template[:76] +old_id_size = struct.unpack_from(' "$target/MSBATCH.INF" || return 1 return 0 @@ -1632,7 +2553,11 @@ writeWin9xBrowserPowerRegistry() { 'HKU,".DEFAULT\Control Panel\PowerCfg\PowerPolicies\3","Policies",0x00000001,01,00,00,00,02,00,00,00,01,00,00,00,00,00,00,00,02,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,32,32,00,00,04,00,00,00,05,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,01,64,64,64,64,00,00' \ '' \ '[Win9x.BrowserDefault]' \ - 'HKU,".DEFAULT\Software\Microsoft\Internet Connection Wizard","Completed",0x00010001,1' \ + 'HKU,".DEFAULT\Software\Microsoft\Internet Connection Wizard","Completed",1,01,00,00,00' \ + 'HKU,".DEFAULT\Software\Microsoft\Internet Connection Wizard","DesktopChanged",0x00010001,1' \ + 'HKU,".DEFAULT\Software\Microsoft\Windows\CurrentVersion\Internet Settings","EnableAutodial",0x00010001,0' \ + 'HKU,".DEFAULT\Software\Microsoft\Windows\CurrentVersion\Internet Settings","NoNetAutodial",0x00010001,0' \ + 'HKU,".DEFAULT\Software\Microsoft\Windows\CurrentVersion\Internet Settings","ProxyEnable",0x00010001,0' \ 'HKU,".DEFAULT\Software\Microsoft\Internet Explorer\Main","Start Page",,"http://www.google.com"' \ 'HKU,".DEFAULT\Software\Microsoft\Internet Explorer\Main","First Home Page",,"http://www.google.com"' \ 'HKU,".DEFAULT\Software\Microsoft\Internet Explorer\Main","Default_Page_URL",,"http://www.google.com"' \ @@ -1655,7 +2580,11 @@ writeWin9xBrowserPowerRegistry() { 'HKCU,"Control Panel\PowerCfg\PowerPolicies\3","Policies",0x00000001,01,00,00,00,02,00,00,00,01,00,00,00,00,00,00,00,02,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,32,32,00,00,04,00,00,00,05,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,01,64,64,64,64,00,00' \ '' \ '[Win9x.BrowserUser]' \ - 'HKCU,"Software\Microsoft\Internet Connection Wizard","Completed",0x00010001,1' \ + 'HKCU,"Software\Microsoft\Internet Connection Wizard","Completed",1,01,00,00,00' \ + 'HKCU,"Software\Microsoft\Internet Connection Wizard","DesktopChanged",0x00010001,1' \ + 'HKCU,"Software\Microsoft\Windows\CurrentVersion\Internet Settings","EnableAutodial",0x00010001,0' \ + 'HKCU,"Software\Microsoft\Windows\CurrentVersion\Internet Settings","NoNetAutodial",0x00010001,0' \ + 'HKCU,"Software\Microsoft\Windows\CurrentVersion\Internet Settings","ProxyEnable",0x00010001,0' \ 'HKCU,"Software\Microsoft\Internet Explorer\Main","Start Page",,"http://www.google.com"' \ 'HKCU,"Software\Microsoft\Internet Explorer\Main","First Home Page",,"http://www.google.com"' \ 'HKCU,"Software\Microsoft\Internet Explorer\Main","Default_Page_URL",,"http://www.google.com"' \ @@ -1676,6 +2605,12 @@ writeWin9xStorageRegistry() { writeWin9xCleanupRegistry() { printf '%s\n' \ + '[Win95.InitShell]' \ + 'HKLM,"SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce","InitShell",,,' \ + '' \ + '[Win95.Welcome]' \ + 'HKU,".DEFAULT\Software\Microsoft\Windows\CurrentVersion\Explorer\Tips","Show",1,00' \ + '' \ '[Win9x.Welcome]' \ 'HKLM,"Software\Microsoft\Windows\CurrentVersion\Run","Welcome",,,' \ '' \ @@ -1695,6 +2630,8 @@ writeWin9xCleanupRegistry() { 'HKLM,"SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce","^SetupICWDesktop",,,' \ 'HKCU,"SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce","^SetupICWDesktop",,,' \ 'HKU,".DEFAULT\Software\Microsoft\Windows\CurrentVersion\RunOnce","^SetupICWDesktop",,,' \ + 'HKCU,"Software\Microsoft\Internet Connection Wizard","ShellNext",,,' \ + 'HKU,".DEFAULT\Software\Microsoft\Internet Connection Wizard","ShellNext",,,' \ '' \ '[Win9x.Connect]' \ 'connec~1.lnk' \ @@ -2131,6 +3068,7 @@ createWin9xSystemImage() { '[Options]' \ "BootGUI=$boot_gui" \ 'BootDelay=0' \ + 'AutoScan=2' \ 'Logo=0' \ '' } | unix2dos > "$msdos" || return 1 diff --git a/src/define.sh b/src/define.sh index 5d04a898..a719d425 100644 --- a/src/define.sh +++ b/src/define.sh @@ -113,9 +113,9 @@ parseVersion() { VERSION="win2019-hv" ;; "2012" | "2012r2" | "win2012" | "win2012r2" | "windows2012" | "windows 2012" ) VERSION="win2012r2-eval" ;; - "2008" | "2008r2" | "win2008" | "win2008r2" | "windows2008" | "windows 2008" ) + "2008" | "2008r2" | "2k8" | "win2008" | "win2008r2" | "windows2008" | "windows 2008" ) VERSION="win2008r2" ;; - "2003" | "2003r2" | "win2003" | "win2003r2" | "windows2003" | "windows 2003" ) + "2003" | "2003r2" | "2k3" | "win2003" | "win2003r2" | "windows2003" | "windows 2003" ) VERSION="win2003r2" ;; "core11" | "core 11" ) VERSION="core11" @@ -875,22 +875,6 @@ supportsSIF() { return 1 } -supportsACPI() { - - local id="$1" - - case "${id,,}" in - - "reactos" ) - - # If the ISO is a Live-CD it will ignore ACPI signals. - hasSystemImage && return 1 ;; - - esac - - return 0 -} - supportsBootKey() { local id="$1" diff --git a/src/install.sh b/src/install.sh index 625afef0..198b8ea9 100644 --- a/src/install.sh +++ b/src/install.sh @@ -388,7 +388,7 @@ finishInstall() { local aborted="$2" local boot="$3" - local base secure=0 + local base file bios target if ! hasImage "$iso"; then error "Failed to find ISO file: $iso" && return 1 @@ -400,7 +400,7 @@ finishInstall() { fi fi - local file="$STORAGE/windows.ver" + file="$(stateFile "ver")" cp -f /etc/version "$file" || { error "Failed to save the Windows installation version!" return 1 @@ -435,17 +435,16 @@ finishInstall() { # Aborted Win11 installs boot without any answer file present, # so enable Secure Boot and TPM to satisfy its hardware checks. if enabled "$aborted" || enabled "$MANUAL"; then - [[ "${DETECTED,,}" == "win11"* ]] && secure=1 - fi - if (( secure )); then + if [[ "${DETECTED,,}" == "win11"* ]]; then - BOOT_MODE="windows_secure" - writeState "mode" "$BOOT_MODE" || { - error "Failed to save the Windows boot mode!" - return 1 - } + BOOT_MODE="windows_secure" + writeState "mode" "$BOOT_MODE" || { + error "Failed to save the Windows boot mode!" + return 1 + } + fi fi fi @@ -460,12 +459,12 @@ finishInstall() { if [[ "$SYSTEM" == "$TMP/"* ]]; then - if ! mv -f -- "$SYSTEM" "$STORAGE/windows.img"; then + if ! mv -f -- "$SYSTEM" "$(stateFile "img")"; then error "Failed to finalize the Windows system image!" return 1 fi - BOOT="$STORAGE/windows.img" + BOOT="$(stateFile "img")" else @@ -481,6 +480,36 @@ finishInstall() { BOOT="$SYSTEM" fi + + if ! setOwner "$BOOT"; then + warn "failed to set the owner for \"$BOOT\" !" + fi + + fi + + if [ -n "${BIOS:-}" ]; then + + bios="$(stateFile "bios")" + target="${bios}.tmp" + + if ! cp -f -- "$BIOS" "$target"; then + rm -f -- "$target" + error "Failed to copy the BIOS file!" + return 1 + fi + + if ! mv -f -- "$target" "$bios"; then + rm -f -- "$target" + error "Failed to finalize the BIOS file!" + return 1 + fi + + BIOS="$bios" + + if ! setOwner "$BIOS"; then + warn "failed to set the owner for \"$BIOS\" !" + fi + fi if ! rm -rf -- "$TMP"; then @@ -964,9 +993,33 @@ isLegacyBoot() { [[ "${BOOT_MODE,,}" == "windows_legacy" ]] } +hasMarker() { + + [ -f "$(stateFile "$1")" ] +} + hasBootMarker() { - [ -f "$STORAGE/windows.boot" ] + [ -f "$(stateFile "boot")" ] +} + +createMarker() { + + local marker + marker="$(stateFile "$1")" + + if ! touch "$marker"; then + warn "failed to create marker \"$marker\" !" + return 1 + fi + + if ! setOwner "$marker"; then + rm -f "$marker" + warn "failed to set the owner for \"$marker\" !" + return 1 + fi + + return 0 } hasImage() { @@ -991,7 +1044,7 @@ getSystemImage() { return 0 fi - image="$STORAGE/windows.img" + image="$(stateFile "img")" hasImage "$image" || return 1 echo "$image" diff --git a/src/legacy.sh b/src/legacy.sh index c2e24603..410f9b41 100644 --- a/src/legacy.sh +++ b/src/legacy.sh @@ -15,7 +15,6 @@ setMachine() { return 1 fi - writeState "vga" "std" || return 1 writeState "mode" "windows_legacy" || return 1 case "${id,,}" in @@ -23,25 +22,27 @@ setMachine() { "win9"* | "winnt4" | "win2k"* | "reactos" ) writeState "old" "pc" || return 1 - writeState "type" "auto" || return 1 + writeState "type" "auto" || return 1 ;; esac case "${id,,}" in - "win95" | "winnt4" ) + "winnt4" | "win2k"* ) + writeState "vga" "cirrus" || return 1 ;; + + *) writeState "vga" "std" || return 1 ;; + + esac + + case "${id,,}" in + + "win9"* | "winnt4" ) writeState "usb" "N" || return 1 writeState "port" "on" || return 1 writeState "net" "pcnet" || return 1 - writeState "sound" "sb16" || return 1 ;; - - "win98" | "win9x" ) - - writeState "port" "on" || return 1 - writeState "net" "pcnet" || return 1 - writeState "sound" "sb16" || return 1 - writeState "usb" "pci-ohci" || return 1 ;; + writeState "sound" "AC97" || return 1 ;; "win2k"* ) @@ -63,13 +64,23 @@ setMachine() { if isReactOSLiveCD "$iso"; then SYSTEM="$iso" + createMarker "kill" || return 1 fi ;; esac fi - restoreBootMode || return 1 restoreMachine || return 1 + restoreBootMode || return 1 + + case "${id,,}" in + + "win95" | "winnt4" ) + + # Windows 95 does not support ACPI so disable graceful shutdown + createMarker "kill" || return 1 ;; + + esac case "${id,,}" in @@ -135,6 +146,10 @@ restoreMachineState() { restoreState "CPU_MODEL" "cpu" || return 1 restoreState "DISK_TYPE" "type" || return 1 + if [ -z "${BIOS:-}" ] && [ -s "$(stateFile "bios")" ]; then + BIOS="$(stateFile "bios")" + fi + mergeState "CPU_FLAGS" "flag" "," || return 1 mergeState "ARGUMENTS" "args" " " || return 1 diff --git a/src/power.sh b/src/power.sh index 18a81a30..8b1c440e 100644 --- a/src/power.sh +++ b/src/power.sh @@ -398,18 +398,7 @@ markWindowsBooted() { # now booting from the installed disk rather than from setup media. ready || return 0 - local marker="$STORAGE/windows.boot" - - if ! touch "$marker"; then - warn "failed to create Windows installation marker!" - return 0 - fi - - if ! setOwner "$marker"; then - rm -f "$marker" - warn "failed to set the owner for \"$marker\" !" - return 0 - fi + createMarker "boot" || return 0 if ! disabled "$REMOVE"; then case "${BOOT,,}" in @@ -506,20 +495,16 @@ gracefulShutdown() { finish "$code" fi - if ! supportsACPI "$DETECTED"; then - if [[ "${DETECTED,,}" != "reactos" ]]; then - info "This $(app) version does not support ACPI shutdown, decreasing timeout to 10 seconds..." - TIMEOUT=13 - else - info "ReactOS LiveCD does not support ACPI shutdown, decreasing timeout to 1 second..." - TIMEOUT=7 - fi - elif hasSystemImage && ! hasBootMarker; then - info "$(app) will ignore ACPI signals during setup, decreasing timeout to 10 seconds..." - TIMEOUT=13 - elif ! ready; then + if hasMarker "kill"; then + + info "This $(app) version does not support ACPI shutdown, decreasing timeout to 1 second..." + TIMEOUT=7 + + elif ! ready || { hasSystemImage && ! hasBootMarker; }; then + info "$(app) will ignore ACPI signals during setup, decreasing timeout to 10 seconds..." TIMEOUT=13 + fi normalizeTimeout 105 diff --git a/src/sif.sh b/src/sif.sh index 3b5e302b..b14a1ccf 100644 --- a/src/sif.sh +++ b/src/sif.sh @@ -10,6 +10,9 @@ SIFInstall() { local shortcut="Y" local drivers="/tmp/drivers" + local msg="Preparing $desc installation..." + info "$msg" && html "$msg" + if disabled "$SHORTCUT" || disabled "${SAMBA:-Y}"; then shortcut="N" fi @@ -69,8 +72,8 @@ SIFInstall() { oem=$(writeCommand "$install") || return 1 - [ -z "$WIDTH" ] && WIDTH="1280" - [ -z "$HEIGHT" ] && HEIGHT="720" + [ -z "$WIDTH" ] && WIDTH="1024" + [ -z "$HEIGHT" ] && HEIGHT="768" validateResolution "WIDTH" "$WIDTH" 320 || return 1 validateResolution "HEIGHT" "$HEIGHT" 200 || return 1 @@ -551,7 +554,7 @@ writeSIF() { ' AutoPartition=1' \ ' MsDosInitiated="0"' \ ' UnattendedInstall="Yes"' \ - ' AutomaticUpdates="Yes"' \ + ' AutomaticUpdates="No"' \ '' \ '[Unattended]' \ ' UnattendSwitch=Yes' \ @@ -658,6 +661,9 @@ writeRegistry() { '[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\wscsvc]' \ '"Start"=dword:00000004' \ '' \ + '[HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU]' \ + '"NoAutoUpdate"=dword:00000001' \ + '' \ '[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\SharedAccess\Parameters\FirewallPolicy\StandardProfile\GloballyOpenPorts\List]' \ '"3389:TCP"="3389:TCP:*:Enabled:@xpsp2res.dll,-22009"' \ '' \ @@ -712,6 +718,7 @@ appendRegistry() { '[HKEY_CURRENT_USER\Control Panel\Desktop]' \ '"SCRNSAVE.EXE"="off"' \ '"ScreenSaveActive"="0"' \ + '"DragFullWindows"="1"' \ '"MenuShowDelay"="100"' \ '' \ '[HKEY_CURRENT_USER\Control Panel\Desktop\WindowMetrics]' \ @@ -733,6 +740,12 @@ appendRegistry() { if [[ "$driver" == "2k" ]]; then { printf '%s\n' \ + '[HKEY_CURRENT_USER\Control Panel\PowerCfg]' \ + '"CurrentPowerPolicy"="3"' \ + '' \ + '[HKEY_CURRENT_USER\Control Panel\PowerCfg\PowerPolicies\3]' \ + '"Policies"=hex:01,00,00,00,00,00,00,00,01,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,32,32,00,00,04,00,00,00,04,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,01,64,64,64,64,00,00' \ + '' \ '[HKEY_USERS\.DEFAULT\Software\Microsoft\Windows\CurrentVersion\Runonce]' \ '"^SetupICWDesktop"=-' '' } | unix2dos >> "$dir/\$OEM\$/install.reg" || return 1 @@ -857,15 +870,16 @@ writeVBS() { 'Set FSO = WScript.CreateObject("Scripting.FileSystemObject")' \ 'PowerCfg = Shell.ExpandEnvironmentStrings("%SystemRoot%\System32\POWERCFG.EXE")' \ '' \ + 'Shell.RegWrite "HKCU\Control Panel\Desktop\SCRNSAVE.EXE", "off", "REG_SZ"' \ + 'Shell.RegWrite "HKCU\Control Panel\Desktop\ScreenSaveActive", "0", "REG_SZ"' \ + '' \ 'If FSO.FileExists(PowerCfg) Then' \ - ' Err.Clear' \ - ' Policy = Shell.RegRead("HKCU\Control Panel\PowerCfg\CurrentPowerPolicy")' \ - ' If Err.Number = 0 Then' \ - ' Cmd = Chr(34) & PowerCfg & Chr(34) & " /CHANGE " & Policy & " /NUMERICAL "' \ - ' For Each Setting In Array("/monitor-timeout-ac", "/monitor-timeout-dc", "/disk-timeout-ac", "/disk-timeout-dc", "/standby-timeout-ac", "/standby-timeout-dc")' \ - ' Shell.Run Cmd & Setting & " 0", 0, True' \ - ' Next' \ - ' End If' \ + ' Policy = 3' \ + ' Cmd = Chr(34) & PowerCfg & Chr(34) & " /CHANGE " & Policy & " /NUMERICAL "' \ + ' For Each Setting In Array("/monitor-timeout-ac", "/monitor-timeout-dc", "/disk-timeout-ac", "/disk-timeout-dc", "/standby-timeout-ac", "/standby-timeout-dc")' \ + ' Shell.Run Cmd & Setting & " 0", 0, True' \ + ' Next' \ + ' Shell.Run Chr(34) & PowerCfg & Chr(34) & " /SETACTIVE 3 /NUMERICAL", 0, True' \ 'End If' \ '' } | unix2dos > "$power" || return 1