Skip to content
Permalink
Browse files

west: Implement CMake helpers in scripts/west_commands

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>
  • Loading branch information...
carlescufi committed Apr 26, 2019
1 parent af3f81e commit 31bdad52990103f1c02324976763b6655d35da61
@@ -8,11 +8,11 @@
import subprocess

from west import log
from west import cmake
from west.configuration import config
from west.build import DEFAULT_CMAKE_GENERATOR, is_zephyr_build
from cmake import DEFAULT_CMAKE_GENERATOR, run_cmake, run_build, CMakeCache
from build_helpers import is_zephyr_build, find_build_dir, BUILD_DIR_DESCRIPTION

from zephyr_ext_common import find_build_dir, Forceable, BUILD_DIR_DESCRIPTION
from zephyr_ext_common import Forceable

_ARG_SEPARATOR = '--'

@@ -221,7 +221,7 @@ def _sanity_precheck(self):

def _update_cache(self):
try:
self.cmake_cache = cmake.CMakeCache.from_build_dir(self.build_dir)
self.cmake_cache = CMakeCache.from_build_dir(self.build_dir)
except FileNotFoundError:
pass

@@ -371,7 +371,7 @@ def _run_cmake(self, cmake_opts):
final_cmake_args.append('-DBOARD={}'.format(self.args.board))
if cmake_opts:
final_cmake_args.extend(cmake_opts)
cmake.run_cmake(final_cmake_args)
run_cmake(final_cmake_args)

def _run_pristine(self):
log.inf('Making build dir {} pristine'.format(self.build_dir))
@@ -386,13 +386,8 @@ def _run_pristine(self):
'build system')

cmake_args = ['-P', '{}/cmake/pristine.cmake'.format(zb)]
cmake = shutil.which('cmake')
if cmake is None:
log.die('CMake is not installed or cannot be found; cannot make '
'the build folder pristine')
cmd = [cmake] + cmake_args
subprocess.check_call(cmd, cwd=self.build_dir)
run_cmake(cmake_args, cwd=self.build_dir)

def _run_build(self, target):
extra_args = ['--target', target] if target else []
cmake.run_build(self.build_dir, extra_args=extra_args)
run_build(self.build_dir, extra_args=extra_args)
@@ -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 @@
# 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())
@@ -11,33 +11,10 @@
import os

from west import log
from west.build import DEFAULT_BUILD_DIR, is_zephyr_build
from west.commands import WestCommand

from runners.core import RunnerConfig

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, west.build.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)


class Forceable(WestCommand):
'''WestCommand subclass for commands with a --force option.'''

0 comments on commit 31bdad5

Please sign in to comment.
You can’t perform that action at this time.