Files
zephyr/cmake/llext-edk.cmake
Jordan Yates b4a8035433 llext: option to preserve EDK folder
Add an option to preserve the EDK folder, instead of deleting it after
creating the archive. This can simplify test scripting which can now
avoid immediately uncompressing the archive when compiling an
extension.

If this option is enabled, default to the much faster `.tar.Z` archive
format, since the compression ratio of the archive is not important.

Signed-off-by: Jordan Yates <jordan@embeint.com>
2025-11-17 09:21:56 -05:00

287 lines
11 KiB
CMake

# 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(<dir> <relative_out> <bindir_out>)
#
# 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
# <install-dir>/include/zephyr/include/generated, files coming from zephyr base at
# <install-dir>/include/zephyr/include, files from west top dir (for instance, hal modules),
# at <install-dir>/include and application ones at <install-dir>/include/<application-dir>.
# Finally, everything else (such as external libs not at any of those places) will end up
# at <install-dir>/include/<full-path-to-external-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(<target> <str_in> <str_out>)
#
# Escape problematic characters in the string <str_in> and store the result in
# <str_out>. The escaping is done to make the string suitable for <target>.
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(<target>)
#
# Initialize the file associated with <target> and write its header.
function(edk_write_header target)
file(WRITE ${edk_file_${target}} "")
endfunction()
# Usage:
# edk_write_comment(<target> <text>)
#
# Write to the file associated with <target> the string <text> as a comment.
function(edk_write_comment target text)
file(APPEND ${edk_file_${target}} "\n# ${text}\n")
endfunction()
# Usage:
# edk_write_var(<target> <var_name> <var_value>)
#
# Write to the file associated with <target> an entry where <var_name> is
# assigned the value <var_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=<stuff> or imacros<stuff>, immediately convert <stuff>
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}
)
if(NOT CONFIG_LLEXT_EDK_PRESERVE_INPUT_FOLDER)
file(REMOVE_RECURSE ${llext_edk})
endif()