"""
Preferences related to C++ compilation
Preferences
--------------------
.. document_brian_prefs:: codegen.cpp
"""
import distutils
import json
import os
import platform
import re
import socket
import struct
import subprocess
import sys
import tempfile
try:
from setuptools.msvc import msvc14_get_vc_env as _get_vc_env
except ImportError: # Setuptools 0.74.0 removed this function
from distutils._msvccompiler import _get_vc_env
from distutils.ccompiler import get_default_compiler
from brian2.core.preferences import BrianPreference, prefs
from brian2.utils.filetools import ensure_directory
from brian2.utils.logger import get_logger, std_silent
__all__ = ["get_compiler_and_args", "get_msvc_env", "compiler_supports_c99", "C99Check"]
logger = get_logger(__name__)
# default_buildopts stores default build options for Gcc compiler
default_buildopts = []
# Try to get architecture information to get the best compiler setting for
# Windows
msvc_arch_flag = ""
if platform.system() == "Windows":
flags = None
previously_stored_flags = None
# Check whether we've already stored the CPU flags previously
user_dir = os.path.join(os.path.expanduser("~"), ".brian")
ensure_directory(user_dir)
flag_file = os.path.join(user_dir, "cpu_flags.txt")
hostname = socket.gethostname()
if os.path.isfile(flag_file):
try:
with open(flag_file, encoding="utf-8") as f:
previously_stored_flags = json.load(f)
if hostname not in previously_stored_flags:
logger.debug("Ignoring stored CPU flags for a different host")
else:
flags = previously_stored_flags[hostname]
except OSError as ex:
logger.debug(
f'Opening file "{flag_file}" to get CPU flags failed with error'
f' "{str(ex)}".'
)
if flags is None: # If we don't have stored info, run get_cpu_flags.py
get_cpu_flags_script = os.path.join(
os.path.dirname(__file__), "get_cpu_flags.py"
)
get_cpu_flags_script = os.path.abspath(get_cpu_flags_script)
try:
output = subprocess.check_output(
[sys.executable, get_cpu_flags_script],
text=True,
encoding="utf-8",
)
flags = json.loads(output)
# Store flags to a file so we don't have to call cpuinfo next time
try:
if previously_stored_flags is not None:
to_store = previously_stored_flags
to_store[hostname] = flags
else:
to_store = {hostname: flags}
with open(flag_file, "w", encoding="utf-8") as f:
json.dump(to_store, f)
except OSError as ex:
logger.debug(
f'Writing file "{flag_file}" to store CPU flags failed with error'
f' "{str(ex)}".'
)
except subprocess.CalledProcessError as ex:
logger.debug(
"Could not determine optimized MSVC flags, get_cpu_flags failed with:"
f" {str(ex)}"
)
if flags is not None:
# Note that this overwrites the arch_flag, i.e. only the best option
# will be used
if "sse" in flags:
msvc_arch_flag = "/arch:SSE"
if "sse2" in flags:
msvc_arch_flag = "/arch:SSE2"
if "avx" in flags:
msvc_arch_flag = "/arch:AVX"
if "avx2" in flags:
msvc_arch_flag = "/arch:AVX2"
else:
# Optimized default build options for a range a CPU architectures
machine = os.uname().machine
if re.match("^(x86_64|aarch64|arm.*|s390.*|i.86.*)$", machine):
default_buildopts = [
"-w",
"-O3",
"-ffast-math",
"-fno-finite-math-only",
"-march=native",
"-std=c++11",
]
elif re.match("^(alpha|ppc.*|sparc.*)$", machine):
default_buildopts = [
"-w",
"-O3",
"-ffast-math",
"-fno-finite-math-only",
"-mcpu=native",
"-mtune=native",
"-std=c++11",
]
elif re.match("^(parisc.*|riscv.*|mips.*|loong64.*)$", machine):
default_buildopts = [
"-w",
"-O3",
"-ffast-math",
"-fno-finite-math-only",
"-std=c++11",
]
else:
default_buildopts = ["-w"]
if os.environ.get("READTHEDOCS", "False").lower() == "true":
# We are getting imported during a documentation build. Set a fake prefix
# to avoid having the name of the local environment in the documentation
sys_prefix = "/path/to/your/Python/environment"
else:
sys_prefix = sys.prefix
if sys.platform == "win32":
prefix_dir = os.path.join(sys_prefix, "Library")
else:
prefix_dir = sys_prefix
# Preferences
prefs.register_preferences(
"codegen.cpp",
"C++ compilation preferences",
compiler=BrianPreference(
default="",
docs="""
Compiler to use (uses default if empty).
Should be ``'unix'`` or ``'msvc'``.
To specify a specific compiler binary on unix systems, set the `CXX` environment
variable instead.
""",
),
extra_compile_args=BrianPreference(
default=None,
validator=lambda v: True,
docs="""
Extra arguments to pass to compiler (if None, use either
``extra_compile_args_gcc`` or ``extra_compile_args_msvc``).
""",
),
extra_compile_args_gcc=BrianPreference(
default=default_buildopts,
docs="""
Extra compile arguments to pass to GCC compiler
""",
),
extra_compile_args_msvc=BrianPreference(
default=["/Ox", "/w", msvc_arch_flag, "/MP"],
docs="""
Extra compile arguments to pass to MSVC compiler (the default
``/arch:`` flag is determined based on the processor architecture)
""",
),
extra_link_args=BrianPreference(
default=[],
docs="""
Any extra platform- and compiler-specific information to use when
linking object files together.
""",
),
include_dirs=BrianPreference(
default=[os.path.join(prefix_dir, "include")],
docs="""
Include directories to use.
The default value is ``$prefix/include`` (or ``$prefix/Library/include``
on Windows), where ``$prefix`` is Python's site-specific directory
prefix as returned by `sys.prefix`. This will make compilation use
library files installed into a conda environment.
""",
),
library_dirs=BrianPreference(
default=[os.path.join(prefix_dir, "lib")],
docs="""
List of directories to search for C/C++ libraries at link time.
The default value is ``$prefix/lib`` (or ``$prefix/Library/lib``
on Windows), where ``$prefix`` is Python's site-specific directory
prefix as returned by `sys.prefix`. This will make compilation use
library files installed into a conda environment.
""",
),
runtime_library_dirs=BrianPreference(
default=[os.path.join(prefix_dir, "lib")] if sys.platform != "win32" else [],
docs="""
List of directories to search for C/C++ libraries at run time.
The default value is ``$prefix/lib`` (not used on Windows), where
``$prefix`` is Python's site-specific directory prefix as returned by
`sys.prefix`. This will make compilation use library files installed
into a conda environment.
""",
),
libraries=BrianPreference(
default=[],
docs="""
List of library names (not filenames or paths) to link against.
""",
),
headers=BrianPreference(
default=[],
docs="""
A list of strings specifying header files to use when compiling the
code. The list might look like ["<vector>","'my_header'"]. Note that
the header strings need to be in a form than can be pasted at the end
of a #include statement in the C++ code.
""",
),
define_macros=BrianPreference(
default=[],
docs="""
List of macros to define; each macro is defined using a 2-tuple,
where 'value' is either the string to define it to or None to
define it without a particular value (equivalent of "#define
FOO" in source or -DFOO on Unix C compiler command line).
""",
),
msvc_vars_location=BrianPreference(
default="",
docs="""
Location of the MSVC command line tool (or search for best by default).
""",
),
msvc_architecture=BrianPreference(
default="",
docs="""
MSVC architecture name (or use system architectue by default).
Could take values such as x86, amd64, etc.
""",
),
)
# check whether compiler supports a flag
# Adapted from https://github.com/pybind/pybind11/
def _determine_flag_compatibility(compiler, flagname):
import tempfile
from distutils.errors import CompileError
with (
tempfile.TemporaryDirectory(prefix="brian_flag_test_") as temp_dir,
std_silent(),
):
fname = os.path.join(temp_dir, "flag_test.cpp")
with open(fname, "w") as f:
f.write("int main (int argc, char **argv) { return 0; }")
try:
compiler.compile([fname], output_dir=temp_dir, extra_postargs=[flagname])
except CompileError:
logger.warn(f"Removing unsupported flag '{flagname}' from compiler flags.")
return False
return True
_compiler_flag_compatibility = {}
[docs]
def has_flag(compiler, flagname):
if compiler.compiler_type == "msvc":
# MSVC does not raise an error for illegal flags, so determining
# whether it accepts a flag would mean parsing the output for warnings
# This is non-trivial so we don't do it (the main reason to check
# flags in the first place are differences between gcc and clang)
return True
else:
compiler_exe = " ".join(compiler.executables["compiler_cxx"])
if (compiler_exe, flagname) not in _compiler_flag_compatibility:
compatibility = _determine_flag_compatibility(compiler, flagname)
_compiler_flag_compatibility[(compiler_exe, flagname)] = compatibility
return _compiler_flag_compatibility[(compiler_exe, flagname)]
[docs]
def get_compiler_and_args():
"""
Returns the computed compiler and compilation flags
"""
compiler = prefs["codegen.cpp.compiler"]
if compiler == "":
compiler = get_default_compiler()
extra_compile_args = prefs["codegen.cpp.extra_compile_args"]
if extra_compile_args is None:
if compiler in ("gcc", "unix"):
extra_compile_args = prefs["codegen.cpp.extra_compile_args_gcc"]
elif compiler == "msvc":
extra_compile_args = prefs["codegen.cpp.extra_compile_args_msvc"]
else:
extra_compile_args = []
logger.warn(f"Unsupported compiler '{compiler}'.")
from distutils.ccompiler import new_compiler
from distutils.sysconfig import customize_compiler
compiler_obj = new_compiler(compiler=compiler, verbose=0)
customize_compiler(compiler_obj)
extra_compile_args = [
flag for flag in extra_compile_args if has_flag(compiler_obj, flag)
]
return compiler, extra_compile_args
_msvc_env = None
[docs]
def get_msvc_env():
global _msvc_env
arch_name = prefs["codegen.cpp.msvc_architecture"]
if arch_name == "":
bits = struct.calcsize("P") * 8
if bits == 64:
arch_name = "x86_amd64"
else:
arch_name = "x86"
# Manual specification of vcvarsall.bat location by the user
vcvars_loc = prefs["codegen.cpp.msvc_vars_location"]
if vcvars_loc:
vcvars_cmd = f'"{vcvars_loc}" {arch_name}'
return None, vcvars_cmd
# Search for MSVC environment if not already cached
if _msvc_env is None:
try:
_msvc_env = _get_vc_env(arch_name)
except distutils.errors.DistutilsPlatformError:
raise OSError(
"Cannot find Microsoft Visual Studio, You "
"can try to set the path to vcvarsall.bat "
"via the codegen.cpp.msvc_vars_location "
"preference explicitly."
)
return _msvc_env, None
_compiler_supports_c99 = None
[docs]
def compiler_supports_c99():
global _compiler_supports_c99
if _compiler_supports_c99 is None:
if platform.system() == "Windows":
fd, tmp_file = tempfile.mkstemp(suffix=".cpp")
os.write(
fd,
b"""
#if _MSC_VER < 1800
#error
#endif
""",
)
os.close(fd)
msvc_env, vcvars_cmd = get_msvc_env()
if vcvars_cmd:
cmd = f"{vcvars_cmd} && cl /E {tmp_file} > NUL 2>&1"
else:
os.environ.update(msvc_env)
cmd = f"cl /E {tmp_file} > NUL 2>&1"
return_value = os.system(cmd)
_compiler_supports_c99 = return_value == 0
os.remove(tmp_file)
else:
cmd = (
'echo "#if (__STDC_VERSION__ < 199901L)\n#error\n#endif" | '
"cc -xc -E - > /dev/null 2>&1"
)
return_value = os.system(cmd)
_compiler_supports_c99 = return_value == 0
return _compiler_supports_c99
[docs]
class C99Check:
"""
Helper class to create objects that can be passed as an ``availability_check`` to
a `FunctionImplementation`.
"""
def __init__(self, name):
self.name = name
[docs]
def __call__(self, *args, **kwargs):
if not compiler_supports_c99():
raise NotImplementedError(
f'The "{self.name}" function needs C99 compiler support'
)