-
Notifications
You must be signed in to change notification settings - Fork 6
/
Copy pathcmake.py
294 lines (227 loc) · 10.5 KB
/
cmake.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0.
import os
import pprint
import re
import shutil
from collections import defaultdict
from functools import lru_cache, partial
from pathlib import Path
from builder.core.action import Action
from builder.core.toolchain import Toolchain
from builder.core.util import UniqueList, run_command
@lru_cache(1)
def _find_cmake():
for cmake_alias in ['cmake3', 'cmake']:
cmake = shutil.which(cmake_alias)
if cmake:
return cmake
raise Exception("cmake not found")
@lru_cache(1)
def _find_ctest():
for ctest_alias in ['ctest3', 'ctest']:
ctest = shutil.which(ctest_alias)
if ctest:
return ctest
raise Exception("cmake not found")
def cmake_path(cross_compile=False):
if cross_compile:
return 'cmake'
return _find_cmake()
def cmake_version(cross_compile=False):
if cross_compile:
return '3.17.1'
output = run_command([cmake_path(), '--version'], quiet=True).output
m = re.match(r'cmake(3?) version ([\d\.])', output)
if m:
return m.group(2)
return None
def cmake_binary(cross_compile=False):
if cross_compile:
return 'cmake'
return os.path.basename(_find_cmake())
def ctest_binary(cross_compile):
if cross_compile:
return 'ctest'
return os.path.basename(_find_ctest())
def _cmake_path(self):
return cmake_path(self.cross_compile)
def _cmake_version(self):
return cmake_version(self.cross_compile)
def _cmake_binary(self):
return cmake_binary(self.cross_compile)
def _ctest_binary(self):
return ctest_binary(self.cross_compile)
Toolchain.cmake_path = _cmake_path
Toolchain.cmake_version = _cmake_version
Toolchain.cmake_binary = _cmake_binary
Toolchain.ctest_binary = _ctest_binary
def _project_dirs(env, project):
if not project.resolved():
raise Exception('Project is not resolved: {}'.format(project.name))
source_dir = project.path
build_dir = os.path.join(env.build_dir, project.name)
install_dir = env.install_dir
# cross compiles are effectively chrooted to the source_dir, normal builds need absolute paths
# or cmake gets lost because it wants directories relative to source
if env.toolchain.cross_compile:
# all dirs used should be relative to env.source_dir, as this is where the cross
# compilation will be mounting to do its work
source_dir = str(Path(source_dir).relative_to(env.root_dir))
build_dir = str(Path(build_dir).relative_to(env.root_dir))
install_dir = str(Path(install_dir).relative_to(env.root_dir))
return source_dir, build_dir, install_dir
def _merge_cmake_lang_flags(cmake_args):
print("=== _merge_cmake_lang_flags: cmake args")
pprint.pprint(cmake_args, indent=4, depth=4)
pattern = re.compile(r'''-D(CMAKE_C(?:XX)?_FLAGS)=["']?([^"']+)''')
new_cmake_args = []
cmake_lang_flags = defaultdict(list)
for arg in cmake_args:
m = pattern.match(arg)
if m:
cmake_lang_flags[m.group(1)].append(m.group(2))
else:
new_cmake_args.append(arg)
pprint.pprint(cmake_lang_flags, indent=4, depth=4)
for (k, v) in cmake_lang_flags.items():
new_cmake_args.append('-D{}={}'.format(k, ' '.join(v)))
return new_cmake_args
def _build_project(env, project, cmake_extra, build_tests=False, args_transformer=None, coverage=False):
sh = env.shell
config = project.get_config(env.spec)
build_env = []
toolchain = env.toolchain
if toolchain.cross_compile and 'go_path' in env.variables:
# We need to set the envrionment variable of GO_PATH for cross compile
build_env = ["GO_PATH={}\n".format(env.variables['go_path'])]
# build dependencies first, let cmake decide what needs doing
for dep in project.get_dependencies(env.spec):
_build_project(env, dep, cmake_extra)
project_source_dir, project_build_dir, project_install_dir = _project_dirs(
env, project)
abs_project_build_dir = project_build_dir
if not os.path.isabs(project_build_dir):
abs_project_build_dir = os.path.join(env.root_dir, project_build_dir)
sh.mkdir(abs_project_build_dir)
# If cmake has already run, assume we're good
if os.path.isfile(os.path.join(abs_project_build_dir, 'CMakeCache.txt')):
return
cmake = toolchain.cmake_binary()
cmake_version = toolchain.cmake_version()
assert cmake_version != None
# TODO These platforms don't succeed when doing a RelWithDebInfo build
build_config = env.args.config
if toolchain.host in ("al2012", "manylinux"):
build_config = "Debug"
# Set compiler flags
compiler_flags = []
c_path = None
if toolchain.compiler != 'default' and toolchain.compiler != 'msvc' and not toolchain.cross_compile:
c_path = toolchain.compiler_path()
cxx_path = toolchain.cxx_compiler_path()
for opt, value in [('c', c_path), ('cxx', cxx_path)]:
if value:
compiler_flags.append(
'-DCMAKE_{}_COMPILER={}'.format(opt.upper(), value))
cmake_args = UniqueList([
"-B{}".format(project_build_dir),
"-H{}".format(project_source_dir),
"-DAWS_WARNINGS_ARE_ERRORS=ON",
"-DPERFORM_HEADER_CHECK=ON",
"-DCMAKE_INSTALL_PREFIX=" + project_install_dir,
"-DCMAKE_PREFIX_PATH=" + project_install_dir,
"-DCMAKE_EXPORT_COMPILE_COMMANDS=ON",
"-DCMAKE_BUILD_TYPE=" + build_config,
"-DBUILD_TESTING=" + ("ON" if build_tests else "OFF"),
"--no-warn-unused-cli",
*compiler_flags,
])
# Merging in cmake_args from all upstream projects inevitably leads to duplicate arguments.
# Using a UniqueList seems to solve the problem well enough for now.
# TODO: this can lead to unpredictable results for cases where same flag is
# set on and off multiple times. ex. Flag A is added to args with value On,
# Off, On. With UniqueList last On will be removed and cmake will treat flag
# as off. Without UniqueList cmake will treat it as on.
cmake_args += project.cmake_args(env)
cmake_args += cmake_extra
cmake_args += ['-DCMAKE_VERBOSE_MAKEFILE=ON']
if coverage:
if c_path and "gcc" in c_path:
# Tell cmake to add coverage related configuration. And make sure GCC is used to compile the project.
# CMAKE_C_FLAGS for GCC to enable code coverage information.
# COVERAGE_EXTRA_FLAGS="*" is configuration for gcov
# --preserve-paths: to include path information in the report file name
# --source-prefix `pwd`: to exclude the `pwd` from the file name
cmake_args += [
"-DCMAKE_C_FLAGS=-fprofile-arcs -ftest-coverage",
"-DCOVERAGE_EXTRA_FLAGS=--preserve-paths --source-prefix `pwd`"
]
else:
raise Exception('--coverage only support GCC as compiler. Current compiler is: {}'.format(c_path))
# If there are multiple of the same -DCMAKE_<LANG>_FLAGS arguments, CMake takes only the last one.
# Since -DCMAKE_<LANG>_FLAGS can be set in multiple places (e.g. in a default config for a specific platform or
# compiler, in a user project's config, in this Python module, etc.), we should merge language flags into one per
# language.
cmake_args = _merge_cmake_lang_flags(cmake_args)
print("=== _build_project: cmake_args: {}".format(cmake_args))
# Allow caller to programmatically tweak the cmake_args,
# as a last resort in case data merging wasn't working out
if args_transformer:
cmake_args = args_transformer(env, project, cmake_args)
# When cross compiling, we must inject the build_env into the cross compile container
if toolchain.cross_compile:
build_env = build_env + ['{}={}\n'.format(key, val)
for key, val in config.get('build_env', {}).items()]
with open(toolchain.env_file, 'a') as f:
f.writelines(build_env)
# set parallism via env var (cmake's --parallel CLI option doesn't exist until 3.12)
if os.environ.get('CMAKE_BUILD_PARALLEL_LEVEL') is None:
sh.setenv('CMAKE_BUILD_PARALLEL_LEVEL', str(os.cpu_count()))
working_dir = env.root_dir if toolchain.cross_compile else os.getcwd()
# configure
sh.exec(*toolchain.shell_env, cmake, cmake_args, working_dir=working_dir, check=True)
print("=== toolchain.host is {}".format(toolchain.host))
# build & install
sh.exec(*toolchain.shell_env, cmake, "--build", project_build_dir, "--config",
build_config, "--target", "install", working_dir=working_dir, check=True)
class CMakeBuild(Action):
""" Runs cmake configure, build """
def __init__(self, project, *, args_transformer=None):
self.project = project
self.args_transformer = args_transformer
def run(self, env):
sh = env.shell
for d in (env.build_dir, env.deps_dir, env.install_dir):
sh.mkdir(d)
# BUILD
build_tests = self.project.needs_tests(env)
_build_project(env, self.project, env.args.cmake_extra, build_tests, self.args_transformer, env.args.coverage)
def __str__(self):
return 'cmake build {} @ {}'.format(self.project.name, self.project.path)
class CTestRun(Action):
""" Uses ctest to run tests if tests are enabled/built via 'build_tests' """
def __init__(self, project):
self.project = project
def run(self, env):
sh = env.shell
toolchain = env.toolchain
if not self.project.needs_tests(env):
print('Tests not needed for project. Skipping')
return
if toolchain.cross_compile:
print('WARNING: Running tests for cross compile is not yet supported')
return
project_source_dir, project_build_dir, project_install_dir = _project_dirs(
env, self.project)
if not os.path.isdir(project_build_dir):
print("No build dir found, skipping CTest")
return
ctest = toolchain.ctest_binary()
sh.exec(*toolchain.shell_env, ctest,
"--output-on-failure", working_dir=project_build_dir, check=True)
# Try to generate the coverage report. Will be ignored by ctest if no coverage data available.
sh.exec(*toolchain.shell_env, ctest,
"-T", "coverage", working_dir=project_build_dir, check=True)
def __str__(self):
return 'ctest {} @ {}'.format(self.project.name, self.project.path)