|  | # Copyright (c) 2018 Open Source Foundries Limited. | 
|  | # | 
|  | # SPDX-License-Identifier: Apache-2.0 | 
|  | '''Common definitions for building Zephyr applications with CMake. | 
|  |  | 
|  | This provides some default settings and convenience wrappers for | 
|  | building Zephyr applications needed by multiple commands. | 
|  |  | 
|  | See build.py for the build command itself. | 
|  | ''' | 
|  |  | 
|  | from collections import OrderedDict | 
|  | import argparse | 
|  | import os.path | 
|  | import re | 
|  | import subprocess | 
|  | import shutil | 
|  | import sys | 
|  |  | 
|  | import packaging.version | 
|  | from west import log | 
|  | from west.util import quote_sh_list | 
|  |  | 
|  | DEFAULT_CACHE = 'CMakeCache.txt' | 
|  |  | 
|  | DEFAULT_CMAKE_GENERATOR = 'Ninja' | 
|  | '''Name of the default CMake generator.''' | 
|  |  | 
|  |  | 
|  | def run_cmake(args, cwd=None, capture_output=False, dry_run=False, env=None): | 
|  | '''Run cmake to (re)generate a build system, a script, etc. | 
|  |  | 
|  | :param args: arguments to pass to CMake | 
|  | :param cwd: directory to run CMake in, cwd is default | 
|  | :param capture_output: if True, the output is returned instead of being | 
|  | displayed (None is returned by default, or if | 
|  | dry_run is also True) | 
|  | :param dry_run: don't actually execute the command, just print what | 
|  | would have been run | 
|  | :param env: used adjusted environment when running CMake | 
|  |  | 
|  | If capture_output is set to True, returns the output of the command instead | 
|  | of displaying it on stdout/stderr..''' | 
|  | cmake = shutil.which('cmake') | 
|  | if cmake is None and not dry_run: | 
|  | log.die('CMake is not installed or cannot be found; cannot build.') | 
|  | _ensure_min_version(cmake, dry_run) | 
|  |  | 
|  | cmd = [cmake] + args | 
|  |  | 
|  | kwargs = dict() | 
|  | if capture_output: | 
|  | kwargs['stdout'] = subprocess.PIPE | 
|  | # CMake sends the output of message() to stderr unless it's STATUS | 
|  | kwargs['stderr'] = subprocess.STDOUT | 
|  | if cwd: | 
|  | kwargs['cwd'] = cwd | 
|  |  | 
|  | if dry_run: | 
|  | in_cwd = ' (in {})'.format(cwd) if cwd else '' | 
|  | log.inf('Dry run{}:'.format(in_cwd), quote_sh_list(cmd)) | 
|  | return None | 
|  |  | 
|  | log.dbg('Running CMake:', quote_sh_list(cmd), level=log.VERBOSE_NORMAL) | 
|  | p = subprocess.Popen(cmd, env=env, **kwargs) | 
|  | out, _ = p.communicate() | 
|  | if p.returncode == 0: | 
|  | if out: | 
|  | return out.decode(sys.getdefaultencoding()).splitlines() | 
|  | else: | 
|  | return None | 
|  | else: | 
|  | # A real error occurred, raise an exception | 
|  | raise subprocess.CalledProcessError(p.returncode, p.args) | 
|  |  | 
|  |  | 
|  | def run_build(build_directory, **kwargs): | 
|  | '''Run cmake in build tool mode. | 
|  |  | 
|  | :param build_directory: runs "cmake --build build_directory" | 
|  | :param extra_args: optional kwarg. List of additional CMake arguments; | 
|  | these come after "--build <build_directory>" | 
|  | on the command line. | 
|  |  | 
|  | Any additional keyword arguments are passed as-is to run_cmake(). | 
|  | ''' | 
|  | cmake_env = None | 
|  | extra_args = kwargs.pop('extra_args', []) | 
|  |  | 
|  | try: | 
|  | index = extra_args.index('--') + 1 | 
|  | build_opt_parser = argparse.ArgumentParser(allow_abbrev=False) | 
|  | build_opt_parser.add_argument('-j', '--jobs') | 
|  | build_opt_parser.add_argument('-v', '--verbose', action='store_true') | 
|  | build_opts, native_args = build_opt_parser.parse_known_args(extra_args[index:]) | 
|  | extra_args = extra_args[:index] + native_args | 
|  |  | 
|  | if build_opts: | 
|  | cmake_env = os.environ.copy() | 
|  | if build_opts.jobs: | 
|  | cmake_env["CMAKE_BUILD_PARALLEL_LEVEL"] = build_opts.jobs | 
|  |  | 
|  | if build_opts.verbose: | 
|  | cmake_env["VERBOSE"] = "1" | 
|  |  | 
|  | except ValueError: | 
|  | pass # Ignore, no presence of '--' so nothing to do. | 
|  |  | 
|  | return run_cmake(['--build', build_directory] + extra_args, env=cmake_env, **kwargs) | 
|  |  | 
|  |  | 
|  | def make_c_identifier(string): | 
|  | '''Make a C identifier from a string in the same way CMake does. | 
|  | ''' | 
|  | # The behavior of CMake's string(MAKE_C_IDENTIFIER ...)  is not | 
|  | # precisely documented. This behavior matches the test case | 
|  | # that introduced the function: | 
|  | # | 
|  | # https://gitlab.kitware.com/cmake/cmake/commit/0ab50aea4c4d7099b339fb38b4459d0debbdbd85 | 
|  | ret = [] | 
|  |  | 
|  | alpha_under = re.compile('[A-Za-z_]') | 
|  | alpha_num_under = re.compile('[A-Za-z0-9_]') | 
|  |  | 
|  | if not alpha_under.match(string): | 
|  | ret.append('_') | 
|  | for c in string: | 
|  | if alpha_num_under.match(c): | 
|  | ret.append(c) | 
|  | else: | 
|  | ret.append('_') | 
|  |  | 
|  | return ''.join(ret) | 
|  |  | 
|  |  | 
|  | class CMakeCacheEntry: | 
|  | '''Represents a CMake cache entry. | 
|  |  | 
|  | This class understands the type system in a CMakeCache.txt, and | 
|  | converts the following cache types to Python types: | 
|  |  | 
|  | Cache Type    Python type | 
|  | ----------    ------------------------------------------- | 
|  | FILEPATH      str | 
|  | PATH          str | 
|  | STRING        str OR list of str (if ';' is in the value) | 
|  | BOOL          bool | 
|  | INTERNAL      str OR list of str (if ';' is in the value) | 
|  | STATIC        str OR list of str (if ';' is in the value) | 
|  | UNINITIALIZED str OR list of str (if ';' is in the value) | 
|  | ----------    ------------------------------------------- | 
|  | ''' | 
|  |  | 
|  | # Regular expression for a cache entry. | 
|  | # | 
|  | # CMake variable names can include escape characters, allowing a | 
|  | # wider set of names than is easy to match with a regular | 
|  | # expression. To be permissive here, use a non-greedy match up to | 
|  | # the first colon (':'). This breaks if the variable name has a | 
|  | # colon inside, but it's good enough. | 
|  | CACHE_ENTRY = re.compile( | 
|  | r'''(?P<name>.*?)                                                   # name | 
|  | :(?P<type>FILEPATH|PATH|STRING|BOOL|INTERNAL|STATIC|UNINITIALIZED) # type | 
|  | =(?P<value>.*)                                                     # value | 
|  | ''', re.X) | 
|  |  | 
|  | @classmethod | 
|  | def _to_bool(cls, val): | 
|  | # Convert a CMake BOOL string into a Python bool. | 
|  | # | 
|  | #   "True if the constant is 1, ON, YES, TRUE, Y, or a | 
|  | #   non-zero number. False if the constant is 0, OFF, NO, | 
|  | #   FALSE, N, IGNORE, NOTFOUND, the empty string, or ends in | 
|  | #   the suffix -NOTFOUND. Named boolean constants are | 
|  | #   case-insensitive. If the argument is not one of these | 
|  | #   constants, it is treated as a variable." | 
|  | # | 
|  | # https://cmake.org/cmake/help/v3.0/command/if.html | 
|  | val = val.upper() | 
|  | if val in ('ON', 'YES', 'TRUE', 'Y'): | 
|  | return True | 
|  | elif val in ('OFF', 'NO', 'FALSE', 'N', 'IGNORE', 'NOTFOUND', ''): | 
|  | return False | 
|  | elif val.endswith('-NOTFOUND'): | 
|  | return False | 
|  | else: | 
|  | try: | 
|  | v = int(val) | 
|  | return v != 0 | 
|  | except ValueError as exc: | 
|  | raise ValueError('invalid bool {}'.format(val)) from exc | 
|  |  | 
|  | @classmethod | 
|  | def from_line(cls, line, line_no): | 
|  | # Comments can only occur at the beginning of a line. | 
|  | # (The value of an entry could contain a comment character). | 
|  | if line.startswith('//') or line.startswith('#'): | 
|  | return None | 
|  |  | 
|  | # Whitespace-only lines do not contain cache entries. | 
|  | if not line.strip(): | 
|  | return None | 
|  |  | 
|  | m = cls.CACHE_ENTRY.match(line) | 
|  | if not m: | 
|  | return None | 
|  |  | 
|  | name, type_, value = (m.group(g) for g in ('name', 'type', 'value')) | 
|  | if type_ == 'BOOL': | 
|  | try: | 
|  | value = cls._to_bool(value) | 
|  | except ValueError as exc: | 
|  | args = exc.args + ('on line {}: {}'.format(line_no, line),) | 
|  | raise ValueError(args) from exc | 
|  | elif type_ in {'STRING', 'INTERNAL', 'STATIC', 'UNINITIALIZED'}: | 
|  | # If the value is a CMake list (i.e. is a string which | 
|  | # contains a ';'), convert to a Python list. | 
|  | if ';' in value: | 
|  | value = value.split(';') | 
|  |  | 
|  | return CMakeCacheEntry(name, value) | 
|  |  | 
|  | def __init__(self, name, value): | 
|  | self.name = name | 
|  | self.value = value | 
|  |  | 
|  | def __str__(self): | 
|  | fmt = 'CMakeCacheEntry(name={}, value={})' | 
|  | return fmt.format(self.name, self.value) | 
|  |  | 
|  |  | 
|  | class CMakeCache: | 
|  | '''Parses and represents a CMake cache file.''' | 
|  |  | 
|  | @staticmethod | 
|  | def from_build_dir(build_dir): | 
|  | return CMakeCache(os.path.join(build_dir, DEFAULT_CACHE)) | 
|  |  | 
|  | def __init__(self, cache_file): | 
|  | self.cache_file = cache_file | 
|  | self.load(cache_file) | 
|  |  | 
|  | def load(self, cache_file): | 
|  | entries = [] | 
|  | with open(cache_file, 'r', encoding="utf-8") as cache: | 
|  | for line_no, line in enumerate(cache): | 
|  | entry = CMakeCacheEntry.from_line(line, line_no) | 
|  | if entry: | 
|  | entries.append(entry) | 
|  | self._entries = OrderedDict((e.name, e) for e in entries) | 
|  |  | 
|  | def get(self, name, default=None): | 
|  | entry = self._entries.get(name) | 
|  | if entry is not None: | 
|  | return entry.value | 
|  | else: | 
|  | return default | 
|  |  | 
|  | def get_list(self, name, default=None): | 
|  | if default is None: | 
|  | default = [] | 
|  | entry = self._entries.get(name) | 
|  | if entry is not None: | 
|  | value = entry.value | 
|  | if isinstance(value, list): | 
|  | return value | 
|  | elif isinstance(value, str): | 
|  | return [value] if value else [] | 
|  | else: | 
|  | msg = 'invalid value {} type {}' | 
|  | raise RuntimeError(msg.format(value, type(value))) | 
|  | else: | 
|  | return default | 
|  |  | 
|  | def __contains__(self, name): | 
|  | return name in self._entries | 
|  |  | 
|  | def __getitem__(self, name): | 
|  | return self._entries[name].value | 
|  |  | 
|  | def __setitem__(self, name, entry): | 
|  | if not isinstance(entry, CMakeCacheEntry): | 
|  | msg = 'improper type {} for value {}, expecting CMakeCacheEntry' | 
|  | raise TypeError(msg.format(type(entry), entry)) | 
|  | self._entries[name] = entry | 
|  |  | 
|  | def __delitem__(self, name): | 
|  | del self._entries[name] | 
|  |  | 
|  | def __iter__(self): | 
|  | return iter(self._entries.values()) | 
|  |  | 
|  | def _ensure_min_version(cmake, dry_run): | 
|  | cmd = [cmake, '--version'] | 
|  | if dry_run: | 
|  | log.inf('Dry run:', quote_sh_list(cmd)) | 
|  | return | 
|  |  | 
|  | try: | 
|  | version_out = subprocess.check_output(cmd, stderr=subprocess.DEVNULL) | 
|  | except subprocess.CalledProcessError as cpe: | 
|  | log.die('cannot get cmake version:', str(cpe)) | 
|  | decoded = version_out.decode('utf-8') | 
|  | lines = decoded.splitlines() | 
|  | if not lines: | 
|  | log.die('can\'t get cmake version: ' + | 
|  | 'unexpected "cmake --version" output:\n{}\n'. | 
|  | format(decoded) + | 
|  | 'Please install CMake ' + _MIN_CMAKE_VERSION_STR + | 
|  | ' or higher (https://cmake.org/download/).') | 
|  | version = lines[0].split()[2] | 
|  | if '-' in version: | 
|  | # Handle semver cases like "3.19.20210206-g1e50ab6" | 
|  | # which Kitware uses for prerelease versions. | 
|  | version = version.split('-', 1)[0] | 
|  | if packaging.version.parse(version) < _MIN_CMAKE_VERSION: | 
|  | log.die('cmake version', version, | 
|  | 'is less than minimum version {};'. | 
|  | format(_MIN_CMAKE_VERSION_STR), | 
|  | 'please update your CMake (https://cmake.org/download/).') | 
|  | else: | 
|  | log.dbg('cmake version', version, 'is OK; minimum version is', | 
|  | _MIN_CMAKE_VERSION_STR) | 
|  |  | 
|  | _MIN_CMAKE_VERSION_STR = '3.13.1' | 
|  | _MIN_CMAKE_VERSION = packaging.version.parse(_MIN_CMAKE_VERSION_STR) |