blob: aad616aadd423de1ba22b95505e7a7b51893b6f7 [file] [log] [blame]
Carles Cufi31bdad52019-04-26 21:53:02 +02001# Copyright (c) 2018 Open Source Foundries Limited.
2#
3# SPDX-License-Identifier: Apache-2.0
4'''Common definitions for building Zephyr applications with CMake.
5
6This provides some default settings and convenience wrappers for
7building Zephyr applications needed by multiple commands.
8
9See build.py for the build command itself.
10'''
11
12from collections import OrderedDict
13import os.path
14import re
15import subprocess
16import shutil
17import sys
18
Marti Bolivar146580e2019-08-25 12:53:24 -060019import packaging.version
Carles Cufi31bdad52019-04-26 21:53:02 +020020from west import log
21from west.util import quote_sh_list
22
23DEFAULT_CACHE = 'CMakeCache.txt'
24
25DEFAULT_CMAKE_GENERATOR = 'Ninja'
26'''Name of the default CMake generator.'''
27
Marti Bolivar8465cf22019-05-01 17:24:23 -060028
29def run_cmake(args, cwd=None, capture_output=False, dry_run=False):
30 '''Run cmake to (re)generate a build system, a script, etc.
31
32 :param args: arguments to pass to CMake
33 :param cwd: directory to run CMake in, cwd is default
34 :param capture_output: if True, the output is returned instead of being
35 displayed (None is returned by default, or if
36 dry_run is also True)
37 :param dry_run: don't actually execute the command, just print what
38 would have been run
39
Carles Cufi31bdad52019-04-26 21:53:02 +020040 If capture_output is set to True, returns the output of the command instead
41 of displaying it on stdout/stderr..'''
42 cmake = shutil.which('cmake')
Marti Bolivar8465cf22019-05-01 17:24:23 -060043 if cmake is None and not dry_run:
Carles Cufi31bdad52019-04-26 21:53:02 +020044 log.die('CMake is not installed or cannot be found; cannot build.')
Marti Bolivar146580e2019-08-25 12:53:24 -060045 _ensure_min_version(cmake, dry_run)
46
Carles Cufi31bdad52019-04-26 21:53:02 +020047 cmd = [cmake] + args
Torsten Rasmussenef3c5e52020-06-08 21:09:15 +020048
Carles Cufi31bdad52019-04-26 21:53:02 +020049 kwargs = dict()
50 if capture_output:
51 kwargs['stdout'] = subprocess.PIPE
52 # CMake sends the output of message() to stderr unless it's STATUS
53 kwargs['stderr'] = subprocess.STDOUT
54 if cwd:
55 kwargs['cwd'] = cwd
Marti Bolivar8465cf22019-05-01 17:24:23 -060056
57 if dry_run:
58 in_cwd = ' (in {})'.format(cwd) if cwd else ''
59 log.inf('Dry run{}:'.format(in_cwd), quote_sh_list(cmd))
60 return None
61
Carles Cufi31bdad52019-04-26 21:53:02 +020062 log.dbg('Running CMake:', quote_sh_list(cmd), level=log.VERBOSE_NORMAL)
63 p = subprocess.Popen(cmd, **kwargs)
Ulf Magnusson859c4ed2019-05-07 10:01:36 +020064 out, _ = p.communicate()
Carles Cufi31bdad52019-04-26 21:53:02 +020065 if p.returncode == 0:
66 if out:
67 return out.decode(sys.getdefaultencoding()).splitlines()
68 else:
69 return None
70 else:
71 # A real error occurred, raise an exception
Carles Cufic9f4bb62019-05-04 11:02:40 +020072 raise subprocess.CalledProcessError(p.returncode, p.args)
Carles Cufi31bdad52019-04-26 21:53:02 +020073
74
Marti Bolivar8465cf22019-05-01 17:24:23 -060075def run_build(build_directory, **kwargs):
76 '''Run cmake in build tool mode.
77
78 :param build_directory: runs "cmake --build build_directory"
79 :param extra_args: optional kwarg. List of additional CMake arguments;
80 these come after "--build <build_directory>"
81 on the command line.
82
83 Any additional keyword arguments are passed as-is to run_cmake().
84 '''
85 extra_args = kwargs.pop('extra_args', [])
86 return run_cmake(['--build', build_directory] + extra_args, **kwargs)
Carles Cufi31bdad52019-04-26 21:53:02 +020087
88
89def make_c_identifier(string):
90 '''Make a C identifier from a string in the same way CMake does.
91 '''
92 # The behavior of CMake's string(MAKE_C_IDENTIFIER ...) is not
93 # precisely documented. This behavior matches the test case
94 # that introduced the function:
95 #
96 # https://gitlab.kitware.com/cmake/cmake/commit/0ab50aea4c4d7099b339fb38b4459d0debbdbd85
97 ret = []
98
99 alpha_under = re.compile('[A-Za-z_]')
100 alpha_num_under = re.compile('[A-Za-z0-9_]')
101
102 if not alpha_under.match(string):
103 ret.append('_')
104 for c in string:
105 if alpha_num_under.match(c):
106 ret.append(c)
107 else:
108 ret.append('_')
109
110 return ''.join(ret)
111
112
113class CMakeCacheEntry:
114 '''Represents a CMake cache entry.
115
116 This class understands the type system in a CMakeCache.txt, and
117 converts the following cache types to Python types:
118
119 Cache Type Python type
120 ---------- -------------------------------------------
121 FILEPATH str
122 PATH str
123 STRING str OR list of str (if ';' is in the value)
124 BOOL bool
125 INTERNAL str OR list of str (if ';' is in the value)
Julien D'Ascenziofb242142020-04-17 15:53:19 +0200126 STATIC str OR list of str (if ';' is in the value)
Torsten Rasmussenf08af4c2022-07-08 13:28:42 +0200127 UNINITIALIZED str OR list of str (if ';' is in the value)
Carles Cufi31bdad52019-04-26 21:53:02 +0200128 ---------- -------------------------------------------
129 '''
130
131 # Regular expression for a cache entry.
132 #
133 # CMake variable names can include escape characters, allowing a
134 # wider set of names than is easy to match with a regular
135 # expression. To be permissive here, use a non-greedy match up to
136 # the first colon (':'). This breaks if the variable name has a
137 # colon inside, but it's good enough.
138 CACHE_ENTRY = re.compile(
Torsten Rasmussenf08af4c2022-07-08 13:28:42 +0200139 r'''(?P<name>.*?) # name
140 :(?P<type>FILEPATH|PATH|STRING|BOOL|INTERNAL|STATIC|UNINITIALIZED) # type
141 =(?P<value>.*) # value
Carles Cufi31bdad52019-04-26 21:53:02 +0200142 ''', re.X)
143
144 @classmethod
145 def _to_bool(cls, val):
146 # Convert a CMake BOOL string into a Python bool.
147 #
148 # "True if the constant is 1, ON, YES, TRUE, Y, or a
149 # non-zero number. False if the constant is 0, OFF, NO,
150 # FALSE, N, IGNORE, NOTFOUND, the empty string, or ends in
151 # the suffix -NOTFOUND. Named boolean constants are
152 # case-insensitive. If the argument is not one of these
153 # constants, it is treated as a variable."
154 #
155 # https://cmake.org/cmake/help/v3.0/command/if.html
156 val = val.upper()
157 if val in ('ON', 'YES', 'TRUE', 'Y'):
158 return True
159 elif val in ('OFF', 'NO', 'FALSE', 'N', 'IGNORE', 'NOTFOUND', ''):
160 return False
161 elif val.endswith('-NOTFOUND'):
162 return False
163 else:
164 try:
165 v = int(val)
166 return v != 0
167 except ValueError as exc:
168 raise ValueError('invalid bool {}'.format(val)) from exc
169
170 @classmethod
171 def from_line(cls, line, line_no):
172 # Comments can only occur at the beginning of a line.
173 # (The value of an entry could contain a comment character).
174 if line.startswith('//') or line.startswith('#'):
175 return None
176
177 # Whitespace-only lines do not contain cache entries.
178 if not line.strip():
179 return None
180
181 m = cls.CACHE_ENTRY.match(line)
182 if not m:
183 return None
184
185 name, type_, value = (m.group(g) for g in ('name', 'type', 'value'))
186 if type_ == 'BOOL':
187 try:
188 value = cls._to_bool(value)
189 except ValueError as exc:
190 args = exc.args + ('on line {}: {}'.format(line_no, line),)
191 raise ValueError(args) from exc
Torsten Rasmussenf08af4c2022-07-08 13:28:42 +0200192 elif type_ in {'STRING', 'INTERNAL', 'STATIC', 'UNINITIALIZED'}:
Carles Cufi31bdad52019-04-26 21:53:02 +0200193 # If the value is a CMake list (i.e. is a string which
194 # contains a ';'), convert to a Python list.
195 if ';' in value:
196 value = value.split(';')
197
198 return CMakeCacheEntry(name, value)
199
200 def __init__(self, name, value):
201 self.name = name
202 self.value = value
203
204 def __str__(self):
205 fmt = 'CMakeCacheEntry(name={}, value={})'
206 return fmt.format(self.name, self.value)
207
208
209class CMakeCache:
210 '''Parses and represents a CMake cache file.'''
211
212 @staticmethod
213 def from_build_dir(build_dir):
214 return CMakeCache(os.path.join(build_dir, DEFAULT_CACHE))
215
216 def __init__(self, cache_file):
217 self.cache_file = cache_file
218 self.load(cache_file)
219
220 def load(self, cache_file):
221 entries = []
Carles Cufi97542012019-07-18 16:44:31 +0200222 with open(cache_file, 'r', encoding="utf-8") as cache:
Carles Cufi31bdad52019-04-26 21:53:02 +0200223 for line_no, line in enumerate(cache):
224 entry = CMakeCacheEntry.from_line(line, line_no)
225 if entry:
226 entries.append(entry)
227 self._entries = OrderedDict((e.name, e) for e in entries)
228
229 def get(self, name, default=None):
230 entry = self._entries.get(name)
231 if entry is not None:
232 return entry.value
233 else:
234 return default
235
236 def get_list(self, name, default=None):
237 if default is None:
238 default = []
239 entry = self._entries.get(name)
240 if entry is not None:
241 value = entry.value
242 if isinstance(value, list):
243 return value
244 elif isinstance(value, str):
245 return [value] if value else []
246 else:
247 msg = 'invalid value {} type {}'
248 raise RuntimeError(msg.format(value, type(value)))
249 else:
250 return default
251
252 def __contains__(self, name):
253 return name in self._entries
254
255 def __getitem__(self, name):
256 return self._entries[name].value
257
258 def __setitem__(self, name, entry):
259 if not isinstance(entry, CMakeCacheEntry):
260 msg = 'improper type {} for value {}, expecting CMakeCacheEntry'
261 raise TypeError(msg.format(type(entry), entry))
262 self._entries[name] = entry
263
264 def __delitem__(self, name):
265 del self._entries[name]
266
267 def __iter__(self):
268 return iter(self._entries.values())
Marti Bolivar146580e2019-08-25 12:53:24 -0600269
270def _ensure_min_version(cmake, dry_run):
271 cmd = [cmake, '--version']
272 if dry_run:
273 log.inf('Dry run:', quote_sh_list(cmd))
274 return
275
276 try:
277 version_out = subprocess.check_output(cmd, stderr=subprocess.DEVNULL)
278 except subprocess.CalledProcessError as cpe:
279 log.die('cannot get cmake version:', str(cpe))
280 decoded = version_out.decode('utf-8')
281 lines = decoded.splitlines()
282 if not lines:
283 log.die('can\'t get cmake version: ' +
284 'unexpected "cmake --version" output:\n{}\n'.
285 format(decoded) +
286 'Please install CMake ' + _MIN_CMAKE_VERSION_STR +
287 ' or higher (https://cmake.org/download/).')
288 version = lines[0].split()[2]
Martí Bolívar16a74652021-02-08 10:02:55 -0800289 if '-' in version:
290 # Handle semver cases like "3.19.20210206-g1e50ab6"
291 # which Kitware uses for prerelease versions.
292 version = version.split('-', 1)[0]
Marti Bolivar146580e2019-08-25 12:53:24 -0600293 if packaging.version.parse(version) < _MIN_CMAKE_VERSION:
294 log.die('cmake version', version,
295 'is less than minimum version {};'.
296 format(_MIN_CMAKE_VERSION_STR),
297 'please update your CMake (https://cmake.org/download/).')
298 else:
299 log.dbg('cmake version', version, 'is OK; minimum version is',
300 _MIN_CMAKE_VERSION_STR)
301
302_MIN_CMAKE_VERSION_STR = '3.13.1'
303_MIN_CMAKE_VERSION = packaging.version.parse(_MIN_CMAKE_VERSION_STR)