cmake_minimum_required(VERSION 3.22)

# ---------------------------------------------------------------------------
# Bootloader subproject CMakeLists (cmakev2 build system)
#
# This file drives the build of the ESP-IDF second-stage bootloader as a
# standalone CMake sub-project that is invoked by the top-level build when
# the project links against the `bootloader` component.
# ---------------------------------------------------------------------------

# ---------------------------------------------------------------------------
# Extra component directories
#
# Users may supply custom bootloader components by placing them inside a
# `bootloader_components/` directory at the root of their application
# project.  If the directory exists it is appended to EXTRA_COMPONENT_DIRS
# so that the IDF component discovery logic can find them.
#
# IGNORE_EXTRA_COMPONENT (list) – optional variable that names individual
# sub-directories of `bootloader_components/` to *exclude* from the build.
# Each entry is turned into an absolute path and collected in
# EXTRA_COMPONENT_EXCLUDE_DIRS which is consumed by the component resolver.
# ---------------------------------------------------------------------------
set(PROJECT_EXTRA_COMPONENTS "${PROJECT_SOURCE_DIR}/bootloader_components")
if(EXISTS ${PROJECT_EXTRA_COMPONENTS})
    list(APPEND EXTRA_COMPONENT_DIRS "${PROJECT_EXTRA_COMPONENTS}")
endif()

if(IGNORE_EXTRA_COMPONENT)
    foreach(extra_component_dir ${EXTRA_COMPONENT_DIRS})
        # Strip trailing slash so get_filename_component(NAME) returns the
        # component name rather than an empty string.
        string(REGEX REPLACE "/+$" "" extra_component_dir "${extra_component_dir}")
        if(EXISTS "${extra_component_dir}/CMakeLists.txt")
            # BOOTLOADER_EXTRA_COMPONENT_DIRS may point directly to a single component.
            get_filename_component(component "${extra_component_dir}" NAME)
            if(component AND ${component} IN_LIST IGNORE_EXTRA_COMPONENT)
                list(APPEND EXTRA_COMPONENT_EXCLUDE_DIRS "${extra_component_dir}")
            endif()
        else()
            # BOOTLOADER_EXTRA_COMPONENT_DIRS may also point to a directory containing
            # multiple component directories.
            file(GLOB proj_components RELATIVE ${extra_component_dir} ${extra_component_dir}/*)
            foreach(component ${proj_components})
                # Only directories are considered components.
                if(IS_DIRECTORY "${extra_component_dir}/${component}"
                   AND ${component} IN_LIST IGNORE_EXTRA_COMPONENT)
                    list(APPEND EXTRA_COMPONENT_EXCLUDE_DIRS "${extra_component_dir}/${component}")
                endif()
            endforeach()
        endif()
    endforeach()
endif()

# ---------------------------------------------------------------------------
# Include the cmakev2 IDF build framework
# ---------------------------------------------------------------------------
include("${IDF_PATH}/tools/cmakev2/idf.cmake")

project(bootloader C CXX ASM)

# Suppress warning: "Manually-specified variables were not used by the project: SECURE_BOOT_SIGNING_KEY"
set(_ignore_signing_key "${SECURE_BOOT_SIGNING_KEY}")

# ---------------------------------------------------------------------------
# Bootloader-specific build properties set before project initialization
# ---------------------------------------------------------------------------
set(BOOTLOADER_BUILD 1)
set(NON_OS_BUILD 1)
idf_build_set_property(BOOTLOADER_BUILD "${BOOTLOADER_BUILD}")
idf_build_set_property(NON_OS_BUILD "${NON_OS_BUILD}")
# The bootloader subproject has a different set of components and hence
# different Kconfig files. Do not regenerate the main project's sdkconfig.
idf_build_set_property(GENERATE_SDKCONFIG 0)
# Treat the bootloader subproject's own components (main/, components/) as
# IDF components (priority 0) instead of the default project components
# (priority 3).  The bootloader's built-in components are provided by ESP-IDF
# and should be overridable by user-supplied components placed in the
# application's bootloader_components/ directory, which are discovered through
# EXTRA_COMPONENT_DIRS (priority 2).  The cmakev1 build system allowed this
# override because it used a last-one-wins strategy.  In cmakev2 with strict
# priority-based resolution, project_components would always win and prevent
# the override, so we downgrade them to idf_components priority.
idf_build_set_property(PROJECT_COMPONENTS_SOURCE "idf_components")
# Allow lazy optional component requirements evaluation for
# `idf_component_optional_requires`. The bootloader uses a single ESP-IDF
# library, and since it includes `esp_common`, this ensures that not all
# components listed in `esp_common` as optional requirements are pulled into
# the build.
idf_build_set_property(IDF_COMPONENT_OPTIONAL_REQUIRES_MODE DEFERRED)
# The bootloader uses its own compiler optimization flags
# (CONFIG_BOOTLOADER_COMPILER_OPTIMIZATION_*) set below, not the app-level
# CONFIG_COMPILER_OPTIMIZATION_* defaults.
idf_build_set_property(SET_COMPILER_OPTIMIZATION NO)

# Perform internal IDF project initialisation
idf_project_init()

# ---------------------------------------------------------------------------
# Common component requirements
# ---------------------------------------------------------------------------
idf_build_get_property(idf_target_arch IDF_TARGET_ARCH)
set(common_req log esp_rom esp_common esp_hw_support esp_libc ${idf_target_arch})
idf_build_set_property(__COMPONENT_REQUIRES_COMMON "${common_req}")

# ---------------------------------------------------------------------------
# Compiler options and defines specific for bootloader
# ---------------------------------------------------------------------------
if(CONFIG_BOOTLOADER_COMPILER_OPTIMIZATION_SIZE)
    if(CMAKE_C_COMPILER_ID MATCHES "Clang")
        idf_build_set_property(COMPILE_OPTIONS "-Oz" APPEND)
    else()
        idf_build_set_property(COMPILE_OPTIONS "-Os" APPEND)
    endif()
    if(CMAKE_C_COMPILER_ID MATCHES "GNU")
        idf_build_set_property(COMPILE_OPTIONS "-freorder-blocks" APPEND)
        if(CONFIG_IDF_TARGET_ARCH_XTENSA)
            idf_build_set_property(COMPILE_OPTIONS "-mno-target-align" APPEND)
        endif()
    endif()
elseif(CONFIG_BOOTLOADER_COMPILER_OPTIMIZATION_DEBUG)
    idf_build_set_property(COMPILE_OPTIONS "-Og" APPEND)
    if(CMAKE_C_COMPILER_ID MATCHES "GNU" AND NOT CONFIG_IDF_TARGET_LINUX)
        # Disable shrink-wrapping to reduce binary size
        idf_build_set_property(COMPILE_OPTIONS "-fno-shrink-wrap" APPEND)
    endif()
elseif(CONFIG_BOOTLOADER_COMPILER_OPTIMIZATION_PERF)
    idf_build_set_property(COMPILE_OPTIONS "-O2" APPEND)
endif()

idf_build_set_property(COMPILE_DEFINITIONS "BOOTLOADER_BUILD=1" APPEND)
idf_build_set_property(COMPILE_DEFINITIONS "NON_OS_BUILD=1" APPEND)
idf_build_set_property(COMPILE_OPTIONS "-fno-stack-protector" APPEND)

# ---------------------------------------------------------------------------
# Bootloader Linker script
# ---------------------------------------------------------------------------
set(LD_DEFAULT_PATH "${CMAKE_CURRENT_SOURCE_DIR}/main/ld/${IDF_TARGET}")
idf_build_set_property(BOOTLOADER_LINKER_SCRIPT "${LD_DEFAULT_PATH}/bootloader.memory.ld.in" APPEND)
idf_build_set_property(BOOTLOADER_LINKER_SCRIPT "${LD_DEFAULT_PATH}/bootloader.sections.ld.in" APPEND)

# ---------------------------------------------------------------------------
# Build the bootloader ELF
# ---------------------------------------------------------------------------
# Build system v2 discovers components from EXTRA_COMPONENT_DIRS (the application's
# bootloader_components/ and BOOTLOADER_EXTRA_COMPONENT_DIRS entries) but does
# not link them as idf_build_executable only links the COMPONENTS list and its
# REQUIRES field; i.e, main and its dependencies. The application has no way
# to link the bootloader with these extra components. So promote every project_extra_components
# tagged component source to a root component here.
set(_bootloader_root_components main)
idf_build_get_property(_discovered_components COMPONENTS_DISCOVERED)
foreach(component IN LISTS _discovered_components)
    idf_component_get_property(_src "${component}" COMPONENT_SOURCE)
    if(_src STREQUAL "project_extra_components")
        list(APPEND _bootloader_root_components ${component})
    endif()
endforeach()
list(REMOVE_DUPLICATES _bootloader_root_components)

idf_build_executable(bootloader_elf
                     NAME bootloader
                     COMPONENTS ${_bootloader_root_components}
                     MAPFILE_TARGET bootloader_mapfile)

# ---------------------------------------------------------------------------
# Binary generation and post-build messages
#
# Collect tool command lists from the esptool_py component so that we can
# assemble human-readable flash / key-management commands for the developer.
# ---------------------------------------------------------------------------
# Build display strings for the post-build message
idf_component_get_property(esptool_py_cmd esptool_py ESPTOOLPY_CMD)
idf_component_get_property(main_args esptool_py FLASH_ARGS)
idf_component_get_property(sub_args esptool_py FLASH_SUB_ARGS)
idf_component_get_property(espsecure_py_cmd esptool_py ESPSECUREPY_CMD)
idf_component_get_property(espefuse_py_cmd esptool_py ESPEFUSEPY_CMD)

# String for printing flash command
string(REPLACE ";" " " esptoolpy_write_flash
    "${esptool_py_cmd} --port=(PORT) --baud=(BAUD) ${main_args} write-flash ${sub_args}")

string(REPLACE ";" " " espsecurepy "${espsecure_py_cmd}")
string(REPLACE ";" " " espefusepy "${espefuse_py_cmd}")

# ---------------------------------------------------------------------------
# Guard: if binary generation is disabled or esptool_py is not part of the
# build, there is no binary to produce and no flash commands to print, so we
# stop here.
# ---------------------------------------------------------------------------
if(NOT CONFIG_APP_BUILD_GENERATE_BINARIES OR NOT TARGET idf::esptool_py)
    # Binaries should not be generated; there's nothing else to do.
    return()

# ---------------------------------------------------------------------------
# No secure boot
#
# Convert the ELF directly to a flat binary.  The size check target
# (idf_check_bootloader_size) verifies that the binary fits within the
# region reserved for the bootloader in the partition table / flash layout.
# idf_build_generate_metadata emits a JSON metadata file consumed by the
# top-level build and flash tooling.
# ---------------------------------------------------------------------------
elseif(NOT CONFIG_SECURE_BOOT)
    # No bootloader signing is required
    idf_build_binary(bootloader_elf
                     OUTPUT_FILE "${CMAKE_BINARY_DIR}/bootloader.bin"
                     TARGET bootloader_bin
                     ALL)
    idf_check_bootloader_size(bootloader_bin)
    idf_build_generate_metadata(EXECUTABLE bootloader_elf)

# ---------------------------------------------------------------------------
# Secure Boot V1
#
# Secure Boot V1 uses a symmetric HMAC-SHA256 key burned into eFuse.  Two
# sub-modes are supported:
#
#   ONE_TIME_FLASH – the bootloader is flashed once and can never be
#     replaced on the same device.  The developer is shown the exact
#     esptool.py command to use for the initial flash.
#
#   REFLASHABLE – a symmetric key is derived from the private signing key
#     and burned into eFuse.  Later reflashes are accepted only if the
#     bootloader image is bundled with a digest computed using the same
#     derived key.  This mode requires generating the derived key file
#     (secure-bootloader-key-<N>.bin) from the project signing key, and a
#     bootloader digest image (bootloader-reflash-digest.bin) that prepends
#     the bootloader binary with the matching digest.
# ---------------------------------------------------------------------------
elseif(CONFIG_SECURE_BOOT_V1_ENABLED)

    idf_build_binary(bootloader_elf
                     OUTPUT_FILE "${CMAKE_BINARY_DIR}/bootloader.bin"
                     TARGET bootloader_bin
                     ALL)
    idf_check_bootloader_size(bootloader_bin)
    idf_build_generate_metadata(EXECUTABLE bootloader_elf)

    if(CONFIG_SECURE_BOOTLOADER_ONE_TIME_FLASH)

        idf_target_post_build_msg(bootloader_bin
            "=============================================================================="
            "Bootloader built. Secure boot enabled, so bootloader not flashed automatically."
            "One-time flash command is:"
            "\t${esptoolpy_write_flash} ${BOOTLOADER_OFFSET} ${CMAKE_BINARY_DIR}/bootloader.bin"
            "* IMPORTANT: After first boot, BOOTLOADER CANNOT BE RE-FLASHED on same device"
        )

    elseif(CONFIG_SECURE_BOOTLOADER_REFLASHABLE)

        # Derived key length depends on encoding: 192-bit or 256-bit (default).
        if(CONFIG_SECURE_BOOTLOADER_KEY_ENCODING_192BIT)
            set(key_digest_len 192)
        else()
            set(key_digest_len 256)
        endif()

        set(bootloader_digest_bin "${CMAKE_BINARY_DIR}/bootloader-reflash-digest.bin")
        set(secure_bootloader_key "${CMAKE_BINARY_DIR}/secure-bootloader-key-${key_digest_len}.bin")

        # Derive the symmetric eFuse key from the ECDSA signing key.
        # The resulting binary is burned into the SECURE_BOOT_V1 eFuse block.
        add_custom_command(OUTPUT "${secure_bootloader_key}"
            COMMAND ${espsecure_py_cmd} digest-private-key
                --keylen "${key_digest_len}"
                --keyfile "${SECURE_BOOT_SIGNING_KEY}"
                "${secure_bootloader_key}"
            DEPENDS "${SECURE_BOOT_SIGNING_KEY}"
            VERBATIM)

        if(NOT CONFIG_SECURE_BOOT_BUILD_SIGNED_BINARIES AND NOT EXISTS "${secure_bootloader_key}")
            idf_die(
                "No pre-generated key for a reflashable secure bootloader is available, "
                "due to signing configuration."
                "\nTo generate one, you can use this command:"
                "\n\t${espsecurepy} generate-flash-encryption-key ${secure_bootloader_key}"
                "\nIf a signing key is present, then instead use:"
                "\n\t${espsecurepy} digest-private-key "
                "--keylen (192/256) --keyfile KEYFILE "
                "${secure_bootloader_key}")
        endif()

        # Produce the reflash-digest image: the bootloader binary prefixed
        # with a digest computed using the derived symmetric key.
        add_custom_command(OUTPUT "${bootloader_digest_bin}"
            COMMAND ${CMAKE_COMMAND} -E echo "DIGEST ${bootloader_digest_bin}"
            COMMAND ${espsecure_py_cmd} digest-secure-bootloader --keyfile "${secure_bootloader_key}"
            -o "${bootloader_digest_bin}" "${CMAKE_BINARY_DIR}/bootloader.bin"
            DEPENDS "${secure_bootloader_key}" bootloader_bin
            VERBATIM)

        add_custom_target(bootloader_digest_bin ALL DEPENDS "${bootloader_digest_bin}")

        idf_target_post_build_msg(bootloader_bin
            "=============================================================================="
            "Bootloader built and secure digest generated."
            "Secure boot enabled, so bootloader not flashed automatically."
            "Burn secure boot key to efuse using:"
            "\t${espefusepy} burn-key secure_boot_v1 ${secure_bootloader_key}"
            "First time flash command is:"
            "\t${esptoolpy_write_flash} ${BOOTLOADER_OFFSET} ${CMAKE_BINARY_DIR}/bootloader.bin"
            "=============================================================================="
            "To reflash the bootloader after initial flash:"
            "\t${esptoolpy_write_flash} 0x0 ${bootloader_digest_bin}"
            "=============================================================================="
            "* After first boot, only re-flashes of this kind (with same key) will be accepted."
            "* Not recommended to reuse the same secure boot keyfile on multiple production devices."
        )
    endif()

# ---------------------------------------------------------------------------
# Secure Boot V2
#
# Secure Boot V2 uses asymmetric signing. The bootloader ELF is first created
# as an *unsigned* flat binary; then, depending on
# CONFIG_SECURE_BOOT_BUILD_SIGNED_BINARIES, it is either signed in-place
# (producing bootloader.bin) or left unsigned for the developer to sign
# externally.
#
# Multiple signing keys – devices with more than one eFuse key digest slot
# (CONFIG_SOC_EFUSE_SECURE_BOOT_KEY_DIGESTS > 1) may have additional keys
# appended to the signature block after the build; instructions for doing so
# are printed in the post-build message.
#
# CONFIG_SECURE_BOOT_FLASH_BOOTLOADER_DEFAULT – when set, the bootloader is
# flashed automatically by the normal flash target and no extra instructions
# are needed, so the post-build message block is skipped.
# ---------------------------------------------------------------------------
elseif(CONFIG_SECURE_BOOT_V2_ENABLED)
    if(CONFIG_SECURE_BOOT_BUILD_SIGNED_BINARIES)
        # When signing during build, produce the raw binary as
        # bootloader-unsigned.bin and then sign it into bootloader.bin.
        set(bootloader_unsigned_bin "bootloader-unsigned.bin")
    else()
        # Without build-time signing, produce the binary directly as
        # bootloader.bin (matching v1 behavior). The user is expected
        # to sign it externally before flashing.
        set(bootloader_unsigned_bin "bootloader.bin")
    endif()

    idf_build_binary(bootloader_elf
                     OUTPUT_FILE "${CMAKE_BINARY_DIR}/${bootloader_unsigned_bin}"
                     TARGET bootloader_unsigned_bin
                     ALL)
    idf_check_bootloader_size(bootloader_unsigned_bin)

    if(CONFIG_SECURE_BOOT_BUILD_SIGNED_BINARIES)
        if(NOT EXISTS "${SECURE_BOOT_SIGNING_KEY}")
            idf_die(
                "Secure Boot Signing Key Not found."
                "\nGenerate the Secure Boot V2 RSA-PSS 3072 Key."
                "\nTo generate one, you can use this command:"
                "\n\t${espsecurepy} generate-signing-key --version 2 your_key.pem"
            )
        endif()

        # Sign the unsigned binary with the configured signing key and
        # write the signed image to bootloader.bin.
        idf_sign_binary(bootloader_unsigned_bin
                        OUTPUT_FILE "${CMAKE_BINARY_DIR}/bootloader.bin"
                        TARGET bootloader_signed_bin
                        KEYFILE "${SECURE_BOOT_SIGNING_KEY}"
                        ALL)
        idf_check_bootloader_size(bootloader_signed_bin)

        set(post_build_target bootloader_signed_bin)
    else()
        # Signing will be performed externally; communicate this to the user.
        set(post_build_target bootloader_unsigned_bin)
        idf_target_post_build_msg(bootloader_unsigned_bin
            "Bootloader generated but not signed"
        )
    endif()

    # Emit post-build flash instructions.  The message varies depending on
    # whether the device has multiple eFuse key digest slots and whether the
    # bootloader is flashed by default.
    if(CONFIG_SOC_EFUSE_SECURE_BOOT_KEY_DIGESTS GREATER 1
            AND NOT CONFIG_SECURE_BOOT_FLASH_BOOTLOADER_DEFAULT)
        idf_target_post_build_msg(${post_build_target}
            "=============================================================================="
            "Bootloader built. Secure boot enabled, so bootloader not flashed automatically."
            "To sign the bootloader with additional private keys."
            "\t${espsecurepy} sign-data -k secure_boot_signing_key2.pem -v 2 \
            --append-signatures -o signed_bootloader.bin build/bootloader/bootloader.bin"
            "Secure boot enabled, so bootloader not flashed automatically."
            "\t${esptoolpy_write_flash} ${BOOTLOADER_OFFSET} ${CMAKE_BINARY_DIR}/bootloader.bin"
            "=============================================================================="
        )
    elseif(NOT CONFIG_SECURE_BOOT_FLASH_BOOTLOADER_DEFAULT)
        idf_target_post_build_msg(${post_build_target}
            "=============================================================================="
            "Bootloader built. Secure boot enabled, so bootloader not flashed automatically."
            "\t${esptoolpy_write_flash} ${BOOTLOADER_OFFSET} ${CMAKE_BINARY_DIR}/bootloader.bin"
            "=============================================================================="
        )
    endif()

    idf_build_generate_metadata(EXECUTABLE bootloader_elf)
endif()
