#!/usr/bin/env python # # Command line tool to convert simple ESP-IDF Makefile & component.mk files to # CMakeLists.txt files # import argparse import glob import os.path import re import subprocess debug = False def get_make_variables(path, makefile='Makefile', expected_failure=False, variables={}): """ Given the path to a Makefile of some kind, return a dictionary of all variables defined in this Makefile Uses 'make' to parse the Makefile syntax, so we don't have to! Overrides IDF_PATH= to avoid recursively evaluating the entire project Makefile structure. """ variable_setters = [('%s=%s' % (k,v)) for (k,v) in variables.items()] cmdline = ['make', '-rpn', '-C', path, '-f', makefile] + variable_setters if debug: print('Running %s...' % (' '.join(cmdline))) p = subprocess.Popen(cmdline, stdout=subprocess.PIPE, stderr=subprocess.PIPE) (output, stderr) = p.communicate('\n') if (not expected_failure) and p.returncode != 0: raise RuntimeError('Unexpected make failure, result %d' % p.returncode) if debug: print('Make stdout:') print(output) print('Make stderr:') print(stderr) next_is_makefile = False # is the next line a makefile variable? result = {} BUILT_IN_VARS = set(['MAKEFILE_LIST', 'SHELL', 'CURDIR', 'MAKEFLAGS']) for line in output.decode('utf-8').split('\n'): if line.startswith('# makefile'): # this line appears before any variable defined in the makefile itself next_is_makefile = True elif next_is_makefile: next_is_makefile = False m = re.match(r'(?P<var>[^ ]+) :?= (?P<val>.+)', line) if m is not None: if not m.group('var') in BUILT_IN_VARS: result[m.group('var')] = m.group('val').strip() return result def get_component_variables(project_path, component_path): make_vars = get_make_variables(component_path, os.path.join(os.environ['IDF_PATH'], 'make', 'component_wrapper.mk'), expected_failure=True, variables={ 'COMPONENT_MAKEFILE': os.path.join(component_path, 'component.mk'), 'COMPONENT_NAME': os.path.basename(component_path), 'PROJECT_PATH': project_path, }) if 'COMPONENT_OBJS' in make_vars: # component.mk specifies list of object files # Convert to sources def find_src(obj): obj = os.path.splitext(obj)[0] for ext in ['c', 'cpp', 'S']: if os.path.exists(os.path.join(component_path, obj) + '.' + ext): return obj + '.' + ext print("WARNING: Can't find source file for component %s COMPONENT_OBJS %s" % (component_path, obj)) return None srcs = [] for obj in make_vars['COMPONENT_OBJS'].split(): src = find_src(obj) if src is not None: srcs.append(src) make_vars['COMPONENT_SRCS'] = ' '.join(srcs) else: component_srcs = list() for component_srcdir in make_vars.get('COMPONENT_SRCDIRS', '.').split(): component_srcdir_path = os.path.abspath(os.path.join(component_path, component_srcdir)) srcs = list() srcs += glob.glob(os.path.join(component_srcdir_path, '*.[cS]')) srcs += glob.glob(os.path.join(component_srcdir_path, '*.cpp')) srcs = [('"%s"' % str(os.path.relpath(s, component_path))) for s in srcs] make_vars['COMPONENT_ADD_INCLUDEDIRS'] = make_vars.get('COMPONENT_ADD_INCLUDEDIRS', 'include') component_srcs += srcs make_vars['COMPONENT_SRCS'] = ' '.join(component_srcs) return make_vars def convert_project(project_path): if not os.path.exists(project_path): raise RuntimeError("Project directory '%s' not found" % project_path) if not os.path.exists(os.path.join(project_path, 'Makefile')): raise RuntimeError("Directory '%s' doesn't contain a project Makefile" % project_path) project_cmakelists = os.path.join(project_path, 'CMakeLists.txt') if os.path.exists(project_cmakelists): raise RuntimeError('This project already has a CMakeLists.txt file') project_vars = get_make_variables(project_path, expected_failure=True) if 'PROJECT_NAME' not in project_vars: raise RuntimeError('PROJECT_NAME does not appear to be defined in IDF project Makefile at %s' % project_path) component_paths = project_vars['COMPONENT_PATHS'].split() converted_components = 0 # Convert components as needed for p in component_paths: if 'MSYSTEM' in os.environ: cmd = ['cygpath', '-w', p] p = subprocess.check_output(cmd).decode('utf-8').strip() converted_components += convert_component(project_path, p) project_name = project_vars['PROJECT_NAME'] # Generate the project CMakeLists.txt file with open(project_cmakelists, 'w') as f: f.write(""" # (Automatically converted from project Makefile by convert_to_cmake.py.) # The following lines of boilerplate have to be in your project's CMakeLists # in this exact order for cmake to work correctly cmake_minimum_required(VERSION 3.5) """) f.write(""" include($ENV{IDF_PATH}/tools/cmake/project.cmake) """) f.write('project(%s)\n' % project_name) print('Converted project %s' % project_cmakelists) if converted_components > 0: print('Note: Newly created component CMakeLists.txt do not have any REQUIRES or PRIV_REQUIRES ' 'lists to declare their component requirements. Builds may fail to include other ' "components' header files. If so requirements need to be added to the components' " "CMakeLists.txt files. See the 'Component Requirements' section of the " 'Build System docs for more details.') def convert_component(project_path, component_path): if debug: print('Converting %s...' % (component_path)) cmakelists_path = os.path.join(component_path, 'CMakeLists.txt') if os.path.exists(cmakelists_path): print('Skipping already-converted component %s...' % cmakelists_path) return 0 v = get_component_variables(project_path, component_path) # Look up all the variables before we start writing the file, so it's not # created if there's an erro component_srcs = v.get('COMPONENT_SRCS', None) component_add_includedirs = v['COMPONENT_ADD_INCLUDEDIRS'] cflags = v.get('CFLAGS', None) with open(cmakelists_path, 'w') as f: if component_srcs is not None: f.write('idf_component_register(SRCS %s)\n' % component_srcs) f.write(' INCLUDE_DIRS %s' % component_add_includedirs) f.write(' # Edit following two lines to set component requirements (see docs)\n') f.write(' REQUIRES '')\n') f.write(' PRIV_REQUIRES '')\n\n') else: f.write('idf_component_register()\n') if cflags is not None: f.write('target_compile_options(${COMPONENT_LIB} PRIVATE %s)\n' % cflags) print('Converted %s' % cmakelists_path) return 1 def main(): global debug parser = argparse.ArgumentParser(description='convert_to_cmake.py - ESP-IDF Project Makefile to CMakeLists.txt converter', prog='convert_to_cmake') parser.add_argument('--debug', help='Display debugging output', action='store_true') parser.add_argument('project', help='Path to project to convert (defaults to CWD)', default=os.getcwd(), metavar='project path', nargs='?') args = parser.parse_args() debug = args.debug print('Converting %s...' % args.project) convert_project(args.project) if __name__ == '__main__': main()