Pure-Python reader and writer for Linux romfs (-rom1fs-) filesystem images.
Project description
romfs
Pure-Python reader and writer for Linux romfs (
-rom1fs-) filesystem images. Standard library only, no runtime dependencies.
romfs lets you build, inspect, and unpack the small read-only filesystem
images the Linux kernel uses for initrds and embedded root filesystems. Images
produced by the writer are byte-compatible with mount -t romfs and
interchangeable with genromfs; the reader parses any such image, including
real genromfs output with ./.. self-links and dedup hardlinks.
Features
- Read an image into a node tree; list entries and read file contents
lazily via
mmap— large images don't blow up memory. - Write an image from a local directory tree, preserving the structure, regular files, and symbolic links.
- CLI with
list,unpack,pack, anddiffsubcommands; a bareromfs <image>prints header info and the per-entry space breakdown. - Importable library API for embedding in other tools.
- Byte-exact conformance with the kernel romfs format: 16-byte alignment, big-endian, superblock + header checksums.
- Entries sorted by name for a deterministic, diff-friendly layout.
[!NOTE] The writer lays out entries sorted by name, so two versions of the same resource tree produce byte-stable images — keeping binary diffs (delta packages) small.
Installation
pip install romfs
Run from source with no install:
./main.py <args>
Quick start
# Pack a folder tree into a single romfs image
romfs pack ./rootfs -o rootfs.img -n myvol
# Print header info + per-entry space breakdown for an image
# (an image path with no subcommand is shorthand for this)
romfs rootfs.img
romfs rootfs.img --verify # also verify the superblock checksum
# List the file tree (type, size, path)
romfs list rootfs.img
# Unpack one file to stdout, or everything to a directory
romfs unpack rootfs.img etc/init.d/rcS
romfs unpack rootfs.img -o ./out
# Compare two images file-by-file (exit 0 if identical, 1 if different)
romfs diff old.img new.img
romfs diff old.img new.img --text # also print a line-level diff for changed text files
diff output is a five-column table — symbol | old file | new file | size delta
| detail — framed by dash rules. Columns 2/3 hold the path on each side (blank =
absent), so an addition (blank old cell) or removal (blank new cell) is visible at
a glance; column 4 is the signed byte delta. The column headers are the two image
basenames. A leading ~ volume row appears when the volume name differs:
~ volume "v1" -> "v2"
old.img new.img SIZE DETAIL
-------------------------------------------------------
+ bin/newtool +12345B exec
~ etc/init.d/rcS etc/init.d/rcS -4B exec (same size, content differs)
- etc/old.conf -89B
~ init init - rcS -> rc.local
~ lib/libfoo.so lib/libfoo.so 0B (same size, content differs)
= var/run var/run - f->l
-------------------------------------------------------
TOTAL +1 added -1 removed ~4 changed =1 type-changed
image: A=65536B B=65536B
disk: A=131072B B=131072B
size: +12345B -93B -> +12252B
The far-left symbol is + added (in new only), - removed (in old only), ~
changed (both sides), = type-changed (both sides); a blank path cell on either
side means that image lacks the entry. The SIZE column is the signed byte delta
(+N grew, -N shrank, 0B unchanged size); - means size does not apply
(symlink / type-change / volume). The DETAIL column carries the executable bit (exec, or exec:true->false when
it flips), dir for directories, symlink targets (-> target or old -> new),
the type pair for a type-change (f->l), and the (same size, content differs)
flag — regular files of equal size are compared byte-for-byte, so same-size
content edits are surfaced explicitly. The trailing footer reports three
independent size bases on separate lines so they are not conflated: image:
is each image's full_size (the romfs superblock's declared volume size —
header, node metadata, payload, 16-byte alignment); disk: is the on-disk
file size, which may exceed full_size when the file was truncate-padded to
a flash-erase-block boundary; size: is the signed byte delta of
regular-file payload only (+XB appeared, -YB disappeared, -> the signed
net). size: does NOT equal disk_b - disk_a, because image size also
includes header, metadata, and alignment — which is why the three are split.
CLI reference
romfs <image> [--verify] print header info + per-entry space breakdown
romfs list <image> [--sort S] [--all] list the file tree (size-0 hidden without --all)
romfs unpack <image> [-o OUTDIR] [PATH] unpack all, or one entry to stdout
romfs pack <srcdir> -o <image> [-n NAME] build an image from a directory tree
romfs diff <a.img> <b.img> [--text] compare two images file-by-file (exit 1 if different)
romfs -V prints the version; romfs -h shows help. list --sort orders by
name (default), size (largest first), or none (on-disk order); --all
also shows size-0 entries (directories and empty files). To inspect file
contents, use unpack; the bare form and --verify only print image-level
metadata.
Library API
from romfs import RomFSReader, RomFSWriter, EntryType, diff_images
# Read an image
with RomFSReader("rootfs.img") as r:
print(r.volume_name, r.full_size)
for path, node in r.walk():
print(f"{node.type.name:9} {node.size:>8} {path}")
# File contents are sliced from the mmap on demand
data = r.read(r.root.children[0])
# Path-based access: find a node, read its payload, list a directory.
node = r.find("etc/init.d/rcS") # None if absent
if node is not None:
print(node.path, node.executable)
print(r.read_path("etc/init.d/rcS"))
print([n.name for n in r.listdir("etc")])
# Build an image from a directory tree
RomFSWriter.from_directory("./rootfs", volume_name="myvol").write("out.img")
# Compare two images: added / removed / changed / type-changed paths.
# Regular files of equal size are compared byte-for-byte, so same-size
# content edits are caught, not just size changes.
with RomFSReader("old.img") as a, RomFSReader("new.img") as b:
for entry in diff_images(a, b):
print(entry.status, entry.path)
Supported entry types
| Type | Read | Write |
|---|---|---|
| Regular file | yes | yes |
| Directory | yes | yes |
| Symlink | yes | yes |
| Hardlink / device / socket / fifo | yes | no |
[!NOTE] romfs stores only the executable bit (no full Unix mode), so the kernel assigns fixed default modes for the rest. The writer preserves the exec bit from the source file (set when any execute bit is on, matching
genromfs); other permission bits are not preserved. Hardlinks and special files are parsed on read but not emitted on write.
Development
pip install -e ".[dev]" # pytest, black, ruff, mypy, build, twine
pytest # runs against src/ directly, no install needed
black src tests main.py # format
ruff check src tests # lint
mypy src # type check
python -m build # build sdist + wheel into dist/
CI runs black + ruff + mypy + pytest across Python 3.10-3.13, plus a packaging job that builds the wheel and tests the installed package. Releases publish to PyPI automatically on a GitHub Release via Trusted Publishing (OIDC).
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file romfs-0.0.6.tar.gz.
File metadata
- Download URL: romfs-0.0.6.tar.gz
- Upload date:
- Size: 33.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
75f315ffc3e7ad951548d7e1842845f69fa08fc2107aafa3942442ee5e4eebcc
|
|
| MD5 |
9efe87dd26a3b58cc61a8e775600e851
|
|
| BLAKE2b-256 |
837ded4fcd46d1c11f57bd303d1a98c44180ed01eeb5d68b302ae4fec1062847
|
File details
Details for the file romfs-0.0.6-py3-none-any.whl.
File metadata
- Download URL: romfs-0.0.6-py3-none-any.whl
- Upload date:
- Size: 28.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
995ca5dd3c9566e73b98a117038ac1ddf425de66e130f2214e587c9d0c008e7c
|
|
| MD5 |
d04468809de58b7798861a07a0b2cccd
|
|
| BLAKE2b-256 |
a235d39f3cf90a577dd355322ab3c83bf05e14ce7e87a2e4362903a6b8e20b4b
|