Browse Source
Move the existing CMake and build functionality from the west repository to zephyr. The rationale behind this move is that it's very tightly coupled with the Zephyr build system and is only used by the extension commands implemented in the zephyr tree. If additional extension commands in third-party repos want to use the functionality they can add $ZEPHYR_BASE/scripts/west_commands to the Python system path. The implmentations in the west repo will be deprecated. Signed-off-by: Carles Cufi <carles.cufi@nordicsemi.no>pull/15894/head
4 changed files with 311 additions and 35 deletions
@ -0,0 +1,65 @@
@@ -0,0 +1,65 @@
|
||||
# Copyright 2018 (c) Foundries.io. |
||||
# |
||||
# SPDX-License-Identifier: Apache-2.0 |
||||
|
||||
'''Common definitions for building Zephyr applications. |
||||
|
||||
This provides some default settings and convenience wrappers for |
||||
building Zephyr applications needed by multiple commands. |
||||
|
||||
See build.py for the build command itself. |
||||
''' |
||||
|
||||
import cmake |
||||
import os |
||||
from west import log |
||||
|
||||
DEFAULT_BUILD_DIR = 'build' |
||||
'''Name of the default Zephyr build directory.''' |
||||
|
||||
DEFAULT_CMAKE_GENERATOR = 'Ninja' |
||||
'''Name of the default CMake generator.''' |
||||
|
||||
BUILD_DIR_DESCRIPTION = '''\ |
||||
Explicitly sets the build directory. If not given and the current |
||||
directory is a Zephyr build directory, it will be used; otherwise, |
||||
"{}" is assumed.'''.format(DEFAULT_BUILD_DIR) |
||||
|
||||
|
||||
def find_build_dir(dir): |
||||
'''Heuristic for finding a build directory. |
||||
|
||||
If the given argument is truthy, it is returned. Otherwise, if |
||||
the current working directory is a build directory, it is |
||||
returned. Otherwise, DEFAULT_BUILD_DIR is returned.''' |
||||
if dir: |
||||
build_dir = dir |
||||
else: |
||||
cwd = os.getcwd() |
||||
if is_zephyr_build(cwd): |
||||
build_dir = cwd |
||||
else: |
||||
build_dir = DEFAULT_BUILD_DIR |
||||
return os.path.abspath(build_dir) |
||||
|
||||
|
||||
def is_zephyr_build(path): |
||||
'''Return true if and only if `path` appears to be a valid Zephyr |
||||
build directory. |
||||
|
||||
"Valid" means the given path is a directory which contains a CMake |
||||
cache with a 'ZEPHYR_TOOLCHAIN_VARIANT' key. |
||||
''' |
||||
try: |
||||
cache = cmake.CMakeCache.from_build_dir(path) |
||||
except FileNotFoundError: |
||||
cache = {} |
||||
|
||||
if 'ZEPHYR_TOOLCHAIN_VARIANT' in cache: |
||||
log.dbg('{} is a zephyr build directory'.format(path), |
||||
level=log.VERBOSE_EXTREME) |
||||
return True |
||||
else: |
||||
log.dbg('{} is NOT a valid zephyr build directory'.format(path), |
||||
level=log.VERBOSE_EXTREME) |
||||
return False |
@ -0,0 +1,239 @@
@@ -0,0 +1,239 @@
|
||||
# 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 os.path |
||||
import re |
||||
import subprocess |
||||
import shutil |
||||
import sys |
||||
|
||||
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): |
||||
'''Run cmake to (re)generate a build system. |
||||
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: |
||||
log.die('CMake is not installed or cannot be found; cannot build.') |
||||
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 |
||||
log.dbg('Running CMake:', quote_sh_list(cmd), level=log.VERBOSE_NORMAL) |
||||
p = subprocess.Popen(cmd, **kwargs) |
||||
out, err = 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(cmd=p.args, |
||||
returncode=p.returncode) |
||||
|
||||
|
||||
def run_build(build_directory, extra_args=(), cwd=None, capture_output=False): |
||||
'''Run cmake in build tool mode in `build_directory`''' |
||||
run_cmake(['--build', build_directory] + list(extra_args), |
||||
capture_output=capture_output) |
||||
|
||||
|
||||
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 |
||||
# 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) # 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_ == '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()) |
Loading…
Reference in new issue