1# Copyright (c) 2024 Intel Corporation
2# SPDX-License-Identifier: Apache-2.0
3
4# This script generates a tarball containing all headers and flags necessary to
5# build an llext extension. It does so by copying all headers accessible from
6# INTERFACE_INCLUDE_DIRECTORIES and generating a Makefile.cflags file (and a
7# cmake.cflags one) with all flags necessary to build the extension.
8#
9# The tarball can be extracted and used in the extension build system to include
10# all necessary headers and flags. File paths are made relative to a few key
11# directories (build/zephyr, zephyr base, west top dir and application source
12# dir), to avoid leaking any information about the host system.
13#
14# The script expects a build_info.yml file in the project binary directory.
15# This file should contain the following entries:
16#  - cmake application source-dir
17#  - cmake board name
18#  - cmake board qualifiers
19#  - cmake board revision
20#  - cmake llext-edk cflags
21#  - cmake llext-edk file
22#  - cmake llext-edk include-dirs
23#  - west topdir
24
25cmake_minimum_required(VERSION 3.20.0)
26
27# initialize the same paths as the main CMakeLists.txt for consistency
28set(PROJECT_BINARY_DIR ${CMAKE_BINARY_DIR})
29set(ZEPHYR_BASE ${CMAKE_CURRENT_LIST_DIR}/../)
30list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/modules")
31
32include(extensions)
33include(yaml)
34
35# Usage:
36#   relative_dir(<dir> <relative_out> <bindir_out>)
37#
38# Helper function to generate relative paths to a few key directories
39# (PROJECT_BINARY_DIR, ZEPHYR_BASE, WEST_TOPDIR and APPLICATION_SOURCE_DIR).
40# The generated path is relative to the key directory, and the bindir_out
41# output variable is set to TRUE if the path is relative to PROJECT_BINARY_DIR.
42#
43function(relative_dir dir relative_out bindir_out)
44    cmake_path(IS_PREFIX PROJECT_BINARY_DIR ${dir} NORMALIZE to_prj_bindir)
45    cmake_path(IS_PREFIX ZEPHYR_BASE ${dir} NORMALIZE to_zephyr_base)
46    if("${WEST_TOPDIR}" STREQUAL "")
47        set(to_west_topdir FALSE)
48    else()
49        cmake_path(IS_PREFIX WEST_TOPDIR ${dir} NORMALIZE to_west_topdir)
50    endif()
51    cmake_path(IS_PREFIX APPLICATION_SOURCE_DIR ${dir} NORMALIZE to_app_srcdir)
52
53    # Overall idea is to place included files in the destination dir based on the source:
54    # files coming from build/zephyr/generated will end up at
55    # <install-dir>/include/zephyr/include/generated, files coming from zephyr base at
56    # <install-dir>/include/zephyr/include, files from west top dir (for instance, hal modules),
57    # at <install-dir>/include and application ones at <install-dir>/include/<application-dir>.
58    # Finally, everything else (such as external libs not at any of those places) will end up
59    # at <install-dir>/include/<full-path-to-external-include>, so we avoid any external lib
60    # stepping at any other lib toes.
61    if(to_prj_bindir)
62        cmake_path(RELATIVE_PATH dir BASE_DIRECTORY ${PROJECT_BINARY_DIR} OUTPUT_VARIABLE dir_tmp)
63        set(dest ${llext_edk_inc}/zephyr/${dir_tmp})
64    elseif(to_zephyr_base)
65        cmake_path(RELATIVE_PATH dir BASE_DIRECTORY ${ZEPHYR_BASE} OUTPUT_VARIABLE dir_tmp)
66        set(dest ${llext_edk_inc}/zephyr/${dir_tmp})
67    elseif(to_west_topdir)
68        cmake_path(RELATIVE_PATH dir BASE_DIRECTORY ${WEST_TOPDIR} OUTPUT_VARIABLE dir_tmp)
69        set(dest ${llext_edk_inc}/${dir_tmp})
70    elseif(to_app_srcdir)
71        cmake_path(GET APPLICATION_SOURCE_DIR FILENAME app_dir)
72        cmake_path(RELATIVE_PATH dir BASE_DIRECTORY ${APPLICATION_SOURCE_DIR} OUTPUT_VARIABLE dir_tmp)
73        set(dest ${llext_edk_inc}/${app_dir}/${dir_tmp})
74    else()
75        set(dest ${llext_edk_inc}/${dir})
76    endif()
77
78    set(${relative_out} ${dest} PARENT_SCOPE)
79    if(to_prj_bindir)
80        set(${bindir_out} TRUE PARENT_SCOPE)
81    else()
82        set(${bindir_out} FALSE PARENT_SCOPE)
83    endif()
84endfunction()
85
86# Usage:
87#   edk_escape(<target> <str_in> <str_out>)
88#
89# Escape problematic characters in the string <str_in> and store the result in
90# <str_out>. The escaping is done to make the string suitable for <target>.
91function(edk_escape target str_in str_out)
92    string(REPLACE "\\" "\\\\" str_escaped "${str_in}")
93    string(REPLACE "\"" "\\\"" str_escaped "${str_escaped}")
94    set(${str_out} "${str_escaped}" PARENT_SCOPE)
95endfunction()
96
97# Usage:
98#   edk_write_header(<target>)
99#
100# Initialize the file associated with <target> and write its header.
101function(edk_write_header target)
102    file(WRITE ${edk_file_${target}} "")
103endfunction()
104
105# Usage:
106#   edk_write_comment(<target> <text>)
107#
108# Write to the file associated with <target> the string <text> as a comment.
109function(edk_write_comment target text)
110    file(APPEND ${edk_file_${target}} "\n# ${text}\n")
111endfunction()
112
113# Usage:
114#   edk_write_var(<target> <var_name> <var_value>)
115#
116# Write to the file associated with <target> an entry where <var_name> is
117# assigned the value <var_value>.
118function(edk_write_var target var_name var_value)
119    if(target STREQUAL "CMAKE")
120        # CMake: export assignments of the form:
121        #
122        #   set(var "value1;value2;...")
123        #
124        set(DASHIMACROS "-imacros\${CMAKE_CURRENT_LIST_DIR}/")
125        set(DASHI "-I\${CMAKE_CURRENT_LIST_DIR}/")
126        edk_escape(${target} "${var_value}" var_value)
127        string(CONFIGURE "${var_value}" exp_var_value @ONLY)
128        # The list is otherwise exported verbatim, surrounded by quotes.
129        file(APPEND ${edk_file_${target}} "set(${var_name} \"${exp_var_value}\")\n")
130    elseif(target STREQUAL "MAKEFILE")
131        # Makefile: export assignments of the form:
132        #
133        #   var = "value1" "value2" ...
134        #
135        set(DASHIMACROS "-imacros\$(${install_dir_var})/")
136        set(DASHI "-I\$(${install_dir_var})/")
137        edk_escape(${target} "${var_value}" var_value)
138        string(CONFIGURE "${var_value}" exp_var_value @ONLY)
139        # Each element of the list is wrapped in quotes and is separated by a space.
140        list(JOIN exp_var_value "\" \"" exp_var_value_str)
141        file(APPEND ${edk_file_${target}} "${var_name} = \"${exp_var_value_str}\"\n")
142    endif()
143endfunction()
144
145
146
147# read in computed build configuration
148import_kconfig(CONFIG ${PROJECT_BINARY_DIR}/.config)
149
150if (CONFIG_LLEXT_EXPORT_BUILTINS_BY_SLID)
151  message(FATAL_ERROR
152    "The LLEXT EDK is not compatible with CONFIG_LLEXT_EXPORT_BUILTINS_BY_SLID.")
153endif()
154
155set(build_info_file ${PROJECT_BINARY_DIR}/../build_info.yml)
156yaml_load(FILE ${build_info_file} NAME build_info)
157
158yaml_get(llext_edk_cflags NAME build_info KEY cmake llext-edk cflags)
159yaml_get(llext_edk_file NAME build_info KEY cmake llext-edk file)
160yaml_get(INTERFACE_INCLUDE_DIRECTORIES NAME build_info KEY cmake llext-edk include-dirs)
161yaml_get(APPLICATION_SOURCE_DIR NAME build_info KEY cmake application source-dir)
162yaml_get(WEST_TOPDIR NAME build_info KEY west topdir)
163
164yaml_get(board_name NAME build_info KEY cmake board name)
165yaml_get(board_qualifiers NAME build_info KEY cmake board qualifiers)
166yaml_get(board_revision NAME build_info KEY cmake board revision)
167zephyr_build_string(normalized_board_target
168    BOARD ${board_name}
169    BOARD_QUALIFIERS ${board_qualifiers})
170
171set(llext_edk_name ${CONFIG_LLEXT_EDK_NAME})
172set(llext_edk ${PROJECT_BINARY_DIR}/${llext_edk_name})
173set(llext_edk_inc ${llext_edk}/include)
174
175zephyr_string(SANITIZE TOUPPER var_prefix ${llext_edk_name})
176set(install_dir_var "${var_prefix}_INSTALL_DIR")
177
178set(make_relative FALSE)
179foreach(flag ${llext_edk_cflags})
180    # Detect all combinations of 'imacros' flag:
181    # - with one or two preceding dashes
182    # - separated from the argument, joined by '=', or joined (no separator)
183    if(flag MATCHES "^--?imacros$")
184        # imacros followed by a space, convert next argument
185        set(make_relative TRUE)
186        continue()
187    elseif(flag MATCHES "^--?imacros=?([^=].*)$")
188        # imacros=<stuff> or imacros<stuff>, immediately convert <stuff>
189        set(flag ${CMAKE_MATCH_1})
190        set(make_relative TRUE)
191    endif()
192
193    if(make_relative)
194        set(make_relative FALSE)
195        cmake_path(GET flag PARENT_PATH parent)
196        cmake_path(GET flag FILENAME name)
197        relative_dir(${parent} dest bindir)
198        cmake_path(RELATIVE_PATH dest BASE_DIRECTORY ${llext_edk} OUTPUT_VARIABLE dest_rel)
199        if(bindir)
200            list(APPEND imacros_gen "@DASHIMACROS@${dest_rel}/${name}")
201        else()
202            list(APPEND imacros "@DASHIMACROS@${dest_rel}/${name}")
203        endif()
204    else()
205        list(APPEND new_cflags ${flag})
206    endif()
207endforeach()
208set(llext_edk_cflags ${new_cflags})
209
210list(APPEND base_flags ${llext_edk_cflags} ${imacros})
211
212file(MAKE_DIRECTORY ${llext_edk_inc})
213foreach(dir ${INTERFACE_INCLUDE_DIRECTORIES})
214    if (NOT EXISTS ${dir})
215        continue()
216    endif()
217
218    relative_dir(${dir} dest bindir)
219    # Use destination parent, as the last part of the source directory is copied as well
220    cmake_path(GET dest PARENT_PATH dest_p)
221
222    file(MAKE_DIRECTORY ${dest_p})
223    file(COPY ${dir} DESTINATION ${dest_p} FILES_MATCHING PATTERN "*.h")
224
225    cmake_path(RELATIVE_PATH dest BASE_DIRECTORY ${llext_edk} OUTPUT_VARIABLE dest_rel)
226    if(bindir)
227        list(APPEND gen_inc_flags "@DASHI@${dest_rel}")
228    else()
229        list(APPEND inc_flags "@DASHI@${dest_rel}")
230    endif()
231    list(APPEND all_inc_flags "@DASHI@${dest_rel}")
232endforeach()
233
234list(APPEND all_flags ${base_flags} ${imacros_gen} ${all_inc_flags})
235
236if(CONFIG_LLEXT_EDK_USERSPACE_ONLY)
237    # Copy syscall headers from edk directory, as they were regenerated there.
238    file(COPY ${PROJECT_BINARY_DIR}/edk/include/generated/ DESTINATION ${llext_edk_inc}/zephyr/include/generated)
239endif()
240
241#
242# Generate the EDK flags files
243#
244
245set(edk_targets MAKEFILE CMAKE)
246set(edk_file_MAKEFILE ${llext_edk}/Makefile.cflags)
247set(edk_file_CMAKE ${llext_edk}/cmake.cflags)
248
249foreach(target ${edk_targets})
250    edk_write_header(${target})
251
252    edk_write_comment(${target} "Target information")
253    edk_write_var(${target} "${var_prefix}_BOARD_NAME" "${board_name}")
254    edk_write_var(${target} "${var_prefix}_BOARD_QUALIFIERS" "${board_qualifiers}")
255    edk_write_var(${target} "${var_prefix}_BOARD_REVISION" "${board_revision}")
256    edk_write_var(${target} "${var_prefix}_BOARD_TARGET" "${normalized_board_target}")
257
258    edk_write_comment(${target} "Compile flags")
259    edk_write_var(${target} "LLEXT_CFLAGS" "${all_flags}")
260    edk_write_var(${target} "LLEXT_ALL_INCLUDE_CFLAGS" "${all_inc_flags}")
261    edk_write_var(${target} "LLEXT_INCLUDE_CFLAGS" "${inc_flags}")
262    edk_write_var(${target} "LLEXT_GENERATED_INCLUDE_CFLAGS" "${gen_inc_flags}")
263    edk_write_var(${target} "LLEXT_BASE_CFLAGS" "${base_flags}")
264    edk_write_var(${target} "LLEXT_GENERATED_IMACROS_CFLAGS" "${imacros_gen}")
265endforeach()
266
267if(CONFIG_LLEXT_EDK_FORMAT_TAR_XZ)
268  set(llext_edk_format FORMAT gnutar COMPRESSION XZ)
269elseif(CONFIG_LLEXT_EDK_FORMAT_TAR_ZSTD)
270  set(llext_edk_format FORMAT gnutar COMPRESSION Zstd)
271elseif(CONFIG_LLEXT_EDK_FORMAT_ZIP)
272  set(llext_edk_format FORMAT zip)
273else()
274  message(FATAL_ERROR "Unsupported LLEXT_EDK_FORMAT choice")
275endif()
276
277# Generate the tarball
278file(ARCHIVE_CREATE
279    OUTPUT ${llext_edk_file}
280    PATHS ${llext_edk}
281    ${llext_edk_format}
282)
283
284file(REMOVE_RECURSE ${llext_edk})
285