component_requirements.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. # SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD
  2. # SPDX-License-Identifier: Apache-2.0
  3. import os
  4. import re
  5. from typing import Optional
  6. from idf_py_actions.tools import get_build_context
  7. '''
  8. glossary:
  9. orignal_component: component which compilation failed
  10. source_component: component containing file which is including the missing header file
  11. candidate_component: component which contain the missing header file
  12. original_filename: abs path of file(compilation unit) in original_component
  13. source_filename: abs path of file in source_component which is including the missing header file
  14. missing_header: filename of the missing header included in source_filename
  15. '''
  16. # Regex to find source_filename in preprocessor's error message
  17. ENOENT_RE = re.compile(r'^(.+):\d+:\d+: fatal error: (.+): No such file or directory$',
  18. flags=re.MULTILINE)
  19. # Regex to find full preprocessor's error message to identify the original_filename
  20. # in case the missing_header is reported in indirect include.
  21. ENOENT_FULL_RE = re.compile(r'^(In file included.*No such file or directory)$',
  22. flags=re.MULTILINE | re.DOTALL)
  23. # Regex to find original_filename in preprocessor's error message
  24. ORIGINAL_FILE_RE = re.compile(r'.*from (.*):[\d]+:')
  25. def _get_absolute_path(filename: str, base: str) -> str:
  26. # If filename path is relative, return absolute path based
  27. # on base directory. The filename is normalized in any case.
  28. if not os.path.isabs(filename):
  29. filename = os.path.join(base, filename)
  30. filename = os.path.normpath(filename)
  31. return filename
  32. def generate_hint(output: str) -> Optional[str]:
  33. # get the project description
  34. proj_desc = get_build_context().get('proj_desc')
  35. if not proj_desc:
  36. # hints cannot be generated because we are not in the build context,
  37. # meaning ensure_build_directory() was not ran and project description
  38. # is not available
  39. return None
  40. hint_match = ENOENT_RE.search(output)
  41. if not hint_match:
  42. return None
  43. # this is the file where the error has occurred
  44. source_filename = _get_absolute_path(hint_match.group(1), proj_desc['build_dir'])
  45. # this is the header file we tried to include
  46. missing_header = hint_match.group(2)
  47. # find the source_component that contains the source file
  48. found_source_component_name = None
  49. found_source_component_info = None
  50. for component_name, component_info in proj_desc['all_component_info'].items():
  51. # look if the source_filename is within a component directory, not only
  52. # at component_info['sources'], because the missing file may be included
  53. # from header file, which is not present in component_info['sources']
  54. component_dir = os.path.normpath(component_info['dir'])
  55. if source_filename.startswith(component_dir):
  56. if found_source_component_info and len(found_source_component_info['dir']) >= len(component_dir):
  57. continue
  58. found_source_component_info = component_info
  59. found_source_component_name = component_name
  60. if not found_source_component_name:
  61. # The source file is not in any component.
  62. # It could be in a subproject added via ExternalProject_Add, in which case
  63. # we can't help much.
  64. return None
  65. # find the original_component, which may be different from sourc_component
  66. found_original_component_name = found_source_component_name
  67. found_original_component_info = found_source_component_info
  68. original_filename = source_filename
  69. hint_match_full = ENOENT_FULL_RE.search(output)
  70. if hint_match_full:
  71. lines = hint_match_full.group().splitlines()
  72. # second line from the end contains filename which is part of the
  73. # original_component
  74. original_file_match = ORIGINAL_FILE_RE.match(lines[-2])
  75. if original_file_match:
  76. original_filename = _get_absolute_path(original_file_match.group(1), proj_desc['build_dir'])
  77. for component_name, component_info in proj_desc['all_component_info'].items():
  78. component_dir = os.path.normpath(component_info['dir'])
  79. if original_filename.startswith(component_dir):
  80. found_original_component_name = component_name
  81. found_original_component_info = component_info
  82. break
  83. else:
  84. # We should never reach this path. It would probably mean
  85. # the preprocessor output was changed. Anyway we can still
  86. # report something meaningful, so just keep going.
  87. pass
  88. # look for the header file in the public include directories of all components
  89. found_dep_component_names = []
  90. for candidate_component_name, candidate_component_info in proj_desc['all_component_info'].items():
  91. if candidate_component_name == found_source_component_name:
  92. # skip the component that contains the source file
  93. continue
  94. candidate_component_include_dirs = candidate_component_info['include_dirs']
  95. component_dir = os.path.normpath(candidate_component_info['dir'])
  96. for candidate_component_include_dir in candidate_component_include_dirs:
  97. candidate_header_path = os.path.join(component_dir, candidate_component_include_dir, missing_header)
  98. if os.path.exists(candidate_header_path):
  99. found_dep_component_names.append(candidate_component_name)
  100. break # no need to look further in this component
  101. if not found_dep_component_names:
  102. # Header file not found in any component INCLUDE_DIRS. Try to scan whole component
  103. # directories if we can find the missing header there and notify user about possible
  104. # missing entry in INCLUDE_DIRS.
  105. candidate_component_include_dirs = []
  106. for component_name, component_info in proj_desc['all_component_info'].items():
  107. component_dir = os.path.normpath(component_info['dir'])
  108. for root, _, _ in os.walk(component_dir):
  109. full_path = os.path.normpath(os.path.join(root, missing_header))
  110. # sanity check that the full_path is still within component's directory
  111. if not full_path.startswith(component_dir):
  112. continue
  113. if os.path.isfile(full_path):
  114. candidate_component_include_dirs.append(f'{component_name}({full_path})')
  115. if candidate_component_include_dirs:
  116. candidates = ', '.join(candidate_component_include_dirs)
  117. return (f'Missing "{missing_header}" file name found in the following component(s): {candidates}. '
  118. f'Maybe one of the components needs to add the missing header directory to INCLUDE_DIRS '
  119. f'of idf_component_register call in CMakeLists.txt.')
  120. # The missing header not found anywhere, nothing much we can do here.
  121. return None
  122. assert found_source_component_info is not None # to help mypy
  123. assert found_original_component_info is not None # to help mypy
  124. # Sanity check: verify we didn't somehow find a component which is already in the requirements list
  125. all_reqs = (found_source_component_info['reqs']
  126. + found_source_component_info['managed_reqs'])
  127. if found_original_component_name == found_source_component_name:
  128. # Add also private reqs, but only if source_component is same original_component.
  129. # The missing_header may be part of component which is already added as private
  130. # req for source_component. Meaning it's not part of source_component public
  131. # interface.
  132. all_reqs += (found_source_component_info['priv_reqs']
  133. + found_source_component_info['managed_priv_reqs'])
  134. for dep_component_name in found_dep_component_names:
  135. if dep_component_name in all_reqs:
  136. # Oops. This component is already in the requirements list.
  137. # How did this happen?
  138. return f'BUG: {missing_header} found in component {dep_component_name} which is already in the requirements list of {found_source_component_name}'
  139. # try to figure out the correct require type: REQUIRES or PRIV_REQUIRES
  140. requires_type = None
  141. source_component_has_priv_dep = False
  142. if original_filename == source_filename:
  143. # The error is reported directly in compilation unit, so
  144. # missing_header should not be part of public interface.
  145. requires_type = 'PRIV_REQUIRES'
  146. elif found_original_component_name == found_source_component_name:
  147. # The original_component and source_component are the same and original_filename
  148. # is different from source_filename. Check if the source_file is part of the
  149. # original_component's public interface. If so, the REQUIRES should be used.
  150. for include_dir in found_original_component_info['include_dirs']:
  151. include_dir = _get_absolute_path(found_original_component_info['dir'], include_dir)
  152. if source_filename.startswith(include_dir):
  153. # source_filename is part of public interface
  154. requires_type = 'REQUIRES'
  155. break
  156. if not requires_type:
  157. # source_file not part of public interface, suggest PRIV_REQUIRES
  158. requires_type = 'PRIV_REQUIRES'
  159. else:
  160. # The source_filename is part of different component than the original_component, so
  161. # the source_component needs to use REQUIRES to make the missing_header available for
  162. # original_component.
  163. requires_type = 'REQUIRES'
  164. if len(found_dep_component_names) == 1:
  165. # If there is only one component found as missing dependency, look at
  166. # source_component private requires to see if the missing dependency is
  167. # already there. If so, we suggest to move it from PRIV_REQUIRES to REQUIRES.
  168. # This is done only if there is one component in found_dep_component_names, because
  169. # otherwise we cannot be sure which component should be moved.
  170. priv_reqs = (found_source_component_info['priv_reqs']
  171. + found_source_component_info['managed_priv_reqs'])
  172. if found_dep_component_names[0] in priv_reqs:
  173. source_component_has_priv_dep = True
  174. found_dep_component_names_list = ', '.join(found_dep_component_names)
  175. source_filename_short = os.path.basename(source_filename)
  176. cmakelists_file_to_fix = os.path.normpath(os.path.join(found_source_component_info['dir'], 'CMakeLists.txt'))
  177. problem_description = (
  178. f'Compilation failed because {source_filename_short} (in "{found_source_component_name}" component) '
  179. f'includes {missing_header}, provided by {found_dep_component_names_list} component(s).\n')
  180. if source_component_has_priv_dep:
  181. problem_solution = (
  182. f'However, {found_dep_component_names_list} component(s) is in the private requirements list '
  183. f'of "{found_source_component_name}".\n'
  184. f'To fix this, move {found_dep_component_names_list} from PRIV_REQUIRES into '
  185. f'REQUIRES list of idf_component_register call in {cmakelists_file_to_fix}.')
  186. else:
  187. problem_solution = (
  188. f'However, {found_dep_component_names_list} component(s) is not in the requirements list '
  189. f'of "{found_source_component_name}".\n'
  190. f'To fix this, add {found_dep_component_names_list} to {requires_type} list '
  191. f'of idf_component_register call in {cmakelists_file_to_fix}.')
  192. return problem_description + problem_solution