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 2019 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 # WARNING: 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.
32 import multiprocessing
41 class FatalError(RuntimeError):
43 Wrapper class for runtime errors that aren't caused by bugs in idf.py or the build proces.s
49 # Use this Python interpreter for any subprocesses we launch
50 PYTHON = sys.executable
52 # note: os.environ changes don't automatically propagate to child processes,
53 # you have to pass env=os.environ explicitly anywhere that we create a process
54 os.environ["PYTHON"] = sys.executable
56 # Name of the program, normally 'idf.py'.
57 # Can be overridden from idf.bat using IDF_PY_PROGRAM_NAME
58 PROG = os.getenv("IDF_PY_PROGRAM_NAME", sys.argv[0])
60 # Make flavors, across the various kinds of Windows environments & POSIX...
61 if "MSYSTEM" in os.environ: # MSYS
63 MAKE_GENERATOR = "MSYS Makefiles"
64 elif os.name == "nt": # other Windows
65 MAKE_CMD = "mingw32-make"
66 MAKE_GENERATOR = "MinGW Makefiles"
69 MAKE_GENERATOR = "Unix Makefiles"
72 # ('generator name', 'build command line', 'version command line', 'verbose flag')
73 ("Ninja", ["ninja"], ["ninja", "--version"], "-v"),
76 [MAKE_CMD, "-j", str(multiprocessing.cpu_count() + 2)],
77 [MAKE_CMD, "--version"],
81 GENERATOR_CMDS = dict((a[0], a[1]) for a in GENERATORS)
82 GENERATOR_VERBOSE = dict((a[0], a[3]) for a in GENERATORS)
85 def _run_tool(tool_name, args, cwd):
87 " Quote 'arg' if necessary "
88 if " " in arg and not (arg.startswith('"') or arg.startswith("'")):
89 return "'" + arg + "'"
92 display_args = " ".join(quote_arg(arg) for arg in args)
93 print("Running %s in directory %s" % (tool_name, quote_arg(cwd)))
94 print('Executing "%s"...' % str(display_args))
96 # Note: we explicitly pass in os.environ here, as we may have set IDF_PATH there during startup
97 subprocess.check_call(args, env=os.environ, cwd=cwd)
98 except subprocess.CalledProcessError as e:
99 raise FatalError("%s failed with exit code %d" % (tool_name, e.returncode))
104 Return the cannonical path with normalized case.
106 It is useful on Windows to comparision paths in case-insensitive manner.
107 On Unix and Mac OS X it works as `os.path.realpath()` only.
109 return os.path.normcase(os.path.realpath(path))
112 def check_environment():
114 Verify the environment contains the top-level tools we need to operate
116 (cmake will check a lot of other things)
118 if not executable_exists(["cmake", "--version"]):
119 raise FatalError("'cmake' must be available on the PATH to use %s" % PROG)
120 # find the directory idf.py is in, then the parent directory of this, and assume this is IDF_PATH
121 detected_idf_path = _realpath(os.path.join(os.path.dirname(__file__), ".."))
122 if "IDF_PATH" in os.environ:
123 set_idf_path = _realpath(os.environ["IDF_PATH"])
124 if set_idf_path != detected_idf_path:
126 "WARNING: IDF_PATH environment variable is set to %s but %s path indicates IDF directory %s. "
127 "Using the environment variable directory, but results may be unexpected..."
128 % (set_idf_path, PROG, detected_idf_path)
131 print("Setting IDF_PATH environment variable: %s" % detected_idf_path)
132 os.environ["IDF_PATH"] = detected_idf_path
134 # check Python dependencies
135 print("Checking Python dependencies...")
137 subprocess.check_call(
139 os.environ["PYTHON"],
141 os.environ["IDF_PATH"], "tools", "check_python_dependencies.py"
146 except subprocess.CalledProcessError:
150 def executable_exists(args):
152 subprocess.check_output(args)
158 def detect_cmake_generator():
160 Find the default cmake generator, if none was specified. Raises an exception if no valid generator is found.
162 for (generator, _, version_check, _) in GENERATORS:
163 if executable_exists(version_check):
166 "To use %s, either the 'ninja' or 'GNU make' build tool must be available in the PATH"
171 def _strip_quotes(value, regexp=re.compile(r"^\"(.*)\"$|^'(.*)'$|^(.*)$")):
173 Strip quotes like CMake does during parsing cache entries
176 return [x for x in regexp.match(value).groups() if x is not None][0].rstrip()
179 def _new_cmakecache_entries(cache_path, new_cache_entries):
180 if not os.path.exists(cache_path):
183 current_cache = parse_cmakecache(cache_path)
185 if new_cache_entries:
186 current_cache = parse_cmakecache(cache_path)
188 for entry in new_cache_entries:
189 key, value = entry.split("=", 1)
190 current_value = current_cache.get(key, None)
191 if current_value is None or _strip_quotes(value) != current_value:
197 def _ensure_build_directory(args, always_run_cmake=False):
198 """Check the build directory exists and that cmake has been run there.
200 If this isn't the case, create the build directory (if necessary) and
201 do an initial cmake run to configure it.
203 This function will also check args.generator parameter. If the parameter is incompatible with
204 the build directory, an error is raised. If the parameter is None, this function will set it to
205 an auto-detected default generator or to the value already configured in the build directory.
207 project_dir = args.project_dir
208 # Verify the project directory
209 if not os.path.isdir(project_dir):
210 if not os.path.exists(project_dir):
211 raise FatalError("Project directory %s does not exist" % project_dir)
213 raise FatalError("%s must be a project directory" % project_dir)
214 if not os.path.exists(os.path.join(project_dir, "CMakeLists.txt")):
216 "CMakeLists.txt not found in project directory %s" % project_dir
219 # Verify/create the build directory
220 build_dir = args.build_dir
221 if not os.path.isdir(build_dir):
222 os.makedirs(build_dir)
223 cache_path = os.path.join(build_dir, "CMakeCache.txt")
225 if always_run_cmake or _new_cmakecache_entries(cache_path, args.define_cache_entry):
226 if args.generator is None:
227 args.generator = detect_cmake_generator()
233 "-DPYTHON_DEPS_CHECKED=1",
236 if not args.no_warnings:
237 cmake_args += ["--warn-uninitialized"]
239 cmake_args += ["-DCCACHE_ENABLE=1"]
240 if args.define_cache_entry:
241 cmake_args += ["-D" + d for d in args.define_cache_entry]
242 cmake_args += [project_dir]
244 _run_tool("cmake", cmake_args, cwd=args.build_dir)
246 # don't allow partially valid CMakeCache.txt files,
247 # to keep the "should I run cmake?" logic simple
248 if os.path.exists(cache_path):
249 os.remove(cache_path)
252 # Learn some things from the CMakeCache.txt file in the build directory
253 cache = parse_cmakecache(cache_path)
255 generator = cache["CMAKE_GENERATOR"]
257 generator = detect_cmake_generator()
258 if args.generator is None:
261 ) # reuse the previously configured generator, if none was given
262 if generator != args.generator:
264 "Build is configured for generator '%s' not '%s'. Run '%s fullclean' to start again."
265 % (generator, args.generator, PROG)
269 home_dir = cache["CMAKE_HOME_DIRECTORY"]
270 if _realpath(home_dir) != _realpath(project_dir):
272 "Build directory '%s' configured for project '%s' not '%s'. Run '%s fullclean' to start again."
273 % (build_dir, _realpath(home_dir), _realpath(project_dir), PROG)
276 pass # if cmake failed part way, CMAKE_HOME_DIRECTORY may not be set yet
279 def parse_cmakecache(path):
281 Parse the CMakeCache file at 'path'.
283 Returns a dict of name:value.
285 CMakeCache entries also each have a "type", but this is currently ignored.
288 with open(path) as f:
290 # cmake cache lines look like: CMAKE_CXX_FLAGS_DEBUG:STRING=-g
291 # groups are name, type, value
292 m = re.match(r"^([^#/:=]+):([^:=]+)=(.*)\n$", line)
294 result[m.group(1)] = m.group(3)
298 def build_target(target_name, ctx, args):
300 Execute the target build system to build target 'target_name'
302 Calls _ensure_build_directory() which will run cmake to generate a build
303 directory (with the specified generator) as needed.
305 _ensure_build_directory(args)
306 generator_cmd = GENERATOR_CMDS[args.generator]
309 # Setting CCACHE_BASEDIR & CCACHE_NO_HASHDIR ensures that project paths aren't stored in the ccache entries
310 # (this means ccache hits can be shared between different projects. It may mean that some debug information
311 # will point to files in another project, if these files are perfect duplicates of each other.)
313 # It would be nicer to set these from cmake, but there's no cross-platform way to set build-time environment
314 # os.environ["CCACHE_BASEDIR"] = args.build_dir
315 # os.environ["CCACHE_NO_HASHDIR"] = "1"
318 generator_cmd += [GENERATOR_VERBOSE[args.generator]]
320 _run_tool(generator_cmd[0], generator_cmd + [target_name], args.build_dir)
323 def _get_esptool_args(args):
324 esptool_path = os.path.join(
325 os.environ["IDF_PATH"], "components/esptool_py/esptool/esptool.py"
327 if args.port is None:
328 args.port = get_default_serial_port()
329 result = [PYTHON, esptool_path]
330 result += ["-p", args.port]
331 result += ["-b", str(args.baud)]
333 with open(os.path.join(args.build_dir, "flasher_args.json")) as f:
334 flasher_args = json.load(f)
336 extra_esptool_args = flasher_args["extra_esptool_args"]
337 result += ["--after", extra_esptool_args["after"]]
341 def flash(action, ctx, args):
343 Run esptool to flash the entire project, from an argfile generated by the build system
345 flasher_args_path = { # action -> name of flasher args file generated by build system
346 "bootloader-flash": "flash_bootloader_args",
347 "partition_table-flash": "flash_partition_table_args",
348 "app-flash": "flash_app_args",
349 "flash": "flash_project_args",
350 "encrypted-app-flash": "flash_encrypted_app_args",
351 "encrypted-flash": "flash_encrypted_project_args",
355 esptool_args = _get_esptool_args(args)
356 esptool_args += ["write_flash", "@" + flasher_args_path]
357 _run_tool("esptool.py", esptool_args, args.build_dir)
360 def erase_flash(action, ctx, args):
361 esptool_args = _get_esptool_args(args)
362 esptool_args += ["erase_flash"]
363 _run_tool("esptool.py", esptool_args, args.build_dir)
366 def monitor(action, ctx, args, print_filter):
368 Run idf_monitor.py to watch build output
370 if args.port is None:
371 args.port = get_default_serial_port()
372 desc_path = os.path.join(args.build_dir, "project_description.json")
373 if not os.path.exists(desc_path):
374 _ensure_build_directory(args)
375 with open(desc_path, "r") as f:
376 project_desc = json.load(f)
378 elf_file = os.path.join(args.build_dir, project_desc["app_elf"])
379 if not os.path.exists(elf_file):
381 "ELF file '%s' not found. You need to build & flash the project before running 'monitor', "
382 "and the binary on the device must match the one in the build directory exactly. "
383 "Try '%s flash monitor'." % (elf_file, PROG)
385 idf_monitor = os.path.join(os.environ["IDF_PATH"], "tools/idf_monitor.py")
386 monitor_args = [PYTHON, idf_monitor]
387 if args.port is not None:
388 monitor_args += ["-p", args.port]
389 monitor_args += ["-b", project_desc["monitor_baud"]]
390 if print_filter is not None:
391 monitor_args += ["--print_filter", print_filter]
392 monitor_args += [elf_file]
394 idf_py = [PYTHON] + get_commandline_options(ctx) # commands to re-run idf.py
395 monitor_args += ["-m", " ".join("'%s'" % a for a in idf_py)]
397 if "MSYSTEM" in os.environ:
398 monitor_args = ["winpty"] + monitor_args
399 _run_tool("idf_monitor", monitor_args, args.project_dir)
402 def clean(action, ctx, args):
403 if not os.path.isdir(args.build_dir):
404 print("Build directory '%s' not found. Nothing to clean." % args.build_dir)
406 build_target("clean", ctx, args)
409 def reconfigure(action, ctx, args):
410 _ensure_build_directory(args, True)
413 def _delete_windows_symlinks(directory):
415 It deletes symlinks recursively on Windows. It is useful for Python 2 which doesn't detect symlinks on Windows.
421 for root, dirnames, _filenames in os.walk(directory):
423 full_path = os.path.join(root, d)
425 full_path = full_path.decode("utf-8")
428 if ctypes.windll.kernel32.GetFileAttributesW(full_path) & 0x0400:
430 deleted_paths.append(full_path)
434 def fullclean(action, ctx, args):
435 build_dir = args.build_dir
436 if not os.path.isdir(build_dir):
437 print("Build directory '%s' not found. Nothing to clean." % build_dir)
439 if len(os.listdir(build_dir)) == 0:
440 print("Build directory '%s' is empty. Nothing to clean." % build_dir)
443 if not os.path.exists(os.path.join(build_dir, "CMakeCache.txt")):
445 "Directory '%s' doesn't seem to be a CMake build directory. Refusing to automatically "
446 "delete files in this directory. Delete the directory manually to 'clean' it."
449 red_flags = ["CMakeLists.txt", ".git", ".svn"]
450 for red in red_flags:
451 red = os.path.join(build_dir, red)
452 if os.path.exists(red):
454 "Refusing to automatically delete files in directory containing '%s'. Delete files manually if you're sure."
457 # OK, delete everything in the build directory...
458 # Note: Python 2.7 doesn't detect symlinks on Windows (it is supported form 3.2). Tools promising to not
459 # follow symlinks will actually follow them. Deleting the build directory with symlinks deletes also items
460 # outside of this directory.
461 deleted_symlinks = _delete_windows_symlinks(build_dir)
462 if args.verbose and len(deleted_symlinks) > 1:
464 "The following symlinks were identified and removed:\n%s"
465 % "\n".join(deleted_symlinks)
469 ): # TODO: once we are Python 3 only, this can be os.scandir()
470 f = os.path.join(build_dir, f)
472 print("Removing: %s" % f)
479 def _safe_relpath(path, start=None):
480 """ Return a relative path, same as os.path.relpath, but only if this is possible.
482 It is not possible on Windows, if the start directory and the path are on different drives.
485 return os.path.relpath(path, os.curdir if start is None else start)
487 return os.path.abspath(path)
490 def get_commandline_options(ctx):
491 """ Return all the command line options up to first action """
492 # This approach ignores argument parsing done Click
496 if arg in ctx.command.commands_with_aliases:
504 def get_default_serial_port():
505 """ Return a default serial port. esptool can do this (smarter), but it can create
506 inconsistencies where esptool.py uses one port and idf_monitor uses another.
508 Same logic as esptool.py search order, reverse sort by name and choose the first port.
510 # Import is done here in order to move it after the check_environment() ensured that pyserial has been installed
511 import serial.tools.list_ports
513 ports = list(reversed(sorted(p.device for p in serial.tools.list_ports.comports())))
516 "Choosing default port %s (use '-p PORT' option to set a specific serial port)"
517 % ports[0].encode("ascii", "ignore")
522 "No serial ports found. Connect a device, or use '-p PORT' option to set a specific port."
526 class PropertyDict(dict):
527 def __init__(self, *args, **kwargs):
528 super(PropertyDict, self).__init__(*args, **kwargs)
533 # Click is imported here to run it after check_environment()
538 self, callback, name, aliases, dependencies, order_dependencies, action_args
540 self.callback = callback
542 self.dependencies = dependencies
543 self.order_dependencies = order_dependencies
544 self.action_args = action_args
545 self.aliases = aliases
547 def run(self, context, global_args, action_args=None):
548 if action_args is None:
549 action_args = self.action_args
551 self.callback(self.name, context, global_args, **action_args)
553 class Action(click.Command):
559 order_dependencies=None,
562 super(Action, self).__init__(name, **kwargs)
564 self.name = self.name or self.callback.__name__
568 self.aliases = aliases
570 self.help = self.help or self.callback.__doc__
571 if self.help is None:
574 if dependencies is None:
577 if order_dependencies is None:
578 order_dependencies = []
580 # Show first line of help if short help is missing
581 self.short_help = self.short_help or self.help.split("\n")[0]
583 # Add aliases to help string
585 aliases_help = "Aliases: %s." % ", ".join(aliases)
587 self.help = "\n".join([self.help, aliases_help])
588 self.short_help = " ".join([aliases_help, self.short_help])
590 if self.callback is not None:
591 callback = self.callback
593 def wrapped_callback(**action_args):
597 dependencies=dependencies,
598 order_dependencies=order_dependencies,
599 action_args=action_args,
600 aliases=self.aliases,
603 self.callback = wrapped_callback
605 class Argument(click.Argument):
606 """Positional argument"""
608 def __init__(self, **kwargs):
609 names = kwargs.pop("names")
610 super(Argument, self).__init__(names, **kwargs)
614 Scope for sub-command option.
616 - default - only available on defined level (global/action)
617 - global - When defined for action, also available as global
618 - shared - Opposite to 'global': when defined in global scope, also available for all actions
621 SCOPES = ("default", "global", "shared")
623 def __init__(self, scope=None):
625 self._scope = "default"
626 elif isinstance(scope, str) and scope in self.SCOPES:
628 elif isinstance(scope, Scope):
629 self._scope = str(scope)
631 raise FatalError("Unknown scope for option: %s" % scope)
635 return self._scope == "global"
639 return self._scope == "shared"
644 class Option(click.Option):
645 """Option that knows whether it should be global"""
647 def __init__(self, scope=None, **kwargs):
648 kwargs["param_decls"] = kwargs.pop("names")
649 super(Option, self).__init__(**kwargs)
651 self.scope = Scope(scope)
653 if self.scope.is_global:
654 self.help += " This option can be used at most once either globally, or for one subcommand."
656 class CLI(click.MultiCommand):
657 """Action list contains all actions with options available for CLI"""
659 def __init__(self, action_lists=None, help=None):
660 super(CLI, self).__init__(
662 invoke_without_command=True,
663 result_callback=self.execute_tasks,
664 context_settings={"max_content_width": 140},
668 self.global_action_callbacks = []
669 self.commands_with_aliases = {}
671 if action_lists is None:
676 for action_list in action_lists:
678 for option_args in action_list.get("global_options", []):
679 option = Option(**option_args)
680 self.params.append(option)
682 if option.scope.is_shared:
683 shared_options.append(option)
685 for action_list in action_lists:
686 # Global options validators
687 self.global_action_callbacks.extend(
688 action_list.get("global_action_callbacks", [])
691 for action_list in action_lists:
693 for name, action in action_list.get("actions", {}).items():
694 arguments = action.pop("arguments", [])
695 options = action.pop("options", [])
697 if arguments is None:
703 self._actions[name] = Action(name=name, **action)
704 for alias in [name] + action.get("aliases", []):
705 self.commands_with_aliases[alias] = name
707 for argument_args in arguments:
708 self._actions[name].params.append(Argument(**argument_args))
710 # Add all shared options
711 for option in shared_options:
712 self._actions[name].params.append(option)
714 for option_args in options:
715 option = Option(**option_args)
717 if option.scope.is_shared:
719 '"%s" is defined for action "%s". '
720 ' "shared" options can be declared only on global level' % (option.name, name)
723 # Promote options to global if see for the first time
724 if option.scope.is_global and option.name not in [o.name for o in self.params]:
725 self.params.append(option)
727 self._actions[name].params.append(option)
729 def list_commands(self, ctx):
730 return sorted(self._actions)
732 def get_command(self, ctx, name):
733 return self._actions.get(self.commands_with_aliases.get(name))
735 def _print_closing_message(self, args, actions):
736 # print a closing message of some kind
738 if "flash" in str(actions):
742 # Otherwise, if we built any binaries print a message about
744 def print_flashing_message(title, key):
745 print("\n%s build complete. To flash, run this command:" % title)
747 with open(os.path.join(args.build_dir, "flasher_args.json")) as f:
748 flasher_args = json.load(f)
751 return _safe_relpath(os.path.join(args.build_dir, f))
753 if key != "project": # flashing a single item
757 ): # bootloader needs --flash-mode, etc to be passed in
758 cmd = " ".join(flasher_args["write_flash_args"]) + " "
760 cmd += flasher_args[key]["offset"] + " "
761 cmd += flasher_path(flasher_args[key]["file"])
762 else: # flashing the whole project
763 cmd = " ".join(flasher_args["write_flash_args"]) + " "
764 flash_items = sorted(
767 for (o, f) in flasher_args["flash_files"].items()
770 key=lambda x: int(x[0], 0),
772 for o, f in flash_items:
773 cmd += o + " " + flasher_path(f) + " "
776 "%s -p %s -b %s --after %s write_flash %s"
779 "%s/components/esptool_py/esptool/esptool.py"
780 % os.environ["IDF_PATH"]
782 args.port or "(PORT)",
784 flasher_args["extra_esptool_args"]["after"],
789 "or run 'idf.py -p %s %s'"
791 args.port or "(PORT)",
792 key + "-flash" if key != "project" else "flash",
796 if "all" in actions or "build" in actions:
797 print_flashing_message("Project", "project")
800 print_flashing_message("App", "app")
801 if "partition_table" in actions:
802 print_flashing_message("Partition Table", "partition_table")
803 if "bootloader" in actions:
804 print_flashing_message("Bootloader", "bootloader")
806 def execute_tasks(self, tasks, **kwargs):
807 ctx = click.get_current_context()
808 global_args = PropertyDict(ctx.params)
810 # Set propagated global options
812 for key in list(task.action_args):
813 option = next((o for o in ctx.command.params if o.name == key), None)
814 if option and (option.scope.is_global or option.scope.is_shared):
815 local_value = task.action_args.pop(key)
816 global_value = global_args[key]
817 default = () if option.multiple else option.default
819 if global_value != default and local_value != default and global_value != local_value:
821 'Option "%s" provided for "%s" is already defined to a different value. '
822 "This option can appear at most once in the command line." % (key, task.name)
824 if local_value != default:
825 global_args[key] = local_value
827 # Validate global arguments
828 for action_callback in ctx.command.global_action_callbacks:
829 action_callback(ctx, global_args, tasks)
831 # very simple dependency management
832 completed_tasks = set()
835 print(ctx.get_help())
840 tasks_dict = dict([(t.name, t) for t in tasks])
842 name_with_aliases = task.name
844 name_with_aliases += " (aliases: %s)" % ", ".join(task.aliases)
847 for dep in task.dependencies:
848 if dep not in completed_tasks:
850 'Adding %s\'s dependency "%s" to list of actions'
853 dep_task = ctx.invoke(ctx.command.get_command(ctx, dep))
855 # Remove global options from dependent tasks
856 for key in list(dep_task.action_args):
857 option = next((o for o in ctx.command.params if o.name == key), None)
858 if option and (option.scope.is_global or option.scope.is_shared):
859 dep_task.action_args.pop(key)
861 tasks.insert(0, dep_task)
864 for dep in task.order_dependencies:
865 if dep in tasks_dict.keys() and dep not in completed_tasks:
866 tasks.insert(0, tasks.pop(tasks.index(tasks_dict[dep])))
872 if task.name in completed_tasks:
874 "Skipping action that is already done: %s"
878 print("Executing action: %s" % name_with_aliases)
879 task.run(ctx, global_args, task.action_args)
881 completed_tasks.add(task.name)
883 self._print_closing_message(global_args, completed_tasks)
886 def merge_action_lists(*action_lists):
888 "global_options": [],
890 "global_action_callbacks": [],
892 for action_list in action_lists:
893 merged_actions["global_options"].extend(
894 action_list.get("global_options", [])
896 merged_actions["actions"].update(action_list.get("actions", {}))
897 merged_actions["global_action_callbacks"].extend(
898 action_list.get("global_action_callbacks", [])
900 return merged_actions
902 # That's a tiny parser that parse project-dir even before constructing
903 # fully featured click parser to be sure that extensions are loaded from the right place
905 add_help_option=False,
906 context_settings={"allow_extra_args": True, "ignore_unknown_options": True},
908 @click.option("-C", "--project-dir", default=os.getcwd())
909 def parse_project_dir(project_dir):
910 return _realpath(project_dir)
912 project_dir = parse_project_dir(standalone_mode=False)
914 # Load base idf commands
915 def validate_root_options(ctx, args, tasks):
916 args.project_dir = _realpath(args.project_dir)
917 if args.build_dir is not None and args.project_dir == _realpath(args.build_dir):
919 "Setting the build directory to the project directory is not supported. Suggest dropping "
920 "--build-dir option, the default is a 'build' subdirectory inside the project directory."
922 if args.build_dir is None:
923 args.build_dir = os.path.join(args.project_dir, "build")
924 args.build_dir = _realpath(args.build_dir)
926 # Possible keys for action dict are: global_options, actions and global_action_callbacks
929 "names": ["-D", "--define-cache-entry"],
930 "help": "Create a cmake cache entry.",
939 "names": ["-C", "--project-dir"],
940 "help": "Project directory.",
941 "type": click.Path(),
942 "default": os.getcwd(),
945 "names": ["-B", "--build-dir"],
946 "help": "Build directory.",
947 "type": click.Path(),
951 "names": ["-n", "--no-warnings"],
952 "help": "Disable Cmake warnings.",
957 "names": ["-v", "--verbose"],
958 "help": "Verbose build output.",
963 "names": ["--ccache"],
964 "help": "Use ccache in build",
969 # This is unused/ignored argument, as ccache use was originally opt-out.
970 # Use of ccache has been made opt-in using --cache arg.
971 "names": ["--no-ccache"],
977 "names": ["-G", "--generator"],
978 "help": "CMake generator.",
979 "type": click.Choice(GENERATOR_CMDS.keys()),
982 "global_action_callbacks": [validate_root_options],
988 "aliases": ["build"],
989 "callback": build_target,
990 "short_help": "Build the project.",
991 "help": "Build the project. This can involve multiple steps:\n\n"
992 + "1. Create the build directory if needed. The sub-directory 'build' is used to hold build output, "
993 + "although this can be changed with the -B option.\n\n"
994 + "2. Run CMake as necessary to configure the project and generate build files for the main build tool.\n\n"
995 + "3. Run the main build tool (Ninja or GNU Make). By default, the build tool is automatically detected "
996 + "but it can be explicitly set by passing the -G option to idf.py.\n\n",
997 "options": global_options,
998 "order_dependencies": [
1006 "callback": build_target,
1007 "help": 'Run "menuconfig" project configuration tool.',
1008 "options": global_options,
1011 "callback": build_target,
1012 "help": "Run JSON configuration server.",
1013 "options": global_options,
1016 "callback": build_target,
1017 "help": "Print basic size information about the app.",
1018 "options": global_options,
1019 "dependencies": ["app"],
1021 "size-components": {
1022 "callback": build_target,
1023 "help": "Print per-component size information.",
1024 "options": global_options,
1025 "dependencies": ["app"],
1028 "callback": build_target,
1029 "help": "Print per-source-file size information.",
1030 "options": global_options,
1031 "dependencies": ["app"],
1034 "callback": build_target,
1035 "help": "Build only bootloader.",
1036 "options": global_options,
1039 "callback": build_target,
1040 "help": "Build only the app.",
1041 "order_dependencies": ["clean", "fullclean", "reconfigure"],
1042 "options": global_options,
1044 "efuse_common_table": {
1045 "callback": build_target,
1046 "help": "Genereate C-source for IDF's eFuse fields.",
1047 "order_dependencies": ["reconfigure"],
1048 "options": global_options,
1050 "efuse_custom_table": {
1051 "callback": build_target,
1052 "help": "Genereate C-source for user's eFuse fields.",
1053 "order_dependencies": ["reconfigure"],
1054 "options": global_options,
1056 "show_efuse_table": {
1057 "callback": build_target,
1058 "help": "Print eFuse table.",
1059 "order_dependencies": ["reconfigure"],
1060 "options": global_options,
1062 "partition_table": {
1063 "callback": build_target,
1064 "help": "Build only partition table.",
1065 "order_dependencies": ["reconfigure"],
1066 "options": global_options,
1069 "callback": build_target,
1070 "help": "Erase otadata partition.",
1071 "options": global_options,
1074 "callback": build_target,
1075 "help": "Read otadata partition.",
1076 "options": global_options,
1084 "callback": reconfigure,
1085 "short_help": "Re-run CMake.",
1086 "help": "Re-run CMake even if it doesn't seem to need re-running. This isn't necessary during normal usage, "
1087 + "but can be useful after adding/removing files from the source tree, or when modifying CMake cache variables. "
1088 + "For example, \"idf.py -DNAME='VALUE' reconfigure\" "
1089 + 'can be used to set variable "NAME" in CMake cache to value "VALUE".',
1090 "options": global_options,
1091 "order_dependencies": ["menuconfig"],
1095 "short_help": "Delete build output files from the build directory.",
1096 "help": "Delete build output files from the build directory , forcing a 'full rebuild' the next time "
1097 + "the project is built. Cleaning doesn't delete CMake configuration output and some other files",
1098 "order_dependencies": ["fullclean"],
1101 "callback": fullclean,
1102 "short_help": "Delete the entire build directory contents.",
1103 "help": "Delete the entire build directory contents. This includes all CMake configuration output."
1104 + "The next time the project is built, CMake will configure it from scratch. "
1105 + "Note that this option recursively deletes all files in the build directory, so use with care."
1106 + "Project configuration is not deleted.",
1112 "names": ["-b", "--baud"],
1113 "help": "Baud rate.",
1115 "envvar": "ESPBAUD",
1120 "names": ["-p", "--port"],
1121 "help": "Serial port.",
1123 "envvar": "ESPPORT",
1131 "help": "Flash the project.",
1132 "options": global_options + [baud_rate, port],
1133 "dependencies": ["all"],
1134 "order_dependencies": ["erase_flash"],
1137 "callback": erase_flash,
1138 "help": "Erase entire flash chip.",
1139 "options": [baud_rate, port],
1142 "callback": monitor,
1143 "help": "Display serial output.",
1147 "names": ["--print-filter", "--print_filter"],
1149 "Filter monitor output.\n"
1150 "Restrictions on what to print can be specified as a series of <tag>:<log_level> items "
1151 "where <tag> is the tag string and <log_level> is a character from the set "
1152 "{N, E, W, I, D, V, *} referring to a level. "
1153 'For example, "tag1:W" matches and prints only the outputs written with '
1154 'ESP_LOGW("tag1", ...) or at lower verbosity level, i.e. ESP_LOGE("tag1", ...). '
1155 'Not specifying a <log_level> or using "*" defaults to Verbose level.\n'
1156 'Please see the IDF Monitor section of the ESP-IDF documentation '
1157 'for a more detailed description and further examples.'),
1161 "order_dependencies": [
1163 "partition_table-flash",
1168 "partition_table-flash": {
1170 "help": "Flash partition table only.",
1171 "options": [baud_rate, port],
1172 "dependencies": ["partition_table"],
1173 "order_dependencies": ["erase_flash"],
1175 "bootloader-flash": {
1177 "help": "Flash bootloader only.",
1178 "options": [baud_rate, port],
1179 "dependencies": ["bootloader"],
1180 "order_dependencies": ["erase_flash"],
1184 "help": "Flash the app only.",
1185 "options": [baud_rate, port],
1186 "dependencies": ["app"],
1187 "order_dependencies": ["erase_flash"],
1189 "encrypted-app-flash": {
1191 "help": "Flash the encrypted app only.",
1192 "dependencies": ["app"],
1193 "order_dependencies": ["erase_flash"],
1195 "encrypted-flash": {
1197 "help": "Flash the encrypted project.",
1198 "dependencies": ["all"],
1199 "order_dependencies": ["erase_flash"],
1204 base_actions = CLI.merge_action_lists(
1205 root_options, build_actions, clean_actions, serial_actions
1207 all_actions = [base_actions]
1210 if os.path.exists(os.path.join(project_dir, "idf_ext.py")):
1211 sys.path.append(project_dir)
1213 from idf_ext import action_extensions
1215 print("Error importing extension file idf_ext.py. Skipping.")
1217 "Please make sure that it contains implementation (even if it's empty) of add_action_extensions"
1220 # Add actions extensions
1222 all_actions.append(action_extensions(base_actions, project_dir))
1226 return CLI(help="ESP-IDF build management", action_lists=all_actions)
1235 def _valid_unicode_config():
1236 # Python 2 is always good
1237 if sys.version_info[0] == 2:
1240 # With python 3 unicode environment is required
1242 return codecs.lookup(locale.getpreferredencoding()).name != "ascii"
1247 def _find_usable_locale():
1249 locales = subprocess.Popen(
1250 ["locale", "-a"], stdout=subprocess.PIPE, stderr=subprocess.PIPE
1254 if isinstance(locales, bytes):
1255 locales = locales.decode("ascii", "replace")
1258 for line in locales.splitlines():
1259 locale = line.strip()
1260 locale_name = locale.lower().replace("-", "")
1262 # C.UTF-8 is the best option, if supported
1263 if locale_name == "c.utf8":
1266 if locale_name.endswith(".utf8"):
1267 # Make a preference of english locales
1268 if locale.startswith("en_"):
1269 usable_locales.insert(0, locale)
1271 usable_locales.append(locale)
1273 if not usable_locales:
1275 "Support for Unicode filenames is required, but no suitable UTF-8 locale was found on your system."
1276 " Please refer to the manual for your operating system for details on locale reconfiguration."
1279 return usable_locales[0]
1282 if __name__ == "__main__":
1284 # On MSYS2 we need to run idf.py with "winpty" in order to be able to cancel the subprocesses properly on
1285 # keyboard interrupt (CTRL+C).
1286 # Using an own global variable for indicating that we are running with "winpty" seems to be the most suitable
1287 # option as os.environment['_'] contains "winpty" only when it is run manually from console.
1288 WINPTY_VAR = "WINPTY"
1289 WINPTY_EXE = "winpty"
1290 if ("MSYSTEM" in os.environ) and (
1291 not os.environ.get("_", "").endswith(WINPTY_EXE) and WINPTY_VAR not in os.environ
1293 os.environ[WINPTY_VAR] = "1" # the value is of no interest to us
1294 # idf.py calls itself with "winpty" and WINPTY global variable set
1295 ret = subprocess.call(
1296 [WINPTY_EXE, sys.executable] + sys.argv, env=os.environ
1299 raise SystemExit(ret)
1301 elif os.name == "posix" and not _valid_unicode_config():
1302 # Trying to find best utf-8 locale available on the system and restart python with it
1303 best_locale = _find_usable_locale()
1306 "Your environment is not configured to handle unicode filenames outside of ASCII range."
1307 " Environment variable LC_ALL is temporary set to %s for unicode support."
1311 os.environ["LC_ALL"] = best_locale
1312 ret = subprocess.call([sys.executable] + sys.argv, env=os.environ)
1314 raise SystemExit(ret)
1319 except FatalError as e: