diff mbox series

[v7,2/2] arm64: boot: Support Flat Image Tree

Message ID 20231129172200.430674-3-sjg@chromium.org (mailing list archive)
State New, archived
Headers show
Series arm64: Add a build target for Flat Image Tree | expand

Commit Message

Simon Glass Nov. 29, 2023, 5:21 p.m. UTC
Add a script which produces a Flat Image Tree (FIT), a single file
containing the built kernel and associated devicetree files.
Compression defaults to gzip which gives a good balance of size and
performance.

The files compress from about 86MB to 24MB using this approach.

The FIT can be used by bootloaders which support it, such as U-Boot
and Linuxboot. It permits automatic selection of the correct
devicetree, matching the compatible string of the running board with
the closest compatible string in the FIT. There is no need for
filenames or other workarounds.

Add a 'make image.fit' build target for arm64, as well.

The FIT can be examined using 'dumpimage -l'.

This features requires pylibfdt (use 'pip install libfdt'). It also
requires compression utilities for the algorithm being used. Supported
compression options are the same as the Image.xxx files. For now there
is no way to change the compression other than by editing the rule for
$(obj)/image.fit

While FIT supports a ramdisk / initrd, no attempt is made to support
this here, since it must be built separately from the Linux build.

Signed-off-by: Simon Glass <sjg@chromium.org>
---

Changes in v7:
- Add Image as a dependency of image.fit
- Drop kbuild tag
- Add dependency on dtbs
- Drop unnecessary path separator for dtbs
- Rebase to -next

Changes in v5:
- Drop patch previously applied
- Correct compression rule which was broken in v4

Changes in v4:
- Use single quotes for UIMAGE_NAME

Changes in v3:
- Drop temporary file image.itk
- Drop patch 'Use double quotes for image name'
- Drop double quotes in use of UIMAGE_NAME
- Drop unnecessary CONFIG_EFI_ZBOOT condition for help
- Avoid hard-coding "arm64" for the DT architecture

Changes in v2:
- Drop patch previously applied
- Add .gitignore file
- Move fit rule to Makefile.lib using an intermediate file
- Drop dependency on CONFIG_EFI_ZBOOT
- Pick up .dtb files separately from the kernel
- Correct pylint too-many-args warning for write_kernel()
- Include the kernel image in the file count
- Add a pointer to the FIT spec and mention of its wide industry usage
- Mention the kernel version in the FIT description

 MAINTAINERS                |   7 +
 arch/arm64/Makefile        |   9 +-
 arch/arm64/boot/.gitignore |   1 +
 arch/arm64/boot/Makefile   |   6 +-
 scripts/Makefile.lib       |  13 ++
 scripts/make_fit.py        | 289 +++++++++++++++++++++++++++++++++++++
 6 files changed, 322 insertions(+), 3 deletions(-)
 create mode 100755 scripts/make_fit.py

Comments

Ahmad Fatoum Nov. 29, 2023, 6:35 p.m. UTC | #1
Hello Simon,

On 29.11.23 18:21, Simon Glass wrote:
> Add a script which produces a Flat Image Tree (FIT), a single file
> containing the built kernel and associated devicetree files.
> Compression defaults to gzip which gives a good balance of size and
> performance.

Thanks for working on this. I think it's useful to have the kernel
generate a FIT image out of the box. More complex use cases are always
free to call mkimage with a custom ITS.

 
> The files compress from about 86MB to 24MB using this approach.
> 
> The FIT can be used by bootloaders which support it, such as U-Boot
> and Linuxboot. It permits automatic selection of the correct
> devicetree, matching the compatible string of the running board with
> the closest compatible string in the FIT. There is no need for
> filenames or other workarounds.
> 
> Add a 'make image.fit' build target for arm64, as well.

not that it matters much, but should this maybe called Image.fit
as the other Image types are capitalized too?

>  EFI_ZBOOT_PAYLOAD	:= Image
>  EFI_ZBOOT_BFD_TARGET	:= elf64-littleaarch64
>  EFI_ZBOOT_MACH_TYPE	:= ARM64
> diff --git a/scripts/Makefile.lib b/scripts/Makefile.lib
> index 1a965fe68e01..e1c06ca3c847 100644
> --- a/scripts/Makefile.lib
> +++ b/scripts/Makefile.lib
> @@ -496,6 +496,19 @@ quiet_cmd_uimage = UIMAGE  $@
>  			-a $(UIMAGE_LOADADDR) -e $(UIMAGE_ENTRYADDR) \
>  			-n '$(UIMAGE_NAME)' -d $< $@

Doesn't hardcoding a load address and entry address here defeat the point
of having FIT as generic portable image format?

At least barebox will try to place the kernel image at physical address 0 and
will exit with an error message if no SDRAM is located at that address.
The recommendation in that case is to omit load and entry address altogether
to have barebox find a suitable location, but I see now that the FIT specification
requires a load and entry address. What would happen if U-Boot tries to load this
FIT image on a board that has no DRAM at address 0?

Please Cc me on subsequent revisions. I am interested in testing that this works for barebox
too.

Thanks,
Ahmad
Ahmad Fatoum Nov. 29, 2023, 6:59 p.m. UTC | #2
Hi,

a few more comments after decompiling the FIT image:

On 29.11.23 18:21, Simon Glass wrote:
> +    with fsw.add_node('kernel'):
> +        fsw.property_string('description', args.name)
> +        fsw.property_string('type', 'kernel_noload')

The specification only says no loading done, but doesn't explain what it
means for a bootloader to _not_ load an image. Looking into the U-Boot commit
b9b50e89d317 ("image: Implement IH_TYPE_KERNEL_NOLOAD") that introduces this,
apparently no loading means ignoring load and entry address?

I presume missing load and entry is something older U-Boot versions
were unhappy about? Please let me know if the semantics are as I understood,
so I can prepare a barebox patch supporting it.

> +        fsw.property_string('arch', args.arch)
> +        fsw.property_string('os', args.os)
> +        fsw.property_string('compression', args.compress)
> +        fsw.property('data', data)
> +        fsw.property_u32('load', 0)
> +        fsw.property_u32('entry', 0)
> +
> +
> +def finish_fit(fsw, entries):
> +    """Finish the FIT ready for use
> +
> +    Writes the /configurations node and subnodes
> +
> +    Args:
> +        fsw (libfdt.FdtSw): Object to use for writing
> +        entries (list of tuple): List of configurations:
> +            str: Description of model
> +            str: Compatible stringlist
> +    """
> +    fsw.end_node()
> +    seq = 0
> +    with fsw.add_node('configurations'):
> +        for model, compat in entries:
> +            seq += 1
> +            with fsw.add_node(f'conf-{seq}'):
> +                fsw.property('compatible', bytes(compat))

The specification says that this is the root U-Boot compatible,
which I presume to mean the top-level compatible, which makes sense to me.

The code here though adds all compatible strings from the device tree though,
is this intended?

> +        fsw.property_string('description', model)
> +        fsw.property_string('type', 'flat_dt')
> +        fsw.property_string('arch', arch)
> +        fsw.property_string('compression', compress)
> +        fsw.property('compatible', bytes(compat))

I think I've never seen a compatible for a fdt node before.
What use does this serve?

Cheers,
Ahmad
Simon Glass Nov. 29, 2023, 7 p.m. UTC | #3
Hi Ahmad,

On Wed, 29 Nov 2023 at 11:35, Ahmad Fatoum <a.fatoum@pengutronix.de> wrote:
>
> Hello Simon,
>
> On 29.11.23 18:21, Simon Glass wrote:
> > Add a script which produces a Flat Image Tree (FIT), a single file
> > containing the built kernel and associated devicetree files.
> > Compression defaults to gzip which gives a good balance of size and
> > performance.
>
> Thanks for working on this. I think it's useful to have the kernel
> generate a FIT image out of the box. More complex use cases are always
> free to call mkimage with a custom ITS.
>
>
> > The files compress from about 86MB to 24MB using this approach.
> >
> > The FIT can be used by bootloaders which support it, such as U-Boot
> > and Linuxboot. It permits automatic selection of the correct
> > devicetree, matching the compatible string of the running board with
> > the closest compatible string in the FIT. There is no need for
> > filenames or other workarounds.
> >
> > Add a 'make image.fit' build target for arm64, as well.
>
> not that it matters much, but should this maybe called Image.fit
> as the other Image types are capitalized too?
>
> >  EFI_ZBOOT_PAYLOAD    := Image
> >  EFI_ZBOOT_BFD_TARGET := elf64-littleaarch64
> >  EFI_ZBOOT_MACH_TYPE  := ARM64
> > diff --git a/scripts/Makefile.lib b/scripts/Makefile.lib
> > index 1a965fe68e01..e1c06ca3c847 100644
> > --- a/scripts/Makefile.lib
> > +++ b/scripts/Makefile.lib
> > @@ -496,6 +496,19 @@ quiet_cmd_uimage = UIMAGE  $@
> >                       -a $(UIMAGE_LOADADDR) -e $(UIMAGE_ENTRYADDR) \
> >                       -n '$(UIMAGE_NAME)' -d $< $@
>
> Doesn't hardcoding a load address and entry address here defeat the point
> of having FIT as generic portable image format?
>
> At least barebox will try to place the kernel image at physical address 0 and
> will exit with an error message if no SDRAM is located at that address.
> The recommendation in that case is to omit load and entry address altogether
> to have barebox find a suitable location, but I see now that the FIT specification
> requires a load and entry address. What would happen if U-Boot tries to load this
> FIT image on a board that has no DRAM at address 0?

The 'kernel_noload' type indicates that the load/exec address are ignored.

>
> Please Cc me on subsequent revisions. I am interested in testing that this works for barebox
> too.

There has been some discussion about this recently in U-Boot too,
along with a series [1] which you could try if you like.

The FIT spec[2] does not provide enough detail on exactly what
kernel_noload means and we should improve this at some point.

Regards,
Simon

[1] https://patchwork.ozlabs.org/project/uboot/list/?series=382849
[2] https://github.com/open-source-firmware/flat-image-tree



>
> Thanks,
> Ahmad
>
> --
> Pengutronix e.K.                           |                             |
> Steuerwalder Str. 21                       | http://www.pengutronix.de/  |
> 31137 Hildesheim, Germany                  | Phone: +49-5121-206917-0    |
> Amtsgericht Hildesheim, HRA 2686           | Fax:   +49-5121-206917-5555 |
>
Tom Rini Nov. 29, 2023, 7:02 p.m. UTC | #4
On Wed, Nov 29, 2023 at 07:59:00PM +0100, Ahmad Fatoum wrote:
> Hi,
> 
> a few more comments after decompiling the FIT image:
> 
> On 29.11.23 18:21, Simon Glass wrote:
> > +    with fsw.add_node('kernel'):
> > +        fsw.property_string('description', args.name)
> > +        fsw.property_string('type', 'kernel_noload')
> 
> The specification only says no loading done, but doesn't explain what it
> means for a bootloader to _not_ load an image. Looking into the U-Boot commit
> b9b50e89d317 ("image: Implement IH_TYPE_KERNEL_NOLOAD") that introduces this,
> apparently no loading means ignoring load and entry address?
> 
> I presume missing load and entry is something older U-Boot versions
> were unhappy about? Please let me know if the semantics are as I understood,
> so I can prepare a barebox patch supporting it.

So the matching side for this series in U-Boot is:
https://patchwork.ozlabs.org/project/uboot/list/?series=382849&state=*

And in short, for IH_TYPE_KERNEL_NOLOAD we do our best to use it
in-place. For decompression we allocate some space to decompress to.
Simon Glass Nov. 29, 2023, 7:02 p.m. UTC | #5
Hi Ahmad,

On Wed, 29 Nov 2023 at 11:59, Ahmad Fatoum <a.fatoum@pengutronix.de> wrote:
>
> Hi,
>
> a few more comments after decompiling the FIT image:
>
> On 29.11.23 18:21, Simon Glass wrote:
> > +    with fsw.add_node('kernel'):
> > +        fsw.property_string('description', args.name)
> > +        fsw.property_string('type', 'kernel_noload')
>
> The specification only says no loading done, but doesn't explain what it
> means for a bootloader to _not_ load an image. Looking into the U-Boot commit
> b9b50e89d317 ("image: Implement IH_TYPE_KERNEL_NOLOAD") that introduces this,
> apparently no loading means ignoring load and entry address?
>
> I presume missing load and entry is something older U-Boot versions
> were unhappy about? Please let me know if the semantics are as I understood,
> so I can prepare a barebox patch supporting it.

Oh, see my previous email.

>
> > +        fsw.property_string('arch', args.arch)
> > +        fsw.property_string('os', args.os)
> > +        fsw.property_string('compression', args.compress)
> > +        fsw.property('data', data)
> > +        fsw.property_u32('load', 0)
> > +        fsw.property_u32('entry', 0)
> > +
> > +
> > +def finish_fit(fsw, entries):
> > +    """Finish the FIT ready for use
> > +
> > +    Writes the /configurations node and subnodes
> > +
> > +    Args:
> > +        fsw (libfdt.FdtSw): Object to use for writing
> > +        entries (list of tuple): List of configurations:
> > +            str: Description of model
> > +            str: Compatible stringlist
> > +    """
> > +    fsw.end_node()
> > +    seq = 0
> > +    with fsw.add_node('configurations'):
> > +        for model, compat in entries:
> > +            seq += 1
> > +            with fsw.add_node(f'conf-{seq}'):
> > +                fsw.property('compatible', bytes(compat))
>
> The specification says that this is the root U-Boot compatible,
> which I presume to mean the top-level compatible, which makes sense to me.
>
> The code here though adds all compatible strings from the device tree though,
> is this intended?

Yes, since it saves needing to read in each DT just to get the
compatible stringlist.

>
> > +        fsw.property_string('description', model)
> > +        fsw.property_string('type', 'flat_dt')
> > +        fsw.property_string('arch', arch)
> > +        fsw.property_string('compression', compress)
> > +        fsw.property('compatible', bytes(compat))
>
> I think I've never seen a compatible for a fdt node before.
> What use does this serve?

It indicates the machine that the DT is for.

Regards,
Simon
Ahmad Fatoum Nov. 29, 2023, 7:14 p.m. UTC | #6
Hello Simon,

On 29.11.23 20:02, Simon Glass wrote:
> Hi Ahmad,
> 
> On Wed, 29 Nov 2023 at 11:59, Ahmad Fatoum <a.fatoum@pengutronix.de> wrote:
>>
>> Hi,
>>
>> a few more comments after decompiling the FIT image:
>>
>> On 29.11.23 18:21, Simon Glass wrote:
>>> +    with fsw.add_node('kernel'):
>>> +        fsw.property_string('description', args.name)
>>> +        fsw.property_string('type', 'kernel_noload')
>>
>> The specification only says no loading done, but doesn't explain what it
>> means for a bootloader to _not_ load an image. Looking into the U-Boot commit
>> b9b50e89d317 ("image: Implement IH_TYPE_KERNEL_NOLOAD") that introduces this,
>> apparently no loading means ignoring load and entry address?
>>
>> I presume missing load and entry is something older U-Boot versions
>> were unhappy about? Please let me know if the semantics are as I understood,
>> so I can prepare a barebox patch supporting it.
> 
> Oh, see my previous email.

Thanks.

> 
>>
>>> +        fsw.property_string('arch', args.arch)
>>> +        fsw.property_string('os', args.os)
>>> +        fsw.property_string('compression', args.compress)
>>> +        fsw.property('data', data)
>>> +        fsw.property_u32('load', 0)
>>> +        fsw.property_u32('entry', 0)
>>> +
>>> +
>>> +def finish_fit(fsw, entries):
>>> +    """Finish the FIT ready for use
>>> +
>>> +    Writes the /configurations node and subnodes
>>> +
>>> +    Args:
>>> +        fsw (libfdt.FdtSw): Object to use for writing
>>> +        entries (list of tuple): List of configurations:
>>> +            str: Description of model
>>> +            str: Compatible stringlist
>>> +    """
>>> +    fsw.end_node()
>>> +    seq = 0
>>> +    with fsw.add_node('configurations'):
>>> +        for model, compat in entries:
>>> +            seq += 1
>>> +            with fsw.add_node(f'conf-{seq}'):
>>> +                fsw.property('compatible', bytes(compat))
>>
>> The specification says that this is the root U-Boot compatible,
>> which I presume to mean the top-level compatible, which makes sense to me.
>>
>> The code here though adds all compatible strings from the device tree though,
>> is this intended?
> 
> Yes, since it saves needing to read in each DT just to get the
> compatible stringlist.

The spec reads as if only one string (root) is supposed to be in the list.
The script adds all compatibles though. This is not really useful as a bootloader
that's compatible with e.g. fsl,imx8mm would just take the first device tree
with that SoC, which is most likely to be wrong. It would be better to just
specify the top-level compatible, so the bootloader fails instead of taking
the first DT it finds.

>>> +        fsw.property_string('description', model)
>>> +        fsw.property_string('type', 'flat_dt')
>>> +        fsw.property_string('arch', arch)
>>> +        fsw.property_string('compression', compress)
>>> +        fsw.property('compatible', bytes(compat))
>>
>> I think I've never seen a compatible for a fdt node before.
>> What use does this serve?
> 
> It indicates the machine that the DT is for.

Who makes use of this information?

Thanks,
Ahmad

> 
> Regards,
> Simon
>
Ahmad Fatoum Nov. 29, 2023, 7:16 p.m. UTC | #7
Hello Tom,

On 29.11.23 20:02, Tom Rini wrote:
> On Wed, Nov 29, 2023 at 07:59:00PM +0100, Ahmad Fatoum wrote:
>> Hi,
>>
>> a few more comments after decompiling the FIT image:
>>
>> On 29.11.23 18:21, Simon Glass wrote:
>>> +    with fsw.add_node('kernel'):
>>> +        fsw.property_string('description', args.name)
>>> +        fsw.property_string('type', 'kernel_noload')
>>
>> The specification only says no loading done, but doesn't explain what it
>> means for a bootloader to _not_ load an image. Looking into the U-Boot commit
>> b9b50e89d317 ("image: Implement IH_TYPE_KERNEL_NOLOAD") that introduces this,
>> apparently no loading means ignoring load and entry address?
>>
>> I presume missing load and entry is something older U-Boot versions
>> were unhappy about? Please let me know if the semantics are as I understood,
>> so I can prepare a barebox patch supporting it.
> 
> So the matching side for this series in U-Boot is:
> https://patchwork.ozlabs.org/project/uboot/list/?series=382849&state=*
> 
> And in short, for IH_TYPE_KERNEL_NOLOAD we do our best to use it
> in-place. For decompression we allocate some space to decompress to.

Thanks. I am still curious why "kernel" couldn't have been used back then
with missing entry and load address to arrive at the same result?

Thanks,
Ahmad
Simon Glass Nov. 29, 2023, 7:27 p.m. UTC | #8
Hi Ahmad,

On Wed, 29 Nov 2023 at 12:15, Ahmad Fatoum <a.fatoum@pengutronix.de> wrote:
>
> Hello Simon,
>
> On 29.11.23 20:02, Simon Glass wrote:
> > Hi Ahmad,
> >
> > On Wed, 29 Nov 2023 at 11:59, Ahmad Fatoum <a.fatoum@pengutronix.de> wrote:
> >>
> >> Hi,
> >>
> >> a few more comments after decompiling the FIT image:
> >>
> >> On 29.11.23 18:21, Simon Glass wrote:
> >>> +    with fsw.add_node('kernel'):
> >>> +        fsw.property_string('description', args.name)
> >>> +        fsw.property_string('type', 'kernel_noload')
> >>
> >> The specification only says no loading done, but doesn't explain what it
> >> means for a bootloader to _not_ load an image. Looking into the U-Boot commit
> >> b9b50e89d317 ("image: Implement IH_TYPE_KERNEL_NOLOAD") that introduces this,
> >> apparently no loading means ignoring load and entry address?
> >>
> >> I presume missing load and entry is something older U-Boot versions
> >> were unhappy about? Please let me know if the semantics are as I understood,
> >> so I can prepare a barebox patch supporting it.
> >
> > Oh, see my previous email.
>
> Thanks.
>
> >
> >>
> >>> +        fsw.property_string('arch', args.arch)
> >>> +        fsw.property_string('os', args.os)
> >>> +        fsw.property_string('compression', args.compress)
> >>> +        fsw.property('data', data)
> >>> +        fsw.property_u32('load', 0)
> >>> +        fsw.property_u32('entry', 0)
> >>> +
> >>> +
> >>> +def finish_fit(fsw, entries):
> >>> +    """Finish the FIT ready for use
> >>> +
> >>> +    Writes the /configurations node and subnodes
> >>> +
> >>> +    Args:
> >>> +        fsw (libfdt.FdtSw): Object to use for writing
> >>> +        entries (list of tuple): List of configurations:
> >>> +            str: Description of model
> >>> +            str: Compatible stringlist
> >>> +    """
> >>> +    fsw.end_node()
> >>> +    seq = 0
> >>> +    with fsw.add_node('configurations'):
> >>> +        for model, compat in entries:
> >>> +            seq += 1
> >>> +            with fsw.add_node(f'conf-{seq}'):
> >>> +                fsw.property('compatible', bytes(compat))
> >>
> >> The specification says that this is the root U-Boot compatible,
> >> which I presume to mean the top-level compatible, which makes sense to me.
> >>
> >> The code here though adds all compatible strings from the device tree though,
> >> is this intended?
> >
> > Yes, since it saves needing to read in each DT just to get the
> > compatible stringlist.
>
> The spec reads as if only one string (root) is supposed to be in the list.
> The script adds all compatibles though. This is not really useful as a bootloader
> that's compatible with e.g. fsl,imx8mm would just take the first device tree
> with that SoC, which is most likely to be wrong. It would be better to just
> specify the top-level compatible, so the bootloader fails instead of taking
> the first DT it finds.

We do need to have a list, since we have to support different board revs, etc.

>
> >>> +        fsw.property_string('description', model)
> >>> +        fsw.property_string('type', 'flat_dt')
> >>> +        fsw.property_string('arch', arch)
> >>> +        fsw.property_string('compression', compress)
> >>> +        fsw.property('compatible', bytes(compat))
> >>
> >> I think I've never seen a compatible for a fdt node before.
> >> What use does this serve?
> >
> > It indicates the machine that the DT is for.
>
> Who makes use of this information?

U-Boot uses it, I believe. There is an optimisation to use this
instead of reading the DT itself.

Regards,
Simon
Ahmad Fatoum Nov. 29, 2023, 7:33 p.m. UTC | #9
On 29.11.23 20:27, Simon Glass wrote:
> On Wed, 29 Nov 2023 at 12:15, Ahmad Fatoum <a.fatoum@pengutronix.de> wrote:
>> On 29.11.23 20:02, Simon Glass wrote:
>>> On Wed, 29 Nov 2023 at 11:59, Ahmad Fatoum <a.fatoum@pengutronix.de> wrote:
>>>> The specification says that this is the root U-Boot compatible,
>>>> which I presume to mean the top-level compatible, which makes sense to me.
>>>>
>>>> The code here though adds all compatible strings from the device tree though,
>>>> is this intended?
>>>
>>> Yes, since it saves needing to read in each DT just to get the
>>> compatible stringlist.
>>
>> The spec reads as if only one string (root) is supposed to be in the list.
>> The script adds all compatibles though. This is not really useful as a bootloader
>> that's compatible with e.g. fsl,imx8mm would just take the first device tree
>> with that SoC, which is most likely to be wrong. It would be better to just
>> specify the top-level compatible, so the bootloader fails instead of taking
>> the first DT it finds.
> 
> We do need to have a list, since we have to support different board revs, etc.

Can you give me an example? The way I see it, a bootloader with
compatible "vendor,board" and a FIT with configuration with compatibles:

  "vendor,board-rev-a", "vendor,board"
  "vendor,board-rev-b", "vendor,board"

would just result in the bootloader booting the first configuration, even if
the device is actually rev-b.


>>>>> +        fsw.property_string('description', model)
>>>>> +        fsw.property_string('type', 'flat_dt')
>>>>> +        fsw.property_string('arch', arch)
>>>>> +        fsw.property_string('compression', compress)
>>>>> +        fsw.property('compatible', bytes(compat))
>>>>
>>>> I think I've never seen a compatible for a fdt node before.
>>>> What use does this serve?
>>>
>>> It indicates the machine that the DT is for.
>>
>> Who makes use of this information?
> 
> U-Boot uses it, I believe. There is an optimisation to use this
> instead of reading the DT itself.

The configuration already has a compatible entry. What extra use is the compatible
entry in the FDT node?

Thanks,
Ahmad

> 
> Regards,
> Simon
>
Simon Glass Nov. 29, 2023, 7:44 p.m. UTC | #10
Hi Ahmad,

On Wed, 29 Nov 2023 at 12:33, Ahmad Fatoum <a.fatoum@pengutronix.de> wrote:
>
> On 29.11.23 20:27, Simon Glass wrote:
> > On Wed, 29 Nov 2023 at 12:15, Ahmad Fatoum <a.fatoum@pengutronix.de> wrote:
> >> On 29.11.23 20:02, Simon Glass wrote:
> >>> On Wed, 29 Nov 2023 at 11:59, Ahmad Fatoum <a.fatoum@pengutronix.de> wrote:
> >>>> The specification says that this is the root U-Boot compatible,
> >>>> which I presume to mean the top-level compatible, which makes sense to me.
> >>>>
> >>>> The code here though adds all compatible strings from the device tree though,
> >>>> is this intended?
> >>>
> >>> Yes, since it saves needing to read in each DT just to get the
> >>> compatible stringlist.
> >>
> >> The spec reads as if only one string (root) is supposed to be in the list.
> >> The script adds all compatibles though. This is not really useful as a bootloader
> >> that's compatible with e.g. fsl,imx8mm would just take the first device tree
> >> with that SoC, which is most likely to be wrong. It would be better to just
> >> specify the top-level compatible, so the bootloader fails instead of taking
> >> the first DT it finds.
> >
> > We do need to have a list, since we have to support different board revs, etc.
>
> Can you give me an example? The way I see it, a bootloader with
> compatible "vendor,board" and a FIT with configuration with compatibles:
>
>   "vendor,board-rev-a", "vendor,board"
>   "vendor,board-rev-b", "vendor,board"
>
> would just result in the bootloader booting the first configuration, even if
> the device is actually rev-b.

You need to find the best match, not just any match. This is
documented in the function comment for fit_conf_find_compat().

>
>
> >>>>> +        fsw.property_string('description', model)
> >>>>> +        fsw.property_string('type', 'flat_dt')
> >>>>> +        fsw.property_string('arch', arch)
> >>>>> +        fsw.property_string('compression', compress)
> >>>>> +        fsw.property('compatible', bytes(compat))
> >>>>
> >>>> I think I've never seen a compatible for a fdt node before.
> >>>> What use does this serve?
> >>>
> >>> It indicates the machine that the DT is for.
> >>
> >> Who makes use of this information?
> >
> > U-Boot uses it, I believe. There is an optimisation to use this
> > instead of reading the DT itself.
>
> The configuration already has a compatible entry. What extra use is the compatible
> entry in the FDT node?

It allows seeing the compatible stringlist without having to read the
FDT itself. I don't believe it is necessary though, so long as we are
scanning the configurations and not the FDT nodes.

Regards,
Simon
Ahmad Fatoum Nov. 29, 2023, 7:53 p.m. UTC | #11
Hello Simon,

On 29.11.23 20:44, Simon Glass wrote:
> Hi Ahmad,
> 
> On Wed, 29 Nov 2023 at 12:33, Ahmad Fatoum <a.fatoum@pengutronix.de> wrote:
>>
>> On 29.11.23 20:27, Simon Glass wrote:
>>> On Wed, 29 Nov 2023 at 12:15, Ahmad Fatoum <a.fatoum@pengutronix.de> wrote:
>>>> On 29.11.23 20:02, Simon Glass wrote:
>>>>> On Wed, 29 Nov 2023 at 11:59, Ahmad Fatoum <a.fatoum@pengutronix.de> wrote:
>>>>>> The specification says that this is the root U-Boot compatible,
>>>>>> which I presume to mean the top-level compatible, which makes sense to me.
>>>>>>
>>>>>> The code here though adds all compatible strings from the device tree though,
>>>>>> is this intended?
>>>>>
>>>>> Yes, since it saves needing to read in each DT just to get the
>>>>> compatible stringlist.
>>>>
>>>> The spec reads as if only one string (root) is supposed to be in the list.
>>>> The script adds all compatibles though. This is not really useful as a bootloader
>>>> that's compatible with e.g. fsl,imx8mm would just take the first device tree
>>>> with that SoC, which is most likely to be wrong. It would be better to just
>>>> specify the top-level compatible, so the bootloader fails instead of taking
>>>> the first DT it finds.
>>>
>>> We do need to have a list, since we have to support different board revs, etc.
>>
>> Can you give me an example? The way I see it, a bootloader with
>> compatible "vendor,board" and a FIT with configuration with compatibles:
>>
>>   "vendor,board-rev-a", "vendor,board"
>>   "vendor,board-rev-b", "vendor,board"
>>
>> would just result in the bootloader booting the first configuration, even if
>> the device is actually rev-b.
> 
> You need to find the best match, not just any match. This is
> documented in the function comment for fit_conf_find_compat().

In my above example, both configuration are equally good.
Can you give me an example where it makes sense to have multiple
compatibles automatically extracted from the device tree compatible?

The way I see it having more than one compatible here just has
downsides.

>> The configuration already has a compatible entry. What extra use is the compatible
>> entry in the FDT node?
> 
> It allows seeing the compatible stringlist without having to read the
> FDT itself. I don't believe it is necessary though, so long as we are
> scanning the configurations and not the FDT nodes.

I think it's better to drop this if it has no use.

Cheers,
Ahmad

> 
> Regards,
> Simon
>
Tom Rini Nov. 29, 2023, 8:10 p.m. UTC | #12
On Wed, Nov 29, 2023 at 08:16:20PM +0100, Ahmad Fatoum wrote:
> Hello Tom,
> 
> On 29.11.23 20:02, Tom Rini wrote:
> > On Wed, Nov 29, 2023 at 07:59:00PM +0100, Ahmad Fatoum wrote:
> >> Hi,
> >>
> >> a few more comments after decompiling the FIT image:
> >>
> >> On 29.11.23 18:21, Simon Glass wrote:
> >>> +    with fsw.add_node('kernel'):
> >>> +        fsw.property_string('description', args.name)
> >>> +        fsw.property_string('type', 'kernel_noload')
> >>
> >> The specification only says no loading done, but doesn't explain what it
> >> means for a bootloader to _not_ load an image. Looking into the U-Boot commit
> >> b9b50e89d317 ("image: Implement IH_TYPE_KERNEL_NOLOAD") that introduces this,
> >> apparently no loading means ignoring load and entry address?
> >>
> >> I presume missing load and entry is something older U-Boot versions
> >> were unhappy about? Please let me know if the semantics are as I understood,
> >> so I can prepare a barebox patch supporting it.
> > 
> > So the matching side for this series in U-Boot is:
> > https://patchwork.ozlabs.org/project/uboot/list/?series=382849&state=*
> > 
> > And in short, for IH_TYPE_KERNEL_NOLOAD we do our best to use it
> > in-place. For decompression we allocate some space to decompress to.
> 
> Thanks. I am still curious why "kernel" couldn't have been used back then
> with missing entry and load address to arrive at the same result?

Some level or another of historical oversight, yeah.
Ahmad Fatoum Nov. 29, 2023, 8:35 p.m. UTC | #13
Hi Simon,

On 29.11.23 20:00, Simon Glass wrote:
> On Wed, 29 Nov 2023 at 11:35, Ahmad Fatoum <a.fatoum@pengutronix.de> wrote:
>> Doesn't hardcoding a load address and entry address here defeat the point
>> of having FIT as generic portable image format?
>>
>> At least barebox will try to place the kernel image at physical address 0 and
>> will exit with an error message if no SDRAM is located at that address.
>> The recommendation in that case is to omit load and entry address altogether
>> to have barebox find a suitable location, but I see now that the FIT specification
>> requires a load and entry address. What would happen if U-Boot tries to load this
>> FIT image on a board that has no DRAM at address 0?
> 
> The 'kernel_noload' type indicates that the load/exec address are ignored.

Can the script not insert load/exec addresses with dummy values to avoid confusion?

>> Please Cc me on subsequent revisions. I am interested in testing that this works for barebox
>> too.
> 
> There has been some discussion about this recently in U-Boot too,
> along with a series [1] which you could try if you like.

Thanks for the pointer. I have just sent out a first patch to add support
for kernel_noload to the barebox mailing list[1]. With that change applied,
barebox can boot the FIT images generated by this series.

Once that's accepted, I'll reply with a Tested-by.

[1]: https://lore.barebox.org/barebox/20231129203106.2417486-1-a.fatoum@pengutronix.de/T/#u

> The FIT spec[2] does not provide enough detail on exactly what
> kernel_noload means and we should improve this at some point.

Yes, that would be nice. Also straight references to e.g. U-Boot configuration
symbols could use some rewording.

Thanks,
Ahmad

> 
> Regards,
> Simon
> 
> [1] https://patchwork.ozlabs.org/project/uboot/list/?series=382849
> [2] https://github.com/open-source-firmware/flat-image-tree
> 
> 
> 
>>
>> Thanks,
>> Ahmad
>>
>> --
>> Pengutronix e.K.                           |                             |
>> Steuerwalder Str. 21                       | http://www.pengutronix.de/  |
>> 31137 Hildesheim, Germany                  | Phone: +49-5121-206917-0    |
>> Amtsgericht Hildesheim, HRA 2686           | Fax:   +49-5121-206917-5555 |
>>
>
Nicolas Schier Nov. 30, 2023, 8:20 a.m. UTC | #14
Simon,

thanks for the patch!  Below are some nitpicks and bike-shedding 
questions.

On Wed 29 Nov 2023 10:21:53 GMT, Simon Glass wrote:
> Add a script which produces a Flat Image Tree (FIT), a single file
> containing the built kernel and associated devicetree files.
> Compression defaults to gzip which gives a good balance of size and
> performance.
> 
> The files compress from about 86MB to 24MB using this approach.
> 
> The FIT can be used by bootloaders which support it, such as U-Boot
> and Linuxboot. It permits automatic selection of the correct
> devicetree, matching the compatible string of the running board with
> the closest compatible string in the FIT. There is no need for
> filenames or other workarounds.

Have you thought about updating the arch/mips ITB rules to also use the 
new scripts/make_fit.py?  Or is the FIT/ITB format for mips different 
from the one for arm64?

> Add a 'make image.fit' build target for arm64, as well.
> 
> The FIT can be examined using 'dumpimage -l'.
> 
> This features requires pylibfdt (use 'pip install libfdt'). It also
> requires compression utilities for the algorithm being used. Supported
> compression options are the same as the Image.xxx files. For now there
> is no way to change the compression other than by editing the rule for
> $(obj)/image.fit
> 
> While FIT supports a ramdisk / initrd, no attempt is made to support
> this here, since it must be built separately from the Linux build.
> 
> Signed-off-by: Simon Glass <sjg@chromium.org>
> ---
> 
> Changes in v7:
> - Add Image as a dependency of image.fit
> - Drop kbuild tag
> - Add dependency on dtbs
> - Drop unnecessary path separator for dtbs
> - Rebase to -next
> 
> Changes in v5:
> - Drop patch previously applied
> - Correct compression rule which was broken in v4
> 
> Changes in v4:
> - Use single quotes for UIMAGE_NAME
> 
> Changes in v3:
> - Drop temporary file image.itk
> - Drop patch 'Use double quotes for image name'
> - Drop double quotes in use of UIMAGE_NAME
> - Drop unnecessary CONFIG_EFI_ZBOOT condition for help
> - Avoid hard-coding "arm64" for the DT architecture
> 
> Changes in v2:
> - Drop patch previously applied
> - Add .gitignore file
> - Move fit rule to Makefile.lib using an intermediate file
> - Drop dependency on CONFIG_EFI_ZBOOT
> - Pick up .dtb files separately from the kernel
> - Correct pylint too-many-args warning for write_kernel()
> - Include the kernel image in the file count
> - Add a pointer to the FIT spec and mention of its wide industry usage
> - Mention the kernel version in the FIT description
> 
>  MAINTAINERS                |   7 +
>  arch/arm64/Makefile        |   9 +-
>  arch/arm64/boot/.gitignore |   1 +
>  arch/arm64/boot/Makefile   |   6 +-
>  scripts/Makefile.lib       |  13 ++
>  scripts/make_fit.py        | 289 +++++++++++++++++++++++++++++++++++++
>  6 files changed, 322 insertions(+), 3 deletions(-)
>  create mode 100755 scripts/make_fit.py
> 
> diff --git a/MAINTAINERS b/MAINTAINERS
> index 14587be87a33..d609f0e8deb3 100644
> --- a/MAINTAINERS
> +++ b/MAINTAINERS
> @@ -1585,6 +1585,13 @@ F:	Documentation/process/maintainer-soc*.rst
>  F:	arch/arm/boot/dts/Makefile
>  F:	arch/arm64/boot/dts/Makefile
>  
> +ARM64 FIT SUPPORT
> +M:	Simon Glass <sjg@chromium.org>
> +L:	linux-arm-kernel@lists.infradead.org (moderated for non-subscribers)
> +S:	Maintained
> +F:	arch/arm64/boot/Makefile
> +F:	scripts/make_fit.py
> +

I'm afraid that the location does not match the requested sorting, it 
should be right before "ARM64 PORT".

>  ARM ARCHITECTED TIMER DRIVER
>  M:	Mark Rutland <mark.rutland@arm.com>
>  M:	Marc Zyngier <maz@kernel.org>
> diff --git a/arch/arm64/Makefile b/arch/arm64/Makefile
> index 1bd4fae6e806..18e092de7cdb 100644
> --- a/arch/arm64/Makefile
> +++ b/arch/arm64/Makefile
> @@ -36,6 +36,8 @@ ifeq ($(CONFIG_BROKEN_GAS_INST),y)
>  $(warning Detected assembler with broken .inst; disassembly will be unreliable)
>  endif
>  
> +KBUILD_DTBS      := dtbs

Might you want to use tabs here as in the lines below?

> +
>  KBUILD_CFLAGS	+= -mgeneral-regs-only	\
>  		   $(compat_vdso) $(cc_has_k_constraint)
>  KBUILD_CFLAGS	+= $(call cc-disable-warning, psabi)
> @@ -150,7 +152,7 @@ libs-$(CONFIG_EFI_STUB) += $(objtree)/drivers/firmware/efi/libstub/lib.a
>  # Default target when executing plain make
>  boot		:= arch/arm64/boot
>  
> -BOOT_TARGETS	:= Image vmlinuz.efi
> +BOOT_TARGETS	:= Image vmlinuz.efi image.fit
>  
>  PHONY += $(BOOT_TARGETS)
>  
> @@ -162,7 +164,9 @@ endif
>  
>  all:	$(notdir $(KBUILD_IMAGE))
>  
> -vmlinuz.efi: Image
> +image.fit: $(KBUILD_DTBS)
> +
> +vmlinuz.efi image.fit: Image
>  $(BOOT_TARGETS): vmlinux
>  	$(Q)$(MAKE) $(build)=$(boot) $(boot)/$@
>  
> @@ -215,6 +219,7 @@ virtconfig:
>  define archhelp
>    echo  '* Image.gz      - Compressed kernel image (arch/$(ARCH)/boot/Image.gz)'
>    echo  '  Image         - Uncompressed kernel image (arch/$(ARCH)/boot/Image)'
> +  echo  '  image.fit     - Flat Image Tree (arch/$(ARCH)/boot/image.fit)'
>    echo  '  install       - Install uncompressed kernel'
>    echo  '  zinstall      - Install compressed kernel'
>    echo  '                  Install using (your) ~/bin/installkernel or'
> diff --git a/arch/arm64/boot/.gitignore b/arch/arm64/boot/.gitignore
> index af5dc61f8b43..abaae9de1bdd 100644
> --- a/arch/arm64/boot/.gitignore
> +++ b/arch/arm64/boot/.gitignore
> @@ -2,3 +2,4 @@
>  Image
>  Image.gz
>  vmlinuz*
> +image.fit
> diff --git a/arch/arm64/boot/Makefile b/arch/arm64/boot/Makefile
> index 1761f5972443..8d591fda078f 100644
> --- a/arch/arm64/boot/Makefile
> +++ b/arch/arm64/boot/Makefile
> @@ -16,7 +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
> +targets := Image Image.bz2 Image.gz Image.lz4 Image.lzma Image.lzo \
> +	Image.zst image.fit

Do you introduce the first lower-case image.* target by intention?  
(All others have a capital 'I'.)

>  
>  $(obj)/Image: vmlinux FORCE
>  	$(call if_changed,objcopy)
> @@ -39,6 +40,9 @@ $(obj)/Image.lzo: $(obj)/Image FORCE
>  $(obj)/Image.zst: $(obj)/Image FORCE
>  	$(call if_changed,zstd)
>  
> +$(obj)/image.fit: $(obj)/Image FORCE
> +	$(call cmd,fit,gzip)
> +
>  EFI_ZBOOT_PAYLOAD	:= Image
>  EFI_ZBOOT_BFD_TARGET	:= elf64-littleaarch64
>  EFI_ZBOOT_MACH_TYPE	:= ARM64
> diff --git a/scripts/Makefile.lib b/scripts/Makefile.lib
> index 1a965fe68e01..e1c06ca3c847 100644
> --- a/scripts/Makefile.lib
> +++ b/scripts/Makefile.lib
> @@ -496,6 +496,19 @@ quiet_cmd_uimage = UIMAGE  $@
>  			-a $(UIMAGE_LOADADDR) -e $(UIMAGE_ENTRYADDR) \
>  			-n '$(UIMAGE_NAME)' -d $< $@
>  
> +# Flat Image Tree (FIT)
> +# This allows for packaging of a kernel and all devicetrees files, using
> +# compression.
> +# ---------------------------------------------------------------------------
> +
> +MAKE_FIT := $(srctree)/scripts/make_fit.py
> +
> +quiet_cmd_fit = FIT     $@
> +      cmd_fit = $(MAKE_FIT) -f $@ --arch $(UIMAGE_ARCH) --os linux \
> +			--name '$(UIMAGE_NAME)' \
> +			--compress $(UIMAGE_COMPRESSION) -k $< \
> +			$(dir $<)dts

Alternatively, you could use '$(<D)/dts' instead of '$(dir $<)dts' 
(bike-shedding).

> +
>  # XZ
>  # ---------------------------------------------------------------------------
>  # Use xzkern to compress the kernel image and xzmisc to compress other things.
> diff --git a/scripts/make_fit.py b/scripts/make_fit.py
> new file mode 100755
> index 000000000000..e1059825de9c
> --- /dev/null
> +++ b/scripts/make_fit.py
> @@ -0,0 +1,289 @@
> +#!/usr/bin/env python3
> +# SPDX-License-Identifier: GPL-2.0+
> +#
> +# Copyright 2023 Google LLC
> +# Written by Simon Glass <sjg@chromium.org>
> +#
> +
> +"""Build a FIT containing a lot of devicetree files
> +
> +Usage:
> +    make_fit.py -A arm64 -n 'Linux-6.6' -O linux
> +        -f arch/arm64/boot/image.fit -k /tmp/kern/arch/arm64/boot/image.itk
> +        /tmp/kern/arch/arm64/boot/dts/ -E -c gzip
> +
> +Creates a FIT containing the supplied kernel and a directory containing the
> +devicetree files.
> +
> +Use -E to generate an external FIT (where the data is placed after the
> +FIT data structure). This allows parsing of the data without loading
> +the entire FIT.
> +
> +Use -c to compress the data, using bzip2, gzip, lz4, lzma, lzo and
> +zstd

Lost a dot at EOL?

> +
> +The resulting FIT can be booted by bootloaders which support FIT, such
> +as U-Boot, Linuxboot, Tianocore, etc.
> +
> +Note that this tool does not yet support adding a ramdisk / initrd.
> +"""
> +
> +import argparse
> +import collections
> +import os
> +import subprocess
> +import sys
> +import tempfile
> +import time
> +
> +import libfdt
> +
> +
> +# Tool extension and the name of the command-line tools
> +CompTool = collections.namedtuple('CompTool', 'ext,tools')
> +
> +COMP_TOOLS = {
> +    'bzip2': CompTool('.bz2', 'bzip2'),
> +    'gzip': CompTool('.gz', 'pigz,gzip'),
> +    'lz4': CompTool('.lz4', 'lz4'),
> +    'lzma': CompTool('.lzma', 'lzma'),
> +    'lzo': CompTool('.lzo', 'lzop'),
> +    'zstd': CompTool('.zstd', 'zstd'),
> +}
> +
> +def parse_args():
> +    """Parse the program ArgumentParser
> +
> +    Returns:
> +        Namespace object containing the arguments
> +    """
> +    epilog = 'Build a FIT from a directory tree containing .dtb files'
> +    parser = argparse.ArgumentParser(epilog=epilog)
> +    parser.add_argument('-A', '--arch', type=str, required=True,
> +          help='Specifies the architecture')
> +    parser.add_argument('-c', '--compress', type=str, default='none',
> +          help='Specifies the compression')
> +    parser.add_argument('-E', '--external', action='store_true',
> +          help='Convert the FIT to use external data')
> +    parser.add_argument('-n', '--name', type=str, required=True,
> +          help='Specifies the name')
> +    parser.add_argument('-O', '--os', type=str, required=True,
> +          help='Specifies the operating system')
> +    parser.add_argument('-f', '--fit', type=str, required=True,
> +          help='Specifies the output file (.fit)')
> +    parser.add_argument('-k', '--kernel', type=str, required=True,
> +          help='Specifies the (uncompressed) kernel input file (.itk)')
> +    parser.add_argument('srcdir', type=str, nargs='*',
> +          help='Specifies the directory tree that contains .dtb files')
> +
> +    return parser.parse_args()
> +
> +def setup_fit(fsw, name):
> +    """Make a start on writing the FIT
> +
> +    Outputs the root properties and the 'images' node
> +
> +    Args:
> +        fsw (libfdt.FdtSw): Object to use for writing
> +        name (str): Name of kernel image
> +    """
> +    fsw.INC_SIZE = 65536
> +    fsw.finish_reservemap()
> +    fsw.begin_node('')
> +    fsw.property_string('description', f'{name} with devicetree set')
> +    fsw.property_u32('#address-cells', 1)
> +
> +    fsw.property_u32('timestamp', int(time.time()))
> +    fsw.begin_node('images')
> +
> +
> +def write_kernel(fsw, data, args):
> +    """Write out the kernel image
> +
> +    Writes a kernel node along with the required properties
> +
> +    Args:
> +        fsw (libfdt.FdtSw): Object to use for writing
> +        data (bytes): Data to write (possibly compressed)
> +        args (Namespace): Contains necessary strings:
> +            arch: FIT architecture, e.g. 'arm64'
> +            fit_os: Operating Systems, e.g. 'linux'
> +            name: Name of OS, e.g. 'Linux-6.6.0-rc7'
> +            compress: Compression algorithm to use, e.g. 'gzip'
> +    """
> +    with fsw.add_node('kernel'):
> +        fsw.property_string('description', args.name)
> +        fsw.property_string('type', 'kernel_noload')
> +        fsw.property_string('arch', args.arch)
> +        fsw.property_string('os', args.os)
> +        fsw.property_string('compression', args.compress)
> +        fsw.property('data', data)
> +        fsw.property_u32('load', 0)
> +        fsw.property_u32('entry', 0)
> +
> +
> +def finish_fit(fsw, entries):
> +    """Finish the FIT ready for use
> +
> +    Writes the /configurations node and subnodes
> +
> +    Args:
> +        fsw (libfdt.FdtSw): Object to use for writing
> +        entries (list of tuple): List of configurations:
> +            str: Description of model
> +            str: Compatible stringlist
> +    """
> +    fsw.end_node()
> +    seq = 0
> +    with fsw.add_node('configurations'):
> +        for model, compat in entries:
> +            seq += 1
> +            with fsw.add_node(f'conf-{seq}'):
> +                fsw.property('compatible', bytes(compat))
> +                fsw.property_string('description', model)
> +                fsw.property_string('fdt', f'fdt-{seq}')
> +                fsw.property_string('kernel', 'kernel')
> +    fsw.end_node()
> +
> +
> +def compress_data(inf, compress):
> +    """Compress data using a selected algorithm
> +
> +    Args:
> +        inf (IOBase): Filename containing the data to compress
> +        compress (str): Compression algorithm, e.g. 'gzip'
> +
> +    Return:
> +        bytes: Compressed data
> +    """
> +    if compress == 'none':
> +        return inf.read()
> +
> +    comp = COMP_TOOLS.get(compress)
> +    if not comp:
> +        raise ValueError(f"Unknown compression algorithm '{compress}'")
> +
> +    with tempfile.NamedTemporaryFile() as comp_fname:
> +        with open(comp_fname.name, 'wb') as outf:
> +            done = False
> +            for tool in comp.tools.split(','):
> +                try:
> +                    subprocess.call([tool, '-c'], stdin=inf, stdout=outf)
> +                    done = True
> +                    break
> +                except FileNotFoundError:
> +                    pass
> +            if not done:
> +                raise ValueError(f'Missing tool(s): {comp.tools}\n')
> +            with open(comp_fname.name, 'rb') as compf:
> +                comp_data = compf.read()
> +    return comp_data
> +
> +
> +def output_dtb(fsw, seq, fname, arch, compress):
> +    """Write out a single devicetree to the FIT
> +
> +    Args:
> +        fsw (libfdt.FdtSw): Object to use for writing
> +        seq (int): Sequence number (1 for first)
> +        fmame (str): Filename containing the DTB
> +        arch: FIT architecture, e.g. 'arm64'
> +        compress (str): Compressed algorithm, e.g. 'gzip'
> +
> +    Returns:
> +        tuple:
> +            str: Model name
> +            bytes: Compatible stringlist
> +    """
> +    with fsw.add_node(f'fdt-{seq}'):
> +        # Get the compatible / model information
> +        with open(fname, 'rb') as inf:
> +            data = inf.read()
> +        fdt = libfdt.FdtRo(data)
> +        model = fdt.getprop(0, 'model').as_str()
> +        compat = fdt.getprop(0, 'compatible')
> +
> +        fsw.property_string('description', model)
> +        fsw.property_string('type', 'flat_dt')
> +        fsw.property_string('arch', arch)
> +        fsw.property_string('compression', compress)
> +        fsw.property('compatible', bytes(compat))
> +
> +        with open(fname, 'rb') as inf:
> +            compressed = compress_data(inf, compress)
> +        fsw.property('data', compressed)
> +    return model, compat
> +
> +
> +def build_fit(args):
> +    """Build the FIT from the provided files and arguments
> +
> +    Args:
> +        args (Namespace): Program arguments
> +
> +    Returns:
> +        tuple:
> +            bytes: FIT data
> +            int: Number of configurations generated
> +            size: Total uncompressed size of data
> +    """
> +    fsw = libfdt.FdtSw()
> +    setup_fit(fsw, args.name)
> +    seq = 0
> +    size = 0
> +    entries = []
> +
> +    # Handle the kernel
> +    with open(args.kernel, 'rb') as inf:
> +        comp_data = compress_data(inf, args.compress)
> +    size += os.path.getsize(args.kernel)
> +    write_kernel(fsw, comp_data, args)
> +
> +    for path in args.srcdir:
> +        # Handle devicetree files
> +        if os.path.isdir(path):
> +            for dirpath, _, fnames in os.walk(path):
> +                for fname in fnames:
> +                    if os.path.splitext(fname)[1] != '.dtb':
> +                        continue
> +                    pathname = os.path.join(dirpath, fname)
> +                    seq += 1
> +                    size += os.path.getsize(pathname)
> +                    model, compat = output_dtb(fsw, seq, pathname,
> +                                               args.arch, args.compress)
> +                    entries.append([model, compat])
> +
> +    finish_fit(fsw, entries)
> +
> +    # Include the kernel itself in the returned file count
> +    return fsw.as_fdt().as_bytearray(), seq + 1, size
> +
> +
> +def run_make_fit():
> +    """Run the tool's main logic"""
> +    args = parse_args()
> +
> +    out_data, count, size = build_fit(args)
> +    with open(args.fit, 'wb') as outf:
> +        outf.write(out_data)
> +
> +    ext_fit_size = None
> +    if args.external:
> +        mkimage = os.environ.get('MKIMAGE', 'mkimage')

Do we need to add a 'mkimage' line in 
Documentation/process/changes.rst?

Kind regards,
Nicolas


> +        subprocess.check_call([mkimage, '-E', '-F', args.fit],
> +                              stdout=subprocess.DEVNULL)
> +
> +        with open(args.fit, 'rb') as inf:
> +            data = inf.read()
> +        ext_fit = libfdt.FdtRo(data)
> +        ext_fit_size = ext_fit.totalsize()
> +
> +    comp_size = len(out_data)
> +    print(f'FIT size {comp_size:#x}/{comp_size / 1024 / 1024:.1f} MB', end='')
> +    if ext_fit_size:
> +        print(f', header {ext_fit_size:#x}/{ext_fit_size / 1024:.1f} KB', end='')
> +    print(f', {count} files, uncompressed {size / 1024 / 1024:.1f} MB')
> +
> +
> +if __name__ == "__main__":
> +    sys.exit(run_make_fit())
> -- 
> 2.43.0.rc2.451.g8631bc7472-goog
>
Masahiro Yamada Nov. 30, 2023, 3:12 p.m. UTC | #15
On Thu, Nov 30, 2023 at 5:26 PM Nicolas Schier <nicolas@fjasle.eu> wrote:
>
> Simon,
>
> thanks for the patch!  Below are some nitpicks and bike-shedding
> questions.
>
> On Wed 29 Nov 2023 10:21:53 GMT, Simon Glass wrote:
> > Add a script which produces a Flat Image Tree (FIT), a single file
> > containing the built kernel and associated devicetree files.
> > Compression defaults to gzip which gives a good balance of size and
> > performance.
> >
> > The files compress from about 86MB to 24MB using this approach.
> >
> > The FIT can be used by bootloaders which support it, such as U-Boot
> > and Linuxboot. It permits automatic selection of the correct
> > devicetree, matching the compatible string of the running board with
> > the closest compatible string in the FIT. There is no need for
> > filenames or other workarounds.
>
> Have you thought about updating the arch/mips ITB rules to also use the
> new scripts/make_fit.py?  Or is the FIT/ITB format for mips different
> from the one for arm64?



I recommend not touching MIPS at this moment
because this tool simply picks up *.dtb files
that exist under arch/*/boot/dts/, some of which
may be stale files.




Think of this scenario:


[1] Enable CONFIG_ARCH_FOO and build

   foo.dtb

will be created.


[2] Next, disable CONFIG_ARCH_FOO and
    enable CONFIG_ARCH_BAR, and build.

   bar.dtb

will be created.


This script will pick up both foo.dtb and bar.dtb
although foo.dtb is a left-over from the previous build.



Without cleaning, stale *.dtb will accumulate
and unwanted files will be included in image.fit.



Currently, MIPS hard-codes its files.
It always works in a deterministic way.




I do not request Simon to implement everything perfectly
because I know that would require much more effort.

We could do something like modules.order to list out the
dtb files from the current build, but I am not asking for it
in this patchset.



But, you are right. This tool is not arm64-specific at all
(and that is the reason why I think the MAINTAINERS
entry is a little odd)
Perhaps it can be applicable to MIPS after everything
works correctly.







> >  ARM ARCHITECTED TIMER DRIVER
> >  M:   Mark Rutland <mark.rutland@arm.com>
> >  M:   Marc Zyngier <maz@kernel.org>
> > diff --git a/arch/arm64/Makefile b/arch/arm64/Makefile
> > index 1bd4fae6e806..18e092de7cdb 100644
> > --- a/arch/arm64/Makefile
> > +++ b/arch/arm64/Makefile
> > @@ -36,6 +36,8 @@ ifeq ($(CONFIG_BROKEN_GAS_INST),y)
> >  $(warning Detected assembler with broken .inst; disassembly will be unreliable)
> >  endif
> >
> > +KBUILD_DTBS      := dtbs
>
> Might you want to use tabs here as in the lines below?



This should not exist in the first place.


  image.fit: dtbs


is better.









--
Best Regards
Masahiro Yamada
Masahiro Yamada Nov. 30, 2023, 3:38 p.m. UTC | #16
On Thu, Nov 30, 2023 at 2:22 AM Simon Glass <sjg@chromium.org> wrote:
>
> Add a script which produces a Flat Image Tree (FIT), a single file
> containing the built kernel and associated devicetree files.
> Compression defaults to gzip which gives a good balance of size and
> performance.
>
> The files compress from about 86MB to 24MB using this approach.
>
> The FIT can be used by bootloaders which support it, such as U-Boot
> and Linuxboot. It permits automatic selection of the correct
> devicetree, matching the compatible string of the running board with
> the closest compatible string in the FIT. There is no need for
> filenames or other workarounds.
>
> Add a 'make image.fit' build target for arm64, as well.
>
> The FIT can be examined using 'dumpimage -l'.
>
> This features requires pylibfdt (use 'pip install libfdt'). It also
> requires compression utilities for the algorithm being used. Supported
> compression options are the same as the Image.xxx files. For now there
> is no way to change the compression other than by editing the rule for
> $(obj)/image.fit
>
> While FIT supports a ramdisk / initrd, no attempt is made to support
> this here, since it must be built separately from the Linux build.
>
> Signed-off-by: Simon Glass <sjg@chromium.org>
> ---
>
> Changes in v7:
> - Add Image as a dependency of image.fit
> - Drop kbuild tag
> - Add dependency on dtbs
> - Drop unnecessary path separator for dtbs
> - Rebase to -next
>
> Changes in v5:
> - Drop patch previously applied
> - Correct compression rule which was broken in v4
>
> Changes in v4:
> - Use single quotes for UIMAGE_NAME
>
> Changes in v3:
> - Drop temporary file image.itk
> - Drop patch 'Use double quotes for image name'
> - Drop double quotes in use of UIMAGE_NAME
> - Drop unnecessary CONFIG_EFI_ZBOOT condition for help
> - Avoid hard-coding "arm64" for the DT architecture
>
> Changes in v2:
> - Drop patch previously applied
> - Add .gitignore file
> - Move fit rule to Makefile.lib using an intermediate file
> - Drop dependency on CONFIG_EFI_ZBOOT
> - Pick up .dtb files separately from the kernel
> - Correct pylint too-many-args warning for write_kernel()
> - Include the kernel image in the file count
> - Add a pointer to the FIT spec and mention of its wide industry usage
> - Mention the kernel version in the FIT description
>
>  MAINTAINERS                |   7 +
>  arch/arm64/Makefile        |   9 +-
>  arch/arm64/boot/.gitignore |   1 +
>  arch/arm64/boot/Makefile   |   6 +-
>  scripts/Makefile.lib       |  13 ++
>  scripts/make_fit.py        | 289 +++++++++++++++++++++++++++++++++++++
>  6 files changed, 322 insertions(+), 3 deletions(-)
>  create mode 100755 scripts/make_fit.py
>
> diff --git a/MAINTAINERS b/MAINTAINERS
> index 14587be87a33..d609f0e8deb3 100644
> --- a/MAINTAINERS
> +++ b/MAINTAINERS
> @@ -1585,6 +1585,13 @@ F:       Documentation/process/maintainer-soc*.rst
>  F:     arch/arm/boot/dts/Makefile
>  F:     arch/arm64/boot/dts/Makefile
>
> +ARM64 FIT SUPPORT
> +M:     Simon Glass <sjg@chromium.org>
> +L:     linux-arm-kernel@lists.infradead.org (moderated for non-subscribers)
> +S:     Maintained
> +F:     arch/arm64/boot/Makefile
> +F:     scripts/make_fit.py
> +
>  ARM ARCHITECTED TIMER DRIVER
>  M:     Mark Rutland <mark.rutland@arm.com>
>  M:     Marc Zyngier <maz@kernel.org>
> diff --git a/arch/arm64/Makefile b/arch/arm64/Makefile
> index 1bd4fae6e806..18e092de7cdb 100644
> --- a/arch/arm64/Makefile
> +++ b/arch/arm64/Makefile
> @@ -36,6 +36,8 @@ ifeq ($(CONFIG_BROKEN_GAS_INST),y)
>  $(warning Detected assembler with broken .inst; disassembly will be unreliable)
>  endif
>
> +KBUILD_DTBS      := dtbs


Please remove this, and hard-code

 image.fit: dtbs



>
>  $(obj)/Image: vmlinux FORCE
>         $(call if_changed,objcopy)
> @@ -39,6 +40,9 @@ $(obj)/Image.lzo: $(obj)/Image FORCE
>  $(obj)/Image.zst: $(obj)/Image FORCE
>         $(call if_changed,zstd)
>
> +$(obj)/image.fit: $(obj)/Image FORCE
> +       $(call cmd,fit,gzip)


The gzip parameter is not used.
Please do

     $(call cmd,fit)





In the python script, functions are separated with two blank lines,
but there is only one blank line between parse_args() and setup_fit().


I do not mind either way because it does not contain any class,
but please keep consistency.







--
Best Regards
Masahiro Yamada
Simon Glass Nov. 30, 2023, 8:30 p.m. UTC | #17
Hi Ahmad,

On Wed, 29 Nov 2023 at 12:54, Ahmad Fatoum <a.fatoum@pengutronix.de> wrote:
>
> Hello Simon,
>
> On 29.11.23 20:44, Simon Glass wrote:
> > Hi Ahmad,
> >
> > On Wed, 29 Nov 2023 at 12:33, Ahmad Fatoum <a.fatoum@pengutronix.de> wrote:
> >>
> >> On 29.11.23 20:27, Simon Glass wrote:
> >>> On Wed, 29 Nov 2023 at 12:15, Ahmad Fatoum <a.fatoum@pengutronix.de> wrote:
> >>>> On 29.11.23 20:02, Simon Glass wrote:
> >>>>> On Wed, 29 Nov 2023 at 11:59, Ahmad Fatoum <a.fatoum@pengutronix.de> wrote:
> >>>>>> The specification says that this is the root U-Boot compatible,
> >>>>>> which I presume to mean the top-level compatible, which makes sense to me.
> >>>>>>
> >>>>>> The code here though adds all compatible strings from the device tree though,
> >>>>>> is this intended?
> >>>>>
> >>>>> Yes, since it saves needing to read in each DT just to get the
> >>>>> compatible stringlist.
> >>>>
> >>>> The spec reads as if only one string (root) is supposed to be in the list.
> >>>> The script adds all compatibles though. This is not really useful as a bootloader
> >>>> that's compatible with e.g. fsl,imx8mm would just take the first device tree
> >>>> with that SoC, which is most likely to be wrong. It would be better to just
> >>>> specify the top-level compatible, so the bootloader fails instead of taking
> >>>> the first DT it finds.
> >>>
> >>> We do need to have a list, since we have to support different board revs, etc.
> >>
> >> Can you give me an example? The way I see it, a bootloader with
> >> compatible "vendor,board" and a FIT with configuration with compatibles:
> >>
> >>   "vendor,board-rev-a", "vendor,board"
> >>   "vendor,board-rev-b", "vendor,board"
> >>
> >> would just result in the bootloader booting the first configuration, even if
> >> the device is actually rev-b.
> >
> > You need to find the best match, not just any match. This is
> > documented in the function comment for fit_conf_find_compat().
>
> In my above example, both configuration are equally good.
> Can you give me an example where it makes sense to have multiple
> compatibles automatically extracted from the device tree compatible?
>
> The way I see it having more than one compatible here just has
> downsides.

I don't have an example to hand, but this is the required mechanism of
FIT. This feature has been in place for many years and is used by
ChromeOS, at least.

>
> >> The configuration already has a compatible entry. What extra use is the compatible
> >> entry in the FDT node?
> >
> > It allows seeing the compatible stringlist without having to read the
> > FDT itself. I don't believe it is necessary though, so long as we are
> > scanning the configurations and not the FDT nodes.
>
> I think it's better to drop this if it has no use.

OK. I cannot think of a use for it.

Regards,
Simon
Simon Glass Nov. 30, 2023, 8:30 p.m. UTC | #18
Hi Ahmad,

On Wed, 29 Nov 2023 at 11:35, Ahmad Fatoum <a.fatoum@pengutronix.de> wrote:
>
> Hello Simon,
>
> On 29.11.23 18:21, Simon Glass wrote:
> > Add a script which produces a Flat Image Tree (FIT), a single file
> > containing the built kernel and associated devicetree files.
> > Compression defaults to gzip which gives a good balance of size and
> > performance.
>
> Thanks for working on this. I think it's useful to have the kernel
> generate a FIT image out of the box. More complex use cases are always
> free to call mkimage with a custom ITS.
>
>
> > The files compress from about 86MB to 24MB using this approach.
> >
> > The FIT can be used by bootloaders which support it, such as U-Boot
> > and Linuxboot. It permits automatic selection of the correct
> > devicetree, matching the compatible string of the running board with
> > the closest compatible string in the FIT. There is no need for
> > filenames or other workarounds.
> >
> > Add a 'make image.fit' build target for arm64, as well.
>
> not that it matters much, but should this maybe called Image.fit
> as the other Image types are capitalized too?

I missed this comment earlier. I believe Image is intended to refer to
a raw image, with the other extensions being compressed versions of
these. So I believe it would be confusing for the FIT version to have
a capital I.

>
> >  EFI_ZBOOT_PAYLOAD    := Image
> >  EFI_ZBOOT_BFD_TARGET := elf64-littleaarch64
> >  EFI_ZBOOT_MACH_TYPE  := ARM64
> > diff --git a/scripts/Makefile.lib b/scripts/Makefile.lib
> > index 1a965fe68e01..e1c06ca3c847 100644
> > --- a/scripts/Makefile.lib
> > +++ b/scripts/Makefile.lib
> > @@ -496,6 +496,19 @@ quiet_cmd_uimage = UIMAGE  $@
> >                       -a $(UIMAGE_LOADADDR) -e $(UIMAGE_ENTRYADDR) \
> >                       -n '$(UIMAGE_NAME)' -d $< $@
>
> Doesn't hardcoding a load address and entry address here defeat the point
> of having FIT as generic portable image format?
>
> At least barebox will try to place the kernel image at physical address 0 and
> will exit with an error message if no SDRAM is located at that address.
> The recommendation in that case is to omit load and entry address altogether
> to have barebox find a suitable location, but I see now that the FIT specification
> requires a load and entry address. What would happen if U-Boot tries to load this
> FIT image on a board that has no DRAM at address 0?
>
> Please Cc me on subsequent revisions. I am interested in testing that this works for barebox
> too.

I have added you.

Regards,
Simon
Simon Glass Nov. 30, 2023, 8:32 p.m. UTC | #19
Hi Masahiro,

On Thu, 30 Nov 2023 at 08:39, Masahiro Yamada <masahiroy@kernel.org> wrote:
>
> On Thu, Nov 30, 2023 at 2:22 AM Simon Glass <sjg@chromium.org> wrote:
> >
> > Add a script which produces a Flat Image Tree (FIT), a single file
> > containing the built kernel and associated devicetree files.
> > Compression defaults to gzip which gives a good balance of size and
> > performance.
> >
> > The files compress from about 86MB to 24MB using this approach.
> >
> > The FIT can be used by bootloaders which support it, such as U-Boot
> > and Linuxboot. It permits automatic selection of the correct
> > devicetree, matching the compatible string of the running board with
> > the closest compatible string in the FIT. There is no need for
> > filenames or other workarounds.
> >
> > Add a 'make image.fit' build target for arm64, as well.
> >
> > The FIT can be examined using 'dumpimage -l'.
> >
> > This features requires pylibfdt (use 'pip install libfdt'). It also
> > requires compression utilities for the algorithm being used. Supported
> > compression options are the same as the Image.xxx files. For now there
> > is no way to change the compression other than by editing the rule for
> > $(obj)/image.fit
> >
> > While FIT supports a ramdisk / initrd, no attempt is made to support
> > this here, since it must be built separately from the Linux build.
> >
> > Signed-off-by: Simon Glass <sjg@chromium.org>
> > ---
> >
> > Changes in v7:
> > - Add Image as a dependency of image.fit
> > - Drop kbuild tag
> > - Add dependency on dtbs
> > - Drop unnecessary path separator for dtbs
> > - Rebase to -next
> >
> > Changes in v5:
> > - Drop patch previously applied
> > - Correct compression rule which was broken in v4
> >
> > Changes in v4:
> > - Use single quotes for UIMAGE_NAME
> >
> > Changes in v3:
> > - Drop temporary file image.itk
> > - Drop patch 'Use double quotes for image name'
> > - Drop double quotes in use of UIMAGE_NAME
> > - Drop unnecessary CONFIG_EFI_ZBOOT condition for help
> > - Avoid hard-coding "arm64" for the DT architecture
> >
> > Changes in v2:
> > - Drop patch previously applied
> > - Add .gitignore file
> > - Move fit rule to Makefile.lib using an intermediate file
> > - Drop dependency on CONFIG_EFI_ZBOOT
> > - Pick up .dtb files separately from the kernel
> > - Correct pylint too-many-args warning for write_kernel()
> > - Include the kernel image in the file count
> > - Add a pointer to the FIT spec and mention of its wide industry usage
> > - Mention the kernel version in the FIT description
> >
> >  MAINTAINERS                |   7 +
> >  arch/arm64/Makefile        |   9 +-
> >  arch/arm64/boot/.gitignore |   1 +
> >  arch/arm64/boot/Makefile   |   6 +-
> >  scripts/Makefile.lib       |  13 ++
> >  scripts/make_fit.py        | 289 +++++++++++++++++++++++++++++++++++++
> >  6 files changed, 322 insertions(+), 3 deletions(-)
> >  create mode 100755 scripts/make_fit.py
> >
> > diff --git a/MAINTAINERS b/MAINTAINERS
> > index 14587be87a33..d609f0e8deb3 100644
> > --- a/MAINTAINERS
> > +++ b/MAINTAINERS
> > @@ -1585,6 +1585,13 @@ F:       Documentation/process/maintainer-soc*.rst
> >  F:     arch/arm/boot/dts/Makefile
> >  F:     arch/arm64/boot/dts/Makefile
> >
> > +ARM64 FIT SUPPORT
> > +M:     Simon Glass <sjg@chromium.org>
> > +L:     linux-arm-kernel@lists.infradead.org (moderated for non-subscribers)
> > +S:     Maintained
> > +F:     arch/arm64/boot/Makefile
> > +F:     scripts/make_fit.py
> > +
> >  ARM ARCHITECTED TIMER DRIVER
> >  M:     Mark Rutland <mark.rutland@arm.com>
> >  M:     Marc Zyngier <maz@kernel.org>
> > diff --git a/arch/arm64/Makefile b/arch/arm64/Makefile
> > index 1bd4fae6e806..18e092de7cdb 100644
> > --- a/arch/arm64/Makefile
> > +++ b/arch/arm64/Makefile
> > @@ -36,6 +36,8 @@ ifeq ($(CONFIG_BROKEN_GAS_INST),y)
> >  $(warning Detected assembler with broken .inst; disassembly will be unreliable)
> >  endif
> >
> > +KBUILD_DTBS      := dtbs
>
>
> Please remove this, and hard-code
>
>  image.fit: dtbs

OK

>
>
>
> >
> >  $(obj)/Image: vmlinux FORCE
> >         $(call if_changed,objcopy)
> > @@ -39,6 +40,9 @@ $(obj)/Image.lzo: $(obj)/Image FORCE
> >  $(obj)/Image.zst: $(obj)/Image FORCE
> >         $(call if_changed,zstd)
> >
> > +$(obj)/image.fit: $(obj)/Image FORCE
> > +       $(call cmd,fit,gzip)
>
>
> The gzip parameter is not used.
> Please do
>
>      $(call cmd,fit)

I do want to be able to control the compression algo. I added a
FIT_COMPRESS for that, so that this arg is used.
>
> In the python script, functions are separated with two blank lines,
> but there is only one blank line between parse_args() and setup_fit().
>
>
> I do not mind either way because it does not contain any class,
> but please keep consistency.

OK

Regards,
Simon
Ahmad Fatoum Dec. 1, 2023, 2:03 a.m. UTC | #20
Hello Simon,

On 30.11.23 21:30, Simon Glass wrote:
> On Wed, 29 Nov 2023 at 12:54, Ahmad Fatoum <a.fatoum@pengutronix.de> wrote:
>> On 29.11.23 20:44, Simon Glass wrote:
>>> On Wed, 29 Nov 2023 at 12:33, Ahmad Fatoum <a.fatoum@pengutronix.de> wrote:
>>>>
>>>> On 29.11.23 20:27, Simon Glass wrote:
>>>>> On Wed, 29 Nov 2023 at 12:15, Ahmad Fatoum <a.fatoum@pengutronix.de> wrote:
>>>>>> On 29.11.23 20:02, Simon Glass wrote:
>>>>>>> On Wed, 29 Nov 2023 at 11:59, Ahmad Fatoum <a.fatoum@pengutronix.de> wrote:
>>>>>>>> The specification says that this is the root U-Boot compatible,
>>>>>>>> which I presume to mean the top-level compatible, which makes sense to me.
>>>>>>>>
>>>>>>>> The code here though adds all compatible strings from the device tree though,
>>>>>>>> is this intended?
>>>>>>>
>>>>>>> Yes, since it saves needing to read in each DT just to get the
>>>>>>> compatible stringlist.
>>>>>>
>>>>>> The spec reads as if only one string (root) is supposed to be in the list.
>>>>>> The script adds all compatibles though. This is not really useful as a bootloader
>>>>>> that's compatible with e.g. fsl,imx8mm would just take the first device tree
>>>>>> with that SoC, which is most likely to be wrong. It would be better to just
>>>>>> specify the top-level compatible, so the bootloader fails instead of taking
>>>>>> the first DT it finds.
>>>>>
>>>>> We do need to have a list, since we have to support different board revs, etc.
>>>>
>>>> Can you give me an example? The way I see it, a bootloader with
>>>> compatible "vendor,board" and a FIT with configuration with compatibles:
>>>>
>>>>   "vendor,board-rev-a", "vendor,board"
>>>>   "vendor,board-rev-b", "vendor,board"
>>>>
>>>> would just result in the bootloader booting the first configuration, even if
>>>> the device is actually rev-b.
>>>
>>> You need to find the best match, not just any match. This is
>>> documented in the function comment for fit_conf_find_compat().
>>
>> In my above example, both configuration are equally good.
>> Can you give me an example where it makes sense to have multiple
>> compatibles automatically extracted from the device tree compatible?
>>
>> The way I see it having more than one compatible here just has
>> downsides.
> 
> I don't have an example to hand, but this is the required mechanism of
> FIT. This feature has been in place for many years and is used by
> ChromeOS, at least.

I see the utility of a FIT configuration with

    compatible = "vendor,board-rev-a", "vendor,board-rev-b";

I fail to see a utility for a configuration with

    compatible = "vendor,board", "vendor,SoM", "vendor,SoC";

Any configuration that ends up being booted because "vendor,SoC" was matched is
most likely doomed to fail. Therefore, I would suggest that only the top level
configuration is written into the FIT configurations automatically.

Cheers,
Ahmad
Simon Glass Dec. 2, 2023, 4:36 p.m. UTC | #21
Hi Ahmad,

On Thu, 30 Nov 2023 at 19:04, Ahmad Fatoum <a.fatoum@pengutronix.de> wrote:
>
> Hello Simon,
>
> On 30.11.23 21:30, Simon Glass wrote:
> > On Wed, 29 Nov 2023 at 12:54, Ahmad Fatoum <a.fatoum@pengutronix.de> wrote:
> >> On 29.11.23 20:44, Simon Glass wrote:
> >>> On Wed, 29 Nov 2023 at 12:33, Ahmad Fatoum <a.fatoum@pengutronix.de> wrote:
> >>>>
> >>>> On 29.11.23 20:27, Simon Glass wrote:
> >>>>> On Wed, 29 Nov 2023 at 12:15, Ahmad Fatoum <a.fatoum@pengutronix.de> wrote:
> >>>>>> On 29.11.23 20:02, Simon Glass wrote:
> >>>>>>> On Wed, 29 Nov 2023 at 11:59, Ahmad Fatoum <a.fatoum@pengutronix.de> wrote:
> >>>>>>>> The specification says that this is the root U-Boot compatible,
> >>>>>>>> which I presume to mean the top-level compatible, which makes sense to me.
> >>>>>>>>
> >>>>>>>> The code here though adds all compatible strings from the device tree though,
> >>>>>>>> is this intended?
> >>>>>>>
> >>>>>>> Yes, since it saves needing to read in each DT just to get the
> >>>>>>> compatible stringlist.
> >>>>>>
> >>>>>> The spec reads as if only one string (root) is supposed to be in the list.
> >>>>>> The script adds all compatibles though. This is not really useful as a bootloader
> >>>>>> that's compatible with e.g. fsl,imx8mm would just take the first device tree
> >>>>>> with that SoC, which is most likely to be wrong. It would be better to just
> >>>>>> specify the top-level compatible, so the bootloader fails instead of taking
> >>>>>> the first DT it finds.
> >>>>>
> >>>>> We do need to have a list, since we have to support different board revs, etc.
> >>>>
> >>>> Can you give me an example? The way I see it, a bootloader with
> >>>> compatible "vendor,board" and a FIT with configuration with compatibles:
> >>>>
> >>>>   "vendor,board-rev-a", "vendor,board"
> >>>>   "vendor,board-rev-b", "vendor,board"
> >>>>
> >>>> would just result in the bootloader booting the first configuration, even if
> >>>> the device is actually rev-b.
> >>>
> >>> You need to find the best match, not just any match. This is
> >>> documented in the function comment for fit_conf_find_compat().
> >>
> >> In my above example, both configuration are equally good.
> >> Can you give me an example where it makes sense to have multiple
> >> compatibles automatically extracted from the device tree compatible?
> >>
> >> The way I see it having more than one compatible here just has
> >> downsides.
> >
> > I don't have an example to hand, but this is the required mechanism of
> > FIT. This feature has been in place for many years and is used by
> > ChromeOS, at least.
>
> I see the utility of a FIT configuration with
>
>     compatible = "vendor,board-rev-a", "vendor,board-rev-b";
>
> I fail to see a utility for a configuration with
>
>     compatible = "vendor,board", "vendor,SoM", "vendor,SoC";
>
> Any configuration that ends up being booted because "vendor,SoC" was matched is
> most likely doomed to fail. Therefore, I would suggest that only the top level
> configuration is written into the FIT configurations automatically.

Firstly, I am not an expert on this.

Say you have a board with variants. The compatible string in U-Boot
may be something like:

"google,veyron-brain-rev1", "google,veyron-brain", "google,veyron",
"rockchip,rk3288";

If you then have several FIT configurations, they may be something like:

"google,veyron-brain-rev0", "google,veyron-brain", "google,veyron",
"rockchip,rk3288";
"google,veyron-brain-rev1", "google,veyron-brain", "google,veyron",
"rockchip,rk3288";
"google,veyron-brain-rev2", "google,veyron-brain", "google,veyron",
"rockchip,rk3288";

You want to choose the second one, since it is a better match than the others.

+Doug Anderson who knows a lot more about this than me.

Regards,
Simon
Doug Anderson Dec. 4, 2023, 5:52 p.m. UTC | #22
Hi,

On Sat, Dec 2, 2023 at 8:37 AM Simon Glass <sjg@chromium.org> wrote:
>
> Hi Ahmad,
>
> On Thu, 30 Nov 2023 at 19:04, Ahmad Fatoum <a.fatoum@pengutronix.de> wrote:
> >
> > Hello Simon,
> >
> > On 30.11.23 21:30, Simon Glass wrote:
> > > On Wed, 29 Nov 2023 at 12:54, Ahmad Fatoum <a.fatoum@pengutronix.de> wrote:
> > >> On 29.11.23 20:44, Simon Glass wrote:
> > >>> On Wed, 29 Nov 2023 at 12:33, Ahmad Fatoum <a.fatoum@pengutronix.de> wrote:
> > >>>>
> > >>>> On 29.11.23 20:27, Simon Glass wrote:
> > >>>>> On Wed, 29 Nov 2023 at 12:15, Ahmad Fatoum <a.fatoum@pengutronix.de> wrote:
> > >>>>>> On 29.11.23 20:02, Simon Glass wrote:
> > >>>>>>> On Wed, 29 Nov 2023 at 11:59, Ahmad Fatoum <a.fatoum@pengutronix.de> wrote:
> > >>>>>>>> The specification says that this is the root U-Boot compatible,
> > >>>>>>>> which I presume to mean the top-level compatible, which makes sense to me.
> > >>>>>>>>
> > >>>>>>>> The code here though adds all compatible strings from the device tree though,
> > >>>>>>>> is this intended?
> > >>>>>>>
> > >>>>>>> Yes, since it saves needing to read in each DT just to get the
> > >>>>>>> compatible stringlist.
> > >>>>>>
> > >>>>>> The spec reads as if only one string (root) is supposed to be in the list.
> > >>>>>> The script adds all compatibles though. This is not really useful as a bootloader
> > >>>>>> that's compatible with e.g. fsl,imx8mm would just take the first device tree
> > >>>>>> with that SoC, which is most likely to be wrong. It would be better to just
> > >>>>>> specify the top-level compatible, so the bootloader fails instead of taking
> > >>>>>> the first DT it finds.
> > >>>>>
> > >>>>> We do need to have a list, since we have to support different board revs, etc.
> > >>>>
> > >>>> Can you give me an example? The way I see it, a bootloader with
> > >>>> compatible "vendor,board" and a FIT with configuration with compatibles:
> > >>>>
> > >>>>   "vendor,board-rev-a", "vendor,board"
> > >>>>   "vendor,board-rev-b", "vendor,board"
> > >>>>
> > >>>> would just result in the bootloader booting the first configuration, even if
> > >>>> the device is actually rev-b.
> > >>>
> > >>> You need to find the best match, not just any match. This is
> > >>> documented in the function comment for fit_conf_find_compat().
> > >>
> > >> In my above example, both configuration are equally good.
> > >> Can you give me an example where it makes sense to have multiple
> > >> compatibles automatically extracted from the device tree compatible?
> > >>
> > >> The way I see it having more than one compatible here just has
> > >> downsides.
> > >
> > > I don't have an example to hand, but this is the required mechanism of
> > > FIT. This feature has been in place for many years and is used by
> > > ChromeOS, at least.
> >
> > I see the utility of a FIT configuration with
> >
> >     compatible = "vendor,board-rev-a", "vendor,board-rev-b";
> >
> > I fail to see a utility for a configuration with
> >
> >     compatible = "vendor,board", "vendor,SoM", "vendor,SoC";
> >
> > Any configuration that ends up being booted because "vendor,SoC" was matched is
> > most likely doomed to fail. Therefore, I would suggest that only the top level
> > configuration is written into the FIT configurations automatically.
>
> Firstly, I am not an expert on this.
>
> Say you have a board with variants. The compatible string in U-Boot
> may be something like:
>
> "google,veyron-brain-rev1", "google,veyron-brain", "google,veyron",
> "rockchip,rk3288";
>
> If you then have several FIT configurations, they may be something like:
>
> "google,veyron-brain-rev0", "google,veyron-brain", "google,veyron",
> "rockchip,rk3288";
> "google,veyron-brain-rev1", "google,veyron-brain", "google,veyron",
> "rockchip,rk3288";
> "google,veyron-brain-rev2", "google,veyron-brain", "google,veyron",
> "rockchip,rk3288";
>
> You want to choose the second one, since it is a better match than the others.
>
> +Doug Anderson who knows a lot more about this than me.

Hopefully this is all explained by:

https://docs.kernel.org/arch/arm/google/chromebook-boot-flow.html
Ahmad Fatoum Dec. 5, 2023, 11:15 a.m. UTC | #23
Hello,

On 04.12.23 18:52, Doug Anderson wrote:> On Sat, Dec 2, 2023 at 8:37 AM Simon Glass <sjg@chromium.org> wrote:
>> On Thu, 30 Nov 2023 at 19:04, Ahmad Fatoum <a.fatoum@pengutronix.de> wrote:
>>> On 30.11.23 21:30, Simon Glass wrote:
>>>> On Wed, 29 Nov 2023 at 12:54, Ahmad Fatoum <a.fatoum@pengutronix.de> wrote:
>>>>> On 29.11.23 20:44, Simon Glass wrote:
>>>> I don't have an example to hand, but this is the required mechanism of
>>>> FIT. This feature has been in place for many years and is used by
>>>> ChromeOS, at least.
>>>
>>> I see the utility of a FIT configuration with
>>>
>>>     compatible = "vendor,board-rev-a", "vendor,board-rev-b";
>>>
>>> I fail to see a utility for a configuration with
>>>
>>>     compatible = "vendor,board", "vendor,SoM", "vendor,SoC";
>>>
>>> Any configuration that ends up being booted because "vendor,SoC" was matched is
>>> most likely doomed to fail. Therefore, I would suggest that only the top level
>>> configuration is written into the FIT configurations automatically.
>>
>> Firstly, I am not an expert on this.
>>
>> Say you have a board with variants. The compatible string in U-Boot
>> may be something like:
>>
>> "google,veyron-brain-rev1", "google,veyron-brain", "google,veyron",
>> "rockchip,rk3288";
>>
>> If you then have several FIT configurations, they may be something like:
>>
>> "google,veyron-brain-rev0", "google,veyron-brain", "google,veyron",
>> "rockchip,rk3288";
>> "google,veyron-brain-rev1", "google,veyron-brain", "google,veyron",
>> "rockchip,rk3288";
>> "google,veyron-brain-rev2", "google,veyron-brain", "google,veyron",
>> "rockchip,rk3288";
>>
>> You want to choose the second one, since it is a better match than the others.

Now imagine, you are building a kernel that has no DT support for the Veyron,
but instead has support for the Phytec RK3288 PCM-947:

  phytec,rk3288-pcm-947", "phytec,rk3288-phycore-som", "rockchip,rk3288

As far as I understand U-Boot code, A veyron U-Boot would boot the Phytec DT
if CONFIG_FIT_BEST_MATCH is set, although it's a bad match and a boot failure
should rather have occurred.

>> +Doug Anderson who knows a lot more about this than me.
> 
> Hopefully this is all explained by:
> 
> https://docs.kernel.org/arch/arm/google/chromebook-boot-flow.html

Thanks Doug, this was helpful. The missing information to me was that
depthcharge only compares the top-level board compatible in addition
to runtime generated board compatibles, unlike what U-Boot seems to do.

barebox only compares its top-level compatible against the FIT configuration
compatibles, so adding a full compatible list to the configuration nodes as done
by this series should be ok there as well. I think U-Boot could run into
issues though as described above.

Out of curiosity: I only heard about Depthcharge before as exploitation toolkit
for U-Boot. Can you point me at some documentation on what the Depthcharge bootloader
does what U-Boot (which was presumably used before?) doesn't?

Thanks,
Ahmad

>
Doug Anderson Dec. 5, 2023, 5:06 p.m. UTC | #24
Hi,

On Tue, Dec 5, 2023 at 3:16 AM Ahmad Fatoum <a.fatoum@pengutronix.de> wrote:
>
> Hello,
>
> On 04.12.23 18:52, Doug Anderson wrote:> On Sat, Dec 2, 2023 at 8:37 AM Simon Glass <sjg@chromium.org> wrote:
> >> On Thu, 30 Nov 2023 at 19:04, Ahmad Fatoum <a.fatoum@pengutronix.de> wrote:
> >>> On 30.11.23 21:30, Simon Glass wrote:
> >>>> On Wed, 29 Nov 2023 at 12:54, Ahmad Fatoum <a.fatoum@pengutronix.de> wrote:
> >>>>> On 29.11.23 20:44, Simon Glass wrote:
> >>>> I don't have an example to hand, but this is the required mechanism of
> >>>> FIT. This feature has been in place for many years and is used by
> >>>> ChromeOS, at least.
> >>>
> >>> I see the utility of a FIT configuration with
> >>>
> >>>     compatible = "vendor,board-rev-a", "vendor,board-rev-b";
> >>>
> >>> I fail to see a utility for a configuration with
> >>>
> >>>     compatible = "vendor,board", "vendor,SoM", "vendor,SoC";
> >>>
> >>> Any configuration that ends up being booted because "vendor,SoC" was matched is
> >>> most likely doomed to fail. Therefore, I would suggest that only the top level
> >>> configuration is written into the FIT configurations automatically.
> >>
> >> Firstly, I am not an expert on this.
> >>
> >> Say you have a board with variants. The compatible string in U-Boot
> >> may be something like:
> >>
> >> "google,veyron-brain-rev1", "google,veyron-brain", "google,veyron",
> >> "rockchip,rk3288";
> >>
> >> If you then have several FIT configurations, they may be something like:
> >>
> >> "google,veyron-brain-rev0", "google,veyron-brain", "google,veyron",
> >> "rockchip,rk3288";
> >> "google,veyron-brain-rev1", "google,veyron-brain", "google,veyron",
> >> "rockchip,rk3288";
> >> "google,veyron-brain-rev2", "google,veyron-brain", "google,veyron",
> >> "rockchip,rk3288";
> >>
> >> You want to choose the second one, since it is a better match than the others.
>
> Now imagine, you are building a kernel that has no DT support for the Veyron,
> but instead has support for the Phytec RK3288 PCM-947:
>
>   phytec,rk3288-pcm-947", "phytec,rk3288-phycore-som", "rockchip,rk3288
>
> As far as I understand U-Boot code, A veyron U-Boot would boot the Phytec DT
> if CONFIG_FIT_BEST_MATCH is set, although it's a bad match and a boot failure
> should rather have occurred.

On depthcharge the bootloader never matches on just a SoC name.


> >> +Doug Anderson who knows a lot more about this than me.
> >
> > Hopefully this is all explained by:
> >
> > https://docs.kernel.org/arch/arm/google/chromebook-boot-flow.html
>
> Thanks Doug, this was helpful. The missing information to me was that
> depthcharge only compares the top-level board compatible in addition
> to runtime generated board compatibles, unlike what U-Boot seems to do.
>
> barebox only compares its top-level compatible against the FIT configuration
> compatibles, so adding a full compatible list to the configuration nodes as done
> by this series should be ok there as well. I think U-Boot could run into
> issues though as described above.
>
> Out of curiosity: I only heard about Depthcharge before as exploitation toolkit
> for U-Boot. Can you point me at some documentation on what the Depthcharge bootloader
> does what U-Boot (which was presumably used before?) doesn't?

I can only assume that the depthcharge you're talking about is
different. The one used by Chromebooks is basically:

https://chromium.googlesource.com/chromiumos/platform/depthcharge/+/refs/heads/main

I assume you're asking: why are we using depthcharge in ChromeOS
instead of U-Boot?

There was much discussion about this back in the day. From what I
recall, part of the reason was that folks wanted the boot flow to be a
bit more standard between x86 Chromebooks and ARM Chromebooks. x86
Chromebooks were using coreboot for the initial hardware booting and
then needed a 2nd stage to actually load Linux and ended up creating
depthcharge. ...and then we switched to the same model for ARM boards.

I didn't personally make that decision and I'm also not on the
firmware team, so that's about all I'll say on the topic. ;-)

Oh, hmmm. Searching found me:

https://www.chromium.org/chromium-os/developer-information-for-chrome-os-devices/custom-firmware/

...which pointed at:

https://www.chromium.org/chromium-os/2014-firmware-summit/ChromeOS%20firmware%20summit%20-%20Depthcharge.pdf

-Doug
diff mbox series

Patch

diff --git a/MAINTAINERS b/MAINTAINERS
index 14587be87a33..d609f0e8deb3 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -1585,6 +1585,13 @@  F:	Documentation/process/maintainer-soc*.rst
 F:	arch/arm/boot/dts/Makefile
 F:	arch/arm64/boot/dts/Makefile
 
+ARM64 FIT SUPPORT
+M:	Simon Glass <sjg@chromium.org>
+L:	linux-arm-kernel@lists.infradead.org (moderated for non-subscribers)
+S:	Maintained
+F:	arch/arm64/boot/Makefile
+F:	scripts/make_fit.py
+
 ARM ARCHITECTED TIMER DRIVER
 M:	Mark Rutland <mark.rutland@arm.com>
 M:	Marc Zyngier <maz@kernel.org>
diff --git a/arch/arm64/Makefile b/arch/arm64/Makefile
index 1bd4fae6e806..18e092de7cdb 100644
--- a/arch/arm64/Makefile
+++ b/arch/arm64/Makefile
@@ -36,6 +36,8 @@  ifeq ($(CONFIG_BROKEN_GAS_INST),y)
 $(warning Detected assembler with broken .inst; disassembly will be unreliable)
 endif
 
+KBUILD_DTBS      := dtbs
+
 KBUILD_CFLAGS	+= -mgeneral-regs-only	\
 		   $(compat_vdso) $(cc_has_k_constraint)
 KBUILD_CFLAGS	+= $(call cc-disable-warning, psabi)
@@ -150,7 +152,7 @@  libs-$(CONFIG_EFI_STUB) += $(objtree)/drivers/firmware/efi/libstub/lib.a
 # Default target when executing plain make
 boot		:= arch/arm64/boot
 
-BOOT_TARGETS	:= Image vmlinuz.efi
+BOOT_TARGETS	:= Image vmlinuz.efi image.fit
 
 PHONY += $(BOOT_TARGETS)
 
@@ -162,7 +164,9 @@  endif
 
 all:	$(notdir $(KBUILD_IMAGE))
 
-vmlinuz.efi: Image
+image.fit: $(KBUILD_DTBS)
+
+vmlinuz.efi image.fit: Image
 $(BOOT_TARGETS): vmlinux
 	$(Q)$(MAKE) $(build)=$(boot) $(boot)/$@
 
@@ -215,6 +219,7 @@  virtconfig:
 define archhelp
   echo  '* Image.gz      - Compressed kernel image (arch/$(ARCH)/boot/Image.gz)'
   echo  '  Image         - Uncompressed kernel image (arch/$(ARCH)/boot/Image)'
+  echo  '  image.fit     - Flat Image Tree (arch/$(ARCH)/boot/image.fit)'
   echo  '  install       - Install uncompressed kernel'
   echo  '  zinstall      - Install compressed kernel'
   echo  '                  Install using (your) ~/bin/installkernel or'
diff --git a/arch/arm64/boot/.gitignore b/arch/arm64/boot/.gitignore
index af5dc61f8b43..abaae9de1bdd 100644
--- a/arch/arm64/boot/.gitignore
+++ b/arch/arm64/boot/.gitignore
@@ -2,3 +2,4 @@ 
 Image
 Image.gz
 vmlinuz*
+image.fit
diff --git a/arch/arm64/boot/Makefile b/arch/arm64/boot/Makefile
index 1761f5972443..8d591fda078f 100644
--- a/arch/arm64/boot/Makefile
+++ b/arch/arm64/boot/Makefile
@@ -16,7 +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
+targets := Image Image.bz2 Image.gz Image.lz4 Image.lzma Image.lzo \
+	Image.zst image.fit
 
 $(obj)/Image: vmlinux FORCE
 	$(call if_changed,objcopy)
@@ -39,6 +40,9 @@  $(obj)/Image.lzo: $(obj)/Image FORCE
 $(obj)/Image.zst: $(obj)/Image FORCE
 	$(call if_changed,zstd)
 
+$(obj)/image.fit: $(obj)/Image FORCE
+	$(call cmd,fit,gzip)
+
 EFI_ZBOOT_PAYLOAD	:= Image
 EFI_ZBOOT_BFD_TARGET	:= elf64-littleaarch64
 EFI_ZBOOT_MACH_TYPE	:= ARM64
diff --git a/scripts/Makefile.lib b/scripts/Makefile.lib
index 1a965fe68e01..e1c06ca3c847 100644
--- a/scripts/Makefile.lib
+++ b/scripts/Makefile.lib
@@ -496,6 +496,19 @@  quiet_cmd_uimage = UIMAGE  $@
 			-a $(UIMAGE_LOADADDR) -e $(UIMAGE_ENTRYADDR) \
 			-n '$(UIMAGE_NAME)' -d $< $@
 
+# Flat Image Tree (FIT)
+# This allows for packaging of a kernel and all devicetrees files, using
+# compression.
+# ---------------------------------------------------------------------------
+
+MAKE_FIT := $(srctree)/scripts/make_fit.py
+
+quiet_cmd_fit = FIT     $@
+      cmd_fit = $(MAKE_FIT) -f $@ --arch $(UIMAGE_ARCH) --os linux \
+			--name '$(UIMAGE_NAME)' \
+			--compress $(UIMAGE_COMPRESSION) -k $< \
+			$(dir $<)dts
+
 # XZ
 # ---------------------------------------------------------------------------
 # Use xzkern to compress the kernel image and xzmisc to compress other things.
diff --git a/scripts/make_fit.py b/scripts/make_fit.py
new file mode 100755
index 000000000000..e1059825de9c
--- /dev/null
+++ b/scripts/make_fit.py
@@ -0,0 +1,289 @@ 
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0+
+#
+# Copyright 2023 Google LLC
+# Written by Simon Glass <sjg@chromium.org>
+#
+
+"""Build a FIT containing a lot of devicetree files
+
+Usage:
+    make_fit.py -A arm64 -n 'Linux-6.6' -O linux
+        -f arch/arm64/boot/image.fit -k /tmp/kern/arch/arm64/boot/image.itk
+        /tmp/kern/arch/arm64/boot/dts/ -E -c gzip
+
+Creates a FIT containing the supplied kernel and a directory containing the
+devicetree files.
+
+Use -E to generate an external FIT (where the data is placed after the
+FIT data structure). This allows parsing of the data without loading
+the entire FIT.
+
+Use -c to compress the data, using bzip2, gzip, lz4, lzma, lzo and
+zstd
+
+The resulting FIT can be booted by bootloaders which support FIT, such
+as U-Boot, Linuxboot, Tianocore, etc.
+
+Note that this tool does not yet support adding a ramdisk / initrd.
+"""
+
+import argparse
+import collections
+import os
+import subprocess
+import sys
+import tempfile
+import time
+
+import libfdt
+
+
+# Tool extension and the name of the command-line tools
+CompTool = collections.namedtuple('CompTool', 'ext,tools')
+
+COMP_TOOLS = {
+    'bzip2': CompTool('.bz2', 'bzip2'),
+    'gzip': CompTool('.gz', 'pigz,gzip'),
+    'lz4': CompTool('.lz4', 'lz4'),
+    'lzma': CompTool('.lzma', 'lzma'),
+    'lzo': CompTool('.lzo', 'lzop'),
+    'zstd': CompTool('.zstd', 'zstd'),
+}
+
+def parse_args():
+    """Parse the program ArgumentParser
+
+    Returns:
+        Namespace object containing the arguments
+    """
+    epilog = 'Build a FIT from a directory tree containing .dtb files'
+    parser = argparse.ArgumentParser(epilog=epilog)
+    parser.add_argument('-A', '--arch', type=str, required=True,
+          help='Specifies the architecture')
+    parser.add_argument('-c', '--compress', type=str, default='none',
+          help='Specifies the compression')
+    parser.add_argument('-E', '--external', action='store_true',
+          help='Convert the FIT to use external data')
+    parser.add_argument('-n', '--name', type=str, required=True,
+          help='Specifies the name')
+    parser.add_argument('-O', '--os', type=str, required=True,
+          help='Specifies the operating system')
+    parser.add_argument('-f', '--fit', type=str, required=True,
+          help='Specifies the output file (.fit)')
+    parser.add_argument('-k', '--kernel', type=str, required=True,
+          help='Specifies the (uncompressed) kernel input file (.itk)')
+    parser.add_argument('srcdir', type=str, nargs='*',
+          help='Specifies the directory tree that contains .dtb files')
+
+    return parser.parse_args()
+
+def setup_fit(fsw, name):
+    """Make a start on writing the FIT
+
+    Outputs the root properties and the 'images' node
+
+    Args:
+        fsw (libfdt.FdtSw): Object to use for writing
+        name (str): Name of kernel image
+    """
+    fsw.INC_SIZE = 65536
+    fsw.finish_reservemap()
+    fsw.begin_node('')
+    fsw.property_string('description', f'{name} with devicetree set')
+    fsw.property_u32('#address-cells', 1)
+
+    fsw.property_u32('timestamp', int(time.time()))
+    fsw.begin_node('images')
+
+
+def write_kernel(fsw, data, args):
+    """Write out the kernel image
+
+    Writes a kernel node along with the required properties
+
+    Args:
+        fsw (libfdt.FdtSw): Object to use for writing
+        data (bytes): Data to write (possibly compressed)
+        args (Namespace): Contains necessary strings:
+            arch: FIT architecture, e.g. 'arm64'
+            fit_os: Operating Systems, e.g. 'linux'
+            name: Name of OS, e.g. 'Linux-6.6.0-rc7'
+            compress: Compression algorithm to use, e.g. 'gzip'
+    """
+    with fsw.add_node('kernel'):
+        fsw.property_string('description', args.name)
+        fsw.property_string('type', 'kernel_noload')
+        fsw.property_string('arch', args.arch)
+        fsw.property_string('os', args.os)
+        fsw.property_string('compression', args.compress)
+        fsw.property('data', data)
+        fsw.property_u32('load', 0)
+        fsw.property_u32('entry', 0)
+
+
+def finish_fit(fsw, entries):
+    """Finish the FIT ready for use
+
+    Writes the /configurations node and subnodes
+
+    Args:
+        fsw (libfdt.FdtSw): Object to use for writing
+        entries (list of tuple): List of configurations:
+            str: Description of model
+            str: Compatible stringlist
+    """
+    fsw.end_node()
+    seq = 0
+    with fsw.add_node('configurations'):
+        for model, compat in entries:
+            seq += 1
+            with fsw.add_node(f'conf-{seq}'):
+                fsw.property('compatible', bytes(compat))
+                fsw.property_string('description', model)
+                fsw.property_string('fdt', f'fdt-{seq}')
+                fsw.property_string('kernel', 'kernel')
+    fsw.end_node()
+
+
+def compress_data(inf, compress):
+    """Compress data using a selected algorithm
+
+    Args:
+        inf (IOBase): Filename containing the data to compress
+        compress (str): Compression algorithm, e.g. 'gzip'
+
+    Return:
+        bytes: Compressed data
+    """
+    if compress == 'none':
+        return inf.read()
+
+    comp = COMP_TOOLS.get(compress)
+    if not comp:
+        raise ValueError(f"Unknown compression algorithm '{compress}'")
+
+    with tempfile.NamedTemporaryFile() as comp_fname:
+        with open(comp_fname.name, 'wb') as outf:
+            done = False
+            for tool in comp.tools.split(','):
+                try:
+                    subprocess.call([tool, '-c'], stdin=inf, stdout=outf)
+                    done = True
+                    break
+                except FileNotFoundError:
+                    pass
+            if not done:
+                raise ValueError(f'Missing tool(s): {comp.tools}\n')
+            with open(comp_fname.name, 'rb') as compf:
+                comp_data = compf.read()
+    return comp_data
+
+
+def output_dtb(fsw, seq, fname, arch, compress):
+    """Write out a single devicetree to the FIT
+
+    Args:
+        fsw (libfdt.FdtSw): Object to use for writing
+        seq (int): Sequence number (1 for first)
+        fmame (str): Filename containing the DTB
+        arch: FIT architecture, e.g. 'arm64'
+        compress (str): Compressed algorithm, e.g. 'gzip'
+
+    Returns:
+        tuple:
+            str: Model name
+            bytes: Compatible stringlist
+    """
+    with fsw.add_node(f'fdt-{seq}'):
+        # Get the compatible / model information
+        with open(fname, 'rb') as inf:
+            data = inf.read()
+        fdt = libfdt.FdtRo(data)
+        model = fdt.getprop(0, 'model').as_str()
+        compat = fdt.getprop(0, 'compatible')
+
+        fsw.property_string('description', model)
+        fsw.property_string('type', 'flat_dt')
+        fsw.property_string('arch', arch)
+        fsw.property_string('compression', compress)
+        fsw.property('compatible', bytes(compat))
+
+        with open(fname, 'rb') as inf:
+            compressed = compress_data(inf, compress)
+        fsw.property('data', compressed)
+    return model, compat
+
+
+def build_fit(args):
+    """Build the FIT from the provided files and arguments
+
+    Args:
+        args (Namespace): Program arguments
+
+    Returns:
+        tuple:
+            bytes: FIT data
+            int: Number of configurations generated
+            size: Total uncompressed size of data
+    """
+    fsw = libfdt.FdtSw()
+    setup_fit(fsw, args.name)
+    seq = 0
+    size = 0
+    entries = []
+
+    # Handle the kernel
+    with open(args.kernel, 'rb') as inf:
+        comp_data = compress_data(inf, args.compress)
+    size += os.path.getsize(args.kernel)
+    write_kernel(fsw, comp_data, args)
+
+    for path in args.srcdir:
+        # Handle devicetree files
+        if os.path.isdir(path):
+            for dirpath, _, fnames in os.walk(path):
+                for fname in fnames:
+                    if os.path.splitext(fname)[1] != '.dtb':
+                        continue
+                    pathname = os.path.join(dirpath, fname)
+                    seq += 1
+                    size += os.path.getsize(pathname)
+                    model, compat = output_dtb(fsw, seq, pathname,
+                                               args.arch, args.compress)
+                    entries.append([model, compat])
+
+    finish_fit(fsw, entries)
+
+    # Include the kernel itself in the returned file count
+    return fsw.as_fdt().as_bytearray(), seq + 1, size
+
+
+def run_make_fit():
+    """Run the tool's main logic"""
+    args = parse_args()
+
+    out_data, count, size = build_fit(args)
+    with open(args.fit, 'wb') as outf:
+        outf.write(out_data)
+
+    ext_fit_size = None
+    if args.external:
+        mkimage = os.environ.get('MKIMAGE', 'mkimage')
+        subprocess.check_call([mkimage, '-E', '-F', args.fit],
+                              stdout=subprocess.DEVNULL)
+
+        with open(args.fit, 'rb') as inf:
+            data = inf.read()
+        ext_fit = libfdt.FdtRo(data)
+        ext_fit_size = ext_fit.totalsize()
+
+    comp_size = len(out_data)
+    print(f'FIT size {comp_size:#x}/{comp_size / 1024 / 1024:.1f} MB', end='')
+    if ext_fit_size:
+        print(f', header {ext_fit_size:#x}/{ext_fit_size / 1024:.1f} KB', end='')
+    print(f', {count} files, uncompressed {size / 1024 / 1024:.1f} MB')
+
+
+if __name__ == "__main__":
+    sys.exit(run_make_fit())