/ tools / cmake / convert_to_cmake.py
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  
 12  debug = False
 13  
 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('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  
 58  def 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  
103  def 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
138  cmake_minimum_required(VERSION 3.5)
139  
140  """)
141          f.write("""
142  include($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  
156  def 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  
188  def 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  
204  if __name__ == "__main__":
205      main()