Paul Eggert wrote: > I was testing all modules that depend on c99, this way: > > ./gnulib-tool -h --create-testdir --dir foo $(cd modules && grep -lx > 'c99' $(git ls-files))
The libunistring maintenance also needs a way to find all modules that depend on a particular module. I've missed a corresponding gnulib-tool option for a long time. This patch adds it. So that you can now write ./gnulib-tool -h --create-testdir --dir foo $(./gnulib-tool --extract-dependents c99) 2024-07-28 Bruno Haible <br...@clisp.org> gnulib-tool.py: New options --extract-[recursive-]dependents. * pygnulib/GLInfo.py (GLInfo.usage): Document --extract-dependents and --extract-recursive-dependents options. * pygnulib/GLModuleSystem.py (GLModule.getDependenciesRecursively): Move method. (getDependents, getDependentsRecursively): New methods. * pygnulib/main.py (main): Add support for --extract-dependents and --extract-recursive-dependents. diff --git a/pygnulib/GLInfo.py b/pygnulib/GLInfo.py index 9ed3ea61d4..83659b1c1f 100644 --- a/pygnulib/GLInfo.py +++ b/pygnulib/GLInfo.py @@ -134,6 +134,8 @@ def usage(self) -> str: gnulib-tool --extract-filelist module gnulib-tool --extract-dependencies module gnulib-tool --extract-recursive-dependencies module + gnulib-tool --extract-dependents module + gnulib-tool --extract-recursive-dependents module gnulib-tool --extract-autoconf-snippet module gnulib-tool --extract-automake-snippet module gnulib-tool --extract-include-directive module @@ -175,6 +177,11 @@ def usage(self) -> str: --extract-recursive-dependencies extract the dependencies of the module and its dependencies, recursively, all together, but without the conditions + --extract-dependents list the modules which depend on the given + module directly. This is also known as the + "reverse dependencies". + --extract-recursive-dependents list the modules which depend on the given + module directly or indirectly --extract-autoconf-snippet extract the snippet for configure.ac --extract-automake-snippet extract the snippet for library makefile --extract-include-directive extract the #include directive diff --git a/pygnulib/GLModuleSystem.py b/pygnulib/GLModuleSystem.py index 248d9ec800..e713b60fc9 100644 --- a/pygnulib/GLModuleSystem.py +++ b/pygnulib/GLModuleSystem.py @@ -23,6 +23,7 @@ import sys import hashlib import subprocess as sp +import shlex from collections import defaultdict from typing import Any, ClassVar from .constants import ( @@ -330,33 +331,6 @@ def repeatModuleInTests(self) -> bool: result = self.name == 'libtextstyle-optional' return result - def getDependenciesRecursively(self) -> str: - '''Return a list of recursive dependencies of this module separated - by a newline.''' - handledmodules = set() - inmodules = set() - outmodules = set() - - # In order to process every module only once (for speed), process an "input - # list" of modules, producing an "output list" of modules. During each round, - # more modules can be queued in the input list. Once a module on the input - # list has been processed, it is added to the "handled list", so we can avoid - # to process it again. - inmodules.add(self) - while len(inmodules) > 0: - inmodules_this_round = inmodules - inmodules = set() # Accumulator, queue for next round - for module in inmodules_this_round: - outmodules.add(module) - inmodules = inmodules.union(module.getDependenciesWithoutConditions()) - handledmodules = handledmodules.union(inmodules_this_round) - # Remove handledmodules from inmodules. - inmodules = inmodules.difference(handledmodules) - - module_names = sorted([ module.name - for module in outmodules ]) - return lines_to_multiline(module_names) - def getLinkDirectiveRecursively(self) -> str: '''Return a list of the link directives of this module separated by a newline.''' @@ -554,6 +528,107 @@ def getDependenciesWithConditions(self) -> list[tuple[GLModule, str | None]]: self.cache['dependenciesWithCond'] = result return self.cache['dependenciesWithCond'] + def getDependenciesRecursively(self) -> str: + '''Return a list of recursive dependencies of this module separated + by a newline.''' + handledmodules = set() + inmodules = set() + outmodules = set() + + # In order to process every module only once (for speed), process an "input + # list" of modules, producing an "output list" of modules. During each round, + # more modules can be queued in the input list. Once a module on the input + # list has been processed, it is added to the "handled list", so we can avoid + # to process it again. + inmodules.add(self) + while len(inmodules) > 0: + inmodules_this_round = inmodules + inmodules = set() # Accumulator, queue for next round + for module in inmodules_this_round: + outmodules.add(module) + inmodules = inmodules.union(module.getDependenciesWithoutConditions()) + handledmodules = handledmodules.union(inmodules_this_round) + # Remove handledmodules from inmodules. + inmodules = inmodules.difference(handledmodules) + + module_names = sorted([ module.name + for module in outmodules ]) + return lines_to_multiline(module_names) + + def getDependents(self) -> list[GLModule]: + '''Return list of dependents (a.k.a. "reverse dependencies"), + as a list of GLModule objects. + GLConfig: localpath.''' + if 'dependents' not in self.cache: + localpath = self.config['localpath'] + # Find a set of module candidates quickly. + # TODO: Optimize. This approach is fine for a single getDependents + # invocation, but not for 100 or 1000 of them. + # Convert the module name to a POSIX basic regex. + # Needs to handle . [ \ * ^ $. + regex = self.name.replace('\\', '\\\\').replace('[', '\\[').replace('^', '\\^') + regex = re.compile(r'([.*$])').sub(r'[\1]', regex) + line_regex = '^' + regex + # We can't add a '$' to line_regex, because that would fail to match + # lines that denote conditional dependencies. We could invoke grep + # twice, once to search for line_regex + '$' and once to search + # for line_regex + [ <TAB>] but that would be twice as slow. + # Read module candidates from gnulib root directory. + command = "find modules -type f -print | xargs -n 100 grep -l %s /dev/null | sed -e 's,^modules/,,'" % shlex.quote(line_regex) + with sp.Popen(command, shell=True, cwd=DIRS['root'], stdout=sp.PIPE) as proc: + result = proc.stdout.read().decode('UTF-8') + # Read module candidates from local directories. + if localpath != None and len(localpath) > 0: + command = "find modules -type f -print | xargs -n 100 grep -l %s /dev/null | sed -e 's,^modules/,,' -e 's,\\.diff$,,'" % shlex.quote(line_regex) + for localdir in localpath: + with sp.Popen(command, shell=True, cwd=localdir, stdout=sp.PIPE) as proc: + result += proc.stdout.read().decode('UTF-8') + listing = [ line + for line in result.split('\n') + if line.strip() ] + # Remove modules/ prefix from each file name. + pattern = re.compile(r'^modules/') + listing = [ pattern.sub('', line) + for line in listing ] + # Filter out undesired file names. + listing = [ line + for line in listing + if self.modulesystem.file_is_module(line) ] + candidates = sorted(set(listing)) + result = [] + for name in candidates: + module = self.modulesystem.find(name) + if module: # Ignore module candidates that don't actually exist. + if self in module.getDependenciesWithoutConditions(): + result.append(module) + self.cache['dependents'] = result + return self.cache['dependents'] + + def getDependentsRecursively(self) -> str: + '''Return a list of recursive dependents of this module, + as a list of GLModule objects.''' + handledmodules = set() + inmodules = set() + outmodules = set() + + # In order to process every module only once (for speed), process an "input + # list" of modules, producing an "output list" of modules. During each round, + # more modules can be queued in the input list. Once a module on the input + # list has been processed, it is added to the "handled list", so we can avoid + # to process it again. + inmodules.add(self) + while len(inmodules) > 0: + inmodules_this_round = inmodules + inmodules = set() # Accumulator, queue for next round + for module in inmodules_this_round: + outmodules.add(module) + inmodules = inmodules.union(module.getDependents()) + handledmodules = handledmodules.union(inmodules_this_round) + # Remove handledmodules from inmodules. + inmodules = inmodules.difference(handledmodules) + + return outmodules + def getAutoconfEarlySnippet(self) -> str: '''Return autoconf-early snippet.''' return self.sections.get('configure.ac-early', '') diff --git a/pygnulib/main.py b/pygnulib/main.py index 6953a84917..dbff9dc76a 100644 --- a/pygnulib/main.py +++ b/pygnulib/main.py @@ -213,6 +213,14 @@ def main(temp_directory: str) -> None: dest='mode_xrecursive_dependencies', default=None, action='store_true') + parser.add_argument('--extract-dependents', + dest='mode_xdependents', + default=None, + action='store_true') + parser.add_argument('--extract-recursive-dependents', + dest='mode_xrecursive_dependents', + default=None, + action='store_true') parser.add_argument('--extract-autoconf-snippet', dest='mode_xautoconf', default=None, @@ -558,6 +566,9 @@ def main(temp_directory: str) -> None: cmdargs.mode_xusability_in_testdir, cmdargs.mode_xfilelist, cmdargs.mode_xdependencies, + cmdargs.mode_xrecursive_dependencies, + cmdargs.mode_xdependents, + cmdargs.mode_xrecursive_dependents, cmdargs.mode_xautoconf, cmdargs.mode_xautomake, cmdargs.mode_xinclude, @@ -648,6 +659,12 @@ def main(temp_directory: str) -> None: if cmdargs.mode_xrecursive_dependencies != None: mode = 'extract-recursive-dependencies' modules = list(cmdargs.non_option_arguments) + if cmdargs.mode_xdependents != None: + mode = 'extract-dependents' + modules = list(cmdargs.non_option_arguments) + if cmdargs.mode_xrecursive_dependents != None: + mode = 'extract-recursive-dependents' + modules = list(cmdargs.non_option_arguments) if cmdargs.mode_xinclude != None: mode = 'extract-include-directive' modules = list(cmdargs.non_option_arguments) @@ -1241,6 +1258,38 @@ def main(temp_directory: str) -> None: if module: sys.stdout.write(module.getDependenciesRecursively()) + elif mode == 'extract-dependents': + if avoids: + message = '%s: *** ' % APP['name'] + message += 'cannot combine --avoid and --extract-dependents\n' + message += '%s: *** Stop.\n' % APP['name'] + sys.stderr.write(message) + sys.exit(1) + modulesystem = GLModuleSystem(config) + for name in modules: + module = modulesystem.find(name) + if module: + dependents = module.getDependents() + dependents_names = sorted([ m.name + for m in dependents ]) + sys.stdout.write(lines_to_multiline(dependents_names)) + + elif mode == 'extract-recursive-dependents': + if avoids: + message = '%s: *** ' % APP['name'] + message += 'cannot combine --avoid and --extract-recursive-dependents\n' + message += '%s: *** Stop.\n' % APP['name'] + sys.stderr.write(message) + sys.exit(1) + modulesystem = GLModuleSystem(config) + for name in modules: + module = modulesystem.find(name) + if module: + dependents = module.getDependentsRecursively() + dependents_names = sorted([ m.name + for m in dependents ]) + sys.stdout.write(lines_to_multiline(dependents_names)) + elif mode == 'extract-autoconf-snippet': modulesystem = GLModuleSystem(config) for name in modules: