1"""Generate and run C code.
2"""
3
4# Copyright The Mbed TLS Contributors
5# SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
6#
7
8import os
9import platform
10import subprocess
11import sys
12import tempfile
13
14def remove_file_if_exists(filename):
15    """Remove the specified file, ignoring errors."""
16    if not filename:
17        return
18    try:
19        os.remove(filename)
20    except OSError:
21        pass
22
23def create_c_file(file_label):
24    """Create a temporary C file.
25
26    * ``file_label``: a string that will be included in the file name.
27
28    Return ```(c_file, c_name, exe_name)``` where ``c_file`` is a Python
29    stream open for writing to the file, ``c_name`` is the name of the file
30    and ``exe_name`` is the name of the executable that will be produced
31    by compiling the file.
32    """
33    c_fd, c_name = tempfile.mkstemp(prefix='tmp-{}-'.format(file_label),
34                                    suffix='.c')
35    exe_suffix = '.exe' if platform.system() == 'Windows' else ''
36    exe_name = c_name[:-2] + exe_suffix
37    remove_file_if_exists(exe_name)
38    c_file = os.fdopen(c_fd, 'w', encoding='ascii')
39    return c_file, c_name, exe_name
40
41def generate_c_printf_expressions(c_file, cast_to, printf_format, expressions):
42    """Generate C instructions to print the value of ``expressions``.
43
44    Write the code with ``c_file``'s ``write`` method.
45
46    Each expression is cast to the type ``cast_to`` and printed with the
47    printf format ``printf_format``.
48    """
49    for expr in expressions:
50        c_file.write('    printf("{}\\n", ({}) {});\n'
51                     .format(printf_format, cast_to, expr))
52
53def generate_c_file(c_file,
54                    caller, header,
55                    main_generator):
56    """Generate a temporary C source file.
57
58    * ``c_file`` is an open stream on the C source file.
59    * ``caller``: an informational string written in a comment at the top
60      of the file.
61    * ``header``: extra code to insert before any function in the generated
62      C file.
63    * ``main_generator``: a function called with ``c_file`` as its sole argument
64      to generate the body of the ``main()`` function.
65    """
66    c_file.write('/* Generated by {} */'
67                 .format(caller))
68    c_file.write('''
69#include <stdio.h>
70''')
71    c_file.write(header)
72    c_file.write('''
73int main(void)
74{
75''')
76    main_generator(c_file)
77    c_file.write('''    return 0;
78}
79''')
80
81def compile_c_file(c_filename, exe_filename, include_dirs):
82    """Compile a C source file with the host compiler.
83
84    * ``c_filename``: the name of the source file to compile.
85    * ``exe_filename``: the name for the executable to be created.
86    * ``include_dirs``: a list of paths to include directories to be passed
87      with the -I switch.
88    """
89    # Respect $HOSTCC if it is set
90    cc = os.getenv('HOSTCC', None)
91    if cc is None:
92        cc = os.getenv('CC', 'cc')
93    cmd = [cc]
94
95    proc = subprocess.Popen(cmd,
96                            stdout=subprocess.DEVNULL,
97                            stderr=subprocess.PIPE,
98                            universal_newlines=True)
99    cc_is_msvc = 'Microsoft (R) C/C++' in proc.communicate()[1]
100
101    cmd += ['-I' + dir for dir in include_dirs]
102    if cc_is_msvc:
103        # MSVC has deprecated using -o to specify the output file,
104        # and produces an object file in the working directory by default.
105        obj_filename = exe_filename[:-4] + '.obj'
106        cmd += ['-Fe' + exe_filename, '-Fo' + obj_filename]
107    else:
108        cmd += ['-o' + exe_filename]
109
110    subprocess.check_call(cmd + [c_filename])
111
112def get_c_expression_values(
113        cast_to, printf_format,
114        expressions,
115        caller=__name__, file_label='',
116        header='', include_path=None,
117        keep_c=False,
118): # pylint: disable=too-many-arguments, too-many-locals
119    """Generate and run a program to print out numerical values for expressions.
120
121    * ``cast_to``: a C type.
122    * ``printf_format``: a printf format suitable for the type ``cast_to``.
123    * ``header``: extra code to insert before any function in the generated
124      C file.
125    * ``expressions``: a list of C language expressions that have the type
126      ``cast_to``.
127    * ``include_path``: a list of directories containing header files.
128    * ``keep_c``: if true, keep the temporary C file (presumably for debugging
129      purposes).
130
131    Use the C compiler specified by the ``CC`` environment variable, defaulting
132    to ``cc``. If ``CC`` looks like MSVC, use its command line syntax,
133    otherwise assume the compiler supports Unix traditional ``-I`` and ``-o``.
134
135    Return the list of values of the ``expressions``.
136    """
137    if include_path is None:
138        include_path = []
139    c_name = None
140    exe_name = None
141    obj_name = None
142    try:
143        c_file, c_name, exe_name = create_c_file(file_label)
144        generate_c_file(
145            c_file, caller, header,
146            lambda c_file: generate_c_printf_expressions(c_file,
147                                                         cast_to, printf_format,
148                                                         expressions)
149        )
150        c_file.close()
151
152        compile_c_file(c_name, exe_name, include_path)
153        if keep_c:
154            sys.stderr.write('List of {} tests kept at {}\n'
155                             .format(caller, c_name))
156        else:
157            os.remove(c_name)
158        output = subprocess.check_output([exe_name])
159        return output.decode('ascii').strip().split('\n')
160    finally:
161        remove_file_if_exists(exe_name)
162        remove_file_if_exists(obj_name)
163