]> granicus.if.org Git - esp-idf/blob - tools/idf.py
Merge branch 'master' into feature/cmake
[esp-idf] / tools / idf.py
1 #!/usr/bin/env python
2 #
3 # 'idf.py' is a top-level config/build command line tool for ESP-IDF
4 #
5 # You don't have to use idf.py, you can use cmake directly
6 # (or use cmake in an IDE)
7 #
8 #
9 #
10 # Copyright 2018 Espressif Systems (Shanghai) PTE LTD
11 #
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
15 #
16 #     http://www.apache.org/licenses/LICENSE-2.0
17 #
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.
23 #
24
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.
29 import sys
30 import argparse
31 import os
32 import os.path
33 import subprocess
34 import multiprocessing
35 import re
36 import shutil
37 import json
38
39 class FatalError(RuntimeError):
40     """
41     Wrapper class for runtime errors that aren't caused by bugs in idf.py or the build proces.s
42     """
43     pass
44
45 # Use this Python interpreter for any subprocesses we launch
46 PYTHON=sys.executable
47
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
51
52 # Make flavors, across the various kinds of Windows environments & POSIX...
53 if "MSYSTEM" in os.environ:  # MSYS
54     MAKE_CMD = "make"
55     MAKE_GENERATOR = "MSYS Makefiles"
56 elif os.name == 'nt':  # other Windows
57     MAKE_CMD = "mingw32-make"
58     MAKE_GENERATOR = "MinGW Makefiles"
59 else:
60     MAKE_CMD = "make"
61     MAKE_GENERATOR = "Unix Makefiles"
62
63 GENERATORS = [
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"),
67     ]
68 GENERATOR_CMDS = dict( (a[0], a[1]) for a in GENERATORS )
69 GENERATOR_VERBOSE = dict( (a[0], a[3]) for a in GENERATORS )
70
71 def _run_tool(tool_name, args, cwd):
72     def quote_arg(arg):
73         " Quote 'arg' if necessary "
74         if " " in arg and not (arg.startswith('"') or arg.startswith("'")):
75             return "'" + arg + "'"
76         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)
80     try:
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))
85
86
87 def check_environment():
88     """
89     Verify the environment contains the top-level tools we need to operate
90
91     (cmake will check a lot of other things)
92     """
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))
102     else:
103         print("Setting IDF_PATH environment variable: %s" % detected_idf_path)
104         os.environ["IDF_PATH"] = detected_idf_path
105
106     # check Python dependencies
107     print("Checking Python dependencies...")
108     try:
109         subprocess.check_call([ os.environ["PYTHON"],
110                                 os.path.join(os.environ["IDF_PATH"], "tools", "check_python_dependencies.py")],
111                               env=os.environ)
112     except subprocess.CalledProcessError:
113         raise SystemExit(1)
114
115 def executable_exists(args):
116     try:
117         subprocess.check_output(args)
118         return True
119     except:
120         return False
121
122 def detect_cmake_generator():
123     """
124     Find the default cmake generator, if none was specified. Raises an exception if no valid generator is found.
125     """
126     for (generator, _, version_check, _) in GENERATORS:
127         if executable_exists(version_check):
128             return generator
129     raise FatalError("To use idf.py, either the 'ninja' or 'GNU make' build tool must be available in the PATH")
130
131 def _ensure_build_directory(args, always_run_cmake=False):
132     """Check the build directory exists and that cmake has been run there.
133
134     If this isn't the case, create the build directory (if necessary) and
135     do an initial cmake run to configure it.
136
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.
140     """
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")
146         else:
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)
150
151     # Verify/create the build directory
152     build_dir = args.build_dir
153     if not os.path.isdir(build_dir):
154         os.mkdir(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()
159         try:
160             cmake_args = ["cmake", "-G", args.generator, "-DPYTHON_DEPS_CHECKED=1"]
161             if not args.no_warnings:
162                 cmake_args += [ "--warn-uninitialized" ]
163             if args.no_ccache:
164                 cmake_args += [ "-DCCACHE_DISABLE=1" ]
165             cmake_args += [ project_dir]
166             _run_tool("cmake", cmake_args, cwd=args.build_dir)
167         except:
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)
172             raise
173
174     # Learn some things from the CMakeCache.txt file in the build directory
175     cache = parse_cmakecache(cache_path)
176     try:
177         generator = cache["CMAKE_GENERATOR"]
178     except KeyError:
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))
185
186     try:
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)))
191     except KeyError:
192         pass  # if cmake failed part way, CMAKE_HOME_DIRECTORY may not be set yet
193
194
195 def parse_cmakecache(path):
196     """
197     Parse the CMakeCache file at 'path'.
198
199     Returns a dict of name:value.
200
201     CMakeCache entries also each have a "type", but this is currently ignored.
202     """
203     result = {}
204     with open(path) as f:
205         for line in 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)
209             if m:
210                result[m.group(1)] = m.group(3)
211     return result
212
213 def build_target(target_name, args):
214     """
215     Execute the target build system to build target 'target_name'
216
217     Calls _ensure_build_directory() which will run cmake to generate a build
218     directory (with the specified generator) as needed.
219     """
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.)
226         #
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"
230         pass
231     if args.verbose:
232         generator_cmd += [ GENERATOR_VERBOSE[args.generator] ]
233
234     _run_tool(generator_cmd[0], generator_cmd + [target_name], args.build_dir)
235
236
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) ]
243     return result
244
245 def flash(action, args):
246     """
247     Run esptool to flash the entire project, from an argfile generated by the build system
248     """
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",
254     }[action]
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)
258
259
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)
264
265
266 def monitor(action, args):
267     """
268     Run idf_monitor.py to watch build output
269     """
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)
275
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 ]
285
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) ]
288
289     if "MSYSTEM" is os.environ:
290         monitor_args = [ "winpty" ] + monitor_args
291     _run_tool("idf_monitor", monitor_args, args.project_dir)
292
293
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)
297         return
298     build_target("clean", args)
299
300 def reconfigure(action, args):
301     _ensure_build_directory(args, True)
302
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)
307         return
308     if len(os.listdir(build_dir)) == 0:
309         print("Build directory '%s' is empty. Nothing to clean." % build_dir)
310         return
311
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)
322         if os.path.isdir(f):
323             shutil.rmtree(f)
324         else:
325             os.remove(f)
326
327 def print_closing_message(args):
328     # print a closing message of some kind
329     #
330
331     if "flash" in str(args.actions):
332         print("Done")
333         return
334
335     # Otherwise, if we built any binaries print a message about
336     # how to flash them
337     def print_flashing_message(title, key):
338         print("\n%s build complete. To flash, run this command:" % title)
339
340         with open(os.path.join(args.build_dir, "flasher_args.json")) as f:
341             flasher_args = json.load(f)
342
343         def flasher_path(f):
344             return os.path.relpath(os.path.join(args.build_dir, f))
345
346         if key != "project":  # flashing a single item
347             cmd = ""
348             if key == "bootloader":  # bootloader needs --flash-mode, etc to be passed in
349                 cmd = " ".join(flasher_args["write_flash_args"]) + " "
350
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) + " "
359
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)",
363             args.baud,
364             cmd.strip()))
365         print("or run 'idf.py -p %s %s'" % (args.port or "(PORT)", key + "-flash" if key != "project" else "flash",))
366
367     if "all" in args.actions or "build" in args.actions:
368         print_flashing_message("Project", "project")
369     else:
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")
376
377 ACTIONS = {
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" ]),
399 }
400
401
402 def get_commandline_options():
403     """ Return all the command line options up to but not including the action """
404     result = []
405     for a in sys.argv:
406         if a in ACTIONS.keys():
407             break
408         else:
409             result.append(a)
410     return result
411
412 def main():
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])
415
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())
429
430     args = parser.parse_args()
431
432     check_environment()
433
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)
440
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)
452
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)
457         else:
458             function(action, args)
459
460         completed_actions.add(action)
461
462     actions = list(args.actions)
463     while len(actions) > 0:
464         execute_action(actions[0], actions[1:])
465         actions.pop(0)
466
467     print_closing_message(args)
468
469 if __name__ == "__main__":
470     try:
471         main()
472     except FatalError as e:
473         print(e)
474         sys.exit(2)
475
476