Skip to main content

romfs

PyPI CI Python License

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, and diff subcommands; a bare romfs <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).

Release files for romfs 0.0.6

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for romfs 0.0.6
File Size Uploaded
romfs-0.0.6.tar.gz 33.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for romfs 0.0.6
File Interpreter ABI Platform
romfs-0.0.6-py3-none-any.whl Python 3 none any Details

Total release size: 61.8 kB

Release files / romfs-0.0.6.tar.gz

Download URL romfs-0.0.6.tar.gz
Size 33.5 kB
Tags Source
SHA-256 checksum
How to use checksums
75f315ffc3e7ad951548d7e1842845f69fa08fc2107aafa3942442ee5e4eebcc
BLAKE2b-256 checksum
How to use checksums
837ded4fcd46d1c11f57bd303d1a98c44180ed01eeb5d68b302ae4fec1062847
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.10.12

Release files / romfs-0.0.6-py3-none-any.whl

Download URL romfs-0.0.6-py3-none-any.whl
Size 28.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
995ca5dd3c9566e73b98a117038ac1ddf425de66e130f2214e587c9d0c008e7c
BLAKE2b-256 checksum
How to use checksums
a235d39f3cf90a577dd355322ab3c83bf05e14ce7e87a2e4362903a6b8e20b4b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.10.12

Release history Release notifications | RSS feed

This release

0.0.6 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page