From: Dmitry Selyutin Date: Fri, 8 Sep 2017 20:35:13 +0000 (+0300) Subject: [python] python gnulib API development branch X-Git-Url: https://gitweb.git.savannah.gnu.org/gitweb/?a=commitdiff_plain;h=f4274dd93418c8fd0b20fe75c66fe55312aece60;p=gnulib.git [python] python gnulib API development branch --- diff --git a/pygnulib/config.py b/pygnulib/config.py new file mode 100644 index 0000000000..ae84b9820d --- /dev/null +++ b/pygnulib/config.py @@ -0,0 +1,1170 @@ +#!/usr/bin/python +# encoding: UTF-8 + + +import argparse +import codecs +import os +import re + +from .error import AutoconfVersionError + + +class Config: + """gnulib generic configuration""" + _TABLE_ = { + "root" : "", + "local_dir" : "", + "source_base" : "lib", + "m4_base" : "m4", + "po_base" : "po", + "doc_base" : "doc", + "tests_base" : "tests", + "auxdir" : "", + "lib" : "libgnu", + "makefile_name" : "Makefile.am", + "macro_prefix" : "gl", + "po_domain" : "", + "witness_c_macro" : "", + "lgpl" : 0, + "tests" : False, + "obsolete" : False, + "cxx_tests" : False, + "longrunning_tests" : False, + "privileged_tests" : False, + "unportable_tests" : False, + "all_tests" : False, + "libtool" : False, + "conddeps" : False, + "vc_files" : False, + "autoconf" : 2.59, + "modules" : [], + "avoid" : [], + "files" : [], + } + + + def __init__(self, **kwargs): + self._table_ = dict() + for key in Config._TABLE_: + self._table_[key] = Config._TABLE_[key] + for key, value in kwargs.items(): + self[key] = value + + + def __repr__(self): + return repr(self._table_) + + + def __iter__(self): + for key in Config._TABLE_: + value = self[key] + yield key, value + + + @property + def source_base(self): + """directory relative to ROOT where source code is placed; defaults to 'lib'""" + return self["source_base"] + + @source_base.setter + def source_base(self, value): + self["source_base"] = value + + + @property + def m4_base(self): + """directory relative to ROOT where *.m4 macros are placed; defaults to 'm4'""" + return self["m4_base"] + + @m4_base.setter + def m4_base(self, value): + self["m4_base"] = value + + + @property + def po_base(self): + """directory relative to ROOT where *.po files are placed; defaults to 'po'""" + return self["po_base"] + + @po_base.setter + def po_base(self, value): + self["po_base"] = value + + + @property + def doc_base(self): + """directory relative to ROOT where doc files are placed; defaults to 'doc'""" + return self["doc_base"] + + @doc_base.setter + def doc_base(self, value): + self["doc_base"] = value + + + @property + def tests_base(self): + """directory relative to ROOT where unit tests are placed; defaults to 'tests'""" + return self["tests_base"] + + @tests_base.setter + def tests_base(self, value): + self["tests_base"] = value + + + @property + def auxdir(self): + """directory relative to ROOT where auxiliary build tools are placed""" + return self["auxdir"] + + @auxdir.setter + def auxdir(self, value): + self["auxdir"] = value + + + @property + def lib(self): + """library name; defaults to 'libgnu'""" + return self["lib"] + + @lib.setter + def lib(self, value): + self["lib"] = value + + + @property + def makefile_name(self): + """name of makefile in automake syntax in the source-base and tests-base directories""" + return self["makefile_name"] + + @makefile_name.setter + def makefile_name(self, value): + self["makefile_name"] = value + + + @property + def macro_prefix(self): + """ + the prefix of the macros 'gl_EARLY' and 'gl_INIT' (default is 'gl'); + the change of this parameter also affects include_guard_prefix parameter + """ + return self["macro_prefix"] + + @macro_prefix.setter + def macro_prefix(self, value): + self["macro_prefix"] = value + + + @property + def po_domain(self): + """the prefix of the i18n domain""" + return self["po_domain"] + + @po_domain.setter + def po_domain(self, value): + self["po_domain"] = value + + + @property + def witness_c_macro(self): + """the C macro that is defined when the sources are compiled or used""" + return self["witness_c_macro"] + + @witness_c_macro.setter + def witness_c_macro(self, value): + self["witness_c_macro"] = value + + + @property + def lgpl(self): + """abort if modules aren't available under the LGPL; also modify license template""" + return self["lgpl"] + + @lgpl.setter + def lgpl(self, value): + if value is None: + value = 0 + if value not in [0, 2, 3]: + raise TypeError("lgpl option must be either None or integral version (2 or 3)") + self["lgpl"] = value + + + @property + def tests(self): + """include unit tests for the included modules""" + return self["tests"] + + @tests.setter + def tests(self, value): + self["tests"] = value + + + @property + def obsolete(self): + """include obsolete modules when they occur among the modules""" + return self["obsolete"] + + @obsolete.setter + def obsolete(self, value): + self["obsolete"] = value + + + @property + def cxx_tests(self): + """include even unit tests for C++ interoperability""" + return self["cxx_tests"] + + @cxx_tests.setter + def cxx_tests(self, value): + self["cxx_tests"] = value + + + @property + def longrunning_tests(self): + """include even unit tests that are long-runners""" + return self["longrunning_tests"] + + @longrunning_tests.setter + def longrunning_tests(self, value): + self["longrunning_tests"] = value + + + @property + def privileged_tests(self): + """include even unit tests that require root privileges""" + return self["privileged_tests"] + + @privileged_tests.setter + def privileged_tests(self, value): + self["privileged_tests"] = value + + + @property + def unportable_tests(self): + """include even unit tests that fail on some platforms""" + return self["unportable_tests"] + + @unportable_tests.setter + def unportable_tests(self, value): + self["unportable_tests"] = value + + + @property + def all_tests(self): + """include all kinds of problematic unit tests""" + result = True + result &= self.tests + result &= self.cxx_tests + result &= self.privileged_tests + result &= self.unportable_tests + result &= self.longrunning_tests + return result + + @all_tests.setter + def all_tests(self, value): + self.tests = value + self.cxx_tests = value + self.privileged_tests = value + self.unportable_tests = value + self.longrunning_tests = value + + + @property + def libtool(self): + """use libtool rules""" + return self["libtool"] + + @libtool.setter + def libtool(self, value): + self["libtool"] = value + + + @property + def conddeps(self): + """support conditional dependencies (may save configure time and object code)""" + return self["conddeps"] + + @conddeps.setter + def conddeps(self, value): + self["conddeps"] = value + + + @property + def vc_files(self): + """update version control related files""" + return self["vc_files"] + + @vc_files.setter + def vc_files(self, value): + self["vc_files"] = value + + + @property + def autoconf(self): + """autoconf version""" + return self["autoconf"] + + @autoconf.setter + def autoconf(self, value): + self["autoconf"] = value + + + @property + def modules(self): + """list of modules""" + return self["modules"] + + @modules.setter + def modules(self, value): + self["modules"] = tuple(value) + + + @property + def avoid(self): + """list of modules to avoid""" + return self["avoid"] + + @avoid.setter + def avoid(self, value): + self["avoid"] = tuple(value) + + + @property + def files(self): + """list of files to be processed""" + return self["files"] + + @files.setter + def files(self, value): + self["files"] = list(value) + + + @property + def include_guard_prefix(self): + """include guard prefix""" + prefix = self["macro_prefix"].upper() + default = Config._TABLE_["macro_prefix"] + return "GL_%s" % prefix if prefix == default else "GL" + + + def __getitem__(self, key): + if key not in Config._TABLE_: + key = key.replace("-", "_") + if key not in Config._TABLE_: + raise KeyError("unsupported option: %r" % key) + return self._table_[key] + + + def __setitem__(self, key, value): + if key not in Config._TABLE_: + key = key.replace("_", "-") + if key not in Config._TABLE_: + raise KeyError("unsupported option: %r" % key) + key = key.replace("-", "_") + typeid = type(Config._TABLE_[key]) + if key == "lgpl": + if value not in [None, 2, 3]: + raise TypeError("lgpl option must be either None or integral version (2 or 3)") + elif key == "autoconf": + if value < 2.59: + raise AutoconfVersionError(2.59) + elif not isinstance(value, typeid): + raise TypeError("%r option must be of %r type" % (key, typeid)) + self._table_[key] = value + + + def items(self): + """a set-like object providing a view on configuration items""" + return self._table_.items() + + + def keys(self): + """a set-like object providing a view on configuration keys""" + return self._table_.keys() + + + def values(self): + """a set-like object providing a view on configuration values""" + return self._table_.values() + + +class Cache(Config): + """gnulib cached configuration""" + _AUTOCONF_ = { + "autoconf" : re.compile(".*AC_PREREQ\\(\\[(.*?)\\]\\)", re.S | re.M), + "auxdir" : re.compile("^AC_CONFIG_AUX_DIR\\(\\[(.*?)\\]\\)$", re.S | re.M), + "libtool" : re.compile("A[CM]_PROG_LIBTOOL", re.S | re.M) + } + _GNULIB_CACHE_ = { + "libtool" : (bool, "gl_LIBTOOL"), + "conddeps" : (bool, "gl_CONDITIONAL_DEPENDENCIES"), + "vc_files" : (bool, "gl_VC_FILES"), + "tests" : (bool, "gl_WITH_TESTS"), + "obsolete" : (bool, "gl_WITH_OBSOLETE"), + "cxx_tests" : (bool, "gl_WITH_CXX_TESTS"), + "longrunning_tests" : (bool, "gl_WITH_LONGRUNNING_TESTS"), + "privileged_tests" : (bool, "gl_WITH_PRIVILEGED_TESTS"), + "unportable_tests" : (bool, "gl_WITH_UNPORTABLE_TESTS"), + "all_tests" : (bool, "gl_WITH_ALL_TESTS"), + "local_dir" : (str, "gl_LOCAL_DIR"), + "source_base" : (str, "gl_SOURCE_BASE"), + "m4_base" : (str, "gl_M4_BASE"), + "po_base" : (str, "gl_PO_BASE"), + "doc_base" : (str, "gl_DOC_BASE"), + "tests_base" : (str, "gl_TESTS_BASE"), + "makefile_name" : (str, "gl_MAKEFILE_NAME"), + "macro_prefix" : (str, "gl_MACRO_PREFIX"), + "po_domain" : (str, "gl_PO_DOMAIN"), + "witness_c_macro" : (str, "gl_WITNESS_C_MACRO"), + "lib" : (str, "gl_LIB"), + "modules" : (list, "gl_MODULES"), + "avoid" : (list, "gl_AVOID"), + "lgpl" : (str, "gl_LGPL"), + } + _GNULIB_CACHE_BOOL_ = [] + _GNULIB_CACHE_STR_ = [] + _GNULIB_CACHE_LIST_ = [] + for _key_, (_typeid_, _) in _GNULIB_CACHE_.items(): + if _typeid_ is bool: + _GNULIB_CACHE_BOOL_ += [_key_] + elif _typeid_ is str: + _GNULIB_CACHE_STR_ += [_key_] + else: + _GNULIB_CACHE_LIST_ += [_key_] + _GNULIB_CACHE_PATTERN_ = re.compile("^(gl_.*?)\\(\\[(.*?)\\]\\)$", re.S | re.M) + + + def __init__(self, root, autoconf=None, **kwargs): + if not isinstance(root, str): + raise TypeError("root must be of 'str' type") + super().__init__(**kwargs) + self._autoconf_(root, autoconf) + self._gnulib_cache_(root) + self._gnulib_comp_(root) + + def _autoconf_(self, root, autoconf): + if not autoconf: + autoconf = os.path.join(root, "configure.ac") + if not os.path.exists(autoconf): + autoconf = os.path.join(root, "configure.in") + if not os.path.isabs(autoconf): + autoconf = os.path.join(root, autoconf) + autoconf = os.path.normpath(autoconf) + with codecs.open(autoconf, "rb", "UTF-8") as stream: + data = stream.read() + for key, pattern in Cache._AUTOCONF_.items(): + match = pattern.findall(data) + if not match: + continue + if key == "autoconf": + self["autoconf"] = sorted(set([float(_.strip()) for _ in match if _.strip()]))[-1] + else: + self[key] = match[-1] + + def _gnulib_cache_(self, root): + m4base = self["m4-base"] + gnulib_cache = os.path.join(root, m4base, "gnulib-cache.m4") + if os.path.exists(gnulib_cache): + with codecs.open(gnulib_cache, "rb", "UTF-8") as stream: + data = stream.read() + for key in Cache._GNULIB_CACHE_BOOL_: + (_, macro) = Cache._GNULIB_CACHE_[key] + if key in data: + self[key] = True + match = dict(Cache._GNULIB_CACHE_PATTERN_.findall(data)) + for key in Cache._GNULIB_CACHE_STR_: + (_, macro) = Cache._GNULIB_CACHE_[key] + if macro in match: + self[key] = match[macro].strip() + for key in Cache._GNULIB_CACHE_LIST_: + (_, macro) = Cache._GNULIB_CACHE_[key] + if macro in match: + self[key] = [_.strip() for _ in match[macro].split("\n") if _.strip()] + + def _gnulib_comp_(self, root): + m4base = self["m4-base"] + gnulib_comp = os.path.join(root, m4base, "gnulib-comp.m4") + if os.path.exists(gnulib_comp): + with codecs.open(gnulib_comp, "rb", "UTF-8") as stream: + data = stream.read() + regex = "AC_DEFUN\\(\\[%s_FILE_LIST\\], \\[(.*?)\\]\\)" % self["macro-prefix"] + pattern = re.compile(regex, re.S | re.M) + match = pattern.findall(data) + if match: + self.files = [_.strip() for _ in match[-1].split("\n") if _.strip()] + + + +class CommandLine(Config): + """gnulib-tool command line configuration""" + _LIST_ = (1 << 0) + _FIND_ = (1 << 1) + _IMPORT_ = (1 << 2) + _ADD_IMPORT_ = (1 << 3) + _REMOVE_IMPORT_ = (1 << 4) + _UPDATE_ = (1 << 5) + _TEST_DIRECTORY_ = (1 << 6) + _MEGA_TEST_DIRECTORY_ = (1 << 7) + _TEST_ = (1 << 8) + _MEGA_TEST_ = (1 << 9) + _COPY_FILE_ = (1 << 10) + _EXTRACT_DESCRIPTION_ = (1 << 11) + _EXTRACT_COMMENT_ = (1 << 12) + _EXTRACT_STATUS_ = (1 << 13) + _EXTRACT_NOTICE_ = (1 << 14) + _EXTRACT_APPLICABILITY_ = (1 << 15) + _EXTRACT_FILELIST_ = (1 << 16) + _EXTRACT_DEPENDENCIES_ = (1 << 17) + _EXTRACT_AUTOCONF_SNIPPET_ = (1 << 18) + _EXTRACT_AUTOMAKE_SNIPPET_ = (1 << 19) + _EXTRACT_INCLUDE_DIRECTIVE_ = (1 << 20) + _EXTRACT_LINK_DIRECTIVE_ = (1 << 21) + _EXTRACT_LICENSE_ = (1 << 22) + _EXTRACT_MAINTAINER_ = (1 << 23) + _EXTRACT_TESTS_MODULE_ = (1 << 24) + _ANY_IMPORT_ = _IMPORT_ | _ADD_IMPORT_ | _REMOVE_IMPORT_ | _UPDATE_ + _ANY_TEST_ = _TEST_ | _MEGA_TEST_ | _TEST_DIRECTORY_ | _MEGA_TEST_DIRECTORY_ + _ALL_ = _LIST_ | _FIND_ | _ANY_IMPORT_ | _ANY_TEST_ + _MODES_ = ( + (_LIST_, "list", ""), + (_FIND_, "find", "filename"), + (_IMPORT_, "import", "[module1 ... moduleN]"), + (_ADD_IMPORT_, "add-import", "[module1 ... moduleN]"), + (_REMOVE_IMPORT_, "remove-import", "[module1 ... moduleN]"), + (_UPDATE_, "update", ""), + (_TEST_DIRECTORY_, "testdir", "--dir=directory [module1 ... moduleN]"), + (_MEGA_TEST_DIRECTORY_, "megatestdir", "--dir=directory [module1 ... moduleN]"), + (_TEST_, "test", "--dir=directory [module1 ... moduleN]"), + (_MEGA_TEST_, "megatest", "--dir=directory [module1 ... moduleN]"), + (_EXTRACT_DESCRIPTION_, "extract-description", "module"), + (_EXTRACT_COMMENT_, "extract-comment", "module"), + (_EXTRACT_STATUS_, "extract-status", "module"), + (_EXTRACT_NOTICE_, "extract-notice", "module"), + (_EXTRACT_APPLICABILITY_, "extract-applicability", "module"), + (_EXTRACT_FILELIST_, "extract-filelist", "module"), + (_EXTRACT_DEPENDENCIES_, "extract-dependencies", "module"), + (_EXTRACT_AUTOCONF_SNIPPET_, "extract-autoconf-snippet", "module"), + (_EXTRACT_AUTOMAKE_SNIPPET_, "extract-automake-snippet", "module"), + (_EXTRACT_INCLUDE_DIRECTIVE_, "extract-include-directive", "module"), + (_EXTRACT_LINK_DIRECTIVE_, "extract-link-directive", "module"), + (_EXTRACT_LICENSE_, "extract-license", "module"), + (_EXTRACT_MAINTAINER_, "extract-maintainer", "module"), + (_EXTRACT_TESTS_MODULE_, "extract-tests-module", "module"), + (_COPY_FILE_, "copy", "file [destination]"), + ) + + + class _AvoidAction_(argparse.Action): + def __call__(self, parser, namespace, value, option=None): + values = getattr(namespace, self.dest) + values += value + + + class _VerboseAction_(argparse.Action): + def __call__(self, parser, namespace, value, option=None): + value = getattr(namespace, self.dest) + verbose = option in ("-v", "--verbose") + value += +1 if verbose else -1 + setattr(namespace, self.dest, value) + + + # section0: (section0_modes, (([section0_option0, section0_optionN], **section0_kwargs))) + # sectionN: (sectionN_modes, (([sectionN_option0, sectionN_optionN], **sectionN_kwargs))) + # + # for (name, flags, arguments) in sections: + # for argument in arguments: + # (options, kwargs) = argument + _SECTIONS_ = ( + ( + "Operation modes", + None, + ( + (["-l", "--list"], { + "help": ( + "print the available module names", + ), + "dest": "mode", + "default": None, + "action": "store_const", + "const": _LIST_, + }), + (["-f", "--find"], { + "help": ( + "find the modules which contain the specified file", + ), + "dest": "mode", + "default": None, + "action": "store_const", + "const": _FIND_, + }), + (["-i", "--import"], { + "help": ( + "import the given modules into the current package", + ), + "dest": "mode", + "default": None, + "action": "store_const", + "const": _IMPORT_, + }), + (["-a", "--add-import"], { + "help": ( + "augment the list of imports from gnulib into the", + "current package, by adding the given modules;", + "if no modules are specified, update the current", + "package from the current gnulib", + ), + "dest": "mode", + "default": None, + "action": "store_const", + "const": _ADD_IMPORT_, + }), + (["-r", "--remove-import"], { + "help": ( + "reduce the list of imports from gnulib into the", + "current package, by removing the given modules", + ), + "dest": "mode", + "default": None, + "action": "store_const", + "const": _REMOVE_IMPORT_, + }), + (["-u", "--update"], { + "help": ( + "update the current package, restore files omitted", + "from version control", + ), + "dest": "mode", + "default": None, + "action": "store_const", + "const": _UPDATE_, + }), + + (["--extract-description"], { + "help": ( + "extract the description", + ), + "dest": "mode", + "default": None, + "action": "store_const", + "const": _EXTRACT_DESCRIPTION_, + }), + (["--extract-comment"], { + "help": ( + "extract the comment", + ), + "dest": "mode", + "default": None, + "action": "store_const", + "const": _EXTRACT_COMMENT_, + }), + (["--extract-status"], { + "help": ( + "extract the status (obsolete etc.)", + ), + "dest": "mode", + "default": None, + "action": "store_const", + "const": _EXTRACT_STATUS_, + }), + (["--extract-notice"], { + "help": ( + "extract the notice or banner", + ), + "dest": "mode", + "default": None, + "action": "store_const", + "const": _EXTRACT_NOTICE_, + }), + (["--extract-applicability"], { + "help": ( + "extract the applicability", + ), + "dest": "mode", + "default": None, + "action": "store_const", + "const": _EXTRACT_APPLICABILITY_, + }), + (["--extract-filelist"], { + "help": ( + "extract the list of files", + ), + "dest": "mode", + "default": None, + "action": "store_const", + "const": _EXTRACT_FILELIST_, + }), + (["--extract-dependencies"], { + "help": ( + "extract the dependencies", + ), + "dest": "mode", + "default": None, + "action": "store_const", + "const": _EXTRACT_DEPENDENCIES_, + }), + (["--extract-autoconf-snippet"], { + "help": ( + "extract the snippet for configure.ac", + ), + "dest": "mode", + "default": None, + "action": "store_const", + "const": _EXTRACT_AUTOCONF_SNIPPET_, + }), + (["--extract-automake-snippet"], { + "help": ( + "extract the snippet for library makefile", + ), + "dest": "mode", + "default": None, + "action": "store_const", + "const": _EXTRACT_AUTOMAKE_SNIPPET_, + }), + (["--extract-include-directive"], { + "help": ( + "extract the #include directive", + ), + "dest": "mode", + "default": None, + "action": "store_const", + "const": _EXTRACT_INCLUDE_DIRECTIVE_, + }), + (["--extract-link-directive"], { + "help": ( + "extract the linker directive", + ), + "dest": "mode", + "default": None, + "action": "store_const", + "const": _EXTRACT_LINK_DIRECTIVE_, + }), + (["--extract-license"], { + "help": ( + "report the license terms of the source files", + "under lib/", + ), + "dest": "mode", + "default": None, + "action": "store_const", + "const": _EXTRACT_LICENSE_, + }), + (["--extract-maintainer"], { + "help": ( + "report the maintainer(s) inside gnulib", + ), + "dest": "mode", + "default": None, + "action": "store_const", + "const": _EXTRACT_MAINTAINER_, + }), + (["--extract-tests-module"], { + "help": ( + "report the unit test module, if it exists", + ), + "dest": "mode", + "default": None, + "action": "store_const", + "const": _EXTRACT_TESTS_MODULE_, + }), + + (["--copy-file"], { + "help": ( + "copy a file that is not part of any module", + ), + "dest": "mode", + "default": None, + "action": "store_const", + "const": _COPY_FILE_, + }), + ), + ), + + + ( + "General options", + _ALL_, + ( + (["--dir"], { + "help": ( + "specify the target directory; on --import,", + "this specifies where your configure.ac", + "can be found", + ), + "dest": "root", + "default": ".", + "metavar": "DIRECTORY", + }), + (["--local-dir"], { + "help": ( + "pecify a local override directory where to look", + "up files before looking in gnulib's directory", + ), + "dest": "local_dir", + "default": ".", + "nargs": 1, + "metavar": "DIRECTORY", + }), + (["-v", "--verbose"], { + "help": ( + "increase verbosity; may be repeated", + ), + "dest": "verbosity", + "action": _VerboseAction_, + "default": 0, + "nargs": 0, + }), + (["-q", "--quiet"], { + "help": ( + "decrease verbosity; may be repeated", + ), + "dest": "verbosity", + "action": _VerboseAction_, + "default": 0, + "nargs": 0, + }), + ), + ), + + + ( + "Options for --import, --add/remove-import,\n" + " --create-[mega]testdir, --[mega]test", + (_ANY_IMPORT_ & ~_UPDATE_) | _ANY_TEST_, + ( + (["--with-tests"], { + "help": ( + "include unit tests for the included modules", + ), + "dest": "tests", + "action": "store_true", + "default": False, + }), + (["--with-obsolete"], { + "help": ( + "include obsolete modules when they occur among the", + "dependencies; by default, dependencies to obsolete", + "modules are ignored", + ), + "dest": "obsolete", + "action": "store_true", + "default": False, + }), + (["--with-c++-tests"], { + "help": ( + "include even unit tests for C++ interoperability", + ), + "dest": "cxx_tests", + "action": "store_true", + "default": False, + }), + (["--with-longrunning-tests"], { + "help": ( + "include even unit tests that are long-runners", + ), + "dest": "longrunning_tests", + "action": "store_true", + "default": False, + }), + (["--with-privileged-tests"], { + "help": ( + "include even unit tests that require root", + "privileges", + ), + "dest": "privileged_tests", + "action": "store_true", + "default": False, + }), + (["--with-unportable-tests"], { + "help": ( + "include even unit tests that fail on some platforms", + ), + "dest": "unportable_tests", + "action": "store_true", + "default": False, + }), + (["--with-all-tests"], { + "help": ( + "include all kinds of problematic unit tests", + ), + "dest": "all_tests", + "action": "store_true", + "default": False, + }), + (["--avoid"], { + "help": ( + "avoid including the given MODULE; useful if you", + "have code that provides equivalent functionality", + "this option can be repeated", + ), + "action": _AvoidAction_, + "nargs": 1, + "default": [], + "metavar": "MODULE", + }), + (["--conditional-dependencies"], { + "help": ( + "support conditional dependencies (may save configure", + "time and object code)", + ), + "action": "store_true", + "default": False, + "dest": "conddeps", + }), + (["--no-conditional-dependencies"], { + "help": ( + "don't use conditional dependencies", + ), + "action": "store_false", + "default": False, + "dest": "conddeps", + }), + (["--libtool"], { + "help": ( + "use libtool rules", + ), + "action": "store_true", + "default": False, + "dest": "libtool", + }), + (["--no-libtool"], { + "help": ( + "don't use libtool rules", + ), + "action": "store_false", + "default": False, + "dest": "libtool", + }), + ), + ), + + + ( + "Options for --import, --add/remove-import", + (_ANY_IMPORT_ & ~_UPDATE_), + ( + (["--lib"], { + "help": ( + "specify the library name; defaults to 'libgnu'", + ), + "default": "libgnu", + "metavar": "LIBRARY", + }), + (["--source-base"], { + "help": ( + "directory relative to --dir where source code is", + "placed (default \"lib\")", + ), + "default": "lib", + "metavar": "DIRECTORY", + }), + (["--m4-base"], { + "help": ( + "directory relative to --dir where *.m4 macros are", + "placed (default \"m4\")", + ), + "default": "m4", + "metavar": "DIRECTORY", + }), + (["--po-base"], { + "help": ( + "directory relative to --dir where *.po files are", + "placed (default \"po\")", + ), + "default": "po", + "metavar": "DIRECTORY", + }), + (["--doc-base"], { + "help": ( + "directory relative to --dir where doc files are", + "placed (default \"doc\")", + ), + "default": "doc", + "metavar": "DIRECTORY", + }), + (["--tests-base"], { + "help": ( + "directory relative to --dir where unit tests are", + "placed (default \"tests\")", + ), + "default": "tests", + "metavar": "DIRECTORY", + }), + (["--aux-dir"], { + "help": ( + "directory relative to --dir where auxiliary build", + "tools are placed (default comes from configure.ac);", + ), + "dest": "auxdir", + "default": "", + "metavar": "DIRECTORY", + }), + (["--lgpl"], { + "help": ( + "abort if modules aren't available under the LGPL;", + "also modify license template from GPL to LGPL;", + "the version number of the LGPL can be specified;", + "the default is currently LGPLv3.", + ), + "choices": (2, 3), + "type": int, + "metavar": "[=2|=3]", + }), + (["--makefile-name"], { + "help": ( + "name of makefile in automake syntax in the", + "source-base and tests-base directories", + "(default \"Makefile.am\")", + ), + "default": "Makefile.am", + "metavar": "NAME", + }), + (["--macro-prefix"], { + "help": ( + "specify the prefix of the macros 'gl_EARLY' and", + "'gl_INIT'; default is 'gl'", + ), + "default": "gl", + "metavar": "PREFIX", + }), + (["--po-domain"], { + "help": ( + "specify the prefix of the i18n domain; usually use", + "the package name; a suffix '-gnulib' is appended", + ), + "default": "", + "metavar": "NAME", + }), + (["--witness-c-macro"], { + "help": ( + "specify the C macro that is defined when the", + "sources in this directory are compiled or used", + ), + "default": "", + "metavar": "NAME", + }), + (["--vc-files"], { + "help": ( + "update version control related files", + "(.gitignore and/or .cvsignore)", + ), + "action": "store_true", + "default": False, + "dest": "vc_files", + }), + (["--no-vc-files"], { + "help": ( + "don't update version control related files", + "(.gitignore and/or .cvsignore)", + ), + "action": "store_false", + "default": False, + "dest": "libtool", + }), + (["--no-changelog"], { + "help": ( + "don't update or create ChangeLog files;", + "this option is currently deprecated", + ), + "default": None, + "action": "store_const", + "const": None, + }), + ), + ), + ) + + + def _usage_(self): + iterable = iter(CommandLine._MODES_) + (_, cmd, args) = next(iterable) + fmt = (" --{cmd}" + (" {args}" if args else "")) + lines = ["usage: {program}" + fmt.format(cmd=cmd, args=args)] + for (_, cmd, args) in iterable: + fmt = (" --{cmd}" + (" {args}" if args else "")) + lines += [" {program}" + fmt.format(cmd=cmd, args=args)] + lines += ["", ""] + return "\n".join(lines).format(program=self._program_) + + + def _help_(self): + lines = [""] + for (name, _, args) in CommandLine._SECTIONS_: + offset = -1 + lines += ["", "%s:" % name, ""] + for arg in args: + (options, kwargs) = arg + options = ", ".join(options) + if "metavar" in kwargs: + options += (" " + kwargs["metavar"]) + length = len(options) + if length > offset: + offset = length + fmt1 = " %-{0}s %s".format(offset) + fmt2 = " " + " " * offset + " %s" + for arg in args: + (options, kwargs) = arg + options = ", ".join(options) + if "metavar" in kwargs: + sep = "" if "choices" in kwargs else "=" + options += ("%s%s" % (sep, kwargs["metavar"])) + description = iter(kwargs["help"]) + line = next(description) + lines += [fmt1 % (options, line)] + for line in description: + lines += [fmt2 % line] + lines += [""] + return self._usage_()[:-1] + "\n".join(lines) + + + def __init__(self, program, argv, **kwargs): + super().__init__(**kwargs) + + exclude = {} + parser = argparse.ArgumentParser(prog=program, allow_abbrev=False) + modes = parser.add_mutually_exclusive_group() + for (options, kwargs) in CommandLine._SECTIONS_[0][2]: + for (flag, cmd, _) in CommandLine._MODES_: + if "--%s" % cmd not in options: + continue + exclude[flag] = [options, []] + modes.add_argument(*options, **kwargs) + for (_, flags, xargs) in CommandLine._SECTIONS_[1:]: + if not flags & flag: + for xarg in xargs: + (options, kwargs) = xarg + exclude[flag][1] += options + break + + for (_, _, args) in CommandLine._SECTIONS_[1:]: + for arg in args: + (options, kwargs) = arg + parser.add_argument(*options, **kwargs) + parser.add_argument("modules", nargs="+", metavar="module1 ... moduleN") + + self._program_ = os.path.basename(program) + parser.format_usage = self._usage_ + parser.format_help = self._help_ + + namespace = vars(parser.parse_args(argv)) + self._mode_ = namespace.pop("mode") + if self._mode_ is None: + parser.error("no operating mode selected") + self._verbosity_ = namespace.pop("verbosity") + + (mode, conflict) = exclude[self._mode_] + conflict = set(conflict).intersection(argv) + if conflict: + conflict = list(conflict)[0] + fmt = "argument {0}: not allowed with argument {1}" + parser.error(fmt.format(conflict, "/".join(mode))) + _ = namespace.pop("no_changelog", None) + for (key, value) in namespace.items(): + self[key] = value + + + @property + def mode(self): + for (flag, cmd, _) in CommandLine._MODES_: + if flag == self._mode_: + return cmd + return "" diff --git a/pygnulib/error.py b/pygnulib/error.py new file mode 100644 index 0000000000..d618df0ee2 --- /dev/null +++ b/pygnulib/error.py @@ -0,0 +1,54 @@ +#!/usr/bin/python +# encoding: UTF-8 + + + +class AutoconfVersionError(Exception): + """minimum supported autoconf version mismatch""" + def __init__(self, version): + fmt = "minimum supported autoconf version is %f" + message = fmt % version + super().__init__(message) + + + +class M4BaseMismatchError(Exception): + """ is expected to contain gl_M4_BASE([m4base])""" + def __init__(self, m4_base): + fmt = " is expected to contain gl_M4_BASE([%s])" + message = fmt % m4_base + super().__init__(message) + + + +class ConditionalDependenciesUnavailableError(Exception): + """conditional dependencies are not supported with tests""" + def __init__(self): + message = "conditional dependencies are not supported with tests" + super().__init__(message) + + + +class IncompatibleLicenseError(Exception): + """incompatible licenses on modules""" + def __init__(self, modules): + fmt = "incompatible licenses on modules: %r" + message = fmt % modules + super().__init__(message) + + + +class EmptyFileListError(Exception): + """cannot process empty file list""" + def __init__(self): + message = "cannot process empty file list" + super().__init__(message) + + + +class UnknownLicenseError(Exception): + """module lacks a license""" + def __init__(self, module): + fmt = "module lacks a license: %r" + message = fmt % module + super().__init__(message) diff --git a/pygnulib/filesystem.py b/pygnulib/filesystem.py new file mode 100644 index 0000000000..3d5721ff30 --- /dev/null +++ b/pygnulib/filesystem.py @@ -0,0 +1,114 @@ +#!/usr/bin/python +# encoding: UTF-8 + + + +import os + +from .config import Config +from .module import Module +from .module import FileModule + + + +class Directory: + """gnulib generic virtual file system""" + _SUBST_ = { + "build-aux" : "aux-dir", + "doc" : "doc-base", + "lib" : "source-base", + "m4" : "m4-base", + "tests" : "tests-base", + "tests=lib" : "tests-base", + "po" : "po-base", + } + + + def __init__(self, root, config): + if not isinstance(root, str): + raise TypeError("root must be of 'str' type") + if not isinstance(config, Config): + raise TypeError("config must be of 'Config' type") + if not os.path.exists(root): + raise FileNotFoundError(root) + if not os.path.isdir(root): + raise NotADirectoryError(root) + self._config_ = config + self._root_ = os.path.realpath(root) + + + def __getitem__(self, name): + """retrieve the canonical path of the specified file name""" + parts = [] + replaced = False + if not isinstance(name, str): + raise TypeError("name must be of 'str' type") + path = os.path.normpath(name) + if os.path.isabs(path): + raise ValueError("name must be a relative path") + for part in path.split(os.path.sep): + if part == "..": + parts += [part] + continue + if not replaced: + for old, new in Directory._SUBST_.items(): + if part == old: + part = self._config_[new] + replaced = True + parts += [part] + path = os.path.sep.join([self._root_] + parts) + if not os.path.exists(path): + raise FileNotFoundError(name) + return path + + + +class Git(Directory): + """gnulib Git-based virtual file system""" + _EXCLUDE_ = { + "." : str.startswith, + "~" : str.endswith, + "-tests" : str.endswith, + "CVS" : str.startswith, + "ChangeLog" : str.__eq__, + "COPYING" : str.__eq__, + "README" : str.__eq__, + "TEMPLATE" : str.__eq__, + "TEMPLATE-TESTS" : str.__eq__, + "TEMPLATE-EXTENDED" : str.__eq__, + } + + + def __init__(self, root, config): + if not os.path.isdir(root): + raise FileNotFoundError(root) + super().__init__(root, config) + if not os.path.isdir(os.path.join(self._root_, ".git")): + raise TypeError("%r is not a gnulib repository") + + + def module(self, name, full=True): + """instantiate gnulib module by its name""" + if name in Git._EXCLUDE_: + raise ValueError("illegal module name") + path = os.path.join(self["modules"], name) + return FileModule(path, name=name) if full else Module(name) + + + def modules(self, full=True): + """iterate over all available modules""" + prefix = self["modules"] + for root, _, files in os.walk(prefix): + names = [] + for name in files: + exclude = False + for key, method in Git._EXCLUDE_.items(): + if method(name, key): + exclude = True + break + if not exclude: + names += [name] + for name in names: + path = os.path.join(root, name) + name = path[len(prefix) + 1:] + yield self.module(name, full) diff --git a/pygnulib/generator.py b/pygnulib/generator.py new file mode 100644 index 0000000000..fef2b49e46 --- /dev/null +++ b/pygnulib/generator.py @@ -0,0 +1,442 @@ +#!/usr/bin/python +# encoding: UTF-8 + + + +import os + +from .config import Config +from .module import Module + + + +class Generator: + """gnulib file content generator""" + _TEMPLATE_ = ( + "## DO NOT EDIT! GENERATED AUTOMATICALLY!", + "#", + "# This file is free software; you can redistribute it and/or modify", + "# it under the terms of the GNU General Public License as published by", + "# the Free Software Foundation; either version 3 of the License, or", + "# (at your option) any later version.", + "#", + "# This file is distributed in the hope that it will be useful,", + "# but WITHOUT ANY WARRANTY; without even the implied warranty of", + "# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the", + "# GNU General Public License for more details.", + "#", + "# You should have received a copy of the GNU General Public License", + "# along with this file. If not, see .", + "#", + "# As a special exception to the GNU General Public License,", + "# this file may be distributed as part of a program that", + "# contains a configuration script generated by Autoconf, under", + "# the same distribution terms as the rest of that program.", + "#", + "# Generated by gnulib-tool.", + ) + + def __repr__(self): + return "pygnulib.generator.Generator" + + def __str__(self): + return "\n".join([_ for _ in self]) + + def __iter__(self): + for line in Generator._TEMPLATE_: + yield line + + + +class POMakefile(Generator): + """PO Makefile parameterization""" + _TEMPLATE_ = ( + "# These options get passed to xgettext.", + "XGETTEXT_OPTIONS = \\", + " --keyword=_ --flag=_:1:pass-c-format \\", + " --keyword=N_ --flag=N_:1:pass-c-format \\", + " --keyword='proper_name:1,\"This is a proper name." # comma omitted + " See the gettext manual, section Names.\"' \\", + " --keyword='proper_name_utf8:1,\"This is a proper name." # comma omitted + " See the gettext manual, section Names.\"' \\", + " --flag=error:3:c-format --flag=error_at_line:5:c-format", + "", + "# This is the copyright holder that gets inserted into the header of the", + "# $(DOMAIN).pot file. gnulib is copyrighted by the FSF.", + "COPYRIGHT_HOLDER = Free Software Foundation, Inc.", + "", + "# This is the email address or URL to which the translators shall report", + "# bugs in the untranslated strings:", + "# - Strings which are not entire sentences, see the maintainer guidelines", + "# in the GNU gettext documentation, section 'Preparing Strings'.", + "# - Strings which use unclear terms or require additional context to be", + "# understood.", + "# - Strings which make invalid assumptions about notation of date, time or", + "# money.", + "# - Pluralisation problems.", + "# - Incorrect English spelling.", + "# - Incorrect formatting.", + "# It can be your email address, or a mailing list address where translators", + "# can write to without being subscribed, or the URL of a web page through", + "# which the translators can contact you.", + "MSGID_BUGS_ADDRESS = bug-gnulib@gnu.org", + "", + "# This is the list of locale categories, beyond LC_MESSAGES, for which the", + "# message catalogs shall be used. It is usually empty.", + "EXTRA_LOCALE_CATEGORIES =", + "", + "# This tells whether the $(DOMAIN).pot file contains messages with an 'msgctxt'", + "# context. Possible values are \"yes\" and \"no\". Set this to yes if the", + "# package uses functions taking also a message context, like pgettext(), or", + "# if in $(XGETTEXT_OPTIONS) you define keywords with a context argument.", + "USE_MSGCTXT = no" + ) + def __init__(self, config): + if not isinstance(config, Config): + raise TypeError("config must be of pygnulib.Config type") + self._config_ = config + super().__init__() + + + @property + def po_base(self): + """directory relative to ROOT where *.po files are placed; defaults to 'po'""" + return self._config_.po_base + + + @property + def po_domain(self): + """the prefix of the i18n domain""" + return self._config_.po_domain + + + def __repr__(self): + fmt = "pygnulib.generator.POMakefile(po_base=%r, po_domain=%r)" + return fmt % (self.po_base, self.po_domain) + + + def __iter__(self): + for line in super().__iter__(): + yield line + yield "# Usually the message domain is the same as the package name." + yield "# But here it has a '-gnulib' suffix." + yield "DOMAIN = %s-gnulib" % self.po_domain + yield "" + yield "# These two variables depend on the location of this directory." + yield "subdir = %s" % self.po_domain + yield "top_subdir = %s" % "/".join([".." for _ in self.po_base.split(os.path.sep)]) + for line in POMakefile._TEMPLATE_: + yield line + + + +class POTFILES(Generator): + """file list to be passed to xgettext""" + def __init__(self, config, files): + if not isinstance(config, Config): + raise TypeError("config must be of pygnulib.Config type") + super().__init__() + self._config_ = config + self._files_ = tuple(files) + + + @property + def files(self): + """list of files""" + return tuple(self.files) + + + def __repr__(self): + fmt = "pygnulib.generator.POTFILES(files=%r)" + return fmt % self.files + + + def __iter__(self): + for line in super().__iter__(): + yield line + yield "# List of files which contain translatable strings." + for file in [_ for _ in self.files if _.startswith("lib/")]: + yield os.path.join(self._config_.source_base, file[4:]) + + + +class AutoconfSnippet(Generator): + """autoconf snippet generator for standalone module""" + def __init__(self, config, module, toplevel, no_libtool, no_gettext): + """ + config: gnulib configuration + module: gnulib module instance + toplevel: make a subordinate use of gnulib if False + no_libtool: disable libtool (regardless of configuration) + no_gettext: disable AM_GNU_GETTEXT invocations if True + """ + if not isinstance(config, Config): + raise TypeError("config must be of pygnulib.config.Config type") + if not isinstance(module, Module): + raise TypeError("module must be of pygnulib.module.Module type") + if not isinstance(toplevel, bool): + raise TypeError("toplevel must be of bool type") + if not isinstance(no_libtool, bool): + raise TypeError("no_libtool must be of bool type") + if not isinstance(no_gettext, bool): + raise TypeError("no_gettext must be of bool type") + super().__init__() + self._config_ = config + self._module_ = module + self._toplevel_ = toplevel + self._no_libtool_ = no_libtool + self._no_gettext_ = no_gettext + + + @property + def toplevel(self): + """top level indicator; subordinate use of pygnulib""" + return self._toplevel_ + + + @property + def libtool(self): + """libtool switch, disabling libtool configuration parameter""" + return self._config_.libtool and not self._no_libtool_ + + + @property + def gettext(self): + """gettext switch, disabling AM_GNU_GETTEXT invocations""" + return not self._no_gettext_ + + + def __repr__(self): + flags = [] + if self.toplevel: + flags += ["toplevel"] + if self.libtool: + flags += ["libtool"] + if self.gettext: + flags += ["gettext"] + fmt = "pygnulib.generator.AutoconfSnippet(include_guard_prefix=%r, flags=%s)" + include_guard_prefix = self._config_.include_guard_prefix + return fmt % (include_guard_prefix, "|".join(flags)) + + + def __iter__(self): + module = self._module_ + if module.name not in ("gnumakefile", "maintainer-makefile") or self.toplevel: + snippet = module.configure_ac_snippet + include_guard_prefix = self._config_.include_guard_prefix + snippet.replace(r"${gl_include_guard_prefix}", include_guard_prefix) + if not self.libtool: + table = ( + (r"$gl_cond_libtool", "false"), + ("gl_libdeps", "gltests_libdeps"), + ("gl_ltlibdeps", "gltests_ltlibdeps"), + ) + for (src, dst) in table: + snippet = snippet.replace(src, dst) + if not self.gettext: + src = "AM_GNU_GETTEXT([external])" + dst = "dnl you must add AM_GNU_GETTEXT([external]) or similar to configure.ac.'" + snippet = snippet.replace(src, dst) + lines = (_ for _ in snippet.split("\n") if _) + for line in lines: + yield line + if module.name == "alloca" and self.libtool: + yield "changequote(,)dnl" + yield "LTALLOCA=`echo \"$ALLOCA\" | sed -e 's/\\.[^.]* /.lo /g;s/\\.[^.]*$/.lo/'`" + yield "changequote([, ])dnl" + yield "AC_SUBST([LTALLOCA])" + + + +class InitMacro(Generator): + """basic gl_INIT macro generator""" + def __init__(self, config, macro_prefix=None): + """ + config: gnulib configuration + macro_prefix: macro prefix; if None, consider configuration + """ + if not isinstance(config, Config): + raise TypeError("config must be of pygnulib.config.Config type") + if macro_prefix is None: + macro_prefix = config.macro_prefix + if not isinstance(macro_prefix, str): + raise TypeError("macro_prefix must be of str type") + self._macro_prefix_ = macro_prefix + + + @property + def macro_prefix(self): + """the prefix of the macros 'gl_EARLY' and 'gl_INIT'""" + return self._macro_prefix_ + + + def __repr__(self): + fmt = "pygnulib.generator.InitMacro(macro_prefix=%r)" + return fmt % self.macro_prefix + + + +class InitMacroHeader(InitMacro): + """the first few statements of the gl_INIT macro""" + def __init__(self, config, macro_prefix=None): + """ + config: gnulib configuration + macro_prefix: macro prefix; if None, consider configuration + """ + super().__init__(config=config, macro_prefix=macro_prefix) + + + def __repr__(self): + fmt = "pygnulib.generator.InitMacroHeader(macro_prefix=%r)" + return fmt % self.macro_prefix + + + def __iter__(self): + # Overriding AC_LIBOBJ and AC_REPLACE_FUNCS has the effect of storing + # platform-dependent object files in ${macro_prefix_arg}_LIBOBJS instead + # of LIBOBJS. The purpose is to allow several gnulib instantiations under + # a single configure.ac file. (AC_CONFIG_LIBOBJ_DIR does not allow this + # flexibility). + # Furthermore it avoids an automake error like this when a Makefile.am + # that uses pieces of gnulib also uses $(LIBOBJ): + # automatically discovered file `error.c' should not be explicitly + # mentioned. + yield " m4_pushdef([AC_LIBOBJ], m4_defn([%s_LIBOBJ]))" % self.macro_prefix + yield " m4_pushdef([AC_REPLACE_FUNCS], m4_defn([%s_REPLACE_FUNCS]))" % self.macro_prefix + + # Overriding AC_LIBSOURCES has the same purpose of avoiding the automake + # error when a Makefile.am that uses pieces of gnulib also uses $(LIBOBJ): + # automatically discovered file `error.c' should not be explicitly + # mentioned + # We let automake know about the files to be distributed through the + # EXTRA_lib_SOURCES variable. + yield " m4_pushdef([AC_LIBSOURCES], m4_defn([%s_LIBSOURCES]))" % self.macro_prefix + + # Create data variables for checking the presence of files that are + # mentioned as AC_LIBSOURCES arguments. These are m4 variables, not shell + # variables, because we want the check to happen when the configure file is + # created, not when it is run. ${macro_prefix_arg}_LIBSOURCES_LIST is the + # list of files to check for. ${macro_prefix_arg}_LIBSOURCES_DIR is the + # subdirectory in which to expect them. + yield " m4_pushdef([%s_LIBSOURCES_LIST], [])" % self.macro_prefix + yield " m4_pushdef([%s_LIBSOURCES_DIR], [])" % self.macro_prefix + yield " gl_COMMON" + + + +class InitMacroFooter(InitMacro): + """the last few statements of the gl_INIT macro""" + _TEMPLATE_ = ( + " m4_ifval({macro_prefix}_LIBSOURCES_LIST, [", + " m4_syscmd([test ! -d ]m4_defn([{macro_prefix}_LIBSOURCES_DIR])[ ||", + " for gl_file in ]{macro_prefix}_LIBSOURCES_LIST[ ; do", + " if test ! -r ]m4_defn([{macro_prefix}_LIBSOURCES_DIR])[/$gl_file ; then", + " echo \"missing file ]m4_defn([{macro_prefix}_LIBSOURCES_DIR])[/$gl_file\" >&2", + " exit 1", + " fi", + " done])dnl", + " m4_if(m4_sysval, [0], [],", + " [AC_FATAL([expected source file, required through AC_LIBSOURCES, not found])])", + " ])", + " m4_popdef([{macro_prefix}_LIBSOURCES_DIR])", + " m4_popdef([{macro_prefix}_LIBSOURCES_LIST])", + " m4_popdef([AC_LIBSOURCES])", + " m4_popdef([AC_REPLACE_FUNCS])", + " m4_popdef([AC_LIBOBJ])", + " AC_CONFIG_COMMANDS_PRE([", + " {macro_prefix}_libobjs=", + " {macro_prefix}_ltlibobjs=", + " if test -n \"${macro_prefix}_LIBOBJS\"; then", + " # Remove the extension.", + " sed_drop_objext='s/\\.o$//;s/\\.obj$//'", + " for i in `for i in ${macro_prefix}_LIBOBJS; " # comma omitted + "do echo \"$i\"; done | sed -e \"$sed_drop_objext\" | sort | uniq`; do", + " {macro_prefix}_libobjs=\"${macro_prefix}_libobjs $i.$ac_objext\"", + " {macro_prefix}_ltlibobjs=\"${macro_prefix}_ltlibobjs $i.lo\"", + " done", + " fi", + " AC_SUBST([{macro_prefix}_LIBOBJS], [${macro_prefix}_libobjs])", + " AC_SUBST([{macro_prefix}_LTLIBOBJS], [${macro_prefix}_ltlibobjs])", + " ])", + ) + + + def __init__(self, config, macro_prefix=None): + """ + config: gnulib configuration + macro_prefix: macro prefix; if None, consider configuration + """ + super().__init__(config=config, macro_prefix=macro_prefix) + + + def __repr__(self): + fmt = "pygnulib.generator.InitMacroFooter(macro_prefix=%r)" + return fmt % self.macro_prefix + + + def __iter__(self): + # Check the presence of files that are mentioned as AC_LIBSOURCES + # arguments. The check is performed only when autoconf is run from the + # directory where the configure.ac resides; if it is run from a different + # directory, the check is skipped. + for line in InitMacroFooter._TEMPLATE_: + yield line.format(macro_prefix=self.macro_prefix) + + + +class InitMacroDone(InitMacro): + """few statements AFTER the gl_INIT macro""" + _TEMPLATE_ = ( + "", + "# Like AC_LIBOBJ, except that the module name goes", + "# into {macro_prefix}_LIBOBJS instead of into LIBOBJS.", + "AC_DEFUN([{macro_prefix}_LIBOBJ], [", + " AS_LITERAL_IF([$1], [{macro_prefix}_LIBSOURCES([$1.c])])dnl", + " {macro_prefix}_LIBOBJS=\"${macro_prefix}_LIBOBJS $1.$ac_objext\"", + "])", + "", + "# Like AC_REPLACE_FUNCS, except that the module name goes", + "# into {macro_prefix}_LIBOBJS instead of into LIBOBJS.", + "AC_DEFUN([{macro_prefix}_REPLACE_FUNCS], [", + " m4_foreach_w([gl_NAME], [$1], [AC_LIBSOURCES(gl_NAME[.c])])dnl", + " AC_CHECK_FUNCS([$1], , [{macro_prefix}_LIBOBJ($ac_func)])", + "])", + "", + "# Like AC_LIBSOURCES, except the directory where the source file is", + "# expected is derived from the gnulib-tool parameterization,", + "# and alloca is special cased (for the alloca-opt module).", + "# We could also entirely rely on EXTRA_lib..._SOURCES.", + "AC_DEFUN([{macro_prefix}_LIBSOURCES], [", + " m4_foreach([_gl_NAME], [$1], [", + " m4_if(_gl_NAME, [alloca.c], [], [", + " m4_define([{macro_prefix}_LIBSOURCES_DIR], [{source_base}])", + " m4_append([{macro_prefix}_LIBSOURCES_LIST], _gl_NAME, [ ])", + " ])", + " ])", + "])", + ) + + + def __init__(self, config, source_base=None, macro_prefix=None): + if source_base is None: + source_base = config.source_base + if not isinstance(source_base, str): + raise TypeError("source_base must be of str type") + super().__init__(config=config, macro_prefix=macro_prefix) + self._source_base_ = source_base + + + @property + def source_base(self): + """directory relative to ROOT where source code is placed; defaults to 'lib'""" + return self._source_base_ + + + def __repr__(self): + fmt = "pygnulib.generator.InitMacroDone(source_base=%r, macro_prefix=%r)" + return fmt % (self.source_base, self.macro_prefix) + + + def __iter__(self): + for line in InitMacroDone._TEMPLATE_: + yield line.format(source_base=self._source_base_, macro_prefix=self._macro_prefix_) diff --git a/pygnulib/module.py b/pygnulib/module.py new file mode 100644 index 0000000000..75aad88a96 --- /dev/null +++ b/pygnulib/module.py @@ -0,0 +1,416 @@ +#!/usr/bin/python +# encoding: UTF-8 + + + +import codecs +import collections +import hashlib +import os +import re + + + +class Module: + """gnulib generic module""" + _TABLE_ = { + "description" : (0x00, str, "Description"), + "comment" : (0x01, str, "Comment"), + "status" : (0x02, str, "Status"), + "notice" : (0x03, str, "Notice"), + "applicability" : (0x04, str, "Applicability"), + "files" : (0x05, list, "Files"), + "dependencies" : (0x06, list, "Depends-on"), + "early_configure_ac_snippet" : (0x07, str, "configure.ac-early"), + "configure_ac_snippet" : (0x08, str, "configure.ac"), + "makefile_am_snippet" : (0x09, str, "Makefile.am"), + "include" : (0x0A, list, "Include"), + "link" : (0x0B, list, "Link"), + "license" : (0x0C, str, "License"), + "maintainers" : (0x0D, list, "Maintainer"), + } + _PATTERN_DEPENDENCIES_ = re.compile("^(\\S+)(?:\\s+(.+))*$") + _PATTERN_INCLUDE_ = re.compile("^[\\<\"]([A-Za-z0-9/\\-_]+\\.h)[\\>\"](?:\\s+.*^)*$") + + + def __init__(self, name, **kwargs): + if not isinstance(name, str): + raise TypeError("name must be of 'str' type") + self._name_ = name + self._table_ = {"maintainers": ["all"]} + for key in Module._TABLE_: + self._table_[key] = "" + for key, value in kwargs.items(): + self._table_[key] = value + + + @property + def name(self): + """name""" + return self._name_ + + @name.setter + def name(self, value): + if not isinstance(value, str): + raise TypeError("'str' type is expected") + self._name_ = value + + + @property + def description(self): + """description""" + return self._table_["description"] + + @description.setter + def description(self, value): + if not isinstance(value, str): + raise TypeError("'str' type is expected") + self._table_["description"] = value + + + @property + def comment(self): + """comment""" + return self._table_["comment"] + + @comment.setter + def comment(self, value): + if not isinstance(value, str): + raise TypeError("'str' type is expected") + self._table_["comment"] = value + + + @property + def status(self): + """status""" + return self._table_["status"] + + @status.setter + def status(self, value): + if not isinstance(value, str): + raise TypeError("'str' type is expected") + self._table_["status"] = value + + + @property + def notice(self): + """notice""" + return self._table_["notice"] + + @notice.setter + def notice(self, value): + if not isinstance(value, str): + raise TypeError("'str' type is expected") + self._table_["notice"] = value + + + @property + def applicability(self): + """applicability (usually "main" or "tests")""" + default = "main" if self.name.endswith("-tests") else "tests" + current = self._table_["applicability"] + return current if current.strip() else default + + @applicability.setter + def applicability(self, value): + if not isinstance(value, str): + raise TypeError("'str' type is expected") + self._table_["applicability"] = value + + + @property + def files(self): + """file dependencies iterator (set of strings)""" + for file in self._table_["files"]: + yield file + + @files.setter + def files(self, iterable): + if isinstance(iterable, (bytes, str)) \ + or not isinstance(iterable, collections.Iterable): + raise TypeError("iterable of strings is expected") + result = set() + for item in iterable: + if not isinstance(item, str): + raise TypeError("iterable of strings is expected") + result.update([item]) + self._table_["files"] = result + + + @property + def dependencies(self): + """dependencies iterator (name, condition)""" + for entry in self._table_["dependencies"]: + yield Module._PATTERN_DEPENDENCIES_.findall(entry)[0] + + @dependencies.setter + def dependencies(self, iterable): + error = TypeError("iterable of pairs (name, condition) is expected") + if isinstance(iterable, (bytes, str)) \ + or not isinstance(iterable, collections.Iterable): + raise error + result = set() + try: + for pair in iterable: + (name, condition) = pair + if not isinstance(name, str) \ + or not isinstance(condition, str): + raise error + result.update([(name, condition)]) + except ValueError: + raise error + self._table_["dependencies"] = result + + + @property + def early_configure_ac_snippet(self): + """early configure.ac snippet""" + return self._table_["early_configure_ac_snippet"] + + @early_configure_ac_snippet.setter + def early_configure_ac_snippet(self, value): + if not isinstance(value, str): + raise TypeError("'str' type is expected") + self._table_["early_configure_ac_snippet"] = value + + + @property + def configure_ac_snippet(self): + """configure.ac snippet""" + return self._table_["configure_ac_snippet"] + + @configure_ac_snippet.setter + def configure_ac_snippet(self, value): + if not isinstance(value, str): + raise TypeError("'str' type is expected") + self._table_["configure_ac_snippet"] = value + + + @property + def makefile_am_snippet(self): + """Makefile.am snippet""" + return self._table_["makefile_am_snippet"] + + @makefile_am_snippet.setter + def makefile_am_snippet(self, value): + if not isinstance(value, str): + raise TypeError("'str' type is expected") + self._table_["makefile_am_snippet"] = value + + + @property + def include(self): + """include files iterator (header, comment)""" + for entry in self._table_["include"]: + match = Module._PATTERN_INCLUDE_.findall(entry) + yield match[0] if match else entry + + @include.setter + def include(self, iterable): + error = TypeError("iterable of pairs (header, comment) is expected") + if isinstance(iterable, (bytes, str)) \ + or not isinstance(iterable, collections.Iterable): + raise error + result = set() + try: + for pair in iterable: + (header, comment) = pair + if not isinstance(header, str) \ + or not isinstance(comment, str): + raise error + result.update([(header, comment)]) + except ValueError: + raise error + self._table_["include"] = result + + + @property + def link(self): + """linkage iterator (string)""" + for entry in self._table_["link"]: + yield entry + + @link.setter + def link(self, iterable): + if isinstance(iterable, (bytes, str)) \ + or not isinstance(iterable, collections.Iterable): + raise TypeError("iterable of strings is expected") + result = set() + for item in iterable: + if not isinstance(item, str): + raise TypeError("iterable of strings is expected") + result.update([item]) + self._table_["link"] = result + + + @property + def license(self): + """license""" + return self._table_["license"] + + @license.setter + def license(self, value): + if not isinstance(value, str): + raise TypeError("'str' type is expected") + self._table_["license"] = value + + + @property + def maintainers(self): + """maintainers iterator (maintainer)""" + for entry in self._table_["maintainers"]: + yield entry + + @maintainers.setter + def maintainers(self, iterable): + if isinstance(iterable, (bytes, str)) \ + or not isinstance(iterable, collections.Iterable): + raise TypeError("iterable of strings is expected") + result = set() + for item in iterable: + if not isinstance(item, str): + raise TypeError("iterable of strings is expected") + result.update([item]) + self._table_["maintainers"] = result + + + def shell_variable(self, macro_prefix="gl"): + """Get the name of the shell variable set to true once m4 macros have been executed.""" + module = self.name + if len(module) != len(module.encode()): + module = (module + "\n").encode("UTF-8") + module = hashlib.md5(module).hexdigest() + return "%s_gnulib_enabled_%s" % (macro_prefix, module) + + + def shell_function(self, macro_prefix="gl"): + """Get the name of the shell function containing the m4 macros.""" + module = self.name + if len(module) != len(module.encode()): + module = (module + "\n").encode("UTF-8") + module = hashlib.md5(module).hexdigest() + return "func_%s_gnulib_m4code_%s" % (macro_prefix, module) + + + def conditional_name(self, macro_prefix="gl"): + """Get the automake conditional name.""" + module = self.name + if len(module) != len(module.encode()): + module = (module + "\n").encode("UTF-8") + module = hashlib.md5(module).hexdigest() + return "%s_GNULIB_ENABLED_%s" % (macro_prefix, module) + + + def __hash__(self): + return hash(str(self)) + + + def __repr__(self): + return self._name_ + + + def __str__(self): + result = "" + for key, (_, typeid, field) in sorted(Module._TABLE_.items(), key=lambda k: k[1][0]): + field += ":\n" + if typeid is list: + value = "\n".join(self._table_[key]) + else: + value = self._table_[key] + if value: + result += field + result += value + result += "\n\n" if value else "\n" + return result.strip() + "\n" + + + def __lt__(self, value): + return self.name < value.name + + def __le__(self, value): + return self.__lt__(value) or self.__eq__(value) + + def __eq__(self, value): + return self.name == value.name + + def __ne__(self, value): + return not self.__eq__(value) + + def __ge__(self, value): + return value.__le__(self) + + def __gt__(self, value): + return value.__lt__(self) + + + +class FileModule(Module): + """gnulib module text file""" + _TABLE_ = { + "Description" : (str, "description"), + "Comment" : (str, "comment"), + "Status" : (str, "status"), + "Notice" : (str, "notice"), + "Applicability" : (str, "applicability"), + "Files" : (list, "files"), + "Depends-on" : (list, "dependencies"), + "configure.ac-early" : (str, "early_configure_ac_snippet"), + "configure.ac" : (str, "configure_ac_snippet"), + "Makefile.am" : (str, "makefile_am_snippet"), + "Include" : (list, "include"), + "Link" : (list, "link"), + "License" : (str, "license"), + "Maintainer" : (list, "maintainers"), + } + _FIELDS_ = [field for (_, _, field) in Module._TABLE_.values()] + _PATTERN_ = re.compile("(%s):" % "|".join(_FIELDS_)) + + def __init__(self, path, mode="r", name=None, **kwargs): + if name is None: + name = os.path.basename(path) + if mode not in ("r", "w", "rw"): + raise ValueError("illegal mode: %r" % mode) + if mode == "r": + super().__init__(name) + with codecs.open(path, "rb", "UTF-8") as stream: + data = "" + for line in stream: + line = line.strip("\n") + if line.startswith("#") \ + or (line.startswith("/*") and line.endswith("*/")): + continue + data += (line + "\n") + match = FileModule._PATTERN_.split(data)[1:] + for (group, value) in zip(match[::2], match[1::2]): + (typeid, key) = FileModule._TABLE_[group] + if typeid is list: + self._table_[key] = [_ for _ in "".join(value).split("\n") if _.strip()] + else: + self._table_[key] = value.strip() + self._stream_ = None + elif mode == "w": + super().__init__(name) + self._stream_ = codecs.open(path, "w+", "UTF-8") + elif mode == "rw": + self.__init__(path, "r") + self._stream_ = codecs.open(path, "w+", "UTF-8") + else: + raise ValueError("invalid mode: %r" % mode) + for key, value in kwargs.items(): + self._table_[key] = value + + + def close(self): + """Close the underlying stream and write data into the file.""" + if self._stream_: + self._stream_.truncate(0) + self._stream_.write(str(self)) + self._stream_.close() + + + def __enter__(self): + return self + + + def __exit__(self, exctype, excval, exctrace): + self.close()