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()