BootBuilder Writes a FAT32 filesystem image, byte by byte, from a directory tree Techlosoft - Deployment Media Center WHAT IT IS ========== BootBuilder builds the USB image file: it hand-writes a complete FAT32 filesystem - boot sector, FSInfo, two FAT copies, cluster chains, directories, 8.3 short names and VFAT long-name entries - and wraps it in an MBR or a GPT. The result is an ordinary file on disk with your installer tree and answer file already inside it, in the one filesystem every UEFI machine on earth can read. It produces an IMAGE FILE. It does not touch physical devices. There is no code path in this program that opens a raw disk, a block device or a drive letter. Writing that image to a stick is somebody else's job - dd, Rufus, balenaEtcher, Disk Utility. BootBuilder stops at the file. Sibling tools cover the neighbouring problems: WinImageKit runs deployment recipes as a dependency graph; SetupPilot writes unattend.xml answer files; BootForge and RescueUSB handle rescue media workflows. BootBuilder is the only one in the line that implements a filesystem format itself. The other half of the tool is the READER. `inspect` and `verify` parse an image back from raw bytes using code written separately from the writer - it shares no struct, no offset table and no helper with it. That separation is the point: round-trip verification is only evidence if the two halves are independent. INSTALL ======= Pre-built binaries are in dist/. There is nothing to install - copy the one for your platform anywhere on your PATH and run it. dist/bootbuilder-linux-amd64 Linux, x86-64 dist/bootbuilder-darwin-arm64 macOS, Apple Silicon dist/bootbuilder-darwin-amd64 macOS, Intel dist/bootbuilder-windows-amd64.exe Windows, x86-64 On macOS and Linux you may need to mark it executable: chmod +x bootbuilder-linux-amd64 To build from source you need Go 1.24 or newer. There are no dependencies of any kind, so no network access is required: go build -o bootbuilder . go test ./... COMMANDS ======== bootbuilder plan --src --size 8GB [--scheme mbr|gpt] [--cluster-size 4096] [--label NAME] [--json] bootbuilder build --src --out usb.img --size 8GB [--scheme mbr|gpt] [--label NAME] [--cluster-size 4096] [--force] [--json] bootbuilder inspect [--json] bootbuilder verify --against [--json] bootbuilder help | -h | --help plan Computes the exact layout build would produce - sectors per cluster from Microsoft's own FAT32 table, FAT size, data start LBA, cluster count, the slack sectors past the last cluster - and the capacity check for your source tree. Writes nothing at all. Exits 1 if the tree does not fit, and says by exactly how many bytes. build Writes the image file named by --out and nothing else. Refuses to overwrite an existing file without --force, and refuses outright to write to anything that is not a regular file. inspect Opens an image read-only and parses it back from bytes with the independent reader: partition table (including GPT header and array CRC32 verification against the recomputed values), the whole BPB, the FSInfo sector, whether the two FAT copies are byte-identical, whether the backup boot sector matches, and a full recursive directory listing with each entry's short name, long name, first cluster and chain length. verify Walks the image and the source tree in parallel and compares every file by SHA-256, reporting missing files, extra files, content differences and any long name that did not survive. FLAGS --src Source tree. Its CONTENTS become the volume root, so --src ./winpe puts ./winpe/EFI at ::/EFI. --out Image file to write. --size Total image size: 8GB, 512MiB, 2TiB or raw bytes. --scheme mbr|gpt Partition table, default mbr. --label FAT volume label, up to 11 characters, default BOOTBUILDER. Written both to the BPB and as a volume-label record in the root directory. --cluster-size Cluster size override in bytes, a power of two from 512 to 65536. Without it the size comes from Microsoft's table (see LAYOUT below). --against Source tree for verify. --force Allow overwriting an existing --out. --json Machine-readable output on plan, build, inspect, verify. Short forms -s, -o and -a are accepted for --src, --out and --against. Flags may appear before or after positional arguments; either order works. SIZE STRINGS ============ 8GB 8 GiB (8589934592 bytes) 512MB 512 MiB (536870912 bytes) 2TiB 2 TiB 1.5G 1.5 GiB (1610612736 bytes) 67108864 raw bytes Suffixes are BINARY: 1KB means 1024 bytes, not 1000. KB/MB/GB and KiB/MiB/GiB are treated identically. Underscores and commas in the number are ignored. QUICK START =========== # what would the layout be? bootbuilder plan --src ./winpe --size 8GB --scheme gpt # write it bootbuilder build --src ./winpe --out usb.img --size 8GB --scheme gpt \ --label WINPE # read it back with the independent reader bootbuilder inspect usb.img # prove every byte survived bootbuilder verify usb.img --against ./winpe # then, with a tool that IS allowed to touch devices, and at your own risk: # sudo dd if=usb.img of=/dev/sdX bs=4M status=progress conv=fsync EXAMPLE OUTPUT ============== BootBuilder layout plan (nothing was written) source : ./winpe image size : 8.0 GiB (8589934592 bytes, 16777216 sectors of 512) scheme : gpt GEOMETRY partition start : LBA 2048 (1.0 MiB in) partition size : 16775135 sectors (8.0 GiB) sectors/cluster : 8 -> cluster 4.0 KiB (4096 bytes, from the Microsoft FAT32 table) reserved sectors : 32 FAT copies : 2 x 16351 sectors (8.0 MiB each) data starts at : LBA 34782 data sectors : 16742400 clusters : 2092800 (usable 8.0 GiB) slack : 1 sectors (512 B) past the last cluster root cluster : 2 volume label : "WINPE" ALLOCATION file clusters : 1304 directory clusters: 10 total clusters : 1314 of 2092800 bytes on volume : 5.1 MiB (5382144 bytes, including cluster slack) root dir entries : 15 (480 bytes) verdict : FITS - 2091486 clusters (8.0 GiB) would remain free WHAT THE WRITER ACTUALLY EMITS ============================== BOOT SECTOR AND BPB (partition LBA 0, mirrored at LBA 6) The 0xEB 0x58 0x90 jump instruction, the "MSWIN4.1" OEM name, bytes per sector, sectors per cluster, 32 reserved sectors, 2 FATs, a zero BPB_RootEntCnt and BPB_TotSec16 and BPB_FATSz16 as FAT32 requires, media descriptor 0xF8, CHS geometry 63/255, BPB_HiddSec set to the partition's start LBA, BPB_TotSec32, BPB_FATSz32, root cluster 2, FSInfo at sector 1, backup boot sector at sector 6, drive number 0x80, extended boot signature 0x29, a volume serial number, the 11-byte volume label, the "FAT32 " filesystem type string, and the 0x55AA signature. Bytes 90..509, where boot code would live, are left zero - see SCOPE. FSINFO (partition LBA 1, mirrored at LBA 7) Lead signature 0x41615252 ("RRaA"), structure signature 0x61417272 ("rrAa"), the real free-cluster count, the next-free-cluster hint, and the 0xAA550000 trailing signature. FILE ALLOCATION TABLE Two copies, written byte-identical. Entry 0 is 0x0FFFFFF8 (the media descriptor in its low byte), entry 1 is 0x0FFFFFFF. Every file and every directory gets a real cluster chain terminated with the 0x0FFFFFFF end-of-chain marker. The top four bits of each 32-bit entry are reserved by the specification and are written as zero. Clusters are allocated contiguously from cluster 2 in depth-first order, so a freshly built image has no fragmentation; unallocated entries are left zero. DIRECTORIES The root directory is a cluster chain starting at cluster 2, carrying the volume-label record. Every subdirectory is its own chain and begins with the "." and ".." records; ".." in a first-level directory correctly stores cluster 0 to mean "the root". Timestamps are packed into FAT's 16-bit date (year-1980 << 9 | month << 5 | day) and time (hour << 11 | minute << 5 | seconds/2) words, with the odd-second remainder in the creation-tenths byte. 8.3 SHORT NAMES The real generation algorithm, not a hash. Leading and trailing spaces and leading periods are stripped; the name is uppercased; characters FAT forbids (" * + , / : ; < = > ? [ \ ] | and everything below 0x20 or above 0x7F) are substituted with '_'; embedded spaces and dots are dropped; base and extension are truncated to 8 and 3. If the basis is lossy in any of those ways, or if it collides with a name already used in the same directory, a numeric tail is appended - ~1, then ~2, and so on - with the base truncated to make room, so the twelfth collision on "configuration-NN.ini" comes out as CONFI~12.INI. A name that is already valid 8.3 and already uppercase keeps its short name untouched and gets no long-name entries at all. VFAT LONG NAMES Names that a short name cannot reproduce - which includes anything with lowercase letters - get long-name slots, 13 UTF-16 code units each. They are written in the order the specification requires: on disk the HIGHEST ordinal comes first, flagged 0x40 LAST_LONG_ENTRY, counting down to ordinal 1, immediately followed by the 8.3 record. Every slot carries the OSTA checksum of that 8.3 record - the right-rotate-through-8-bits sum from fatgen103's ChkSum() - so a driver can tell that the slots belong to the entry that follows them. The name is NUL-terminated and the remaining character positions in the last slot are padded with 0xFFFF. MBR One partition entry of type 0x0C (FAT32 with LBA addressing), the start LBA and sector count as 32-bit LBA fields, the CHS fields filled in from the LBA with 255 heads and 63 sectors per track and saturated to 1023/254/63 past the CHS limit, a disk signature at offset 440, and 0x55AA. GPT A protective MBR with a single 0xEE entry covering the disk; a primary header at LBA 1 and a backup header at the last LBA, each with the correct CRC-32 computed over its own 92 bytes with the CRC field zeroed; a 128 x 128 byte partition array at LBA 2 with its own CRC-32, mirrored in the 32 sectors before the backup header; random version-4 disk and partition GUIDs; and a partition entry of type C12A7328-F81F-11D2-BA4B-00A0C93EC93B, the EFI System Partition. The FAT partition is sized so it can never overlap the backup GPT structures. LAYOUT RULES ============ The partition starts at LBA 2048 - one mebibyte in - which is the alignment every modern partitioner uses and which leaves room for the GPT array. Sectors per cluster comes from Microsoft's DSKSZTOSECPERCLUS table for FAT32, reproduced from the specification and applied to the PARTITION size: partition <= 66600 sectors (32.5 MB) FAT32 is not permitted partition <= 532480 sectors (260 MB) 1 sector/cluster 512 B partition <= 16777216 sectors (8 GB) 8 sectors/cluster 4 KiB partition <= 33554432 sectors (16 GB) 16 sectors/cluster 8 KiB partition <= 67108864 sectors (32 GB) 32 sectors/cluster 16 KiB anything larger 64 sectors/cluster 32 KiB The FAT size is then solved to a fixed point: a bigger FAT leaves fewer data clusters, which needs a smaller FAT, so BootBuilder iterates until the FAT is the smallest one that can still address every cluster that fits beside it. Any sectors left over past the last whole cluster are reported as slack. FAT32 is only FAT32 above 65525 clusters - below that the same layout would be FAT16 by definition. BootBuilder refuses such a volume rather than writing something a driver would misread, which in practice sets a floor around 36 MiB. EXIT CODES ========== 0 Success. 1 Bad invocation (unknown command, missing flag, unparseable size), an I/O error, a refusal to overwrite without --force, or a source tree that does not fit the volume. Explicit help exits 0. 2 verify completed and found differences: a missing file, an extra file, a content mismatch or a long name that did not survive. Reserved for "the check ran and the answer is no", as opposed to "the check could not run", which is 1. SCOPE - WHAT THIS DOES NOT DO ============================= Read this section before you assume anything. - IT DOES NOT WRITE TO PHYSICAL USB DEVICES OR ANY OTHER DRIVE. It writes one regular file, at the path you give to --out, and nothing else. There is no raw-device code path in this program at all: the entire import list is ordinary standard-library packages (os, io, encoding, crypto, hash, flag and friends), there is no ioctl, no platform disk API, no os/exec and no networking, and it refuses outright to write to a path that exists and is not a regular file - so pointing --out at /dev/sdb is an error, not a disaster. Getting the image onto a stick is a separate step you take with a separate tool. - IT DOES NOT DELETE OR MODIFY ANY OF YOUR FILES. The source tree is opened read-only. --out is the only path written, and an existing file there is refused unless you pass --force. - IT DOES NOT MAKE A LEGACY-BIOS-BOOTABLE IMAGE. No bootloader is installed. Bytes 90..509 of the boot sector, where boot code lives, are zero, and the MBR contains no boot code and sets no active/bootable flag. A BIOS asked to boot this image will not boot it. - IT DOES NOT MAKE THE IMAGE UEFI-BOOTABLE BY ITSELF EITHER. A UEFI machine boots a FAT volume by loading \EFI\BOOT\BOOTX64.EFI from it. If your source tree already contains EFI/BOOT/BOOTX64.EFI, the image will contain it at the right path and a UEFI machine can boot it. If your source tree does not, nothing here will conjure one - BootBuilder does not ship, download or generate a bootloader. - IT HAS NOT BEEN TESTED BY ACTUALLY BOOTING A MACHINE. The format has been verified by reading it back and by third-party tools (see below). No hardware or virtual machine was booted from an image it produced. Treat "correct FAT32" and "boots your laptop" as separate claims, because they are. - NO NTFS, NO exFAT, NO ext4. FAT32 only. That means no single file larger than 4 GiB - 1 byte, which is a real problem for Windows install media whose sources/install.wim exceeds it. BootBuilder rejects such a file by name with its exact size rather than silently truncating it. Splitting install.wim into install.swm is not implemented. - NO MULTI-PARTITION LAYOUTS. Exactly one partition is written. There is no second NTFS partition, no hybrid MBR/GPT, no ESP-plus-data split. - NO ISO EXTRACTION. Point --src at a directory. If your content is in an ISO, mount or extract it first; WinImageKit is the sibling that concerns itself with image content. - NO SYMLINKS, DEVICE NODES, FIFOS OR SOCKETS. FAT has no representation for them. They are skipped and reported, never followed - so a symlink cannot pull content in from outside the source tree. - NO PERMISSIONS, OWNERSHIP OR TIMESTAMPS BEYOND FAT'S OWN. FAT stores a two-second-resolution local timestamp and a handful of attribute bits. Unix modes, ACLs, extended attributes and sub-second times do not survive, and every file is written with the archive attribute. - NO OEM CODE-PAGE CONVERSION FOR SHORT NAMES. Long names are full UTF-16 and survive exactly. Short names are ASCII; any character above 0x7F in a short name is substituted with '_'. That is a cosmetic difference in the 8.3 alias only, never in the real name. - NO INCREMENTAL UPDATES. There is no "add this file to an existing image". Every build writes a fresh filesystem from scratch. - NO FREE-SPACE REUSE OR FRAGMENTATION HANDLING, because there is nothing to reuse: the volume starts empty and clusters are handed out contiguously. - THE IMAGE FILE IS SPARSE WHERE IT CAN BE. An 8 GB image holding 5 MB of files occupies about 5 MB on a filesystem that supports holes. Copying it with a tool that does not understand sparse files will expand it to its full nominal size. INDEPENDENT VERIFICATION ======================== Round-trip verification inside the program is deliberately two-sided: the reader in fatread.go was written from the specification, separately from the writer, and shares no parsing code with it. `verify` re-hashes every file through the FAT chains and compares SHA-256 against the source tree. That is still one program checking itself, so images produced by BootBuilder were also handed to third-party implementations that had no part in writing them: - fsck.fat (dosfstools 4.2) reports no errors on the produced filesystem and agrees with the reported cluster count and free-cluster summary. - mtools 4.0.43 (minfo, mdir, mcopy) reads the BPB, lists every directory with both short and long names, and extracts every file; the extracted files hash identically to the source tree. - sgdisk (gdisk) validates the GPT - both header CRC32s, the partition-array CRC32, the backup header and the EFI System Partition type code. If those tools are not installed where you are, BootBuilder cannot and does not claim they confirmed anything.