# -*- mode: python; -*-

# This SConscript describes build rules for the "mongo" project.

import os
import itertools
import subprocess
import sys
from buildscripts import utils

import SCons.Action

Import("env")
Import("has_option")
Import("get_option")
Import("usev8")
Import("use_system_version_of_library")

# Boost we need everywhere. 's2' is spammed in all over the place by
# db/geo unfortunately. pcre is also used many places.
env.InjectThirdPartyIncludePaths(libraries=['boost', 's2', 'pcre'])
env.InjectMongoIncludePaths()

env.SConscript(
    dirs=[
        'base',
        'bson',
        'client',
        'crypto',
        'db',
        'dbtests',
        'installer',
        'logger',
        'platform',
        'rpc',
        's',
        'scripting',
        'shell',
        'tools',
        'unittest',
        'util',
    ],
)

def get_toolchain_ver(tool):
    # By default we don't know the version of each tool, and only report what
    # command gets executed (gcc vs /opt/mongodbtoolchain/bin/gcc).
    verstr = "version unknown"
    proc = None
    if env.ToolchainIs('clang', 'gcc'):
        proc = SCons.Action._subproc(env,
            env.subst("${%s} --version" % tool),
            stdout=subprocess.PIPE,
            stderr='devnull',
            stdin='devnull',
            universal_newlines=True,
            error='raise',
            shell=True)
        verstr = proc.stdout.readline()

    elif env.ToolchainIs('msvc') and env.TargetOSIs('windows'):
        proc = SCons.Action._subproc(env,
            env.subst("${%s}" % tool),
            stdout='devnull',
            stderr=subprocess.PIPE,
            stdin='devnull',
            universal_newlines=True,
            error='raise',
            shell=True)
        verstr = proc.stderr.readline()

    # If we started a process, we should drain its stdout/stderr and wait for
    # it to end.
    if proc:
        proc.communicate()

    return env.subst('${%s}: %s' % (tool, verstr))

js_engine_ver = get_option("js-engine") if get_option("server-js") == "on" else "none"

# On windows, we need to escape the backslashes in the command-line
# so that windows paths look okay.
cmd_line = " ".join(sys.argv).encode('string-escape')
if env.TargetOSIs('windows'):
    cmd_line = cmd_line.replace('\\', r'\\')

module_list = '{ %s }' % ', '.join([ '"{0}"'.format(x) for x in env['MONGO_MODULES'] ])

versionInfo = env.Substfile(
    'util/version.cpp.in',
    SUBST_DICT=[
        ('@mongo_code_version@', env['MONGO_CODE_VERSION']),
        ('@buildinfo_git_version@', env['MONGO_GIT_VERSION']),
        ('@buildinfo_js_engine@', js_engine_ver),
        ('@buildinfo_allocator@', GetOption('allocator')),
        ('@buildinfo_ccflags@', env['CCFLAGS']),
        ('@buildinfo_cflags@', env['CFLAGS']),
        ('@buildinfo_cxxflags@', env['CXXFLAGS']),
        ('@buildinfo_linkflags@', env['LINKFLAGS']),
        ('@buildinfo_cmdline@', ''),
        ('@buildinfo_modules@', module_list),
        ('@buildinfo_target_arch@', env['TARGET_ARCH']),
        ('@buildinfo_target_os@', env.GetTargetOSName()),
        ('@buildinfo_cc_version@', get_toolchain_ver('CC')),
        ('@buildinfo_cxx_version@', get_toolchain_ver('CXX')),
    ])

config_header_substs = (
    ('@mongo_config_byte_order@', 'MONGO_CONFIG_BYTE_ORDER'),
    ('@mongo_config_debug_build@', 'MONGO_CONFIG_DEBUG_BUILD'),
    ('@mongo_config_have___declspec_thread@', 'MONGO_CONFIG_HAVE___DECLSPEC_THREAD'),
    ('@mongo_config_have___thread@', 'MONGO_CONFIG_HAVE___THREAD'),
    ('@mongo_config_have_execinfo_backtrace@', 'MONGO_CONFIG_HAVE_EXECINFO_BACKTRACE'),
    ('@mongo_config_have_header_unistd_h@', 'MONGO_CONFIG_HAVE_HEADER_UNISTD_H'),
    ('@mongo_config_have_posix_monotonic_clock@', 'MONGO_CONFIG_HAVE_POSIX_MONOTONIC_CLOCK'),
    ('@mongo_config_have_std_is_trivially_copyable@', 'MONGO_CONFIG_HAVE_STD_IS_TRIVIALLY_COPYABLE'),
    ('@mongo_config_have_std_make_unique@', 'MONGO_CONFIG_HAVE_STD_MAKE_UNIQUE'),
    ('@mongo_config_optimized_build@', 'MONGO_CONFIG_OPTIMIZED_BUILD'),
    ('@mongo_config_ssl@', 'MONGO_CONFIG_SSL'),
    ('@mongo_config_ssl_fips@', 'MONGO_CONFIG_SSL_FIPS'),
    ('@mongo_config_use_gdbserver@', 'MONGO_CONFIG_USE_GDBSERVER'),
)

def makeConfigHeaderDefine(self, key):
    val = "// #undef {0}".format(key)
    if key in self['CONFIG_HEADER_DEFINES']:
        val = "#define {0} {1}".format(key, self['CONFIG_HEADER_DEFINES'][key])
    return val
env.AddMethod(makeConfigHeaderDefine)

configHeaderFile = env.Substfile(
    'config.h.in',
    SUBST_DICT=[(k, env.makeConfigHeaderDefine(v)) for (k, v) in config_header_substs]
)

env.Library('version',
            [
                'util/version.cpp'
            ],
            LIBDEPS=[
                'bson/bson',
                '$BUILD_DIR/mongo/base/base'
            ])

env.CppUnitTest(
    target="util/version_test",
    source=["util/version_test.cpp"],
    LIBDEPS=[]
)

mongod = env.Program(
    target="mongod",
    source=[
        "db/db.cpp",
        "db/mongod_options_init.cpp",
    ],
    LIBDEPS=[
        "db/coredb",
        "db/conn_pool_options",
        "db/mongod_options",
        "db/mongodandmongos",
        "db/mongodwebserver",
        "db/serveronly",
        "util/ntservice",
    ],
)

env.Default(env.Install('#/', mongod))

# tools
rewrittenTools = [ "mongodump", "mongorestore", "mongoexport", "mongoimport", "mongostat", "mongotop", "bsondump", "mongofiles", "mongooplog" ]

# mongobridge and mongoperf
env.Install(
    '#/',
    [
        env.Program("mongoperf",
                    [
                        "client/examples/mongoperf.cpp",
                    ],
                    LIBDEPS=[
                        "util/ntservice_mock",
                        "db/serveronly",
                        "db/coredb",
                        "util/signal_handlers_synchronous",
                    ]),
    ])

# mongos
env.Install(
    '#/',
    env.Program(
        "mongos",
        [
            "s/server.cpp",
            "s/mongos_options.cpp",
            "s/mongos_options_init.cpp",
        ],
        LIBDEPS=[
            's/catalog/legacy/catalog_manager_legacy',
            's/client/sharding_connection_hook',
            's/commands/cluster_commands',
            "s/mongoscore",
            "db/coredb",
            "s/coreshard",
            "util/ntservice",
            "db/mongodandmongos",
            "db/conn_pool_options",
            '$BUILD_DIR/mongo/util/options_parser/options_parser_init',
        ]))

# --- sniffer ---
mongosniff_built = False
if env.TargetOSIs('osx') or env["_HAVEPCAP"]:
    mongosniff_built = True
    sniffEnv = env.Clone()
    sniffEnv.Append( CPPDEFINES="MONGO_EXPOSE_MACROS" )

    if not env.TargetOSIs('windows'):
        sniffEnv.Append( LIBS=[ "pcap" ] )
    else:
        sniffEnv.Append( LIBS=[ "wpcap" ] )

    sniffEnv.Install( '#/', sniffEnv.Program( "mongosniff", "tools/sniffer.cpp",
                                              LIBDEPS = [
                                                 "db/serveronly",
                                                 "db/coredb",
                                                 "util/signal_handlers_synchronous",
                                              ] ) )

# --- shell ---

if not has_option('noshell') and usev8:
    shell_core_env = env.Clone()
    if has_option("safeshell"):
        shell_core_env.Append(CPPDEFINES=["MONGO_SAFE_SHELL"])
    shell_core_env.Library("shell_core",
                source=[
                    "shell/bench.cpp",
                    "shell/clientAndShell.cpp",
                    "shell/linenoise.cpp",
                    "shell/linenoise_utf8.cpp",
                    "shell/mk_wcwidth.cpp",
                    "shell/mongo-server.cpp",
                    "shell/shell_utils.cpp",
                    "shell/shell_utils_extended.cpp",
                    "shell/shell_utils_launcher.cpp",
                    "shell/shell_options_init.cpp"
                ],
                LIBDEPS=[
                    'db/index/external_key_generator',
                    'db/catalog/index_key_validate',
                    'scripting/scripting',
                    'util/processinfo',
                    'util/signal_handlers',
                    'shell/mongojs',
                ])

    # mongo shell options
    shell_core_env.Library("shell_options", ["shell/shell_options.cpp"],
                LIBDEPS=['$BUILD_DIR/mongo/util/options_parser/options_parser_init'])

    shellEnv = env.Clone()
    if env.TargetOSIs('windows'):
       shellEnv.Append(LIBS=["winmm.lib"])

    mongo_shell = shellEnv.Program(
        "mongo",
        "shell/dbshell.cpp",
        LIBDEPS=["$BUILD_DIR/third_party/shim_pcrecpp",
                 "shell_options",
                 "shell_core",
                 "db/server_options_core",
                 "client/clientdriver",
                 "$BUILD_DIR/mongo/util/password",
                 ])

    shellEnv.Install( '#/', mongo_shell )
else:
    shellEnv = None

#  ----  INSTALL -------

# binaries

distBinaries = []

if env.TargetOSIs('windows'):
    distBinaries.extend(['mongod.pdb', 'mongos.pdb'])

def add_exe( v ):
    return "${PROGPREFIX}%s${PROGSUFFIX}" % v

def installBinary( e, name ):
    name = add_exe( name )

    if env.TargetOSIs('solaris', 'linux') and (not has_option("nostrip")):
        name = e.Command('stripped/%s' % name, name, Copy('$TARGET', '$SOURCE'))[0]
        e.AddPostAction(name, 'strip $TARGET')

    distBinaries.append(name)
    inst = e.Install( "$INSTALL_DIR/bin", name )

    if env.TargetOSIs('posix'):
        e.AddPostAction( inst, 'chmod 755 $TARGET' )

def installExternalBinary( e, name_str ):
    name = env.File("#/%s" % add_exe(name_str))
    if not name.isfile():
        env.FatalError("ERROR: external binary not found: {0}", name)

    distBinaries.append(name)
    inst = e.Install( "$INSTALL_DIR/bin", name )

    if env.TargetOSIs('posix'):
        e.AddPostAction( inst, 'chmod 755 $TARGET' )


# "--use-new-tools" adds dependencies for rewritten (Go) tools
# It is required for "dist" but optional for "install"
if has_option("use-new-tools"):
    toolsRoot = "src/mongo-tools"
    for t in rewrittenTools:
        installExternalBinary(env, "%s/%s" % (toolsRoot, t))

# legacy tools
installBinary(env, "mongoperf")
env.Alias("tools", '#/' + add_exe("mongoperf"))

env.Alias("tools", "#/" + add_exe("mongobridge"))

if mongosniff_built:
    installBinary(env, "mongosniff")
    env.Alias("tools", '#/' + add_exe("mongosniff"))

installBinary( env, "mongod" )
installBinary( env, "mongos" )

if shellEnv is not None:
    installBinary( shellEnv, "mongo" )

env.Alias( "core", [ '#/%s' % b for b in [ add_exe( "mongo" ), add_exe( "mongod" ), add_exe( "mongos" ) ] ] )

# Stage the top-level mongodb banners
distsrc = env.Dir('#distsrc')
env.Append(MODULE_BANNERS = [distsrc.File('README'),
                             distsrc.File('THIRD-PARTY-NOTICES')])

# If no module has introduced a file named LICENSE.txt, then inject the AGPL.
if sum(itertools.imap(lambda x: x.name == "LICENSE.txt", env['MODULE_BANNERS'])) == 0:
    env.Append(MODULE_BANNERS = [distsrc.File('GNU-AGPL-3.0')])

# All module banners get staged to the top level of the tarfile, so we
# need to fail if we are going to have a name collision.
module_banner_filenames = set([f.name for f in env['MODULE_BANNERS']])
if not len(module_banner_filenames) == len(env['MODULE_BANNERS']):
    # TODO: Be nice and identify conflicts in error.
    env.FatalError("ERROR: Filename conflicts exist in module banners.")

# Build a set of directories containing module banners, and use that
# to build a --transform option for each directory so that the files
# are tar'ed up to the proper location.
module_banner_dirs = set([Dir('#').rel_path(f.get_dir()) for f in env['MODULE_BANNERS']])
module_banner_transforms = ["--transform %s=$SERVER_DIST_BASENAME" % d for d in module_banner_dirs]

# Allow modules to map original file name directories to subdirectories 
# within the archive (e.g. { "src/mongo/db/modules/enterprise/docs": "snmp"})
archive_addition_transforms = []
for full_dir, archive_dir in env["ARCHIVE_ADDITION_DIR_MAP"].items():
  archive_addition_transforms.append("--transform \"%s=$SERVER_DIST_BASENAME/%s\"" %
                                     (full_dir, archive_dir))

# "dist" target is valid only when --use-new-tools is specified
# Attempts to build release artifacts without tools must fail
if has_option("use-new-tools"):
    env.Command(
        '#/${SERVER_ARCHIVE}',
        ['#buildscripts/make_archive.py'] + env["MODULE_BANNERS"] + env["ARCHIVE_ADDITIONS"] +
        distBinaries, ' '.join(['$PYTHON ${SOURCES[0]} -o $TARGET'] + archive_addition_transforms +
        module_banner_transforms + [
                '--transform ${str(Dir(BUILD_DIR))}/mongo/stripped=$SERVER_DIST_BASENAME/bin',
                '--transform ${str(Dir(BUILD_DIR))}/mongo=$SERVER_DIST_BASENAME/bin',
                '--transform ${str(Dir(BUILD_DIR))}/mongo/stripped/src/mongo-tools=$SERVER_DIST_BASENAME/bin',
                '--transform src/mongo-tools=$SERVER_DIST_BASENAME/bin',
                '${TEMPFILE(SOURCES[1:])}']))

    env.Alias("dist", source='#/${SERVER_ARCHIVE}')
else:
    def failDist(env, target, source):
        env.FatalError("ERROR: 'dist' target only valid with --use-new-tools.")
    env.Alias("dist", [], [ failDist ] )
    env.AlwaysBuild("dist")


#final alias
env.Alias( "install", "$INSTALL_DIR" )
