Carles Cufi | 31bdad5 | 2019-04-26 21:53:02 +0200 | [diff] [blame] | 1 | # 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 | |
| 6 | This provides some default settings and convenience wrappers for |
| 7 | building Zephyr applications needed by multiple commands. |
| 8 | |
| 9 | See build.py for the build command itself. |
| 10 | ''' |
| 11 | |
| 12 | from collections import OrderedDict |
| 13 | import os.path |
| 14 | import re |
| 15 | import subprocess |
| 16 | import shutil |
| 17 | import sys |
| 18 | |
Marti Bolivar | 146580e | 2019-08-25 12:53:24 -0600 | [diff] [blame] | 19 | import packaging.version |
Carles Cufi | 31bdad5 | 2019-04-26 21:53:02 +0200 | [diff] [blame] | 20 | from west import log |
| 21 | from west.util import quote_sh_list |
| 22 | |
| 23 | DEFAULT_CACHE = 'CMakeCache.txt' |
| 24 | |
| 25 | DEFAULT_CMAKE_GENERATOR = 'Ninja' |
| 26 | '''Name of the default CMake generator.''' |
| 27 | |
Marti Bolivar | 8465cf2 | 2019-05-01 17:24:23 -0600 | [diff] [blame] | 28 | |
| 29 | def 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 Cufi | 31bdad5 | 2019-04-26 21:53:02 +0200 | [diff] [blame] | 40 | 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 Bolivar | 8465cf2 | 2019-05-01 17:24:23 -0600 | [diff] [blame] | 43 | if cmake is None and not dry_run: |
Carles Cufi | 31bdad5 | 2019-04-26 21:53:02 +0200 | [diff] [blame] | 44 | log.die('CMake is not installed or cannot be found; cannot build.') |
Marti Bolivar | 146580e | 2019-08-25 12:53:24 -0600 | [diff] [blame] | 45 | _ensure_min_version(cmake, dry_run) |
| 46 | |
Carles Cufi | 31bdad5 | 2019-04-26 21:53:02 +0200 | [diff] [blame] | 47 | cmd = [cmake] + args |
Torsten Rasmussen | ef3c5e5 | 2020-06-08 21:09:15 +0200 | [diff] [blame] | 48 | |
Carles Cufi | 31bdad5 | 2019-04-26 21:53:02 +0200 | [diff] [blame] | 49 | 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 Bolivar | 8465cf2 | 2019-05-01 17:24:23 -0600 | [diff] [blame] | 56 | |
| 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 Cufi | 31bdad5 | 2019-04-26 21:53:02 +0200 | [diff] [blame] | 62 | log.dbg('Running CMake:', quote_sh_list(cmd), level=log.VERBOSE_NORMAL) |
| 63 | p = subprocess.Popen(cmd, **kwargs) |
Ulf Magnusson | 859c4ed | 2019-05-07 10:01:36 +0200 | [diff] [blame] | 64 | out, _ = p.communicate() |
Carles Cufi | 31bdad5 | 2019-04-26 21:53:02 +0200 | [diff] [blame] | 65 | 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 Cufi | c9f4bb6 | 2019-05-04 11:02:40 +0200 | [diff] [blame] | 72 | raise subprocess.CalledProcessError(p.returncode, p.args) |
Carles Cufi | 31bdad5 | 2019-04-26 21:53:02 +0200 | [diff] [blame] | 73 | |
| 74 | |
Marti Bolivar | 8465cf2 | 2019-05-01 17:24:23 -0600 | [diff] [blame] | 75 | def 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 Cufi | 31bdad5 | 2019-04-26 21:53:02 +0200 | [diff] [blame] | 87 | |
| 88 | |
| 89 | def 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 | |
| 113 | class 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'Ascenzio | fb24214 | 2020-04-17 15:53:19 +0200 | [diff] [blame] | 126 | STATIC str OR list of str (if ';' is in the value) |
Carles Cufi | 31bdad5 | 2019-04-26 21:53:02 +0200 | [diff] [blame] | 127 | ---------- ------------------------------------------- |
| 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'Ascenzio | fb24214 | 2020-04-17 15:53:19 +0200 | [diff] [blame] | 138 | r'''(?P<name>.*?) # name |
| 139 | :(?P<type>FILEPATH|PATH|STRING|BOOL|INTERNAL|STATIC) # type |
| 140 | =(?P<value>.*) # value |
Carles Cufi | 31bdad5 | 2019-04-26 21:53:02 +0200 | [diff] [blame] | 141 | ''', 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'Ascenzio | fb24214 | 2020-04-17 15:53:19 +0200 | [diff] [blame] | 191 | elif type_ in {'STRING', 'INTERNAL', 'STATIC'}: |
Carles Cufi | 31bdad5 | 2019-04-26 21:53:02 +0200 | [diff] [blame] | 192 | # 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 | |
| 208 | class 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 Cufi | 9754201 | 2019-07-18 16:44:31 +0200 | [diff] [blame] | 221 | with open(cache_file, 'r', encoding="utf-8") as cache: |
Carles Cufi | 31bdad5 | 2019-04-26 21:53:02 +0200 | [diff] [blame] | 222 | 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 Bolivar | 146580e | 2019-08-25 12:53:24 -0600 | [diff] [blame] | 268 | |
| 269 | def _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ívar | 16a7465 | 2021-02-08 10:02:55 -0800 | [diff] [blame] | 288 | 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 Bolivar | 146580e | 2019-08-25 12:53:24 -0600 | [diff] [blame] | 292 | 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) |