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    BOARD_REVISION ${board_revision})
171
172set(llext_edk_name ${CONFIG_LLEXT_EDK_NAME})
173set(llext_edk ${PROJECT_BINARY_DIR}/${llext_edk_name})
174set(llext_edk_inc ${llext_edk}/include)
175
176zephyr_string(SANITIZE TOUPPER var_prefix ${llext_edk_name})
177set(install_dir_var "${var_prefix}_INSTALL_DIR")
178
179set(make_relative FALSE)
180foreach(flag ${llext_edk_cflags})
181    # Detect all combinations of 'imacros' flag:
182    # - with one or two preceding dashes
183    # - separated from the argument, joined by '=', or joined (no separator)
184    if(flag MATCHES "^--?imacros$")
185        # imacros followed by a space, convert next argument
186        set(make_relative TRUE)
187        continue()
188    elseif(flag MATCHES "^--?imacros=?([^=].*)$")
189        # imacros=<stuff> or imacros<stuff>, immediately convert <stuff>
190        set(flag ${CMAKE_MATCH_1})
191        set(make_relative TRUE)
192    endif()
193
194    if(make_relative)
195        set(make_relative FALSE)
196        cmake_path(GET flag PARENT_PATH parent)
197        cmake_path(GET flag FILENAME name)
198        relative_dir(${parent} dest bindir)
199        cmake_path(RELATIVE_PATH dest BASE_DIRECTORY ${llext_edk} OUTPUT_VARIABLE dest_rel)
200        if(bindir)
201            list(APPEND imacros_gen "@DASHIMACROS@${dest_rel}/${name}")
202        else()
203            list(APPEND imacros "@DASHIMACROS@${dest_rel}/${name}")
204        endif()
205    else()
206        list(APPEND new_cflags ${flag})
207    endif()
208endforeach()
209set(llext_edk_cflags ${new_cflags})
210
211list(APPEND base_flags ${llext_edk_cflags} ${imacros})
212
213file(MAKE_DIRECTORY ${llext_edk_inc})
214foreach(dir ${INTERFACE_INCLUDE_DIRECTORIES})
215    if (NOT EXISTS ${dir})
216        continue()
217    endif()
218
219    relative_dir(${dir} dest bindir)
220    # Use destination parent, as the last part of the source directory is copied as well
221    cmake_path(GET dest PARENT_PATH dest_p)
222
223    file(MAKE_DIRECTORY ${dest_p})
224    file(COPY ${dir} DESTINATION ${dest_p} FILES_MATCHING PATTERN "*.h")
225
226    cmake_path(RELATIVE_PATH dest BASE_DIRECTORY ${llext_edk} OUTPUT_VARIABLE dest_rel)
227    if(bindir)
228        list(APPEND gen_inc_flags "@DASHI@${dest_rel}")
229    else()
230        list(APPEND inc_flags "@DASHI@${dest_rel}")
231    endif()
232    list(APPEND all_inc_flags "@DASHI@${dest_rel}")
233endforeach()
234
235list(APPEND all_flags ${base_flags} ${imacros_gen} ${all_inc_flags})
236
237if(CONFIG_LLEXT_EDK_USERSPACE_ONLY)
238    # Copy syscall headers from edk directory, as they were regenerated there.
239    file(COPY ${PROJECT_BINARY_DIR}/edk/include/generated/ DESTINATION ${llext_edk_inc}/zephyr/include/generated)
240endif()
241
242#
243# Generate the EDK flags files
244#
245
246set(edk_targets MAKEFILE CMAKE)
247set(edk_file_MAKEFILE ${llext_edk}/Makefile.cflags)
248set(edk_file_CMAKE ${llext_edk}/cmake.cflags)
249
250foreach(target ${edk_targets})
251    edk_write_header(${target})
252
253    edk_write_comment(${target} "Target information")
254    edk_write_var(${target} "${var_prefix}_BOARD_NAME" "${board_name}")
255    edk_write_var(${target} "${var_prefix}_BOARD_QUALIFIERS" "${board_qualifiers}")
256    edk_write_var(${target} "${var_prefix}_BOARD_REVISION" "${board_revision}")
257    edk_write_var(${target} "${var_prefix}_BOARD_TARGET" "${normalized_board_target}")
258
259    edk_write_comment(${target} "Compile flags")
260    edk_write_var(${target} "LLEXT_CFLAGS" "${all_flags}")
261    edk_write_var(${target} "LLEXT_ALL_INCLUDE_CFLAGS" "${all_inc_flags}")
262    edk_write_var(${target} "LLEXT_INCLUDE_CFLAGS" "${inc_flags}")
263    edk_write_var(${target} "LLEXT_GENERATED_INCLUDE_CFLAGS" "${gen_inc_flags}")
264    edk_write_var(${target} "LLEXT_BASE_CFLAGS" "${base_flags}")
265    edk_write_var(${target} "LLEXT_GENERATED_IMACROS_CFLAGS" "${imacros_gen}")
266endforeach()
267
268if(CONFIG_LLEXT_EDK_FORMAT_TAR_XZ)
269  set(llext_edk_format FORMAT gnutar COMPRESSION XZ)
270elseif(CONFIG_LLEXT_EDK_FORMAT_TAR_ZSTD)
271  set(llext_edk_format FORMAT gnutar COMPRESSION Zstd)
272elseif(CONFIG_LLEXT_EDK_FORMAT_ZIP)
273  set(llext_edk_format FORMAT zip)
274else()
275  message(FATAL_ERROR "Unsupported LLEXT_EDK_FORMAT choice")
276endif()
277
278# Generate the tarball
279file(ARCHIVE_CREATE
280    OUTPUT ${llext_edk_file}
281    PATHS ${llext_edk}
282    ${llext_edk_format}
283)
284
285file(REMOVE_RECURSE ${llext_edk})
286