splitconfig 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. #! /usr/bin/env python3
  2. """Extract configuration items into various configuration headers.
  3. This uses the configitems file, a database consisting of text lines with the
  4. following single-tab-separated fields:
  5. - Name of the configuration item, e.g. PQXX_HAVE_PTRDIFF_T.
  6. - Publication marker: public or internal.
  7. - A single environmental factor determining the item, e.g. libpq or compiler.
  8. """
  9. from __future__ import (
  10. absolute_import,
  11. print_function,
  12. unicode_literals,
  13. )
  14. from argparse import ArgumentParser
  15. import codecs
  16. from errno import ENOENT
  17. import os.path
  18. from os import getcwd
  19. import re
  20. from sys import (
  21. getdefaultencoding,
  22. getfilesystemencoding,
  23. stdout,
  24. )
  25. __metaclass__ = type
  26. def guess_fs_encoding():
  27. """Try to establish the filesystem encoding.
  28. It's a sad thing: some guesswork is involved. The encoding often seems to
  29. be conservatively, and incorrectly, set to ascii.
  30. """
  31. candidates = [
  32. getfilesystemencoding(),
  33. getdefaultencoding(),
  34. 'utf-8',
  35. ]
  36. for encoding in candidates:
  37. lower = encoding.lower()
  38. if lower != 'ascii' and lower != 'ansi_x3.4-1968':
  39. return encoding
  40. raise AssertionError("unreachable code reached.")
  41. def guess_output_encoding():
  42. """Return the encoding of standard output."""
  43. # Apparently builds in Docker containers may have None as an encoding.
  44. # Fall back to ASCII. If this ever happens in a non-ASCII path, well,
  45. # there may be a more difficult decision to be made. We'll burn that
  46. # bridge when we get to it, as they almost say.
  47. return stdout.encoding or 'ascii'
  48. def decode_path(path):
  49. """Decode a path element from bytes to unicode string."""
  50. return path.decode(guess_fs_encoding())
  51. def encode_path(path):
  52. """Encode a path element from unicode string to bytes."""
  53. # Nasty detail: unicode strings are stored as UTF-16. Which can contain
  54. # surrogate pairs. And those break in encoding, unless you use this
  55. # special error handler.
  56. return path.encode(guess_fs_encoding(), 'surrogateescape')
  57. def read_text_file(path, encoding='utf-8'):
  58. """Read text file, return as string, or `None` if file is not there."""
  59. assert isinstance(path, type(''))
  60. try:
  61. with codecs.open(encode_path(path), encoding=encoding) as stream:
  62. return stream.read()
  63. except IOError as error:
  64. if error.errno == ENOENT:
  65. return None
  66. else:
  67. raise
  68. def read_lines(path, encoding='utf-8'):
  69. """Read text file, return as list of lines."""
  70. assert isinstance(path, type(''))
  71. with codecs.open(encode_path(path), encoding=encoding) as stream:
  72. return list(stream)
  73. def read_configitems(filename):
  74. """Read the configuration-items database.
  75. :param filename: Path to the configitems file.
  76. :return: Sequence of text lines from configitems file.
  77. """
  78. return [line.split() for line in read_lines(filename)]
  79. def map_configitems(items):
  80. """Map each config item to publication/factor.
  81. :param items: Sequence of config items: (name, publication, factor).
  82. :return: Dict mapping each item name to a tuple (publication, factor).
  83. """
  84. return {
  85. item: (publication, factor)
  86. for item, publication, factor in items
  87. }
  88. def read_header(source_tree, filename):
  89. """Read the original config.h generated by autoconf.
  90. :param source_tree: Path to libpqxx source tree.
  91. :param filename: Path to the config.h file.
  92. :return: Sequence of text lines from config.h.
  93. """
  94. assert isinstance(source_tree, type(''))
  95. assert isinstance(filename, type(''))
  96. return read_lines(os.path.join(source_tree, filename))
  97. def extract_macro_name(config_line):
  98. """Extract a cpp macro name from a configuration line.
  99. :param config_line: Text line from config.h which may define a macro.
  100. :return: Name of macro defined in `config_line` if it is a `#define`
  101. statement, or None.
  102. """
  103. config_line = config_line.strip()
  104. match = re.match('\s*#\s*define\s+([^\s]+)', config_line)
  105. if match is None:
  106. return None
  107. else:
  108. return match.group(1)
  109. def extract_section(header_lines, items, publication, factor):
  110. """Extract config items for given publication/factor from header lines.
  111. :param header_lines: Sequence of header lines from config.h.
  112. :param items: Dict mapping macro names to (publication, factor).
  113. :param publication: Extract only macros for this publication tag.
  114. :param factor: Extract only macros for this environmental factor.
  115. :return: Sequence of `#define` lines from `header_lines` insofar they
  116. fall within the requested section.
  117. """
  118. return sorted(
  119. line.strip()
  120. for line in header_lines
  121. if items.get(extract_macro_name(line)) == (publication, factor)
  122. )
  123. def compose_header(lines, publication, factor):
  124. """Generate header text containing given lines."""
  125. intro = (
  126. "/* Automatically generated from config.h: %s/%s config. */"
  127. % (publication, factor)
  128. )
  129. return '\n'.join([intro, ''] + lines + [''])
  130. def generate_config(source_tree, header_lines, items, publication, factor):
  131. """Generate config file for a given section, if appropriate.
  132. Writes nothing if the configuration file ends up identical to one that's
  133. already there.
  134. :param source_tree: Location of the libpqxx source tree.
  135. :param header_lines: Sequence of header lines from config.h.
  136. :param items: Dict mapping macro names to (publication, factor).
  137. :param publication: Extract only macros for this publication tag.
  138. :param factor: Extract only macros for this environmental factor.
  139. """
  140. assert isinstance(source_tree, type(''))
  141. config_file = os.path.join(
  142. source_tree, 'include', 'pqxx',
  143. 'config-%s-%s.h' % (publication, factor))
  144. unicode_path = config_file.encode(guess_output_encoding(), 'replace')
  145. section = extract_section(header_lines, items, publication, factor)
  146. contents = compose_header(section, publication, factor)
  147. if read_text_file(config_file) == contents:
  148. print("Generating %s: no changes--skipping." % unicode_path)
  149. return
  150. print("Generating %s: %d item(s)." % (unicode_path, len(section)))
  151. path = encode_path(config_file)
  152. with codecs.open(path, 'wb', encoding='ascii') as header:
  153. header.write(contents)
  154. def parse_args():
  155. """Parse command-line arguments."""
  156. default_source_tree = os.path.dirname(
  157. os.path.dirname(os.path.normpath(os.path.abspath(__file__))))
  158. parser = ArgumentParser(description=__doc__)
  159. parser.add_argument(
  160. 'sourcetree', metavar='PATH', default=default_source_tree,
  161. help="Location of libpqxx source tree. Defaults to '%(default)s'.")
  162. return parser.parse_args()
  163. def check_args(args):
  164. """Validate command-line arguments."""
  165. if not os.path.isdir(args.sourcetree):
  166. raise Exception("Not a directory: '%s'." % args.sourcetree)
  167. def get_current_dir():
  168. cwd = getcwd()
  169. if isinstance(cwd, bytes):
  170. return decode_path(cwd)
  171. else:
  172. return cwd
  173. def main():
  174. """Main program entry point."""
  175. args = parse_args()
  176. check_args(args)
  177. # The configitems file is under revision control; it's in sourcetree.
  178. items = read_configitems(os.path.join(args.sourcetree, 'configitems'))
  179. publications = sorted(set(item[1] for item in items))
  180. factors = sorted(set(item[2] for item in items))
  181. # The config.h header is generated; it's in the build tree, which should
  182. # be where we are.
  183. directory = get_current_dir()
  184. original_header = read_header(
  185. directory,
  186. os.path.join('include', 'pqxx', 'config.h'))
  187. items_map = map_configitems(items)
  188. for publication in publications:
  189. for factor in factors:
  190. generate_config(
  191. directory, original_header, items_map, publication, factor)
  192. if __name__ == '__main__':
  193. main()