Skip to content

Commit cc26500

Browse files
committed
Issue #958: Travis CI should enfore clang-format standards
This patch adds clang format support to the travis bots.
1 parent 21e3d21 commit cc26500

File tree

4 files changed

+367
-4
lines changed

4 files changed

+367
-4
lines changed

.travis.yml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,18 @@ addons:
1515
apt:
1616
sources:
1717
- ubuntu-toolchain-r-test
18-
- llvm-toolchain-xenial-8
18+
- llvm-toolchain-xenial-9
1919
packages:
20-
- clang-8
20+
- clang-format-9
21+
- clang-9
2122
- valgrind
2223
matrix:
2324
allow_failures:
2425
- os: osx
2526
include:
2627
- name: Mac clang meson static release testing
2728
os: osx
28-
osx_image: xcode10.2
29+
osx_image: xcode11
2930
compiler: clang
3031
env:
3132
CXX="clang++"

.travis_scripts/meson_builder.sh

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,11 @@ meson --version
6363
ninja --version
6464
_COMPILER_NAME=`basename ${CXX}`
6565
_BUILD_DIR_NAME="build-${BUILD_TYPE}_${LIB_TYPE}_${_COMPILER_NAME}"
66+
67+
./.travis_scripts/run-clang-format.sh
6668
meson --buildtype ${BUILD_TYPE} --default-library ${LIB_TYPE} . "${_BUILD_DIR_NAME}"
6769
ninja -v -j 2 -C "${_BUILD_DIR_NAME}"
68-
#ninja -v -j 2 -C "${_BUILD_DIR_NAME}" test
70+
6971
cd "${_BUILD_DIR_NAME}"
7072
meson test --no-rebuild --print-errorlogs
7173

.travis_scripts/run-clang-format.py

Lines changed: 356 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,356 @@
1+
#!/usr/bin/env python
2+
"""A wrapper script around clang-format, suitable for linting multiple files
3+
and to use for continuous integration.
4+
This is an alternative API for the clang-format command line.
5+
It runs over multiple files and directories in parallel.
6+
A diff output is produced and a sensible exit code is returned.
7+
8+
NOTE: pulled from https://github.com/Sarcasm/run-clang-format, which is
9+
licensed under the MIT license.
10+
"""
11+
12+
from __future__ import print_function, unicode_literals
13+
14+
import argparse
15+
import codecs
16+
import difflib
17+
import fnmatch
18+
import io
19+
import multiprocessing
20+
import os
21+
import signal
22+
import subprocess
23+
import sys
24+
import traceback
25+
26+
from functools import partial
27+
28+
try:
29+
from subprocess import DEVNULL # py3k
30+
except ImportError:
31+
DEVNULL = open(os.devnull, "wb")
32+
33+
34+
DEFAULT_EXTENSIONS = 'c,h,C,H,cpp,hpp,cc,hh,c++,h++,cxx,hxx'
35+
36+
37+
class ExitStatus:
38+
SUCCESS = 0
39+
DIFF = 1
40+
TROUBLE = 2
41+
42+
43+
def list_files(files, recursive=False, extensions=None, exclude=None):
44+
if extensions is None:
45+
extensions = []
46+
if exclude is None:
47+
exclude = []
48+
49+
out = []
50+
for file in files:
51+
if recursive and os.path.isdir(file):
52+
for dirpath, dnames, fnames in os.walk(file):
53+
fpaths = [os.path.join(dirpath, fname) for fname in fnames]
54+
for pattern in exclude:
55+
# os.walk() supports trimming down the dnames list
56+
# by modifying it in-place,
57+
# to avoid unnecessary directory listings.
58+
dnames[:] = [
59+
x for x in dnames
60+
if
61+
not fnmatch.fnmatch(os.path.join(dirpath, x), pattern)
62+
]
63+
fpaths = [
64+
x for x in fpaths if not fnmatch.fnmatch(x, pattern)
65+
]
66+
for f in fpaths:
67+
ext = os.path.splitext(f)[1][1:]
68+
if ext in extensions:
69+
out.append(f)
70+
else:
71+
out.append(file)
72+
return out
73+
74+
75+
def make_diff(file, original, reformatted):
76+
return list(
77+
difflib.unified_diff(
78+
original,
79+
reformatted,
80+
fromfile='{}\t(original)'.format(file),
81+
tofile='{}\t(reformatted)'.format(file),
82+
n=3))
83+
84+
85+
class DiffError(Exception):
86+
def __init__(self, message, errs=None):
87+
super(DiffError, self).__init__(message)
88+
self.errs = errs or []
89+
90+
91+
class UnexpectedError(Exception):
92+
def __init__(self, message, exc=None):
93+
super(UnexpectedError, self).__init__(message)
94+
self.formatted_traceback = traceback.format_exc()
95+
self.exc = exc
96+
97+
98+
def run_clang_format_diff_wrapper(args, file):
99+
try:
100+
ret = run_clang_format_diff(args, file)
101+
return ret
102+
except DiffError:
103+
raise
104+
except Exception as e:
105+
raise UnexpectedError('{}: {}: {}'.format(file, e.__class__.__name__,
106+
e), e)
107+
108+
109+
def run_clang_format_diff(args, file):
110+
try:
111+
with io.open(file, 'r', encoding='utf-8') as f:
112+
original = f.readlines()
113+
except IOError as exc:
114+
raise DiffError(str(exc))
115+
invocation = [args.clang_format_executable, file]
116+
117+
# Use of utf-8 to decode the process output.
118+
#
119+
# Hopefully, this is the correct thing to do.
120+
#
121+
# It's done due to the following assumptions (which may be incorrect):
122+
# - clang-format will returns the bytes read from the files as-is,
123+
# without conversion, and it is already assumed that the files use utf-8.
124+
# - if the diagnostics were internationalized, they would use utf-8:
125+
# > Adding Translations to Clang
126+
# >
127+
# > Not possible yet!
128+
# > Diagnostic strings should be written in UTF-8,
129+
# > the client can translate to the relevant code page if needed.
130+
# > Each translation completely replaces the format string
131+
# > for the diagnostic.
132+
# > -- http://clang.llvm.org/docs/InternalsManual.html#internals-diag-translation
133+
#
134+
# It's not pretty, due to Python 2 & 3 compatibility.
135+
encoding_py3 = {}
136+
if sys.version_info[0] >= 3:
137+
encoding_py3['encoding'] = 'utf-8'
138+
139+
try:
140+
proc = subprocess.Popen(
141+
invocation,
142+
stdout=subprocess.PIPE,
143+
stderr=subprocess.PIPE,
144+
universal_newlines=True,
145+
**encoding_py3)
146+
except OSError as exc:
147+
raise DiffError(
148+
"Command '{}' failed to start: {}".format(
149+
subprocess.list2cmdline(invocation), exc
150+
)
151+
)
152+
proc_stdout = proc.stdout
153+
proc_stderr = proc.stderr
154+
if sys.version_info[0] < 3:
155+
# make the pipes compatible with Python 3,
156+
# reading lines should output unicode
157+
encoding = 'utf-8'
158+
proc_stdout = codecs.getreader(encoding)(proc_stdout)
159+
proc_stderr = codecs.getreader(encoding)(proc_stderr)
160+
# hopefully the stderr pipe won't get full and block the process
161+
outs = list(proc_stdout.readlines())
162+
errs = list(proc_stderr.readlines())
163+
proc.wait()
164+
if proc.returncode:
165+
raise DiffError(
166+
"Command '{}' returned non-zero exit status {}".format(
167+
subprocess.list2cmdline(invocation), proc.returncode
168+
),
169+
errs,
170+
)
171+
return make_diff(file, original, outs), errs
172+
173+
174+
def bold_red(s):
175+
return '\x1b[1m\x1b[31m' + s + '\x1b[0m'
176+
177+
178+
def colorize(diff_lines):
179+
def bold(s):
180+
return '\x1b[1m' + s + '\x1b[0m'
181+
182+
def cyan(s):
183+
return '\x1b[36m' + s + '\x1b[0m'
184+
185+
def green(s):
186+
return '\x1b[32m' + s + '\x1b[0m'
187+
188+
def red(s):
189+
return '\x1b[31m' + s + '\x1b[0m'
190+
191+
for line in diff_lines:
192+
if line[:4] in ['--- ', '+++ ']:
193+
yield bold(line)
194+
elif line.startswith('@@ '):
195+
yield cyan(line)
196+
elif line.startswith('+'):
197+
yield green(line)
198+
elif line.startswith('-'):
199+
yield red(line)
200+
else:
201+
yield line
202+
203+
204+
def print_diff(diff_lines, use_color):
205+
if use_color:
206+
diff_lines = colorize(diff_lines)
207+
if sys.version_info[0] < 3:
208+
sys.stdout.writelines((l.encode('utf-8') for l in diff_lines))
209+
else:
210+
sys.stdout.writelines(diff_lines)
211+
212+
213+
def print_trouble(prog, message, use_colors):
214+
error_text = 'error:'
215+
if use_colors:
216+
error_text = bold_red(error_text)
217+
print("{}: {} {}".format(prog, error_text, message), file=sys.stderr)
218+
219+
220+
def main():
221+
parser = argparse.ArgumentParser(description=__doc__)
222+
parser.add_argument(
223+
'--clang-format-executable',
224+
metavar='EXECUTABLE',
225+
help='path to the clang-format executable',
226+
default='clang-format')
227+
parser.add_argument(
228+
'--extensions',
229+
help='comma separated list of file extensions (default: {})'.format(
230+
DEFAULT_EXTENSIONS),
231+
default=DEFAULT_EXTENSIONS)
232+
parser.add_argument(
233+
'-r',
234+
'--recursive',
235+
action='store_true',
236+
help='run recursively over directories')
237+
parser.add_argument('files', metavar='file', nargs='+')
238+
parser.add_argument(
239+
'-q',
240+
'--quiet',
241+
action='store_true')
242+
parser.add_argument(
243+
'-j',
244+
metavar='N',
245+
type=int,
246+
default=0,
247+
help='run N clang-format jobs in parallel'
248+
' (default number of cpus + 1)')
249+
parser.add_argument(
250+
'--color',
251+
default='auto',
252+
choices=['auto', 'always', 'never'],
253+
help='show colored diff (default: auto)')
254+
parser.add_argument(
255+
'-e',
256+
'--exclude',
257+
metavar='PATTERN',
258+
action='append',
259+
default=[],
260+
help='exclude paths matching the given glob-like pattern(s)'
261+
' from recursive search')
262+
263+
args = parser.parse_args()
264+
265+
# use default signal handling, like diff return SIGINT value on ^C
266+
# https://bugs.python.org/issue14229#msg156446
267+
signal.signal(signal.SIGINT, signal.SIG_DFL)
268+
try:
269+
signal.SIGPIPE
270+
except AttributeError:
271+
# compatibility, SIGPIPE does not exist on Windows
272+
pass
273+
else:
274+
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
275+
276+
colored_stdout = False
277+
colored_stderr = False
278+
if args.color == 'always':
279+
colored_stdout = True
280+
colored_stderr = True
281+
elif args.color == 'auto':
282+
colored_stdout = sys.stdout.isatty()
283+
colored_stderr = sys.stderr.isatty()
284+
285+
version_invocation = [args.clang_format_executable, str("--version")]
286+
try:
287+
subprocess.check_call(version_invocation, stdout=DEVNULL)
288+
except subprocess.CalledProcessError as e:
289+
print_trouble(parser.prog, str(e), use_colors=colored_stderr)
290+
return ExitStatus.TROUBLE
291+
except OSError as e:
292+
print_trouble(
293+
parser.prog,
294+
"Command '{}' failed to start: {}".format(
295+
subprocess.list2cmdline(version_invocation), e
296+
),
297+
use_colors=colored_stderr,
298+
)
299+
return ExitStatus.TROUBLE
300+
301+
retcode = ExitStatus.SUCCESS
302+
files = list_files(
303+
args.files,
304+
recursive=args.recursive,
305+
exclude=args.exclude,
306+
extensions=args.extensions.split(','))
307+
308+
if not files:
309+
return
310+
311+
njobs = args.j
312+
if njobs == 0:
313+
njobs = multiprocessing.cpu_count() + 1
314+
njobs = min(len(files), njobs)
315+
316+
if njobs == 1:
317+
# execute directly instead of in a pool,
318+
# less overhead, simpler stacktraces
319+
it = (run_clang_format_diff_wrapper(args, file) for file in files)
320+
pool = None
321+
else:
322+
pool = multiprocessing.Pool(njobs)
323+
it = pool.imap_unordered(
324+
partial(run_clang_format_diff_wrapper, args), files)
325+
while True:
326+
try:
327+
outs, errs = next(it)
328+
except StopIteration:
329+
break
330+
except DiffError as e:
331+
print_trouble(parser.prog, str(e), use_colors=colored_stderr)
332+
retcode = ExitStatus.TROUBLE
333+
sys.stderr.writelines(e.errs)
334+
except UnexpectedError as e:
335+
print_trouble(parser.prog, str(e), use_colors=colored_stderr)
336+
sys.stderr.write(e.formatted_traceback)
337+
retcode = ExitStatus.TROUBLE
338+
# stop at the first unexpected error,
339+
# something could be very wrong,
340+
# don't process all files unnecessarily
341+
if pool:
342+
pool.terminate()
343+
break
344+
else:
345+
sys.stderr.writelines(errs)
346+
if outs == []:
347+
continue
348+
if not args.quiet:
349+
print_diff(outs, use_color=colored_stdout)
350+
if retcode == ExitStatus.SUCCESS:
351+
retcode = ExitStatus.DIFF
352+
return retcode
353+
354+
355+
if __name__ == '__main__':
356+
sys.exit(main())

.travis_scripts/run-clang-format.sh

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
#!/usr/bin/env bash
2+
3+
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
4+
python $DIR/run-clang-format.py -r $DIR/../src/**/ $DIR/../include/**/

0 commit comments

Comments
 (0)