]> granicus.if.org Git - esp-idf/blob - tools/cmake/scripts/expand_requirements.cmake
cmake: add ulp components build support
[esp-idf] / tools / cmake / scripts / expand_requirements.cmake
1 # expand_requires.cmake is a utility cmake script to expand component requirements early in the build,
2 # before the components are ready to be included.
3 #
4 # Parameters:
5 # - COMPONENTS = Space-separated list of initial components to include in the build.
6 #   Can be empty, in which case all components are in the build.
7 # - COMPONENT_REQUIRES_COMMON = Components to always include in the build, and treated as dependencies
8 #   of all other components.
9 # - DEPENDENCIES_FILE = Path of generated cmake file which will contain the expanded dependencies for these
10 #   components.
11 # - COMPONENT_DIRS = List of paths to search for all components.
12 # - DEBUG = Set -DDEBUG=1 to debug component lists in the build.
13 #
14 # If successful, DEPENDENCIES_FILE can be expanded to set BUILD_COMPONENTS & BUILD_COMPONENT_PATHS with all
15 # components required for the build, and the get_component_requirements() function to return each component's
16 # recursively expanded requirements.
17 #
18 # BUILD_COMPONENTS & BUILD_COMPONENT_PATHS will be ordered in a best-effort way so that dependencies are listed first.
19 # (Note that IDF supports cyclic dependencies, and dependencies in a cycle have ordering guarantees.)
20 #
21 # Determinism:
22 #
23 # Given the the same list of names in COMPONENTS (regardless of order), and an identical value of
24 # COMPONENT_REQUIRES_COMMON, and all the same COMPONENT_REQUIRES & COMPONENT_PRIV_REQUIRES values in
25 # each component, then the output of BUILD_COMPONENTS should always be in the same
26 # order.
27 #
28 # BUILD_COMPONENT_PATHS will be in the same component order as BUILD_COMPONENTS, even if the
29 # actual component paths are different due to different paths.
30 #
31 # TODO: Error out if a component requirement is missing
32 cmake_minimum_required(VERSION 3.5)
33 include("${IDF_PATH}/tools/cmake/utilities.cmake")
34
35 if(NOT DEPENDENCIES_FILE)
36     message(FATAL_ERROR "DEPENDENCIES_FILE must be set.")
37 endif()
38
39 if(NOT COMPONENT_DIRS)
40     message(FATAL_ERROR "COMPONENT_DIRS variable must be set")
41 endif()
42 spaces2list(COMPONENT_DIRS)
43
44 spaces2list(COMPONENT_REQUIRES_COMMON)
45
46 function(debug message)
47     if(DEBUG)
48         message(STATUS "${message}")
49     endif()
50 endfunction()
51
52 # Dummy register_component used to save requirements variables as global properties, for later expansion
53 #
54 # (expand_component_requirements() includes the component CMakeLists.txt, which then sets its component variables,
55 # calls this dummy macro, and immediately exits again.)
56 macro(register_component)
57     if(COMPONENT STREQUAL main AND NOT COMPONENT_REQUIRES)
58         set(main_component_requires ${COMPONENTS})
59         list(REMOVE_ITEM main_component_requires "main")
60
61         set_property(GLOBAL PROPERTY "${COMPONENT}_REQUIRES" "${main_component_requires}")
62     else()
63         spaces2list(COMPONENT_REQUIRES)
64         set_property(GLOBAL PROPERTY "${COMPONENT}_REQUIRES" "${COMPONENT_REQUIRES}")
65     endif()
66
67     spaces2list(COMPONENT_PRIV_REQUIRES)
68     set_property(GLOBAL PROPERTY "${COMPONENT}_PRIV_REQUIRES" "${COMPONENT_PRIV_REQUIRES}")
69
70     # This is tricky: we override register_component() so it returns out of the component CMakeLists.txt
71     # (as we're declaring it as a macro not a function, so it doesn't have its own scope.)
72     #
73     # This means no targets are defined, and the component expansion ends early.
74     return()
75 endmacro()
76
77 macro(register_config_only_component)
78     register_component()
79 endmacro()
80
81 # Given a component name (find_name) and a list of component paths (component_paths),
82 # return the path to the component in 'variable'
83 #
84 # Fatal error is printed if the component is not found.
85 function(find_component_path find_name component_paths variable)
86     foreach(path ${component_paths})
87         get_filename_component(name "${path}" NAME)
88         if("${name}" STREQUAL "${find_name}")
89             set("${variable}" "${path}" PARENT_SCOPE)
90             return()
91         endif()
92     endforeach()
93     # TODO: find a way to print the dependency chain that lead to this not-found component
94     message(WARNING "Required component ${find_name} is not found in any of the provided COMPONENT_DIRS")
95 endfunction()
96
97 # components_find_all: Search 'component_dirs' for components and return them
98 # as a list of names in 'component_names' and a list of full paths in
99 # 'component_paths'
100 #
101 # component_paths contains only unique component names. Directories
102 # earlier in the component_dirs list take precedence.
103 function(components_find_all component_dirs component_paths component_names)
104     # component_dirs entries can be files or lists of files
105     set(paths "")
106     set(names "")
107
108     # start by expanding the component_dirs list with all subdirectories
109     foreach(dir ${component_dirs})
110         # Iterate any subdirectories for values
111         file(GLOB subdirs LIST_DIRECTORIES true "${dir}/*")
112         foreach(subdir ${subdirs})
113             set(component_dirs "${component_dirs};${subdir}")
114         endforeach()
115     endforeach()
116
117     # Look for a component in each component_dirs entry
118     foreach(dir ${component_dirs})
119         debug("Looking for CMakeLists.txt in ${dir}")
120         file(GLOB component "${dir}/CMakeLists.txt")
121         if(component)
122             debug("CMakeLists.txt file ${component}")
123             get_filename_component(component "${component}" DIRECTORY)
124             get_filename_component(name "${component}" NAME)
125             if(NOT name IN_LIST names)
126                 set(names "${names};${name}")
127                 set(paths "${paths};${component}")
128             endif()
129
130         else()  # no CMakeLists.txt file
131             # test for legacy component.mk and warn
132             file(GLOB legacy_component "${dir}/component.mk")
133             if(legacy_component)
134                 get_filename_component(legacy_component "${legacy_component}" DIRECTORY)
135                 message(WARNING "Component ${legacy_component} contains old-style component.mk but no CMakeLists.txt. "
136                     "Component will be skipped.")
137             endif()
138         endif()
139
140     endforeach()
141
142     set(${component_paths} ${paths} PARENT_SCOPE)
143     set(${component_names} ${names} PARENT_SCOPE)
144 endfunction()
145
146 # expand_component_requirements: Recursively expand a component's requirements,
147 # setting global properties BUILD_COMPONENTS & BUILD_COMPONENT_PATHS and
148 # also invoking the components to call register_component() above,
149 # which will add per-component global properties with dependencies, etc.
150 function(expand_component_requirements component)
151     get_property(seen_components GLOBAL PROPERTY SEEN_COMPONENTS)
152     if(${component} IN_LIST seen_components)
153         return()  # already added, or in process of adding, this component
154     endif()
155     set_property(GLOBAL APPEND PROPERTY SEEN_COMPONENTS ${component})
156
157     find_component_path("${component}" "${ALL_COMPONENT_PATHS}" component_path)
158     debug("Expanding dependencies of ${component} @ ${component_path}")
159     if(NOT component_path)
160         set_property(GLOBAL APPEND PROPERTY COMPONENTS_NOT_FOUND ${component})
161         return()
162     endif()
163
164     # include the component CMakeLists.txt to expand its properties
165     # into the global cache (via register_component(), above)
166     unset(COMPONENT_REQUIRES)
167     unset(COMPONENT_PRIV_REQUIRES)
168     set(COMPONENT ${component})
169     include(${component_path}/CMakeLists.txt)
170
171     get_property(requires GLOBAL PROPERTY "${component}_REQUIRES")
172     get_property(requires_priv GLOBAL PROPERTY "${component}_PRIV_REQUIRES")
173
174     # Recurse dependencies first, so that they appear first in the list (when possible)
175     foreach(req ${COMPONENT_REQUIRES_COMMON} ${requires} ${requires_priv})
176         expand_component_requirements(${req})
177     endforeach()
178
179     # Now append this component to the full list (after its dependencies)
180     set_property(GLOBAL APPEND PROPERTY BUILD_COMPONENT_PATHS ${component_path})
181     set_property(GLOBAL APPEND PROPERTY BUILD_COMPONENTS ${component})
182 endfunction()
183
184 # Main functionality goes here
185
186 # Find every available component in COMPONENT_DIRS, save as ALL_COMPONENT_PATHS and ALL_COMPONENTS
187 components_find_all("${COMPONENT_DIRS}" ALL_COMPONENT_PATHS ALL_COMPONENTS)
188
189 if(NOT COMPONENTS)
190     set(COMPONENTS "${ALL_COMPONENTS}")
191 endif()
192 spaces2list(COMPONENTS)
193
194 debug("ALL_COMPONENT_PATHS ${ALL_COMPONENT_PATHS}")
195 debug("ALL_COMPONENTS ${ALL_COMPONENTS}")
196
197 set_property(GLOBAL PROPERTY SEEN_COMPONENTS "")  # anti-infinite-recursion
198 set_property(GLOBAL PROPERTY BUILD_COMPONENTS "")
199 set_property(GLOBAL PROPERTY BUILD_COMPONENT_PATHS "")
200 set_property(GLOBAL PROPERTY COMPONENTS_NOT_FOUND "")
201
202 # Indicate that the component CMakeLists.txt is being included in the early expansion phase of the build,
203 # and might not want to execute particular operations.
204 set(CMAKE_BUILD_EARLY_EXPANSION 1)
205 foreach(component ${COMPONENTS})
206     debug("Expanding initial component ${component}")
207     expand_component_requirements(${component})
208 endforeach()
209 unset(CMAKE_BUILD_EARLY_EXPANSION)
210
211 get_property(build_components GLOBAL PROPERTY BUILD_COMPONENTS)
212 get_property(build_component_paths GLOBAL PROPERTY BUILD_COMPONENT_PATHS)
213 get_property(not_found GLOBAL PROPERTY COMPONENTS_NOT_FOUND)
214
215 debug("components in build: ${build_components}")
216 debug("components in build: ${build_component_paths}")
217 debug("components not found: ${not_found}")
218
219 function(line contents)
220     file(APPEND "${DEPENDENCIES_FILE}.tmp" "${contents}\n")
221 endfunction()
222
223 file(WRITE "${DEPENDENCIES_FILE}.tmp" "# Component requirements generated by expand_requirements.cmake\n\n")
224 line("set(BUILD_COMPONENTS ${build_components})")
225 line("set(BUILD_COMPONENT_PATHS ${build_component_paths})")
226 line("")
227
228 line("# get_component_requirements: Generated function to read the dependencies of a given component.")
229 line("#")
230 line("# Parameters:")
231 line("# - component: Name of component")
232 line("# - var_requires: output variable name. Set to recursively expanded COMPONENT_REQUIRES ")
233 line("#   for this component.")
234 line("# - var_private_requires: output variable name. Set to recursively expanded COMPONENT_PRIV_REQUIRES ")
235 line("#   for this component.")
236 line("#")
237 line("# Throws a fatal error if 'componeont' is not found (indicates a build system problem).")
238 line("#")
239 line("function(get_component_requirements component var_requires var_private_requires)")
240 foreach(build_component ${build_components})
241     get_property(reqs GLOBAL PROPERTY "${build_component}_REQUIRES")
242     get_property(private_reqs GLOBAL PROPERTY "${build_component}_PRIV_REQUIRES")
243     line("  if(\"\$\{component}\" STREQUAL \"${build_component}\")")
244     line("    set(\${var_requires} \"${reqs}\" PARENT_SCOPE)")
245     line("    set(\${var_private_requires} \"${private_reqs}\" PARENT_SCOPE)")
246     line("    return()")
247     line("  endif()")
248 endforeach()
249
250 line("  message(FATAL_ERROR \"Component not found: \${component}\")")
251 line("endfunction()")
252
253 # only replace DEPENDENCIES_FILE if it has changed (prevents ninja/make build loops.)
254 execute_process(COMMAND ${CMAKE_COMMAND} -E copy_if_different "${DEPENDENCIES_FILE}.tmp" "${DEPENDENCIES_FILE}")
255 execute_process(COMMAND ${CMAKE_COMMAND} -E remove "${DEPENDENCIES_FILE}.tmp")