-
Notifications
You must be signed in to change notification settings - Fork 37
Expand file tree
/
Copy pathbuild.py
More file actions
311 lines (236 loc) · 9.2 KB
/
Copy pathbuild.py
File metadata and controls
311 lines (236 loc) · 9.2 KB
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
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
#!/usr/bin/env python
"""
build.py makes building projects with CMake or Meson + Ninja even simpler.
It facilitates easy testing across operating systems and compiler vendors.
Michael Hirsch, Ph.D.
## Per-compiler tips
### PGI
PATH must include the PGI compilers bin/ directory before running build.py.
### Intel
The Intel compiler environment must be configured before running build.py:
* Windows: compilervars.bat intel64
* Linux / Mac: source compilervars.sh intel64
"""
from pathlib import Path
import os
import sys
import shutil
import subprocess
from typing import Dict, List, Tuple
from argparse import ArgumentParser
if sys.version_info < (3, 6):
raise RuntimeError('build.py requires Python >= 3.6')
MESON = shutil.which('meson')
NINJA = shutil.which('ninja')
CMAKE = shutil.which('cmake')
CTEST = shutil.which('ctest')
MSVC = 'Visual Studio 15 2017'
# Must have .resolve() to work in general regardless of invocation directory
SRC = Path(__file__).parent.resolve()
BUILD = SRC / 'build'
# %% function
def do_build(buildsys: str, compilers: Dict[str, str],
args: List[str],
wipe: bool = True,
dotest: bool = True,
install: str = None):
"""
attempts build with Meson or CMake
"""
if buildsys == 'meson' and MESON and NINJA:
meson_setup(compilers, args, wipe, dotest, install)
elif buildsys == 'cmake' and CMAKE:
cmake_setup(compilers, args, wipe, dotest, install)
else:
raise FileNotFoundError('Could not find CMake or Meson + Ninja')
def _needs_wipe(fn: Path, wipe: bool) -> bool:
"""
This detection of regeneration needed is not perfect.
"""
if not fn.is_file():
return False
if wipe:
return True
with fn.open() as f:
for line in f:
if line.startswith('CMAKE_C_COMPILER:FILEPATH'):
cc = line.split('/')[-1].strip() # must have strip() for junk in cache
if cc != compilers['CC']:
print('regenerating due to C compiler change:', cc, '=>', compilers['CC'])
wipe = True
break
elif line.startswith('CMAKE_GENERATOR:INTERNAL'):
gen = line.split('=')[-1]
if gen.startswith('Unix') and os.name == 'nt':
print('regenerating due to OS change: Unix => Windows')
wipe = True
break
elif gen.startswith(('MinGW', 'Visual')) and os.name != 'nt':
print('regenerating due to OS change: Windows => Unix')
wipe = True
break
elif gen.startswith('Visual') and compilers['CC'] != 'cl':
print('regenerating due to C compiler change: MSVC =>', compilers['CC'])
wipe = True
break
return wipe
def cmake_setup(compilers: Dict[str, str],
args: List[str],
wipe: bool = True, dotest: bool = True,
install: str = None):
"""
attempt to build using CMake >= 3
"""
if compilers['CC'] == 'cl':
wopts = ['-G', MSVC, '-A', 'x64']
elif os.name == 'nt':
wopts = ['-G', 'MinGW Makefiles', '-DCMAKE_SH="CMAKE_SH-NOTFOUND']
else:
wopts = []
wopts += args
if isinstance(install, str) and install.strip(): # path specified
wopts.append('-DCMAKE_INSTALL_PREFIX:PATH='+str(Path(install).expanduser()))
cachefile = BUILD / 'CMakeCache.txt'
if _needs_wipe(cachefile, wipe):
cachefile.unlink()
shutil.rmtree(BUILD/'CMakeFiles', ignore_errors=True)
# we didn't use -S -B to be compatible with CMake < 3.12
ret = subprocess.run([CMAKE] + wopts + [str(SRC)],
cwd=BUILD, env=os.environ.update(compilers))
if ret.returncode:
raise SystemExit(ret.returncode)
ret = subprocess.run([CMAKE, '--build', str(BUILD), '--parallel'])
test_result(ret)
# %% test
_cmake_test(dotest)
# %% install
if install is not None: # blank '' or ' ' etc. will use dfault install path
subprocess.run([CMAKE, '--build', str(BUILD), '--parallel', '--target', 'install'])
if ret.returncode:
raise SystemExit(ret.returncode)
def _cmake_test(dotest: bool):
if not dotest:
return
if not CTEST:
raise FileNotFoundError('CTest not available')
if compilers['CC'] == 'cl':
ret = subprocess.run([CMAKE, '--build', str(BUILD), '--target', 'RUN_TESTS'])
if ret.returncode:
raise SystemExit(ret.returncode)
else:
ret = subprocess.run([CTEST, '--parallel', '--output-on-failure'], cwd=BUILD)
if ret.returncode:
raise SystemExit(ret.returncode)
def meson_setup(compilers: Dict[str, str],
args: List[str],
wipe: bool = True, dotest: bool = True,
install: str = None):
"""
attempt to build with Meson + Ninja
"""
build_ninja = BUILD / 'build.ninja'
meson_setup = [MESON] + ['setup'] + args
if isinstance(install, str) and install.strip(): # path specified
meson_setup.append('--prefix '+str(Path(install).expanduser()))
if wipe and build_ninja.is_file():
meson_setup.append('--wipe')
meson_setup += [str(BUILD), str(SRC)]
if wipe or not build_ninja.is_file():
ret = subprocess.run(meson_setup, env=os.environ.update(compilers))
if ret.returncode:
raise SystemExit(ret.returncode)
ret = subprocess.run([NINJA, '-C', str(BUILD)])
test_result(ret)
if dotest:
if not ret.returncode:
ret = subprocess.run([MESON, 'test', '-C', str(BUILD)]) # type: ignore # MyPy bug
if ret.returncode:
raise SystemExit(ret.returncode)
if install:
if not ret.returncode:
ret = subprocess.run([MESON, 'install', '-C', str(BUILD)]) # type: ignore # MyPy bug
if ret.returncode:
raise SystemExit(ret.returncode)
def test_result(ret: subprocess.CompletedProcess):
if not ret.returncode:
print('\nBuild Complete!')
else:
raise SystemExit(ret.returncode)
# %% compilers
def clang_params() -> Tuple[Dict[str, str], List[str]]:
"""
LLVM compilers e.g. Clang, Flang
"""
compilers = {'CC': 'clang', 'CXX': 'clang++', 'FC': 'flang'}
args: List[str] = []
return compilers, args
def gnu_params() -> Tuple[Dict[str, str], List[str]]:
"""
GNU compilers e.g. GCC, Gfortran
"""
compilers = {'FC': 'gfortran', 'CC': 'gcc', 'CXX': 'g++'}
args: List[str] = []
return compilers, args
def intel_params() -> Tuple[Dict[str, str], List[str]]:
"""
Intel compilers
"""
if not os.environ.get('MKLROOT'):
raise EnvironmentError('must have set MKLROOT by running compilervars.bat or source compilervars.sh before this script.')
# %% compiler variables
compilers = {'FC': 'ifort'}
if os.name == 'nt':
compilers['CC'] = compilers['CXX'] = 'icl'
else:
compilers['CC'] = 'icc'
compilers['CXX'] = 'icpc'
args: List[str] = []
return compilers, args
def msvc_params() -> Tuple[Dict[str, str], List[str]]:
"""
Micro$oft Visual Studio
Note in general MSVC doesn't have good modern C++ features,
so don't be surprised if a C++11 or newer program doesn't compile.
"""
if not shutil.which('cl'):
raise EnvironmentError('Must have PATH set to include MSVC cl.exe compiler bin directory')
compilers = {'CC': 'cl', 'CXX': 'cl'}
args: List[str] = []
return compilers, args
def pgi_params() -> Tuple[Dict[str, str], List[str]]:
"""
Nvidia PGI compilers
pgc++ is not available on Windows at this time
"""
if not shutil.which('pgcc') or not shutil.which('pgfortran'):
raise EnvironmentError('Must have PATH set to include PGI compiler bin directory')
# %% compiler variables
compilers = {'FC': 'pgfortran', 'CC': 'pgcc'}
if os.name != 'nt':
compilers['CXX'] = 'pgc++'
args: List[str] = []
return compilers, args
if __name__ == '__main__':
p = ArgumentParser()
p.add_argument('vendor', help='compiler vendor [clang, gnu, intel, msvc, pgi]', nargs='?', default='gnu')
p.add_argument('-wipe', help='wipe and rebuild from scratch', action='store_true')
p.add_argument('-b', '--buildsys', help='default build system', default='cmake')
p.add_argument('-d', '--args', help='preprocessor arguments', nargs='+', default=[])
p.add_argument('-n', '--no-test', help='do not run self-test / example', action='store_false')
p.add_argument('-install', help='specify directory to install to')
a = p.parse_args()
dotest = a.no_test
if a.vendor == 'clang':
compilers, args = clang_params()
elif a.vendor in ('gnu', 'gcc'):
compilers, args = gnu_params()
elif a.vendor == 'intel':
compilers, args = intel_params()
elif a.vendor == 'msvc':
compilers, args = msvc_params()
elif a.vendor == 'pgi':
compilers, args = pgi_params()
else:
raise ValueError('unknown compiler vendor {}'.format(a.vendor))
args += a.args
do_build(a.buildsys, compilers, args, a.wipe, dotest, a.install)