]> granicus.if.org Git - esp-idf/blob - tools/idf.py
idf.py build & flash tool
[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 # Use this Python interpreter for any subprocesses we launch
35 PYTHON=sys.executable
36
37 # note: os.environ changes don't automatically propagate to child processes,
38 # you have to pass this in explicitly
39 os.environ["PYTHON"]=sys.executable
40
41 # Make flavors, across the various kinds of Windows environments & POSIX...
42 if "MSYSTEM" in os.environ:  # MSYS
43     MAKE_CMD = "make"
44     MAKE_GENERATOR = "MSYS Makefiles"
45 elif os.name == 'nt':  # other Windows
46     MAKE_CMD = "mingw32-make"
47     MAKE_GENERATOR = "MinGW Makefiles"
48 else:
49     MAKE_CMD = "make"
50     MAKE_GENERATOR = "Unix Makefiles"
51
52 GENERATORS = [
53     # ('generator name', 'build command line', 'version command line')
54     ("Ninja", [ "ninja" ], [ "ninja", "--version" ]),
55     (MAKE_GENERATOR, [ MAKE_CMD, "-j", str(multiprocessing.cpu_count()+2) ], [ "make", "--version" ]),
56     ]
57 GENERATOR_CMDS = dict( (a[0], a[1]) for a in GENERATORS )
58
59 def check_environment():
60     """
61     Verify the environment contains the top-level tools we need to operate
62
63     (cmake will check a lot of other things)
64     """
65     if not executable_exists(["cmake", "--version"]):
66         raise RuntimeError("'cmake' must be available on the PATH to use idf.py")
67     # find the directory idf.py is in, then the parent directory of this, and assume this is IDF_PATH
68     detected_idf_path = os.path.realpath(os.path.join(os.path.dirname(__file__), ".."))
69     if "IDF_PATH" in os.environ:
70         set_idf_path = os.path.realpath(os.environ["IDF_PATH"])
71         if set_idf_path != detected_idf_path:
72             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..."
73                   % (set_idf_path, detected_idf_path))
74     else:
75         os.environ["IDF_PATH"] = detected_idf_path
76
77 def executable_exists(args):
78     try:
79         subprocess.check_output(args)
80         return True
81     except:
82         return False
83
84 def detect_cmake_generator():
85     """
86     Find the default cmake generator, if none was specified. Raises an exception if no valid generator is found.
87     """
88     for (generator, _, version_check) in GENERATORS:
89         if executable_exists(version_check):
90             return generator
91     raise RuntimeError("To use idf.py, either the 'ninja' or 'GNU make' build tool must be available in the PATH")
92
93 def _ensure_build_directory(args):
94     """Check the build directory exists and that cmake has been run there.
95
96     If this isn't the case, create the build directory (if necessary) and
97     do an initial cmake run to configure it.
98
99     This function will also check args.generator parameter. If the parameter is incompatible with
100     the build directory, an error is raised. If the parameter is None, this function will set it to
101     an auto-detected default generator or to the value already configured in the build directory.
102     """
103     project_dir = args.project_dir
104     # Verify the project directory
105     if not os.path.isdir(project_dir):
106         if not os.path.exists(project_dir):
107             raise RuntimeError("Project directory %s does not exist")
108         else:
109             raise RuntimeError("%s must be a project directory")
110     if not os.path.exists(os.path.join(project_dir, "CMakeLists.txt")):
111         raise RuntimeError("CMakeLists.txt not found in project directory %s" % project_dir)
112
113     # Verify/create the build directory
114     build_dir = args.build_dir
115     if not os.path.isdir(build_dir):
116         os.mkdir(build_dir)
117     cache_path = os.path.join(build_dir, "CMakeCache.txt")
118     if not os.path.exists(cache_path):
119         if args.generator is None:
120             args.generator = detect_cmake_generator()
121         print("Running cmake...")
122         # Note: we explicitly pass in os.environ here, as we may have set IDF_PATH there during startup
123         try:
124             subprocess.check_call(["cmake", "-G", args.generator, project_dir], env=os.environ, cwd=args.build_dir)
125         except:
126             if os.path.exists(cache_path):  # don't allow partial CMakeCache.txt files, to keep the "should I run cmake?" logic simple
127                 os.remove(cache_path)
128             raise
129
130     # Learn some things from the CMakeCache.txt file in the build directory
131     cache = parse_cmakecache(cache_path)
132     try:
133         generator = cache["CMAKE_GENERATOR"]
134     except KeyError:
135         generator = detect_cmake_generator()
136     if args.generator is None:
137         args.generator = generator  # reuse the previously configured generator, if none was given
138     if generator != args.generator:
139         raise RuntimeError("Build is configured for generator '%s' not '%s'. Run 'idf.py fullclean' to start again."
140                            % (generator, args.generator))
141
142     try:
143         home_dir = cache["CMAKE_HOME_DIRECTORY"]
144         if os.path.realpath(home_dir) != os.path.realpath(project_dir):
145             raise RuntimeError("Build directory '%s' configured for project '%s' not '%s'. Run 'idf.py fullclean' to start again."
146                             % (build_dir, os.path.realpath(home_dir), os.path.realpath(project_dir)))
147     except KeyError:
148         pass  # if cmake failed part way, CMAKE_HOME_DIRECTORY may not be set yet
149
150
151 def parse_cmakecache(path):
152     """
153     Parse the CMakeCache file at 'path'.
154
155     Returns a dict of name:value.
156
157     CMakeCache entries also each have a "type", but this is currently ignored.
158     """
159     result = {}
160     with open(path) as f:
161         for line in f:
162             # cmake cache lines look like: CMAKE_CXX_FLAGS_DEBUG:STRING=-g
163             # groups are name, type, value
164             m = re.match(r"^([^#/:=]+):([^:=]+)=(.+)\n$", line)
165             if m:
166                result[m.group(1)] = m.group(3)
167     return result
168
169 def build_target(target_name, args):
170     """
171     Execute the target build system to build target 'target_name'
172
173     Calls _ensure_build_directory() which will run cmake to generate a build
174     directory (with the specified generator) as needed.
175     """
176     _ensure_build_directory(args)
177     generator_cmd = GENERATOR_CMDS[args.generator]
178     print("Running '%s %s' in %s..." % (generator_cmd, target_name, args.build_dir))
179     subprocess.check_call(generator_cmd + [target_name], cwd=args.build_dir)
180
181 def _get_esptool_args(args):
182     esptool_path = os.path.join(os.environ["IDF_PATH"], "components/esptool_py/esptool/esptool.py")
183     result = [ PYTHON, esptool_path ]
184     if args.port is not None:
185         result += [ "-p", args.port ]
186     result += [ "-b", str(args.baud) ]
187     return result
188
189 def flash(action, args):
190     """
191     Run esptool to flash the entire project, from an argfile generated by the build system
192     """
193     flasher_args_path = {
194         "bootloader-flash":      "flash_bootloader_args",
195         "partition_table-flash": "flash_partition_table_args",
196         "app-flash":             "flash_app_args",
197         "flash":                 "flash_project_args",
198     }[action]
199     esptool_args = _get_esptool_args(args)
200     esptool_args += [ "write_flash", "@"+flasher_args_path ]
201     subprocess.check_call(esptool_args, cwd=args.build_dir)
202
203 def erase_flash(action, args):
204     esptool_args = _get_esptool_args(args)
205     esptool_args += [ "erase_flash" ]
206     subprocess.check_call(esptool_args, cwd=args.build_dir)
207
208 def monitor(action, args):
209     """
210     Run idf_monitor.py to watch build output
211     """
212     desc_path = os.path.join(args.build_dir, "project_description.json")
213     if not os.path.exists(desc_path):
214         _ensure_build_directory(args)
215     with open(desc_path, "r") as f:
216         project_desc = json.load(f)
217
218     elf_file = os.path.join(args.build_dir, project_desc["app_elf"])
219     if not os.path.exists(elf_file):
220         raise RuntimeError("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)
221     idf_monitor = os.path.join(os.environ["IDF_PATH"], "tools/idf_monitor.py")
222     monitor_args = [PYTHON, idf_monitor ]
223     if args.port is not None:
224         monitor_args += [ "-p", args.port ]
225     monitor_args += [ "-b", project_desc["monitor_baud"] ]
226     monitor_args += [ elf_file ]
227     subprocess.check_call(monitor_args, cwd=args.build_dir)
228
229 def clean(action, args):
230     if not os.path.isdir(args.build_dir):
231         print("Build directory '%s' not found. Nothing to clean." % args.build_dir)
232         return
233     build_target("clean", args)
234
235 def fullclean(action, args):
236     build_dir = args.build_dir
237     if not os.path.isdir(build_dir):
238         print("Build directory '%s' not found. Nothing to clean." % build_dir)
239         return
240     if len(os.listdir(build_dir)) == 0:
241         print("Build directory '%s' is empty. Nothing to clean." % build_dir)
242         return
243
244     if not os.path.exists(os.path.join(build_dir, "CMakeCache.txt")):
245         raise RuntimeError("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)
246     red_flags = [ "CMakeLists.txt", ".git", ".svn" ]
247     for red in red_flags:
248         red = os.path.join(build_dir, red)
249         if os.path.exists(red):
250             raise RuntimeError("Refusing to automatically delete files in directory containing '%s'. Delete files manually if you're sure." % red)
251     # OK, delete everything in the build directory...
252     for f in os.listdir(build_dir):  # TODO: once we are Python 3 only, this can be os.scandir()
253         f = os.path.join(build_dir, f)
254         if os.path.isdir(f):
255             shutil.rmtree(f)
256         else:
257             os.remove(f)
258
259 ACTIONS = {
260     # action name : ( function (or alias), dependencies, order-only dependencies )
261     "all" :                  ( build_target, [], [ "menuconfig", "clean", "fullclean" ] ),
262     "build":                 ( "all",        [], [] ),  # build is same as 'all' target
263     "clean":                 ( clean,        [], [ "fullclean" ] ),
264     "fullclean":             ( fullclean,    [], [] ),
265     "menuconfig":            ( build_target, [], [] ),
266     "size":                  ( build_target, [], [ "app" ] ),
267     "size-components":       ( build_target, [], [ "app" ] ),
268     "size-files":            ( build_target, [], [ "app" ] ),
269     "bootloader":            ( build_target, [], [] ),
270     "bootloader-clean":      ( build_target, [], [] ),
271     "bootloader-flash":      ( flash,        [ "bootloader" ], [] ),
272     "app":                   ( build_target, [], [] ),
273     "app-flash":             ( flash,        [], [ "app" ]),
274     "partition_table":       ( build_target, [], [] ),
275     "partition_table-flash": ( flash,        [ "partition_table" ], []),
276     "flash":                 ( flash,        [ "all" ], [ ] ),
277     "erase_flash":           ( erase_flash,  [], []),
278     "monitor":               ( monitor,      [], [ "flash", "partition_table-flash", "bootloader-flash", "app-flash" ]),
279 }
280
281
282 def main():
283     parser = argparse.ArgumentParser(description='ESP-IDF build management tool')
284     parser.add_argument('-p', '--port', help="Serial port", default=None)
285     parser.add_argument('-b', '--baud', help="Baud rate", default=460800)
286     parser.add_argument('-C', '--project-dir', help="Project directory", default=os.getcwd())
287     parser.add_argument('-B', '--build-dir', help="Build directory", default=None)
288     parser.add_argument('-G', '--generator', help="Cmake generator", choices=GENERATOR_CMDS.keys())
289     parser.add_argument('actions', help="Actions (build targets or other operations)", nargs='+',
290                         choices=ACTIONS.keys())
291
292     args = parser.parse_args()
293
294     check_environment()
295
296     # Advanced parameter checks
297     if args.build_dir is not None and os.path.realpath(args.project_dir) == os.path.realpath(args.build_dir):
298         raise RuntimeError("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.")
299     if args.build_dir is None:
300         args.build_dir = os.path.join(args.project_dir, "build")
301     args.build_dir = os.path.realpath(args.build_dir)
302
303     completed_actions = set()
304     def execute_action(action, remaining_actions):
305         ( function, dependencies, order_dependencies ) = ACTIONS[action]
306         # very simple dependency management, build a set of completed actions and make sure
307         # all dependencies are in it
308         for dep in dependencies:
309             if not dep in completed_actions:
310                 execute_action(dep, remaining_actions)
311         for dep in order_dependencies:
312             if dep in remaining_actions and not dep in completed_actions:
313                 execute_action(dep, remaining_actions)
314
315         if action in completed_actions:
316             pass  # we've already done this, don't do it twice...
317         elif function in ACTIONS:  # alias of another action
318             execute_action(function, remaining_actions)
319         else:
320             function(action, args)
321
322         completed_actions.add(action)
323
324     while len(args.actions) > 0:
325         execute_action(args.actions[0], args.actions[1:])
326         args.actions.pop(0)
327
328
329 if __name__ == "__main__":
330     main()
331