chromium/third_party/sqlite/scripts/generate_amalgamation.py

#!/usr/bin/env python3
"""Creates the Chromium SQLite amalgamation.

The amalgamation is a single large source file (sqlite3.c) containing all
of the SQLite code. More at https://www.sqlite.org/amalgamation.html.

Usage:
    generate_amalgamation.py
"""

import argparse
import os
import stat
import subprocess
import sys
import tempfile
from shutil import copyfile, rmtree
from extract_sqlite_api import ProcessSourceFile, header_line, footer_line

# The Chromium SQLite third party directory (i.e. //third_party/sqlite).
_SQLITE_ROOT_DIR = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))

# The Chromium SQLite source directory (i.e. //third_party/sqlite/src).
_SQLITE_SRC_DIR = os.path.join(_SQLITE_ROOT_DIR, 'src')

# The .gni file (also used by BUILD.gn when building) which contains all
# flags passed to the `configuration` script and also used for the compile.
_COMMON_CONFIGURATION_FLAGS_GNI_FILE = os.path.join(
    _SQLITE_ROOT_DIR, 'sqlite_common_configuration_flags.gni')

_CHROMIUM_CONFIGURATION_FLAGS_GNI_FILE = os.path.join(
    _SQLITE_ROOT_DIR, 'sqlite_chromium_configuration_flags.gni')

_DEV_CONFIGURATION_FLAGS_GNI_FILE = os.path.join(
    _SQLITE_ROOT_DIR, 'sqlite_dev_configuration_flags.gni')

# The temporary directory where `make configure` and the amalgamation
# is temporarily created.
_TEMP_CONFIG_DIR = tempfile.mkdtemp()

# Set to True to generate a configuration which is compatible for
# running the SQLite tests.
_CONFIGURE_FOR_TESTING = False


def get_amalgamation_dir(config_name):
    if config_name == 'chromium':
        return os.path.join(_SQLITE_SRC_DIR, 'amalgamation')
    elif config_name == 'dev':
        return os.path.join(_SQLITE_SRC_DIR, 'amalgamation_dev')
    else:
        assert False


def _icu_cpp_flags():
    """Return the libicu C++ flags."""
    cmd = ['icu-config', '--cppflags']
    try:
        return subprocess.check_output(cmd)
    except Exception:
        return ''


def _icu_ld_flags():
    """Return the libicu linker flags."""
    cmd = ['icu-config', '--ldflags']
    try:
        return subprocess.check_output(cmd)
    except Exception:
        return ''


def _strip_flags_for_testing(flags):
    """Accepts the default configure/build flags and strips out those

    incompatible with the SQLite tests.

    When configuring SQLite to run tests this script uses a configuration
    as close to what Chromium ships as possible. Some flags need to be
    omitted for the tests to link and run correct. See comments below.
    """
    test_flags = []
    for flag in flags:
        # Omitting features can cause tests to hang/crash/fail because the
        # SQLite tests don't seem to detect feature omission. Keep them enabled.
        if flag.startswith('SQLITE_OMIT_'):
            continue

        # Some tests compile with specific SQLITE_DEFAULT_PAGE_SIZE so do
        # not hard-code.
        if flag.startswith('SQLITE_DEFAULT_PAGE_SIZE='):
            continue

        # Some tests compile with specific SQLITE_DEFAULT_MEMSTATUS so do
        # not hard-code.
        if flag.startswith('SQLITE_DEFAULT_MEMSTATUS='):
            continue

        # If enabled then get undefined reference to `uregex_open_63' and
        # other *_64 functions.
        if flag == 'SQLITE_ENABLE_ICU':
            continue

        # If defined then the fts4umlaut tests fail with the following error:
        #
        # Error: unknown tokenizer: unicode61
        if flag == 'SQLITE_DISABLE_FTS3_UNICODE':
            continue

        test_flags.append(flag)
    return test_flags


def _read_flags(file_name, param_name):
    config_globals = dict()
    with open(file_name) as input_file:
        code = compile(input_file.read(), file_name, 'exec')
        exec (code, config_globals)
    return config_globals[param_name]


def _read_configuration_values(config_name):
    """Read the configuration flags and return them in an array.


    |config_name| is one of "chromium" or "dev".
    """
    common_flags = _read_flags(_COMMON_CONFIGURATION_FLAGS_GNI_FILE,
                               'sqlite_common_configuration_flags')
    chromium_flags = _read_flags(_CHROMIUM_CONFIGURATION_FLAGS_GNI_FILE,
                                 'sqlite_chromium_configuration_flags')
    dev_flags = _read_flags(_DEV_CONFIGURATION_FLAGS_GNI_FILE,
                            'sqlite_dev_configuration_flags')

    if config_name == 'chromium':
        flags = common_flags + chromium_flags
    elif config_name == 'dev':
        flags = common_flags + dev_flags
    else:
        print('Incorrect config "%s"' % config_name, file=sys.stderr)
        sys.exit(1)

    if _CONFIGURE_FOR_TESTING:
        flags = _strip_flags_for_testing(flags)
    return flags


def _do_configure(config_name):
    """Run the configure script for the SQLite source."""
    configure = os.path.join(_SQLITE_SRC_DIR, 'configure')
    build_flags = ' '.join(
        ['-D' + f for f in _read_configuration_values(config_name)])
    cflags = '-Os {} {}'.format(build_flags, _icu_cpp_flags())
    ldflags = _icu_ld_flags()

    cmd = [
        configure,
        'CFLAGS={}'.format(cflags),
        'LDFLAGS={}'.format(ldflags),
        '--disable-load-extension',
        '--enable-amalgamation',
        '--enable-threadsafe',
    ]
    subprocess.check_call(cmd)

    if _CONFIGURE_FOR_TESTING:
        # Copy the files necessary for building/running tests back
        #into the source directory.
        files = ['Makefile', 'config.h', 'libtool']
        for file_name in files:
            copyfile(
                os.path.join(_TEMP_CONFIG_DIR, file_name),
                os.path.join(_SQLITE_SRC_DIR, file_name))
        file_name = os.path.join(_SQLITE_SRC_DIR, 'libtool')
        st = os.stat(file_name)
        os.chmod(file_name, st.st_mode | stat.S_IEXEC)


def make_aggregate(config_name):
    """Generate the aggregate source files."""
    if not os.path.exists(_TEMP_CONFIG_DIR):
        os.mkdir(_TEMP_CONFIG_DIR)
    try:
        os.chdir(_TEMP_CONFIG_DIR)
        _do_configure(config_name)

        # Chromium compiles 'sqlite3r.c' and 'sqlite3r.h' to use the built-in
        # corruption recovery module. These files are then mapped to the standard
        # 'sqlite3.c' and 'sqlite3.h' files below. This mapping is required if
        # the "SQLITE_HAVE_SQLITE3R" configuration option is specified.
        cmd = ['make', 'shell.c', 'sqlite3r.h', 'sqlite3r.c']
        subprocess.check_call(cmd)

        amalgamation_dir = get_amalgamation_dir(config_name)
        if not os.path.exists(amalgamation_dir):
            os.mkdir(amalgamation_dir)

        readme_dst = os.path.join(amalgamation_dir, 'README.md')
        if not os.path.exists(readme_dst):
            readme_src = os.path.join(_SQLITE_ROOT_DIR, 'scripts',
                                      'README_amalgamation.md')
            copyfile(readme_src, readme_dst)

        copyfile(os.path.join(_TEMP_CONFIG_DIR, 'sqlite3r.c'),
                 os.path.join(amalgamation_dir, 'sqlite3.c'))
        copyfile(os.path.join(_TEMP_CONFIG_DIR, 'sqlite3r.h'),
                 os.path.join(amalgamation_dir, 'sqlite3.h'))

        # shell.c must be placed in a different directory from sqlite3.h,
        # because it contains an '#include "sqlite3.h"' that we want to resolve
        # to our custom //third_party/sqlite/sqlite3.h, not to the sqlite3.h
        # produced here.
        shell_dir = os.path.join(amalgamation_dir, 'shell')
        if not os.path.exists(shell_dir):
            os.mkdir(shell_dir)
        copyfile(
            os.path.join(_TEMP_CONFIG_DIR, 'shell.c'),
            os.path.join(shell_dir, 'shell.c'))
    finally:
        rmtree(_TEMP_CONFIG_DIR)


def extract_sqlite_api(config_name):
    amalgamation_dir = get_amalgamation_dir(config_name)
    input_file = os.path.join(amalgamation_dir, 'sqlite3.h')
    output_file = os.path.join(amalgamation_dir, 'rename_exports.h')
    ProcessSourceFile(
        api_export_macro='SQLITE_API',
        symbol_prefix='chrome_',
        header_line=header_line,
        footer_line=footer_line,
        input_file=input_file,
        output_file=output_file)


if __name__ == '__main__':
    desc = \
    ('Create the SQLite amalgamation. The SQLite amalgamation is documented at '
     'https://www.sqlite.org/amalgamation.html and is a single large file '
     'containing the SQLite source code. Chromium generates the amalgamation with'
     ' this script to ensure that the configuration parameters are identical to '
     'those in the Ninja build file.')

    parser = argparse.ArgumentParser(description=desc)
    parser.add_argument(
        '-t',
        '--testing',
        action='store_true',
        help='Generate an amalgamation for testing (default: false)')
    namespace = parser.parse_args()
    if namespace.testing:
        _CONFIGURE_FOR_TESTING = True
        print('Running configure for testing.')

    for config_name in ['chromium', 'dev']:
        make_aggregate(config_name)
        extract_sqlite_api(config_name)