]> granicus.if.org Git - esp-idf/blob - tools/idf.py
cmake: Fix issues when IDF_PATH is not set in environment
[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 env=os.environ explicitly anywhere that we create a process
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         print("Setting IDF_PATH environment variable: %s" % detected_idf_path)
99         os.environ["IDF_PATH"] = detected_idf_path
100
101 def executable_exists(args):
102     try:
103         subprocess.check_output(args)
104         return True
105     except:
106         return False
107
108 def detect_cmake_generator():
109     """
110     Find the default cmake generator, if none was specified. Raises an exception if no valid generator is found.
111     """
112     for (generator, _, version_check, _) in GENERATORS:
113         if executable_exists(version_check):
114             return generator
115     raise FatalError("To use idf.py, either the 'ninja' or 'GNU make' build tool must be available in the PATH")
116
117 def _ensure_build_directory(args, always_run_cmake=False):
118     """Check the build directory exists and that cmake has been run there.
119
120     If this isn't the case, create the build directory (if necessary) and
121     do an initial cmake run to configure it.
122
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.
126     """
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")
132         else:
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)
136
137     # Verify/create the build directory
138     build_dir = args.build_dir
139     if not os.path.isdir(build_dir):
140         os.mkdir(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()
145         try:
146             cmake_args = ["cmake", "-G", args.generator]
147             if not args.no_warnings:
148                 cmake_args += [ "--warn-uninitialized" ]
149             if args.no_ccache:
150                 cmake_args += [ "-DCCACHE_DISABLE=1" ]
151             cmake_args += [ project_dir]
152             _run_tool("cmake", cmake_args, cwd=args.build_dir)
153         except:
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)
158             raise
159
160     # Learn some things from the CMakeCache.txt file in the build directory
161     cache = parse_cmakecache(cache_path)
162     try:
163         generator = cache["CMAKE_GENERATOR"]
164     except KeyError:
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))
171
172     try:
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)))
177     except KeyError:
178         pass  # if cmake failed part way, CMAKE_HOME_DIRECTORY may not be set yet
179
180
181 def parse_cmakecache(path):
182     """
183     Parse the CMakeCache file at 'path'.
184
185     Returns a dict of name:value.
186
187     CMakeCache entries also each have a "type", but this is currently ignored.
188     """
189     result = {}
190     with open(path) as f:
191         for line in 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)
195             if m:
196                result[m.group(1)] = m.group(3)
197     return result
198
199 def build_target(target_name, args):
200     """
201     Execute the target build system to build target 'target_name'
202
203     Calls _ensure_build_directory() which will run cmake to generate a build
204     directory (with the specified generator) as needed.
205     """
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.)
212         #
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"
216         pass
217     if args.verbose:
218         generator_cmd += [ GENERATOR_VERBOSE[args.generator] ]
219
220     _run_tool(generator_cmd[0], generator_cmd + [target_name], args.build_dir)
221
222
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) ]
229     return result
230
231 def flash(action, args):
232     """
233     Run esptool to flash the entire project, from an argfile generated by the build system
234     """
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",
240     }[action]
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)
244
245
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)
250
251
252 def monitor(action, args):
253     """
254     Run idf_monitor.py to watch build output
255     """
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)
261
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 ]
271
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) ]
274
275     if "MSYSTEM" is os.environ:
276         monitor_args = [ "winpty" ] + monitor_args
277     _run_tool("idf_monitor", monitor_args, args.project_dir)
278
279
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)
283         return
284     build_target("clean", args)
285
286 def reconfigure(action, args):
287     _ensure_build_directory(args, True)
288
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)
293         return
294     if len(os.listdir(build_dir)) == 0:
295         print("Build directory '%s' is empty. Nothing to clean." % build_dir)
296         return
297
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)
308         if os.path.isdir(f):
309             shutil.rmtree(f)
310         else:
311             os.remove(f)
312
313 def print_closing_message(args):
314     # print a closing message of some kind
315     #
316
317     if "flash" in str(args.actions):
318         print("Done")
319         return
320
321     # Otherwise, if we built any binaries print a message about
322     # how to flash them
323     def print_flashing_message(title, key):
324         print("\n%s build complete. To flash, run this command:" % title)
325
326         with open(os.path.join(args.build_dir, "flasher_args.json")) as f:
327             flasher_args = json.load(f)
328
329         def flasher_path(f):
330             return os.path.relpath(os.path.join(args.build_dir, f))
331
332         if key != "project":
333             cmd = ""
334             if key == "bootloader":
335                 cmd = " ".join(flasher_args["write_flash_args"]) + " "
336
337             cmd += flasher_args[key]["offset"] + " "
338             cmd += flasher_path(flasher_args[key]["file"])
339         else:
340             cmd = " ".join(flasher_args["write_flash_args"]) + " "
341             for o,f in flasher_args["flash_files"].items():
342                 cmd += o + " " + flasher_path(f) + " "
343
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)",
347             args.baud,
348             cmd.strip()))
349         print("or run 'idf.py %s'" % (key + "-flash" if key != "project" else "flash",))
350
351     if "all" in args.actions or "build" in args.actions:
352         print_flashing_message("Project", "project")
353     else:
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")
360
361 ACTIONS = {
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" ]),
383 }
384
385
386 def get_commandline_options():
387     """ Return all the command line options up to but not including the action """
388     result = []
389     for a in sys.argv:
390         if a in ACTIONS.keys():
391             break
392         else:
393             result.append(a)
394     return result
395
396 def main():
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])
399
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())
413
414     args = parser.parse_args()
415
416     check_environment()
417
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)
424
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)
436
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)
441         else:
442             function(action, args)
443
444         completed_actions.add(action)
445
446     actions = list(args.actions)
447     while len(actions) > 0:
448         execute_action(actions[0], actions[1:])
449         actions.pop(0)
450
451     print_closing_message(args)
452
453 if __name__ == "__main__":
454     try:
455         main()
456     except FatalError as e:
457         print(e)
458         sys.exit(2)
459
460