# Copyright (c) 2018 Open Source Foundries Limited. # # SPDX-License-Identifier: Apache-2.0 '''Helpers for dealing with CMake''' from collections import OrderedDict import os.path import re import subprocess import shutil from west import log from west.util import quote_sh_list __all__ = ['run_cmake', 'run_build', 'make_c_identifier', 'CMakeCacheEntry', 'CMakeCache'] DEFAULT_CACHE = 'CMakeCache.txt' def run_cmake(args, quiet=False): '''Run cmake to (re)generate a build system''' cmake = shutil.which('cmake') if cmake is None: log.die('CMake is not installed or cannot be found; cannot build.') cmd = [cmake] + args kwargs = dict() if quiet: kwargs['stdout'] = subprocess.DEVNULL kwargs['stderr'] = subprocess.STDOUT log.dbg('Running CMake:', cmd, level=log.VERBOSE_VERY) log.dbg('As command:', quote_sh_list(cmd), level=log.VERBOSE_VERY) subprocess.check_call(cmd, **kwargs) def run_build(build_directory, extra_args=(), quiet=False): '''Run cmake in build tool mode in `build_directory`''' run_cmake(['--build', build_directory] + list(extra_args), quiet=quiet) 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) ---------- ------------------------------------------- ''' # 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 # expresion. 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 :(?PFILEPATH|PATH|STRING|BOOL|INTERNAL) # type =(?P.*) # 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_ == 'STRING' or type_ == 'INTERNAL': # 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') 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())