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 <[email protected]>
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: