OpenMiInk/third_party/krita-deps-management/tools/generate-graphvis-file.py
Hellings b8c8ff98e2 Hi
2026-10-16 09:05:56 -04:00

170 lines
6 KiB
Python

#!/usr/bin/python3
import os
import sys
import yaml
import argparse
sys.path.append(os.path.join(os.path.dirname(__file__), '..', 'ci-utilities'))
from components import CommonUtils, Dependencies, PlatformFlavor
from components.CiConfigurationUtils import *
import graphviz
# Capture our command line parameters
parser = argparse.ArgumentParser(description='Utility to run builds for multiple projects and their dependencies on CI')
parser.add_argument('--branch', type=str, required=True)
parser.add_argument('--seed-file', type=str, required=True)
parser.add_argument('-s','--skip-deps', nargs='+', help='A space-separated list of dependencies to skip from the graph (\'ext_foo\' format); when combined with --only-deps removes deps from the --only-deps subset', required=False)
parser.add_argument('-d','--only-deps', nargs='+', help='A space-separated list of dependencies to include into the graph, all other deps are ignored (\'ext_foo\' format)', required=False)
parser.add_argument('-o','--output', type=str, help='Name of the output file', required=False, default='dependencies.png')
arguments = parser.parse_args()
if not arguments.skip_deps is None and \
not arguments.only_deps is None:
skipSet = set(arguments.skip_deps)
onlySet = set(arguments.only_deps)
intersection = skipSet & onlySet
if intersection:
print ('ERROR: --skip-deps and --only-deps sets of dependencies intersect: {}'.format(intersection))
sys.exit(1)
# This is a giant speedup on @same dependency lookup, especially on Windows
if not 'CI_MERGE_REQUEST_TARGET_BRANCH_NAME' in os.environ:
os.environ['CI_MERGE_REQUEST_TARGET_BRANCH_NAME'] = arguments.branch
workingDirectory = os.getcwd()
linuxPlatform = PlatformFlavor.PlatformFlavor('Linux')
linuxDependencyResolver = prepareDependenciesResolver(linuxPlatform)
linuxReverseDeps = genReverseDeps(workingDirectory, linuxDependencyResolver, arguments.branch)
windowsPlatform = PlatformFlavor.PlatformFlavor('Windows')
windowsDependencyResolver = prepareDependenciesResolver(windowsPlatform)
windowsReverseDeps = genReverseDeps(workingDirectory, windowsDependencyResolver, arguments.branch)
onlyLegengLine = ''
skipLegendLine = ''
if not arguments.only_deps is None:
onlyLegengLine = '<tr><td colspan=\"3\">Only: {}</td></tr>'.format(', '.join(arguments.only_deps))
if not arguments.skip_deps is None:
skipLegendLine = '<tr><td colspan=\"3\">Skip: {}</td></tr>'.format(', '.join(arguments.skip_deps))
dot = graphviz.Digraph(comment='Krita Dependencies', engine = 'neato')
dot.attr(overlap='false')
dot.attr(splines='true')
dot.attr(root='ext_qt')
dot.attr(pack='true')
dot.attr(packMode='clust')
dot.attr(labelloc='t')
dot.attr(labeljust='l')
dot.attr(label='<\
<table>\
<tr><td colspan=\"3\">Krita dependencies</td></tr>\
<tr>\
<td><font color=\"black\">All</font></td>\
<td><font color=\"magenta\">Linux</font></td>\
<td><font color=\"blue\">Windows</font></td>\
</tr>\
{}{}\
</table>>'.format(onlyLegengLine, skipLegendLine))
knownNodes = {}
knownEdges = {}
seedFile = arguments.seed_file
if not os.path.isabs(seedFile) and not os.path.exists(seedFile):
seedFile = os.path.abspath(os.path.join(workingDirectory, seedFile))
dependencies = []
with open(seedFile, 'r') as f:
dependencies = yaml.safe_load(f)
for rule in dependencies:
if '@all' in rule['on']:
for dep in rule['require'].keys():
projectId = linuxDependencyResolver.projects[dep]['identifier']
knownNodes[projectId] = 'black'
if 'Linux' in rule['on']:
for dep in rule['require'].keys():
projectId = linuxDependencyResolver.projects[dep]['identifier']
if projectId in knownNodes:
knownNodes[projectId] = 'black'
else:
knownNodes[projectId] = 'magenta'
if 'Windows' in rule['on']:
for dep in rule['require'].keys():
projectId = linuxDependencyResolver.projects[dep]['identifier']
if projectId in knownNodes:
knownNodes[projectId] = 'black'
else:
knownNodes[projectId] = 'blue'
def addNodeWithSanityCheck(projectName):
if not projectName in knownNodes:
knownNodes[projectName] = 'red'
for projectName, dependants in linuxReverseDeps.items():
addNodeWithSanityCheck(projectName)
for dep in dependants:
addNodeWithSanityCheck(dep)
knownEdges[(projectName, dep)] = 'magenta'
for projectName, dependants in windowsReverseDeps.items():
addNodeWithSanityCheck(projectName)
for dep in dependants:
addNodeWithSanityCheck(dep)
color = 'blue'
if (projectName, dep) in knownEdges:
color = 'black'
knownEdges[(projectName, dep)] = color
skippedProjects = set(arguments.skip_deps if not arguments.skip_deps is None else [])
onlyProjects = set(arguments.only_deps if not arguments.only_deps is None else [])
def allowDep(projectId):
if skippedProjects and projectId in skippedProjects:
return False
if onlyProjects:
if projectId in onlyProjects:
return True
edge = next(((a,b) for a, b in knownEdges.keys()
if
(a == projectId and b in onlyProjects) or
(b == projectId and a in onlyProjects)),
None)
return not edge is None
return True
for projectName, color in knownNodes.items():
if not allowDep(projectName):
continue
dot.node(projectName, color = color)
for (projectName, dep), color in knownEdges.items():
if not allowDep(projectName) or not allowDep(dep):
continue
# print ('{} -> {}: {}'.format(projectName, dep, color))
dot.edge(projectName, dep, color = color)
# print(dot.source)
outputFile = arguments.output
if not os.path.isabs(outputFile):
outputFile = os.path.abspath(os.path.join(workingDirectory, outputFile))
dot.render(outfile=outputFile, cleanup=True).replace('\\', '/')