Repository navigation
Expand file tree
/
Copy pathproduct-dependency-graph.py
More file actions
88 lines (80 loc) · 3.58 KB
/
Copy pathproduct-dependency-graph.py
File metadata and controls
88 lines (80 loc) · 3.58 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
import json, subprocess, collections, sys
from PIL import Image, ImageDraw, ImageFont
sys.setrecursionlimit(20000)
APPS = ['navop','navop-shell','navop-data']
meta = json.loads(subprocess.run(['cargo','metadata','--no-deps','--format-version','1'],capture_output=True,text=True).stdout)
ws = {p['name'] for p in meta['packages']}
def edges(product):
out = subprocess.run(['cargo','tree','-p',product,'-e','normal','--prefix','depth'],capture_output=True,text=True).stdout
stack, es = {}, set()
for line in out.splitlines():
if not line.strip(): continue
d=0
while d < len(line) and line[d].isdigit(): d+=1
depth=int(line[:d]) if d else 0
name=line[d:].strip().split(' v')[0].split(' ')[0]
stack[depth]=name
if depth>0 and name in ws and stack.get(depth-1) in ws:
es.add((stack[depth-1],name))
return es
per={p:edges(p) for p in APPS}
membership=collections.defaultdict(set)
for p in APPS:
for n in {x for e in per[p] for x in e}|{p}:
membership[n].add(p)
nodes=set(membership)
union=set().union(*per.values())
preds=collections.defaultdict(set)
for a,b in union:
preds[b].add(a)
state={}
def level(n, seen=()):
if n in state: return state[n]
if n in seen: return 0
if n in APPS: state[n]=0; return 0
ps=preds.get(n,set())
lv=1+max((level(p, seen+(n,)) for p in ps), default=-1) if ps else 0
state[n]=max(lv,0); return state[n]
for n in nodes: level(n)
levels=collections.defaultdict(list)
for n in nodes: levels[state[n]].append(n)
def color(n):
if n in APPS: return (62,74,92), (255,255,255)
m=membership[n]
if len(m)==3: return (203,232,203),(20,20,20)
if m=={'navop','navop-shell'}: return (255,213,160),(20,20,20)
if m=={'navop','navop-data'}: return (186,214,252),(20,20,20)
return (228,228,228),(20,20,20)
fp='/System/Library/Fonts/Supplemental/Arial Unicode.ttf'
F=lambda s: ImageFont.truetype(fp,s)
NW,NH,GX,GY,PAD=152,22,26,10,70
maxlvl=max(len(v) for v in levels.values())
W=PAD*2+maxlvl*(NW+GX)
H=PAD*2+(max(levels)+1)*(NH+GY)+110
img=Image.new('RGB',(W,H),(255,255,255)); d=ImageDraw.Draw(img)
mid={}
order_rank=lambda n:(0 if n in APPS else 1, {'all':0,'shell':1,'data':2,'full':3}[('all' if len(membership[n])==3 else 'shell' if membership[n]=={'navop','navop-shell'} else 'data' if membership[n]=={'navop','navop-data'} else 'full')], n)
for lv,ns in sorted(levels.items()):
ns=sorted(ns,key=order_rank)
tw=len(ns)*(NW+GX)-GX; x0=(W-tw)//2; y=PAD+lv*(NH+GY)
for i,n in enumerate(ns):
x=x0+i*(NW+GX); cx=x+NW/2; cy=y+NH/2
mid[n]=(cx,cy,x,y)
for a,b in union:
if a in mid and b in mid:
ax,ay,_,_=mid[a]; bx,by,_,_=mid[b]
d.line([(ax,ay+NH/2-3),(bx,by-NH/2+2)],fill=(206,206,206),width=1)
for n,(cx,cy,x,y) in mid.items():
fill,fg=color(n)
d.rounded_rectangle([x,y,x+NW,y+NH],radius=4,fill=fill,outline=(110,110,110))
d.text((x+5,y+5),n,font=F(12),fill=fg)
lx,ly=40,H-64
d.text((lx,ly-22),'navop 三产品依赖图(仅工作区内部 crate;node 颜色 = 出现在哪些产品的 cargo tree 中)',font=F(14),fill=(0,0,0))
import math
def swatch(x,y,c): d.rounded_rectangle([x,y,x+24,y+15],radius=3,fill=c,outline=(110,110,110))
legend=[('应用入口',(62,74,92)),('三产品共有',(203,232,203)),('全量 + 终端(shell)',(255,213,160)),('全量 + 数据(data)',(186,214,252)),('仅全量',(228,228,228))]
x=lx
for label,c in legend:
swatch(x,ly,c); d.text((x+32,ly),label,font=F(13),fill=(0,0,0)); x+=270
img.save("docs/design/navop-product-dependency-graph.png")
print('saved',W,H,'nodes',len(nodes),'edges',len(union))