Back to blog
FILE 0xC4·MAKING BOOTABLE WINDOWS 98 FLOPPIES FROM A MODERN MAC

Making bootable Windows 98 floppies from a modern Mac

March 8, 2026 · retrocomputing, python, floppies

I wanted to install Windows 98 SE on a period-correct Pentium III without going through the usual ISO-to-USB workaround. That meant making the actual floppy disks. There is no good off-the-shelf tool to bin-pack a Windows 98 install across ~140 1.44 MB floppies, so I wrote one.

What was happening

Windows 98 SE shipped on a single boot floppy plus a CD. To install without a CD-ROM drive you historically used the full floppy set: 140-something disks containing a packed copy of the CAB files. Microsoft did not ship that floppy set publicly; you assembled it from the CD with proprietary tools that haven't run on a modern OS in 20 years.

The boot disk alone is easier — it's DOS plus the right drivers (HIMEM, EMM386, SMARTDRV, MSCDEX, OAKCDROM, FDISK, FORMAT, SYS, EDIT) and a CONFIG.SYS that loads the CD-ROM driver. The full floppy set is harder: you have to lay out the CAB contents across disks while respecting FAT12 limits and the install order Windows expects.

What I found

The whole thing is a Python program with no third-party dependencies (stdlib only, optional py7zr for ISO extraction on Windows). Two entry points:

win98_floppy_maker.py    # full ~140-disk set with bin-packing
boot_disk_creator.py     # single bootable disk with CD-ROM drivers

Architecture is straightforward:

The cross-platform formatting calls were the most annoying part. macOS doesn't ship a real FAT12 formatter; you use newfs_msdos -F 12 -v WIN98_SETUP. Linux is mkfs.fat -F 12. Windows is format.com with stdin piped in for the "Press ENTER" prompt.

For the boot disk specifically, the contents are baked into the source as a manifest:

BOOT_FILES = [
    ("IO.SYS",       "system",   True),    # marked system
    ("MSDOS.SYS",    "system",   True),
    ("COMMAND.COM",  "system",   False),
    ("HIMEM.SYS",    "support",  False),
    ("EMM386.EXE",   "support",  False),
    ("SMARTDRV.EXE", "support",  False),
    ("MSCDEX.EXE",   "support",  False),
    ("OAKCDROM.SYS", "support",  False),
    # ...
]

After the file copy, the writer marks the system files with the FAT12 system attribute and writes a boot sector that points IO.SYS at the right cluster. The CONFIG.SYS and AUTOEXEC.BAT come from string templates so you can customize the drive letter or skip the CD-ROM driver entirely.

What I'd do differently

The udev rules and the auto-detect loop are clever but they're also the source of every bug. If I wrote this again I would skip the autodetect and just take a --device flag. The 500ms poll is fine on Linux but on macOS the diskutil call is slow enough that you can't actually meet the polling interval. A user running through 140 disks does not need autodetect — they need an obvious "next disk inserted, press ENTER" prompt.

The other thing I'd add: a SHA-256 over the planned layout written to a sidecar file. Re-running the same source produces the same layout, but recovering after a disk failure mid-set means trusting that the program will lay disks 43+ identically. A persisted manifest makes that explicit instead of implicit.