3 # 'idf.py' is a top-level config/build command line tool for ESP-IDF
5 # You don't have to use idf.py, you can use cmake directly
6 # (or use cmake in an IDE)
10 # Copyright 2018 Espressif Systems (Shanghai) PTE LTD
12 # Licensed under the Apache License, Version 2.0 (the "License");
13 # you may not use this file except in compliance with the License.
14 # You may obtain a copy of the License at
16 # http://www.apache.org/licenses/LICENSE-2.0
18 # Unless required by applicable law or agreed to in writing, software
19 # distributed under the License is distributed on an "AS IS" BASIS,
20 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
21 # See the License for the specific language governing permissions and
22 # limitations under the License.
29 import multiprocessing
34 # Use this Python interpreter for any subprocesses we launch
37 # note: os.environ changes don't automatically propagate to child processes,
38 # you have to pass this in explicitly
39 os.environ["PYTHON"]=sys.executable
41 # Make flavors, across the various kinds of Windows environments & POSIX...
42 if "MSYSTEM" in os.environ: # MSYS
44 MAKE_GENERATOR = "MSYS Makefiles"
45 elif os.name == 'nt': # other Windows
46 MAKE_CMD = "mingw32-make"
47 MAKE_GENERATOR = "MinGW Makefiles"
50 MAKE_GENERATOR = "Unix Makefiles"
53 # ('generator name', 'build command line', 'version command line')
54 ("Ninja", [ "ninja" ], [ "ninja", "--version" ]),
55 (MAKE_GENERATOR, [ MAKE_CMD, "-j", str(multiprocessing.cpu_count()+2) ], [ "make", "--version" ]),
57 GENERATOR_CMDS = dict( (a[0], a[1]) for a in GENERATORS )
59 def check_environment():
61 Verify the environment contains the top-level tools we need to operate
63 (cmake will check a lot of other things)
65 if not executable_exists(["cmake", "--version"]):
66 raise RuntimeError("'cmake' must be available on the PATH to use idf.py")
67 # find the directory idf.py is in, then the parent directory of this, and assume this is IDF_PATH
68 detected_idf_path = os.path.realpath(os.path.join(os.path.dirname(__file__), ".."))
69 if "IDF_PATH" in os.environ:
70 set_idf_path = os.path.realpath(os.environ["IDF_PATH"])
71 if set_idf_path != detected_idf_path:
72 print("WARNING: IDF_PATH environment variable is set to %s but idf.py path indicates IDF directory %s. Using the environment variable directory, but results may be unexpected..."
73 % (set_idf_path, detected_idf_path))
75 os.environ["IDF_PATH"] = detected_idf_path
77 def executable_exists(args):
79 subprocess.check_output(args)
84 def detect_cmake_generator():
86 Find the default cmake generator, if none was specified. Raises an exception if no valid generator is found.
88 for (generator, _, version_check) in GENERATORS:
89 if executable_exists(version_check):
91 raise RuntimeError("To use idf.py, either the 'ninja' or 'GNU make' build tool must be available in the PATH")
93 def _ensure_build_directory(args):
94 """Check the build directory exists and that cmake has been run there.
96 If this isn't the case, create the build directory (if necessary) and
97 do an initial cmake run to configure it.
99 This function will also check args.generator parameter. If the parameter is incompatible with
100 the build directory, an error is raised. If the parameter is None, this function will set it to
101 an auto-detected default generator or to the value already configured in the build directory.
103 project_dir = args.project_dir
104 # Verify the project directory
105 if not os.path.isdir(project_dir):
106 if not os.path.exists(project_dir):
107 raise RuntimeError("Project directory %s does not exist")
109 raise RuntimeError("%s must be a project directory")
110 if not os.path.exists(os.path.join(project_dir, "CMakeLists.txt")):
111 raise RuntimeError("CMakeLists.txt not found in project directory %s" % project_dir)
113 # Verify/create the build directory
114 build_dir = args.build_dir
115 if not os.path.isdir(build_dir):
117 cache_path = os.path.join(build_dir, "CMakeCache.txt")
118 if not os.path.exists(cache_path):
119 if args.generator is None:
120 args.generator = detect_cmake_generator()
121 print("Running cmake...")
122 # Note: we explicitly pass in os.environ here, as we may have set IDF_PATH there during startup
124 subprocess.check_call(["cmake", "-G", args.generator, project_dir], env=os.environ, cwd=args.build_dir)
126 if os.path.exists(cache_path): # don't allow partial CMakeCache.txt files, to keep the "should I run cmake?" logic simple
127 os.remove(cache_path)
130 # Learn some things from the CMakeCache.txt file in the build directory
131 cache = parse_cmakecache(cache_path)
133 generator = cache["CMAKE_GENERATOR"]
135 generator = detect_cmake_generator()
136 if args.generator is None:
137 args.generator = generator # reuse the previously configured generator, if none was given
138 if generator != args.generator:
139 raise RuntimeError("Build is configured for generator '%s' not '%s'. Run 'idf.py fullclean' to start again."
140 % (generator, args.generator))
143 home_dir = cache["CMAKE_HOME_DIRECTORY"]
144 if os.path.realpath(home_dir) != os.path.realpath(project_dir):
145 raise RuntimeError("Build directory '%s' configured for project '%s' not '%s'. Run 'idf.py fullclean' to start again."
146 % (build_dir, os.path.realpath(home_dir), os.path.realpath(project_dir)))
148 pass # if cmake failed part way, CMAKE_HOME_DIRECTORY may not be set yet
151 def parse_cmakecache(path):
153 Parse the CMakeCache file at 'path'.
155 Returns a dict of name:value.
157 CMakeCache entries also each have a "type", but this is currently ignored.
160 with open(path) as f:
162 # cmake cache lines look like: CMAKE_CXX_FLAGS_DEBUG:STRING=-g
163 # groups are name, type, value
164 m = re.match(r"^([^#/:=]+):([^:=]+)=(.+)\n$", line)
166 result[m.group(1)] = m.group(3)
169 def build_target(target_name, args):
171 Execute the target build system to build target 'target_name'
173 Calls _ensure_build_directory() which will run cmake to generate a build
174 directory (with the specified generator) as needed.
176 _ensure_build_directory(args)
177 generator_cmd = GENERATOR_CMDS[args.generator]
178 print("Running '%s %s' in %s..." % (generator_cmd, target_name, args.build_dir))
179 subprocess.check_call(generator_cmd + [target_name], cwd=args.build_dir)
181 def _get_esptool_args(args):
182 esptool_path = os.path.join(os.environ["IDF_PATH"], "components/esptool_py/esptool/esptool.py")
183 result = [ PYTHON, esptool_path ]
184 if args.port is not None:
185 result += [ "-p", args.port ]
186 result += [ "-b", str(args.baud) ]
189 def flash(action, args):
191 Run esptool to flash the entire project, from an argfile generated by the build system
193 flasher_args_path = {
194 "bootloader-flash": "flash_bootloader_args",
195 "partition_table-flash": "flash_partition_table_args",
196 "app-flash": "flash_app_args",
197 "flash": "flash_project_args",
199 esptool_args = _get_esptool_args(args)
200 esptool_args += [ "write_flash", "@"+flasher_args_path ]
201 subprocess.check_call(esptool_args, cwd=args.build_dir)
203 def erase_flash(action, args):
204 esptool_args = _get_esptool_args(args)
205 esptool_args += [ "erase_flash" ]
206 subprocess.check_call(esptool_args, cwd=args.build_dir)
208 def monitor(action, args):
210 Run idf_monitor.py to watch build output
212 desc_path = os.path.join(args.build_dir, "project_description.json")
213 if not os.path.exists(desc_path):
214 _ensure_build_directory(args)
215 with open(desc_path, "r") as f:
216 project_desc = json.load(f)
218 elf_file = os.path.join(args.build_dir, project_desc["app_elf"])
219 if not os.path.exists(elf_file):
220 raise RuntimeError("ELF file '%s' not found. You need to build & flash the project before running 'monitor', and the binary on the device must match the one in the build directory exactly. Try 'idf.py flash monitor'." % elf_file)
221 idf_monitor = os.path.join(os.environ["IDF_PATH"], "tools/idf_monitor.py")
222 monitor_args = [PYTHON, idf_monitor ]
223 if args.port is not None:
224 monitor_args += [ "-p", args.port ]
225 monitor_args += [ "-b", project_desc["monitor_baud"] ]
226 monitor_args += [ elf_file ]
227 subprocess.check_call(monitor_args, cwd=args.build_dir)
229 def clean(action, args):
230 if not os.path.isdir(args.build_dir):
231 print("Build directory '%s' not found. Nothing to clean." % args.build_dir)
233 build_target("clean", args)
235 def fullclean(action, args):
236 build_dir = args.build_dir
237 if not os.path.isdir(build_dir):
238 print("Build directory '%s' not found. Nothing to clean." % build_dir)
240 if len(os.listdir(build_dir)) == 0:
241 print("Build directory '%s' is empty. Nothing to clean." % build_dir)
244 if not os.path.exists(os.path.join(build_dir, "CMakeCache.txt")):
245 raise RuntimeError("Directory '%s' doesn't seem to be a CMake build directory. Refusing to automatically delete files in this directory. Delete the directory manually to 'clean' it." % build_dir)
246 red_flags = [ "CMakeLists.txt", ".git", ".svn" ]
247 for red in red_flags:
248 red = os.path.join(build_dir, red)
249 if os.path.exists(red):
250 raise RuntimeError("Refusing to automatically delete files in directory containing '%s'. Delete files manually if you're sure." % red)
251 # OK, delete everything in the build directory...
252 for f in os.listdir(build_dir): # TODO: once we are Python 3 only, this can be os.scandir()
253 f = os.path.join(build_dir, f)
260 # action name : ( function (or alias), dependencies, order-only dependencies )
261 "all" : ( build_target, [], [ "menuconfig", "clean", "fullclean" ] ),
262 "build": ( "all", [], [] ), # build is same as 'all' target
263 "clean": ( clean, [], [ "fullclean" ] ),
264 "fullclean": ( fullclean, [], [] ),
265 "menuconfig": ( build_target, [], [] ),
266 "size": ( build_target, [], [ "app" ] ),
267 "size-components": ( build_target, [], [ "app" ] ),
268 "size-files": ( build_target, [], [ "app" ] ),
269 "bootloader": ( build_target, [], [] ),
270 "bootloader-clean": ( build_target, [], [] ),
271 "bootloader-flash": ( flash, [ "bootloader" ], [] ),
272 "app": ( build_target, [], [] ),
273 "app-flash": ( flash, [], [ "app" ]),
274 "partition_table": ( build_target, [], [] ),
275 "partition_table-flash": ( flash, [ "partition_table" ], []),
276 "flash": ( flash, [ "all" ], [ ] ),
277 "erase_flash": ( erase_flash, [], []),
278 "monitor": ( monitor, [], [ "flash", "partition_table-flash", "bootloader-flash", "app-flash" ]),
283 parser = argparse.ArgumentParser(description='ESP-IDF build management tool')
284 parser.add_argument('-p', '--port', help="Serial port", default=None)
285 parser.add_argument('-b', '--baud', help="Baud rate", default=460800)
286 parser.add_argument('-C', '--project-dir', help="Project directory", default=os.getcwd())
287 parser.add_argument('-B', '--build-dir', help="Build directory", default=None)
288 parser.add_argument('-G', '--generator', help="Cmake generator", choices=GENERATOR_CMDS.keys())
289 parser.add_argument('actions', help="Actions (build targets or other operations)", nargs='+',
290 choices=ACTIONS.keys())
292 args = parser.parse_args()
296 # Advanced parameter checks
297 if args.build_dir is not None and os.path.realpath(args.project_dir) == os.path.realpath(args.build_dir):
298 raise RuntimeError("Setting the build directory to the project directory is not supported. Suggest dropping --build-dir option, the default is a 'build' subdirectory inside the project directory.")
299 if args.build_dir is None:
300 args.build_dir = os.path.join(args.project_dir, "build")
301 args.build_dir = os.path.realpath(args.build_dir)
303 completed_actions = set()
304 def execute_action(action, remaining_actions):
305 ( function, dependencies, order_dependencies ) = ACTIONS[action]
306 # very simple dependency management, build a set of completed actions and make sure
307 # all dependencies are in it
308 for dep in dependencies:
309 if not dep in completed_actions:
310 execute_action(dep, remaining_actions)
311 for dep in order_dependencies:
312 if dep in remaining_actions and not dep in completed_actions:
313 execute_action(dep, remaining_actions)
315 if action in completed_actions:
316 pass # we've already done this, don't do it twice...
317 elif function in ACTIONS: # alias of another action
318 execute_action(function, remaining_actions)
320 function(action, args)
322 completed_actions.add(action)
324 while len(args.actions) > 0:
325 execute_action(args.actions[0], args.actions[1:])
329 if __name__ == "__main__":