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