Making bootable Windows 98 floppies from a modern Mac
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:
FloppyDriveDetectorpolls every 500ms for a newly inserted floppy and resolves the device path per OS. macOS gives you/dev/disk5and friends viadiskutil; Linux watches udev forID_DRIVE_FLOPPY=1; Windows enumerates removable drives.ISOExtractormounts or extracts the source ISO and returns a flat file tree.DiskSetPlannerruns the bin-packing. First-fit-decreasing over CAB file sizes, with a hard 1,440 × 1024 - filesystem-overhead ceiling per disk. Some files (notably PRECOPY*.CAB) must be in fixed slots; those are pinned first.FloppyWriterformats FAT12 and writes the planned set for the current disk, then prompts for the next.- State is persisted between disk-changes so you can stop after disk 42 and resume tomorrow without re-bin-packing.
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.