convert_to_cmake.py 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. #!/usr/bin/env python
  2. #
  3. # Command line tool to convert simple ESP-IDF Makefile & component.mk files to
  4. # CMakeLists.txt files
  5. #
  6. import argparse
  7. import glob
  8. import os.path
  9. import re
  10. import subprocess
  11. debug = False
  12. def get_make_variables(path, makefile='Makefile', expected_failure=False, variables={}):
  13. """
  14. Given the path to a Makefile of some kind, return a dictionary of all variables defined in this Makefile
  15. Uses 'make' to parse the Makefile syntax, so we don't have to!
  16. Overrides IDF_PATH= to avoid recursively evaluating the entire project Makefile structure.
  17. """
  18. variable_setters = [('%s=%s' % (k,v)) for (k,v) in variables.items()]
  19. cmdline = ['make', '-rpn', '-C', path, '-f', makefile] + variable_setters
  20. if debug:
  21. print('Running %s...' % (' '.join(cmdline)))
  22. p = subprocess.Popen(cmdline, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  23. (output, stderr) = p.communicate('\n')
  24. if (not expected_failure) and p.returncode != 0:
  25. raise RuntimeError('Unexpected make failure, result %d' % p.returncode)
  26. if debug:
  27. print('Make stdout:')
  28. print(output)
  29. print('Make stderr:')
  30. print(stderr)
  31. next_is_makefile = False # is the next line a makefile variable?
  32. result = {}
  33. BUILT_IN_VARS = set(['MAKEFILE_LIST', 'SHELL', 'CURDIR', 'MAKEFLAGS'])
  34. for line in output.decode('utf-8').split('\n'):
  35. if line.startswith('# makefile'): # this line appears before any variable defined in the makefile itself
  36. next_is_makefile = True
  37. elif next_is_makefile:
  38. next_is_makefile = False
  39. m = re.match(r'(?P<var>[^ ]+) :?= (?P<val>.+)', line)
  40. if m is not None:
  41. if not m.group('var') in BUILT_IN_VARS:
  42. result[m.group('var')] = m.group('val').strip()
  43. return result
  44. def get_component_variables(project_path, component_path):
  45. make_vars = get_make_variables(component_path,
  46. os.path.join(os.environ['IDF_PATH'],
  47. 'make',
  48. 'component_wrapper.mk'),
  49. expected_failure=True,
  50. variables={
  51. 'COMPONENT_MAKEFILE': os.path.join(component_path, 'component.mk'),
  52. 'COMPONENT_NAME': os.path.basename(component_path),
  53. 'PROJECT_PATH': project_path,
  54. })
  55. if 'COMPONENT_OBJS' in make_vars: # component.mk specifies list of object files
  56. # Convert to sources
  57. def find_src(obj):
  58. obj = os.path.splitext(obj)[0]
  59. for ext in ['c', 'cpp', 'S']:
  60. if os.path.exists(os.path.join(component_path, obj) + '.' + ext):
  61. return obj + '.' + ext
  62. print("WARNING: Can't find source file for component %s COMPONENT_OBJS %s" % (component_path, obj))
  63. return None
  64. srcs = []
  65. for obj in make_vars['COMPONENT_OBJS'].split():
  66. src = find_src(obj)
  67. if src is not None:
  68. srcs.append(src)
  69. make_vars['COMPONENT_SRCS'] = ' '.join(srcs)
  70. else:
  71. component_srcs = list()
  72. for component_srcdir in make_vars.get('COMPONENT_SRCDIRS', '.').split():
  73. component_srcdir_path = os.path.abspath(os.path.join(component_path, component_srcdir))
  74. srcs = list()
  75. srcs += glob.glob(os.path.join(component_srcdir_path, '*.[cS]'))
  76. srcs += glob.glob(os.path.join(component_srcdir_path, '*.cpp'))
  77. srcs = [('"%s"' % str(os.path.relpath(s, component_path))) for s in srcs]
  78. make_vars['COMPONENT_ADD_INCLUDEDIRS'] = make_vars.get('COMPONENT_ADD_INCLUDEDIRS', 'include')
  79. component_srcs += srcs
  80. make_vars['COMPONENT_SRCS'] = ' '.join(component_srcs)
  81. return make_vars
  82. def convert_project(project_path):
  83. if not os.path.exists(project_path):
  84. raise RuntimeError("Project directory '%s' not found" % project_path)
  85. if not os.path.exists(os.path.join(project_path, 'Makefile')):
  86. raise RuntimeError("Directory '%s' doesn't contain a project Makefile" % project_path)
  87. project_cmakelists = os.path.join(project_path, 'CMakeLists.txt')
  88. if os.path.exists(project_cmakelists):
  89. raise RuntimeError('This project already has a CMakeLists.txt file')
  90. project_vars = get_make_variables(project_path, expected_failure=True)
  91. if 'PROJECT_NAME' not in project_vars:
  92. raise RuntimeError('PROJECT_NAME does not appear to be defined in IDF project Makefile at %s' % project_path)
  93. component_paths = project_vars['COMPONENT_PATHS'].split()
  94. converted_components = 0
  95. # Convert components as needed
  96. for p in component_paths:
  97. if 'MSYSTEM' in os.environ:
  98. cmd = ['cygpath', '-w', p]
  99. p = subprocess.check_output(cmd).decode('utf-8').strip()
  100. converted_components += convert_component(project_path, p)
  101. project_name = project_vars['PROJECT_NAME']
  102. # Generate the project CMakeLists.txt file
  103. with open(project_cmakelists, 'w') as f:
  104. f.write("""
  105. # (Automatically converted from project Makefile by convert_to_cmake.py.)
  106. # The following lines of boilerplate have to be in your project's CMakeLists
  107. # in this exact order for cmake to work correctly
  108. cmake_minimum_required(VERSION 3.5)
  109. """)
  110. f.write("""
  111. include($ENV{IDF_PATH}/tools/cmake/project.cmake)
  112. """)
  113. f.write('project(%s)\n' % project_name)
  114. print('Converted project %s' % project_cmakelists)
  115. if converted_components > 0:
  116. print('Note: Newly created component CMakeLists.txt do not have any REQUIRES or PRIV_REQUIRES '
  117. 'lists to declare their component requirements. Builds may fail to include other '
  118. "components' header files. If so requirements need to be added to the components' "
  119. "CMakeLists.txt files. See the 'Component Requirements' section of the "
  120. 'Build System docs for more details.')
  121. def convert_component(project_path, component_path):
  122. if debug:
  123. print('Converting %s...' % (component_path))
  124. cmakelists_path = os.path.join(component_path, 'CMakeLists.txt')
  125. if os.path.exists(cmakelists_path):
  126. print('Skipping already-converted component %s...' % cmakelists_path)
  127. return 0
  128. v = get_component_variables(project_path, component_path)
  129. # Look up all the variables before we start writing the file, so it's not
  130. # created if there's an erro
  131. component_srcs = v.get('COMPONENT_SRCS', None)
  132. component_add_includedirs = v['COMPONENT_ADD_INCLUDEDIRS']
  133. cflags = v.get('CFLAGS', None)
  134. with open(cmakelists_path, 'w') as f:
  135. if component_srcs is not None:
  136. f.write('idf_component_register(SRCS %s)\n' % component_srcs)
  137. f.write(' INCLUDE_DIRS %s' % component_add_includedirs)
  138. f.write(' # Edit following two lines to set component requirements (see docs)\n')
  139. f.write(' REQUIRES '')\n')
  140. f.write(' PRIV_REQUIRES '')\n\n')
  141. else:
  142. f.write('idf_component_register()\n')
  143. if cflags is not None:
  144. f.write('target_compile_options(${COMPONENT_LIB} PRIVATE %s)\n' % cflags)
  145. print('Converted %s' % cmakelists_path)
  146. return 1
  147. def main():
  148. global debug
  149. parser = argparse.ArgumentParser(description='convert_to_cmake.py - ESP-IDF Project Makefile to CMakeLists.txt converter', prog='convert_to_cmake')
  150. parser.add_argument('--debug', help='Display debugging output',
  151. action='store_true')
  152. parser.add_argument('project', help='Path to project to convert (defaults to CWD)', default=os.getcwd(), metavar='project path', nargs='?')
  153. args = parser.parse_args()
  154. debug = args.debug
  155. print('Converting %s...' % args.project)
  156. convert_project(args.project)
  157. if __name__ == '__main__':
  158. main()