blob: e6594c2f20d501bfa7bfecebb9b4f43837ce74f4 [file] [log] [blame]
Carles Cufi31bdad52019-04-26 21:53:02 +02001# 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
6This provides some default settings and convenience wrappers for
7building Zephyr applications needed by multiple commands.
8
9See build.py for the build command itself.
10'''
11
12from collections import OrderedDict
13import os.path
14import re
15import subprocess
16import shutil
17import sys
18
19from west import log
20from west.util import quote_sh_list
21
22DEFAULT_CACHE = 'CMakeCache.txt'
23
24DEFAULT_CMAKE_GENERATOR = 'Ninja'
25'''Name of the default CMake generator.'''
26
27def run_cmake(args, cwd=None, capture_output=False):
28 '''Run cmake to (re)generate a build system.
29 If capture_output is set to True, returns the output of the command instead
30 of displaying it on stdout/stderr..'''
31 cmake = shutil.which('cmake')
32 if cmake is None:
33 log.die('CMake is not installed or cannot be found; cannot build.')
34 cmd = [cmake] + args
35 kwargs = dict()
36 if capture_output:
37 kwargs['stdout'] = subprocess.PIPE
38 # CMake sends the output of message() to stderr unless it's STATUS
39 kwargs['stderr'] = subprocess.STDOUT
40 if cwd:
41 kwargs['cwd'] = cwd
42 log.dbg('Running CMake:', quote_sh_list(cmd), level=log.VERBOSE_NORMAL)
43 p = subprocess.Popen(cmd, **kwargs)
44 out, err = p.communicate()
45 if p.returncode == 0:
46 if out:
47 return out.decode(sys.getdefaultencoding()).splitlines()
48 else:
49 return None
50 else:
51 # A real error occurred, raise an exception
52 raise subprocess.CalledProcessError(cmd=p.args,
53 returncode=p.returncode)
54
55
56def run_build(build_directory, extra_args=(), cwd=None, capture_output=False):
57 '''Run cmake in build tool mode in `build_directory`'''
58 run_cmake(['--build', build_directory] + list(extra_args),
59 capture_output=capture_output)
60
61
62def make_c_identifier(string):
63 '''Make a C identifier from a string in the same way CMake does.
64 '''
65 # The behavior of CMake's string(MAKE_C_IDENTIFIER ...) is not
66 # precisely documented. This behavior matches the test case
67 # that introduced the function:
68 #
69 # https://gitlab.kitware.com/cmake/cmake/commit/0ab50aea4c4d7099b339fb38b4459d0debbdbd85
70 ret = []
71
72 alpha_under = re.compile('[A-Za-z_]')
73 alpha_num_under = re.compile('[A-Za-z0-9_]')
74
75 if not alpha_under.match(string):
76 ret.append('_')
77 for c in string:
78 if alpha_num_under.match(c):
79 ret.append(c)
80 else:
81 ret.append('_')
82
83 return ''.join(ret)
84
85
86class CMakeCacheEntry:
87 '''Represents a CMake cache entry.
88
89 This class understands the type system in a CMakeCache.txt, and
90 converts the following cache types to Python types:
91
92 Cache Type Python type
93 ---------- -------------------------------------------
94 FILEPATH str
95 PATH str
96 STRING str OR list of str (if ';' is in the value)
97 BOOL bool
98 INTERNAL str OR list of str (if ';' is in the value)
99 ---------- -------------------------------------------
100 '''
101
102 # Regular expression for a cache entry.
103 #
104 # CMake variable names can include escape characters, allowing a
105 # wider set of names than is easy to match with a regular
106 # expression. To be permissive here, use a non-greedy match up to
107 # the first colon (':'). This breaks if the variable name has a
108 # colon inside, but it's good enough.
109 CACHE_ENTRY = re.compile(
110 r'''(?P<name>.*?) # name
111 :(?P<type>FILEPATH|PATH|STRING|BOOL|INTERNAL) # type
112 =(?P<value>.*) # value
113 ''', re.X)
114
115 @classmethod
116 def _to_bool(cls, val):
117 # Convert a CMake BOOL string into a Python bool.
118 #
119 # "True if the constant is 1, ON, YES, TRUE, Y, or a
120 # non-zero number. False if the constant is 0, OFF, NO,
121 # FALSE, N, IGNORE, NOTFOUND, the empty string, or ends in
122 # the suffix -NOTFOUND. Named boolean constants are
123 # case-insensitive. If the argument is not one of these
124 # constants, it is treated as a variable."
125 #
126 # https://cmake.org/cmake/help/v3.0/command/if.html
127 val = val.upper()
128 if val in ('ON', 'YES', 'TRUE', 'Y'):
129 return True
130 elif val in ('OFF', 'NO', 'FALSE', 'N', 'IGNORE', 'NOTFOUND', ''):
131 return False
132 elif val.endswith('-NOTFOUND'):
133 return False
134 else:
135 try:
136 v = int(val)
137 return v != 0
138 except ValueError as exc:
139 raise ValueError('invalid bool {}'.format(val)) from exc
140
141 @classmethod
142 def from_line(cls, line, line_no):
143 # Comments can only occur at the beginning of a line.
144 # (The value of an entry could contain a comment character).
145 if line.startswith('//') or line.startswith('#'):
146 return None
147
148 # Whitespace-only lines do not contain cache entries.
149 if not line.strip():
150 return None
151
152 m = cls.CACHE_ENTRY.match(line)
153 if not m:
154 return None
155
156 name, type_, value = (m.group(g) for g in ('name', 'type', 'value'))
157 if type_ == 'BOOL':
158 try:
159 value = cls._to_bool(value)
160 except ValueError as exc:
161 args = exc.args + ('on line {}: {}'.format(line_no, line),)
162 raise ValueError(args) from exc
163 elif type_ == 'STRING' or type_ == 'INTERNAL':
164 # If the value is a CMake list (i.e. is a string which
165 # contains a ';'), convert to a Python list.
166 if ';' in value:
167 value = value.split(';')
168
169 return CMakeCacheEntry(name, value)
170
171 def __init__(self, name, value):
172 self.name = name
173 self.value = value
174
175 def __str__(self):
176 fmt = 'CMakeCacheEntry(name={}, value={})'
177 return fmt.format(self.name, self.value)
178
179
180class CMakeCache:
181 '''Parses and represents a CMake cache file.'''
182
183 @staticmethod
184 def from_build_dir(build_dir):
185 return CMakeCache(os.path.join(build_dir, DEFAULT_CACHE))
186
187 def __init__(self, cache_file):
188 self.cache_file = cache_file
189 self.load(cache_file)
190
191 def load(self, cache_file):
192 entries = []
193 with open(cache_file, 'r') as cache:
194 for line_no, line in enumerate(cache):
195 entry = CMakeCacheEntry.from_line(line, line_no)
196 if entry:
197 entries.append(entry)
198 self._entries = OrderedDict((e.name, e) for e in entries)
199
200 def get(self, name, default=None):
201 entry = self._entries.get(name)
202 if entry is not None:
203 return entry.value
204 else:
205 return default
206
207 def get_list(self, name, default=None):
208 if default is None:
209 default = []
210 entry = self._entries.get(name)
211 if entry is not None:
212 value = entry.value
213 if isinstance(value, list):
214 return value
215 elif isinstance(value, str):
216 return [value] if value else []
217 else:
218 msg = 'invalid value {} type {}'
219 raise RuntimeError(msg.format(value, type(value)))
220 else:
221 return default
222
223 def __contains__(self, name):
224 return name in self._entries
225
226 def __getitem__(self, name):
227 return self._entries[name].value
228
229 def __setitem__(self, name, entry):
230 if not isinstance(entry, CMakeCacheEntry):
231 msg = 'improper type {} for value {}, expecting CMakeCacheEntry'
232 raise TypeError(msg.format(type(entry), entry))
233 self._entries[name] = entry
234
235 def __delitem__(self, name):
236 del self._entries[name]
237
238 def __iter__(self):
239 return iter(self._entries.values())