]> granicus.if.org Git - esp-idf/blob - tools/cmake/convert_to_cmake.py
cmake: Generate list of components with dependent items first, use deterministic...
[esp-idf] / tools / cmake / convert_to_cmake.py
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 subprocess
8 import re
9 import os.path
10 import glob
11 import sys
12
13 debug = False
14
15 def 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().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 def get_component_variables(project_path, component_path):
58     make_vars = get_make_variables(component_path,
59                                    os.path.join(os.environ["IDF_PATH"],
60                                                 "make",
61                                                 "component_wrapper.mk"),
62                                    expected_failure=True,
63                                    variables = {
64                                        "COMPONENT_MAKEFILE" : os.path.join(component_path, "component.mk"),
65                                        "COMPONENT_NAME" : os.path.basename(component_path),
66                                        "PROJECT_PATH": project_path,
67                                    })
68
69     if "COMPONENT_OBJS" in make_vars:  # component.mk specifies list of object files
70         # Convert to sources
71         def find_src(obj):
72             obj = os.path.splitext(obj)[0]
73             for ext in [ "c", "cpp", "S" ]:
74                 if os.path.exists(os.path.join(component_path, obj) + "." + ext):
75                     return obj + "." + ext
76             print("WARNING: Can't find source file for component %s COMPONENT_OBJS %s" % (component_path, obj))
77             return None
78
79         srcs = []
80         for obj in make_vars["COMPONENT_OBJS"].split(" "):
81             src = find_src(obj)
82             if src is not None:
83                 srcs.append(src)
84         make_vars["COMPONENT_SRCS"] = " ".join(srcs)
85     else:  # Use COMPONENT_SRCDIRS
86         make_vars["COMPONENT_SRCDIRS"] = make_vars.get("COMPONENT_SRCDIRS", ".")
87
88     make_vars["COMPONENT_ADD_INCLUDEDIRS"] = make_vars.get("COMPONENT_ADD_INCLUDEDIRS", "include")
89
90     return make_vars
91
92
93 def convert_project(project_path):
94     if not os.path.exists(project_path):
95         raise RuntimeError("Project directory '%s' not found" % project_path)
96     if not os.path.exists(os.path.join(project_path, "Makefile")):
97         raise RuntimeError("Directory '%s' doesn't contain a project Makefile" % project_path)
98
99     project_cmakelists = os.path.join(project_path, "CMakeLists.txt")
100     if os.path.exists(project_cmakelists):
101         raise RuntimeError("This project already has a CMakeLists.txt file")
102
103     project_vars = get_make_variables(project_path, expected_failure=True)
104     if not "PROJECT_NAME" in project_vars:
105         raise RuntimeError("PROJECT_NAME does not appear to be defined in IDF project Makefile at %s" % project_path)
106
107     component_paths = project_vars["COMPONENT_PATHS"].split(" ")
108
109     # "main" component is made special in cmake, so extract it from the component_paths list
110     try:
111         main_component_path = [ p for p in component_paths if os.path.basename(p) == "main" ][0]
112         if debug:
113             print("Found main component %s"  % main_component_path)
114         main_vars = get_component_variables(project_path, main_component_path)
115     except IndexError:
116         print("WARNING: Project has no 'main' component, but CMake-based system requires at least one file in MAIN_SRCS...")
117         main_vars = { "COMPONENT_SRCS" : ""} # dummy for MAIN_SRCS
118
119     # Remove main component from list of components we're converting to cmake
120     component_paths = [ p for p in component_paths if os.path.basename(p) != "main" ]
121
122     # Convert components as needed
123     for p in component_paths:
124         convert_component(project_path, p)
125
126     # Look up project variables before we start writing the file, so nothing
127     # is created if there is an error
128
129     main_srcs = main_vars["COMPONENT_SRCS"].split(" ")
130     # convert from component-relative to absolute paths
131     main_srcs = [ os.path.normpath(os.path.join(main_component_path, m)) for m in main_srcs ]
132     # convert to make relative to the project directory
133     main_srcs = [ os.path.relpath(m, project_path) for m in main_srcs ]
134
135     project_name = project_vars["PROJECT_NAME"]
136
137     # Generate the project CMakeLists.txt file
138     with open(project_cmakelists, "w") as f:
139         f.write("""
140 # (Automatically converted from project Makefile by convert_to_cmake.py.)
141
142 # The following four lines of boilerplate have to be in your project's CMakeLists
143 # in this exact order for cmake to work correctly
144 cmake_minimum_required(VERSION 3.5)
145
146 """)
147         f.write("set(MAIN_SRCS %s)\n" % " ".join(main_srcs))
148         f.write("""
149 include($ENV{IDF_PATH}/tools/cmake/project.cmake)
150 """)
151         f.write("project(%s)\n" % project_name)
152
153     print("Converted project %s" % project_cmakelists)
154
155 def convert_component(project_path, component_path):
156     if debug:
157         print("Converting %s..." % (component_path))
158     cmakelists_path = os.path.join(component_path, "CMakeLists.txt")
159     if os.path.exists(cmakelists_path):
160         print("Skipping already-converted component %s..." % cmakelists_path)
161         return
162     v = get_component_variables(project_path, component_path)
163
164     # Look up all the variables before we start writing the file, so it's not
165     # created if there's an erro
166     component_srcs = v.get("COMPONENT_SRCS", None)
167     component_srcdirs = None
168     if component_srcs is not None:
169         # see if we should be using COMPONENT_SRCS or COMPONENT_SRCDIRS, if COMPONENT_SRCS is everything in SRCDIRS
170         component_allsrcs = []
171         for d in v.get("COMPONENT_SRCDIRS", "").split(" "):
172             component_allsrcs += glob.glob(os.path.normpath(os.path.join(component_path, d, "*.[cS]")))
173             component_allsrcs += glob.glob(os.path.normpath(os.path.join(component_path, d, "*.cpp")))
174         abs_component_srcs = [os.path.normpath(os.path.join(component_path, p)) for p in component_srcs.split(" ")]
175         if set(component_allsrcs) == set(abs_component_srcs):
176             component_srcdirs = v.get("COMPONENT_SRCDIRS")
177
178     component_add_includedirs = v["COMPONENT_ADD_INCLUDEDIRS"]
179     cflags = v.get("CFLAGS", None)
180
181     with open(cmakelists_path, "w") as f:
182         f.write("set(COMPONENT_ADD_INCLUDEDIRS %s)\n\n" % component_add_includedirs)
183
184         f.write("# Edit following two lines to set component requirements (see docs)\n")
185         f.write("set(COMPONENT_REQUIRES "")\n")
186         f.write("set(COMPONENT_PRIV_REQUIRES "")\n\n")
187
188         if component_srcdirs is not None:
189             f.write("set(COMPONENT_SRCDIRS %s)\n\n" % component_srcdirs)
190             f.write("register_component()\n")
191         elif component_srcs is not None:
192             f.write("set(COMPONENT_SRCS %s)\n\n" % component_srcs)
193             f.write("register_component()\n")
194         else:
195             f.write("register_config_only_component()\n")
196         if cflags is not None:
197             f.write("component_compile_options(%s)\n" % cflags)
198
199     print("Converted %s" % cmakelists_path)
200
201
202 def main():
203     global debug
204
205     parser = argparse.ArgumentParser(description='convert_to_cmake.py - ESP-IDF Project Makefile to CMakeLists.txt converter', prog='convert_to_cmake')
206
207     parser.add_argument('--debug', help='Display debugging output',
208                         action='store_true')
209
210     parser.add_argument('project', help='Path to project to convert (defaults to CWD)', default=os.getcwd(), metavar='project path', nargs='?')
211
212     args = parser.parse_args()
213     debug = args.debug
214     print("Converting %s..." % args.project)
215     convert_project(args.project)
216
217
218 if __name__ == "__main__":
219     main()