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 env=os.environ explicitly anywhere that we create a process
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 print("Setting IDF_PATH environment variable: %s" % detected_idf_path)
99 os.environ["IDF_PATH"] = detected_idf_path
101 def executable_exists(args):
103 subprocess.check_output(args)
108 def detect_cmake_generator():
110 Find the default cmake generator, if none was specified. Raises an exception if no valid generator is found.
112 for (generator, _, version_check, _) in GENERATORS:
113 if executable_exists(version_check):
115 raise FatalError("To use idf.py, either the 'ninja' or 'GNU make' build tool must be available in the PATH")
117 def _ensure_build_directory(args, always_run_cmake=False):
118 """Check the build directory exists and that cmake has been run there.
120 If this isn't the case, create the build directory (if necessary) and
121 do an initial cmake run to configure it.
123 This function will also check args.generator parameter. If the parameter is incompatible with
124 the build directory, an error is raised. If the parameter is None, this function will set it to
125 an auto-detected default generator or to the value already configured in the build directory.
127 project_dir = args.project_dir
128 # Verify the project directory
129 if not os.path.isdir(project_dir):
130 if not os.path.exists(project_dir):
131 raise FatalError("Project directory %s does not exist")
133 raise FatalError("%s must be a project directory")
134 if not os.path.exists(os.path.join(project_dir, "CMakeLists.txt")):
135 raise FatalError("CMakeLists.txt not found in project directory %s" % project_dir)
137 # Verify/create the build directory
138 build_dir = args.build_dir
139 if not os.path.isdir(build_dir):
141 cache_path = os.path.join(build_dir, "CMakeCache.txt")
142 if not os.path.exists(cache_path) or always_run_cmake:
143 if args.generator is None:
144 args.generator = detect_cmake_generator()
146 cmake_args = ["cmake", "-G", args.generator]
147 if not args.no_warnings:
148 cmake_args += [ "--warn-uninitialized" ]
150 cmake_args += [ "-DCCACHE_DISABLE=1" ]
151 cmake_args += [ project_dir]
152 _run_tool("cmake", cmake_args, cwd=args.build_dir)
154 # don't allow partially valid CMakeCache.txt files,
155 # to keep the "should I run cmake?" logic simple
156 if os.path.exists(cache_path):
157 os.remove(cache_path)
160 # Learn some things from the CMakeCache.txt file in the build directory
161 cache = parse_cmakecache(cache_path)
163 generator = cache["CMAKE_GENERATOR"]
165 generator = detect_cmake_generator()
166 if args.generator is None:
167 args.generator = generator # reuse the previously configured generator, if none was given
168 if generator != args.generator:
169 raise FatalError("Build is configured for generator '%s' not '%s'. Run 'idf.py fullclean' to start again."
170 % (generator, args.generator))
173 home_dir = cache["CMAKE_HOME_DIRECTORY"]
174 if os.path.normcase(os.path.realpath(home_dir)) != os.path.normcase(os.path.realpath(project_dir)):
175 raise FatalError("Build directory '%s' configured for project '%s' not '%s'. Run 'idf.py fullclean' to start again."
176 % (build_dir, os.path.realpath(home_dir), os.path.realpath(project_dir)))
178 pass # if cmake failed part way, CMAKE_HOME_DIRECTORY may not be set yet
181 def parse_cmakecache(path):
183 Parse the CMakeCache file at 'path'.
185 Returns a dict of name:value.
187 CMakeCache entries also each have a "type", but this is currently ignored.
190 with open(path) as f:
192 # cmake cache lines look like: CMAKE_CXX_FLAGS_DEBUG:STRING=-g
193 # groups are name, type, value
194 m = re.match(r"^([^#/:=]+):([^:=]+)=(.+)\n$", line)
196 result[m.group(1)] = m.group(3)
199 def build_target(target_name, args):
201 Execute the target build system to build target 'target_name'
203 Calls _ensure_build_directory() which will run cmake to generate a build
204 directory (with the specified generator) as needed.
206 _ensure_build_directory(args)
207 generator_cmd = GENERATOR_CMDS[args.generator]
208 if not args.no_ccache:
209 # Setting CCACHE_BASEDIR & CCACHE_NO_HASHDIR ensures that project paths aren't stored in the ccache entries
210 # (this means ccache hits can be shared between different projects. It may mean that some debug information
211 # will point to files in another project, if these files are perfect duplicates of each other.)
213 # It would be nicer to set these from cmake, but there's no cross-platform way to set build-time environment
214 #os.environ["CCACHE_BASEDIR"] = args.build_dir
215 #os.environ["CCACHE_NO_HASHDIR"] = "1"
218 generator_cmd += [ GENERATOR_VERBOSE[args.generator] ]
220 _run_tool(generator_cmd[0], generator_cmd + [target_name], args.build_dir)
223 def _get_esptool_args(args):
224 esptool_path = os.path.join(os.environ["IDF_PATH"], "components/esptool_py/esptool/esptool.py")
225 result = [ PYTHON, esptool_path ]
226 if args.port is not None:
227 result += [ "-p", args.port ]
228 result += [ "-b", str(args.baud) ]
231 def flash(action, args):
233 Run esptool to flash the entire project, from an argfile generated by the build system
235 flasher_args_path = { # action -> name of flasher args file generated by build system
236 "bootloader-flash": "flash_bootloader_args",
237 "partition_table-flash": "flash_partition_table_args",
238 "app-flash": "flash_app_args",
239 "flash": "flash_project_args",
241 esptool_args = _get_esptool_args(args)
242 esptool_args += [ "write_flash", "@"+flasher_args_path ]
243 _run_tool("esptool.py", esptool_args, args.build_dir)
246 def erase_flash(action, args):
247 esptool_args = _get_esptool_args(args)
248 esptool_args += [ "erase_flash" ]
249 _run_tool("esptool.py", esptool_args, args.build_dir)
252 def monitor(action, args):
254 Run idf_monitor.py to watch build output
256 desc_path = os.path.join(args.build_dir, "project_description.json")
257 if not os.path.exists(desc_path):
258 _ensure_build_directory(args)
259 with open(desc_path, "r") as f:
260 project_desc = json.load(f)
262 elf_file = os.path.join(args.build_dir, project_desc["app_elf"])
263 if not os.path.exists(elf_file):
264 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)
265 idf_monitor = os.path.join(os.environ["IDF_PATH"], "tools/idf_monitor.py")
266 monitor_args = [PYTHON, idf_monitor ]
267 if args.port is not None:
268 monitor_args += [ "-p", args.port ]
269 monitor_args += [ "-b", project_desc["monitor_baud"] ]
270 monitor_args += [ elf_file ]
272 idf_py = [ PYTHON ] + get_commandline_options() # commands to re-run idf.py
273 monitor_args += [ "-m", " ".join("'%s'" % a for a in idf_py) ]
275 if "MSYSTEM" is os.environ:
276 monitor_args = [ "winpty" ] + monitor_args
277 _run_tool("idf_monitor", monitor_args, args.project_dir)
280 def clean(action, args):
281 if not os.path.isdir(args.build_dir):
282 print("Build directory '%s' not found. Nothing to clean." % args.build_dir)
284 build_target("clean", args)
286 def reconfigure(action, args):
287 _ensure_build_directory(args, True)
289 def fullclean(action, args):
290 build_dir = args.build_dir
291 if not os.path.isdir(build_dir):
292 print("Build directory '%s' not found. Nothing to clean." % build_dir)
294 if len(os.listdir(build_dir)) == 0:
295 print("Build directory '%s' is empty. Nothing to clean." % build_dir)
298 if not os.path.exists(os.path.join(build_dir, "CMakeCache.txt")):
299 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)
300 red_flags = [ "CMakeLists.txt", ".git", ".svn" ]
301 for red in red_flags:
302 red = os.path.join(build_dir, red)
303 if os.path.exists(red):
304 raise FatalError("Refusing to automatically delete files in directory containing '%s'. Delete files manually if you're sure." % red)
305 # OK, delete everything in the build directory...
306 for f in os.listdir(build_dir): # TODO: once we are Python 3 only, this can be os.scandir()
307 f = os.path.join(build_dir, f)
313 def print_closing_message(args):
314 # print a closing message of some kind
317 if "flash" in str(args.actions):
321 # Otherwise, if we built any binaries print a message about
323 def print_flashing_message(title, key):
324 print("\n%s build complete. To flash, run this command:" % title)
326 with open(os.path.join(args.build_dir, "flasher_args.json")) as f:
327 flasher_args = json.load(f)
330 return os.path.relpath(os.path.join(args.build_dir, f))
334 if key == "bootloader":
335 cmd = " ".join(flasher_args["write_flash_args"]) + " "
337 cmd += flasher_args[key]["offset"] + " "
338 cmd += flasher_path(flasher_args[key]["file"])
340 cmd = " ".join(flasher_args["write_flash_args"]) + " "
341 for o,f in flasher_args["flash_files"].items():
342 cmd += o + " " + flasher_path(f) + " "
344 print("%s -p %s -b %s write_flash %s" % (
345 os.path.relpath("%s/components/esptool_py/esptool/esptool.py" % os.environ["IDF_PATH"]),
346 args.port or "(PORT)",
349 print("or run 'idf.py %s'" % (key + "-flash" if key != "project" else "flash",))
351 if "all" in args.actions or "build" in args.actions:
352 print_flashing_message("Project", "project")
354 if "app" in args.actions:
355 print_flashing_message("App", "app")
356 if "partition_table" in args.actions:
357 print_flashing_message("Partition Table", "partition_table")
358 if "bootloader" in args.actions:
359 print_flashing_message("Bootloader", "bootloader")
362 # action name : ( function (or alias), dependencies, order-only dependencies )
363 "all" : ( build_target, [], [ "reconfigure", "menuconfig", "clean", "fullclean" ] ),
364 "build": ( "all", [], [] ), # build is same as 'all' target
365 "clean": ( clean, [], [ "fullclean" ] ),
366 "fullclean": ( fullclean, [], [] ),
367 "reconfigure": ( reconfigure, [], [ "menuconfig" ] ),
368 "menuconfig": ( build_target, [], [] ),
369 "confserver": ( build_target, [], [] ),
370 "size": ( build_target, [ "app" ], [] ),
371 "size-components": ( build_target, [ "app" ], [] ),
372 "size-files": ( build_target, [ "app" ], [] ),
373 "bootloader": ( build_target, [], [] ),
374 "bootloader-clean": ( build_target, [], [] ),
375 "bootloader-flash": ( flash, [ "bootloader" ], [ "erase_flash"] ),
376 "app": ( build_target, [], [ "clean", "fullclean", "reconfigure" ] ),
377 "app-flash": ( flash, [ "app" ], [ "erase_flash"]),
378 "partition_table": ( build_target, [], [ "reconfigure" ] ),
379 "partition_table-flash": ( flash, [ "partition_table" ], [ "erase_flash" ]),
380 "flash": ( flash, [ "all" ], [ "erase_flash" ] ),
381 "erase_flash": ( erase_flash, [], []),
382 "monitor": ( monitor, [], [ "flash", "partition_table-flash", "bootloader-flash", "app-flash" ]),
386 def get_commandline_options():
387 """ Return all the command line options up to but not including the action """
390 if a in ACTIONS.keys():
397 if sys.version_info[0] != 2 or sys.version_info[1] != 7:
398 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])
400 parser = argparse.ArgumentParser(description='ESP-IDF build management tool')
401 parser.add_argument('-p', '--port', help="Serial port",
402 default=os.environ.get('ESPPORT', None))
403 parser.add_argument('-b', '--baud', help="Baud rate",
404 default=os.environ.get('ESPBAUD', 460800))
405 parser.add_argument('-C', '--project-dir', help="Project directory", default=os.getcwd())
406 parser.add_argument('-B', '--build-dir', help="Build directory", default=None)
407 parser.add_argument('-G', '--generator', help="Cmake generator", choices=GENERATOR_CMDS.keys())
408 parser.add_argument('-n', '--no-warnings', help="Disable Cmake warnings", action="store_true")
409 parser.add_argument('-v', '--verbose', help="Verbose build output", action="store_true")
410 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")
411 parser.add_argument('actions', help="Actions (build targets or other operations)", nargs='+',
412 choices=ACTIONS.keys())
414 args = parser.parse_args()
418 # Advanced parameter checks
419 if args.build_dir is not None and os.path.realpath(args.project_dir) == os.path.realpath(args.build_dir):
420 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.")
421 if args.build_dir is None:
422 args.build_dir = os.path.join(args.project_dir, "build")
423 args.build_dir = os.path.realpath(args.build_dir)
425 completed_actions = set()
426 def execute_action(action, remaining_actions):
427 ( function, dependencies, order_dependencies ) = ACTIONS[action]
428 # very simple dependency management, build a set of completed actions and make sure
429 # all dependencies are in it
430 for dep in dependencies:
431 if not dep in completed_actions:
432 execute_action(dep, remaining_actions)
433 for dep in order_dependencies:
434 if dep in remaining_actions and not dep in completed_actions:
435 execute_action(dep, remaining_actions)
437 if action in completed_actions:
438 pass # we've already done this, don't do it twice...
439 elif function in ACTIONS: # alias of another action
440 execute_action(function, remaining_actions)
442 function(action, args)
444 completed_actions.add(action)
446 actions = list(args.actions)
447 while len(actions) > 0:
448 execute_action(actions[0], actions[1:])
451 print_closing_message(args)
453 if __name__ == "__main__":
456 except FatalError as e: