• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

brian-team / brian2 / 18986306718

31 Oct 2025 10:05PM UTC coverage: 92.107%. Remained the same
18986306718

push

github

web-flow
Merge pull request #1711 from brian-team/update-spec0-dependencies-18944752585

chore: Drop support for unsupported packages conform SPEC 0

2518 of 2655 branches covered (94.84%)

14914 of 16192 relevant lines covered (92.11%)

2.62 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

47.27
/brian2/codegen/cpp_prefs.py
1
"""
2
Preferences related to C++ compilation
3

4
Preferences
5
--------------------
6
.. document_brian_prefs:: codegen.cpp
7

8
"""
9

10
import json
2✔
11
import os
2✔
12
import platform
2✔
13
import re
2✔
14
import socket
2✔
15
import struct
2✔
16
import subprocess
2✔
17
import sys
2✔
18
import tempfile
2✔
19

20
import distutils
2✔
21
from distutils.ccompiler import get_default_compiler
2✔
22

23
from brian2.core.preferences import BrianPreference, prefs
2✔
24
from brian2.utils.filetools import ensure_directory
2✔
25
from brian2.utils.logger import get_logger, std_silent
2✔
26

27
__all__ = ["get_compiler_and_args", "get_msvc_env", "compiler_supports_c99", "C99Check"]
2✔
28

29

30
logger = get_logger(__name__)
2✔
31

32
# default_buildopts stores default build options for Gcc compiler
33
default_buildopts = []
2✔
34

35
# Try to get architecture information to get the best compiler setting for
36
# Windows
37
msvc_arch_flag = ""
2✔
38
if platform.system() == "Windows":
2✔
39
    flags = None
×
40
    previously_stored_flags = None
×
41

42
    # Check whether we've already stored the CPU flags previously
43
    user_dir = os.path.join(os.path.expanduser("~"), ".brian")
×
44
    ensure_directory(user_dir)
×
45
    flag_file = os.path.join(user_dir, "cpu_flags.txt")
×
46
    hostname = socket.gethostname()
×
47
    if os.path.isfile(flag_file):
×
48
        try:
×
49
            with open(flag_file, encoding="utf-8") as f:
×
50
                previously_stored_flags = json.load(f)
×
51
            if hostname not in previously_stored_flags:
×
52
                logger.debug("Ignoring stored CPU flags for a different host")
×
53
            else:
54
                flags = previously_stored_flags[hostname]
×
55
        except OSError as ex:
×
56
            logger.debug(
×
57
                f'Opening file "{flag_file}" to get CPU flags failed with error'
58
                f' "{str(ex)}".'
59
            )
60

61
    if flags is None:  # If we don't have stored info, run get_cpu_flags.py
×
62
        get_cpu_flags_script = os.path.join(
×
63
            os.path.dirname(__file__), "get_cpu_flags.py"
64
        )
65
        get_cpu_flags_script = os.path.abspath(get_cpu_flags_script)
×
66
        try:
×
67
            output = subprocess.check_output(
×
68
                [sys.executable, get_cpu_flags_script],
69
                text=True,
70
                encoding="utf-8",
71
            )
72
            flags = json.loads(output)
×
73
            # Store flags to a file so we don't have to call cpuinfo next time
74
            try:
×
75
                if previously_stored_flags is not None:
×
76
                    to_store = previously_stored_flags
×
77
                    to_store[hostname] = flags
×
78
                else:
79
                    to_store = {hostname: flags}
×
80
                with open(flag_file, "w", encoding="utf-8") as f:
×
81
                    json.dump(to_store, f)
×
82
            except OSError as ex:
×
83
                logger.debug(
×
84
                    f'Writing file "{flag_file}" to store CPU flags failed with error'
85
                    f' "{str(ex)}".'
86
                )
87
        except subprocess.CalledProcessError as ex:
×
88
            logger.debug(
×
89
                "Could not determine optimized MSVC flags, get_cpu_flags failed with:"
90
                f" {str(ex)}"
91
            )
92

93
    if flags is not None:
×
94
        # Note that this overwrites the arch_flag, i.e. only the best option
95
        # will be used
96
        if "sse" in flags:
×
97
            msvc_arch_flag = "/arch:SSE"
×
98
        if "sse2" in flags:
×
99
            msvc_arch_flag = "/arch:SSE2"
×
100
        if "avx" in flags:
×
101
            msvc_arch_flag = "/arch:AVX"
×
102
        if "avx2" in flags:
×
103
            msvc_arch_flag = "/arch:AVX2"
×
104

105
else:
106
    # Optimized default build options for a range a CPU architectures
107
    machine = os.uname().machine
2✔
108
    if re.match("^(x86_64|aarch64|arm.*|s390.*|i.86.*)$", machine):
2✔
109
        default_buildopts = [
2✔
110
            "-w",
111
            "-O3",
112
            "-ffast-math",
113
            "-fno-finite-math-only",
114
            "-march=native",
115
            "-std=c++11",
116
        ]
117
    elif re.match("^(alpha|ppc.*|sparc.*)$", machine):
×
118
        default_buildopts = [
×
119
            "-w",
120
            "-O3",
121
            "-ffast-math",
122
            "-fno-finite-math-only",
123
            "-mcpu=native",
124
            "-mtune=native",
125
            "-std=c++11",
126
        ]
127
    elif re.match("^(parisc.*|riscv.*|mips.*|loong64.*)$", machine):
×
128
        default_buildopts = [
×
129
            "-w",
130
            "-O3",
131
            "-ffast-math",
132
            "-fno-finite-math-only",
133
            "-std=c++11",
134
        ]
135
    else:
136
        default_buildopts = ["-w"]
×
137

138
if os.environ.get("READTHEDOCS", "False").lower() == "true":
2✔
139
    # We are getting imported during a documentation build. Set a fake prefix
140
    # to avoid having the name of the local environment in the documentation
141
    sys_prefix = "/path/to/your/Python/environment"
×
142
else:
143
    sys_prefix = sys.prefix
2✔
144

145
if sys.platform == "win32":
2✔
146
    prefix_dir = os.path.join(sys_prefix, "Library")
×
147
else:
148
    prefix_dir = sys_prefix
2✔
149

150
# Preferences
151
prefs.register_preferences(
4✔
152
    "codegen.cpp",
153
    "C++ compilation preferences",
154
    compiler=BrianPreference(
155
        default="",
156
        docs="""
157
        Compiler to use (uses default if empty).
158
        Should be ``'unix'`` or ``'msvc'``.
159

160
        To specify a specific compiler binary on unix systems, set the `CXX` environment
161
        variable instead.
162
        """,
163
    ),
164
    extra_compile_args=BrianPreference(
165
        default=None,
166
        validator=lambda v: True,
167
        docs="""
168
        Extra arguments to pass to compiler (if None, use either
169
        ``extra_compile_args_gcc`` or ``extra_compile_args_msvc``).
170
        """,
171
    ),
172
    extra_compile_args_gcc=BrianPreference(
173
        default=default_buildopts,
174
        docs="""
175
        Extra compile arguments to pass to GCC compiler
176
        """,
177
    ),
178
    extra_compile_args_msvc=BrianPreference(
179
        default=["/Ox", "/w", msvc_arch_flag, "/MP"],
180
        docs="""
181
        Extra compile arguments to pass to MSVC compiler (the default
182
        ``/arch:`` flag is determined based on the processor architecture)
183
        """,
184
    ),
185
    extra_link_args=BrianPreference(
186
        default=[],
187
        docs="""
188
        Any extra platform- and compiler-specific information to use when
189
        linking object files together.
190
        """,
191
    ),
192
    include_dirs=BrianPreference(
193
        default=[os.path.join(prefix_dir, "include")],
194
        docs="""
195
        Include directories to use.
196
        The default value is ``$prefix/include`` (or ``$prefix/Library/include``
197
        on Windows), where ``$prefix`` is Python's site-specific directory
198
        prefix as returned by `sys.prefix`. This will make compilation use
199
        library files installed into a conda environment.
200
        """,
201
    ),
202
    library_dirs=BrianPreference(
203
        default=[os.path.join(prefix_dir, "lib")],
204
        docs="""
205
        List of directories to search for C/C++ libraries at link time.
206
        The default value is ``$prefix/lib`` (or ``$prefix/Library/lib``
207
        on Windows), where ``$prefix`` is Python's site-specific directory
208
        prefix as returned by `sys.prefix`. This will make compilation use
209
        library files installed into a conda environment.
210
        """,
211
    ),
212
    runtime_library_dirs=BrianPreference(
213
        default=[os.path.join(prefix_dir, "lib")] if sys.platform != "win32" else [],
214
        docs="""
215
        List of directories to search for C/C++ libraries at run time.
216
        The default value is ``$prefix/lib`` (not used on Windows), where
217
        ``$prefix`` is Python's site-specific directory prefix as returned by
218
        `sys.prefix`. This will make compilation use library files installed
219
        into a conda environment.
220
        """,
221
    ),
222
    libraries=BrianPreference(
223
        default=[],
224
        docs="""
225
        List of library names (not filenames or paths) to link against.
226
        """,
227
    ),
228
    headers=BrianPreference(
229
        default=[],
230
        docs="""
231
        A list of strings specifying header files to use when compiling the
232
        code. The list might look like ["<vector>","'my_header'"]. Note that
233
        the header strings need to be in a form than can be pasted at the end
234
        of a #include statement in the C++ code.
235
        """,
236
    ),
237
    define_macros=BrianPreference(
238
        default=[],
239
        docs="""
240
        List of macros to define; each macro is defined using a 2-tuple,
241
        where 'value' is either the string to define it to or None to
242
        define it without a particular value (equivalent of "#define
243
        FOO" in source or -DFOO on Unix C compiler command line).
244
        """,
245
    ),
246
    msvc_vars_location=BrianPreference(
247
        default="",
248
        docs="""
249
        Location of the MSVC command line tool (or search for best by default).
250
        """,
251
    ),
252
    msvc_architecture=BrianPreference(
253
        default="",
254
        docs="""
255
        MSVC architecture name (or use system architectue by default).
256

257
        Could take values such as x86, amd64, etc.
258
        """,
259
    ),
260
)
261

262

263
# check whether compiler supports a flag
264
# Adapted from https://github.com/pybind/pybind11/
265
def _determine_flag_compatibility(compiler, flagname):
2✔
266
    import tempfile
4✔
267

268
    from distutils.errors import CompileError
4✔
269

270
    with (
4✔
271
        tempfile.TemporaryDirectory(prefix="brian_flag_test_") as temp_dir,
272
        std_silent(),
273
    ):
274
        fname = os.path.join(temp_dir, "flag_test.cpp")
4✔
275
        with open(fname, "w") as f:
4✔
276
            f.write("int main (int argc, char **argv) { return 0; }")
4✔
277
        try:
4✔
278
            compiler.compile([fname], output_dir=temp_dir, extra_postargs=[flagname])
4✔
279
        except CompileError:
2✔
280
            logger.warn(f"Removing unsupported flag '{flagname}' from compiler flags.")
2✔
281
            return False
2✔
282
    return True
4✔
283

284

285
_compiler_flag_compatibility = {}
2✔
286

287

288
def has_flag(compiler, flagname):
2✔
289
    if compiler.compiler_type == "msvc":
4✔
290
        # MSVC does not raise an error for illegal flags, so determining
291
        # whether it accepts a flag would mean parsing the output for warnings
292
        # This is non-trivial so we don't do it (the main reason to check
293
        # flags in the first place are differences between gcc and clang)
294
        return True
×
295
    else:
296
        compiler_exe = " ".join(compiler.executables["compiler_cxx"])
4✔
297

298
    if (compiler_exe, flagname) not in _compiler_flag_compatibility:
4✔
299
        compatibility = _determine_flag_compatibility(compiler, flagname)
4✔
300
        _compiler_flag_compatibility[(compiler_exe, flagname)] = compatibility
4✔
301

302
    return _compiler_flag_compatibility[(compiler_exe, flagname)]
4✔
303

304

305
def get_compiler_and_args():
2✔
306
    """
307
    Returns the computed compiler and compilation flags
308
    """
309
    compiler = prefs["codegen.cpp.compiler"]
4✔
310
    if compiler == "":
4✔
311
        compiler = get_default_compiler()
4✔
312
    extra_compile_args = prefs["codegen.cpp.extra_compile_args"]
4✔
313
    if extra_compile_args is None:
4✔
314
        if compiler in ("gcc", "unix"):
4✔
315
            extra_compile_args = prefs["codegen.cpp.extra_compile_args_gcc"]
4✔
316
        elif compiler == "msvc":
×
317
            extra_compile_args = prefs["codegen.cpp.extra_compile_args_msvc"]
×
318
        else:
319
            extra_compile_args = []
×
320
            logger.warn(f"Unsupported compiler '{compiler}'.")
×
321

322
    from distutils.ccompiler import new_compiler
4✔
323
    from distutils.sysconfig import customize_compiler
4✔
324

325
    compiler_obj = new_compiler(compiler=compiler, verbose=0)
4✔
326
    customize_compiler(compiler_obj)
4✔
327
    extra_compile_args = [
4✔
328
        flag for flag in extra_compile_args if has_flag(compiler_obj, flag)
329
    ]
330

331
    return compiler, extra_compile_args
4✔
332

333

334
_msvc_env = None
2✔
335

336

337
def get_msvc_env():
2✔
338
    try:
×
339
        from setuptools.msvc import msvc14_get_vc_env as _get_vc_env
×
340
    except ImportError:  # Setuptools 0.74.0 removed this function
×
341
        try:
×
342
            from distutils.compilers.C.msvc import _get_vc_env
×
343
        except ImportError:  # Things keep moving around in distutils/setuptools
×
344
            from distutils._msvccompiler import _get_vc_env
×
345

346
    global _msvc_env
347
    arch_name = prefs["codegen.cpp.msvc_architecture"]
×
348
    if arch_name == "":
×
349
        bits = struct.calcsize("P") * 8
×
350
        if bits == 64:
×
351
            arch_name = "x86_amd64"
×
352
        else:
353
            arch_name = "x86"
×
354
    # Manual specification of vcvarsall.bat location by the user
355
    vcvars_loc = prefs["codegen.cpp.msvc_vars_location"]
×
356
    if vcvars_loc:
×
357
        vcvars_cmd = f'"{vcvars_loc}" {arch_name}'
×
358
        return None, vcvars_cmd
×
359

360
    # Search for MSVC environment if not already cached
361
    if _msvc_env is None:
×
362
        try:
×
363
            _msvc_env = _get_vc_env(arch_name)
×
364
        except distutils.errors.DistutilsPlatformError as ex:
×
365
            raise OSError(
×
366
                "Cannot find Microsoft Visual Studio, You "
367
                "can try to set the path to vcvarsall.bat "
368
                "via the codegen.cpp.msvc_vars_location "
369
                "preference explicitly."
370
            ) from ex
371
    return _msvc_env, None
×
372

373

374
_compiler_supports_c99 = None
2✔
375

376

377
def compiler_supports_c99():
2✔
378
    global _compiler_supports_c99
379
    if _compiler_supports_c99 is None:
4✔
380
        if platform.system() == "Windows":
4✔
381
            fd, tmp_file = tempfile.mkstemp(suffix=".cpp")
×
382
            os.write(
×
383
                fd,
384
                b"""
385
            #if _MSC_VER < 1800
386
            #error
387
            #endif
388
            """,
389
            )
390
            os.close(fd)
×
391
            msvc_env, vcvars_cmd = get_msvc_env()
×
392
            if vcvars_cmd:
×
393
                cmd = f"{vcvars_cmd} && cl /E {tmp_file} > NUL 2>&1"
×
394
            else:
395
                os.environ.update(msvc_env)
×
396
                cmd = f"cl /E {tmp_file} > NUL 2>&1"
×
397
            return_value = os.system(cmd)
×
398
            _compiler_supports_c99 = return_value == 0
×
399
            os.remove(tmp_file)
×
400
        else:
401
            CC = os.environ.get("CC", "cc")
4✔
402
            cmd = (
4✔
403
                'echo "#if (__STDC_VERSION__ < 199901L)\n#error\n#endif" | '
404
                f"'{CC}' -xc -E - > /dev/null 2>&1"
405
            )
406
            return_value = os.system(cmd)
4✔
407
            _compiler_supports_c99 = return_value == 0
4✔
408
    return _compiler_supports_c99
4✔
409

410

411
class C99Check:
2✔
412
    """
413
    Helper class to create objects that can be passed as an ``availability_check`` to
414
    a `FunctionImplementation`.
415
    """
416

417
    def __init__(self, name):
2✔
418
        self.name = name
2✔
419

420
    def __call__(self, *args, **kwargs):
2✔
421
        if not compiler_supports_c99():
4✔
422
            raise NotImplementedError(
423
                f'The "{self.name}" function needs C99 compiler support'
424
            )
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc