mirror of
https://https.git.savannah.gnu.org/git/gnulib.git
synced 2026-06-15 23:35:50 +00:00
1077 lines
38 KiB
Python
1077 lines
38 KiB
Python
#!/usr/bin/python
|
|
# encoding: UTF-8
|
|
"""gnulib module API"""
|
|
|
|
|
|
|
|
import ast as _ast
|
|
import codecs as _codecs
|
|
import collections as _collections
|
|
import hashlib as _hashlib
|
|
import itertools as _itertools
|
|
import json as _json
|
|
import os as _os
|
|
import re as _re
|
|
import sys as _sys
|
|
|
|
|
|
from .error import UnknownModuleError as _UnknownModuleError
|
|
from .config import BaseConfig as _BaseConfig
|
|
from .misc import Property as _Property
|
|
from .misc import PathProperty as _PathProperty
|
|
from .misc import StringListProperty as _StringListProperty
|
|
from .misc import PathListProperty as _PathListProperty
|
|
|
|
|
|
|
|
class BaseModule:
|
|
"""base module"""
|
|
__slots__ = ("__options", "__flags")
|
|
|
|
|
|
__PROPERTIES = {
|
|
"name" : None,
|
|
"description" : "",
|
|
"comment" : "",
|
|
"status" : "",
|
|
"notice" : "",
|
|
"applicability" : "",
|
|
"files" : "",
|
|
"dependencies" : tuple(),
|
|
"early_autoconf_snippet" : "",
|
|
"autoconf_snippet" : "",
|
|
"conditional_automake_snippet" : "",
|
|
"unconditional_automake_snippet" : None,
|
|
"automake_snippet" : None,
|
|
"include_directives" : "",
|
|
"link_directives" : tuple(),
|
|
"licenses" : tuple(),
|
|
"maintainers" : tuple(),
|
|
"test" : False,
|
|
}
|
|
__OPTIONS = {
|
|
"name",
|
|
"description",
|
|
"comment",
|
|
"status",
|
|
"notice",
|
|
"applicability",
|
|
"files",
|
|
"dependencies",
|
|
"early_autoconf_snippet",
|
|
"autoconf_snippet",
|
|
"conditional_automake_snippet",
|
|
"unconditional_automake_snippet",
|
|
"automake_snippet",
|
|
"include_directives",
|
|
"link_directives",
|
|
"licenses",
|
|
"maintainers",
|
|
"test",
|
|
}
|
|
__FLAGS = {}
|
|
__LIB_SOURCES = _re.compile(r"^lib_SOURCES\s*\+\=\s*(.*?)$", _re.S | _re.M)
|
|
|
|
|
|
def __init__(self, name, **kwargs):
|
|
if not isinstance(name, str):
|
|
raise TypeError("name: str expected")
|
|
if not name.strip():
|
|
raise ValueError("name: invalid name")
|
|
|
|
self.__flags = 0
|
|
self.__options = {}
|
|
for key in BaseModule.__OPTIONS:
|
|
value = BaseModule.__PROPERTIES[key]
|
|
if value is not None:
|
|
self.__set_option(key, value)
|
|
for key in BaseModule.__FLAGS:
|
|
state = BaseModule.__PROPERTIES[key]
|
|
mask = getattr(self.__class__, key).mask
|
|
self.__set_flags(mask, state)
|
|
self.__set_option("name", name)
|
|
|
|
for (key, value) in kwargs.items():
|
|
setattr(self, key, value)
|
|
|
|
|
|
def __enter__(self):
|
|
return self
|
|
|
|
|
|
def __hash__(self):
|
|
return hash(_json.dumps(dict(self.__options), sort_keys=True))
|
|
|
|
|
|
def __repr__(self):
|
|
module = self.__class__.__module__
|
|
name = self.__class__.__name__
|
|
return f"{module}.{name}[{self.name}]"
|
|
|
|
|
|
def __getitem__(self, key):
|
|
if key not in BaseModule.__PROPERTIES:
|
|
key = key.replace("-", "_")
|
|
if key not in BaseModule.__PROPERTIES:
|
|
raise KeyError(repr(key))
|
|
return getattr(self, key)
|
|
|
|
|
|
def __setitem__(self, key, value):
|
|
if key not in BaseModule.__PROPERTIES:
|
|
key = key.replace("-", "_")
|
|
if key not in BaseModule.__PROPERTIES:
|
|
raise KeyError(repr(key))
|
|
return setattr(self, key, value)
|
|
|
|
|
|
def __get_option(self, key):
|
|
return self.__options[key]
|
|
|
|
def __set_option(self, key, value):
|
|
self.__options[key] = value
|
|
|
|
|
|
@property
|
|
def gnulib_package(self):
|
|
"""gnulib-compatible module textual representation"""
|
|
def _gnulib():
|
|
yield "Description:"
|
|
yield self.description
|
|
yield "Comment:"
|
|
yield self.comment
|
|
yield "Status:"
|
|
for status in sorted(self.status):
|
|
yield status
|
|
yield "Notice:"
|
|
yield self.notice
|
|
yield "Applicability:"
|
|
yield self.applicability
|
|
yield "Files:"
|
|
for file in self.files:
|
|
yield file
|
|
yield "Depends-on:"
|
|
for (module, condition) in self.dependencies:
|
|
if condition:
|
|
yield f"{module} {condition}"
|
|
else:
|
|
yield f"{module}"
|
|
yield "configure.ac-early:"
|
|
yield self.early_autoconf_snippet
|
|
yield "configure.ac:"
|
|
yield self.autoconf_snippet
|
|
yield "Makefile.am:"
|
|
yield self.conditional_automake_snippet
|
|
yield "Include:"
|
|
for include in self.include_directives:
|
|
yield include
|
|
yield "Link:"
|
|
for link in self.link_directives:
|
|
yield link
|
|
yield "License:"
|
|
for license in self.licenses:
|
|
yield license
|
|
yield "Maintainer:"
|
|
for maintainer in maintainers:
|
|
yield maintainer
|
|
return "\n".join(_gnulib())
|
|
|
|
|
|
name = _Property(
|
|
fget=lambda self: self.__get_option("name"),
|
|
fset=lambda self, string: self.__set_option("name", string),
|
|
check=lambda value: isinstance(value, str) and value,
|
|
doc="name",
|
|
)
|
|
description = _Property(
|
|
fget=lambda self: self.__get_option("description"),
|
|
fset=lambda self, string: self.__set_option("description", string),
|
|
check=lambda value: isinstance(value, str),
|
|
doc="description",
|
|
)
|
|
comment = _Property(
|
|
fget=lambda self: self.__get_option("comment"),
|
|
fset=lambda self, string: self.__set_option("comment", string),
|
|
check=lambda value: isinstance(value, str),
|
|
doc="comment",
|
|
)
|
|
status = _StringListProperty(
|
|
sorted=True,
|
|
unique=True,
|
|
fget=lambda self: self.__get_option("comment"),
|
|
fset=lambda self, string: self.__set_option("comment", string),
|
|
doc="status list",
|
|
)
|
|
obsolete = _Property(
|
|
fget=lambda self: "obsolete" in self.status,
|
|
doc="module is obsolete?",
|
|
)
|
|
cxx_test = _Property(
|
|
fget=lambda self: "c++-test" in self.status,
|
|
doc="module is a C++ test?",
|
|
)
|
|
longrunning_test = _Property(
|
|
fget=lambda self: "longrunning-test" in self.status,
|
|
doc="module is a longrunning test?",
|
|
)
|
|
privileged_test = _Property(
|
|
fget=lambda self: "privileged-test" in self.status,
|
|
doc="module is a privileged test?",
|
|
)
|
|
unportable_test = _Property(
|
|
fget=lambda self: "unportable-test" in self.status,
|
|
doc="module is an unportable test?",
|
|
)
|
|
mask = _Property(
|
|
fget=lambda self: ((0, (1 << 0))[self.obsolete]
|
|
|(0, (1 << 1))[self.cxx_test]
|
|
|(0, (1 << 2))[self.longrunning_test]
|
|
|(0, (1 << 3))[self.privileged_test]
|
|
|(0, (1 << 4))[self.unportable_test]),
|
|
doc="module acceptibility mask",
|
|
)
|
|
notice = _Property(
|
|
fget=lambda self: self.__get_option("notice"),
|
|
fset=lambda self, string: self.__set_option("notice", string),
|
|
check=lambda value: isinstance(value, str),
|
|
doc="module notice or disclaimer",
|
|
)
|
|
applicability = _Property(
|
|
fget=lambda self: self.__get_option("applicability"),
|
|
fset=lambda self, string: self.__set_option("applicability", string),
|
|
check=lambda value: isinstance(value, str) and value in {"main", "tests", "all"},
|
|
doc="applicability ('main', 'tests' or 'all')",
|
|
)
|
|
files = _PathListProperty(
|
|
sorted=True,
|
|
unique=True,
|
|
fget=lambda self: self.__get_option("files"),
|
|
fset=lambda self, string: self.__set_option("files", string),
|
|
doc="file dependencies",
|
|
)
|
|
early_autoconf_snippet = _Property(
|
|
fget=lambda self: self.__get_option("early_autoconf_snippet"),
|
|
fset=lambda self, string: self.__set_option("early_autoconf_snippet", string),
|
|
check=lambda value: isinstance(value, str),
|
|
doc="early configure.ac snippet",
|
|
)
|
|
autoconf_snippet = _Property(
|
|
fget=lambda self: self.__get_option("autoconf_snippet"),
|
|
fset=lambda self, string: self.__set_option("autoconf_snippet", string),
|
|
check=lambda value: isinstance(value, str),
|
|
doc="configure.ac snippet",
|
|
)
|
|
conditional_automake_snippet = _Property(
|
|
fget=lambda self: self.__get_option("conditional_automake_snippet"),
|
|
fset=lambda self, string: self.__set_option("conditional_automake_snippet", string),
|
|
check=lambda value: isinstance(value, str),
|
|
doc="configure.ac snippet",
|
|
)
|
|
automake_snippet = _Property(
|
|
fget=lambda self: "\n".join((self.conditional_automake_snippet, self.unconditional_automake_snippet)),
|
|
doc="full automake snippet (conditional + unconditional parts)",
|
|
)
|
|
include_directives = _StringListProperty(
|
|
fget=lambda self: self.__get_option("include_directives"),
|
|
fset=lambda self, string: self.__set_option("include_directives", string),
|
|
doc="include directive",
|
|
)
|
|
link_directives = _StringListProperty(
|
|
fget=lambda self: self.__get_option("link_directives"),
|
|
fset=lambda self, string: self.__set_option("link_directives", string),
|
|
doc="link directive",
|
|
)
|
|
licenses = _StringListProperty(
|
|
sorted=True,
|
|
unique=True,
|
|
fget=lambda self: self.__get_option("licenses"),
|
|
fset=lambda self, name: self.__set_option("licenses", name),
|
|
doc="acceptable licenses for modules",
|
|
)
|
|
maintainers = _StringListProperty(
|
|
sorted=False,
|
|
unique=True,
|
|
fget=lambda self: self.__get_option("maintainers"),
|
|
fset=lambda self, name: self.__set_option("maintainers", name),
|
|
doc="module maintainers list",
|
|
)
|
|
test = _Property(
|
|
fget=lambda self: self.__get_option("test"),
|
|
fset=lambda self, name: self.__set_option("test", name),
|
|
check=lambda value: isinstance(value, bool),
|
|
doc="module is a test?",
|
|
)
|
|
|
|
|
|
@_Property
|
|
def dependencies(self):
|
|
"""dependencies iterator (name, condition)"""
|
|
return self.__options["dependencies"]
|
|
|
|
@dependencies.setter
|
|
def dependencies(self, value):
|
|
result = []
|
|
types = (list, tuple, set, frozenset, type({}.keys()), type({}.values()))
|
|
if not isinstance(value, types):
|
|
raise TypeError("value: iterable expected")
|
|
for item in value:
|
|
if not isinstance(value, (list, tuple)):
|
|
raise TypeError("item: pair expected")
|
|
(module, condition) = item
|
|
if not isinstance(module, str):
|
|
raise TypeError("module: str expected")
|
|
if condition is not None and not isinstance(condition, str):
|
|
raise TypeError("condition: str or None expected")
|
|
condition = "" if condition is None else condition
|
|
result.append((module, condition))
|
|
self.__options["dependencies"] = tuple(result)
|
|
|
|
|
|
@_Property
|
|
def unconditional_automake_snippet(self):
|
|
"""Makefile.am snippet that must stay outside of Automake conditionals"""
|
|
result = ""
|
|
files = self.files
|
|
if self.test:
|
|
# *-tests module live in tests/, not lib/.
|
|
# Synthesize an EXTRA_DIST augmentation.
|
|
test_files = tuple(file[len("tests/"):] for file in files if file.startswith("tests/"))
|
|
if test_files:
|
|
result += ("EXTRA_DIST += {}".format(" ".join(test_files)) + "\n")
|
|
return result
|
|
snippet = self.conditional_automake_snippet
|
|
lib_SOURCES = False
|
|
lines = list(snippet.splitlines())
|
|
for (index, line) in enumerate(lines):
|
|
if BaseModule.__LIB_SOURCES.findall(line):
|
|
(first, last) = (index, index)
|
|
while line.endswith("\\"):
|
|
line = lines[last]
|
|
last += 1
|
|
lines = list(lines)[first:(last + 1)]
|
|
lines[0] = BaseModule.__LIB_SOURCES.sub("\\1", lines[0])
|
|
lib_SOURCES = True
|
|
break
|
|
lines = tuple(lines) if lib_SOURCES else ()
|
|
lines = filter(lambda line: line.strip(), lines)
|
|
lines = (line.replace("\\", "").strip() for line in lines)
|
|
(all_files, mentioned_files) = (files, [])
|
|
for line in lines:
|
|
for file in line.split():
|
|
if file.strip():
|
|
mentioned_files += [file]
|
|
mentioned_files = tuple(_collections.OrderedDict.fromkeys(mentioned_files))
|
|
lib_files = tuple(file[len("lib/"):] for file in all_files if file.startswith("lib/"))
|
|
extra_files = tuple(file for file in lib_files if file not in mentioned_files)
|
|
if extra_files:
|
|
result += ("EXTRA_DIST += {}".format(" ".join(extra_files)) + "\n")
|
|
|
|
# Synthesize also an EXTRA_lib_SOURCES augmentation.
|
|
# This is necessary so that automake can generate the right list of
|
|
# dependency rules.
|
|
# A possible approach would be to use autom4te --trace of the redefined
|
|
# AC_LIBOBJ and AC_REPLACE_FUNCS macros when creating the Makefile.am
|
|
# (use autom4te --trace, not just grep, so that AC_LIBOBJ invocations
|
|
# inside autoconf's built-in macros are not missed).
|
|
# But it's simpler and more robust to do it here, based on the file list.
|
|
# If some .c file exists and is not used with AC_LIBOBJ - for example,
|
|
# a .c file is preprocessed into another .c file for BUILT_SOURCES -,
|
|
# automake will generate a useless dependency; this is harmless.
|
|
if self.name not in {"relocatable-prog-wrapper", "pt_chown"}:
|
|
extra_files = tuple(file for file in extra_files if file.endswith(".c"))
|
|
if extra_files:
|
|
result += ("EXTRA_lib_SOURCES += {}".format(" ".join(sorted(extra_files))) + "\n")
|
|
|
|
# Synthesize an EXTRA_DIST augmentation also for the files in build-aux/.
|
|
prefix = "$(top_srcdir)/{auxdir}"
|
|
buildaux_files = (file for file in all_files if file.startswith("build-aux/"))
|
|
buildaux_files = tuple(_os.path.join(prefix, file[len("build-aux/"):]) for file in buildaux_files)
|
|
if buildaux_files:
|
|
result += ("EXTRA_DIST += {}".format(" ".join(sorted(buildaux_files))) + "\n")
|
|
return result
|
|
|
|
|
|
def shell_variable(self, macro_prefix="gl"):
|
|
"""Get the name of the shell variable set to true once m4 macros have been executed."""
|
|
name = self.name
|
|
if any(filter(lambda rune: not (rune.isalnum() or rune == "_"), name)):
|
|
name = (name + "\n").encode("UTF-8")
|
|
name = _hashlib.md5(name).hexdigest()
|
|
return f"{macro_prefix}_gnulib_enabled_{name}"
|
|
|
|
|
|
def shell_function(self, macro_prefix="gl"):
|
|
"""Get the name of the shell function containing the m4 macros."""
|
|
name = self.name
|
|
if any(filter(lambda rune: not (rune.isalnum() or rune == "_"), name)):
|
|
name = (name + "\n").encode("UTF-8")
|
|
name = _hashlib.md5(name).hexdigest()
|
|
return f"func_{macro_prefix}_gnulib_m4code_{name}"
|
|
|
|
|
|
def conditional_name(self, macro_prefix="gl"):
|
|
"""Get the automake conditional name."""
|
|
name = self.name
|
|
if any(filter(lambda rune: not (rune.isalnum() or rune == "_"), name)):
|
|
name = (name + "\n").encode("UTF-8")
|
|
name = _hashlib.md5(name).hexdigest()
|
|
return f"{macro_prefix}_GNULIB_ENABLED_{name}"
|
|
|
|
|
|
def items(self):
|
|
"""a set-like object providing a view on module items"""
|
|
for key in BaseModule.__PROPERTIES:
|
|
yield (key, self[key])
|
|
|
|
|
|
@classmethod
|
|
def keys(self):
|
|
"""a set-like object providing a view on module keys"""
|
|
for key in BaseModule.__PROPERTIES:
|
|
yield key
|
|
|
|
|
|
def values(self):
|
|
"""a set-like object providing a view on module values"""
|
|
for key in BaseModule.__PROPERTIES:
|
|
yield self[key]
|
|
|
|
|
|
def __lt__(self, value):
|
|
if value is not None:
|
|
return self.name < value.name
|
|
return False
|
|
|
|
def __le__(self, value):
|
|
return self.__lt__(value) or self.__eq__(value)
|
|
|
|
def __eq__(self, value):
|
|
if value is not None:
|
|
if self.name != value.name:
|
|
return False
|
|
for key in BaseModule.__PROPERTIES:
|
|
if self[key] != value[key]:
|
|
return False
|
|
return True
|
|
return False
|
|
|
|
def __ne__(self, value):
|
|
return not self.__eq__(value)
|
|
|
|
def __ge__(self, value):
|
|
return not value.__le__(self)
|
|
|
|
def __gt__(self, value):
|
|
return not value.__lt__(self)
|
|
|
|
|
|
|
|
class FileModule(BaseModule):
|
|
"""text-based module"""
|
|
__slots__ = ("__path")
|
|
|
|
|
|
__DEPENDENCY = _re.compile(r"(\S+)(?:\s+\[(.*?)\])?$", _re.M)
|
|
__STRING = lambda text: text.strip()
|
|
__MULTILINE = lambda text: tuple(filter(
|
|
lambda line: line.strip() and not line.strip().startswith("#"),
|
|
[line.strip() for line in text.strip().splitlines()],
|
|
))
|
|
__INCLUDE_DIRECTIVES = lambda text: tuple(filter(
|
|
lambda line: line.strip(),
|
|
[line.strip() for line in text.strip().splitlines()],
|
|
))
|
|
__DEPENDENCIES = lambda text: FileModule.__DEPENDENCY.findall("\n".join(FileModule.__MULTILINE(text)))
|
|
__MAINTAINERS = lambda text: tuple(filter(
|
|
lambda line: line.strip() and not line.strip().startswith("#"),
|
|
{line.strip() for line in text.split((",", "\n")["\n" in text.strip()])},
|
|
))
|
|
__LICENSES = lambda text: set(_itertools.chain.from_iterable(
|
|
line.split(" or ") for line in FileModule.__MULTILINE(text)
|
|
))
|
|
__TABLE = {
|
|
"Description": (
|
|
"description",
|
|
__STRING,
|
|
),
|
|
"Comment": (
|
|
"comment",
|
|
__STRING,
|
|
),
|
|
"Status": (
|
|
"status",
|
|
__MULTILINE,
|
|
),
|
|
"Notice": (
|
|
"notice",
|
|
__STRING,
|
|
),
|
|
"Applicability": (
|
|
"applicability",
|
|
__STRING,
|
|
),
|
|
"Files": (
|
|
"files",
|
|
__MULTILINE,
|
|
),
|
|
"Depends-on": (
|
|
"dependencies",
|
|
__DEPENDENCIES,
|
|
),
|
|
"configure.ac-early": (
|
|
"early_autoconf_snippet",
|
|
__STRING,
|
|
),
|
|
"configure.ac": (
|
|
"autoconf_snippet",
|
|
__STRING,
|
|
),
|
|
"Makefile.am": (
|
|
"conditional_automake_snippet",
|
|
__STRING,
|
|
),
|
|
"Include": (
|
|
"include_directives",
|
|
__INCLUDE_DIRECTIVES,
|
|
),
|
|
"Link": (
|
|
"link_directives",
|
|
__MULTILINE,
|
|
),
|
|
"License": (
|
|
"licenses",
|
|
__LICENSES,
|
|
),
|
|
"Maintainer": (
|
|
"maintainers",
|
|
__MAINTAINERS,
|
|
),
|
|
}
|
|
__PATTERN = _re.compile("({}):".format("|".join(__TABLE)))
|
|
|
|
|
|
path = _Property(
|
|
fget=lambda self: self.__path,
|
|
doc="module file path",
|
|
)
|
|
|
|
|
|
def __init__(self, path, name, **kwargs):
|
|
if not isinstance(path, str):
|
|
raise TypeError("path: str expected")
|
|
if not isinstance(name, str):
|
|
raise TypeError("name: str expected")
|
|
with _codecs.open(path, "rb", "UTF-8") as stream:
|
|
match = FileModule.__PATTERN.split(stream.read())[1:]
|
|
for (group, text) in zip(match[::2], match[1::2]):
|
|
(key, hook) = FileModule.__TABLE[group]
|
|
kwargs.setdefault(key, hook(text))
|
|
super().__init__(name=name, **kwargs)
|
|
self.__path = path
|
|
|
|
|
|
|
|
class _DummyModuleMeta(type):
|
|
__INSTANCE = None
|
|
__PROPERTIES = {
|
|
"description": "A dummy file, to make sure the library is non-empty.",
|
|
"comment": "",
|
|
"status": tuple(),
|
|
"notice": "",
|
|
"applicability": "main",
|
|
"files": tuple({"lib/dummy.c"}),
|
|
"dependencies": tuple(),
|
|
"early_autoconf_snippet": "",
|
|
"autoconf_snippet": "",
|
|
"include_directives": "",
|
|
"link_directives": "",
|
|
"licenses": tuple({"public domain"}),
|
|
"maintainers": tuple({"all"}),
|
|
"automake_snippet": "lib_SOURCES += dummy.c",
|
|
"conditional_automake_snippet": "lib_SOURCES += dummy.c",
|
|
"unconditional_automake_snippet": "",
|
|
}
|
|
|
|
|
|
def __new__(mcs, name, parents, attributes):
|
|
for (key, value) in _DummyModuleMeta.__PROPERTIES.items():
|
|
fget=lambda self, value=value: value
|
|
doc = getattr(BaseModule, key).__doc__
|
|
attributes[key] = _Property(fget=fget, doc=doc)
|
|
return super().__new__(mcs, name, parents, attributes)
|
|
|
|
|
|
def __call__(cls, *args, **kwargs):
|
|
if _DummyModuleMeta.__INSTANCE is None:
|
|
_DummyModuleMeta.__INSTANCE = super().__call__(*args, **kwargs)
|
|
return _DummyModuleMeta.__INSTANCE
|
|
|
|
|
|
class DummyModule(BaseModule, metaclass=_DummyModuleMeta):
|
|
"""dummy module singleton"""
|
|
def __init__(self):
|
|
super().__init__(name="dummy")
|
|
|
|
|
|
def __repr__(self):
|
|
return "pygnulib.module.DummyModule"
|
|
|
|
|
|
|
|
class _GnulibModuleMeta(type):
|
|
def __new__(mcs, name, parents, attributes):
|
|
for key in BaseModule.keys():
|
|
if key in attributes:
|
|
continue
|
|
fget = lambda self, key=key: self.__getitem__(key)
|
|
doc = getattr(BaseModule, key).__doc__
|
|
attributes[key] = _Property(fget=fget, doc=doc)
|
|
return super().__new__(mcs, name, parents, attributes)
|
|
|
|
|
|
class GnulibModule(FileModule, metaclass=_GnulibModuleMeta):
|
|
"""read-only gnulib standard module"""
|
|
__slots__ = ("__cache", "__hash", "__mask", "__path", "__test")
|
|
|
|
|
|
__OBSOLETE = (1 << 0)
|
|
__CXX_TEST = (1 << 1)
|
|
__LONGRUNNING_TEST = (1 << 2)
|
|
__PRIVILEGED_TEST = (1 << 3)
|
|
__UNPORTABLE_TEST = (1 << 4)
|
|
|
|
|
|
def __init__(self, path, name):
|
|
super(FileModule, self).__init__(name=name)
|
|
try:
|
|
module = FileModule(path=path, name=name, test=name.endswith("-tests"))
|
|
except FileNotFoundError:
|
|
raise _UnknownModuleError(name)
|
|
self.__cache = {_sys.intern(k):v for (k,v) in module.items()}
|
|
self.__hash = super().__hash__()
|
|
self.__mask = module.mask
|
|
self.__path = module.path
|
|
self.__test = module.test
|
|
|
|
|
|
def __repr__(self):
|
|
return f"{self.name}"
|
|
module = self.__class__.__module__
|
|
name = self.__class__.__name__
|
|
return f"{module}.{name}{{{self.name}}}"
|
|
|
|
|
|
def __getitem__(self, key):
|
|
return self.__cache[key]
|
|
|
|
|
|
def __hash__(self):
|
|
return self.__hash
|
|
|
|
|
|
def __eq__(self, other):
|
|
if isinstance(other, GnulibModule):
|
|
return self.__hash == hash(other)
|
|
return super().__eq__(other)
|
|
|
|
|
|
obsolete = _Property(
|
|
fget=lambda self: bool(self.__mask & GnulibModule.__OBSOLETE),
|
|
doc="module is obsolete?",
|
|
)
|
|
cxx_test = _Property(
|
|
fget=lambda self: bool(self.__mask & GnulibModule.__CXX_TEST),
|
|
doc="module is a C++ test?",
|
|
)
|
|
longrunning_test = _Property(
|
|
fget=lambda self: bool(self.__mask & GnulibModule.__LONGRUNNING_TEST),
|
|
doc="module is a longrunning test?",
|
|
)
|
|
privileged_test = _Property(
|
|
fget=lambda self: bool(self.__mask & GnulibModule.__PRIVILEGED_TEST),
|
|
doc="module is a privileged test?",
|
|
)
|
|
unportable_test = _Property(
|
|
fget=lambda self: bool(self.__mask & GnulibModule.__UNPORTABLE_TEST),
|
|
doc="module is an unportable test?",
|
|
)
|
|
mask = _Property(
|
|
fget=lambda self: self.__mask,
|
|
doc="module acceptibility mask",
|
|
)
|
|
test = _Property(
|
|
fget=lambda self: self.name.endswith("-tests"),
|
|
doc="module is tests-related?",
|
|
)
|
|
path = _Property(
|
|
fget=lambda self: self.__path,
|
|
doc="module file path",
|
|
)
|
|
|
|
|
|
@_Property
|
|
def applicability(self):
|
|
"""WAGH applicability (usually "main" or "tests")"""
|
|
default = "tests" if self.test else "main"
|
|
current = self.__cache["applicability"]
|
|
return current if current else default
|
|
|
|
|
|
|
|
class TransitiveClosure:
|
|
"""transitive closure table"""
|
|
__slots__ = ("__lookup", "__dependencies", "__demanders", "__paths", "__conditional")
|
|
|
|
|
|
__AUTOMAKE_CONDITION = _re.compile("^if\\s+", _re.S | _re.M)
|
|
|
|
|
|
def __init__(self, lookup, modules, mask, gnumake, conditionals, tests=False, error=True):
|
|
if not callable(lookup):
|
|
raise TypeError("lookup: callable expected")
|
|
|
|
table = {None: None}
|
|
def _lookup(module):
|
|
return table.setdefault(module, lookup(module))
|
|
|
|
current = set()
|
|
previous = set()
|
|
demanders = _collections.defaultdict(dict)
|
|
dependencies = _collections.defaultdict(dict)
|
|
|
|
def _update(demander, dependency, condition):
|
|
table[dependency.name] = dependency
|
|
if dependency.mask == mask:
|
|
# A module whose Makefile.am snippet contains a reference to an
|
|
# automake conditional. If we were to use it conditionally, we
|
|
# would get an error
|
|
# configure: error: conditional "..." was never defined.
|
|
# because automake 1.11.1 does not handle nested conditionals
|
|
# correctly. As a workaround, make the module unconditional.
|
|
snippet = dependency.automake_snippet
|
|
pattern = TransitiveClosure.__AUTOMAKE_CONDITION
|
|
if condition and pattern.findall(snippet):
|
|
condition = None
|
|
demander = None
|
|
if not condition:
|
|
condition = None
|
|
if demander is not None:
|
|
demander = demander.name
|
|
dependency = dependency.name
|
|
demanders[demander][dependency] = condition
|
|
dependencies[dependency][demander] = condition
|
|
current.add(dependency)
|
|
|
|
for module in modules:
|
|
dependency = lookup(module)
|
|
_update(None, dependency, None)
|
|
|
|
while True:
|
|
modules = current.difference(previous)
|
|
if not modules:
|
|
break
|
|
previous.update(current)
|
|
for demander in modules:
|
|
demander = _lookup(demander)
|
|
if tests and not demander.test:
|
|
dependency = _lookup(demander.name + "-tests")
|
|
if dependency is not None:
|
|
_update(None, dependency, bool(demanders[demander]))
|
|
for (dependency, condition) in demander.dependencies:
|
|
dependency = _lookup(dependency)
|
|
_update(demander, dependency, condition)
|
|
|
|
self.__lookup = _lookup
|
|
self.__paths = dict()
|
|
self.__conditional = dict()
|
|
self.__demanders = dict(demanders)
|
|
self.__dependencies = dict(dependencies)
|
|
|
|
|
|
def __iter__(self):
|
|
for dependency in self.__dependencies:
|
|
yield self.__lookup(dependency)
|
|
|
|
|
|
def paths(self, module):
|
|
if module in self.__paths:
|
|
return self.__paths[module]
|
|
graph = self.__dependencies
|
|
module = self.__lookup(module).name
|
|
def _paths():
|
|
path = [module]
|
|
seen = {module}
|
|
def search():
|
|
dead_end = True
|
|
for neighbour in graph.get(path[-1], []):
|
|
if neighbour not in seen:
|
|
dead_end = False
|
|
seen.add(neighbour)
|
|
path.append(neighbour)
|
|
yield from search()
|
|
path.pop()
|
|
seen.remove(neighbour)
|
|
if dead_end:
|
|
yield path
|
|
yield from search()
|
|
self.__paths[module] = (tuple(path[:-1]) for path in _paths())
|
|
return tuple(self.__paths[module])
|
|
|
|
|
|
def conditional(self, module):
|
|
"""
|
|
Test whether module is a conditional dependency.
|
|
Note that this check also takes all parent modules into account.
|
|
"""
|
|
if module in self.__conditional:
|
|
return self.__conditional[module]
|
|
for path in self.paths(module):
|
|
unconditional = list()
|
|
for (dependency, demander) in zip(path, path[1:]):
|
|
unconditional.append(not self.__dependencies[dependency][demander])
|
|
if all(unconditional):
|
|
self.__conditional[module] = False
|
|
return False
|
|
self.__conditional[module] = True
|
|
return True
|
|
|
|
|
|
def dump(self, indent=" "):
|
|
"""Export transitive closure result into string."""
|
|
def _dump():
|
|
unconditional = set()
|
|
storage = _collections.defaultdict(dict)
|
|
yield "{{".format()
|
|
for (dependency, entries) in self.__dependencies.items():
|
|
for (demander, condition) in entries.items():
|
|
if condition is None:
|
|
condition = ""
|
|
if not demander and not condition:
|
|
unconditional.add(dependency)
|
|
condition = condition.replace("\"", "\\\"")
|
|
storage[dependency][demander] = condition
|
|
for dependency in sorted(storage):
|
|
if dependency in unconditional:
|
|
yield "{}\"{}\": {{}},".format(indent, dependency)
|
|
continue
|
|
yield "{}\"{}\": {{".format(indent, dependency)
|
|
for demander in sorted(storage[dependency]):
|
|
condition = storage[dependency][demander]
|
|
yield "{}\"{}\": \"{}\",".format((indent * 2), demander, condition)
|
|
yield "{}}},".format(indent)
|
|
yield "}}".format()
|
|
if not self.__dependencies:
|
|
return "{{}}".format()
|
|
return _os.linesep.join(_dump())
|
|
|
|
|
|
def load(self, string):
|
|
"""Import transitive closure result from string."""
|
|
demanders = _collections.defaultdict(dict)
|
|
dependencies = _collections.defaultdict(dict)
|
|
collection = _ast.literal_eval(string)
|
|
for key in collection:
|
|
value = collection[key]
|
|
for (subkey, subvalue) in value.items():
|
|
(dependency, demander, condition) = (key, subkey, subvalue)
|
|
if not condition:
|
|
condition = None
|
|
demanders[demander][dependency] = condition
|
|
dependencies[dependency][demander] = condition
|
|
self.__demanders = dict(demanders)
|
|
self.__dependencies = dict(dependencies)
|
|
|
|
|
|
def demanders(self, module):
|
|
"""For each demander which requires the module yield the demander and the corresponding condition."""
|
|
module = self.__lookup(module).name
|
|
if module in self.__dependencies:
|
|
for (demander, condition) in self.__dependencies.get(module, {}).items():
|
|
yield (self.__lookup(demander), condition)
|
|
|
|
|
|
def dependencies(self, module):
|
|
"""For each dependency of the module yield this dependency and the corresponding condition."""
|
|
module = self.__lookup(module).name
|
|
if module in self.__demanders:
|
|
for (dependency, condition) in self.__demanders.get(module, {}).items():
|
|
yield (self.__lookup(dependency), condition)
|
|
|
|
|
|
|
|
class Database:
|
|
"""gnulib module database"""
|
|
__DUMMY_PATTERN = _re.compile(r"^lib_SOURCES\s*\+\=\s*(.*?)$", _re.S | _re.M)
|
|
|
|
|
|
def __init__(self, lookup, config):
|
|
if not callable(lookup):
|
|
raise TypeError("lookup: callable expected")
|
|
|
|
mask = config.mask
|
|
gnumake = config.gnumake
|
|
lookup = lambda module, lookup=lookup: module if isinstance(module, BaseModule) else lookup(module)
|
|
|
|
def _applicability(module):
|
|
return (module.applicability == "main")
|
|
|
|
def _dummy(modules):
|
|
if "dummy" in config.avoids:
|
|
return False
|
|
for module in modules:
|
|
snippet = module.conditional_automake_snippet
|
|
for match in Database.__DUMMY_PATTERN.findall(snippet):
|
|
files = {file.strip() for file in match.split("#", 1)[0].split(" ") if file.strip()}
|
|
if {file for file in files if not file.endswith(".h")}:
|
|
return False
|
|
return True
|
|
|
|
def _files(modules):
|
|
files = set()
|
|
for module in modules:
|
|
files.update(module.files)
|
|
files.add("m4/00gnulib.m4")
|
|
files.add("m4/gnulib-common.m4")
|
|
if config.ac_version == "2.59":
|
|
files.add("m4/onceonly.m4")
|
|
return files
|
|
|
|
def _libtests(modules):
|
|
for module in modules:
|
|
for file in module.files:
|
|
if file.startswith("lib/"):
|
|
return True
|
|
return False
|
|
|
|
# Perform a transitive closure for modules from the configuration.
|
|
# The result of this transitive closure is a set of main modules.
|
|
conditionals = config.conditionals
|
|
modules = explicit_modules = {lookup(module) for module in config.modules}
|
|
base_closure = TransitiveClosure(lookup, modules, mask, gnumake, conditionals, False)
|
|
modules = map(lambda module: lookup(module.name + "-tests"), set(base_closure))
|
|
modules = set(filter(lambda module: module is not None, modules))
|
|
full_closure = TransitiveClosure(lookup, (explicit_modules | modules), mask, gnumake, conditionals, True)
|
|
|
|
# Once the full transitive closure is completed, populate the database.
|
|
main_modules = set(base_closure)
|
|
final_modules = set(full_closure) if config.tests else main_modules
|
|
test_modules = (final_modules - set(filter(_applicability, sorted(main_modules))))
|
|
nontrivial_tests = False
|
|
for module in test_modules:
|
|
if module.applicability != "all":
|
|
nontrivial_tests = True
|
|
break
|
|
if not nontrivial_tests:
|
|
test_modules = set()
|
|
libtests = _libtests(test_modules)
|
|
if _dummy(main_modules):
|
|
main_modules.add(DummyModule())
|
|
if _dummy(test_modules) and libtests:
|
|
test_modules.add(DummyModule())
|
|
main_files = _files(main_modules)
|
|
test_files = _files(test_modules)
|
|
|
|
self.__libtests = libtests
|
|
self.__closure = full_closure
|
|
self.__main_modules = tuple(sorted(main_modules))
|
|
self.__test_modules = tuple(sorted(test_modules))
|
|
self.__final_modules = tuple(sorted(final_modules))
|
|
self.__explicit_modules = tuple(sorted(explicit_modules))
|
|
self.__main_files = tuple(sorted(main_files))
|
|
self.__test_files = tuple(sorted(test_files))
|
|
|
|
|
|
def __iter__(self):
|
|
return iter(self.__closure)
|
|
|
|
|
|
def paths(self, module):
|
|
return self.__closure.paths(module)
|
|
|
|
|
|
def conditional(self, module):
|
|
"""
|
|
Test whether module is a conditional dependency.
|
|
Note that this check also takes all parent modules into account.
|
|
Any module with an unconditional demander is also unconditional.
|
|
"""
|
|
return self.__closure.conditional(module)
|
|
|
|
|
|
def unconditional(self, module):
|
|
"""
|
|
Test whether module is an unconditional dependency.
|
|
Note that this check also takes all parent modules into account.
|
|
Any module with an unconditional demander is also unconditional.
|
|
"""
|
|
return not self.conditional(module)
|
|
|
|
|
|
def demanders(self, module):
|
|
"""For each demander which requires the module yield the demander and the corresponding condition."""
|
|
return sorted(self.__closure.demanders(module))
|
|
|
|
|
|
def dependencies(self, module):
|
|
"""For each dependency of the module yield this dependency and the corresponding condition."""
|
|
return sorted(self.__closure.dependencies(module))
|
|
|
|
|
|
@property
|
|
def final_modules(self):
|
|
"""
|
|
The final module list is the transitive closure of the specified modules,
|
|
including or ignoring tests modules (depending on the tests configuration
|
|
option).
|
|
"""
|
|
return self.__final_modules
|
|
|
|
|
|
@property
|
|
def main_modules(self):
|
|
"""
|
|
The main module list is the transitive closure of the specified modules,
|
|
ignoring tests modules. Its lib/* sources go into {source_base}. If --lgpl
|
|
is specified, it will consist only of LGPLed source.
|
|
"""
|
|
return self.__main_modules
|
|
|
|
|
|
@property
|
|
def main_files(self):
|
|
"""The full set of the files required for modules in the main modules list."""
|
|
return self.__main_files
|
|
|
|
|
|
@property
|
|
def test_modules(self):
|
|
"""
|
|
The tests-related module list is the transitive closure of the specified
|
|
modules, including tests modules, minus the main module list excluding
|
|
modules of applicability 'all'. Its lib/* sources (brought in through
|
|
dependencies of *-tests modules) go into {tests_base}. It may contain GPLed
|
|
source, even if --lgpl is specified.
|
|
"""
|
|
return self.__test_modules
|
|
|
|
|
|
@property
|
|
def explicit_modules(self):
|
|
"""
|
|
The list of modules which were explicitly required to be imported.
|
|
This list does not include direct or indirect dependencies at all.
|
|
"""
|
|
return self.__explicit_modules
|
|
|
|
|
|
@property
|
|
def test_files(self):
|
|
"""The full set of the files required for modules in the test modules list."""
|
|
return self.__test_files
|
|
|
|
|
|
@property
|
|
def libtests(self):
|
|
"""If libtests.a is required, this variable yields true."""
|
|
return self.__libtests
|