# -*- python -*-
#     Copyright 2016, Kay Hayen, mailto:kay.hayen@gmail.com
#
#     Part of "Nuitka", an optimizing Python compiler that is compatible and
#     integrates with CPython, but also works on its own.
#
#     Licensed under the Apache License, Version 2.0 (the "License");
#     you may not use this file except in compliance with the License.
#     You may obtain a copy of the License at
#
#        http://www.apache.org/licenses/LICENSE-2.0
#
#     Unless required by applicable law or agreed to in writing, software
#     distributed under the License is distributed on an "AS IS" BASIS,
#     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#     See the License for the specific language governing permissions and
#     limitations under the License.
#

# The Nuitka scons file. If you have Scons or platform knowledge, please be
# especially invited and contribute improvements.
#
# This file is used to build an executable or shared library. Nuitka needs no
# build process for itself, although it can be compiled using the same method.

import os
import platform
import re
import shutil
import signal
import subprocess
import sys

ARGUMENTS = ARGUMENTS  # @UndefinedVariable

# The directory containing the C++ files generated by Nuitka to be built using
# scons. They are referred to as sources from here on. The ARGUMENTS is what
# Scons provides to us.
source_name = ARGUMENTS["name"]
source_dir = ARGUMENTS["source_dir"]

# The directory containing Nuitka provided C++ files to be built and where it
# should be used.
nuitka_src = ARGUMENTS["nuitka_src"]
static_src = os.path.join(source_dir, "static")

# The name of executable or extension module that we produce.
result_basepath = ARGUMENTS["result_name"]

def getBoolOption(option_name, default = None):
    """ Small helper for boolean mode flags."""
    if default is None:
        value = ARGUMENTS[option_name]
    else:
        value = ARGUMENTS.get(option_name, "True" if default else "False")

    return value.lower() in ("yes", "true", '1')

# The directory to use for cache directory.
cache_mode = getBoolOption("cache_mode", False)

# Module mode: Create a Python extension module, create an executable otherwise.
module_mode = getBoolOption("module_mode", False)

# Debug mode: Less optimizations, debug information in the resulting binary.
debug_mode = getBoolOption("debug_mode", False)

# Profiling mode: Outputs vmprof based information from program run.
profile_mode = getBoolOption("profile_mode", False)

# Python version to target.
python_version = ARGUMENTS["python_version"]

# Python debug mode: reference count checking, assertions in CPython core.
python_debug = getBoolOption("python_debug", False)

# Optimization mode: Optimize as much as currently possible.
optimize_mode = getBoolOption("optimize_mode", True)

# Full compatibility, even where it's stupid, i.e. do not provide information,
# even if available, in order to assert maximum compatibility. Intended to
# control level of compatibility to absurd.
full_compat_mode = getBoolOption("full_compat", False)

# Experimental mode. Do things that are not yet safe to do.
experimental_mode = getBoolOption("experimental", False)

# Tracing mode. Output program progress.
trace_mode = getBoolOption("trace_mode", False)

# LTO mode: Use link time optimizations of g++ compiler if available and known
# good with the compiler in question. The 4.5 one didn't have good enough
# support, the compiled result would not run correctly.
lto_mode = getBoolOption("lto_mode", False)

# Windows target mode: Compile for Windows. Used to be an option, but we
# no longer cross compile this way.
win_target = os.name == "nt"

# Windows subsystem mode: Disable console for windows builds.
win_disable_console = getBoolOption("win_disable_console", False)

# Windows might be running a Python whose DLL we have to use.
uninstalled_python = getBoolOption("uninstalled_python", False)

# Unstriped mode: Do not remove debug symbols.
unstripped_mode = getBoolOption("unstripped_mode", False)

# Clang compiler mode, default on MacOS X and FreeBSD, optional on Linux.
clang_mode = getBoolOption("clang_mode", False)
if sys.platform == "darwin" or "freebsd" in sys.platform:
    clang_mode = True

# MinGW compiler mode, optional and interesting to Windows only.
mingw_mode = getBoolOption("mingw_mode", False)

# Shared library and compiled modules count, determines the need for the
# compiled module loader.
module_count = int(ARGUMENTS["module_count"])

# Frozen modules count, determines the need for the bytecode frozen
# modules loader.
frozen_modules = int(ARGUMENTS.get("frozen_modules", 0))

# Standalone mode
standalone_mode = getBoolOption("standalone_mode", False)

# Show scons mode, output information about Scons operation
show_scons_mode = getBoolOption("show_scons", False)

# Home of Python to be compiled against, used to find include files and
# libraries to link against.
python_prefix = ARGUMENTS["python_prefix"]

# Target arch, uses for compiler choice and quick linking of constants binary
# data.
target_arch = ARGUMENTS["target_arch"]

# Icon for executable (windows-only)
icon_path = ARGUMENTS.get("icon_path", None)

# Forced MSVC version (windows-only)
msvc_version = ARGUMENTS.get("msvc_version", None)

no_python_warnings = getBoolOption("no_python_warnings", False)

# sys.flags values to pass along
# python_sysflag_py3k_warning
python_sysflag_py3k_warning = getBoolOption("python_sysflag_py3k_warning", False)
# python_sysflag_division_warning
python_sysflag_division_warning = getBoolOption("python_sysflag_division_warning", False)
# python_sysflag_division_warning
python_sysflag_bytes_warning = getBoolOption("python_sysflag_bytes_warning", False)
# python_sysflag_no_site
python_sysflag_no_site = getBoolOption("python_sysflag_no_site", False)
# python_sysflag_verbose
python_sysflag_verbose = getBoolOption("python_sysflag_verbose", False)
# python_sysflag_unicode
python_sysflag_unicode = getBoolOption("python_sysflag_unicode", False)


# Amount of jobs to use.
job_count = GetOption("num_jobs")  # @UndefinedVariable

if "CXX" in os.environ:
    cxx_dirname = os.path.dirname(os.environ["CXX"])

    if os.path.isdir(cxx_dirname):
        os.environ["PATH"] = os.pathsep.join([ cxx_dirname ] + os.environ["PATH"].split(os.pathsep))

def getExecutablePath(filename, initial):
    """ Find an execute in either normal PATH, or Scons detected PATH. """

    # Variable substitution from environment is needed, because this can contain
    # "$CC" which should be looked up too.
    while filename.startswith('$'):
        filename = env[filename[1:]]

    # Append ".exe" suffix  on Windows if not already present.
    if win_target and not filename.lower().endswith(".exe"):
        filename += ".exe"

    # Either look at the initial "PATH" as given from the outside or look at the
    # current environment.
    if initial:
        search_path = os.environ["PATH"]
    else:
        search_path = env._dict["ENV"]["PATH"]

    # Now check in each path element, much like the shell will.
    path_elements = search_path.split(os.pathsep)

    for path_element in path_elements:
        path_element = path_element.strip('"')

        full = os.path.join(path_element, filename)

        if os.path.exists(full):
            return full

    return None


def createEnvironment(compiler_tools):
    args = {}

    # If we are on Windows, and MinGW is not enforced, lets see if we can
    # find "cl.exe", and if we do, disable automatic scan.
    if win_target and \
       not mingw_mode and \
       (getExecutablePath("cl", initial = True) is not None or \
        getExecutablePath("gcc", initial = True) is not None):

        args["MSVC_USE_SCRIPT"] = False

    return Environment(  # @UndefinedVariable
        # We want the outside environment to be passed through.
        ENV          = os.environ,

        # Extra tools configuration for scons.
        tools        = compiler_tools,

        # The shared libraries should not be named "lib...", because CPython
        # requires the filename "module_name.so" to load it.
        SHLIBPREFIX  = "",

        # Under windows, specify the target architecture is needed for Scons
        # to pick up MSVC.
        TARGET_ARCH  = target_arch,

        MSVC_VERSION = msvc_version,

        **args
    )

if mingw_mode:
    # Force usage of MinGW.
    compiler_tools = ["mingw"]
else:
    # Everything else should use default.
    compiler_tools = ["default"]

# Create Scons environment, the main control tool. Don't include "mingw" on
# Windows immediately, we will default to MSVC if available.
env = createEnvironment(
    compiler_tools = compiler_tools
)

# On Windows, in case MSVC was not found and not previously forced, retry with
# it and use that instead then as a fallback. Using both tools in one call
# seems to not work in terms of fallback when both exist.
if win_target and \
   compiler_tools == ["default"] and \
   getExecutablePath(env["CXX"], initial = False) is None:
    env = createEnvironment(
        compiler_tools = ["mingw"]
    )

if show_scons_mode:
    print "Initial CXX:", env.get("CXX", None)
    print "Initial CXXVERSION:", env.get("CXXVERSION", None)

if clang_mode:
    # If requested by the user, use the clang compiler, overriding what was
    # said in environment.
    env["CXX"] = "clang"
    env["CXXVERSION"] = None
elif "CXX" in os.environ:
    # If the environment variable CXX is set, use that.
    env["CXX"] = os.environ["CXX"]
    env["CXXVERSION"] = None

# To work around Windows not supporting command lines of greater than 10K by
# default:
def setupSpawn(env):
    def spawn(sh, escape, cmd, args, env):
        # The "del" appears to not work, but is used with large amounts of
        # files to link. So, lets do this ourselves, plus it avoids a process
        # spawn.
        if cmd == "del":
            assert len(args) == 2

            os.unlink(args[1])
            return 0

        # For quoted arguments that end in a backslash, things don't work well
        # this is a workaround for it.
        def removeTrailingSlashQuote(arg):
            if arg.endswith(r'\"'):
                return arg[:-1] + '\\"'
            else:
                return arg

        newargs = ' '.join(
            removeTrailingSlashQuote(arg)
            for arg in
            args[1:]
        )
        cmdline = cmd + ' ' + newargs

        # Remove useless quoted include directories to "windres", which does not
        # handle them properly in its command line parsing, while they are not
        # used at all.
        if cmd == "windres":
            cmdline = re.sub('--include-dir ".*?"', "", cmdline)

        proc = subprocess.Popen(
            cmdline,
            stdin       = subprocess.PIPE,
            stdout      = subprocess.PIPE,
            stderr      = subprocess.PIPE,
            shell       = False,
            env         = env
        )

        data, err = proc.communicate()
        rv = proc.wait()

        if cmd == "cl":
            data = data[data.find("\r\n") + 2 :]

            source_basenames = [
                os.path.basename(source_file)
                for source_file in
                source_files
            ]

            def check(line):
                return line in ("",  "Generating Code...") or \
                       line in source_basenames

            data = "\r\n".join(
                line
                for line in
                data.split("\r\n")
                if not check(line)
            )

        elif cmd == "rc":
            data = data[data.find("reserved.\r") + 13:]

            data = '\n'.join(
                line
                for line in
                data.split('\n')
                if not "identifier truncated to"
            )
        elif cmd == "link" and module_mode:
            data = "\r\n".join(
                line
                for line in
                data.split("\r\n")
                if "   Creating library" not in line
                # On localized compilers, the message to ignore is not as clear.
                if not (module_mode and ".exp" in line)
            )

        if data.rstrip():
            if not show_scons_mode:
                print cmdline
            print data,

        if err:
            print err,

        return rv

    env["SPAWN"] = spawn

def getGccVersion():
    try:
        proc = subprocess.Popen(
            ["gcc", "-v"],
            stdin       = subprocess.PIPE,
            stdout      = subprocess.PIPE,
            stderr      = subprocess.PIPE,
            shell       = False,
        )
    except OSError:
        return None, None

    _stdout, stderr = proc.communicate()
    _rv = proc.wait()

    lines = stderr.splitlines()

    version = None
    arch = None

    if lines:
        line = lines[-1]

        match = re.search(r'[0-9]+(\.[0-9]+){2}', line)

        if match:
            version = match.group(0)

        for line in lines:
            if line.startswith("Target: "):
                arch = line.split(": ", 2)[1]

                if win_target:
                    if "i686" in arch:
                        arch = "x86"
                    elif "x86_64" in arch:
                        arch = "x86_64"
                    else:
                        arch = None

    return version, arch


def getClangVersion():
    import SCons # @UnresolvedImport

    pipe = SCons.Action._subproc(
        env, [env["CXX"], "--version"],
        stdin  = "devnull",
        stderr = "devnull",
        stdout = subprocess.PIPE
    )

    line = pipe.stdout.readline()

    match = re.search(r'LLVM ([0-9]+(\.[0-9]+){1})', line)

    if match:
        return match.group(1)

    match = re.search(r'([0-9]+(\.[0-9]+){1})', line)

    if match:
        return match.group(1)
    else:
        return None

# Some versions of Scons have "CXX" not set or None, make it explicit.
if env.get("CXX", None) is None:
    env["CXX"] = None

orig_cxx = env["CXX"]
orig_cxx_version = env.get("CXXVERSION", None)

# Remove "g++" as the compiler, in case it's not really existing or not high
# enough version.
if "g++" in (env["CXX"] or "") and "clang" not in (env["CXX"] or ""):
    gpp_version, compiler_arch = getGccVersion()

    if gpp_version is None or int(gpp_version.replace('.', "")) < 440:
        env["CXX"] = None
    elif win_target and compiler_arch != target_arch:
        env["CXX"] = None


# Detect compiler to be used from supported ones, if there is no usable g++
# link.
if not win_target and env["CXX"] is None:
    # Try out the Debian/EPEL names, so no "gcc" default compiler package needs
    # to be installed or might be ignored due to too low version.

    for candidate in ("g++-6.1", "g++-5.3", "g++-5.1", "g++-4.9", "g++-4.8",
                      "g++-4.7", "g++-4.6", "g++-4.5", "g++-4.4", "g++44",
                      "clang", "eg++"):

        for system_path in ("/usr/bin", "/usr/local/bin"):
            if os.path.exists(os.path.join(system_path, candidate)):
                env["CXX"] = candidate
                env["CC"] = candidate.replace("++", "cc")

                if "clang" not in candidate:
                    # Update CXXVERSION in env, after we changed it.
                    env["CXXVERSION"] = getGccVersion()[0]

                break

        if env["CXX"] is not None:
            break

    if env["CXX"] is None:
        sys.exit("""\
The g++ compiler '%s' (version %s) doesn't have the sufficient \
version (>= 4.4).""" % (orig_cxx, orig_cxx_version))

if env["CXX"] is None or \
   getExecutablePath(env["CXX"], initial = False) is None:
    if win_target:
        sys.exit("""\
Error, cannot locate suitable C++ compiler. You have the following options:

a) If a suitable Visual Studio version is installed, it will not be located,
   automatically unless you install pywin32 for the Python installation
   below "%s".

b) To make it find Visual Studio execute from Start Menu the 'Visual Studio
   Command Prompt' or "vcvarsall.bat". That will add Visual Studio to the
   PATH. And it will be detected.

c) Install MinGW64 to 'C:\\MinGW64' or '\\MinGW', where then it is automatically
   detected or add it to PATH before executing Nuitka. But be sure to pick the
   proper variant (32/64 bits, your Python arch is '%s').
""" % (sys.exec_prefix, target_arch))
    else:
        sys.exit("Error, cannot locate suitable C++ compiler.")

gcc_mode = "g++" in env["CXX"] or "clang" in env["CXX"]
msvc_mode = win_target and not gcc_mode
mingw_mode = win_target and gcc_mode

# Do we use C11 for source files, or C++. No detection yet.
if msvc_mode:
    c11_mode = False
elif mingw_mode and env["CXXVERSION"] >= "5":
    c11_mode = True
elif clang_mode:
    c11_mode = True
elif gcc_mode and env["CXXVERSION"] >= "5":
    c11_mode = True
else:
    c11_mode = False

if msvc_mode:
    def getMsvcVersionString():
        import SCons.Tool.MSCommon.vc  # @UnresolvedImport
        return SCons.Tool.MSCommon.vc.get_default_version(env)

    def getMsvcVersion():
        value = getMsvcVersionString()

        value = value.replace("exp","")
        return float(value)


if show_scons_mode:
    print "Scons compiler: Using",
    print getExecutablePath(env["CXX"], initial = False),

    if win_target and msvc_mode:
        print "(MSVC %s)" % getMsvcVersionString()

    print

if win_target:
    setupSpawn(env)

env["BUILD_DIR"] = source_dir

# Store the file signatures database with the rest of the source files
sconsign_dir = os.path.abspath(os.path.join(source_dir, ".sconsign"))

if not os.path.exists(sconsign_dir):
    os.makedirs(sconsign_dir)

env.SConsignFile(sconsign_dir)

# Support for clang.
if "clang" in env["CXX"]:
    env.Append(
        CCFLAGS = ["-w"]
    )
    env.Append(
        CPPDEFINES = ["_XOPEN_SOURCE"]
    )
    env.Append(
        LINKFLAGS = ["-lstdc++" ]
    )

    # Don't export anything by default, this should create smaller executables.
    env.Append(
        CCFLAGS = [
            "-fvisibility=hidden",
            "-fvisibility-inlines-hidden"
        ]
    )

    if debug_mode and not clang_mode:
        env.Append(
            CCFLAGS = ["-Wunused-but-set-variable"]
        )

    clang_version = getClangVersion()
    clang_version = int(clang_version.replace('.', "") + '0')

if gcc_mode:
    if not win_target:
        env.Append(
            CCFLAGS = ["-fvisibility=hidden"]
        )
        env.Append(
            CXXFLAGS = ["-fvisibility-inlines-hidden"]
        )

# Support details for real g++, not clang++.
if "g++" in env["CXX"] and "clang" not in env["CXX"]:
    # Don't export anything by default, this should create smaller executables.

    if env.get("CXXVERSION", None) is None or \
       env.get("CXXVERSION").count('.') < 2:
        env["CXXVERSION"] = getGccVersion()[0]

    # Version dependent options.
    gpp_version = int(env["CXXVERSION"].replace('.', ""))

    # Enforce the minimum version, selecting a potentially existing g++-4.5
    # binary if it's not high enough. This is esp. useful under Debian which
    # allows all compiler to exist next to each other and where g++ might not be
    # good enough, but g++-4.5 would be.
    if gpp_version < 440:
        sys.exit(
            """\
The g++ compiler %s (version %s) doesn't have the sufficient \
version (>= 4.4).""" % (env["CXX"], env["CXXVERSION"])
        )

    # Older g++ complains about aliasing with Py_True and Py_False, but we don't
    # care.
    if gpp_version < 450:
        env.Append(
            CCFLAGS = ["-fno-strict-aliasing"]
        )

    # For LTO mode, the version requirement is even higher, so try that too.
    if lto_mode and gpp_version < 460:
        sys.exit("""\
The g++ compiler %s (version %s) doesn't have the sufficient \
version for lto mode (>= 4.6).""" % (env[ "CXX" ], env[ "CXXVERSION" ]))

    # For g++ 4.6 there are some new interesting functions.
    if gpp_version >= 460:
        env.Append(
            CCFLAGS = ["-fpartial-inlining"]
        )

        if debug_mode:
            env.Append(
                CCFLAGS = ["-Wunused-but-set-variable"]
            )

    # Use link time optimizations so that gcc can help with optimization across
    # files, but unfortunately at this time it seriously slows down the compiled
    # code. This may be because it needs -O3 option to be effective.

    if gpp_version >= 460 and lto_mode:
        env.Append(CCFLAGS = ["-flto"])
        env.Append(LINKFLAGS = ["-flto=%d" % job_count])

        # env.Append( LINKFLAGS = [ "-Wsuggest-attribute=noreturn" ] )
        # env.Append( LINKFLAGS = [ "-Wsuggest-attribute=pure" ] )
        # env.Append( LINKFLAGS = [ "-Wsuggest-attribute=const" ] )
        # env.Append( CCFLAGS = [ "-Wnoexcept" ] )

        if debug_mode:
            env.Append(LINKFLAGS = ["-O2"])

        if optimize_mode:
            env.Append(
                LINKFLAGS = [
                    "-O3",
                    "-fpartial-inlining",
                    "-freorder-functions",
                ]
            )

    # Give a warning if LTO mode was specified, but won't be used.
    if lto_mode and gpp_version < 460:
        print >> sys.stderr, "Warning, LTO mode specified, but not available."

    # The var-tracking does not scale, disable it. Should we really need it, we
    # can enable it. TODO: Does this cause a performance loss?
    env.Append(CCFLAGS = ["-fno-var-tracking"])

if msvc_mode:
    env.Append(CCFLAGS = ["/EHsc", "/J", "/Gd"])
    env.Append(LINKFLAGS = ["/INCREMENTAL:NO"])

    # Stack size 4MB or 8MB, we need more than the default 1MB.
    if target_arch == "x86_64":
        env.Append(CCFLAGS = ["/F8388608"])
    else:
        env.Append(CCFLAGS = ["/F4194304"])

# The 32 bits MinGW does not default for API level properly, so help it.
if mingw_mode:
    # Windows XP
    env.Append(
        CPPDEFINES = ["_WIN32_WINNT=0x0501"]
    )

if debug_mode:

    if gcc_mode:
        # Allow g++/clang to point out all kinds of inconsistency to us by
        # raising an error.
        env.Append(
            CCFLAGS = [
                "-Wall",
                "-Werror",
                # Unfortunately Py_INCREF(Py_False) triggers aliasing warnings,
                # which are unfounded, so disable them.
                "-Wno-error=strict-aliasing",
                "-Wno-strict-aliasing",
                # At least for self-compiled Python3.2, and MinGW this happens
                # and has little use anyway.
                "-Wno-error=format",
                "-Wno-format"

            ]
        )
    elif msvc_mode:
        # Disable warnings that system headers already show.
        env.Append(
            CCFLAGS = [
                "/W4", "/wd4505", "/wd4127", "/wd4100", "/wd4702", "/wd4189",
                "/wd4211", "/WX"
            ]
        )

        # Disable warnings, that CPython headers already show.
        if python_version >= "3.4":
            env.Append(
                CCFLAGS = [
                    "/wd4512", "/wd4510", "/wd4610"
                ]
            )



    # As for sequence points, we are abusing it, so we have to allow it.
    if "g++" in env["CXX"]:
        env.Append(
            CCFLAGS = ["-Wno-sequence-point"]
        )

if full_compat_mode:
    env.Append(
        CPPDEFINES = ["_NUITKA_FULL_COMPAT"]
    )

if experimental_mode:
    env.Append(
        CPPDEFINES = ["_NUITKA_EXPERIMENTAL"]
    )

if profile_mode:
    env.Append(
        CPPDEFINES = ["_NUITKA_PROFILE"]
    )

if trace_mode:
    env.Append(
        CPPDEFINES = ["_NUITKA_TRACE"]
    )

if standalone_mode:
    env.Append(
        CPPDEFINES = ["_NUITKA_STANDALONE"]
    )

    if "linux" in sys.platform:
        env.Append(
            LIBS = ["dl"]
        )

if no_python_warnings:
    env.Append(
        CPPDEFINES = ["_NUITKA_NO_PYTHON_WARNINGS"]
    )

if python_version < "3":
    env.Append(
        CPPDEFINES = ["_NUITKA_SYSFLAG_PY3K_WARNING=%d" % (1 if python_sysflag_py3k_warning else 0)]
    )

    env.Append(
        CPPDEFINES = ["_NUITKA_SYSFLAG_DIVISION_WARNING=%d" % (1 if python_sysflag_division_warning else 0)]
    )

    env.Append(
        CPPDEFINES = ["_NUITKA_SYSFLAG_UNICODE=%d" % (1 if python_sysflag_unicode else 0)]
    )

env.Append(
    CPPDEFINES = ["_NUITKA_SYSFLAG_BYTES_WARNING=%d" % (1 if python_sysflag_bytes_warning else 0)]
)

env.Append(
    CPPDEFINES = ["_NUITKA_SYSFLAG_NO_SITE=%d" % (1 if python_sysflag_no_site else 0)]
)

env.Append(
    CPPDEFINES = ["_NUITKA_SYSFLAG_VERBOSE=%d" % (1 if python_sysflag_verbose else 0)]
)



if win_target:
    # For MinGW and cross compilation, we need to tell the subsystem
    # to target as well as to automatically import everything used.
    if gcc_mode:
        env.Append(
            LINKFLAGS = ["-Wl,--enable-auto-import"]
        )

        if win_disable_console:
            env.Append(
                LINKFLAGS = ["-Wl,--subsystem,windows"]
            )

    if win_disable_console:
        env.Append(
            CPPDEFINES = ["_NUITKA_WINMAIN_ENTRY_POINT"]
        )

    # This is used for "PathRemoveFileSpec" used in Windows code.
    env.Append(
        LIBS = ["Shlwapi"]
    )

    # This is used for "GetCommandLineW" used in Windows code for Python3.
    if python_version >= "3":
        env.Append(
            LIBS = ["Shell32"]
        )

if python_debug:
    env.Append(
        CPPDEFINES = ["Py_DEBUG"]
    )

def detectHostMultiarch():
    import commands

    for line in commands.getoutput("dpkg-architecture").split('\n'):
        if line.startswith("DEB_HOST_MULTIARCH="):
            return line.split('=', 1)[1]
    else:
        return None

if gcc_mode and "linux" in sys.platform:
    if python_version.startswith("3.3"):
        host_multiarch = detectHostMultiarch()

        if host_multiarch is not None:
            env.Append(
                CCFLAGS = [
                    "-I" + os.path.join(
                        "/usr/include/",
                        host_multiarch,
                        "python" + python_version
                    )
                ]
            )


# For Nuitka, it generally is OK to break out of the virtualenv, and use the
# original install. Mind you, this is not about executing anything, this is
# about building, and finding the headers to compile against that Python, we
# do not care about any site packages, and so on.

if not win_target and os.path.islink(os.path.join(python_prefix, ".Python")):
    # Some virtualenv, at least on MacOS, have such handy links.
    python_prefix = os.path.normpath(
        os.path.join(
            os.readlink(os.path.join(python_prefix, ".Python")), # @UndefinedVariable
            ".."
        )
    )
elif not win_target and python_version >= "3.3" and \
     os.path.exists(os.path.join(python_prefix, "bin/activate")):
    # For "venv" of Python3.3, it seems necessary to find include files
    # and libraries on the outside.
    python_binary = os.path.join(python_prefix, "bin", "python")
    python_binary = os.path.realpath(python_binary)

    python_prefix = os.path.normpath(
        os.path.join(
            python_binary,
            "..",
            ".."
        )
    )
elif os.path.exists(os.path.join(python_prefix, "Lib/orig-prefix.txt")):
    python_prefix = open(
        os.path.join(python_prefix, "Lib/orig-prefix.txt")
    ).read()

    # Trailing spaces in the python prefix, please not.
    assert python_prefix == python_prefix.strip()

if win_target:
    # On Windows, the installation layout is relatively fixed.
    python_header_path = os.path.join(python_prefix, "include")
else:
    # The python header path is a combination of python version and debug
    # indication, make sure the headers are found by adding it to the C++
    # include path.

    python_header_path = os.path.join(
        python_prefix,
        "include",
        "python" + python_version
    )

if not os.path.exists(os.path.join(python_header_path, "Python.h")):
    # Not, for --python-debug other headers are used than for normal
    # compilation.
    sys.exit(
        """\
Error, no 'Python.h' %s headers can be found at '%s', dependency \
not satisfied!""" % (
            "debug" if python_debug else "development",
            python_header_path
        )
    )

env.Append(CPPPATH = [python_header_path])

# To support self built Python on Windows, need to also add the "PC" directory,
# that a normal install won't have.
env.Append(CPPPATH = [os.path.join(python_prefix, "PC")])


def getWindowsPythonDLLPath():
    return os.environ["NUITKA_PYTHON_DLL_PATH"]

def makeWindowsMingGW64Lib(win_lib_path):
    # Create the LIB in the build directory.
    new_win_lib_path = os.path.join(source_dir, "libs")

    if not os.path.exists(new_win_lib_path):
        os.makedirs(new_win_lib_path)

    gendef_tool = os.path.join(
        os.path.dirname(
            getExecutablePath(env["CXX"], initial = False)
        ),
        "gendef"
    )

    command = [gendef_tool, "-", getWindowsPythonDLLPath()]

    if show_scons_mode:
        print "Scons: Creating Python library defs with:", command

    try:
        proc = subprocess.Popen(
            command,
            stdin       = subprocess.PIPE,
            stdout      = subprocess.PIPE,
            stderr      = subprocess.PIPE,
            shell       = False
        )
    except WindowsError:
        sys.exit('Error, cannot find "gendef.exe" which is required though.')

    defs, stderr = proc.communicate()
    rv = proc.wait()

    if rv != 0:
        sys.exit('Error, unexpected error from "gendef.exe" %s.' % stderr)

    defs_filename = os.path.join(
        new_win_lib_path,
        "python%s.defs" % python_version.replace('.', "")
    )

    defs_file = open(
        defs_filename,
        'w'
    )
    defs_file.write(defs)
    defs_file.close()

    shutil.copy(
        getWindowsPythonDLLPath(),
        new_win_lib_path
    )

    lib_filename = os.path.join(
        new_win_lib_path,
        "libpython%s.a" % python_version.replace('.', "")
    )

    dll_tool = os.path.join(
        os.path.dirname(
            getExecutablePath(env["CXX"], initial = False)
        ),
        "dlltool"
    )

    result = subprocess.call(
        (
            dll_tool,
            "-d",
            defs_filename,
            "-l",
            lib_filename
        ),
        stdout = subprocess.PIPE
    )

    if result != 0:
        sys.exit("Error, call to 'dlltool.exe' failed.")

    return new_win_lib_path


if win_target:
    # MinGW for 64 bits needs this due to CPython being compiled with MSVC.
    if python_debug:
        win_lib_name ="python" + python_version.replace('.', "") + "_d"
    else:
        win_lib_name = "python" + python_version.replace('.', "")

    for candidate in ("libs", "PCBuild"):
        win_lib_path = os.path.join(python_prefix, candidate)

        if os.path.exists(os.path.join(win_lib_path, win_lib_name + ".lib")):
            break
    else:
        sys.exit("Error, cannot find '%s.lib' file." % win_lib_name)

    if target_arch == "x86_64" and gcc_mode:
        win_lib_path = makeWindowsMingGW64Lib(win_lib_path)

    env.Append(LIBPATH = [win_lib_path])
    env.Append(LIBS = [win_lib_name])

else:
    # Debian and Ubuntu distinguish the system libraries like this.
    if python_debug and \
       python_prefix == "/usr" and \
       not python_version.startswith('3') and \
       platform.dist()[0].lower() in ("debian", "ubuntu"):
        env.Append(LIBS = ["python" + python_version + "_d"])
    else:
        env.Append(LIBS = ["python" + python_version])

    if python_prefix != "/usr" and "linux" in sys.platform:
        env.Append(
            LIBS = ["dl", "pthread", "util", 'm']
        )

        if gcc_mode and "clang" not in env["CXX"]:
            env.Append(
                LINKFLAGS = ["-export-dynamic"]
            )

    # Add the python library path to the library path
    python_lib_path = os.path.join(python_prefix, "lib")
    env.Append(LIBPATH = [python_lib_path])

    # For NetBSD the rpath is required, on FreeBSD it's warned as unused.
    if "netbsd" in sys.platform:
        env.Append(
            LINKFLAGS = ["-rpath=" + python_lib_path]
        )

# The static include files reside in Nuitka installation, which may be where
# the "nuitka.build" package lives.
nuitka_include = os.path.join(
    nuitka_src,
    "include"
)

# We have include files in the build directory and the static include directory
# that is located inside Nuitka installation.
env.Append(
    CPPPATH = [
        source_dir,
        nuitka_include
    ]
)

if debug_mode or unstripped_mode:
    # Use debug format, so we get good tracebacks from it.
    if gcc_mode:
        env.Append(
            CCFLAGS = ["-g"]
        )
        env.Append(
            ASFLAGS = ["-g"]
        )

        if "g++" in env["CXX"]:
            env.Append(
                CCFLAGS = ["-feliminate-unused-debug-types"]
            )
    elif msvc_mode:
        env.Append(
            CCFLAGS = ["/Z7"]
        )

        # Higher MSVC versions need this for parallel compilation
        if job_count > 1 and getMsvcVersion() >= 11:
            env.Append(
                CCFLAGS = ["/FS"]
            )

        env.Append(
            LINKFLAGS = ["/DEBUG"]
        )
else:
    if gcc_mode:
        if sys.platform == "darwin":
            env.Append(
                LINKFLAGS = ["-Wno-deprecated-declarations"]
            )
        else:
            env.Append(
                LINKFLAGS = ["-s"]
            )

# When debugging, optimize less than when optimizing, when not remove
# assertions.
if debug_mode:
    if not optimize_mode:
        if gcc_mode or msvc_mode:
            env.Append(
                CCFLAGS = ["-O2"]
            )
else:
    env.Append(
        CPPDEFINES = ["__NUITKA_NO_ASSERT__"]
    )

if optimize_mode:
    if gcc_mode:
        env.Append(
            CCFLAGS = ["-O3"]
        )

        # Check inlining of calls, except in debug mode, where it will all be
        # wrong due to additional code.
        # if not debug_mode:
        #     env.Append( CCFLAGS = [ "-Winline" ] )
    elif msvc_mode:
        env.Append(
            CCFLAGS = ["/Ox"]
        )

# MinGW for 64 bits needs this due to CPython bugs.
if win_target and target_arch == "x86_64" and gcc_mode:
    env.Append(CPPDEFINES = ["MS_WIN64"])

# Set load libpython from binary directory default
if standalone_mode and gcc_mode and sys.platform != "darwin":
    env.Append(
        LINKFLAGS = ["-Wl,-R,'$$ORIGIN'"]
    )

def getLinkerArch():
    if "linux" in sys.platform:
        if target_arch == "x86_64":
            return "elf64-x86-64"
        elif target_arch == "armv5tel":
            # Debian arch "armel"
            return "elf32-littlearm"
        elif target_arch == "armv7l":
            # Debian arch "armhf"
            return "elf32-littlearm"
        else:
            # TODO: Detect from objdump mayhaps.
            return None
    else:
        # TODO: Missing for MacOS, FreeBSD, other Linux
        return None

constants_bin_filename = os.path.join(source_dir,"__constants.bin")

if win_target and not module_mode:
    # On Windows constants are accesses as a resource, except in shared
    # libraries, where that option is not available.
    constants_generated_filename = None

    env.Append(
        CPPDEFINES = ["_NUITKA_CONSTANTS_FROM_RESOURCE"]
    )
elif gcc_mode and getLinkerArch() is not None:
    env.Append(
        LINKFLAGS = [
            "-Wl,-b", "-Wl,binary",
            "-Wl,%s" % constants_bin_filename,
            "-Wl,-b", "-Wl,%s" % getLinkerArch(),
            "-Wl,-defsym",
            "-Wl,%sconstant_bin=_binary_%s___constants_bin_start" % (
                '_' if mingw_mode else "",
                "".join(re.sub("[^a-zA-Z0-9_]",'_',c) for c in source_dir)
            )
        ]
    )

    constants_generated_filename = None
else:
    constants_generated_filename = os.path.join(
        source_dir,
        "__constants_data.c"
    )

    with open(constants_generated_filename, 'w') as output:
        if not c11_mode:
            output.write('extern "C" ')

        output.write("const unsigned char constant_bin[] =\n{\n")

        for count, stream_byte in enumerate(
                open(constants_bin_filename, "rb").read()
            ):
            if count % 16 == 0:
                if count > 0:
                    output.write('\n')

                output.write("   ")

            output.write(" 0x%02x," % ord(stream_byte))

        output.write("\n};\n");

env.Append(
    CPPDEFINES = [
        "_NUITKA_FROZEN=%d" % frozen_modules,
        "_NUITKA_MODULE_COUNT=%d" % module_count
    ]
)

# Tell compiler to create a shared library or program.
if module_mode:
    if "g++" in env["CXX"]:
        env.Append(
            CCFLAGS = ["-shared"]
        )
    elif "clang" in env["CXX"]:
        pass
    elif msvc_mode:
        env.Append(
            CCFLAGS = ["/LD"]
        )
    else:
        assert False
else:
    if msvc_mode:
        env.Append(
            CCFLAGS = ["/MT"]
        )

if module_mode:
    env.Append(
        CPPDEFINES = ["_NUITKA_MODULE"]
    )
else:
    env.Append(
        CPPDEFINES = ["_NUITKA_EXE"]
    )

def discoverSourceFiles():
    result = []

    # If we use C11 capable compiler, all good. Otherwise use C++, which scons
    # needs to derive from filenames, so make copies (or links) with a different
    # name.
    def cheap_copy(src,dst):
        if win_target:
            # Windows has symlinks these days, but they do not integrate well
            # with Python2 at least.So make a copy in any case.
            if os.path.exists(dst):
                os.unlink(dst)
            shutil.copy(src,dst)
        else:
            # Relative paths work badly for links. Creating them relative is
            # not worth the effort.
            src = os.path.abspath(src)

            try:
                link_target = os.readlink(dst)

                # If it's already a proper link, do nothing then.
                if link_target == src:
                    return

                os.unlink(dst)
            except OSError as e:
                # Broken links work like that, remove them, so we can replace
                # them.
                try:
                    os.unlink(dst)
                except OSError:
                    pass

            os.symlink(src,dst)

    def provideStatic(sub_path):
        # TODO: That should be normpath maybe.
        sub_path = sub_path.replace('/', os.path.sep)

        source_file = os.path.join(nuitka_src, "static_src", sub_path)
        target_file = os.path.join(source_dir, os.path.basename(sub_path))

        if target_file.endswith(".c") and not c11_mode:
            target_file += "pp"

        cheap_copy(source_file, target_file)

        return target_file

    # Scan for Nuitka created source files, and add them too.
    for filename in os.listdir(source_dir):
        # Only C files are of interest here.
        if not filename.endswith(".c"):
            continue

        filename = os.path.join(source_dir, filename)

        target_file = filename

        # We pretend to use C++ if no C11 compiler is present.
        if c11_mode:
            result.append(filename)
        else:
            target_file += "pp" # .cpp" suffix then

            os.rename(filename, target_file)
            result.append(target_file)

    # Main program, unless of course it's a Python module/package we build.
    if not module_mode:
        result.append(provideStatic("MainProgram.c"))

    # Compiled types and helper routines.
    result.append(provideStatic("CompiledFunctionType.c"))
    result.append(provideStatic("CompiledMethodType.c"))
    result.append(provideStatic("CompiledGeneratorType.c"))
    if python_version >= "3.5":
        result.append(provideStatic("CompiledCoroutineType.c"))
    result.append(provideStatic("CompiledFrameType.c"))
    result.append(provideStatic("CompiledCodeHelpers.c"))
    result.append(provideStatic("InspectPatcher.c"))
    result.append(provideStatic("MetaPathBasedLoader.c"))

    # Platform dependent fiber implementations for generators to use.
    if win_target:
        result.append(provideStatic("win32_ucontext_src/fibers_win32.c"))
    elif target_arch == "x86_64" and "linux" in sys.platform:
        result.append(provideStatic("x64_ucontext_src/fibers_x64.c"))
        result.append(provideStatic("x64_ucontext_src/swapfiber.S"))
    elif target_arch == "armv5tel":
        result.append(provideStatic("arm_ucontext_src/fibers_arm.c"))
        result.append(provideStatic("arm_ucontext_src/ucontext.c"))
        result.append(provideStatic("arm_ucontext_src/getcontext.asm"))
    elif "openbsd" in sys.platform:
        result.append(provideStatic("libcoro_ucontext_src/fibers_coro.c"))
        result.append(provideStatic("libcoro_ucontext_src/coro.c"))

        env.Append(
            CPPDEFINES = ["CORO_SJLJ"]
        )
    elif os.path.isfile("/etc/alpine-release"):
        result.append(provideStatic("libcoro_ucontext_src/fibers_coro.c"))
        result.append(provideStatic("libcoro_ucontext_src/coro.c"))

        env.Append(
            CPPDEFINES = ["CORO_SJLJ", "__OpenBSD__"]
        )
    else:
        # Variant based on deprecated, but still present versions of
        # getcontext/setcontext/swapcontext/makecontext
        result.append(provideStatic("gen_ucontext_src/fibers_gen.c"))

    return result

source_targets = []

# Prepare the use of a custom specs file for windows targets. We change the used
# specs for linking to avoid the use of the wrong (for CPython) run time
# library.
if win_target:
    rc_content = []

    # Which files to depend on.
    rc_file_dependencies = []

    if not module_mode:
        rc_content.append(
            '3 RCDATA "%s"' % constants_bin_filename.replace('\\', '/')
        )

        rc_file_dependencies.append(constants_bin_filename)

        if python_version < "3.3":
            manifest_filename = os.path.join(
                source_dir,
                "resources.manifest"
            )

            python_exe = os.environ["NUITKA_PYTHON_EXE_PATH"]
            assert os.path.exists(python_exe), python_exe

            if msvc_mode:
                try:
                    result = subprocess.call(
                        (
                            env["MT"],
                            "-inputresource:%s;#1" % python_exe,
                            "-out:%s" % manifest_filename
                        ),
                        stdout = subprocess.PIPE
                    )
                except OSError:
                    result = -1

                if result != 0:
                    sys.exit(
                        """\
Error, call to 'mt.exe' (%s) to extract manifest failed (error code %d.""" % (
                            env["MT"],
                            result
                        )
                    )
            else:
                try:
                    result = subprocess.call(
                        (
                            "windres.exe",
                            "--input", python_exe,
                            "--output", manifest_filename,
                            "--output-format", "rc"
                        ),
                        stdout = subprocess.PIPE
                    )
                except WindowsError:
                    sys.exit("Error, cannot find 'windres.exe' but it is required.")


                if result != 0:
                    sys.exit("Error, call to 'windres.exe' to extract manifest failed.")

                # Need to reduce to what we use, and remove the quoting of the
                # actual manifest. We created a full "rc" file, but that we do
                # not have use for.
                manifest_file = open(manifest_filename, 'r')
                inside = False
                contents = []
                for line in manifest_file:
                    if "RT_MANIFEST" in line:
                        inside = True
                        continue

                    if not inside:
                        continue

                    line = line.replace(r"\r\n", "\r\n")
                    line = line.replace('""', '"')

                    contents.append(
                        line[line.find('"')+1:line.rfind('"')]
                    )

                manifest_file = open(manifest_filename, 'w')
                manifest_file.write("".join(contents[1:-1]))
                manifest_file.close()

            rc_content.append(
                "1 RT_MANIFEST resources.manifest"
            )

            rc_file_dependencies.append(manifest_filename)

        if icon_path:
            rc_content.append(
                '2 ICON MOVEABLE PURE LOADONCALL DISCARDABLE "%s"' % (
                    icon_path.replace('\\', '/')
                )
            )

            rc_file_dependencies.append(icon_path)

    if rc_content:
        rc_filename = os.path.join(source_dir, "resources.rc")

        rc_file = open(rc_filename, 'w')

        rc_content.insert(
            0,
            '#include "winuser.h"'
        )
        rc_file.write('\n'.join(rc_content))
        rc_file.close()

        res_target = env.RES(rc_filename)

        for rc_file_dependency in rc_file_dependencies:
            Depends(res_target, rc_file_dependency) # @UndefinedVariable

        source_targets.append(
            res_target
        )

source_files = discoverSourceFiles()

if module_mode:
    # For Python modules, the standard shared library extension is not what
    # gets used.
    if win_target:
        module_suffix = ".pyd"
    else:
        module_suffix = ".so"

    env["SHLIBSUFFIX"] = module_suffix

    target = env.SharedLibrary(
        result_basepath,
        source_files  + source_targets
    )
else:

    target = env.Program(
        result_basepath + ".exe",
        source_files + source_targets
    )

# Avoid dependency on MinGW libraries.
if win_target and gcc_mode:
    env.Append(
        LINKFLAGS = [
            "-static-libgcc",
            "-static-libstdc++"
        ]
    )


# On some architectures, makecontext cannot pass pointers reliably.
if target_arch == "x86_64" and "linux" in sys.platform:
    env.Append(CPPDEFINES = ["_NUITKA_MAKECONTEXT_INTS"])

# Avoid IO for compilation as much as possible, this should make the
# compilation more memory hungry, but also faster.
if gcc_mode:
    env.Append(CCFLAGS = "-pipe")

if "CPPFLAGS" in os.environ:
    env.Append(CCFLAGS = os.environ["CPPFLAGS"].split())
if "CCFLAGS" in os.environ:
    env.Append(CCFLAGS = os.environ["CCFLAGS"].split())
if "CXXFLAGS" in os.environ:
    env.Append(CCFLAGS = os.environ["CXXFLAGS"].split())

if "LDFLAGS" in os.environ:
    env.Append(LINKFLAGS = os.environ["LDFLAGS"].split())

# Remove the target file to avoid cases where it falsely doesn't get rebuild
# and then lingers from previous builds,
if os.path.exists(target[0].abspath):
    os.unlink(target[0].abspath)

if show_scons_mode:
    print "scons: Told to run compilation on %d CPUs." % job_count

# Cached, when done, by the fastest possible algorithm and right inside the
# build directory. Makes no sense of course, if that is removed later on by
# Nuitka.
if cache_mode:
    CacheDir(os.path.join(source_dir, "cache-" + target_arch)) # @UndefinedVariable
    Decider("MD5-timestamp") # @UndefinedVariable

# Before we go, also lets turn KeyboardInterrupt into a mere error exit.

def signalHandler(signal, frame):
    sys.exit(2)

signal.signal(signal.SIGINT, signalHandler)

def makeCLiteral(value):
    value = value.replace("\\", r"\\")
    value = value.replace('"', r'\"')

    return '"' + value + '"'

if uninstalled_python:
    if win_target:
        env.Append(
            CPPDEFINES = [
                'DLL_EXTRA_PATH=%s' % \
                   makeCLiteral(
                       os.path.dirname(getWindowsPythonDLLPath())
                   )
            ]
        )
    elif sys.platform.startswith("linux"):
        env.Append(
            LINKFLAGS = ["-Wl,-R," + python_lib_path]
        )

        env.Append(
            CPPDEFINES = [
                "PYTHON_HOME_PATH='%s'" % \
                   makeCLiteral(
                       python_prefix
                   )
            ]
        )

# env["CFLAGS"] = env["CCFLAGS"]

Default(target) # @UndefinedVariable

# Copy the Python DLL to the target directory, so the executable can be run.
if uninstalled_python:
    if win_target:
        # TODO: This should have become unnecessary by DLL_EXTRA_PATH
        shutil.copy(
            getWindowsPythonDLLPath(),
            os.path.dirname(result_basepath) or "."
        )
    elif sys.platform == "darwin":
        python_dll_filename = "libpython" + python_version + ".dylib"

        result = subprocess.call(
            (
                "install_name_tool",
                "--change",
                python_dll_filename,
                os.path.join(python_lib_path, python_dll_filename),
                result_basepath + ".exe"
            ),
            stdout = subprocess.PIPE
        )

        if result != 0:
            sys.exit("Error, call to 'install_name_tool' to fix Python library path failed.")
