[RFC PATCH 4/7] arm64: boot: Support building image.fit from a specified FIT_ITS script

From: Douglas Anderson

Date: Tue Oct 20 2026 - 11:44:53 EST


Allow passing `FIT_ITS=<path>` on the make command line (relative to
either `$(srctree)` or `arch/arm64/boot/dts/`) when building `image.fit`
to assemble the FIT image from a checked-in `.its` script via `mkimage`
instead of auto-generating a FIT image with `scripts/make_fit.py`. When
`FIT_ITS` is not specified, `image.fit` continues to be built using
`scripts/make_fit.py` as before.

Supporting a checked-in `.its` script includes:
- Adding `cmd_lz4_frame` (`Image-framed.lz4` and `%.dtb.lz4`) for
standard LZ4 frame compression (magic `0x184d2204`) expected by FIT
bootloaders such as U-Boot and depthcharge, plus generic compressed
DTB rules (`bz2`, `gz`, `lz4`, `lzma`, `lzo`, `zst`, `xz`) in
`scripts/Makefile.dtbs`.
- Restricting `$(dtb-y)` to the DTBs referenced via `/incbin/(...)` in
`$(FIT_ITS)` and automatically building the referenced kernel image
target (e.g. `Image-framed.lz4`) and any compressed `.dtb.<ext>`
blobs.
- Populating any omitted `compatible` property on
`/configurations/conf-*` nodes in `cmd_fit_its` from the
corresponding uncompressed `.dtb`'s root `/compatible` property
using `fdtget` and `fdtput`. This is useful so we don't have to
manually keep the .its script in sync with the source. This is extra
important if compression is used since the bootloader may not be
able to easily peek into the device trees itself.

The main motivation for checking in an .its script instead of using an
auto-generated one is to be able to manually set the "description"
field of the configurations. This allows us to provide information that
the bootloader can use to figure out which configuration(s) to load.
This idea came from Michal Simek.

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

Makefile | 2 +-
arch/arm64/Makefile | 3 ++
arch/arm64/boot/Makefile | 9 ++++--
scripts/Makefile.dtbs | 43 ++++++++++++++++++++++++++++-
scripts/Makefile.lib | 59 ++++++++++++++++++++++++++++++++++++++++
5 files changed, 111 insertions(+), 5 deletions(-)

diff --git a/Makefile b/Makefile
index 0dc1a9f4e63e..8c5500179e2e 100644
--- a/Makefile
+++ b/Makefile
@@ -2239,7 +2239,7 @@ clean: $(clean-dirs)
\( -name '*.[aios]' -o -name '*.rsi' -o -name '*.ko' -o -name '.*.cmd' \
-o -name '*.ko.*' -o -name '*.o.thinlto.bc' \
-o -name '*.dtb' -o -name '*.dtbo' \
- -o -name '*.dtb.S' -o -name '*.dtbo.S' \
+ -o -name '*.dtb.*' -o -name '*.dtbo.S' \
-o -name '*.dt.yaml' -o -name 'dtbs-list' \
-o -name '*.dwo' -o -name '*.lst' \
-o -name '*.su' -o -name '*.mod' \
diff --git a/arch/arm64/Makefile b/arch/arm64/Makefile
index 6b005c8fef70..f9a25db9ae03 100644
--- a/arch/arm64/Makefile
+++ b/arch/arm64/Makefile
@@ -185,6 +185,9 @@ $(BOOT_TARGETS): vmlinux
Image.%: Image
$(Q)$(MAKE) $(build)=$(boot) $(boot)/$@

+Image-framed.%: Image
+ $(Q)$(MAKE) $(build)=$(boot) $(boot)/$@
+
ifeq ($(CONFIG_COMPRESSED_INSTALL),y)
DEFAULT_KBUILD_IMAGE = $(KBUILD_IMAGE)
else
diff --git a/arch/arm64/boot/Makefile b/arch/arm64/boot/Makefile
index b5a08333bc57..fe21bace3692 100644
--- a/arch/arm64/boot/Makefile
+++ b/arch/arm64/boot/Makefile
@@ -16,8 +16,8 @@

OBJCOPYFLAGS_Image :=-O binary -R .note -R .note.gnu.build-id -R .comment -S

-targets := Image Image.bz2 Image.gz Image.lz4 Image.lzma Image.lzo \
- Image.zst Image.xz image.fit
+targets := Image Image.bz2 Image.gz Image.lz4 Image-framed.lz4 Image.lzma \
+ Image.lzo Image.zst Image.xz image.fit

$(obj)/Image: vmlinux FORCE
$(call if_changed,objcopy)
@@ -31,6 +31,9 @@ $(obj)/Image.gz: $(obj)/Image FORCE
$(obj)/Image.lz4: $(obj)/Image FORCE
$(call if_changed,lz4)

+$(obj)/Image-framed.lz4: $(obj)/Image FORCE
+ $(call if_changed,lz4_frame)
+
$(obj)/Image.lzma: $(obj)/Image FORCE
$(call if_changed,lzma)

@@ -44,7 +47,7 @@ $(obj)/Image.xz: $(obj)/Image FORCE
$(call if_changed,xzkern)

$(obj)/image.fit: $(obj)/Image $(obj)/dts/dtbs-list FORCE
- $(call if_changed,fit)
+ $(if $(FIT_ITS_FILE),$(call if_changed,fit_its),$(call if_changed,fit))

EFI_ZBOOT_PAYLOAD := Image
EFI_ZBOOT_BFD_TARGET := elf64-littleaarch64
diff --git a/scripts/Makefile.dtbs b/scripts/Makefile.dtbs
index c4e466390284..1bc42b30cc52 100644
--- a/scripts/Makefile.dtbs
+++ b/scripts/Makefile.dtbs
@@ -5,6 +5,26 @@ all-dtb := $(dtb-y) $(dtb-)
# If CONFIG_OF_ALL_DTBS is enabled, all DT blobs are built
dtb-$(CONFIG_OF_ALL_DTBS) += $(dtb-)

+# If FIT_ITS is specified, restrict $(dtb-y) to only the DTBs referenced via
+# /incbin/(...) in $(FIT_ITS_FILE) and also build any compressed .dtb.<ext>
+# blobs referenced there alongside their base .dtb.
+ifdef FIT_ITS_FILE
+ifneq ($(filter arch/$(SRCARCH)/boot/dts%, $(obj)),)
+its-dtb-files := $(notdir $(shell $(call fit_its_incbin,\.dtb)))
+
+# Strip compression and .dtb suffixes to get stem names (e.g. "foo", "bar"),
+# then intersect with the stems in this directory's $(all-dtb).
+its-dtb-stems := $(patsubst %.dtb, %, \
+ $(filter %.dtb, $(its-dtb-files) $(basename $(its-dtb-files))))
+its-local-stems := $(filter $(patsubst %.dtb, %, $(all-dtb)), $(its-dtb-stems))
+
+# Build only the matching base .dtb files and any compressed .dtb.<ext> variants
+dtb-y := $(addsuffix .dtb, $(its-local-stems))
+its-dtb-comp := $(addprefix $(obj)/, \
+ $(foreach d, $(its-local-stems), $(filter $(d).dtb.%, $(its-dtb-files))))
+endif
+endif
+
# Composite DTB (i.e. DTB constructed by overlay)
multi-dtb-y := $(call multi-search, $(dtb-y), .dtb, -dtbs)
# Primitive DTB compiled from *.dts
@@ -23,7 +43,7 @@ dtb-y := $(addprefix $(obj)/, $(dtb-y))
multi-dtb-y := $(addprefix $(obj)/, $(multi-dtb-y))
real-dtb-y := $(addprefix $(obj)/, $(real-dtb-y))

-always-y += $(dtb-y)
+always-y += $(dtb-y) $(its-dtb-comp)
targets += $(real-dtb-y)

# dtbs-list
@@ -142,6 +162,27 @@ $(obj)/%.dtb: $(obj)/%.dts $(DTC) $(DT_TMP_SCHEMA) FORCE
$(obj)/%.dtbo: $(src)/%.dtso $(DTC) FORCE
$(call if_changed_dep,dtc)

+$(obj)/%.dtb.bz2: $(obj)/%.dtb FORCE
+ $(call if_changed,bzip2)
+
+$(obj)/%.dtb.gz: $(obj)/%.dtb FORCE
+ $(call if_changed,gzip)
+
+$(obj)/%.dtb.lz4: $(obj)/%.dtb FORCE
+ $(call if_changed,lz4_frame)
+
+$(obj)/%.dtb.lzma: $(obj)/%.dtb FORCE
+ $(call if_changed,lzma)
+
+$(obj)/%.dtb.lzo: $(obj)/%.dtb FORCE
+ $(call if_changed,lzo)
+
+$(obj)/%.dtb.zst: $(obj)/%.dtb FORCE
+ $(call if_changed,zstd)
+
+$(obj)/%.dtb.xz: $(obj)/%.dtb FORCE
+ $(call if_changed,xzmisc)
+
# targets
# ---------------------------------------------------------------------------

diff --git a/scripts/Makefile.lib b/scripts/Makefile.lib
index 0a4fdd8bd975..91880648fe21 100644
--- a/scripts/Makefile.lib
+++ b/scripts/Makefile.lib
@@ -383,9 +383,16 @@ quiet_cmd_lzo = LZO $@
quiet_cmd_lzo_with_size = LZO $@
cmd_lzo_with_size = { cat $(real-prereqs) | $(KLZOP) -9; $(size_append); } > $@

+# cmd_lz4 uses -l (legacy format, magic 0x184c2102) required by the kernel's
+# in-tree decompressor (lib/decompress_unlz4.c). Bootloaders that decompress
+# FIT images (such as U-Boot and depthcharge) require the standard LZ4 frame
+# format (magic 0x184d2204, without -l), provided by cmd_lz4_frame.
quiet_cmd_lz4 = LZ4 $@
cmd_lz4 = cat $(real-prereqs) | $(LZ4) -l -9 - - > $@

+quiet_cmd_lz4_frame = LZ4 $@
+ cmd_lz4_frame = $(LZ4) -12 -z -f -q $< $@
+
quiet_cmd_lz4_with_size = LZ4 $@
cmd_lz4_with_size = { cat $(real-prereqs) | $(LZ4) -l -9 - -; \
$(size_append); } > $@
@@ -429,6 +436,58 @@ quiet_cmd_fit = FIT $@
$(if $(FIT_DECOMPOSE_DTBS),--decompose-dtbs) \
--compress $(FIT_COMPRESSION) -k $< @$(word 2,$^)

+# If FIT_ITS is specified on the command line (either relative to $(srctree)
+# or relative to arch/$(SRCARCH)/boot/dts/), locate that .its script and
+# define a helper to extract /incbin/("...") paths from it.
+ifdef FIT_ITS
+FIT_ITS_FILE := $(firstword $(wildcard \
+ $(FIT_ITS) \
+ $(srctree)/$(FIT_ITS) \
+ $(srctree)/arch/$(SRCARCH)/boot/dts/$(FIT_ITS)))
+ifeq ($(FIT_ITS_FILE),)
+$(error FIT_ITS '$(FIT_ITS)' not found)
+endif
+
+# Extract /incbin/("...") path(s) from $(FIT_ITS_FILE).
+# $(1): optional pattern to match inside the /incbin/("...") path
+# $(2): optional image node name to restrict the search to
+fit_its_incbin = sed -n \
+ "$(if $(2),/^[[:space:]]*$(2)[[:space:]]*{/$(comma)/\/incbin\//) \
+ s|.*/incbin/(\"\([^\"]*$(1)[^\"]*\)\").*|\1|p" \
+ $(FIT_ITS_FILE)
+
+ifneq ($(filter image.fit,$(targets)),)
+FIT_ITS_IMAGES := $(addprefix $(obj)/, \
+ $(notdir $(shell $(call fit_its_incbin,Image))))
+FIT_ITS_DTBS := $(addprefix $(objtree)/, \
+ $(shell $(call fit_its_incbin,\.dtb)))
+$(obj)/image.fit: $(FIT_ITS_IMAGES) $(FIT_ITS_DTBS) $(FIT_ITS_FILE)
+endif
+endif
+
+# Build the FIT image from the .its script with mkimage and populate any
+# missing 'compatible' property on /configurations/conf-* nodes from the
+# corresponding uncompressed .dtb's root '/compatible' property using
+# fdtget/fdtput.
+quiet_cmd_fit_its = FIT $@
+ cmd_fit_its = \
+ PATH="$(abspath $(objtree))/scripts/dtc:$(PATH)"; \
+ $(BASH) $(MKIMAGE) \
+ -D "-I dts -O dtb -i $(objtree) -i $(srctree)" \
+ -f $(FIT_ITS_FILE) $@; \
+ configs=$$(fdtget -l $@ /configurations); \
+ for c in $$configs; do \
+ conf=/configurations/$$c; \
+ if [ -n "$$(fdtget -d '' $@ $$conf compatible)" ]; then \
+ continue; \
+ fi; \
+ fdt=$$(fdtget $@ $$conf fdt); \
+ inc=$$($(call fit_its_incbin,\.dtb,$$fdt)); \
+ dtb="$(objtree)/$${inc%.dtb*}.dtb"; \
+ compat=$$(fdtget -ts "$$dtb" / compatible); \
+ fdtput -ts $@ $$conf compatible $$compat; \
+ done
+
# XZ
# ---------------------------------------------------------------------------
# Use xzkern or xzkern_with_size to compress the kernel image and xzmisc to
--
2.56.0.212.g129ede20d9-goog