]> granicus.if.org Git - esp-idf/blob - tools/idf.py
cmake: Add ESPORT/ESPBAUD environment variables to idf.py & docs
[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 ACTIONS = {
313     # action name : ( function (or alias), dependencies, order-only dependencies )
314     "all" :                  ( build_target, [], [ "reconfigure", "menuconfig", "clean", "fullclean" ] ),
315     "build":                 ( "all",        [], [] ),  # build is same as 'all' target
316     "clean":                 ( clean,        [], [ "fullclean" ] ),
317     "fullclean":             ( fullclean,    [], [] ),
318     "reconfigure":           ( reconfigure,  [], [ "menuconfig" ] ),
319     "menuconfig":            ( build_target, [], [] ),
320     "size":                  ( build_target, [ "app" ], [] ),
321     "size-components":       ( build_target, [ "app" ], [] ),
322     "size-files":            ( build_target, [ "app" ], [] ),
323     "bootloader":            ( build_target, [], [] ),
324     "bootloader-clean":      ( build_target, [], [] ),
325     "bootloader-flash":      ( flash,        [ "bootloader" ], [ "erase_flash"] ),
326     "app":                   ( build_target, [], [ "clean", "fullclean", "reconfigure" ] ),
327     "app-flash":             ( flash,        [ "app" ], [ "erase_flash"]),
328     "partition_table":       ( build_target, [], [ "reconfigure" ] ),
329     "partition_table-flash": ( flash,        [ "partition_table" ], [ "erase_flash" ]),
330     "flash":                 ( flash,        [ "all" ], [ "erase_flash" ] ),
331     "erase_flash":           ( erase_flash,  [], []),
332     "monitor":               ( monitor,      [], [ "flash", "partition_table-flash", "bootloader-flash", "app-flash" ]),
333 }
334
335
336 def get_commandline_options():
337     """ Return all the command line options up to but not including the action """
338     result = []
339     for a in sys.argv:
340         if a in ACTIONS.keys():
341             break
342         else:
343             result.append(a)
344     return result
345
346
347 def main():
348     parser = argparse.ArgumentParser(description='ESP-IDF build management tool')
349     parser.add_argument('-p', '--port', help="Serial port",
350                         default=os.environ.get('ESPPORT', None))
351     parser.add_argument('-b', '--baud', help="Baud rate",
352                         default=os.environ.get('ESPBAUD', 460800))
353     parser.add_argument('-C', '--project-dir', help="Project directory", default=os.getcwd())
354     parser.add_argument('-B', '--build-dir', help="Build directory", default=None)
355     parser.add_argument('-G', '--generator', help="Cmake generator", choices=GENERATOR_CMDS.keys())
356     parser.add_argument('-n', '--no-warnings', help="Disable Cmake warnings", action="store_true")
357     parser.add_argument('-v', '--verbose', help="Verbose build output", action="store_true")
358     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")
359     parser.add_argument('actions', help="Actions (build targets or other operations)", nargs='+',
360                         choices=ACTIONS.keys())
361
362     args = parser.parse_args()
363
364     check_environment()
365
366     # Advanced parameter checks
367     if args.build_dir is not None and os.path.realpath(args.project_dir) == os.path.realpath(args.build_dir):
368         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.")
369     if args.build_dir is None:
370         args.build_dir = os.path.join(args.project_dir, "build")
371     args.build_dir = os.path.realpath(args.build_dir)
372
373     completed_actions = set()
374     def execute_action(action, remaining_actions):
375         ( function, dependencies, order_dependencies ) = ACTIONS[action]
376         # very simple dependency management, build a set of completed actions and make sure
377         # all dependencies are in it
378         for dep in dependencies:
379             if not dep in completed_actions:
380                 execute_action(dep, remaining_actions)
381         for dep in order_dependencies:
382             if dep in remaining_actions and not dep in completed_actions:
383                 execute_action(dep, remaining_actions)
384
385         if action in completed_actions:
386             pass  # we've already done this, don't do it twice...
387         elif function in ACTIONS:  # alias of another action
388             execute_action(function, remaining_actions)
389         else:
390             function(action, args)
391
392         completed_actions.add(action)
393
394     while len(args.actions) > 0:
395         execute_action(args.actions[0], args.actions[1:])
396         args.actions.pop(0)
397
398
399 if __name__ == "__main__":
400     try:
401         main()
402     except FatalError as e:
403         print(e)
404         sys.exit(2)
405
406