[PATCH v3 13/15] fdtfactor: Add -S/--strip-local-fixups option

From: Douglas Anderson

Date: Sun Nov 01 2026 - 17:04:28 EST


By default, `fdtfactor` preserves `/__local_fixups__` on `base.dtb` so
that when `fdt_overlay_apply()` merges a `.dtbo` overlay, the
reconstructed device tree retains full `/__local_fixups__` information
(for instance, if an OS kernel wants to know which property cells
contain phandles at runtime).

When `-L` was passed to `dtc` solely to help `fdtfactor` locate phandle
cells at build time and `/__local_fixups__` is not needed at runtime,
retaining `/__local_fixups__` in `base.dtb` wastes space.

Add `-S` / `--strip-local-fixups` to omit `/__local_fixups__` from
`base.dtb` (and therefore from the merged device trees produced by
`fdt_overlay_apply()`). On a set of 47 Qualcomm SC7180 Trogdor
Chromebook board DTBs, `fdtfactor` without `-S` reduces total
uncompressed size from 7.69 MB to 932 KB (88.2% savings), while
`fdtfactor -S` reduces it to 634 KB (91.8% savings, saving an additional
298 KB by omitting `/__local_fixups__` from `base.dtb`).

Add a test case in `tests/run_tests.sh` verifying that `fdtfactor -S`
followed by `fdtoverlay` reproduces DTBs compiled with `-@` (without
`-L`) via `dtbs_equal_unordered`.

Assisted-by: Gemini:gemini-pro-prerelease
Signed-off-by: Douglas Anderson <dianders@xxxxxxxxxxxx>
---

Changes in v3:
- Include both non--S (932 KB) and -S (634 KB) Trogdor size numbers in
the commit message to show the savings of -S itself (Simon Glass)
- Pass -S to the negative test verifying that inputs lacking
/__local_fixups__ are still rejected when -S is used, and move the
non--S rejection test to the base fdtfactor patch (Simon Glass)

Changes in v2:
- Split -S/--strip-local-fixups into a separate opt-in patch (David
Gibson)

README.md | 9 +++++++++
fdtfactor | 34 ++++++++++++++++++++++++++--------
tests/run_tests.sh | 18 ++++++++++++++++++
3 files changed, 53 insertions(+), 8 deletions(-)

diff --git a/README.md b/README.md
index 61fefc4424fe..bb6f041e1e78 100644
--- a/README.md
+++ b/README.md
@@ -139,6 +139,15 @@ Options:
* `-o`, `--outdir`: Output directory for generated `.dtbo` files and
`--output-base-dtb` (default: alongside each input `.dtb` for `.dtbo` files,
current directory for `--output-base-dtb`).
+* `-S`, `--strip-local-fixups`: Omit `/__local_fixups__` from the generated
+ `base.dtb` and final reconstructed device trees. By default, `fdtfactor`
+ preserves `/__local_fixups__` on `base.dtb` so that when `fdt_overlay_apply()`
+ merges a `.dtbo` overlay, the reconstructed device tree retains full
+ `/__local_fixups__` information (e.g., for an OS kernel that wants to
+ know which property cells contain phandles at runtime). When `-L` was
+ passed to `dtc` solely to help `fdtfactor` locate phandles at build time
+ and `/__local_fixups__` is not needed at runtime, passing `-S` strips it
+ from `base.dtb` to save space.
* `-s`, `--stats`: Print size savings summary.

### TODO
diff --git a/fdtfactor b/fdtfactor
index f6944e36a0f2..7442a2597399 100755
--- a/fdtfactor
+++ b/fdtfactor
@@ -442,7 +442,8 @@ def _build_base_tree(dtbs):
return base_root


-def _serialize_base_dtb(base_root, base_dtb_symbols, rsvmap, boot_cpuid_phys):
+def _serialize_base_dtb(base_root, base_dtb_symbols, rsvmap, boot_cpuid_phys,
+ strip_local_fixups=False):
"""Materialize phandles, `/__symbols__`, and `/__local_fixups__` and flatten.

Args:
@@ -451,6 +452,8 @@ def _serialize_base_dtb(base_root, base_dtb_symbols, rsvmap, boot_cpuid_phys):
base_dtb_symbols: Dict mapping common symbol labels to target paths.
rsvmap: Sequence of `(address, size)` memory reservation tuples.
boot_cpuid_phys: Physical boot CPU ID for the FDT header.
+ strip_local_fixups: If True, omit `/__local_fixups__` from the
+ generated `base.dtb`.

Returns:
Serialized `.dtb` `bytes` for the common base DTB.
@@ -485,11 +488,12 @@ def _serialize_base_dtb(base_root, base_dtb_symbols, rsvmap, boot_cpuid_phys):
for label, target_path in base_dtb_symbols.items():
sym_node.set_str(label, target_path or '/')

- # Create `/__local_fixups__` on the base DTB so `fdt_overlay_apply()`
- # updates it with the overlay's phandle fixups when merging.
- lf_root = out_root.create_path('/__local_fixups__')
- for path, prop_name, offs in lf_entries:
- lf_root.create_path(path).set_u32_list(prop_name, offs)
+ if not strip_local_fixups:
+ # Create `/__local_fixups__` on the base DTB so `fdt_overlay_apply()`
+ # updates it with the overlay's phandle fixups when merging.
+ lf_root = out_root.create_path('/__local_fixups__')
+ for path, prop_name, offs in lf_entries:
+ lf_root.create_path(path).set_u32_list(prop_name, offs)

fdt = out_root.flatten(mem_rsv=rsvmap, boot_cpuid_phys=boot_cpuid_phys)
return bytes(fdt.as_bytearray())
@@ -638,11 +642,13 @@ def _build_board_dtbo(dtb, base_root, base_dtb_symbols,
return bytes(dtbo_root.flatten().as_bytearray())


-def create_base_dtb_and_overlays(dtbs):
+def create_base_dtb_and_overlays(dtbs, strip_local_fixups=False):
"""Compute the base DTB and per-board DTBO blobs from parsed DTBs.

Args:
dtbs: Sequence of `ParsedDtb` instances (at least two).
+ strip_local_fixups: If True, omit `/__local_fixups__` from the
+ generated `base.dtb`.

Returns:
Tuple of `(base_dtb_blob, dtbo_blobs)` where `base_dtb_blob` is the
@@ -657,6 +663,7 @@ def create_base_dtb_and_overlays(dtbs):
base_dtb_blob = _serialize_base_dtb(
base_root, base_dtb_symbols,
dtbs[0].mem_rsv, dtbs[0].boot_cpuid_phys,
+ strip_local_fixups=strip_local_fixups,
)
dtbo_blobs = [
_build_board_dtbo(
@@ -719,6 +726,14 @@ def parse_args(argv=None):
'current directory for --output-base-dtb)'
),
)
+ parser.add_argument(
+ '-S', '--strip-local-fixups',
+ action='store_true',
+ help=(
+ 'Omit /__local_fixups__ from the generated base .dtb and '
+ 'reconstructed device trees'
+ ),
+ )
parser.add_argument(
'-s', '--stats',
action='store_true',
@@ -803,7 +818,10 @@ def factor_dtb_files(args):
seen_paths.add(abs_out)
dtbo_paths.append(out_path)

- base_dtb_blob, dtbo_blobs = create_base_dtb_and_overlays(dtbs)
+ base_dtb_blob, dtbo_blobs = create_base_dtb_and_overlays(
+ dtbs,
+ strip_local_fixups=args.strip_local_fixups,
+ )

if args.outdir:
os.makedirs(args.outdir, exist_ok=True)
diff --git a/tests/run_tests.sh b/tests/run_tests.sh
index 166a662708a5..e5ac38dd2bab 100755
--- a/tests/run_tests.sh
+++ b/tests/run_tests.sh
@@ -1243,6 +1243,24 @@ fdtfactor_tests () {
done
run_wrap_error_test $PYTHON $FDTFACTOR -o fdtfactor_legacy.test.dir \
fdtfactor_board1.nolf.test.dtb fdtfactor_board2.test.dtb
+
+ # Strip local fixups (-S): verify that merged DTBs match DTBs compiled
+ # with -@ (without -L), and that inputs lacking /__local_fixups__ are
+ # still rejected even when -S is passed.
+ run_wrap_error_test $PYTHON $FDTFACTOR -S -o fdtfactor_strip.test.dir \
+ fdtfactor_board1.nolf.test.dtb fdtfactor_board2.test.dtb
+ rm -rf fdtfactor_strip.test.dir
+ run_wrap_test $PYTHON $FDTFACTOR -S --output-base-dtb base_strip.test.dtb \
+ -o fdtfactor_strip.test.dir \
+ fdtfactor_board1.test.dtb fdtfactor_board2.test.dtb \
+ fdtfactor_board3.test.dtb
+ for i in 1 2 3; do
+ run_wrap_test $FDTOVERLAY -i fdtfactor_strip.test.dir/base_strip.test.dtb \
+ -o fdtfactor_strip.test.dir/merged${i}.test.dtb \
+ fdtfactor_strip.test.dir/fdtfactor_board${i}.test.dtbo
+ run_test dtbs_equal_unordered fdtfactor_board${i}.nolf.test.dtb \
+ fdtfactor_strip.test.dir/merged${i}.test.dtb
+ done
}

while getopts "vt:me" ARG ; do
--
2.56.0.317.g454f8302e1-goog