def build_module(
        name, source=None, *, sources=None, preprocess=None, output=None, output_dir='.',
        build_dir='build', include_dirs=None, library_dirs=None, libraries=None, macros=None,
        compiler_preargs=None, compiler_postargs=None, linker_preargs=None, linker_postargs=None, cache=True):

    '''
        Args:
            name (str): The module name (must be unique).
            source (str): the source code in C++.

        Keyword Args:
            sources (str): Source files.
            preprocess (str): Source files that need cfly's preprocessing.
            build_dir (str): The build directory. defaults to 'build'.
            output (str): The output file. defaults to '{name}.{ext}'.
            output_dir (str): The output directory. defaults to '.'.
            build_dir (str): The build directory. defaults to 'build'.
            include_dirs (list): Additional include directories.
            library_dirs (list): Additional library directories.
            libraries (list): Additional libraries.
            macros (list): Predefined macros. (name, value) pairs.
            compiler_preargs (list): Compiler preargs.
            compiler_postargs (list): Compiler postargs.
            linker_preargs (list): Linker preargs.
            linker_postargs (list): Linker postargs.
            cache (bool): Enable cache.

        Returns:
            the compiled and imported module.
    '''

    if output is None:
        output = name + get_config_var('EXT_SUFFIX')

    if sources is None:
        sources = []

    if preprocess is None:
        preprocess = []

    if source is None:
        source = ''

    preprocess_set = set(preprocess)
    sources = [x for x in sources if x not in preprocess_set]

    if source and sources + preprocess:
        raise ValueError('invalid arguments')

    os.makedirs(build_dir, exist_ok=True)

    module_home = os.path.join(build_dir, 'temp', name)
    old_checksum = readall(module_home, 'args.txt')
    checksum = args_checksum(
        name,
        source,
        sources,
        preprocess,
        output,
        output_dir,
        build_dir,
        include_dirs,
        library_dirs,
        libraries,
        macros,
        compiler_preargs,
        compiler_postargs,
        linker_preargs,
        linker_postargs,
    )

    if checksum != old_checksum:
        cache = False

    output = os.path.join(output_dir, output)

    if cache and is_up_to_date(output, sources + preprocess):
        return load_module(name, output)

    with open(os.path.join(build_dir, name + '.log'), 'wb+') as build_log:
        shutil.rmtree(module_home, ignore_errors=True)
        os.makedirs(module_home, exist_ok=True)

        if source:
            preprocess = [writeall(module_home, 'source.cpp', source)]

        global_module_methods = {}
        global_module_types = {}

        sources = [(x, None) for x in sources]

        for filename in preprocess:
            source = readall('.', filename)
            module_methods, module_types = parse_source(source, build_log)
            global_module_methods.update(module_methods)
            global_module_types.update(module_types)
            code = render_template(source_template, module=name, types=module_types, methods=module_methods)
            sources.append((writeall(module_home, filename, source + code), filename))

        code = render_template(module_template, module=name, types=global_module_types, methods=global_module_methods)
        sources.append((writeall(module_home, 'module.cpp', code), None))
        exports = ['PyInit_' + name]

        compiler = create_compiler()

        def spawn(cmd):
            old_path = os.getenv('path', '')
            try:
                os.environ['path'] = getattr(compiler, '_paths', old_path)
                ret = subprocess.call(cmd, stdout=build_log, stderr=subprocess.STDOUT)
                if ret:
                    build_log.seek(0)
                    entire_log = build_log.read().decode()
                    raise DistutilsExecError('Compiler failed:\n' + entire_log)

            finally:
                build_log.flush()
                os.environ['path'] = old_path

        compiler.spawn = spawn

        for include_dir in include_dirs or []:
            compiler.add_include_dir(include_dir)

        for library_dir in library_dirs or []:
            compiler.add_library_dir(library_dir)

        try:
            objects = compiler.object_filenames([source for source, original in sources], 0, build_dir)

            todo = []
            for pair, obj in zip(sources, objects):
                source, original = pair
                if not is_up_to_date(obj, [original or source]):
                    todo.append(pair)

            if not cache:
                todo = sources

            if todo:
                for source, original in todo:
                    original_folder = [os.path.abspath(os.path.dirname(original))] if original else []
                    compiler.compile(
                        [source],
                        build_dir,
                        macros,
                        original_folder,
                        0,
                        compiler_preargs,
                        compiler_postargs,
                    )

                compiler.link(
                    'shared_object',
                    objects,
                    'output',
                    build_dir,
                    libraries,
                    [],
                    [],
                    exports,
                    0,
                    linker_preargs,
                    linker_postargs,
                )

                if os.path.isfile(output):
                    try:
                        os.unlink(output)
                    except PermissionError:
                        shutil.move(output, os.path.join(build_dir, '_' + os.urandom(8).hex()))
                shutil.move(os.path.join(build_dir, 'output'), output)

        except CompileError as ex:
            raise ex from None

        writeall(module_home, 'args.txt', checksum)

    return load_module(name, output)