1"""
2SCons build script for Cantera
3
4Basic usage:
5    'scons help' - print a description of user-specifiable options.
6
7    'scons build' - Compile Cantera and the language interfaces using
8                    default options.
9
10    'scons clean' - Delete files created while building Cantera.
11
12    'scons install' - Install Cantera.
13
14    'scons uninstall' - Uninstall Cantera.
15
16    'scons test' - Run all tests which did not previously pass or for which the
17                   results may have changed.
18
19    'scons test-reset' - Reset the passing status of all tests.
20
21    'scons test-clean' - Delete files created while running the tests.
22
23    'scons test-help' - List available tests.
24
25    'scons test-NAME' - Run the test named "NAME".
26
27    'scons <command> dump' - Dump the state of the SCons environment to the
28                             screen instead of doing <command>, e.g.
29                             'scons build dump'. For debugging purposes.
30
31    'scons samples' - Compile the C++ and Fortran samples.
32
33    'scons msi' - Build a Windows installer (.msi) for Cantera.
34
35    'scons sphinx' - Build the Sphinx documentation
36
37    'scons doxygen' - Build the Doxygen documentation
38"""
39
40# This f-string is deliberately here to trigger a SyntaxError when
41# SConstruct is parsed by Python 2. This seems to be the most robust
42# and simplest option that will reliably trigger an error in Python 2
43# and provide actionable feedback for users.
44f"""
45Cantera must be built using Python 3.6 or higher. You can invoke SCons by executing
46    python3 `which scons`
47followed by any desired options.
48"""
49
50from pathlib import Path
51import sys
52import os
53import platform
54import subprocess
55import re
56import textwrap
57from os.path import join as pjoin
58from pkg_resources import parse_version
59import SCons
60from buildutils import *
61
62if not COMMAND_LINE_TARGETS:
63    # Print usage help
64    logger.info(__doc__, print_level=False)
65    sys.exit(0)
66
67valid_commands = ("build", "clean", "install", "uninstall",
68                  "help", "msi", "samples", "sphinx", "doxygen", "dump")
69
70for command in COMMAND_LINE_TARGETS:
71    if command not in valid_commands and not command.startswith('test'):
72        logger.error("Unrecognized command line target: {!r}", command)
73        sys.exit(1)
74
75if "clean" in COMMAND_LINE_TARGETS:
76    remove_directory("build")
77    remove_directory("stage")
78    remove_directory(".sconf_temp")
79    remove_directory("test/work")
80    remove_file(".sconsign.dblite")
81    remove_file("include/cantera/base/config.h")
82    remove_file("src/pch/system.h.gch")
83    remove_directory("include/cantera/ext")
84    remove_file("interfaces/cython/cantera/_cantera.cpp")
85    remove_file("interfaces/cython/cantera/_cantera.h")
86    remove_file("interfaces/cython/setup.py")
87    remove_file("interfaces/python_minimal/setup.py")
88    remove_file("config.log")
89    remove_directory("doc/sphinx/matlab/examples")
90    remove_file("doc/sphinx/matlab/examples.rst")
91    for name in Path("doc/sphinx/matlab/").glob("**/*.rst"):
92        if name.name != "index.rst":
93            remove_file(name)
94    remove_directory("doc/sphinx/cython/examples")
95    remove_file("doc/sphinx/cython/examples.rst")
96    remove_directory("interfaces/cython/Cantera.egg-info")
97    remove_directory("interfaces/python_minimal/Cantera_minimal.egg-info")
98    for name in Path("interfaces/cython/cantera/data/").iterdir():
99        if name.is_dir():
100            remove_directory(name)
101        elif name.name != "__init__.py":
102            remove_file(name)
103    remove_directory("interfaces/cython/cantera/test/data/test_subdir")
104    for name in Path("interfaces/cython/cantera/test/data/").iterdir():
105        if name.name != "__init__.py":
106            remove_file(name)
107    for name in Path(".").glob("*.msi"):
108        remove_file(name)
109    for name in Path("site_scons").glob("**/*.pyc"):
110        remove_file(name)
111    for name in Path("interfaces/python_minimal/cantera").iterdir():
112        if name.name != "__init__.py":
113            remove_file(name)
114    remove_file("interfaces/matlab/toolbox/cantera_shared.dll")
115    remove_file("interfaces/matlab/Contents.m")
116    remove_file("interfaces/matlab/ctpath.m")
117    for name in Path("interfaces/matlab/toolbox").glob("ctmethods.*"):
118        remove_file(name)
119
120    print("Done removing output files.")
121
122    if COMMAND_LINE_TARGETS == ["clean"]:
123        # Just exit if there's nothing else to do
124        sys.exit(0)
125    else:
126        Alias("clean", [])
127
128if "test-clean" in COMMAND_LINE_TARGETS:
129    remove_directory("build/test")
130    remove_directory("test/work")
131    remove_directory("build/python_local")
132
133# ******************************************************
134# *** Set system-dependent defaults for some options ***
135# ******************************************************
136
137logger.info("SCons is using the following Python interpreter: {}", sys.executable)
138
139opts = Variables('cantera.conf')
140
141windows_compiler_options = []
142extraEnvArgs = {}
143
144if os.name == 'nt':
145    # On Windows, target the same architecture as the current copy of Python,
146    # unless the user specified another option.
147    if '64 bit' in sys.version:
148        target_arch = 'amd64'
149    else:
150        target_arch = 'x86'
151
152    # Make an educated guess about the right default compiler
153    if which('g++') and not which('cl.exe'):
154        defaultToolchain = 'mingw'
155    else:
156        defaultToolchain = 'msvc'
157
158    windows_compiler_options.extend([
159        ('msvc_version',
160         """Version of Visual Studio to use. The default is the newest
161            installed version. Specify '12.0' for Visual Studio 2013 or '14.0'
162            for Visual Studio 2015.""",
163         ''),
164        EnumVariable(
165            'target_arch',
166            """Target architecture. The default is the same architecture as the
167               installed version of Python.""",
168            target_arch, ('amd64', 'x86'))
169    ])
170    opts.AddVariables(*windows_compiler_options)
171
172    pickCompilerEnv = Environment()
173    opts.Update(pickCompilerEnv)
174
175    if pickCompilerEnv['msvc_version']:
176        defaultToolchain = 'msvc'
177
178    windows_compiler_options.append(EnumVariable(
179        'toolchain',
180        """The preferred compiler toolchain.""",
181        defaultToolchain, ('msvc', 'mingw', 'intel')))
182    opts.AddVariables(windows_compiler_options[-1])
183    opts.Update(pickCompilerEnv)
184
185    if pickCompilerEnv['toolchain'] == 'msvc':
186        toolchain = ['default']
187        if pickCompilerEnv['msvc_version']:
188            extraEnvArgs['MSVC_VERSION'] = pickCompilerEnv['msvc_version']
189        print('INFO: Compiling with MSVC', (pickCompilerEnv['msvc_version'] or
190                                            pickCompilerEnv['MSVC_VERSION']))
191
192    elif pickCompilerEnv['toolchain'] == 'mingw':
193        toolchain = ['mingw', 'f90']
194        extraEnvArgs['F77'] = None
195        # Next line fixes http://scons.tigris.org/issues/show_bug.cgi?id=2683
196        extraEnvArgs['WINDOWS_INSERT_DEF'] = 1
197
198    elif pickCompilerEnv['toolchain'] == 'intel':
199        toolchain = ['intelc'] # note: untested
200
201    extraEnvArgs['TARGET_ARCH'] = pickCompilerEnv['target_arch']
202    print('INFO: Compiling for architecture:', pickCompilerEnv['target_arch'])
203    print('INFO: Compiling using the following toolchain(s):', repr(toolchain))
204
205else:
206    toolchain = ['default']
207
208env = Environment(tools=toolchain+['textfile', 'subst', 'recursiveInstall', 'wix', 'gch'],
209                  ENV={'PATH': os.environ['PATH']},
210                  toolchain=toolchain,
211                  **extraEnvArgs)
212
213env['OS'] = platform.system()
214env['OS_BITS'] = int(platform.architecture()[0][:2])
215if 'cygwin' in env['OS'].lower():
216    env['OS'] = 'Cygwin' # remove Windows version suffix
217
218# Fixes a linker error in Windows
219if os.name == 'nt' and 'TMP' in os.environ:
220    env['ENV']['TMP'] = os.environ['TMP']
221
222# Fixes issues with Python subprocesses. See http://bugs.python.org/issue13524
223if os.name == 'nt':
224    env['ENV']['SystemRoot'] = os.environ['SystemRoot']
225
226# Needed for Matlab to source ~/.matlab7rc.sh
227if 'HOME' in os.environ:
228    env['ENV']['HOME'] = os.environ['HOME']
229
230# Fix an issue with Unicode sneaking into the environment on Windows
231if os.name == 'nt':
232    for key,val in env['ENV'].items():
233        env['ENV'][key] = str(val)
234
235if 'FRAMEWORKS' not in env:
236    env['FRAMEWORKS'] = []
237
238add_RegressionTest(env)
239
240class defaults: pass
241
242if os.name == 'posix':
243    defaults.prefix = '/usr/local'
244    defaults.boostIncDir = ''
245    env['INSTALL_MANPAGES'] = True
246elif os.name == 'nt':
247    defaults.prefix = pjoin(os.environ['ProgramFiles'], 'Cantera')
248    defaults.boostIncDir = ''
249    env['INSTALL_MANPAGES'] = False
250else:
251    print("Error: Unrecognized operating system '%s'" % os.name)
252    sys.exit(1)
253
254compiler_options = [
255    ('CXX',
256     """The C++ compiler to use.""",
257     env['CXX']),
258    ('CC',
259     """The C compiler to use. This is only used to compile CVODE.""",
260     env['CC'])]
261opts.AddVariables(*compiler_options)
262opts.Update(env)
263
264defaults.cxxFlags = ''
265defaults.ccFlags = ''
266defaults.noOptimizeCcFlags = '-O0'
267defaults.optimizeCcFlags = '-O3'
268defaults.debugCcFlags = '-g'
269defaults.noDebugCcFlags = ''
270defaults.debugLinkFlags = ''
271defaults.noDebugLinkFlags = ''
272defaults.warningFlags = '-Wall'
273defaults.buildPch = False
274defaults.sphinx_options = '-W --keep-going'
275env['pch_flags'] = []
276env['openmp_flag'] = ['-fopenmp'] # used to generate sample build scripts
277
278env['using_apple_clang'] = False
279# Check if this is actually Apple's clang on macOS
280if env['OS'] == 'Darwin':
281    result = subprocess.check_output([env.subst('$CC'), '--version']).decode('utf-8')
282    if 'clang' in result.lower() and ('Xcode' in result or 'Apple' in result):
283        env['using_apple_clang'] = True
284        env['openmp_flag'].insert(0, '-Xpreprocessor')
285
286if 'gcc' in env.subst('$CC') or 'gnu-cc' in env.subst('$CC'):
287    defaults.optimizeCcFlags += ' -Wno-inline'
288    if env['OS'] == 'Cygwin':
289        # See http://stackoverflow.com/questions/18784112
290        defaults.cxxFlags = '-std=gnu++0x'
291    else:
292        defaults.cxxFlags = '-std=c++0x'
293    defaults.buildPch = True
294    env['pch_flags'] = ['-include', 'src/pch/system.h']
295
296elif env['CC'] == 'cl': # Visual Studio
297    defaults.cxxFlags = ['/EHsc']
298    defaults.ccFlags = ['/MD', '/nologo',
299                        '/D_SCL_SECURE_NO_WARNINGS', '/D_CRT_SECURE_NO_WARNINGS']
300    defaults.debugCcFlags = '/Zi /Fd${TARGET}.pdb'
301    defaults.noOptimizeCcFlags = '/Od /Ob0'
302    defaults.optimizeCcFlags = '/O2'
303    defaults.debugLinkFlags = '/DEBUG'
304    defaults.warningFlags = '/W3'
305    defaults.buildPch = True
306    env['pch_flags'] = ['/FIpch/system.h']
307    env['openmp_flag'] = ['/openmp']
308
309elif 'icc' in env.subst('$CC'):
310    defaults.cxxFlags = '-std=c++0x'
311    defaults.ccFlags = '-vec-report0 -diag-disable 1478'
312    defaults.warningFlags = '-Wcheck'
313    env['openmp_flag'] = ['-openmp']
314
315elif 'clang' in env.subst('$CC') or 'cc' in env.subst('$CC'):
316    defaults.ccFlags = '-fcolor-diagnostics'
317    defaults.cxxFlags = '-std=c++11'
318    defaults.buildPch = True
319    env['pch_flags'] = ['-include-pch', 'src/pch/system.h.gch']
320
321else:
322    print("WARNING: Unrecognized C compiler '%s'" % env['CC'])
323
324if env['OS'] in ('Windows', 'Darwin'):
325    defaults.threadFlags = ''
326else:
327    defaults.threadFlags = '-pthread'
328
329# InstallVersionedLib only fully functional in SCons >= 2.4.0
330# SHLIBVERSION fails with MinGW: http://scons.tigris.org/issues/show_bug.cgi?id=3035
331if (env['toolchain'] == 'mingw'
332    or parse_version(SCons.__version__) < parse_version('2.4.0')):
333    defaults.versionedSharedLibrary = False
334else:
335    defaults.versionedSharedLibrary = True
336
337defaults.fsLayout = 'compact' if env['OS'] == 'Windows' else 'standard'
338defaults.env_vars = 'PATH,LD_LIBRARY_PATH,PYTHONPATH'
339
340defaults.python_prefix = '$prefix' if env['OS'] != 'Windows' else ''
341
342# Transform lists into strings to keep cantera.conf clean
343for key,value in defaults.__dict__.items():
344    if isinstance(value, (list, tuple)):
345        setattr(defaults, key, ' '.join(value))
346
347# **************************************
348# *** Read user-configurable options ***
349# **************************************
350
351config_options = [
352    PathVariable(
353        'prefix',
354        'Set this to the directory where Cantera should be installed.',
355        defaults.prefix, PathVariable.PathAccept),
356    PathVariable(
357        'libdirname',
358        """Set this to the directory where Cantera libraries should be installed.
359           Some distributions (for example, Fedora/RHEL) use 'lib64' instead of 'lib' on 64-bit systems
360           or could use some other library directory name instead of 'lib' depends
361           on architecture and profile (for example, Gentoo 'libx32' on x32 profile).
362           If user didn't set 'libdirname' configuration variable set it to default value 'lib'""",
363        'lib', PathVariable.PathAccept),
364    EnumVariable(
365        'python_package',
366        """If you plan to work in Python, then you need the ``full`` Cantera Python
367           package. If, on the other hand, you will only use Cantera from some
368           other language (for example, MATLAB or Fortran 90/95) and only need Python
369           to process CTI files, then you only need a ``minimal`` subset of the
370           package and Cython and NumPy are not necessary. The ``none`` option
371           doesn't install any components of the Python interface. The default
372           behavior is to build the full Python module for whichever version of
373           Python is running SCons if the required prerequisites (NumPy and
374           Cython) are installed. Note: ``y`` is a synonym for ``full`` and ``n``
375           is a synonym for ``none``.""",
376        'default', ('full', 'minimal', 'none', 'n', 'y', 'default')),
377    PathVariable(
378        'python_cmd',
379        """Cantera needs to know where to find the Python interpreter. If
380           PYTHON_CMD is not set, then the configuration process will use the
381           same Python interpreter being used by SCons.""",
382        sys.executable, PathVariable.PathAccept),
383    PathVariable(
384        'python_prefix',
385        """Use this option if you want to install the Cantera Python package to
386           an alternate location. On Unix-like systems, the default is the same
387           as the 'prefix' option. If the 'python_prefix' option is set to
388           the empty string or the 'prefix' option is not set, then the package
389           will be installed to the system default 'site-packages' directory.
390           To install to the current user's 'site-packages' directory, use
391           'python_prefix=USER'.""",
392        defaults.python_prefix, PathVariable.PathAccept),
393    EnumVariable(
394        'matlab_toolbox',
395        """This variable controls whether the MATLAB toolbox will be built. If
396           set to 'y', you will also need to set the value of the 'matlab_path'
397           variable. If set to 'default', the MATLAB toolbox will be built if
398           'matlab_path' is set.""",
399        'default', ('y', 'n', 'default')),
400    PathVariable(
401        'matlab_path',
402        """Path to the MATLAB install directory. This should be the directory
403           containing the 'extern', 'bin', etc. subdirectories. Typical values
404           are: "C:/Program Files/MATLAB/R2011a" on Windows,
405           "/Applications/MATLAB_R2011a.app" on OS X, or
406           "/opt/MATLAB/R2011a" on Linux.""",
407        '', PathVariable.PathAccept),
408    EnumVariable(
409        'f90_interface',
410        """This variable controls whether the Fortran 90/95 interface will be
411           built. If set to 'default', the builder will look for a compatible
412           Fortran compiler in the 'PATH' environment variable, and compile
413           the Fortran 90 interface if one is found.""",
414        'default', ('y', 'n', 'default')),
415    PathVariable(
416        'FORTRAN',
417        """The Fortran (90) compiler. If unspecified, the builder will look for
418           a compatible compiler (pgfortran, gfortran, ifort, g95) in the 'PATH' environment
419           variable. Used only for compiling the Fortran 90 interface.""",
420        '', PathVariable.PathAccept),
421    ('FORTRANFLAGS',
422     'Compilation options for the Fortran (90) compiler.',
423     '-O3'),
424    BoolVariable(
425        'coverage',
426        """Enable collection of code coverage information with gcov.
427           Available only when compiling with gcc.""",
428        False),
429    BoolVariable(
430        'doxygen_docs',
431        """Build HTML documentation for the C++ interface using Doxygen.""",
432        False),
433    BoolVariable(
434        'sphinx_docs',
435        """Build HTML documentation for Cantera using Sphinx.""",
436        False),
437    PathVariable(
438        'sphinx_cmd',
439        """Command to use for building the Sphinx documentation.""",
440        'sphinx-build', PathVariable.PathAccept),
441    (
442        "sphinx_options",
443        """Options passed to the 'sphinx_cmd' command line. Separate multiple
444           options with spaces, for example, "-W --keep-going".""",
445        defaults.sphinx_options,
446    ),
447    EnumVariable(
448        'system_eigen',
449        """Select whether to use Eigen from a system installation ('y'), from a
450           Git submodule ('n'), or to decide automatically ('default'). If Eigen
451           is not installed directly into a system include directory, for example, it is
452           installed in '/opt/include/eigen3/Eigen', then you will need to add
453           '/opt/include/eigen3' to 'extra_inc_dirs'.
454           """,
455        'default', ('default', 'y', 'n')),
456    EnumVariable(
457        'system_fmt',
458        """Select whether to use the fmt library from a system installation
459           ('y'), from a Git submodule ('n'), or to decide automatically
460           ('default').  If you do not want to use the Git submodule and fmt
461           is not installed directly into system include and library
462           directories, then you will need to add those directories to
463           'extra_inc_dirs' and 'extra_lib_dirs'. This installation of fmt
464           must include the shared version of the library, for example,
465           'libfmt.so'.""",
466        'default', ('default', 'y', 'n')),
467    EnumVariable(
468        'system_yamlcpp',
469        """Select whether to use the yaml-cpp library from a system installation
470           ('y'), from a Git submodule ('n'), or to decide automatically
471           ('default'). If yaml-cpp is not installed directly into system
472           include and library directories, then you will need to add those
473           directories to 'extra_inc_dirs' and 'extra_lib_dirs'.""",
474        'default', ('default', 'y', 'n')),
475    EnumVariable(
476        'system_sundials',
477        """Select whether to use SUNDIALS from a system installation ('y'), from
478           a Git submodule ('n'), or to decide automatically ('default').
479           Specifying 'sundials_include' or 'sundials_libdir' changes the
480           default to 'y'.""",
481        'default', ('default', 'y', 'n')),
482    PathVariable(
483        'sundials_include',
484        """The directory where the SUNDIALS header files are installed. This
485           should be the directory that contains the "cvodes", "nvector", etc.
486           subdirectories. Not needed if the headers are installed in a
487           standard location, for example, '/usr/include'.""",
488        '', PathVariable.PathAccept),
489    PathVariable(
490        'sundials_libdir',
491        """The directory where the SUNDIALS static libraries are installed.
492           Not needed if the libraries are installed in a standard location,
493           for example, '/usr/lib'.""",
494        '', PathVariable.PathAccept),
495    (
496        'blas_lapack_libs',
497        """Cantera can use BLAS and LAPACK libraries available on your system if
498           you have optimized versions available (for example, Intel MKL). Otherwise,
499           Cantera will use Eigen for linear algebra support. To use BLAS
500           and LAPACK, set 'blas_lapack_libs' to the the list of libraries
501           that should be passed to the linker, separated by commas, for example,
502           "lapack,blas" or "lapack,f77blas,cblas,atlas". Eigen is required
503           whether or not BLAS/LAPACK are used.""",
504        ''),
505    PathVariable(
506        'blas_lapack_dir',
507        """Directory containing the libraries specified by 'blas_lapack_libs'. Not
508           needed if the libraries are installed in a standard location, for example,
509           ``/usr/lib``.""",
510        '', PathVariable.PathAccept),
511    EnumVariable(
512        'lapack_names',
513        """Set depending on whether the procedure names in the specified
514           libraries are lowercase or uppercase. If you don't know, run 'nm' on
515           the library file (for example, 'nm libblas.a').""",
516        'lower', ('lower','upper')),
517    BoolVariable(
518        'lapack_ftn_trailing_underscore',
519        """Controls whether the LAPACK functions have a trailing underscore
520           in the Fortran libraries.""",
521        True),
522    BoolVariable(
523        'lapack_ftn_string_len_at_end',
524        """Controls whether the LAPACK functions have the string length
525           argument at the end of the argument list ('yes') or after
526           each argument ('no') in the Fortran libraries.""",
527        True),
528    EnumVariable(
529        'googletest',
530        """Select whether to use gtest/gmock from system
531           installation ('system'), from a Git submodule ('submodule'), to decide
532           automatically ('default') or don't look for gtest/gmock ('none')
533           and don't run tests that depend on gtest/gmock.""",
534        'default', ('default', 'system', 'submodule', 'none')),
535    (
536        'env_vars',
537        """Environment variables to propagate through to SCons. Either the
538           string "all" or a comma separated list of variable names, for example,
539           'LD_LIBRARY_PATH,HOME'.""",
540        defaults.env_vars),
541    BoolVariable(
542        'use_pch',
543        """Use a precompiled-header to speed up compilation""",
544        defaults.buildPch),
545    (
546        'cxx_flags',
547        """Compiler flags passed to the C++ compiler only. Separate multiple
548           options with spaces, for example, "cxx_flags='-g -Wextra -O3 --std=c++11'"
549           """,
550        defaults.cxxFlags),
551    (
552        'cc_flags',
553        """Compiler flags passed to both the C and C++ compilers, regardless of optimization level.""",
554        defaults.ccFlags),
555    (
556        'thread_flags',
557        """Compiler and linker flags for POSIX multithreading support.""",
558        defaults.threadFlags),
559    BoolVariable(
560        'optimize',
561        """Enable extra compiler optimizations specified by the
562           'optimize_flags' variable, instead of the flags specified by the
563           'no_optimize_flags' variable.""",
564        True),
565    (
566        'optimize_flags',
567        """Additional compiler flags passed to the C/C++ compiler when 'optimize=yes'.""",
568        defaults.optimizeCcFlags),
569    (
570        'no_optimize_flags',
571        """Additional compiler flags passed to the C/C++ compiler when 'optimize=no'.""",
572        defaults.noOptimizeCcFlags),
573    BoolVariable(
574        'debug',
575        """Enable compiler debugging symbols.""",
576        True),
577    (
578        'debug_flags',
579        """Additional compiler flags passed to the C/C++ compiler when 'debug=yes'.""",
580        defaults.debugCcFlags),
581    (
582        'no_debug_flags',
583        """Additional compiler flags passed to the C/C++ compiler when 'debug=no'.""",
584        defaults.noDebugCcFlags),
585    (
586        'debug_linker_flags',
587        """Additional options passed to the linker when 'debug=yes'.""",
588        defaults.debugLinkFlags),
589    (
590        'no_debug_linker_flags',
591        """Additional options passed to the linker when 'debug=no'.""",
592        defaults.noDebugLinkFlags),
593    (
594        'warning_flags',
595        """Additional compiler flags passed to the C/C++ compiler to enable
596           extra warnings. Used only when compiling source code that is part
597           of Cantera (for example, excluding code in the 'ext' directory).""",
598        defaults.warningFlags),
599    (
600        'extra_inc_dirs',
601        """Additional directories to search for header files, with multiple
602        directories separated by colons (*nix, macOS) or semicolons (Windows)""",
603        ''),
604    (
605        'extra_lib_dirs',
606        """Additional directories to search for libraries, with multiple
607        directories separated by colons (*nix, macOS) or semicolons (Windows)""",
608        ''),
609    PathVariable(
610        'boost_inc_dir',
611        """Location of the Boost header files. Not needed if the headers are
612           installed in a standard location, for example, '/usr/include'.""",
613        defaults.boostIncDir, PathVariable.PathAccept),
614    PathVariable(
615        'stage_dir',
616        """Directory relative to the Cantera source directory to be
617           used as a staging area for building for example, a Debian
618           package. If specified, 'scons install' will install files
619           to 'stage_dir/prefix/...'.""",
620        '',
621        PathVariable.PathAccept),
622    BoolVariable(
623        'VERBOSE',
624        """Create verbose output about what SCons is doing.""",
625        False),
626    (
627        'gtest_flags',
628        """Additional options passed to each GTest test suite, for example,
629           '--gtest_filter=*pattern*'. Separate multiple options with spaces.""",
630        ''),
631    BoolVariable(
632        'renamed_shared_libraries',
633        """If this option is turned on, the shared libraries that are created
634           will be renamed to have a '_shared' extension added to their base name.
635           If not, the base names will be the same as the static libraries.
636           In some cases this simplifies subsequent linking environments with
637           static libraries and avoids a bug with using valgrind with
638           the '-static' linking flag.""",
639        True),
640    BoolVariable(
641        'versioned_shared_library',
642        """If enabled, create a versioned shared library, with symlinks to the
643           more generic library name, for example, 'libcantera_shared.so.2.5.0' as the
644           actual library and 'libcantera_shared.so' and 'libcantera_shared.so.2'
645           as symlinks.
646           """,
647        defaults.versionedSharedLibrary),
648    BoolVariable(
649        'use_rpath_linkage',
650        """If enabled, link to all shared libraries using 'rpath', i.e., a fixed
651           run-time search path for dynamic library loading.""",
652        True),
653    EnumVariable(
654        'layout',
655        """The layout of the directory structure. 'standard' installs files to
656           several subdirectories under 'prefix', for example, 'prefix/bin',
657           'prefix/include/cantera', 'prefix/lib' etc. This layout is best used in
658           conjunction with "prefix'='/usr/local'". 'compact' puts all installed
659           files in the subdirectory defined by 'prefix'. This layout is best
660           with a prefix like '/opt/cantera'. 'debian' installs to the stage
661           directory in a layout used for generating Debian packages.""",
662        defaults.fsLayout, ('standard','compact','debian')),
663    BoolVariable(
664        "fast_fail_tests",
665        """If enabled, tests will exit at the first failure.""",
666        False),
667    BoolVariable(
668        "skip_slow_tests",
669        """If enabled, skip a subset of tests that are known to have long runtimes.
670           Skipping these may be desirable when running with options that cause tests
671           to run slowly, like disabling optimization or activating code profiling.""",
672        False),
673    BoolVariable(
674        "show_long_tests",
675        """If enabled, duration of slowest tests will be shown.""",
676        False),
677    BoolVariable(
678        "verbose_tests",
679        """If enabled, verbose test output will be shown.""",
680        False),
681    BoolVariable(
682        "legacy_rate_constants",
683        """If enabled, rate constant calculations include third-body concentrations
684        for three-body reactions, which corresponds to the legacy implementation.
685        For Cantera 2.6, the option remains enabled (no change compared to past
686        behavior). After Cantera 2.6, the default will be to disable this option,
687        and rate constant calculations will be consistent with conventional
688        definitions (see Eq. 9.75 in Kee, Coltrin and Glarborg, 'Chemically Reacting
689        Flow', Wiley Interscience, 2003).""",
690        True),
691]
692
693opts.AddVariables(*config_options)
694opts.Update(env)
695opts.Save('cantera.conf', env)
696
697# Expand ~/ and environment variables used in cantera.conf (variables used on
698# the command line will be expanded by the shell)
699for option in opts.keys():
700    original = env[option]
701    if isinstance(original, str):
702        modified = os.path.expandvars(os.path.expanduser(env[option]))
703        if original != modified:
704            print('INFO: Expanding {!r} to {!r}'.format(original, modified))
705            env[option] = modified
706
707if "help" in COMMAND_LINE_TARGETS:
708    help(env, opts)
709    sys.exit(0)
710
711if 'doxygen' in COMMAND_LINE_TARGETS:
712    env['doxygen_docs'] = True
713if 'sphinx' in COMMAND_LINE_TARGETS:
714    env['sphinx_docs'] = True
715
716valid_arguments = (set(opt[0] for opt in windows_compiler_options) |
717                   set(opt[0] for opt in compiler_options) |
718                   set(opt[0] for opt in config_options))
719for arg in ARGUMENTS:
720    if arg not in valid_arguments:
721        print('Encountered unexpected command line argument: %r' % arg)
722        #sys.exit(1)
723
724env["cantera_version"] = "2.6.0a3"
725# For use where pre-release tags are not permitted (MSI, sonames)
726env['cantera_pure_version'] = re.match(r'(\d+\.\d+\.\d+)', env['cantera_version']).group(0)
727env['cantera_short_version'] = re.match(r'(\d+\.\d+)', env['cantera_version']).group(0)
728
729#try:
730#    env["git_commit"] = get_command_output("git", "rev-parse", "--short", "HEAD")
731#except subprocess.CalledProcessError:
732#    env["git_commit"] = "unknown"
733env["git_commit"] = "unknown"
734
735# Print values of all build options:
736print("Configuration variables read from 'cantera.conf' and command line:")
737for line in open('cantera.conf'):
738    print('   ', line.strip())
739print()
740
741# ********************************************
742# *** Configure system-specific properties ***
743# ********************************************
744
745# Copy in external environment variables
746if env['env_vars'] == 'all':
747    env['ENV'].update(os.environ)
748    if 'PYTHONHOME' in env['ENV']:
749        del env['ENV']['PYTHONHOME']
750elif env['env_vars']:
751    for name in env['env_vars'].split(','):
752        if name in os.environ:
753            if name == 'PATH':
754                env.AppendENVPath('PATH', os.environ['PATH'])
755            else:
756                env['ENV'][name] = os.environ[name]
757            if env['VERBOSE']:
758                print('Propagating environment variable {0}={1}'.format(name, env['ENV'][name]))
759        elif name not in defaults.env_vars.split(','):
760            print('WARNING: failed to propagate environment variable', repr(name))
761            print('         Edit cantera.conf or the build command line to fix this.')
762
763# @todo: Remove this Warning after Cantera 2.5
764if os.pathsep == ';':
765    for dirs in (env['extra_inc_dirs'], env['extra_lib_dirs']):
766        if re.search(r':\w:', dirs):
767            print('ERROR: Multiple entries in "extra_inc_dirs" and "extra_lib_dirs" '
768                  'should be separated by semicolons (;) on Windows. Use of OS-specific '
769                  'path separator introduced in Cantera 2.5.')
770            sys.exit(1)
771
772env['extra_inc_dirs'] = [d for d in env['extra_inc_dirs'].split(os.pathsep) if d]
773env['extra_lib_dirs'] = [d for d in env['extra_lib_dirs'].split(os.pathsep) if d]
774
775env.Append(CPPPATH=env['extra_inc_dirs'],
776           LIBPATH=env['extra_lib_dirs'])
777
778if env['use_rpath_linkage']:
779    env.Append(RPATH=env['extra_lib_dirs'])
780
781if env['CC'] == 'cl':
782    # embed manifest file
783    env['LINKCOM'] = [env['LINKCOM'],
784                      'if exist ${TARGET}.manifest mt.exe -nologo -manifest ${TARGET}.manifest -outputresource:$TARGET;1']
785    env['SHLINKCOM'] = [env['SHLINKCOM'],
786                        'if exist ${TARGET}.manifest mt.exe -nologo -manifest ${TARGET}.manifest -outputresource:$TARGET;2']
787    env['FORTRAN_LINK'] = 'link'
788else:
789    env['FORTRAN_LINK'] = '$FORTRAN'
790
791if env['boost_inc_dir']:
792    env.Append(CPPPATH=env['boost_inc_dir'])
793
794if env['blas_lapack_dir']:
795    env.Append(LIBPATH=[env['blas_lapack_dir']])
796    if env['use_rpath_linkage']:
797        env.Append(RPATH=env['blas_lapack_dir'])
798
799if env['system_sundials'] in ('y','default'):
800    if env['sundials_include']:
801        env.Append(CPPPATH=[env['sundials_include']])
802        env['system_sundials'] = 'y'
803    if env['sundials_libdir']:
804        env.Append(LIBPATH=[env['sundials_libdir']])
805        env['system_sundials'] = 'y'
806        if env['use_rpath_linkage']:
807            env.Append(RPATH=env['sundials_libdir'])
808
809# BLAS / LAPACK configuration
810if env['blas_lapack_libs'] != '':
811    env['blas_lapack_libs'] = env['blas_lapack_libs'].split(',')
812    env['use_lapack'] = True
813elif env['OS'] == 'Darwin':
814    env['blas_lapack_libs'] = []
815    env['use_lapack'] = True
816    env.Append(FRAMEWORKS=['Accelerate'])
817else:
818    env['blas_lapack_libs'] = []
819    env['use_lapack'] = False
820
821# ************************************
822# *** Compiler Configuration Tests ***
823# ************************************
824
825def CheckStatement(context, function, includes=""):
826    context.Message('Checking for %s... ' % function)
827    src = """
828%(include)s
829int main(int argc, char** argv) {
830    %(func)s;
831    return 0;
832}
833""" % {'func':function, 'include':includes}
834    result = context.TryCompile(src, '.cpp')
835    context.Result(result)
836    return result
837
838conf = Configure(env, custom_tests={'CheckStatement': CheckStatement})
839
840# Set up compiler options before running configuration tests
841env['CXXFLAGS'] = listify(env['cxx_flags'])
842env['CCFLAGS'] = listify(env['cc_flags']) + listify(env['thread_flags'])
843env['LINKFLAGS'] += listify(env['thread_flags'])
844env['CPPDEFINES'] = {}
845
846env['warning_flags'] = listify(env['warning_flags'])
847
848if env['optimize']:
849    env['CCFLAGS'] += listify(env['optimize_flags'])
850    env.Append(CPPDEFINES=['NDEBUG'])
851else:
852    env['CCFLAGS'] += listify(env['no_optimize_flags'])
853
854if env['debug']:
855    env['CCFLAGS'] += listify(env['debug_flags'])
856    env['LINKFLAGS'] += listify(env['debug_linker_flags'])
857else:
858    env['CCFLAGS'] += listify(env['no_debug_flags'])
859    env['LINKFLAGS'] += listify(env['no_debug_linker_flags'])
860
861if env['coverage']:
862    if  'gcc' in env.subst('$CC') or 'clang' in env.subst('$CC'):
863        env.Append(CCFLAGS=['-fprofile-arcs', '-ftest-coverage'])
864        env.Append(LINKFLAGS=['-fprofile-arcs', '-ftest-coverage'])
865
866    else:
867        print('Error: coverage testing is only available with GCC.')
868        exit(0)
869
870if env['toolchain'] == 'mingw':
871    env.Append(LINKFLAGS=['-static-libgcc', '-static-libstdc++'])
872
873def config_error(message):
874    print('ERROR:', message)
875    if env['VERBOSE']:
876        print('*' * 25, 'Contents of config.log:', '*' * 25)
877        print(open('config.log').read())
878        print('*' * 28, 'End of config.log', '*' * 28)
879    else:
880        print("See 'config.log' for details.")
881    sys.exit(1)
882
883# First, a sanity check:
884if not conf.CheckCXXHeader('cmath', '<>'):
885    config_error('The C++ compiler is not correctly configured.')
886
887
888def get_expression_value(includes, expression, defines=()):
889    s = ['#define ' + d for d in defines]
890    s.extend('#include ' + i for i in includes)
891    s.extend(('#define Q(x) #x',
892              '#define QUOTE(x) Q(x)',
893              '#include <iostream>',
894              '#ifndef SUNDIALS_PACKAGE_VERSION', # name change in Sundials >= 3.0
895              '#define SUNDIALS_PACKAGE_VERSION SUNDIALS_VERSION',
896              '#endif',
897              'int main(int argc, char** argv) {',
898              '    std::cout << %s << std::endl;' % expression,
899              '    return 0;',
900              '}\n'))
901    return '\n'.join(s)
902
903# Check for fmt library and checkout submodule if needed
904# Test for 'ostream.h' to ensure that version >= 3.0.0 is available
905if env['system_fmt'] in ('y', 'default'):
906    if conf.CheckCXXHeader('fmt/ostream.h', '""'):
907        env['system_fmt'] = True
908        print("""INFO: Using system installation of fmt library.""")
909
910    elif env['system_fmt'] == 'y':
911        config_error('Expected system installation of fmt library, but it '
912            'could not be found.')
913
914if env['system_fmt'] in ('n', 'default'):
915    env['system_fmt'] = False
916    print("""INFO: Using private installation of fmt library.""")
917    if not os.path.exists('ext/fmt/include/fmt/ostream.h'):
918        if not os.path.exists('.git'):
919            config_error('fmt is missing. Install source in ext/fmt.')
920
921        try:
922            code = subprocess.call(['git','submodule','update','--init',
923                                    '--recursive','ext/fmt'])
924        except Exception:
925            code = -1
926        if code:
927            config_error('fmt submodule checkout failed.\n'
928                         'Try manually checking out the submodule with:\n\n'
929                         '    git submodule update --init --recursive ext/fmt\n')
930
931fmt_include = '<fmt/format.h>' if env['system_fmt'] else '"../ext/fmt/include/fmt/format.h"'
932fmt_version_source = get_expression_value([fmt_include], 'FMT_VERSION', ['FMT_HEADER_ONLY'])
933retcode, fmt_lib_version = conf.TryRun(fmt_version_source, '.cpp')
934try:
935    fmt_lib_version = divmod(float(fmt_lib_version.strip()), 10000)
936    (fmt_maj, (fmt_min, fmt_pat)) = fmt_lib_version[0], divmod(fmt_lib_version[1], 100)
937    env['FMT_VERSION'] = '{major:.0f}.{minor:.0f}.{patch:.0f}'.format(major=fmt_maj, minor=fmt_min, patch=fmt_pat)
938    print('INFO: Found fmt version {}'.format(env['FMT_VERSION']))
939except ValueError:
940    env['FMT_VERSION'] = '0.0.0'
941    print('INFO: Could not find version of fmt')
942
943# Check for yaml-cpp library and checkout submodule if needed
944if env['system_yamlcpp'] in ('y', 'default'):
945    if conf.CheckCXXHeader('yaml-cpp/yaml.h', '""'):
946        env['system_yamlcpp'] = True
947        print("""INFO: Using system installation of yaml-cpp library.""")
948
949    elif env['system_yamlcpp'] == 'y':
950        config_error('Expected system installation of yaml-cpp library, but it '
951            'could not be found.')
952
953if env['system_yamlcpp'] in ('n', 'default'):
954    env['system_yamlcpp'] = False
955    print("""INFO: Using private installation of yaml-cpp library.""")
956    if not os.path.exists('ext/yaml-cpp/include/yaml-cpp/yaml.h'):
957        if not os.path.exists('.git'):
958            config_error('yaml-cpp is missing. Install source in ext/yaml-cpp.')
959
960        try:
961            code = subprocess.call(['git', 'submodule', 'update', '--init',
962                                    '--recursive', 'ext/yaml-cpp'])
963        except Exception:
964            code = -1
965        if code:
966            config_error('yaml-cpp submodule checkout failed.\n'
967                         'Try manually checking out the submodule with:\n\n'
968                         '    git submodule update --init --recursive ext/yaml-cpp\n')
969
970# Check for googletest and checkout submodule if needed
971if env['googletest'] in ('system', 'default'):
972    has_gtest = conf.CheckCXXHeader('gtest/gtest.h', '""')
973    has_gmock = conf.CheckCXXHeader('gmock/gmock.h', '""')
974    if has_gtest and has_gmock:
975        env['googletest'] = 'system'
976        print("""INFO: Using system installation of Googletest""")
977    elif env['googletest'] == 'system':
978        config_error('Expected system installation of Googletest-1.8.0, but it '
979                     'could not be found.')
980
981if env['googletest'] in ('submodule', 'default'):
982    env['googletest'] = 'submodule'
983    has_gtest = os.path.exists('ext/googletest/googletest/include/gtest/gtest.h')
984    has_gmock = os.path.exists('ext/googletest/googlemock/include/gmock/gmock.h')
985    if not (has_gtest and has_gmock):
986        if not os.path.exists('.git'):
987            config_error('Googletest is missing. Install source in ext/googletest.')
988
989        try:
990            code = subprocess.call(['git','submodule','update','--init',
991                                    '--recursive','ext/googletest'])
992        except Exception:
993            code = -1
994        if code:
995            config_error('Googletest not found and submodule checkout failed.\n'
996                         'Try manually checking out the submodule with:\n\n'
997                         '    git submodule update --init --recursive ext/googletest\n')
998    print("""INFO: Using Googletest from Git submodule""")
999
1000if env['googletest'] == 'none':
1001    print("""INFO: Not using Googletest -- unable to run complete test suite""")
1002
1003# Check for Eigen and checkout submodule if needed
1004if env['system_eigen'] in ('y', 'default'):
1005    if conf.CheckCXXHeader('Eigen/Dense', '<>'):
1006        env['system_eigen'] = True
1007        print("""INFO: Using system installation of Eigen.""")
1008    elif env['system_eigen'] == 'y':
1009        config_error('Expected system installation of Eigen, but it '
1010                     'could not be found.')
1011
1012if env['system_eigen'] in ('n', 'default'):
1013    env['system_eigen'] = False
1014    print("""INFO: Using private installation of Eigen.""")
1015    if not os.path.exists('ext/eigen/Eigen/Dense'):
1016        if not os.path.exists('.git'):
1017            config_error('Eigen is missing. Install Eigen in ext/eigen.')
1018
1019        try:
1020            code = subprocess.call(['git','submodule','update','--init',
1021                                    '--recursive','ext/eigen'])
1022        except Exception:
1023            code = -1
1024        if code:
1025            config_error('Eigen not found and submodule checkout failed.\n'
1026                         'Try manually checking out the submodule with:\n\n'
1027                         '    git submodule update --init --recursive ext/eigen\n')
1028
1029eigen_include = '<Eigen/Core>' if env['system_eigen'] else '"../ext/eigen/Eigen/Core"'
1030eigen_versions = 'QUOTE(EIGEN_WORLD_VERSION) "." QUOTE(EIGEN_MAJOR_VERSION) "." QUOTE(EIGEN_MINOR_VERSION)'
1031eigen_version_source = get_expression_value([eigen_include], eigen_versions)
1032retcode, eigen_lib_version = conf.TryRun(eigen_version_source, '.cpp')
1033env['EIGEN_LIB_VERSION'] = eigen_lib_version.strip()
1034print('INFO: Found Eigen version {}'.format(env['EIGEN_LIB_VERSION']))
1035
1036# Determine which standard library to link to when using Fortran to
1037# compile code that links to Cantera
1038if conf.CheckDeclaration('__GLIBCXX__', '#include <iostream>', 'C++'):
1039    env['cxx_stdlib'] = ['stdc++']
1040elif conf.CheckDeclaration('_LIBCPP_VERSION', '#include <iostream>', 'C++'):
1041    env['cxx_stdlib'] = ['c++']
1042else:
1043    env['cxx_stdlib'] = []
1044
1045env['HAS_CLANG'] = conf.CheckDeclaration('__clang__', '', 'C++')
1046env['HAS_OPENMP'] = conf.CheckLibWithHeader("omp", "omp.h", language="C++")
1047
1048boost_version_source = get_expression_value(['<boost/version.hpp>'], 'BOOST_LIB_VERSION')
1049retcode, boost_lib_version = conf.TryRun(boost_version_source, '.cpp')
1050env['BOOST_LIB_VERSION'] = '.'.join(boost_lib_version.strip().split('_'))
1051if not env['BOOST_LIB_VERSION']:
1052    config_error("Boost could not be found. Install Boost headers or set"
1053                 " 'boost_inc_dir' to point to the boost headers.")
1054else:
1055    print('INFO: Found Boost version {0}'.format(env['BOOST_LIB_VERSION']))
1056# demangle is availble in Boost 1.55 or newer
1057env['has_demangle'] = conf.CheckDeclaration("boost::core::demangle",
1058                                '#include <boost/core/demangle.hpp>', 'C++')
1059
1060import SCons.Conftest, SCons.SConf
1061context = SCons.SConf.CheckContext(conf)
1062
1063# Check initially for Sundials<=3.2 and then for Sundials>=4.0
1064for cvode_call in ['CVodeCreate(CV_BDF, CV_NEWTON);','CVodeCreate(CV_BDF);']:
1065    ret = SCons.Conftest.CheckLib(context,
1066                                  ['sundials_cvodes'],
1067                                  header='#include "cvodes/cvodes.h"',
1068                                  language='C++',
1069                                  call=cvode_call,
1070                                  autoadd=False,
1071                                  extra_libs=env['blas_lapack_libs'])
1072    # CheckLib returns False to indicate success
1073    if not ret:
1074        if env['system_sundials'] == 'default':
1075            env['system_sundials'] = 'y'
1076        break
1077
1078# Execute if the cycle ends without 'break'
1079else:
1080    if env['system_sundials'] == 'default':
1081        env['system_sundials'] = 'n'
1082    elif env['system_sundials'] == 'y':
1083        config_error('Expected system installation of Sundials, but it could '
1084                     'not be found.')
1085
1086# Checkout Sundials submodule if needed
1087if (env['system_sundials'] == 'n' and
1088    not os.path.exists('ext/sundials/include/cvodes/cvodes.h')):
1089    if not os.path.exists('.git'):
1090        config_error('Sundials is missing. Install source in ext/sundials.')
1091
1092    try:
1093        code = subprocess.call(['git','submodule','update','--init',
1094                                '--recursive','ext/sundials'])
1095    except Exception:
1096        code = -1
1097    if code:
1098        config_error('Sundials not found and submodule checkout failed.\n'
1099                     'Try manually checking out the submodule with:\n\n'
1100                     '    git submodule update --init --recursive ext/sundials\n')
1101
1102
1103env['NEED_LIBM'] = not conf.CheckLibWithHeader(None, 'math.h', 'C',
1104                                               'double x; log(x);', False)
1105env['LIBM'] = ['m'] if env['NEED_LIBM'] else []
1106
1107if env['system_sundials'] == 'y':
1108    for subdir in ('sundials', 'nvector', 'cvodes', 'ida', 'sunlinsol', 'sunmatrix'):
1109        remove_directory('include/cantera/ext/' + subdir)
1110
1111    # Determine Sundials version
1112    sundials_version_source = get_expression_value(['"sundials/sundials_config.h"'],
1113                                                   'QUOTE(SUNDIALS_PACKAGE_VERSION)')
1114    retcode, sundials_version = conf.TryRun(sundials_version_source, '.cpp')
1115    if retcode == 0:
1116        config_error("Failed to determine Sundials version.")
1117    sundials_version = sundials_version.strip(' "\n')
1118
1119    # Ignore the minor version, e.g. 2.4.x -> 2.4
1120    env['sundials_version'] = '.'.join(sundials_version.split('.')[:2])
1121    sundials_ver = parse_version(env['sundials_version'])
1122    if sundials_ver < parse_version('2.4') or sundials_ver >= parse_version('6.0'):
1123        print("""ERROR: Sundials version %r is not supported.""" % env['sundials_version'])
1124        sys.exit(1)
1125    elif sundials_ver > parse_version('5.7'):
1126        print("WARNING: Sundials version %r has not been tested." % env['sundials_version'])
1127
1128    print("""INFO: Using system installation of Sundials version %s.""" % sundials_version)
1129
1130    # Determine whether or not Sundials was built with BLAS/LAPACK
1131    if sundials_ver < parse_version('2.6'):
1132        # In Sundials 2.4 / 2.5, SUNDIALS_BLAS_LAPACK is either 0 or 1
1133        sundials_blas_lapack = get_expression_value(['"sundials/sundials_config.h"'],
1134                                                       'SUNDIALS_BLAS_LAPACK')
1135        retcode, has_sundials_lapack = conf.TryRun(sundials_blas_lapack, '.cpp')
1136        if retcode == 0:
1137            config_error("Failed to determine Sundials BLAS/LAPACK.")
1138        env['has_sundials_lapack'] = int(has_sundials_lapack.strip())
1139    elif sundials_ver < parse_version('5.5'):
1140        # In Sundials 2.6-5.5, SUNDIALS_BLAS_LAPACK is either defined or undefined
1141        env['has_sundials_lapack'] = conf.CheckDeclaration('SUNDIALS_BLAS_LAPACK',
1142                '#include "sundials/sundials_config.h"', 'C++')
1143    else:
1144        # In Sundials 5.5 and higher, two defines are included specific to the
1145        # SUNLINSOL packages indicating whether SUNDIALS has been built with LAPACK
1146        lapackband = conf.CheckDeclaration(
1147            "SUNDIALS_SUNLINSOL_LAPACKBAND",
1148            '#include "sundials/sundials_config.h"',
1149            "C++",
1150        )
1151        lapackdense = conf.CheckDeclaration(
1152            "SUNDIALS_SUNLINSOL_LAPACKDENSE",
1153            '#include "sundials/sundials_config.h"',
1154            "C++",
1155        )
1156        env["has_sundials_lapack"] = lapackband and lapackdense
1157
1158    # In the case where a user is trying to link Cantera to an external BLAS/LAPACK
1159    # library, but Sundials was configured without this support, print a Warning.
1160    if not env['has_sundials_lapack'] and env['use_lapack']:
1161        print('WARNING: External BLAS/LAPACK has been specified for Cantera '
1162              'but Sundials was built without this support.')
1163else: # env['system_sundials'] == 'n'
1164    print("""INFO: Using private installation of Sundials version 5.3.""")
1165    env['sundials_version'] = '5.3'
1166    env['has_sundials_lapack'] = int(env['use_lapack'])
1167
1168def set_fortran(pattern, value):
1169    # Set compiler / flags for all Fortran versions to be the same
1170    for version in ("FORTRAN", "F77", "F90", "F95", "F03", "F08"):
1171        env[pattern.format(version)] = value
1172
1173# Try to find a working Fortran compiler:
1174def check_fortran(compiler, expected=False):
1175    hello_world = '''
1176program main
1177   write(*,'(a)') 'Hello, world!'
1178end program main
1179    '''
1180    if which(compiler):
1181        set_fortran("{}", compiler)
1182        success, output = conf.TryRun(hello_world, '.f90')
1183        if success and 'Hello, world!' in output:
1184            return True
1185        else:
1186            print("WARNING: Unable to use '%s' to compile the Fortran "
1187                  "interface. See config.log for details." % compiler)
1188            return False
1189    elif expected:
1190        print("ERROR: Couldn't find specified Fortran compiler: '%s'" % compiler)
1191        sys.exit(1)
1192
1193    return False
1194
1195set_fortran("{}FLAGS", env["FORTRANFLAGS"])
1196
1197if env['f90_interface'] in ('y','default'):
1198    foundF90 = False
1199    if env['FORTRAN']:
1200        foundF90 = check_fortran(env['FORTRAN'], True)
1201
1202    for compiler in ('pgfortran', 'gfortran', 'ifort', 'g95'):
1203        if foundF90:
1204            break
1205        foundF90 = check_fortran(compiler)
1206
1207    if foundF90:
1208        print("INFO: Using '%s' to build the Fortran 90 interface" % env['FORTRAN'])
1209        env['f90_interface'] = 'y'
1210    else:
1211        if env['f90_interface'] == 'y':
1212            print("ERROR: Couldn't find a suitable Fortran compiler to build the Fortran 90 interface.")
1213            sys.exit(1)
1214        else:
1215            env['f90_interface'] = 'n'
1216            env['FORTRAN'] = ''
1217            print("INFO: Skipping compilation of the Fortran 90 interface.")
1218
1219if 'pgfortran' in env['FORTRAN']:
1220    env['FORTRANMODDIRPREFIX'] = '-module '
1221elif 'gfortran' in env['FORTRAN']:
1222    env['FORTRANMODDIRPREFIX'] = '-J'
1223elif 'g95' in env['FORTRAN']:
1224    env['FORTRANMODDIRPREFIX'] = '-fmod='
1225elif 'ifort' in env['FORTRAN']:
1226    env['FORTRANMODDIRPREFIX'] = '-module '
1227
1228set_fortran("{}", env["FORTRAN"])
1229set_fortran("SH{}", env["FORTRAN"])
1230env['FORTRANMODDIR'] = '${TARGET.dir}'
1231
1232env = conf.Finish()
1233
1234if env['VERBOSE']:
1235    print('-------------------- begin config.log --------------------')
1236    print(open('config.log').read())
1237    print('--------------------- end config.log ---------------------')
1238
1239env['python_cmd_esc'] = quoted(env['python_cmd'])
1240
1241# Python Package Settings
1242python_min_version = parse_version('3.5')
1243# The string is used to set python_requires in setup.py.in
1244env['py_min_ver_str'] = str(python_min_version)
1245# Note: cython_min_version is redefined below if the Python version is 3.8 or higher
1246cython_min_version = parse_version('0.23')
1247numpy_min_version = parse_version('1.12.0')
1248
1249# We choose ruamel.yaml 0.15.34 as the minimum version
1250# since it is the highest version available in the Ubuntu
1251# 18.04 repositories and seems to work. Older versions such as
1252# 0.13.14 on CentOS7 and 0.10.23 on Ubuntu 16.04 raise an exception
1253# that they are missing the RoundTripRepresenter
1254ruamel_min_version = parse_version('0.15.34')
1255
1256# Check for the minimum ruamel.yaml version, 0.15.34, at install and test
1257# time. The check happens at install and test time because ruamel.yaml is
1258# only required to run the Python interface, not to build it.
1259check_for_ruamel_yaml = any(
1260    target in COMMAND_LINE_TARGETS
1261    for target in ["install", "test", "test-python-convert"]
1262)
1263
1264if env['python_package'] == 'y':
1265    env['python_package'] = 'full'  # Allow 'y' as a synonym for 'full'
1266elif env['python_package'] == 'n':
1267  env['python_package'] = 'none'  # Allow 'n' as a synonym for 'none'
1268
1269env['install_python_action'] = ''
1270env['python_module_loc'] = ''
1271
1272if env['python_package'] != 'none':
1273    # Test to see if we can import numpy and Cython
1274    script = textwrap.dedent("""\
1275        import sys
1276        print('{v.major}.{v.minor}'.format(v=sys.version_info))
1277        err = ''
1278        try:
1279            import numpy
1280            print(numpy.__version__)
1281        except ImportError as np_err:
1282            print('0.0.0')
1283            err += str(np_err) + '\\n'
1284        try:
1285            import Cython
1286            print(Cython.__version__)
1287        except ImportError as cython_err:
1288            print('0.0.0')
1289            err += str(cython_err) + '\\n'
1290        if err:
1291            print(err)
1292    """)
1293    expected_output_lines = 3
1294    if check_for_ruamel_yaml:
1295        ru_script = textwrap.dedent("""\
1296            try:
1297                import ruamel_yaml as yaml
1298                print(yaml.__version__)
1299            except ImportError as ru_err:
1300                try:
1301                    from ruamel import yaml
1302                    print(yaml.__version__)
1303                except ImportError as ru_err_2:
1304                    print('0.0.0')
1305                    err += str(ru_err) + '\\n'
1306                    err += str(ru_err_2) + '\\n'
1307        """).splitlines()
1308        s = script.splitlines()
1309        s[-2:-2] = ru_script
1310        script = "\n".join(s)
1311        expected_output_lines = 4
1312
1313    try:
1314        info = get_command_output(env["python_cmd"], "-c", script).splitlines()
1315    except OSError as err:
1316        if env['VERBOSE']:
1317            print('Error checking for Python:')
1318            print(err)
1319        warn_no_python = True
1320    except subprocess.CalledProcessError as err:
1321        if env['VERBOSE']:
1322            print('Error checking for Python:')
1323            print(err, err.output)
1324        warn_no_python = True
1325    else:
1326        warn_no_python = False
1327        python_version = parse_version(info[0])
1328        numpy_version = parse_version(info[1])
1329        cython_version = parse_version(info[2])
1330        if check_for_ruamel_yaml:
1331            ruamel_yaml_version = parse_version(info[3])
1332            if ruamel_yaml_version == parse_version("0.0.0"):
1333                print("ERROR: ruamel.yaml was not found. {} or newer is "
1334                    "required".format(ruamel_min_version))
1335                sys.exit(1)
1336            elif ruamel_yaml_version < ruamel_min_version:
1337                print("ERROR: ruamel.yaml is an incompatible version: Found "
1338                    "{}, but {} or newer is required.".format(
1339                        ruamel_yaml_version, ruamel_min_version))
1340                sys.exit(1)
1341
1342    if warn_no_python:
1343        if env['python_package'] == 'default':
1344            print('WARNING: Not building the Python package because the Python '
1345                  'interpreter {!r} could not be found'.format(env['python_cmd']))
1346            env['python_package'] = 'none'
1347        else:
1348            print('ERROR: Could not execute the Python interpreter {!r}'.format(
1349                env['python_cmd']))
1350            sys.exit(1)
1351    elif python_version < python_min_version:
1352        msg = ("{}: Python version is incompatible. Found {} but {} "
1353               "or newer is required")
1354        if env["python_package"] in ("minimal", "full"):
1355            print(msg.format("ERROR", python_version, python_min_version))
1356            sys.exit(1)
1357        elif env["python_package"] == "default":
1358            print(msg.format("WARNING", python_version, python_min_version))
1359            env["python_package"] = "none"
1360    elif env['python_package'] == 'minimal':
1361        # If the minimal package was specified, no further checking
1362        # needs to be done
1363        print('INFO: Building the minimal Python package for Python {}'.format(python_version))
1364    else:
1365
1366        if len(info) > expected_output_lines:
1367            print("WARNING: Unexpected output while checking Python "
1368                  "dependency versions:")
1369            print('| ' + '\n| '.join(info[expected_output_lines:]))
1370
1371        warn_no_full_package = False
1372        if python_version >= parse_version("3.8"):
1373            # Reset the minimum Cython version if the Python version is 3.8 or higher
1374            # Due to internal changes in the CPython API, more recent versions of
1375            # Cython are necessary to build for Python 3.8. There is nothing Cantera
1376            # can do about this, the changes in CPython are handled by Cython. This
1377            # version bump is used to produce a more useful/actionable error message
1378            # for users than the compilation errors that result from using
1379            # Cython < 0.29.12.
1380            cython_min_version = parse_version("0.29.12")
1381
1382        if numpy_version == parse_version('0.0.0'):
1383            print("NumPy not found.")
1384            warn_no_full_package = True
1385        elif numpy_version < numpy_min_version:
1386            print("WARNING: NumPy is an incompatible version: "
1387                  "Found {0} but {1} or newer is required".format(
1388                  numpy_version, numpy_min_version))
1389            warn_no_full_package = True
1390        else:
1391            print('INFO: Using NumPy version {0}.'.format(numpy_version))
1392
1393        if cython_version == parse_version('0.0.0'):
1394            print("Cython not found.")
1395            warn_no_full_package = True
1396        elif cython_version < cython_min_version:
1397            print("WARNING: Cython is an incompatible version: "
1398                  "Found {0} but {1} or newer is required.".format(
1399                  cython_version, cython_min_version))
1400            warn_no_full_package = True
1401        else:
1402            print('INFO: Using Cython version {0}.'.format(cython_version))
1403
1404        if warn_no_full_package:
1405            msg = ('{}: Unable to build the full Python package because compatible '
1406                   'versions of Python, Numpy, and Cython could not be found.')
1407            if env['python_package'] == 'default':
1408                print(msg.format("WARNING"))
1409                print('INFO: Building the minimal Python package for Python {}'.format(python_version))
1410                env['python_package'] = 'minimal'
1411            else:
1412                print(msg.format("ERROR"))
1413                sys.exit(1)
1414        else:
1415            print('INFO: Building the full Python package for Python {0}'.format(python_version))
1416            env['python_package'] = 'full'
1417
1418# Matlab Toolbox settings
1419if env['matlab_path'] != '' and env['matlab_toolbox'] == 'default':
1420    env['matlab_toolbox'] = 'y'
1421
1422if env['matlab_toolbox'] == 'y':
1423    matPath = env['matlab_path']
1424    if matPath == '':
1425        print("ERROR: Unable to build the Matlab toolbox because 'matlab_path' has not been set.")
1426        sys.exit(1)
1427
1428    if env['blas_lapack_libs']:
1429        print('ERROR: The Matlab toolbox is incompatible with external BLAS '
1430              'and LAPACK libraries. Unset blas_lapack_libs (e.g. "scons '
1431              'build blas_lapack_libs=") in order to build the Matlab '
1432              'toolbox, or set matlab_toolbox=n to use the specified BLAS/'
1433              'LAPACK libraries and skip building the Matlab toolbox.')
1434        sys.exit(1)
1435
1436    if env['system_sundials'] == 'y':
1437        print('ERROR: The Matlab toolbox is incompatible with external '
1438              'SUNDIALS libraries. Set system_sundials to no (e.g., "scons build '
1439              'system_sundials=n") in order to build the Matlab '
1440              'toolbox, or set matlab_toolbox=n to use the specified '
1441              'SUNDIALS libraries and skip building the Matlab toolbox.')
1442        sys.exit(1)
1443
1444    if not (os.path.isdir(matPath)):
1445        print("""ERROR: Path set for 'matlab_path' is not correct.""")
1446        print("""ERROR: Path was: '%s'""" % matPath)
1447        sys.exit(1)
1448
1449
1450# **********************************************
1451# *** Set additional configuration variables ***
1452# **********************************************
1453
1454# On Debian-based systems, need to special-case installation to
1455# /usr/local because of dist-packages vs site-packages
1456env['debian'] = any(name.endswith('dist-packages') for name in sys.path)
1457
1458# Directories where things will be after actually being installed. These
1459# variables are the ones that are used to populate header files, scripts, etc.
1460env['prefix'] = os.path.normpath(env['prefix'])
1461env['ct_installroot'] = env['prefix']
1462env['ct_libdir'] = pjoin(env['prefix'], env['libdirname'])
1463env['ct_bindir'] = pjoin(env['prefix'], 'bin')
1464env['ct_incdir'] = pjoin(env['prefix'], 'include', 'cantera')
1465env['ct_incroot'] = pjoin(env['prefix'], 'include')
1466
1467if env['layout'] == 'compact':
1468    env['ct_datadir'] = pjoin(env['prefix'], 'data')
1469    env['ct_sampledir'] = pjoin(env['prefix'], 'samples')
1470    env['ct_mandir'] = pjoin(env['prefix'], 'man1')
1471    env['ct_matlab_dir'] = pjoin(env['prefix'], 'matlab', 'toolbox')
1472else:
1473    env['ct_datadir'] = pjoin(env['prefix'], 'share', 'cantera', 'data')
1474    env['ct_sampledir'] = pjoin(env['prefix'], 'share', 'cantera', 'samples')
1475    env['ct_mandir'] = pjoin(env['prefix'], 'share', 'man', 'man1')
1476    env['ct_matlab_dir'] = pjoin(env['prefix'], env['libdirname'],
1477                                 'cantera', 'matlab', 'toolbox')
1478
1479# Always set the stage directory before building an MSI installer
1480if 'msi' in COMMAND_LINE_TARGETS:
1481    COMMAND_LINE_TARGETS.append('install')
1482    env['stage_dir'] = 'stage'
1483    env['prefix'] = '.'
1484    env['PYTHON_INSTALLER'] = 'binary'
1485elif env['layout'] == 'debian':
1486    COMMAND_LINE_TARGETS.append('install')
1487    env['stage_dir'] = 'stage/cantera'
1488    env['PYTHON_INSTALLER'] = 'debian'
1489    env['INSTALL_MANPAGES'] = False
1490else:
1491    env['PYTHON_INSTALLER'] = 'direct'
1492
1493
1494addInstallActions = ('install' in COMMAND_LINE_TARGETS or
1495                     'uninstall' in COMMAND_LINE_TARGETS)
1496
1497# Directories where things will be staged for package creation. These
1498# variables should always be used by the Install(...) targets
1499if env["stage_dir"]:
1500    stage_prefix = Path(env["prefix"])
1501    # Strip the root off the prefix if it's absolute
1502    if stage_prefix.is_absolute():
1503        stage_prefix = Path(*stage_prefix.parts[1:])
1504
1505    instRoot = Path.cwd().joinpath(env["stage_dir"], stage_prefix)
1506else:
1507    instRoot = env["prefix"]
1508
1509# Prevent setting Cantera installation path to source directory
1510if os.path.abspath(instRoot) == Dir('.').abspath:
1511    print('ERROR: cannot install Cantera into source directory.')
1512    exit(1)
1513
1514if env['layout'] == 'debian':
1515    base = pjoin(os.getcwd(), 'debian')
1516
1517    env['inst_libdir'] = pjoin(base, 'cantera-dev', 'usr', env['libdirname'])
1518    env['inst_incdir'] = pjoin(base, 'cantera-dev', 'usr', 'include', 'cantera')
1519    env['inst_incroot'] = pjoin(base, 'cantera-dev', 'usr' 'include')
1520
1521    env['inst_bindir'] = pjoin(base, 'cantera-common', 'usr', 'bin')
1522    env['inst_datadir'] = pjoin(base, 'cantera-common', 'usr', 'share', 'cantera', 'data')
1523    env['inst_docdir'] = pjoin(base, 'cantera-common', 'usr', 'share', 'cantera', 'doc')
1524    env['inst_sampledir'] = pjoin(base, 'cantera-common', 'usr', 'share', 'cantera', 'samples')
1525    env['inst_mandir'] = pjoin(base, 'cantera-common', 'usr', 'share', 'man', 'man1')
1526
1527    env['inst_matlab_dir'] = pjoin(base, 'cantera-matlab', 'usr',
1528                                   env['libdirname'], 'cantera', 'matlab', 'toolbox')
1529
1530    env['inst_python_bindir'] = pjoin(base, 'cantera-python', 'usr', 'bin')
1531    env['python_prefix'] = pjoin(base, 'cantera-python3')
1532else:
1533    env['inst_libdir'] = pjoin(instRoot, env['libdirname'])
1534    env['inst_bindir'] = pjoin(instRoot, 'bin')
1535    env['inst_python_bindir'] = pjoin(instRoot, 'bin')
1536    env['inst_incdir'] = pjoin(instRoot, 'include', 'cantera')
1537    env['inst_incroot'] = pjoin(instRoot, 'include')
1538
1539    if env['layout'] == 'compact':
1540        env['inst_matlab_dir'] = pjoin(instRoot, 'matlab', 'toolbox')
1541        env['inst_datadir'] = pjoin(instRoot, 'data')
1542        env['inst_sampledir'] = pjoin(instRoot, 'samples')
1543        env['inst_docdir'] = pjoin(instRoot, 'doc')
1544        env['inst_mandir'] = pjoin(instRoot, 'man1')
1545    else: # env['layout'] == 'standard'
1546        env['inst_matlab_dir'] = pjoin(instRoot, env['libdirname'], 'cantera',
1547                                       'matlab', 'toolbox')
1548        env['inst_datadir'] = pjoin(instRoot, 'share', 'cantera', 'data')
1549        env['inst_sampledir'] = pjoin(instRoot, 'share', 'cantera', 'samples')
1550        env['inst_docdir'] = pjoin(instRoot, 'share', 'cantera', 'doc')
1551        env['inst_mandir'] = pjoin(instRoot, 'share', 'man', 'man1')
1552
1553# **************************************
1554# *** Set options needed in config.h ***
1555# **************************************
1556
1557configh = {}
1558
1559configh['CANTERA_VERSION'] = quoted(env['cantera_version'])
1560configh['CANTERA_SHORT_VERSION'] = quoted(env['cantera_short_version'])
1561
1562# Conditional defines
1563def cdefine(definevar, configvar, comp=True, value=1):
1564    if env.get(configvar) == comp:
1565        configh[definevar] = value
1566    else:
1567        configh[definevar] = None
1568
1569# Need to test all of these to see what platform.system() returns
1570configh['SOLARIS'] = 1 if env['OS'] == 'Solaris' else None
1571configh['DARWIN'] = 1 if env['OS'] == 'Darwin' else None
1572
1573if env['OS'] == 'Solaris' or env['HAS_CLANG']:
1574    configh['NEEDS_GENERIC_TEMPL_STATIC_DECL'] = 1
1575
1576configh['CT_SUNDIALS_VERSION'] = env['sundials_version'].replace('.','')
1577
1578if env.get('has_sundials_lapack') and env['use_lapack']:
1579    configh['CT_SUNDIALS_USE_LAPACK'] = 1
1580else:
1581    configh['CT_SUNDIALS_USE_LAPACK'] = 0
1582
1583if env['legacy_rate_constants']:
1584    configh['CT_LEGACY_RATE_CONSTANTS'] = 1
1585else:
1586    configh['CT_LEGACY_RATE_CONSTANTS'] = 0
1587
1588cdefine('LAPACK_FTN_STRING_LEN_AT_END', 'lapack_ftn_string_len_at_end')
1589cdefine('LAPACK_FTN_TRAILING_UNDERSCORE', 'lapack_ftn_trailing_underscore')
1590cdefine('FTN_TRAILING_UNDERSCORE', 'lapack_ftn_trailing_underscore')
1591cdefine('LAPACK_NAMES_LOWERCASE', 'lapack_names', 'lower')
1592cdefine('CT_USE_LAPACK', 'use_lapack')
1593cdefine('CT_USE_SYSTEM_EIGEN', 'system_eigen')
1594cdefine('CT_USE_SYSTEM_FMT', 'system_fmt')
1595cdefine('CT_USE_SYSTEM_YAMLCPP', 'system_yamlcpp')
1596cdefine('CT_USE_DEMANGLE', 'has_demangle')
1597
1598config_h_build = env.Command('build/src/config.h.build',
1599                             'include/cantera/base/config.h.in',
1600                       ConfigBuilder(configh))
1601# This separate copy operation, which SCons will skip if config.h.build is
1602# unmodified, prevents unnecessary rebuilds of the precompiled header
1603config_h = env.Command('include/cantera/base/config.h',
1604                       'build/src/config.h.build',
1605                       Copy('$TARGET', '$SOURCE'))
1606env.AlwaysBuild(config_h_build)
1607env['config_h_target'] = config_h
1608
1609# *********************
1610# *** Build Cantera ***
1611# *********************
1612
1613# Some options to speed up SCons
1614env.SetOption('max_drift', 2)
1615env.SetOption('implicit_cache', True)
1616
1617buildTargets = []
1618env['build_targets'] = buildTargets
1619libraryTargets = [] # objects that go in the Cantera library
1620installTargets = []
1621sampleTargets = []
1622
1623def build(targets):
1624    """ Wrapper to add target to list of build targets """
1625    buildTargets.extend(targets)
1626    return targets
1627
1628def buildSample(*args, **kwargs):
1629    """ Wrapper to add target to list of samples """
1630    targets = args[0](*args[1:], **kwargs)
1631    sampleTargets.extend(targets)
1632    return targets
1633
1634def install(*args, **kwargs):
1635    """ Wrapper to add target to list of install targets """
1636    if not addInstallActions:
1637        return
1638    if len(args) == 2:
1639        inst = env.Install(*args, **kwargs)
1640    else:
1641        inst = args[0](*args[1:], **kwargs)
1642
1643    installTargets.extend(inst)
1644    return inst
1645
1646
1647env.SConsignFile()
1648
1649env.Prepend(CPPPATH=[],
1650           LIBPATH=[Dir('build/lib')])
1651
1652# preprocess input files (cti -> xml)
1653convertedInputFiles = set()
1654for cti in multi_glob(env, 'data/inputs', 'cti'):
1655    build(env.Command('build/data/%s' % cti.name, cti.path,
1656                      Copy('$TARGET', '$SOURCE')))
1657    outName = os.path.splitext(cti.name)[0] + '.xml'
1658    convertedInputFiles.add(outName)
1659    build(env.Command('build/data/%s' % outName, cti.path,
1660                      '$python_cmd_esc interfaces/cython/cantera/ctml_writer.py $SOURCE $TARGET'))
1661
1662# Copy XML input files which are not present as cti:
1663for xml in multi_glob(env, 'data/inputs', 'xml'):
1664    dest = pjoin('build','data',xml.name)
1665    if xml.name not in convertedInputFiles:
1666        build(env.Command(dest, xml.path, Copy('$TARGET', '$SOURCE')))
1667
1668for yaml in multi_glob(env, "data", "yaml"):
1669    dest = pjoin("build", "data", yaml.name)
1670    build(env.Command(dest, yaml.path, Copy("$TARGET", "$SOURCE")))
1671for subdir in os.listdir('data'):
1672    if os.path.isdir(pjoin('data', subdir)):
1673        for yaml in multi_glob(env, pjoin("data", subdir), "yaml"):
1674            dest = pjoin("build", "data", subdir, yaml.name)
1675            if not os.path.exists(pjoin("build", "data", subdir)):
1676                os.makedirs(pjoin("build", "data", subdir))
1677            build(env.Command(dest, yaml.path, Copy("$TARGET", "$SOURCE")))
1678
1679
1680if addInstallActions:
1681    # Put headers in place
1682    headerBase = 'include/cantera'
1683    install(env.RecursiveInstall, '$inst_incdir', 'include/cantera')
1684
1685    # Data files
1686    install(env.RecursiveInstall, '$inst_datadir', 'build/data')
1687
1688
1689### List of libraries needed to link to Cantera ###
1690linkLibs = ['cantera']
1691
1692### List of shared libraries needed to link applications to Cantera
1693linkSharedLibs = ['cantera_shared']
1694
1695if env['system_sundials'] == 'y':
1696    env['sundials_libs'] = ['sundials_cvodes', 'sundials_ida', 'sundials_nvecserial']
1697    if env['use_lapack'] and sundials_ver >= parse_version('3.0'):
1698        if env.get('has_sundials_lapack'):
1699            env['sundials_libs'].extend(('sundials_sunlinsollapackdense',
1700                                         'sundials_sunlinsollapackband'))
1701        else:
1702            env['sundials_libs'].extend(('sundials_sunlinsoldense',
1703                                         'sundials_sunlinsolband'))
1704else:
1705    env['sundials_libs'] = []
1706
1707linkLibs.extend(env['sundials_libs'])
1708linkSharedLibs.extend(env['sundials_libs'])
1709
1710if env['system_fmt']:
1711    linkLibs.append('fmt')
1712    linkSharedLibs.append('fmt')
1713
1714if env['system_yamlcpp']:
1715    linkLibs.append('yaml-cpp')
1716    linkSharedLibs.append('yaml-cpp')
1717
1718#  Add LAPACK and BLAS to the link line
1719if env['blas_lapack_libs']:
1720    linkLibs.extend(env['blas_lapack_libs'])
1721    linkSharedLibs.extend(env['blas_lapack_libs'])
1722
1723# Store the list of needed static link libraries in the environment
1724env['cantera_libs'] = linkLibs
1725env['cantera_shared_libs'] = linkSharedLibs
1726if not env['renamed_shared_libraries']:
1727    env['cantera_shared_libs'] = linkLibs
1728
1729# Add targets from the SConscript files in the various subdirectories
1730Export('env', 'build', 'libraryTargets', 'install', 'buildSample')
1731
1732# ext needs to come before src so that libraryTargets is fully populated
1733VariantDir('build/ext', 'ext', duplicate=0)
1734SConscript('build/ext/SConscript')
1735
1736# Fortran needs to come before src so that libraryTargets is fully populated
1737if env['f90_interface'] == 'y':
1738    VariantDir('build/src/fortran/', 'src/fortran', duplicate=1)
1739    SConscript('build/src/fortran/SConscript')
1740
1741VariantDir('build/src', 'src', duplicate=0)
1742SConscript('build/src/SConscript')
1743
1744if env['python_package'] == 'full':
1745    SConscript('interfaces/cython/SConscript')
1746elif env['python_package'] == 'minimal':
1747    SConscript('interfaces/python_minimal/SConscript')
1748
1749if env['CC'] != 'cl':
1750    VariantDir('build/platform', 'platform/posix', duplicate=0)
1751    SConscript('build/platform/SConscript')
1752
1753if env['matlab_toolbox'] == 'y':
1754    SConscript('build/src/matlab/SConscript')
1755
1756if env['doxygen_docs'] or env['sphinx_docs']:
1757    SConscript('doc/SConscript')
1758
1759# Sample programs (also used from test_problems/SConscript)
1760VariantDir('build/samples', 'samples', duplicate=0)
1761sampledir_excludes = ['\\.o$', '^~$', '\\.in', 'SConscript']
1762SConscript('build/samples/cxx/SConscript')
1763
1764# Install C++ samples
1765install(env.RecursiveInstall, '$inst_sampledir/cxx',
1766        'samples/cxx', exclude=sampledir_excludes)
1767
1768if env['f90_interface'] == 'y':
1769    SConscript('build/samples/f77/SConscript')
1770    SConscript('build/samples/f90/SConscript')
1771
1772    # install F90 / F77 samples
1773    install(env.RecursiveInstall, '$inst_sampledir/f77',
1774            'samples/f77', sampledir_excludes)
1775    install(env.RecursiveInstall, '$inst_sampledir/f90',
1776            'samples/f90', sampledir_excludes)
1777
1778### Meta-targets ###
1779build_samples = Alias('samples', sampleTargets)
1780
1781def postBuildMessage(target, source, env):
1782    print("*******************************************************")
1783    print("Compilation completed successfully.\n")
1784    print("- To run the test suite, type 'scons test'.")
1785    print("- To list available tests, type 'scons test-help'.")
1786    if env['googletest'] == 'none':
1787        print("  WARNING: You set the 'googletest' to 'none' and all it's tests will be skipped.")
1788    if os.name == 'nt':
1789        print("- To install, type 'scons install'.")
1790        print("- To create a Windows MSI installer, type 'scons msi'.")
1791    else:
1792        print("- To install, type 'scons install'.")
1793    print("*******************************************************")
1794
1795finish_build = env.Command('finish_build', [], postBuildMessage)
1796env.Depends(finish_build, buildTargets)
1797build_cantera = Alias('build', finish_build)
1798
1799Default('build')
1800
1801def postInstallMessage(target, source, env):
1802    # Only needed because Python 2 doesn't support textwrap.indent
1803    def indent(inp_str, indent):
1804        return '\n'.join([indent + spl for spl in inp_str.splitlines()])
1805
1806    env_dict = env.Dictionary()
1807    install_message = textwrap.dedent("""
1808        Cantera has been successfully installed.
1809
1810        File locations:
1811
1812          applications                {ct_bindir!s}
1813          library files               {ct_libdir!s}
1814          C++ headers                 {ct_incroot!s}
1815          samples                     {ct_sampledir!s}
1816          data files                  {ct_datadir!s}""".format(**env_dict))
1817
1818    if env['sphinx_docs'] or env['doxygen_docs']:
1819        install_message += indent(textwrap.dedent("""
1820            HTML documentation          {inst_docdir!s}""".format(**env_dict)), '  ')
1821
1822    if env['python_package'] == 'full':
1823        env['python_example_loc'] = pjoin(env['python_module_loc'], 'cantera', 'examples')
1824        install_message += indent(textwrap.dedent("""
1825            Python package (cantera)    {python_module_loc!s}
1826            Python samples              {python_example_loc!s}""".format(**env_dict)), '  ')
1827    elif env['python_package'] == 'minimal':
1828        install_message += indent(textwrap.dedent("""
1829            minimal Python module       {python_module_loc!s}""".format(**env_dict)), '  ')
1830
1831    if env['matlab_toolbox'] == 'y':
1832        env['matlab_sample_loc'] = pjoin(env['ct_sampledir'], 'matlab')
1833        env['matlab_ctpath_loc'] = pjoin(env['ct_matlab_dir'], 'ctpath.m')
1834        install_message += textwrap.dedent("""
1835              Matlab toolbox              {ct_matlab_dir!s}
1836              Matlab samples              {matlab_sample_loc!s}
1837
1838            An m-file to set the correct matlab path for Cantera is at:
1839
1840              {matlab_ctpath_loc!s}
1841        """.format(**env_dict))
1842
1843    if os.name != 'nt':
1844        env['setup_cantera'] = pjoin(env['ct_bindir'], 'setup_cantera')
1845        env['setup_cantera_csh'] = pjoin(env['ct_bindir'], 'setup_cantera.csh')
1846        install_message += textwrap.dedent("""
1847
1848            Setup scripts to configure the environment for Cantera are at:
1849
1850              setup script (bash)         {setup_cantera!s}
1851              setup script (csh/tcsh)     {setup_cantera_csh!s}
1852
1853            It is recommended that you run the script for your shell by typing:
1854
1855              source {setup_cantera!s}
1856
1857            before using Cantera, or else include its contents in your shell login script.
1858        """.format(**env_dict))
1859
1860    print(install_message)
1861
1862finish_install = env.Command('finish_install', [], postInstallMessage)
1863env.Depends(finish_install, installTargets)
1864install_cantera = Alias('install', finish_install)
1865
1866### Uninstallation
1867def getParentDirs(path, top=True):
1868    head,tail = os.path.split(path)
1869    if head == os.path.abspath(env['prefix']):
1870        return [path]
1871    elif not tail:
1872        if head.endswith(os.sep):
1873            return []
1874        else:
1875            return [head]
1876    elif top:
1877        return getParentDirs(head, False)
1878    else:
1879        return getParentDirs(head, False) + [path]
1880
1881# Files installed by SCons
1882allfiles = FindInstalledFiles()
1883
1884# Files installed by the Python installer
1885if os.path.exists('build/python-installed-files.txt'):
1886    with open('build/python-installed-files.txt', 'r') as f:
1887        file_list = f.readlines()
1888
1889    install_base = os.path.dirname(file_list[0].strip())
1890    if os.path.exists(install_base):
1891        not_listed_files = [s for s in os.listdir(install_base) if not any(s in j for j in file_list)]
1892        for f in not_listed_files:
1893            f = pjoin(install_base, f)
1894            if not os.path.isdir(f) and os.path.exists(f):
1895                allfiles.append(File(f))
1896    for f in file_list:
1897        f = f.strip()
1898        if not os.path.isdir(f) and os.path.exists(f):
1899            allfiles.append(File(f))
1900
1901# After removing files (which SCons keeps track of),
1902# remove any empty directories (which SCons doesn't track)
1903def removeDirectories(target, source, env):
1904    # Get all directories where files are installed
1905    alldirs = set()
1906    for f in allfiles:
1907        alldirs.update(getParentDirs(f.path))
1908    if env['layout'] == 'compact':
1909        alldirs.add(os.path.abspath(env['prefix']))
1910    # Sort in order of decreasing directory length so that empty subdirectories
1911    # will be removed before their parents are checked.
1912    alldirs = sorted(alldirs, key=lambda x: -len(x))
1913
1914    # Don't remove directories that probably existed before installation,
1915    # even if they are empty
1916    keepDirs = ['local/share', 'local/lib', 'local/include', 'local/bin',
1917                'man/man1', 'dist-packages', 'site-packages']
1918    for d in alldirs:
1919        if any(d.endswith(k) for k in keepDirs):
1920            continue
1921        if os.path.isdir(d) and not os.listdir(d):
1922            os.rmdir(d)
1923
1924uninstall = env.Command("uninstall", None, Delete(allfiles))
1925env.AddPostAction(uninstall, Action(removeDirectories))
1926
1927### Windows MSI Installer ###
1928if 'msi' in COMMAND_LINE_TARGETS:
1929    def build_wxs(target, source, env):
1930        import wxsgen
1931        wxs = wxsgen.WxsGenerator(env['stage_dir'],
1932                                  short_version=env['cantera_short_version'],
1933                                  full_version=env['cantera_pure_version'],
1934                                  x64=env['TARGET_ARCH']=='amd64',
1935                                  includeMatlab=env['matlab_toolbox']=='y')
1936        wxs.make_wxs(str(target[0]))
1937
1938    wxs_target = env.Command('build/wix/cantera.wxs', [], build_wxs)
1939    env.AlwaysBuild(wxs_target)
1940
1941    env.Append(WIXLIGHTFLAGS=['-ext', 'WixUIExtension'])
1942    msi_target = env.WiX('cantera.msi', ['build/wix/cantera.wxs'])
1943    env.Depends(wxs_target, installTargets)
1944    env.Depends(msi_target, wxs_target)
1945    build_msi = Alias('msi', msi_target)
1946
1947### Tests ###
1948if any(target.startswith('test') for target in COMMAND_LINE_TARGETS):
1949    env['testNames'] = []
1950    env['test_results'] = env.Command('test_results', [], test_results.print_report)
1951
1952    if env['python_package'] == 'none':
1953        # copy scripts from the full Cython module
1954        test_py_int = env.Command('#build/python_local/cantera/__init__.py',
1955                                  '#interfaces/python_minimal/cantera/__init__.py',
1956                                  Copy('$TARGET', '$SOURCE'))
1957        for script in ['ctml_writer', 'ck2cti', 'ck2yaml', 'ctml2yaml']:
1958            s = env.Command('#build/python_local/cantera/{}.py'.format(script),
1959                            '#interfaces/cython/cantera/{}.py'.format(script),
1960                            Copy('$TARGET', '$SOURCE'))
1961            env.Depends(test_py_int, s)
1962
1963        env.Depends(env['test_results'], test_py_int)
1964
1965        env['python_cmd'] = sys.executable
1966        env.PrependENVPath('PYTHONPATH', Dir('build/python_local').abspath)
1967    else:
1968        env.PrependENVPath('PYTHONPATH', Dir('build/python').abspath)
1969
1970    env['ENV']['PYTHON_CMD'] = env.subst('$python_cmd')
1971
1972    # Tests written using the gtest framework, the Python unittest module,
1973    # or the Matlab xunit package.
1974    VariantDir('build/test', 'test', duplicate=0)
1975    SConscript('build/test/SConscript')
1976
1977    # Regression tests
1978    SConscript('test_problems/SConscript')
1979
1980    if 'test-help' in COMMAND_LINE_TARGETS:
1981        print('\n*** Available tests ***\n')
1982        for name in env['testNames']:
1983            print('test-%s' % name)
1984        sys.exit(0)
1985
1986    Alias('test', env['test_results'])
1987
1988### Dump (debugging SCons)
1989if 'dump' in COMMAND_LINE_TARGETS:
1990    import pprint
1991    # Typical usage: 'scons build dump'
1992    print('os.environ:\n', pprint.pprint(dict(os.environ)))
1993    print('env.Dump():\n', env.Dump())
1994    sys.exit(0)
1995