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 class FatalError(RuntimeError):
36 Wrapper class for runtime errors that aren't caused by bugs in idf.py or the build proces.s
40 # Use this Python interpreter for any subprocesses we launch
43 # note: os.environ changes don't automatically propagate to child processes,
44 # you have to pass this in explicitly
45 os.environ["PYTHON"]=sys.executable
47 # Make flavors, across the various kinds of Windows environments & POSIX...
48 if "MSYSTEM" in os.environ: # MSYS
50 MAKE_GENERATOR = "MSYS Makefiles"
51 elif os.name == 'nt': # other Windows
52 MAKE_CMD = "mingw32-make"
53 MAKE_GENERATOR = "MinGW Makefiles"
56 MAKE_GENERATOR = "Unix Makefiles"
59 # ('generator name', 'build command line', 'version command line', 'verbose flag')
60 ("Ninja", [ "ninja" ], [ "ninja", "--version" ], "-v"),
61 (MAKE_GENERATOR, [ MAKE_CMD, "-j", str(multiprocessing.cpu_count()+2) ], [ "make", "--version" ], "VERBOSE=1"),
63 GENERATOR_CMDS = dict( (a[0], a[1]) for a in GENERATORS )
64 GENERATOR_VERBOSE = dict( (a[0], a[3]) for a in GENERATORS )
66 def _run_tool(tool_name, args, cwd):
68 " Quote 'arg' if necessary "
69 if " " in arg and not (arg.startswith('"') or arg.startswith("'")):
70 return "'" + arg + "'"
72 display_args = " ".join(quote_arg(arg) for arg in args)
73 print("Running %s in directory %s" % (tool_name, quote_arg(cwd)))
74 print('Executing "%s"...' % display_args)
76 # Note: we explicitly pass in os.environ here, as we may have set IDF_PATH there during startup
77 subprocess.check_call(args, env=os.environ, cwd=cwd)
78 except subprocess.CalledProcessError as e:
79 raise FatalError("%s failed with exit code %d" % (tool_name, e.returncode))
82 def check_environment():
84 Verify the environment contains the top-level tools we need to operate
86 (cmake will check a lot of other things)
88 if not executable_exists(["cmake", "--version"]):
89 raise FatalError("'cmake' must be available on the PATH to use idf.py")
90 # find the directory idf.py is in, then the parent directory of this, and assume this is IDF_PATH
91 detected_idf_path = os.path.realpath(os.path.join(os.path.dirname(__file__), ".."))
92 if "IDF_PATH" in os.environ:
93 set_idf_path = os.path.realpath(os.environ["IDF_PATH"])
94 if set_idf_path != detected_idf_path:
95 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..."
96 % (set_idf_path, detected_idf_path))
98 os.environ["IDF_PATH"] = detected_idf_path
100 def executable_exists(args):
102 subprocess.check_output(args)
107 def detect_cmake_generator():
109 Find the default cmake generator, if none was specified. Raises an exception if no valid generator is found.
111 for (generator, _, version_check, _) in GENERATORS:
112 if executable_exists(version_check):
114 raise FatalError("To use idf.py, either the 'ninja' or 'GNU make' build tool must be available in the PATH")
116 def _ensure_build_directory(args, always_run_cmake=False):
117 """Check the build directory exists and that cmake has been run there.
119 If this isn't the case, create the build directory (if necessary) and
120 do an initial cmake run to configure it.
122 This function will also check args.generator parameter. If the parameter is incompatible with
123 the build directory, an error is raised. If the parameter is None, this function will set it to
124 an auto-detected default generator or to the value already configured in the build directory.
126 project_dir = args.project_dir
127 # Verify the project directory
128 if not os.path.isdir(project_dir):
129 if not os.path.exists(project_dir):
130 raise FatalError("Project directory %s does not exist")
132 raise FatalError("%s must be a project directory")
133 if not os.path.exists(os.path.join(project_dir, "CMakeLists.txt")):
134 raise FatalError("CMakeLists.txt not found in project directory %s" % project_dir)
136 # Verify/create the build directory
137 build_dir = args.build_dir
138 if not os.path.isdir(build_dir):
140 cache_path = os.path.join(build_dir, "CMakeCache.txt")
141 if not os.path.exists(cache_path) or always_run_cmake:
142 if args.generator is None:
143 args.generator = detect_cmake_generator()
145 cmake_args = ["cmake", "-G", args.generator]
146 if not args.no_warnings:
147 cmake_args += [ "--warn-uninitialized" ]
149 cmake_args += [ "-DCCACHE_DISABLE=1" ]
150 cmake_args += [ project_dir]
151 _run_tool("cmake", cmake_args, cwd=args.build_dir)
153 # don't allow partially valid CMakeCache.txt files,
154 # to keep the "should I run cmake?" logic simple
155 if os.path.exists(cache_path):
156 os.remove(cache_path)
159 # Learn some things from the CMakeCache.txt file in the build directory
160 cache = parse_cmakecache(cache_path)
162 generator = cache["CMAKE_GENERATOR"]
164 generator = detect_cmake_generator()
165 if args.generator is None:
166 args.generator = generator # reuse the previously configured generator, if none was given
167 if generator != args.generator:
168 raise FatalError("Build is configured for generator '%s' not '%s'. Run 'idf.py fullclean' to start again."
169 % (generator, args.generator))
172 home_dir = cache["CMAKE_HOME_DIRECTORY"]
173 if os.path.normcase(os.path.realpath(home_dir)) != os.path.normcase(os.path.realpath(project_dir)):
174 raise FatalError("Build directory '%s' configured for project '%s' not '%s'. Run 'idf.py fullclean' to start again."
175 % (build_dir, os.path.realpath(home_dir), os.path.realpath(project_dir)))
177 pass # if cmake failed part way, CMAKE_HOME_DIRECTORY may not be set yet
180 def parse_cmakecache(path):
182 Parse the CMakeCache file at 'path'.
184 Returns a dict of name:value.
186 CMakeCache entries also each have a "type", but this is currently ignored.
189 with open(path) as f:
191 # cmake cache lines look like: CMAKE_CXX_FLAGS_DEBUG:STRING=-g
192 # groups are name, type, value
193 m = re.match(r"^([^#/:=]+):([^:=]+)=(.+)\n$", line)
195 result[m.group(1)] = m.group(3)
198 def build_target(target_name, args):
200 Execute the target build system to build target 'target_name'
202 Calls _ensure_build_directory() which will run cmake to generate a build
203 directory (with the specified generator) as needed.
205 _ensure_build_directory(args)
206 generator_cmd = GENERATOR_CMDS[args.generator]
207 if not args.no_ccache:
208 # Setting CCACHE_BASEDIR & CCACHE_NO_HASHDIR ensures that project paths aren't stored in the ccache entries
209 # (this means ccache hits can be shared between different projects. It may mean that some debug information
210 # will point to files in another project, if these files are perfect duplicates of each other.)
212 # It would be nicer to set these from cmake, but there's no cross-platform way to set build-time environment
213 #os.environ["CCACHE_BASEDIR"] = args.build_dir
214 #os.environ["CCACHE_NO_HASHDIR"] = "1"
217 generator_cmd += [ GENERATOR_VERBOSE[args.generator] ]
219 _run_tool(generator_cmd[0], generator_cmd + [target_name], args.build_dir)
222 def _get_esptool_args(args):
223 esptool_path = os.path.join(os.environ["IDF_PATH"], "components/esptool_py/esptool/esptool.py")
224 result = [ PYTHON, esptool_path ]
225 if args.port is not None:
226 result += [ "-p", args.port ]
227 result += [ "-b", str(args.baud) ]
230 def flash(action, args):
232 Run esptool to flash the entire project, from an argfile generated by the build system
234 flasher_args_path = { # action -> name of flasher args file generated by build system
235 "bootloader-flash": "flash_bootloader_args",
236 "partition_table-flash": "flash_partition_table_args",
237 "app-flash": "flash_app_args",
238 "flash": "flash_project_args",
240 esptool_args = _get_esptool_args(args)
241 esptool_args += [ "write_flash", "@"+flasher_args_path ]
242 _run_tool("esptool.py", esptool_args, args.build_dir)
245 def erase_flash(action, args):
246 esptool_args = _get_esptool_args(args)
247 esptool_args += [ "erase_flash" ]
248 _run_tool("esptool.py", esptool_args, args.build_dir)
251 def monitor(action, args):
253 Run idf_monitor.py to watch build output
255 desc_path = os.path.join(args.build_dir, "project_description.json")
256 if not os.path.exists(desc_path):
257 _ensure_build_directory(args)
258 with open(desc_path, "r") as f:
259 project_desc = json.load(f)
261 elf_file = os.path.join(args.build_dir, project_desc["app_elf"])
262 if not os.path.exists(elf_file):
263 raise FatalError("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)
264 idf_monitor = os.path.join(os.environ["IDF_PATH"], "tools/idf_monitor.py")
265 monitor_args = [PYTHON, idf_monitor ]
266 if args.port is not None:
267 monitor_args += [ "-p", args.port ]
268 monitor_args += [ "-b", project_desc["monitor_baud"] ]
269 monitor_args += [ elf_file ]
271 idf_py = [ PYTHON ] + get_commandline_options() # commands to re-run idf.py
272 monitor_args += [ "-m", " ".join("'%s'" % a for a in idf_py) ]
274 if "MSYSTEM" is os.environ:
275 monitor_args = [ "winpty" ] + monitor_args
276 _run_tool("idf_monitor", monitor_args, args.project_dir)
279 def clean(action, args):
280 if not os.path.isdir(args.build_dir):
281 print("Build directory '%s' not found. Nothing to clean." % args.build_dir)
283 build_target("clean", args)
285 def reconfigure(action, args):
286 _ensure_build_directory(args, True)
288 def fullclean(action, args):
289 build_dir = args.build_dir
290 if not os.path.isdir(build_dir):
291 print("Build directory '%s' not found. Nothing to clean." % build_dir)
293 if len(os.listdir(build_dir)) == 0:
294 print("Build directory '%s' is empty. Nothing to clean." % build_dir)
297 if not os.path.exists(os.path.join(build_dir, "CMakeCache.txt")):
298 raise FatalError("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)
299 red_flags = [ "CMakeLists.txt", ".git", ".svn" ]
300 for red in red_flags:
301 red = os.path.join(build_dir, red)
302 if os.path.exists(red):
303 raise FatalError("Refusing to automatically delete files in directory containing '%s'. Delete files manually if you're sure." % red)
304 # OK, delete everything in the build directory...
305 for f in os.listdir(build_dir): # TODO: once we are Python 3 only, this can be os.scandir()
306 f = os.path.join(build_dir, f)
312 def print_closing_message(args):
313 # print a closing message of some kind
316 if "flash" in str(args.actions):
320 # Otherwise, if we built any binaries print a message about
322 def print_flashing_message(title, key):
323 print("\n%s build complete. To flash, run this command:" % title)
325 with open(os.path.join(args.build_dir, "flasher_args.json")) as f:
326 flasher_args = json.load(f)
329 return os.path.relpath(os.path.join(args.build_dir, f))
333 if key == "bootloader":
334 cmd = " ".join(flasher_args["write_flash_args"]) + " "
336 cmd += flasher_args[key]["offset"] + " "
337 cmd += flasher_path(flasher_args[key]["file"])
339 cmd = " ".join(flasher_args["write_flash_args"]) + " "
340 for o,f in flasher_args["flash_files"].items():
341 cmd += o + " " + flasher_path(f) + " "
343 print("%s -p %s -b %s write_flash %s" % (
344 os.path.relpath("%s/components/esptool_py/esptool/esptool.py" % os.environ["IDF_PATH"]),
345 args.port or "(PORT)",
348 print("or run 'idf.py %s'" % (key + "-flash" if key != "project" else "flash",))
350 if "all" in args.actions or "build" in args.actions:
351 print_flashing_message("Project", "project")
353 if "app" in args.actions:
354 print_flashing_message("App", "app")
355 if "partition_table" in args.actions:
356 print_flashing_message("Partition Table", "partition_table")
357 if "bootloader" in args.actions:
358 print_flashing_message("Bootloader", "bootloader")
361 # action name : ( function (or alias), dependencies, order-only dependencies )
362 "all" : ( build_target, [], [ "reconfigure", "menuconfig", "clean", "fullclean" ] ),
363 "build": ( "all", [], [] ), # build is same as 'all' target
364 "clean": ( clean, [], [ "fullclean" ] ),
365 "fullclean": ( fullclean, [], [] ),
366 "reconfigure": ( reconfigure, [], [ "menuconfig" ] ),
367 "menuconfig": ( build_target, [], [] ),
368 "size": ( build_target, [ "app" ], [] ),
369 "size-components": ( build_target, [ "app" ], [] ),
370 "size-files": ( build_target, [ "app" ], [] ),
371 "bootloader": ( build_target, [], [] ),
372 "bootloader-clean": ( build_target, [], [] ),
373 "bootloader-flash": ( flash, [ "bootloader" ], [ "erase_flash"] ),
374 "app": ( build_target, [], [ "clean", "fullclean", "reconfigure" ] ),
375 "app-flash": ( flash, [ "app" ], [ "erase_flash"]),
376 "partition_table": ( build_target, [], [ "reconfigure" ] ),
377 "partition_table-flash": ( flash, [ "partition_table" ], [ "erase_flash" ]),
378 "flash": ( flash, [ "all" ], [ "erase_flash" ] ),
379 "erase_flash": ( erase_flash, [], []),
380 "monitor": ( monitor, [], [ "flash", "partition_table-flash", "bootloader-flash", "app-flash" ]),
384 def get_commandline_options():
385 """ Return all the command line options up to but not including the action """
388 if a in ACTIONS.keys():
395 if sys.version_info[0] != 2 or sys.version_info[1] != 7:
396 raise FatalError("ESP-IDF currently only supports Python 2.7, and this is Python %d.%d.%d. Search for 'Setting the Python Interpreter' in the ESP-IDF docs for some tips to handle this." % sys.version_info[:3])
398 parser = argparse.ArgumentParser(description='ESP-IDF build management tool')
399 parser.add_argument('-p', '--port', help="Serial port",
400 default=os.environ.get('ESPPORT', None))
401 parser.add_argument('-b', '--baud', help="Baud rate",
402 default=os.environ.get('ESPBAUD', 460800))
403 parser.add_argument('-C', '--project-dir', help="Project directory", default=os.getcwd())
404 parser.add_argument('-B', '--build-dir', help="Build directory", default=None)
405 parser.add_argument('-G', '--generator', help="Cmake generator", choices=GENERATOR_CMDS.keys())
406 parser.add_argument('-n', '--no-warnings', help="Disable Cmake warnings", action="store_true")
407 parser.add_argument('-v', '--verbose', help="Verbose build output", action="store_true")
408 parser.add_argument('--no-ccache', help="Disable ccache. Otherwise, if ccache is available on the PATH then it will be used for faster builds.", action="store_true")
409 parser.add_argument('actions', help="Actions (build targets or other operations)", nargs='+',
410 choices=ACTIONS.keys())
412 args = parser.parse_args()
416 # Advanced parameter checks
417 if args.build_dir is not None and os.path.realpath(args.project_dir) == os.path.realpath(args.build_dir):
418 raise FatalError("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.")
419 if args.build_dir is None:
420 args.build_dir = os.path.join(args.project_dir, "build")
421 args.build_dir = os.path.realpath(args.build_dir)
423 completed_actions = set()
424 def execute_action(action, remaining_actions):
425 ( function, dependencies, order_dependencies ) = ACTIONS[action]
426 # very simple dependency management, build a set of completed actions and make sure
427 # all dependencies are in it
428 for dep in dependencies:
429 if not dep in completed_actions:
430 execute_action(dep, remaining_actions)
431 for dep in order_dependencies:
432 if dep in remaining_actions and not dep in completed_actions:
433 execute_action(dep, remaining_actions)
435 if action in completed_actions:
436 pass # we've already done this, don't do it twice...
437 elif function in ACTIONS: # alias of another action
438 execute_action(function, remaining_actions)
440 function(action, args)
442 completed_actions.add(action)
444 actions = list(args.actions)
445 while len(actions) > 0:
446 execute_action(actions[0], actions[1:])
449 print_closing_message(args)
451 if __name__ == "__main__":
454 except FatalError as e: