File: compile_headers.py

package info (click to toggle)
persistent-cache-cpp 1.0.7-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 648 kB
  • sloc: cpp: 7,754; python: 183; ansic: 91; sh: 34; makefile: 7
file content (169 lines) | stat: -rwxr-xr-x 6,631 bytes parent folder | download | duplicates (2)
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
#! /usr/bin/env python3

# Copyright (C) 2013 Canonical Ltd.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 3 as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
# Authored by: Michi Henning <michi.henning@canonical.com>

#
# Little helper program to test that header files are stand-alone compilable (and therefore don't depend on
# other headers being included first).
#
# Usage: compile_headers.py directory compiler [compiler_flags]
#
# The directory specifies the location of the header files. All files in that directory ending in .h (but not
# in subdirectories) are tested.
#
# The compiler argument specifies the compiler to use (such as "gcc"), and the compiler_flags argument (which
# must be a single string argument, not a bunch of separate strings) specifies any additional flags, such
# as "-I -g". The flags need not include "-c".
#
# For each header file in the specified directory, the script create a corresponding .cpp that includes the
# header file. The .cpp file is created in the current directory (which isn't necessarily the same one as
# the directory the header files are in). The script runs the compiler on the generated .cpp file and, if the
# compiler returns non-zero exit status, it prints a message (on stdout) reporting the failure.
#
# The script does not stop if a file fails to compile. If all source files compile successfully, no output (other
# than the output from the compiler) is written, and the exit status is zero. If one or more files do not compile,
# or there are any other errors, such as not being able to open a file, exit status is non-zero.
#
# Messages about files that fail to compile are written to stdout. Message about other problems, such as non-existent
# files and the like, are written to stderr.
#
# The compiler's output goes to whatever stream the compiler writes to and is left alone.
#

import argparse
import os
import re
import shlex
import subprocess
import sys
import tempfile
import concurrent.futures, multiprocessing

# Additional #defines that should be set before including a header.
# This is intended for things such as enabling additional features
# that are conditionally compiled in the source.
extra_defines =[]

#
# Write the supplied message to stderr, preceded by the program name.
#
def error(msg):
    print(os.path.basename(sys.argv[0]) + ": " + msg, file=sys.stderr)

#
# Write the supplied message to stdout, preceded by the program name.
#
def message(msg):
    print(os.path.basename(sys.argv[0]) + ": " + msg)

#
# Create a source file in the current directory that includes the specified header, compile it,
# and check exit status from the compiler. Throw if the compile command itself fails,
# return False if the compile command worked but reported errors, True if the compile succeeded.
#
def run_compiler(hdr, compiler, copts, verbose, hdr_dir):
    try:
        src = tempfile.NamedTemporaryFile(suffix='.cpp', dir='.')

        # Add any extra defines at the beginning of the temporary file.
        for flag in extra_defines:
            src.write(bytes("#define " + flag + "" + "\n", 'UTF-8'))

        src.write(bytes("#include <" + hdr + ">" + "\n", 'UTF-8'))
        src.flush()                                                 # Need this to make the file visible
        src_name = os.path.join('.', src.name)

        if verbose:
            print(compiler + " -c " + src_name + " " + copts)

        status = subprocess.call([compiler] + shlex.split(copts) + ["-c", src_name])
        if status != 0:
            message("cannot compile \"" + hdr + "\"") # Yes, write to stdout because this is expected output

        obj = os.path.splitext(src_name)[0] + ".o"
        try:
            os.unlink(obj)
        except:
            pass

        gcov = os.path.splitext(src_name)[0] + ".gcno"
        try:
            os.unlink(gcov)
        except:
            pass

        return status == 0

    except OSError as e:
        error(e.strerror)
        raise

#
# For each of the supplied headers, create a source file in the current directory that includes the header
# and then try to compile the header. Returns normally if all files could be compiled successfully and
# throws, otherwise.
#
def test_files(hdrs, compiler, copts, verbose, hdr_dir):
    num_errs = 0
    executor = concurrent.futures.ThreadPoolExecutor(max_workers=multiprocessing.cpu_count())
    futures = [executor.submit(run_compiler, h, compiler, copts, verbose, hdr_dir) for h in hdrs]
    for f in futures:
        try:
            if not f.result():
                num_errs += 1
        except OSError:
            num_errs += 1
            pass    # Error reported already
    if num_errs != 0:
        msg = str(num_errs) + " file"
        if num_errs != 1:
            msg += "s"
        msg += " failed to compile"
        message(msg)                    # Yes, write to stdout because this is expected output
        sys.exit(1)

def run():
    #
    # Parse arguments.
    #
    parser = argparse.ArgumentParser(description = 'Test that all headers in the passed directory compile stand-alone.')
    parser.add_argument('-v', '--verbose', action='store_true', help = 'Trace invocations of the compiler')
    parser.add_argument('dir', nargs = 1, help = 'The directory to look for header files ending in ".h"')
    parser.add_argument('compiler', nargs = 1, help = 'The compiler executable, such as "gcc"')
    parser.add_argument('copts', nargs = '?', default="",
                        help = 'The compiler options (excluding -c), such as "-g -Wall -I." as a single string.')
    args = parser.parse_args()

    #
    # Find all the .h files in specified directory and do the compilation for each one.
    #
    hdr_dir = args.dir[0]
    try:
        files = os.listdir(hdr_dir)
    except OSError as e:
        msg = "cannot open \"" + hdr_dir + "\": " + e.strerror
        error(msg)
        sys.exit(1)
    hdrs = [hdr for hdr in files if hdr.endswith('.h')]

    try:
        test_files(hdrs, args.compiler[0], args.copts, args.verbose, hdr_dir)
    except OSError:
        sys.exit(1)    # Errors were written earlier

if __name__ == '__main__':
   run()