# Copyright (c) 2024 Intel Corporation # SPDX-License-Identifier: Apache-2.0 # This script generates a tarball containing all headers and flags necessary to # build an llext extension. It does so by copying all headers accessible from # INTERFACE_INCLUDE_DIRECTORIES and generating a Makefile.cflags file (and a # cmake.cflags one) with all flags necessary to build the extension. # # The tarball can be extracted and used in the extension build system to include # all necessary headers and flags. File paths are made relative to a few key # directories (build/zephyr, zephyr base, west top dir and application source # dir), to avoid leaking any information about the host system. # # The script expects a build_info.yml file in the project binary directory. # This file should contain the following entries: # - cmake application source-dir # - cmake board name # - cmake board qualifiers # - cmake board revision # - cmake llext-edk cflags # - cmake llext-edk file # - cmake llext-edk include-dirs # - west topdir cmake_minimum_required(VERSION 3.20.0) # initialize the same paths as the main CMakeLists.txt for consistency set(PROJECT_BINARY_DIR ${CMAKE_BINARY_DIR}) set(ZEPHYR_BASE ${CMAKE_CURRENT_LIST_DIR}/../) list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/modules") include(extensions) include(yaml) # Usage: # relative_dir( ) # # Helper function to generate relative paths to a few key directories # (PROJECT_BINARY_DIR, ZEPHYR_BASE, WEST_TOPDIR and APPLICATION_SOURCE_DIR). # The generated path is relative to the key directory, and the bindir_out # output variable is set to TRUE if the path is relative to PROJECT_BINARY_DIR. # function(relative_dir dir relative_out bindir_out) cmake_path(IS_PREFIX PROJECT_BINARY_DIR ${dir} NORMALIZE to_prj_bindir) cmake_path(IS_PREFIX ZEPHYR_BASE ${dir} NORMALIZE to_zephyr_base) if("${WEST_TOPDIR}" STREQUAL "") set(to_west_topdir FALSE) else() cmake_path(IS_PREFIX WEST_TOPDIR ${dir} NORMALIZE to_west_topdir) endif() cmake_path(IS_PREFIX APPLICATION_SOURCE_DIR ${dir} NORMALIZE to_app_srcdir) # Overall idea is to place included files in the destination dir based on the source: # files coming from build/zephyr/generated will end up at # /include/zephyr/include/generated, files coming from zephyr base at # /include/zephyr/include, files from west top dir (for instance, hal modules), # at /include and application ones at /include/. # Finally, everything else (such as external libs not at any of those places) will end up # at /include/, so we avoid any external lib # stepping at any other lib toes. if(to_prj_bindir) cmake_path(RELATIVE_PATH dir BASE_DIRECTORY ${PROJECT_BINARY_DIR} OUTPUT_VARIABLE dir_tmp) set(dest ${llext_edk_inc}/zephyr/${dir_tmp}) elseif(to_zephyr_base) cmake_path(RELATIVE_PATH dir BASE_DIRECTORY ${ZEPHYR_BASE} OUTPUT_VARIABLE dir_tmp) set(dest ${llext_edk_inc}/zephyr/${dir_tmp}) elseif(to_west_topdir) cmake_path(RELATIVE_PATH dir BASE_DIRECTORY ${WEST_TOPDIR} OUTPUT_VARIABLE dir_tmp) set(dest ${llext_edk_inc}/${dir_tmp}) elseif(to_app_srcdir) cmake_path(GET APPLICATION_SOURCE_DIR FILENAME app_dir) cmake_path(RELATIVE_PATH dir BASE_DIRECTORY ${APPLICATION_SOURCE_DIR} OUTPUT_VARIABLE dir_tmp) set(dest ${llext_edk_inc}/${app_dir}/${dir_tmp}) else() set(dest ${llext_edk_inc}/${dir}) endif() set(${relative_out} ${dest} PARENT_SCOPE) if(to_prj_bindir) set(${bindir_out} TRUE PARENT_SCOPE) else() set(${bindir_out} FALSE PARENT_SCOPE) endif() endfunction() # Usage: # edk_escape( ) # # Escape problematic characters in the string and store the result in # . The escaping is done to make the string suitable for . function(edk_escape target str_in str_out) string(REPLACE "\\" "\\\\" str_escaped "${str_in}") string(REPLACE "\"" "\\\"" str_escaped "${str_escaped}") set(${str_out} "${str_escaped}" PARENT_SCOPE) endfunction() # Usage: # edk_write_header() # # Initialize the file associated with and write its header. function(edk_write_header target) file(WRITE ${edk_file_${target}} "") endfunction() # Usage: # edk_write_comment( ) # # Write to the file associated with the string as a comment. function(edk_write_comment target text) file(APPEND ${edk_file_${target}} "\n# ${text}\n") endfunction() # Usage: # edk_write_var( ) # # Write to the file associated with an entry where is # assigned the value . function(edk_write_var target var_name var_value) if(target STREQUAL "CMAKE") # CMake: export assignments of the form: # # set(var "value1;value2;...") # set(DASHIMACROS "-imacros\${CMAKE_CURRENT_LIST_DIR}/") set(DASHI "-I\${CMAKE_CURRENT_LIST_DIR}/") edk_escape(${target} "${var_value}" var_value) string(CONFIGURE "${var_value}" exp_var_value @ONLY) # The list is otherwise exported verbatim, surrounded by quotes. file(APPEND ${edk_file_${target}} "set(${var_name} \"${exp_var_value}\")\n") elseif(target STREQUAL "MAKEFILE") # Makefile: export assignments of the form: # # var = "value1" "value2" ... # set(DASHIMACROS "-imacros\$(${install_dir_var})/") set(DASHI "-I\$(${install_dir_var})/") edk_escape(${target} "${var_value}" var_value) string(CONFIGURE "${var_value}" exp_var_value @ONLY) # Each element of the list is wrapped in quotes and is separated by a space. list(JOIN exp_var_value "\" \"" exp_var_value_str) file(APPEND ${edk_file_${target}} "${var_name} = \"${exp_var_value_str}\"\n") endif() endfunction() # read in computed build configuration import_kconfig(CONFIG ${PROJECT_BINARY_DIR}/.config) if (CONFIG_LLEXT_EXPORT_BUILTINS_BY_SLID) message(FATAL_ERROR "The LLEXT EDK is not compatible with CONFIG_LLEXT_EXPORT_BUILTINS_BY_SLID.") endif() set(build_info_file ${PROJECT_BINARY_DIR}/../build_info.yml) yaml_load(FILE ${build_info_file} NAME build_info) yaml_get(llext_edk_cflags NAME build_info KEY cmake llext-edk cflags) yaml_get(llext_edk_file NAME build_info KEY cmake llext-edk file) yaml_get(INTERFACE_INCLUDE_DIRECTORIES NAME build_info KEY cmake llext-edk include-dirs) yaml_get(APPLICATION_SOURCE_DIR NAME build_info KEY cmake application source-dir) yaml_get(WEST_TOPDIR NAME build_info KEY west topdir) yaml_get(board_name NAME build_info KEY cmake board name) yaml_get(board_qualifiers NAME build_info KEY cmake board qualifiers) yaml_get(board_revision NAME build_info KEY cmake board revision) zephyr_build_string(normalized_board_target BOARD ${board_name} BOARD_QUALIFIERS ${board_qualifiers}) set(llext_edk_name ${CONFIG_LLEXT_EDK_NAME}) set(llext_edk ${PROJECT_BINARY_DIR}/${llext_edk_name}) set(llext_edk_inc ${llext_edk}/include) zephyr_string(SANITIZE TOUPPER var_prefix ${llext_edk_name}) set(install_dir_var "${var_prefix}_INSTALL_DIR") set(make_relative FALSE) foreach(flag ${llext_edk_cflags}) # Detect all combinations of 'imacros' flag: # - with one or two preceding dashes # - separated from the argument, joined by '=', or joined (no separator) if(flag MATCHES "^--?imacros$") # imacros followed by a space, convert next argument set(make_relative TRUE) continue() elseif(flag MATCHES "^--?imacros=?([^=].*)$") # imacros= or imacros, immediately convert set(flag ${CMAKE_MATCH_1}) set(make_relative TRUE) endif() if(make_relative) set(make_relative FALSE) cmake_path(GET flag PARENT_PATH parent) cmake_path(GET flag FILENAME name) relative_dir(${parent} dest bindir) cmake_path(RELATIVE_PATH dest BASE_DIRECTORY ${llext_edk} OUTPUT_VARIABLE dest_rel) if(bindir) list(APPEND imacros_gen "@DASHIMACROS@${dest_rel}/${name}") else() list(APPEND imacros "@DASHIMACROS@${dest_rel}/${name}") endif() else() list(APPEND new_cflags ${flag}) endif() endforeach() set(llext_edk_cflags ${new_cflags}) list(APPEND base_flags ${llext_edk_cflags} ${imacros}) file(MAKE_DIRECTORY ${llext_edk_inc}) foreach(dir ${INTERFACE_INCLUDE_DIRECTORIES}) if (NOT EXISTS ${dir}) continue() endif() relative_dir(${dir} dest bindir) # Use destination parent, as the last part of the source directory is copied as well cmake_path(GET dest PARENT_PATH dest_p) file(MAKE_DIRECTORY ${dest_p}) file(COPY ${dir} DESTINATION ${dest_p} FILES_MATCHING PATTERN "*.h") cmake_path(RELATIVE_PATH dest BASE_DIRECTORY ${llext_edk} OUTPUT_VARIABLE dest_rel) if(bindir) list(APPEND gen_inc_flags "@DASHI@${dest_rel}") else() list(APPEND inc_flags "@DASHI@${dest_rel}") endif() list(APPEND all_inc_flags "@DASHI@${dest_rel}") endforeach() list(APPEND all_flags ${base_flags} ${imacros_gen} ${all_inc_flags}) if(CONFIG_LLEXT_EDK_USERSPACE_ONLY) # Copy syscall headers from edk directory, as they were regenerated there. file(COPY ${PROJECT_BINARY_DIR}/edk/include/generated/ DESTINATION ${llext_edk_inc}/zephyr/include/generated) endif() # # Generate the EDK flags files # set(edk_targets MAKEFILE CMAKE) set(edk_file_MAKEFILE ${llext_edk}/Makefile.cflags) set(edk_file_CMAKE ${llext_edk}/cmake.cflags) foreach(target ${edk_targets}) edk_write_header(${target}) edk_write_comment(${target} "Target information") edk_write_var(${target} "${var_prefix}_BOARD_NAME" "${board_name}") edk_write_var(${target} "${var_prefix}_BOARD_QUALIFIERS" "${board_qualifiers}") edk_write_var(${target} "${var_prefix}_BOARD_REVISION" "${board_revision}") edk_write_var(${target} "${var_prefix}_BOARD_TARGET" "${normalized_board_target}") edk_write_comment(${target} "Compile flags") edk_write_var(${target} "LLEXT_CFLAGS" "${all_flags}") edk_write_var(${target} "LLEXT_ALL_INCLUDE_CFLAGS" "${all_inc_flags}") edk_write_var(${target} "LLEXT_INCLUDE_CFLAGS" "${inc_flags}") edk_write_var(${target} "LLEXT_GENERATED_INCLUDE_CFLAGS" "${gen_inc_flags}") edk_write_var(${target} "LLEXT_BASE_CFLAGS" "${base_flags}") edk_write_var(${target} "LLEXT_GENERATED_IMACROS_CFLAGS" "${imacros_gen}") endforeach() if(CONFIG_LLEXT_EDK_FORMAT_TAR_XZ) set(llext_edk_format FORMAT gnutar COMPRESSION XZ) elseif(CONFIG_LLEXT_EDK_FORMAT_TAR_ZSTD) set(llext_edk_format FORMAT gnutar COMPRESSION Zstd) elseif(CONFIG_LLEXT_EDK_FORMAT_ZIP) set(llext_edk_format FORMAT zip) else() message(FATAL_ERROR "Unsupported LLEXT_EDK_FORMAT choice") endif() # Generate the tarball file(ARCHIVE_CREATE OUTPUT ${llext_edk_file} PATHS ${llext_edk} ${llext_edk_format} ) file(REMOVE_RECURSE ${llext_edk})