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