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.