blob: bd1dc57e60f4fbfdc1980cd72c25b2deea86fa33 [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)
Carles Cufi31bdad52019-04-26 21:53:02 +0200127 ---------- -------------------------------------------
128 '''
129
130 # Regular expression for a cache entry.
131 #
132 # CMake variable names can include escape characters, allowing a
133 # wider set of names than is easy to match with a regular
134 # expression. To be permissive here, use a non-greedy match up to
135 # the first colon (':'). This breaks if the variable name has a
136 # colon inside, but it's good enough.
137 CACHE_ENTRY = re.compile(
Julien D'Ascenziofb242142020-04-17 15:53:19 +0200138 r'''(?P<name>.*?) # name
139 :(?P<type>FILEPATH|PATH|STRING|BOOL|INTERNAL|STATIC) # type
140 =(?P<value>.*) # value
Carles Cufi31bdad52019-04-26 21:53:02 +0200141 ''', re.X)
142
143 @classmethod
144 def _to_bool(cls, val):
145 # Convert a CMake BOOL string into a Python bool.
146 #
147 # "True if the constant is 1, ON, YES, TRUE, Y, or a
148 # non-zero number. False if the constant is 0, OFF, NO,
149 # FALSE, N, IGNORE, NOTFOUND, the empty string, or ends in
150 # the suffix -NOTFOUND. Named boolean constants are
151 # case-insensitive. If the argument is not one of these
152 # constants, it is treated as a variable."
153 #
154 # https://cmake.org/cmake/help/v3.0/command/if.html
155 val = val.upper()
156 if val in ('ON', 'YES', 'TRUE', 'Y'):
157 return True
158 elif val in ('OFF', 'NO', 'FALSE', 'N', 'IGNORE', 'NOTFOUND', ''):
159 return False
160 elif val.endswith('-NOTFOUND'):
161 return False
162 else:
163 try:
164 v = int(val)
165 return v != 0
166 except ValueError as exc:
167 raise ValueError('invalid bool {}'.format(val)) from exc
168
169 @classmethod
170 def from_line(cls, line, line_no):
171 # Comments can only occur at the beginning of a line.
172 # (The value of an entry could contain a comment character).
173 if line.startswith('//') or line.startswith('#'):
174 return None
175
176 # Whitespace-only lines do not contain cache entries.
177 if not line.strip():
178 return None
179
180 m = cls.CACHE_ENTRY.match(line)
181 if not m:
182 return None
183
184 name, type_, value = (m.group(g) for g in ('name', 'type', 'value'))
185 if type_ == 'BOOL':
186 try:
187 value = cls._to_bool(value)
188 except ValueError as exc:
189 args = exc.args + ('on line {}: {}'.format(line_no, line),)
190 raise ValueError(args) from exc
Julien D'Ascenziofb242142020-04-17 15:53:19 +0200191 elif type_ in {'STRING', 'INTERNAL', 'STATIC'}:
Carles Cufi31bdad52019-04-26 21:53:02 +0200192 # If the value is a CMake list (i.e. is a string which
193 # contains a ';'), convert to a Python list.
194 if ';' in value:
195 value = value.split(';')
196
197 return CMakeCacheEntry(name, value)
198
199 def __init__(self, name, value):
200 self.name = name
201 self.value = value
202
203 def __str__(self):
204 fmt = 'CMakeCacheEntry(name={}, value={})'
205 return fmt.format(self.name, self.value)
206
207
208class CMakeCache:
209 '''Parses and represents a CMake cache file.'''
210
211 @staticmethod
212 def from_build_dir(build_dir):
213 return CMakeCache(os.path.join(build_dir, DEFAULT_CACHE))
214
215 def __init__(self, cache_file):
216 self.cache_file = cache_file
217 self.load(cache_file)
218
219 def load(self, cache_file):
220 entries = []
Carles Cufi97542012019-07-18 16:44:31 +0200221 with open(cache_file, 'r', encoding="utf-8") as cache:
Carles Cufi31bdad52019-04-26 21:53:02 +0200222 for line_no, line in enumerate(cache):
223 entry = CMakeCacheEntry.from_line(line, line_no)
224 if entry:
225 entries.append(entry)
226 self._entries = OrderedDict((e.name, e) for e in entries)
227
228 def get(self, name, default=None):
229 entry = self._entries.get(name)
230 if entry is not None:
231 return entry.value
232 else:
233 return default
234
235 def get_list(self, name, default=None):
236 if default is None:
237 default = []
238 entry = self._entries.get(name)
239 if entry is not None:
240 value = entry.value
241 if isinstance(value, list):
242 return value
243 elif isinstance(value, str):
244 return [value] if value else []
245 else:
246 msg = 'invalid value {} type {}'
247 raise RuntimeError(msg.format(value, type(value)))
248 else:
249 return default
250
251 def __contains__(self, name):
252 return name in self._entries
253
254 def __getitem__(self, name):
255 return self._entries[name].value
256
257 def __setitem__(self, name, entry):
258 if not isinstance(entry, CMakeCacheEntry):
259 msg = 'improper type {} for value {}, expecting CMakeCacheEntry'
260 raise TypeError(msg.format(type(entry), entry))
261 self._entries[name] = entry
262
263 def __delitem__(self, name):
264 del self._entries[name]
265
266 def __iter__(self):
267 return iter(self._entries.values())
Marti Bolivar146580e2019-08-25 12:53:24 -0600268
269def _ensure_min_version(cmake, dry_run):
270 cmd = [cmake, '--version']
271 if dry_run:
272 log.inf('Dry run:', quote_sh_list(cmd))
273 return
274
275 try:
276 version_out = subprocess.check_output(cmd, stderr=subprocess.DEVNULL)
277 except subprocess.CalledProcessError as cpe:
278 log.die('cannot get cmake version:', str(cpe))
279 decoded = version_out.decode('utf-8')
280 lines = decoded.splitlines()
281 if not lines:
282 log.die('can\'t get cmake version: ' +
283 'unexpected "cmake --version" output:\n{}\n'.
284 format(decoded) +
285 'Please install CMake ' + _MIN_CMAKE_VERSION_STR +
286 ' or higher (https://cmake.org/download/).')
287 version = lines[0].split()[2]
Martí Bolívar16a74652021-02-08 10:02:55 -0800288 if '-' in version:
289 # Handle semver cases like "3.19.20210206-g1e50ab6"
290 # which Kitware uses for prerelease versions.
291 version = version.split('-', 1)[0]
Marti Bolivar146580e2019-08-25 12:53:24 -0600292 if packaging.version.parse(version) < _MIN_CMAKE_VERSION:
293 log.die('cmake version', version,
294 'is less than minimum version {};'.
295 format(_MIN_CMAKE_VERSION_STR),
296 'please update your CMake (https://cmake.org/download/).')
297 else:
298 log.dbg('cmake version', version, 'is OK; minimum version is',
299 _MIN_CMAKE_VERSION_STR)
300
301_MIN_CMAKE_VERSION_STR = '3.13.1'
302_MIN_CMAKE_VERSION = packaging.version.parse(_MIN_CMAKE_VERSION_STR)