]> granicus.if.org Git - esp-idf/blob - tools/idf.py
Merge branch 'bugfix/cmake_fixes' 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 import sys
25 import argparse
26 import os
27 import os.path
28 import subprocess
29 import multiprocessing
30 import re
31 import shutil
32 import json
33
34 class FatalError(RuntimeError):
35     """
36     Wrapper class for runtime errors that aren't caused by bugs in idf.py or the build proces.s
37     """
38     pass
39
40 # Use this Python interpreter for any subprocesses we launch
41 PYTHON=sys.executable
42
43 # note: os.environ changes don't automatically propagate to child processes,
44 # you have to pass this in explicitly
45 os.environ["PYTHON"]=sys.executable
46
47 # Make flavors, across the various kinds of Windows environments & POSIX...
48 if "MSYSTEM" in os.environ:  # MSYS
49     MAKE_CMD = "make"
50     MAKE_GENERATOR = "MSYS Makefiles"
51 elif os.name == 'nt':  # other Windows
52     MAKE_CMD = "mingw32-make"
53     MAKE_GENERATOR = "MinGW Makefiles"
54 else:
55     MAKE_CMD = "make"
56     MAKE_GENERATOR = "Unix Makefiles"
57
58 GENERATORS = [
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"),
62     ]
63 GENERATOR_CMDS = dict( (a[0], a[1]) for a in GENERATORS )
64 GENERATOR_VERBOSE = dict( (a[0], a[3]) for a in GENERATORS )
65
66 def _run_tool(tool_name, args, cwd):
67     def quote_arg(arg):
68         " Quote 'arg' if necessary "
69         if " " in arg and not (arg.startswith('"') or arg.startswith("'")):
70             return "'" + arg + "'"
71         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)
75     try:
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))
80
81
82 def check_environment():
83     """
84     Verify the environment contains the top-level tools we need to operate
85
86     (cmake will check a lot of other things)
87     """
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))
97     else:
98         os.environ["IDF_PATH"] = detected_idf_path
99
100 def executable_exists(args):
101     try:
102         subprocess.check_output(args)
103         return True
104     except:
105         return False
106
107 def detect_cmake_generator():
108     """
109     Find the default cmake generator, if none was specified. Raises an exception if no valid generator is found.
110     """
111     for (generator, _, version_check, _) in GENERATORS:
112         if executable_exists(version_check):
113             return generator
114     raise FatalError("To use idf.py, either the 'ninja' or 'GNU make' build tool must be available in the PATH")
115
116 def _ensure_build_directory(args, always_run_cmake=False):
117     """Check the build directory exists and that cmake has been run there.
118
119     If this isn't the case, create the build directory (if necessary) and
120     do an initial cmake run to configure it.
121
122     This function will also check args.generator parameter. If the parameter is incompatible with
123     the build directory, an error is raised. If the parameter is None, this function will set it to
124     an auto-detected default generator or to the value already configured in the build directory.
125     """
126     project_dir = args.project_dir
127     # Verify the project directory
128     if not os.path.isdir(project_dir):
129         if not os.path.exists(project_dir):
130             raise FatalError("Project directory %s does not exist")
131         else:
132             raise FatalError("%s must be a project directory")
133     if not os.path.exists(os.path.join(project_dir, "CMakeLists.txt")):
134         raise FatalError("CMakeLists.txt not found in project directory %s" % project_dir)
135
136     # Verify/create the build directory
137     build_dir = args.build_dir
138     if not os.path.isdir(build_dir):
139         os.mkdir(build_dir)
140     cache_path = os.path.join(build_dir, "CMakeCache.txt")
141     if not os.path.exists(cache_path) or always_run_cmake:
142         if args.generator is None:
143             args.generator = detect_cmake_generator()
144         try:
145             cmake_args = ["cmake", "-G", args.generator]
146             if not args.no_warnings:
147                 cmake_args += [ "--warn-uninitialized" ]
148             if args.no_ccache:
149                 cmake_args += [ "-DCCACHE_DISABLE=1" ]
150             cmake_args += [ project_dir]
151             _run_tool("cmake", cmake_args, cwd=args.build_dir)
152         except:
153             # don't allow partially valid CMakeCache.txt files,
154             # to keep the "should I run cmake?" logic simple
155             if os.path.exists(cache_path):
156                 os.remove(cache_path)
157             raise
158
159     # Learn some things from the CMakeCache.txt file in the build directory
160     cache = parse_cmakecache(cache_path)
161     try:
162         generator = cache["CMAKE_GENERATOR"]
163     except KeyError:
164         generator = detect_cmake_generator()
165     if args.generator is None:
166         args.generator = generator  # reuse the previously configured generator, if none was given
167     if generator != args.generator:
168         raise FatalError("Build is configured for generator '%s' not '%s'. Run 'idf.py fullclean' to start again."
169                            % (generator, args.generator))
170
171     try:
172         home_dir = cache["CMAKE_HOME_DIRECTORY"]
173         if os.path.normcase(os.path.realpath(home_dir)) != os.path.normcase(os.path.realpath(project_dir)):
174             raise FatalError("Build directory '%s' configured for project '%s' not '%s'. Run 'idf.py fullclean' to start again."
175                             % (build_dir, os.path.realpath(home_dir), os.path.realpath(project_dir)))
176     except KeyError:
177         pass  # if cmake failed part way, CMAKE_HOME_DIRECTORY may not be set yet
178
179
180 def parse_cmakecache(path):
181     """
182     Parse the CMakeCache file at 'path'.
183
184     Returns a dict of name:value.
185
186     CMakeCache entries also each have a "type", but this is currently ignored.
187     """
188     result = {}
189     with open(path) as f:
190         for line in f:
191             # cmake cache lines look like: CMAKE_CXX_FLAGS_DEBUG:STRING=-g
192             # groups are name, type, value
193             m = re.match(r"^([^#/:=]+):([^:=]+)=(.+)\n$", line)
194             if m:
195                result[m.group(1)] = m.group(3)
196     return result
197
198 def build_target(target_name, args):
199     """
200     Execute the target build system to build target 'target_name'
201
202     Calls _ensure_build_directory() which will run cmake to generate a build
203     directory (with the specified generator) as needed.
204     """
205     _ensure_build_directory(args)
206     generator_cmd = GENERATOR_CMDS[args.generator]
207     if not args.no_ccache:
208         # Setting CCACHE_BASEDIR & CCACHE_NO_HASHDIR ensures that project paths aren't stored in the ccache entries
209         # (this means ccache hits can be shared between different projects. It may mean that some debug information
210         # will point to files in another project, if these files are perfect duplicates of each other.)
211         #
212         # It would be nicer to set these from cmake, but there's no cross-platform way to set build-time environment
213         #os.environ["CCACHE_BASEDIR"] = args.build_dir
214         #os.environ["CCACHE_NO_HASHDIR"] = "1"
215         pass
216     if args.verbose:
217         generator_cmd += [ GENERATOR_VERBOSE[args.generator] ]
218
219     _run_tool(generator_cmd[0], generator_cmd + [target_name], args.build_dir)
220
221
222 def _get_esptool_args(args):
223     esptool_path = os.path.join(os.environ["IDF_PATH"], "components/esptool_py/esptool/esptool.py")
224     result = [ PYTHON, esptool_path ]
225     if args.port is not None:
226         result += [ "-p", args.port ]
227     result += [ "-b", str(args.baud) ]
228     return result
229
230 def flash(action, args):
231     """
232     Run esptool to flash the entire project, from an argfile generated by the build system
233     """
234     flasher_args_path = {  # action -> name of flasher args file generated by build system
235         "bootloader-flash":      "flash_bootloader_args",
236         "partition_table-flash": "flash_partition_table_args",
237         "app-flash":             "flash_app_args",
238         "flash":                 "flash_project_args",
239     }[action]
240     esptool_args = _get_esptool_args(args)
241     esptool_args += [ "write_flash", "@"+flasher_args_path ]
242     _run_tool("esptool.py", esptool_args, args.build_dir)
243
244
245 def erase_flash(action, args):
246     esptool_args = _get_esptool_args(args)
247     esptool_args += [ "erase_flash" ]
248     _run_tool("esptool.py", esptool_args, args.build_dir)
249
250
251 def monitor(action, args):
252     """
253     Run idf_monitor.py to watch build output
254     """
255     desc_path = os.path.join(args.build_dir, "project_description.json")
256     if not os.path.exists(desc_path):
257         _ensure_build_directory(args)
258     with open(desc_path, "r") as f:
259         project_desc = json.load(f)
260
261     elf_file = os.path.join(args.build_dir, project_desc["app_elf"])
262     if not os.path.exists(elf_file):
263         raise FatalError("ELF file '%s' not found. You need to build & flash the project before running 'monitor', and the binary on the device must match the one in the build directory exactly. Try 'idf.py flash monitor'." % elf_file)
264     idf_monitor = os.path.join(os.environ["IDF_PATH"], "tools/idf_monitor.py")
265     monitor_args = [PYTHON, idf_monitor ]
266     if args.port is not None:
267         monitor_args += [ "-p", args.port ]
268     monitor_args += [ "-b", project_desc["monitor_baud"] ]
269     monitor_args += [ elf_file ]
270
271     idf_py = [ PYTHON ] + get_commandline_options()  # commands to re-run idf.py
272     monitor_args += [ "-m", " ".join("'%s'" % a for a in idf_py) ]
273
274     if "MSYSTEM" is os.environ:
275         monitor_args = [ "winpty" ] + monitor_args
276     _run_tool("idf_monitor", monitor_args, args.project_dir)
277
278
279 def clean(action, args):
280     if not os.path.isdir(args.build_dir):
281         print("Build directory '%s' not found. Nothing to clean." % args.build_dir)
282         return
283     build_target("clean", args)
284
285 def reconfigure(action, args):
286     _ensure_build_directory(args, True)
287
288 def fullclean(action, args):
289     build_dir = args.build_dir
290     if not os.path.isdir(build_dir):
291         print("Build directory '%s' not found. Nothing to clean." % build_dir)
292         return
293     if len(os.listdir(build_dir)) == 0:
294         print("Build directory '%s' is empty. Nothing to clean." % build_dir)
295         return
296
297     if not os.path.exists(os.path.join(build_dir, "CMakeCache.txt")):
298         raise FatalError("Directory '%s' doesn't seem to be a CMake build directory. Refusing to automatically delete files in this directory. Delete the directory manually to 'clean' it." % build_dir)
299     red_flags = [ "CMakeLists.txt", ".git", ".svn" ]
300     for red in red_flags:
301         red = os.path.join(build_dir, red)
302         if os.path.exists(red):
303             raise FatalError("Refusing to automatically delete files in directory containing '%s'. Delete files manually if you're sure." % red)
304     # OK, delete everything in the build directory...
305     for f in os.listdir(build_dir):  # TODO: once we are Python 3 only, this can be os.scandir()
306         f = os.path.join(build_dir, f)
307         if os.path.isdir(f):
308             shutil.rmtree(f)
309         else:
310             os.remove(f)
311
312 def print_closing_message(args):
313     # print a closing message of some kind
314     #
315
316     if "flash" in str(args.actions):
317         print("Done")
318         return
319
320     # Otherwise, if we built any binaries print a message about
321     # how to flash them
322     def print_flashing_message(title, key):
323         print("\n%s build complete. To flash, run this command:" % title)
324
325         with open(os.path.join(args.build_dir, "flasher_args.json")) as f:
326             flasher_args = json.load(f)
327
328         def flasher_path(f):
329             return os.path.relpath(os.path.join(args.build_dir, f))
330
331         if key != "project":
332             cmd = ""
333             if key == "bootloader":
334                 cmd = " ".join(flasher_args["write_flash_args"]) + " "
335
336             cmd += flasher_args[key]["offset"] + " "
337             cmd += flasher_path(flasher_args[key]["file"])
338         else:
339             cmd = " ".join(flasher_args["write_flash_args"]) + " "
340             for o,f in flasher_args["flash_files"].items():
341                 cmd += o + " " + flasher_path(f) + " "
342
343         print("%s -p %s -b %s write_flash %s" % (
344             os.path.relpath("%s/components/esptool_py/esptool/esptool.py" % os.environ["IDF_PATH"]),
345             args.port or "(PORT)",
346             args.baud,
347             cmd.strip()))
348         print("or run 'idf.py %s'" % (key + "-flash" if key != "project" else "flash",))
349
350     if "all" in args.actions or "build" in args.actions:
351         print_flashing_message("Project", "project")
352     else:
353         if "app" in args.actions:
354             print_flashing_message("App", "app")
355         if "partition_table" in args.actions:
356             print_flashing_message("Partition Table", "partition_table")
357         if "bootloader" in args.actions:
358             print_flashing_message("Bootloader", "bootloader")
359
360 ACTIONS = {
361     # action name : ( function (or alias), dependencies, order-only dependencies )
362     "all" :                  ( build_target, [], [ "reconfigure", "menuconfig", "clean", "fullclean" ] ),
363     "build":                 ( "all",        [], [] ),  # build is same as 'all' target
364     "clean":                 ( clean,        [], [ "fullclean" ] ),
365     "fullclean":             ( fullclean,    [], [] ),
366     "reconfigure":           ( reconfigure,  [], [ "menuconfig" ] ),
367     "menuconfig":            ( build_target, [], [] ),
368     "size":                  ( build_target, [ "app" ], [] ),
369     "size-components":       ( build_target, [ "app" ], [] ),
370     "size-files":            ( build_target, [ "app" ], [] ),
371     "bootloader":            ( build_target, [], [] ),
372     "bootloader-clean":      ( build_target, [], [] ),
373     "bootloader-flash":      ( flash,        [ "bootloader" ], [ "erase_flash"] ),
374     "app":                   ( build_target, [], [ "clean", "fullclean", "reconfigure" ] ),
375     "app-flash":             ( flash,        [ "app" ], [ "erase_flash"]),
376     "partition_table":       ( build_target, [], [ "reconfigure" ] ),
377     "partition_table-flash": ( flash,        [ "partition_table" ], [ "erase_flash" ]),
378     "flash":                 ( flash,        [ "all" ], [ "erase_flash" ] ),
379     "erase_flash":           ( erase_flash,  [], []),
380     "monitor":               ( monitor,      [], [ "flash", "partition_table-flash", "bootloader-flash", "app-flash" ]),
381 }
382
383
384 def get_commandline_options():
385     """ Return all the command line options up to but not including the action """
386     result = []
387     for a in sys.argv:
388         if a in ACTIONS.keys():
389             break
390         else:
391             result.append(a)
392     return result
393
394 def main():
395     if sys.version_info[0] != 2 or sys.version_info[1] != 7:
396         raise FatalError("ESP-IDF currently only supports Python 2.7, and this is Python %d.%d.%d. Search for 'Setting the Python Interpreter' in the ESP-IDF docs for some tips to handle this." % sys.version_info[:3])
397
398     parser = argparse.ArgumentParser(description='ESP-IDF build management tool')
399     parser.add_argument('-p', '--port', help="Serial port",
400                         default=os.environ.get('ESPPORT', None))
401     parser.add_argument('-b', '--baud', help="Baud rate",
402                         default=os.environ.get('ESPBAUD', 460800))
403     parser.add_argument('-C', '--project-dir', help="Project directory", default=os.getcwd())
404     parser.add_argument('-B', '--build-dir', help="Build directory", default=None)
405     parser.add_argument('-G', '--generator', help="Cmake generator", choices=GENERATOR_CMDS.keys())
406     parser.add_argument('-n', '--no-warnings', help="Disable Cmake warnings", action="store_true")
407     parser.add_argument('-v', '--verbose', help="Verbose build output", action="store_true")
408     parser.add_argument('--no-ccache', help="Disable ccache. Otherwise, if ccache is available on the PATH then it will be used for faster builds.", action="store_true")
409     parser.add_argument('actions', help="Actions (build targets or other operations)", nargs='+',
410                         choices=ACTIONS.keys())
411
412     args = parser.parse_args()
413
414     check_environment()
415
416     # Advanced parameter checks
417     if args.build_dir is not None and os.path.realpath(args.project_dir) == os.path.realpath(args.build_dir):
418         raise FatalError("Setting the build directory to the project directory is not supported. Suggest dropping --build-dir option, the default is a 'build' subdirectory inside the project directory.")
419     if args.build_dir is None:
420         args.build_dir = os.path.join(args.project_dir, "build")
421     args.build_dir = os.path.realpath(args.build_dir)
422
423     completed_actions = set()
424     def execute_action(action, remaining_actions):
425         ( function, dependencies, order_dependencies ) = ACTIONS[action]
426         # very simple dependency management, build a set of completed actions and make sure
427         # all dependencies are in it
428         for dep in dependencies:
429             if not dep in completed_actions:
430                 execute_action(dep, remaining_actions)
431         for dep in order_dependencies:
432             if dep in remaining_actions and not dep in completed_actions:
433                 execute_action(dep, remaining_actions)
434
435         if action in completed_actions:
436             pass  # we've already done this, don't do it twice...
437         elif function in ACTIONS:  # alias of another action
438             execute_action(function, remaining_actions)
439         else:
440             function(action, args)
441
442         completed_actions.add(action)
443
444     actions = list(args.actions)
445     while len(actions) > 0:
446         execute_action(actions[0], actions[1:])
447         actions.pop(0)
448
449     print_closing_message(args)
450
451 if __name__ == "__main__":
452     try:
453         main()
454     except FatalError as e:
455         print(e)
456         sys.exit(2)
457
458