docs: 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)