common/scripts: import xbps-cycles.
From https://github.com/ahesford/xbps-cycles, that is based on https://gist.github.com/Chocimier/de76441493ec7775c201dac0bb03ced5 . License is compatible with void-packages. Will be run in CI, so it should live in the same repository.
This commit is contained in:
parent
a72a885e01
commit
d0c9f994da
2 changed files with 120 additions and 0 deletions
18
common/scripts/README.xbps-cycles.md
Normal file
18
common/scripts/README.xbps-cycles.md
Normal file
|
@ -0,0 +1,18 @@
|
|||
# Cycle detector for void-packages
|
||||
|
||||
This script enumerates dependencies for packages in a
|
||||
[void-packages repository](https://github.com/void-linux/void-packages)
|
||||
and identifies build-time dependency cycles.
|
||||
|
||||
For command syntax, run `xbps-cycles.py -h`. Often, it may be sufficient to run
|
||||
`xbps-cycles.py` with no arguments. By default, the script will look for a
|
||||
repository at `$XBPS_DISTDIR`; if that variable is not defined, the current
|
||||
directory is used instead. To override this behavior, use the `-d` option to
|
||||
provide the path to your desired void-packages clone.
|
||||
|
||||
The standard behavior will be to spawn multiple processes, one per CPU, to
|
||||
enumerate package dependencies. This is by far the most time-consuming part of
|
||||
the execution. To override the degree of parallelism, use the `-j` option.
|
||||
|
||||
Failures should be harmless but, at this early stage, unlikely to be pretty or
|
||||
even helpful.
|
102
common/scripts/xbps-cycles.py
Executable file
102
common/scripts/xbps-cycles.py
Executable file
|
@ -0,0 +1,102 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
import sys
|
||||
import glob
|
||||
import subprocess
|
||||
import multiprocessing
|
||||
|
||||
from argparse import ArgumentParser
|
||||
|
||||
import networkx as nx
|
||||
|
||||
|
||||
def enum_depends(pkg, xbpsdir):
|
||||
'''
|
||||
Return a pair (pkg, [dependencies]), where [dependencies] is the list
|
||||
of dependencies for the given package pkg. The argument xbpsdir should
|
||||
be a path to a void-packages repository. Dependencies will be
|
||||
determined by invoking
|
||||
|
||||
<xbpsdir>/xbps-src show-build-deps <pkg>
|
||||
|
||||
If the return code of this call nonzero, a message will be printed but
|
||||
the package will treated as if it has no dependencies.
|
||||
'''
|
||||
cmd = [os.path.join(xbpsdir, 'xbps-src'), 'show-build-deps', pkg]
|
||||
|
||||
try:
|
||||
deps = subprocess.check_output(cmd)
|
||||
except subprocess.CalledProcessError as err:
|
||||
print('xbps-src failed to find dependencies for package', pkg)
|
||||
deps = [ ]
|
||||
else:
|
||||
deps = [d for d in deps.decode('utf-8').split('\n') if d]
|
||||
|
||||
return pkg, deps
|
||||
|
||||
|
||||
def find_cycles(depmap, xbpsdir):
|
||||
'''
|
||||
For a map depmap: package -> [dependencies], construct a directed graph
|
||||
and identify any cycles therein.
|
||||
|
||||
The argument xbpsdir should be a path to the root of a void-packages
|
||||
repository. All package names in depmap will be appended to the path
|
||||
<xbpsdir>/srcpkgs and reduced with os.path.realpath to coalesce
|
||||
subpackages.
|
||||
'''
|
||||
G = nx.DiGraph()
|
||||
|
||||
for i, deps in depmap.items():
|
||||
path = os.path.join(xbpsdir, 'srcpkgs', i)
|
||||
i = os.path.basename(os.path.realpath(path))
|
||||
|
||||
for j in deps:
|
||||
path = os.path.join(xbpsdir, 'srcpkgs', j.strip())
|
||||
j = os.path.basename(os.path.realpath(path))
|
||||
G.add_edge(i, j)
|
||||
|
||||
for c in nx.strongly_connected_components(G):
|
||||
if len(c) < 2: continue
|
||||
pkgs = nx.to_dict_of_lists(G, c)
|
||||
|
||||
p = next(iter(pkgs.keys()))
|
||||
cycles = [ ]
|
||||
while True:
|
||||
cycles.append(p)
|
||||
|
||||
# Cycle is complete when package is not in map
|
||||
try: deps = pkgs.pop(p)
|
||||
except KeyError: break
|
||||
|
||||
# Any of the dependencies here contributes to a cycle
|
||||
p = deps[0]
|
||||
if len(deps) > 1:
|
||||
print('Mulitpath: {} -> {}, choosing first'.format(p, deps))
|
||||
|
||||
if cycles:
|
||||
print('Cycle: ' + ' -> '.join(cycles) + '\n')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = ArgumentParser(description='Cycle detector for xbps-src')
|
||||
parser.add_argument('-j', '--jobs', default=None,
|
||||
type=int, help='Number of parallel jobs')
|
||||
parser.add_argument('-d', '--directory',
|
||||
default=None, help='Path to void-packages repo')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.directory:
|
||||
try: args.directory = os.environ['XBPS_DISTDIR']
|
||||
except KeyError: args.directory = '.'
|
||||
|
||||
pool = multiprocessing.Pool(processes = args.jobs)
|
||||
|
||||
pattern = os.path.join(args.directory, 'srcpkgs', '*')
|
||||
depmap = dict(pool.starmap(enum_depends,
|
||||
((os.path.basename(g), args.directory)
|
||||
for g in glob.iglob(pattern))))
|
||||
|
||||
find_cycles(depmap, args.directory)
|
Loading…
Reference in a new issue