blob: 14de2345388631f5cba24b902c275b07ba1ee372 [file]
#!/usr/bin/env fuchsia-vendored-python
# Copyright 2026 The Fuchsia Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Wraps a ZBI inside an Android boot or vendor_boot image.
This script works in tandem with the boot shim to allow booting Fuchsia on Android
bootloaders.
The ZBI encoding (`--encoding`) and boot image location (`--location`) are configured
independently:
Supported encodings (`--encoding`):
* `raw` (default): Uses the input ZBI directly as the payload.
* `bootloader_file`: Wraps the input ZBI in a `ZBI_TYPE_BOOTLOADER_FILE`
container (named `--bootloader-file-name`) using `--zbi-tool`.
Supported locations (`--location`):
* `ramdisk` (default): Places `--boot-shim` in the `boot` image kernel and the
encoded ZBI payload in the `boot` image `ramdisk`.
* `vendor_ramdisk`: Places the encoded ZBI payload in a `vendor_boot` image
`vendor_ramdisk` alongside `--dtb`.
* `kernel_append`: Embeds the encoded ZBI payload into `--boot-shim`'s
`linuxboot` embedded payload area via `--linuxboot-embed` and places the
combined image in the `boot` image kernel with an empty `ramdisk`.
"""
import argparse
import pathlib
import shutil
import subprocess
import sys
import tempfile
from typing import Sequence
DEFAULT_BOOTLOADER_FILE_NAME = "fuchsia.zbi"
def encode_zbi(
zbi: pathlib.Path,
out: pathlib.Path,
zbi_tool: pathlib.Path | None,
bootloader_file_name: str = DEFAULT_BOOTLOADER_FILE_NAME,
) -> None:
"""Wraps `zbi` in a ZBI container with a single ZBI_TYPE_BOOTLOADER_FILE item.
The `zbi` host tool (`--type=bootloader_file`) creates the ZBI container and item
headers, alignment padding, and CRC32 checksum, while expecting the input file to
begin with the 1-byte name length and filename (`| name_len (1B) | name | payload |`).
"""
if zbi_tool is None:
raise ValueError(
"--zbi-tool is required for encoding 'bootloader_file'"
)
name_bytes = bootloader_file_name.encode("utf-8")
if not (0 < len(name_bytes) <= 255):
raise ValueError(
f"Bootloader file name must be 1..255 bytes, got {len(name_bytes)}"
)
with tempfile.TemporaryDirectory() as temp_dir:
named_payload_path = pathlib.Path(temp_dir) / "bootloader_file.bin"
with named_payload_path.open("wb") as out_f, zbi.open("rb") as in_f:
out_f.write(bytes([len(name_bytes)]) + name_bytes)
shutil.copyfileobj(in_f, out_f)
# Resolve so a bare filename in the cwd (e.g. "zbi") is not looked up on $PATH.
subprocess.run(
[
zbi_tool.resolve(),
"--output",
out,
"--type=bootloader_file",
named_payload_path,
],
check=True,
)
def embed_zbi_in_boot_shim(
linuxboot_embed: pathlib.Path | None,
boot_shim: pathlib.Path | None,
zbi: pathlib.Path,
out: pathlib.Path,
) -> None:
"""Embeds `zbi` into `boot_shim` using `linuxboot-embed.py`."""
if linuxboot_embed is None:
raise ValueError(
"--linuxboot-embed is required for location 'kernel_append'"
)
if boot_shim is None:
raise ValueError("--boot-shim is required for location 'kernel_append'")
subprocess.run(
[
sys.executable,
linuxboot_embed,
"--shim",
boot_shim,
"--output",
out,
zbi,
],
check=True,
)
def create_android_boot_image(
mkbootimg: pathlib.Path,
boot_shim: pathlib.Path | None,
out: pathlib.Path,
zbi: pathlib.Path | None = None,
) -> None:
"""Creates an Android `boot` v4 image."""
if boot_shim is None:
raise ValueError("--boot-shim is required to create a boot image")
ramdisk_args: list[str | pathlib.Path] = (
["--ramdisk", zbi] if zbi is not None else []
)
subprocess.run(
[
sys.executable,
mkbootimg,
"--kernel",
boot_shim,
*ramdisk_args,
"--header_version",
"4",
"--pagesize",
"4096",
"--output",
out,
],
check=True,
)
def create_android_vendor_boot_image(
mkbootimg: pathlib.Path,
dtb: pathlib.Path | None,
zbi: pathlib.Path,
out: pathlib.Path,
) -> None:
"""Creates an Android `vendor_boot` v4 image with `zbi` in `vendor_ramdisk`."""
if dtb is None:
raise ValueError("--dtb is required to create a vendor_boot image")
subprocess.run(
[
sys.executable,
mkbootimg,
"--dtb",
dtb,
"--vendor_ramdisk",
zbi,
"--header_version",
"4",
"--pagesize",
"4096",
"--vendor_boot",
out,
],
check=True,
)
def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--location",
choices=("ramdisk", "vendor_ramdisk", "kernel_append"),
default="ramdisk",
help="Where to place the ZBI payload in the Android boot image (default: ramdisk)",
)
parser.add_argument(
"--encoding",
choices=("raw", "bootloader_file"),
default="raw",
help="How to encode the ZBI payload before packing (default: raw)",
)
parser.add_argument(
"--mkbootimg",
required=True,
type=pathlib.Path,
help="Path to Android `mkbootimg` Python tool",
)
parser.add_argument(
"--zbi-tool",
type=pathlib.Path,
help="Path to `zbi` host tool (required for --encoding=bootloader_file)",
)
parser.add_argument(
"--linuxboot-embed",
type=pathlib.Path,
help="Path to `linuxboot-embed.py` (required for --location=kernel_append)",
)
parser.add_argument(
"--boot-shim",
type=pathlib.Path,
help="Path to boot shim (required for --location=ramdisk and --location=kernel_append)",
)
parser.add_argument(
"--dtb",
type=pathlib.Path,
help="Path to DTB (required for --location=vendor_ramdisk)",
)
parser.add_argument(
"--bootloader-file-name",
default=DEFAULT_BOOTLOADER_FILE_NAME,
help=f"Filename inside ZBI_TYPE_BOOTLOADER_FILE container (default: {DEFAULT_BOOTLOADER_FILE_NAME})",
)
parser.add_argument(
"-z", "--zbi", required=True, type=pathlib.Path, help="Path to ZBI"
)
parser.add_argument(
"-o",
"--output",
required=True,
type=pathlib.Path,
help="Path to output file",
)
return parser.parse_args(argv)
def main(argv: Sequence[str] | None = None) -> None:
args = parse_args(argv)
with tempfile.TemporaryDirectory() as temp_dir:
temp_path = pathlib.Path(temp_dir)
if args.encoding == "raw":
zbi = args.zbi
elif args.encoding == "bootloader_file":
zbi = temp_path / "bootloader_file_container.zbi"
encode_zbi(
zbi=args.zbi,
out=zbi,
zbi_tool=args.zbi_tool,
bootloader_file_name=args.bootloader_file_name,
)
else:
raise ValueError(f"Unsupported encoding: {args.encoding}")
if args.location == "ramdisk":
create_android_boot_image(
mkbootimg=args.mkbootimg,
boot_shim=args.boot_shim,
out=args.output,
zbi=zbi,
)
elif args.location == "kernel_append":
embedded_shim = temp_path / "embedded_shim.bin"
embed_zbi_in_boot_shim(
linuxboot_embed=args.linuxboot_embed,
boot_shim=args.boot_shim,
zbi=zbi,
out=embedded_shim,
)
create_android_boot_image(
mkbootimg=args.mkbootimg,
boot_shim=embedded_shim,
out=args.output,
)
elif args.location == "vendor_ramdisk":
create_android_vendor_boot_image(
mkbootimg=args.mkbootimg,
dtb=args.dtb,
zbi=zbi,
out=args.output,
)
else:
raise ValueError(f"Unsupported location: {args.location}")
if __name__ == "__main__":
main()