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.
25 # Note: we don't check for Python build-time dependencies until
26 # check_environment() function below. If possible, avoid importing
27 # any external libraries here - put in external script, or import in
28 # their specific function instead.
34 import multiprocessing
39 class FatalError(RuntimeError):
41 Wrapper class for runtime errors that aren't caused by bugs in idf.py or the build proces.s
45 # Use this Python interpreter for any subprocesses we launch
48 # note: os.environ changes don't automatically propagate to child processes,
49 # you have to pass env=os.environ explicitly anywhere that we create a process
50 os.environ["PYTHON"]=sys.executable
52 # Make flavors, across the various kinds of Windows environments & POSIX...
53 if "MSYSTEM" in os.environ: # MSYS
55 MAKE_GENERATOR = "MSYS Makefiles"
56 elif os.name == 'nt': # other Windows
57 MAKE_CMD = "mingw32-make"
58 MAKE_GENERATOR = "MinGW Makefiles"
61 MAKE_GENERATOR = "Unix Makefiles"
64 # ('generator name', 'build command line', 'version command line', 'verbose flag')
65 ("Ninja", [ "ninja" ], [ "ninja", "--version" ], "-v"),
66 (MAKE_GENERATOR, [ MAKE_CMD, "-j", str(multiprocessing.cpu_count()+2) ], [ "make", "--version" ], "VERBOSE=1"),
68 GENERATOR_CMDS = dict( (a[0], a[1]) for a in GENERATORS )
69 GENERATOR_VERBOSE = dict( (a[0], a[3]) for a in GENERATORS )
71 def _run_tool(tool_name, args, cwd):
73 " Quote 'arg' if necessary "
74 if " " in arg and not (arg.startswith('"') or arg.startswith("'")):
75 return "'" + arg + "'"
77 display_args = " ".join(quote_arg(arg) for arg in args)
78 print("Running %s in directory %s" % (tool_name, quote_arg(cwd)))
79 print('Executing "%s"...' % display_args)
81 # Note: we explicitly pass in os.environ here, as we may have set IDF_PATH there during startup
82 subprocess.check_call(args, env=os.environ, cwd=cwd)
83 except subprocess.CalledProcessError as e:
84 raise FatalError("%s failed with exit code %d" % (tool_name, e.returncode))
87 def check_environment():
89 Verify the environment contains the top-level tools we need to operate
91 (cmake will check a lot of other things)
93 if not executable_exists(["cmake", "--version"]):
94 raise FatalError("'cmake' must be available on the PATH to use idf.py")
95 # find the directory idf.py is in, then the parent directory of this, and assume this is IDF_PATH
96 detected_idf_path = os.path.realpath(os.path.join(os.path.dirname(__file__), ".."))
97 if "IDF_PATH" in os.environ:
98 set_idf_path = os.path.realpath(os.environ["IDF_PATH"])
99 if set_idf_path != detected_idf_path:
100 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..."
101 % (set_idf_path, detected_idf_path))
103 print("Setting IDF_PATH environment variable: %s" % detected_idf_path)
104 os.environ["IDF_PATH"] = detected_idf_path
106 # check Python dependencies
107 print("Checking Python dependencies...")
109 subprocess.check_call([ os.environ["PYTHON"],
110 os.path.join(os.environ["IDF_PATH"], "tools", "check_python_dependencies.py")],
112 except subprocess.CalledProcessError:
115 def executable_exists(args):
117 subprocess.check_output(args)
122 def detect_cmake_generator():
124 Find the default cmake generator, if none was specified. Raises an exception if no valid generator is found.
126 for (generator, _, version_check, _) in GENERATORS:
127 if executable_exists(version_check):
129 raise FatalError("To use idf.py, either the 'ninja' or 'GNU make' build tool must be available in the PATH")
131 def _ensure_build_directory(args, always_run_cmake=False):
132 """Check the build directory exists and that cmake has been run there.
134 If this isn't the case, create the build directory (if necessary) and
135 do an initial cmake run to configure it.
137 This function will also check args.generator parameter. If the parameter is incompatible with
138 the build directory, an error is raised. If the parameter is None, this function will set it to
139 an auto-detected default generator or to the value already configured in the build directory.
141 project_dir = args.project_dir
142 # Verify the project directory
143 if not os.path.isdir(project_dir):
144 if not os.path.exists(project_dir):
145 raise FatalError("Project directory %s does not exist")
147 raise FatalError("%s must be a project directory")
148 if not os.path.exists(os.path.join(project_dir, "CMakeLists.txt")):
149 raise FatalError("CMakeLists.txt not found in project directory %s" % project_dir)
151 # Verify/create the build directory
152 build_dir = args.build_dir
153 if not os.path.isdir(build_dir):
155 cache_path = os.path.join(build_dir, "CMakeCache.txt")
156 if not os.path.exists(cache_path) or always_run_cmake:
157 if args.generator is None:
158 args.generator = detect_cmake_generator()
160 cmake_args = ["cmake", "-G", args.generator, "-DPYTHON_DEPS_CHECKED=1"]
161 if not args.no_warnings:
162 cmake_args += [ "--warn-uninitialized" ]
164 cmake_args += [ "-DCCACHE_DISABLE=1" ]
165 cmake_args += [ project_dir]
166 _run_tool("cmake", cmake_args, cwd=args.build_dir)
168 # don't allow partially valid CMakeCache.txt files,
169 # to keep the "should I run cmake?" logic simple
170 if os.path.exists(cache_path):
171 os.remove(cache_path)
174 # Learn some things from the CMakeCache.txt file in the build directory
175 cache = parse_cmakecache(cache_path)
177 generator = cache["CMAKE_GENERATOR"]
179 generator = detect_cmake_generator()
180 if args.generator is None:
181 args.generator = generator # reuse the previously configured generator, if none was given
182 if generator != args.generator:
183 raise FatalError("Build is configured for generator '%s' not '%s'. Run 'idf.py fullclean' to start again."
184 % (generator, args.generator))
187 home_dir = cache["CMAKE_HOME_DIRECTORY"]
188 if os.path.normcase(os.path.realpath(home_dir)) != os.path.normcase(os.path.realpath(project_dir)):
189 raise FatalError("Build directory '%s' configured for project '%s' not '%s'. Run 'idf.py fullclean' to start again."
190 % (build_dir, os.path.realpath(home_dir), os.path.realpath(project_dir)))
192 pass # if cmake failed part way, CMAKE_HOME_DIRECTORY may not be set yet
195 def parse_cmakecache(path):
197 Parse the CMakeCache file at 'path'.
199 Returns a dict of name:value.
201 CMakeCache entries also each have a "type", but this is currently ignored.
204 with open(path) as f:
206 # cmake cache lines look like: CMAKE_CXX_FLAGS_DEBUG:STRING=-g
207 # groups are name, type, value
208 m = re.match(r"^([^#/:=]+):([^:=]+)=(.+)\n$", line)
210 result[m.group(1)] = m.group(3)
213 def build_target(target_name, args):
215 Execute the target build system to build target 'target_name'
217 Calls _ensure_build_directory() which will run cmake to generate a build
218 directory (with the specified generator) as needed.
220 _ensure_build_directory(args)
221 generator_cmd = GENERATOR_CMDS[args.generator]
222 if not args.no_ccache:
223 # Setting CCACHE_BASEDIR & CCACHE_NO_HASHDIR ensures that project paths aren't stored in the ccache entries
224 # (this means ccache hits can be shared between different projects. It may mean that some debug information
225 # will point to files in another project, if these files are perfect duplicates of each other.)
227 # It would be nicer to set these from cmake, but there's no cross-platform way to set build-time environment
228 #os.environ["CCACHE_BASEDIR"] = args.build_dir
229 #os.environ["CCACHE_NO_HASHDIR"] = "1"
232 generator_cmd += [ GENERATOR_VERBOSE[args.generator] ]
234 _run_tool(generator_cmd[0], generator_cmd + [target_name], args.build_dir)
237 def _get_esptool_args(args):
238 esptool_path = os.path.join(os.environ["IDF_PATH"], "components/esptool_py/esptool/esptool.py")
239 result = [ PYTHON, esptool_path ]
240 if args.port is not None:
241 result += [ "-p", args.port ]
242 result += [ "-b", str(args.baud) ]
245 def flash(action, args):
247 Run esptool to flash the entire project, from an argfile generated by the build system
249 flasher_args_path = { # action -> name of flasher args file generated by build system
250 "bootloader-flash": "flash_bootloader_args",
251 "partition_table-flash": "flash_partition_table_args",
252 "app-flash": "flash_app_args",
253 "flash": "flash_project_args",
255 esptool_args = _get_esptool_args(args)
256 esptool_args += [ "write_flash", "@"+flasher_args_path ]
257 _run_tool("esptool.py", esptool_args, args.build_dir)
260 def erase_flash(action, args):
261 esptool_args = _get_esptool_args(args)
262 esptool_args += [ "erase_flash" ]
263 _run_tool("esptool.py", esptool_args, args.build_dir)
266 def monitor(action, args):
268 Run idf_monitor.py to watch build output
270 desc_path = os.path.join(args.build_dir, "project_description.json")
271 if not os.path.exists(desc_path):
272 _ensure_build_directory(args)
273 with open(desc_path, "r") as f:
274 project_desc = json.load(f)
276 elf_file = os.path.join(args.build_dir, project_desc["app_elf"])
277 if not os.path.exists(elf_file):
278 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)
279 idf_monitor = os.path.join(os.environ["IDF_PATH"], "tools/idf_monitor.py")
280 monitor_args = [PYTHON, idf_monitor ]
281 if args.port is not None:
282 monitor_args += [ "-p", args.port ]
283 monitor_args += [ "-b", project_desc["monitor_baud"] ]
284 monitor_args += [ elf_file ]
286 idf_py = [ PYTHON ] + get_commandline_options() # commands to re-run idf.py
287 monitor_args += [ "-m", " ".join("'%s'" % a for a in idf_py) ]
289 if "MSYSTEM" is os.environ:
290 monitor_args = [ "winpty" ] + monitor_args
291 _run_tool("idf_monitor", monitor_args, args.project_dir)
294 def clean(action, args):
295 if not os.path.isdir(args.build_dir):
296 print("Build directory '%s' not found. Nothing to clean." % args.build_dir)
298 build_target("clean", args)
300 def reconfigure(action, args):
301 _ensure_build_directory(args, True)
303 def fullclean(action, args):
304 build_dir = args.build_dir
305 if not os.path.isdir(build_dir):
306 print("Build directory '%s' not found. Nothing to clean." % build_dir)
308 if len(os.listdir(build_dir)) == 0:
309 print("Build directory '%s' is empty. Nothing to clean." % build_dir)
312 if not os.path.exists(os.path.join(build_dir, "CMakeCache.txt")):
313 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)
314 red_flags = [ "CMakeLists.txt", ".git", ".svn" ]
315 for red in red_flags:
316 red = os.path.join(build_dir, red)
317 if os.path.exists(red):
318 raise FatalError("Refusing to automatically delete files in directory containing '%s'. Delete files manually if you're sure." % red)
319 # OK, delete everything in the build directory...
320 for f in os.listdir(build_dir): # TODO: once we are Python 3 only, this can be os.scandir()
321 f = os.path.join(build_dir, f)
327 def print_closing_message(args):
328 # print a closing message of some kind
331 if "flash" in str(args.actions):
335 # Otherwise, if we built any binaries print a message about
337 def print_flashing_message(title, key):
338 print("\n%s build complete. To flash, run this command:" % title)
340 with open(os.path.join(args.build_dir, "flasher_args.json")) as f:
341 flasher_args = json.load(f)
344 return os.path.relpath(os.path.join(args.build_dir, f))
346 if key != "project": # flashing a single item
348 if key == "bootloader": # bootloader needs --flash-mode, etc to be passed in
349 cmd = " ".join(flasher_args["write_flash_args"]) + " "
351 cmd += flasher_args[key]["offset"] + " "
352 cmd += flasher_path(flasher_args[key]["file"])
353 else: # flashing the whole project
354 cmd = " ".join(flasher_args["write_flash_args"]) + " "
355 flash_items = sorted(((o,f) for (o,f) in flasher_args["flash_files"].items() if len(o) > 0),
356 key = lambda (o,_): int(o, 0))
357 for o,f in flash_items:
358 cmd += o + " " + flasher_path(f) + " "
360 print("%s -p %s -b %s write_flash %s" % (
361 os.path.relpath("%s/components/esptool_py/esptool/esptool.py" % os.environ["IDF_PATH"]),
362 args.port or "(PORT)",
365 print("or run 'idf.py -p %s %s'" % (args.port or "(PORT)", key + "-flash" if key != "project" else "flash",))
367 if "all" in args.actions or "build" in args.actions:
368 print_flashing_message("Project", "project")
370 if "app" in args.actions:
371 print_flashing_message("App", "app")
372 if "partition_table" in args.actions:
373 print_flashing_message("Partition Table", "partition_table")
374 if "bootloader" in args.actions:
375 print_flashing_message("Bootloader", "bootloader")
378 # action name : ( function (or alias), dependencies, order-only dependencies )
379 "all" : ( build_target, [], [ "reconfigure", "menuconfig", "clean", "fullclean" ] ),
380 "build": ( "all", [], [] ), # build is same as 'all' target
381 "clean": ( clean, [], [ "fullclean" ] ),
382 "fullclean": ( fullclean, [], [] ),
383 "reconfigure": ( reconfigure, [], [ "menuconfig" ] ),
384 "menuconfig": ( build_target, [], [] ),
385 "confserver": ( build_target, [], [] ),
386 "size": ( build_target, [ "app" ], [] ),
387 "size-components": ( build_target, [ "app" ], [] ),
388 "size-files": ( build_target, [ "app" ], [] ),
389 "bootloader": ( build_target, [], [] ),
390 "bootloader-clean": ( build_target, [], [] ),
391 "bootloader-flash": ( flash, [ "bootloader" ], [ "erase_flash"] ),
392 "app": ( build_target, [], [ "clean", "fullclean", "reconfigure" ] ),
393 "app-flash": ( flash, [ "app" ], [ "erase_flash"]),
394 "partition_table": ( build_target, [], [ "reconfigure" ] ),
395 "partition_table-flash": ( flash, [ "partition_table" ], [ "erase_flash" ]),
396 "flash": ( flash, [ "all" ], [ "erase_flash" ] ),
397 "erase_flash": ( erase_flash, [], []),
398 "monitor": ( monitor, [], [ "flash", "partition_table-flash", "bootloader-flash", "app-flash" ]),
402 def get_commandline_options():
403 """ Return all the command line options up to but not including the action """
406 if a in ACTIONS.keys():
413 if sys.version_info[0] != 2 or sys.version_info[1] != 7:
414 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])
416 parser = argparse.ArgumentParser(description='ESP-IDF build management tool')
417 parser.add_argument('-p', '--port', help="Serial port",
418 default=os.environ.get('ESPPORT', None))
419 parser.add_argument('-b', '--baud', help="Baud rate",
420 default=os.environ.get('ESPBAUD', 460800))
421 parser.add_argument('-C', '--project-dir', help="Project directory", default=os.getcwd())
422 parser.add_argument('-B', '--build-dir', help="Build directory", default=None)
423 parser.add_argument('-G', '--generator', help="Cmake generator", choices=GENERATOR_CMDS.keys())
424 parser.add_argument('-n', '--no-warnings', help="Disable Cmake warnings", action="store_true")
425 parser.add_argument('-v', '--verbose', help="Verbose build output", action="store_true")
426 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")
427 parser.add_argument('actions', help="Actions (build targets or other operations)", nargs='+',
428 choices=ACTIONS.keys())
430 args = parser.parse_args()
434 # Advanced parameter checks
435 if args.build_dir is not None and os.path.realpath(args.project_dir) == os.path.realpath(args.build_dir):
436 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.")
437 if args.build_dir is None:
438 args.build_dir = os.path.join(args.project_dir, "build")
439 args.build_dir = os.path.realpath(args.build_dir)
441 completed_actions = set()
442 def execute_action(action, remaining_actions):
443 ( function, dependencies, order_dependencies ) = ACTIONS[action]
444 # very simple dependency management, build a set of completed actions and make sure
445 # all dependencies are in it
446 for dep in dependencies:
447 if not dep in completed_actions:
448 execute_action(dep, remaining_actions)
449 for dep in order_dependencies:
450 if dep in remaining_actions and not dep in completed_actions:
451 execute_action(dep, remaining_actions)
453 if action in completed_actions:
454 pass # we've already done this, don't do it twice...
455 elif function in ACTIONS: # alias of another action
456 execute_action(function, remaining_actions)
458 function(action, args)
460 completed_actions.add(action)
462 actions = list(args.actions)
463 while len(actions) > 0:
464 execute_action(actions[0], actions[1:])
467 print_closing_message(args)
469 if __name__ == "__main__":
472 except FatalError as e: