bitp 1.0.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bitbake_project/__init__.py +88 -0
- bitbake_project/__main__.py +14 -0
- bitbake_project/cli.py +1580 -0
- bitbake_project/commands/__init__.py +60 -0
- bitbake_project/commands/branch.py +889 -0
- bitbake_project/commands/common.py +2372 -0
- bitbake_project/commands/config.py +1515 -0
- bitbake_project/commands/deps.py +903 -0
- bitbake_project/commands/explore.py +2269 -0
- bitbake_project/commands/export.py +1030 -0
- bitbake_project/commands/fragment.py +884 -0
- bitbake_project/commands/init.py +515 -0
- bitbake_project/commands/projects.py +1505 -0
- bitbake_project/commands/recipe.py +1374 -0
- bitbake_project/commands/repos.py +154 -0
- bitbake_project/commands/search.py +313 -0
- bitbake_project/commands/update.py +181 -0
- bitbake_project/core.py +1811 -0
- bitp-1.0.7.dist-info/METADATA +401 -0
- bitp-1.0.7.dist-info/RECORD +24 -0
- bitp-1.0.7.dist-info/WHEEL +5 -0
- bitp-1.0.7.dist-info/entry_points.txt +3 -0
- bitp-1.0.7.dist-info/licenses/COPYING +338 -0
- bitp-1.0.7.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,903 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright (C) 2025 Bruce Ashfield <bruce.ashfield@gmail.com>
|
|
3
|
+
#
|
|
4
|
+
# SPDX-License-Identifier: GPL-2.0-only
|
|
5
|
+
#
|
|
6
|
+
"""Deps command - show layer and recipe dependencies."""
|
|
7
|
+
|
|
8
|
+
import glob
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import re
|
|
12
|
+
import shutil
|
|
13
|
+
import subprocess
|
|
14
|
+
import tempfile
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from typing import Dict, List, Optional, Set, Tuple
|
|
17
|
+
|
|
18
|
+
from ..core import Colors, fzf_available, get_fzf_color_args, get_fzf_preview_resize_bindings
|
|
19
|
+
from .common import resolve_bblayers_path, extract_layer_paths, parse_layer_conf, LayerInfo
|
|
20
|
+
from .projects import get_preview_window_arg, get_preferred_graph_renderer, set_graph_renderer
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# =============================================================================
|
|
24
|
+
# Layer Dependency Graph
|
|
25
|
+
# =============================================================================
|
|
26
|
+
|
|
27
|
+
def build_layer_dependency_graph(layer_paths: List[str]) -> Dict[str, LayerInfo]:
|
|
28
|
+
"""
|
|
29
|
+
Build a dependency graph from all layer paths.
|
|
30
|
+
|
|
31
|
+
Returns a dict mapping collection name -> LayerInfo for all parsed layers.
|
|
32
|
+
"""
|
|
33
|
+
layers: Dict[str, LayerInfo] = {}
|
|
34
|
+
|
|
35
|
+
for layer_path in layer_paths:
|
|
36
|
+
info = parse_layer_conf(layer_path)
|
|
37
|
+
if info:
|
|
38
|
+
# Use collection name as key, but handle duplicates
|
|
39
|
+
key = info.name
|
|
40
|
+
if key in layers:
|
|
41
|
+
# Prefer layer with higher priority, or first one found
|
|
42
|
+
if info.priority <= layers[key].priority:
|
|
43
|
+
continue
|
|
44
|
+
layers[key] = info
|
|
45
|
+
|
|
46
|
+
return layers
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def get_forward_deps(layers: Dict[str, LayerInfo], layer_name: str) -> Set[str]:
|
|
50
|
+
"""Get all dependencies of a layer (what it depends on)."""
|
|
51
|
+
if layer_name not in layers:
|
|
52
|
+
return set()
|
|
53
|
+
info = layers[layer_name]
|
|
54
|
+
return set(info.depends)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def get_reverse_deps(layers: Dict[str, LayerInfo], layer_name: str) -> Set[str]:
|
|
58
|
+
"""Get all layers that depend on this layer (reverse dependencies)."""
|
|
59
|
+
reverse = set()
|
|
60
|
+
for name, info in layers.items():
|
|
61
|
+
if layer_name in info.depends:
|
|
62
|
+
reverse.add(name)
|
|
63
|
+
return reverse
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def get_all_deps_recursive(
|
|
67
|
+
layers: Dict[str, LayerInfo],
|
|
68
|
+
layer_name: str,
|
|
69
|
+
reverse: bool = False,
|
|
70
|
+
visited: Optional[Set[str]] = None,
|
|
71
|
+
) -> Dict[str, Set[str]]:
|
|
72
|
+
"""
|
|
73
|
+
Get full dependency tree recursively.
|
|
74
|
+
|
|
75
|
+
Returns dict mapping each layer to its direct dependencies (or dependents if reverse).
|
|
76
|
+
"""
|
|
77
|
+
if visited is None:
|
|
78
|
+
visited = set()
|
|
79
|
+
|
|
80
|
+
result: Dict[str, Set[str]] = {}
|
|
81
|
+
|
|
82
|
+
if layer_name in visited:
|
|
83
|
+
return result
|
|
84
|
+
visited.add(layer_name)
|
|
85
|
+
|
|
86
|
+
if reverse:
|
|
87
|
+
deps = get_reverse_deps(layers, layer_name)
|
|
88
|
+
else:
|
|
89
|
+
deps = get_forward_deps(layers, layer_name)
|
|
90
|
+
|
|
91
|
+
result[layer_name] = deps
|
|
92
|
+
|
|
93
|
+
for dep in deps:
|
|
94
|
+
sub_tree = get_all_deps_recursive(layers, dep, reverse, visited)
|
|
95
|
+
result.update(sub_tree)
|
|
96
|
+
|
|
97
|
+
return result
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# =============================================================================
|
|
101
|
+
# Tree Rendering
|
|
102
|
+
# =============================================================================
|
|
103
|
+
|
|
104
|
+
def _render_tree_recursive(
|
|
105
|
+
name: str,
|
|
106
|
+
graph: Dict[str, Set[str]],
|
|
107
|
+
visited: Set[str],
|
|
108
|
+
prefix: str = "",
|
|
109
|
+
is_last: bool = True,
|
|
110
|
+
is_root: bool = False,
|
|
111
|
+
) -> List[str]:
|
|
112
|
+
"""Recursive tree rendering with box-drawing characters."""
|
|
113
|
+
lines = []
|
|
114
|
+
|
|
115
|
+
if is_root:
|
|
116
|
+
lines.append(f"{Colors.bold(name)}")
|
|
117
|
+
else:
|
|
118
|
+
connector = "\u2514\u2500\u2500 " if is_last else "\u251c\u2500\u2500 "
|
|
119
|
+
lines.append(f"{prefix}{connector}{name}")
|
|
120
|
+
|
|
121
|
+
if name in visited:
|
|
122
|
+
return lines
|
|
123
|
+
visited.add(name)
|
|
124
|
+
|
|
125
|
+
children = sorted(graph.get(name, []))
|
|
126
|
+
if is_root:
|
|
127
|
+
child_prefix = ""
|
|
128
|
+
else:
|
|
129
|
+
child_prefix = prefix + (" " if is_last else "\u2502 ")
|
|
130
|
+
|
|
131
|
+
for i, child in enumerate(children):
|
|
132
|
+
is_last_child = (i == len(children) - 1)
|
|
133
|
+
if child not in visited:
|
|
134
|
+
lines.extend(_render_tree_recursive(
|
|
135
|
+
child, graph, visited, child_prefix, is_last_child
|
|
136
|
+
))
|
|
137
|
+
else:
|
|
138
|
+
# Already visited - show with marker
|
|
139
|
+
connector = "\u2514\u2500\u2500 " if is_last_child else "\u251c\u2500\u2500 "
|
|
140
|
+
lines.append(f"{child_prefix}{connector}{child} {Colors.dim('(see above)')}")
|
|
141
|
+
|
|
142
|
+
return lines
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def render_tree_ascii(
|
|
146
|
+
layers: Dict[str, LayerInfo],
|
|
147
|
+
root: str,
|
|
148
|
+
reverse: bool = False,
|
|
149
|
+
) -> str:
|
|
150
|
+
"""Render dependency tree as ASCII using box-drawing characters."""
|
|
151
|
+
graph = get_all_deps_recursive(layers, root, reverse)
|
|
152
|
+
visited: Set[str] = set()
|
|
153
|
+
lines = _render_tree_recursive(root, graph, visited, is_root=True)
|
|
154
|
+
return "\n".join(lines)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def render_tree_grapheasy(
|
|
158
|
+
layers: Dict[str, LayerInfo],
|
|
159
|
+
root: str,
|
|
160
|
+
reverse: bool = False,
|
|
161
|
+
) -> Optional[str]:
|
|
162
|
+
"""Use graph-easy for better ASCII rendering if available."""
|
|
163
|
+
if not shutil.which("graph-easy"):
|
|
164
|
+
return None
|
|
165
|
+
|
|
166
|
+
graph = get_all_deps_recursive(layers, root, reverse)
|
|
167
|
+
|
|
168
|
+
# Build DOT format
|
|
169
|
+
dot_lines = ["digraph deps {", ' rankdir=TB;', ' node [shape=box];']
|
|
170
|
+
|
|
171
|
+
# Highlight root node
|
|
172
|
+
dot_lines.append(f' "{root}" [style=bold];')
|
|
173
|
+
|
|
174
|
+
for node, deps in graph.items():
|
|
175
|
+
for dep in deps:
|
|
176
|
+
if reverse:
|
|
177
|
+
# Reverse: dep depends on node, so arrow from dep to node
|
|
178
|
+
dot_lines.append(f' "{dep}" -> "{node}";')
|
|
179
|
+
else:
|
|
180
|
+
# Forward: node depends on dep, so arrow from node to dep
|
|
181
|
+
dot_lines.append(f' "{node}" -> "{dep}";')
|
|
182
|
+
dot_lines.append("}")
|
|
183
|
+
|
|
184
|
+
dot_content = "\n".join(dot_lines)
|
|
185
|
+
|
|
186
|
+
try:
|
|
187
|
+
result = subprocess.run(
|
|
188
|
+
["graph-easy", "--as_ascii"],
|
|
189
|
+
input=dot_content,
|
|
190
|
+
capture_output=True,
|
|
191
|
+
text=True,
|
|
192
|
+
timeout=10,
|
|
193
|
+
)
|
|
194
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
195
|
+
return result.stdout
|
|
196
|
+
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
|
|
197
|
+
pass
|
|
198
|
+
|
|
199
|
+
return None
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def render_tree_preferred(
|
|
203
|
+
layers: Dict[str, LayerInfo],
|
|
204
|
+
root: str,
|
|
205
|
+
reverse: bool = False,
|
|
206
|
+
) -> str:
|
|
207
|
+
"""Render tree using the configured graph renderer preference."""
|
|
208
|
+
preference = get_preferred_graph_renderer()
|
|
209
|
+
|
|
210
|
+
if preference == "graph-easy":
|
|
211
|
+
ge_output = render_tree_grapheasy(layers, root, reverse)
|
|
212
|
+
if ge_output:
|
|
213
|
+
return ge_output
|
|
214
|
+
# Fall back to ASCII if graph-easy fails
|
|
215
|
+
return render_tree_ascii(layers, root, reverse)
|
|
216
|
+
else:
|
|
217
|
+
# ASCII or unknown preference
|
|
218
|
+
return render_tree_ascii(layers, root, reverse)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def render_dot(
|
|
222
|
+
layers: Dict[str, LayerInfo],
|
|
223
|
+
root: Optional[str] = None,
|
|
224
|
+
reverse: bool = False,
|
|
225
|
+
) -> str:
|
|
226
|
+
"""Output DOT format for external tools like graphviz."""
|
|
227
|
+
dot_lines = [
|
|
228
|
+
"digraph layer_deps {",
|
|
229
|
+
' rankdir=TB;',
|
|
230
|
+
' node [shape=box, fontname="sans-serif"];',
|
|
231
|
+
' edge [fontname="sans-serif"];',
|
|
232
|
+
]
|
|
233
|
+
|
|
234
|
+
if root:
|
|
235
|
+
graph = get_all_deps_recursive(layers, root, reverse)
|
|
236
|
+
dot_lines.append(f' "{root}" [style=bold, color=blue];')
|
|
237
|
+
else:
|
|
238
|
+
# Full graph of all layers
|
|
239
|
+
graph = {name: set(info.depends) for name, info in layers.items()}
|
|
240
|
+
|
|
241
|
+
for node, deps in graph.items():
|
|
242
|
+
for dep in deps:
|
|
243
|
+
if reverse:
|
|
244
|
+
dot_lines.append(f' "{dep}" -> "{node}";')
|
|
245
|
+
else:
|
|
246
|
+
dot_lines.append(f' "{node}" -> "{dep}";')
|
|
247
|
+
|
|
248
|
+
dot_lines.append("}")
|
|
249
|
+
return "\n".join(dot_lines)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def render_list(
|
|
253
|
+
layers: Dict[str, LayerInfo],
|
|
254
|
+
root: Optional[str] = None,
|
|
255
|
+
reverse: bool = False,
|
|
256
|
+
) -> str:
|
|
257
|
+
"""Simple list format output."""
|
|
258
|
+
lines = []
|
|
259
|
+
|
|
260
|
+
if root:
|
|
261
|
+
graph = get_all_deps_recursive(layers, root, reverse)
|
|
262
|
+
direction = "depends on" if not reverse else "is required by"
|
|
263
|
+
lines.append(f"{root} {direction}:")
|
|
264
|
+
deps = graph.get(root, set())
|
|
265
|
+
for dep in sorted(deps):
|
|
266
|
+
lines.append(f" {dep}")
|
|
267
|
+
else:
|
|
268
|
+
# List all layers with their dependencies
|
|
269
|
+
for name in sorted(layers.keys()):
|
|
270
|
+
info = layers[name]
|
|
271
|
+
if info.depends:
|
|
272
|
+
lines.append(f"{name}: {', '.join(info.depends)}")
|
|
273
|
+
else:
|
|
274
|
+
lines.append(f"{name}: (no dependencies)")
|
|
275
|
+
|
|
276
|
+
return "\n".join(lines)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
# =============================================================================
|
|
280
|
+
# FZF Browser
|
|
281
|
+
# =============================================================================
|
|
282
|
+
|
|
283
|
+
def _build_layer_menu(layers: Dict[str, LayerInfo]) -> str:
|
|
284
|
+
"""Build fzf menu input for layer browser."""
|
|
285
|
+
lines = []
|
|
286
|
+
for name in sorted(layers.keys()):
|
|
287
|
+
info = layers[name]
|
|
288
|
+
dep_count = len(info.depends)
|
|
289
|
+
rec_count = len(info.recommends)
|
|
290
|
+
|
|
291
|
+
# Format: name<TAB>priority<TAB>deps<TAB>recommends
|
|
292
|
+
deps_str = f"{dep_count} deps" if dep_count else "no deps"
|
|
293
|
+
recs_str = f", {rec_count} recs" if rec_count else ""
|
|
294
|
+
|
|
295
|
+
line = f"{name}\t{info.priority}\t{deps_str}{recs_str}"
|
|
296
|
+
lines.append(line)
|
|
297
|
+
|
|
298
|
+
return "\n".join(lines)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _deps_fzf_browser(layers: Dict[str, LayerInfo]) -> int:
|
|
302
|
+
"""
|
|
303
|
+
Interactive fzf browser for layer dependencies.
|
|
304
|
+
|
|
305
|
+
Key bindings:
|
|
306
|
+
- Enter: Show dependency tree
|
|
307
|
+
- r: Show reverse dependencies (what depends on this)
|
|
308
|
+
- d: Output DOT format
|
|
309
|
+
- a: Show all layers full graph (DOT)
|
|
310
|
+
- ?: Toggle preview
|
|
311
|
+
- q/esc: Quit
|
|
312
|
+
"""
|
|
313
|
+
if not layers:
|
|
314
|
+
print("No layers found.")
|
|
315
|
+
return 1
|
|
316
|
+
|
|
317
|
+
if not fzf_available():
|
|
318
|
+
# Fall back to text list
|
|
319
|
+
for name in sorted(layers.keys()):
|
|
320
|
+
info = layers[name]
|
|
321
|
+
deps_str = ", ".join(info.depends) if info.depends else "(none)"
|
|
322
|
+
print(f" {Colors.green(name):<30} priority={info.priority} deps={deps_str}")
|
|
323
|
+
return 0
|
|
324
|
+
|
|
325
|
+
menu_input = _build_layer_menu(layers)
|
|
326
|
+
|
|
327
|
+
# Create preview data file
|
|
328
|
+
preview_data = {}
|
|
329
|
+
for name, info in layers.items():
|
|
330
|
+
preview_data[name] = {
|
|
331
|
+
"name": info.name,
|
|
332
|
+
"path": info.path,
|
|
333
|
+
"depends": info.depends,
|
|
334
|
+
"recommends": info.recommends,
|
|
335
|
+
"priority": info.priority,
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
preview_data_file = tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False)
|
|
339
|
+
json.dump(preview_data, preview_data_file)
|
|
340
|
+
preview_data_file.close()
|
|
341
|
+
|
|
342
|
+
# Create layers graph for tree preview
|
|
343
|
+
layers_graph = {}
|
|
344
|
+
for name, info in layers.items():
|
|
345
|
+
layers_graph[name] = {
|
|
346
|
+
"depends": info.depends,
|
|
347
|
+
"recommends": info.recommends,
|
|
348
|
+
}
|
|
349
|
+
graph_file = tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False)
|
|
350
|
+
json.dump(layers_graph, graph_file)
|
|
351
|
+
graph_file.close()
|
|
352
|
+
|
|
353
|
+
# Create preview script
|
|
354
|
+
preview_script = tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False)
|
|
355
|
+
preview_script.write(f'''#!/usr/bin/env python3
|
|
356
|
+
import json
|
|
357
|
+
import sys
|
|
358
|
+
|
|
359
|
+
key = sys.argv[1] if len(sys.argv) > 1 else ""
|
|
360
|
+
key = key.strip("'\\"").split("\\t")[0]
|
|
361
|
+
|
|
362
|
+
with open("{preview_data_file.name}", "r") as f:
|
|
363
|
+
data = json.load(f)
|
|
364
|
+
|
|
365
|
+
with open("{graph_file.name}", "r") as f:
|
|
366
|
+
graph = json.load(f)
|
|
367
|
+
|
|
368
|
+
info = data.get(key, {{}})
|
|
369
|
+
if not info:
|
|
370
|
+
print(f"No data for: {{key}}")
|
|
371
|
+
sys.exit(0)
|
|
372
|
+
|
|
373
|
+
name = info.get("name", "")
|
|
374
|
+
path = info.get("path", "")
|
|
375
|
+
depends = info.get("depends", [])
|
|
376
|
+
recommends = info.get("recommends", [])
|
|
377
|
+
priority = info.get("priority", 0)
|
|
378
|
+
|
|
379
|
+
print(f"\\033[1m{{name}}\\033[0m")
|
|
380
|
+
print(f"Priority: {{priority}}")
|
|
381
|
+
print(f"Path: {{path}}")
|
|
382
|
+
print()
|
|
383
|
+
|
|
384
|
+
# Show dependency tree
|
|
385
|
+
def render_tree(node, g, visited, prefix="", is_last=True, is_root=False):
|
|
386
|
+
lines = []
|
|
387
|
+
if is_root:
|
|
388
|
+
lines.append(f"\\033[36mDepends on:\\033[0m")
|
|
389
|
+
connector = "\\u2514\\u2500\\u2500 " if is_last else "\\u251c\\u2500\\u2500 "
|
|
390
|
+
if not is_root:
|
|
391
|
+
lines.append(f"{{prefix}}{{connector}}{{node}}")
|
|
392
|
+
if node in visited:
|
|
393
|
+
return lines
|
|
394
|
+
visited.add(node)
|
|
395
|
+
children = sorted(g.get(node, {{}}).get("depends", []))
|
|
396
|
+
child_prefix = prefix + (" " if is_last else "\\u2502 ")
|
|
397
|
+
for i, child in enumerate(children):
|
|
398
|
+
is_last_child = (i == len(children) - 1)
|
|
399
|
+
lines.extend(render_tree(child, g, visited, child_prefix if not is_root else "", is_last_child))
|
|
400
|
+
return lines
|
|
401
|
+
|
|
402
|
+
if depends:
|
|
403
|
+
visited = set()
|
|
404
|
+
visited.add(key)
|
|
405
|
+
for i, dep in enumerate(sorted(depends)):
|
|
406
|
+
is_last = (i == len(depends) - 1)
|
|
407
|
+
lines = render_tree(dep, graph, visited, "", is_last)
|
|
408
|
+
for line in lines:
|
|
409
|
+
print(f" {{line}}")
|
|
410
|
+
else:
|
|
411
|
+
print("\\033[36mDepends on:\\033[0m (none)")
|
|
412
|
+
|
|
413
|
+
print()
|
|
414
|
+
|
|
415
|
+
# Show what depends on this layer (reverse)
|
|
416
|
+
reverse_deps = []
|
|
417
|
+
for n, g in graph.items():
|
|
418
|
+
if key in g.get("depends", []):
|
|
419
|
+
reverse_deps.append(n)
|
|
420
|
+
|
|
421
|
+
if reverse_deps:
|
|
422
|
+
print("\\033[36mRequired by:\\033[0m")
|
|
423
|
+
for rd in sorted(reverse_deps):
|
|
424
|
+
print(f" {{rd}}")
|
|
425
|
+
else:
|
|
426
|
+
print("\\033[36mRequired by:\\033[0m (none)")
|
|
427
|
+
|
|
428
|
+
if recommends:
|
|
429
|
+
print()
|
|
430
|
+
print("\\033[36mRecommends:\\033[0m")
|
|
431
|
+
for rec in sorted(recommends):
|
|
432
|
+
print(f" {{rec}}")
|
|
433
|
+
''')
|
|
434
|
+
preview_script.close()
|
|
435
|
+
|
|
436
|
+
preview_cmd = f'python3 "{preview_script.name}" {{1}}'
|
|
437
|
+
|
|
438
|
+
header = """Layer Dependencies Browser
|
|
439
|
+
Enter=tree | r=reverse deps | d=DOT output | a=all DOT | ?=preview | q=quit"""
|
|
440
|
+
|
|
441
|
+
expect_keys = ["ctrl-r", "ctrl-d", "ctrl-a", "esc"]
|
|
442
|
+
|
|
443
|
+
preview_window = get_preview_window_arg("50%")
|
|
444
|
+
|
|
445
|
+
fzf_args = [
|
|
446
|
+
"fzf",
|
|
447
|
+
"--no-multi",
|
|
448
|
+
"--ansi",
|
|
449
|
+
"--height", "100%",
|
|
450
|
+
"--layout=reverse-list",
|
|
451
|
+
"--header", header,
|
|
452
|
+
"--prompt", "Layer: ",
|
|
453
|
+
"--with-nth", "1",
|
|
454
|
+
"--delimiter", "\t",
|
|
455
|
+
"--preview", preview_cmd,
|
|
456
|
+
"--preview-window", preview_window,
|
|
457
|
+
"--bind", "?:toggle-preview",
|
|
458
|
+
"--bind", "pgup:preview-page-up",
|
|
459
|
+
"--bind", "pgdn:preview-page-down",
|
|
460
|
+
"--bind", "q:abort",
|
|
461
|
+
"--bind", "esc:abort",
|
|
462
|
+
"--expect", ",".join(expect_keys),
|
|
463
|
+
]
|
|
464
|
+
|
|
465
|
+
fzf_args.extend(get_fzf_preview_resize_bindings())
|
|
466
|
+
fzf_args.extend(get_fzf_color_args())
|
|
467
|
+
|
|
468
|
+
try:
|
|
469
|
+
while True:
|
|
470
|
+
result = subprocess.run(
|
|
471
|
+
fzf_args,
|
|
472
|
+
input=menu_input,
|
|
473
|
+
stdout=subprocess.PIPE,
|
|
474
|
+
text=True,
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
if result.returncode != 0 or not result.stdout.strip():
|
|
478
|
+
break
|
|
479
|
+
|
|
480
|
+
lines = result.stdout.split("\n")
|
|
481
|
+
key = lines[0].strip() if lines else ""
|
|
482
|
+
selected = lines[1].split("\t")[0].strip() if len(lines) > 1 else ""
|
|
483
|
+
|
|
484
|
+
if not selected:
|
|
485
|
+
break
|
|
486
|
+
|
|
487
|
+
if key == "ctrl-d":
|
|
488
|
+
# DOT output for selected layer
|
|
489
|
+
print(render_dot(layers, selected, reverse=False))
|
|
490
|
+
input("\nPress Enter to continue...")
|
|
491
|
+
continue
|
|
492
|
+
|
|
493
|
+
if key == "ctrl-r":
|
|
494
|
+
# Reverse dependency tree
|
|
495
|
+
print(f"\n{Colors.bold(selected)} is required by:\n")
|
|
496
|
+
print(render_tree_ascii(layers, selected, reverse=True))
|
|
497
|
+
input("\nPress Enter to continue...")
|
|
498
|
+
continue
|
|
499
|
+
|
|
500
|
+
if key == "ctrl-a":
|
|
501
|
+
# Full DOT graph
|
|
502
|
+
print(render_dot(layers, root=None, reverse=False))
|
|
503
|
+
input("\nPress Enter to continue...")
|
|
504
|
+
continue
|
|
505
|
+
|
|
506
|
+
# Default: show forward dependency tree
|
|
507
|
+
print(f"\n{Colors.bold(selected)} depends on:\n")
|
|
508
|
+
|
|
509
|
+
# Use configured graph renderer preference
|
|
510
|
+
print(render_tree_preferred(layers, selected, reverse=False))
|
|
511
|
+
|
|
512
|
+
input("\nPress Enter to continue...")
|
|
513
|
+
|
|
514
|
+
finally:
|
|
515
|
+
# Clean up temp files
|
|
516
|
+
for f in [preview_data_file.name, preview_script.name, graph_file.name]:
|
|
517
|
+
try:
|
|
518
|
+
os.unlink(f)
|
|
519
|
+
except OSError:
|
|
520
|
+
pass
|
|
521
|
+
|
|
522
|
+
return 0
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
# =============================================================================
|
|
526
|
+
# Text Output (no fzf)
|
|
527
|
+
# =============================================================================
|
|
528
|
+
|
|
529
|
+
def _list_layers_text(layers: Dict[str, LayerInfo], verbose: bool = False) -> int:
|
|
530
|
+
"""List layers in text format (no fzf)."""
|
|
531
|
+
if not layers:
|
|
532
|
+
print("No layers found.")
|
|
533
|
+
return 1
|
|
534
|
+
|
|
535
|
+
for name in sorted(layers.keys()):
|
|
536
|
+
info = layers[name]
|
|
537
|
+
deps_str = ", ".join(info.depends) if info.depends else "(none)"
|
|
538
|
+
|
|
539
|
+
if verbose:
|
|
540
|
+
print(f"{Colors.bold(name)}")
|
|
541
|
+
print(f" Path: {info.path}")
|
|
542
|
+
print(f" Priority: {info.priority}")
|
|
543
|
+
print(f" Depends: {deps_str}")
|
|
544
|
+
if info.recommends:
|
|
545
|
+
print(f" Recommends: {', '.join(info.recommends)}")
|
|
546
|
+
print()
|
|
547
|
+
else:
|
|
548
|
+
print(f" {Colors.green(name):<30} deps: {deps_str}")
|
|
549
|
+
|
|
550
|
+
return 0
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
# =============================================================================
|
|
554
|
+
# Recipe Dependencies
|
|
555
|
+
# =============================================================================
|
|
556
|
+
|
|
557
|
+
@dataclass
|
|
558
|
+
class RecipeInfo:
|
|
559
|
+
"""Recipe metadata for dependency visualization."""
|
|
560
|
+
name: str # Recipe name (PN)
|
|
561
|
+
version: str # Recipe version (PV)
|
|
562
|
+
path: str # Path to .bb file
|
|
563
|
+
layer: str # Layer name
|
|
564
|
+
depends: List[str] # Build-time dependencies (DEPENDS)
|
|
565
|
+
rdepends: List[str] # Runtime dependencies (RDEPENDS)
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
def _is_valid_package_name(name: str) -> bool:
|
|
569
|
+
"""Check if a string looks like a valid BitBake package name."""
|
|
570
|
+
if not name:
|
|
571
|
+
return False
|
|
572
|
+
# Must start with a letter and contain only valid chars
|
|
573
|
+
# Valid: letters, digits, hyphens, underscores, plus (for virtual/), periods
|
|
574
|
+
if not re.match(r'^[a-zA-Z][a-zA-Z0-9+._-]*$', name):
|
|
575
|
+
return False
|
|
576
|
+
# Skip things that look like Python/shell fragments
|
|
577
|
+
if any(c in name for c in "(){}[]'\"@$=,"):
|
|
578
|
+
return False
|
|
579
|
+
# Skip single letters (likely variable fragments like 'd')
|
|
580
|
+
if len(name) == 1:
|
|
581
|
+
return False
|
|
582
|
+
return True
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
def _clean_deps_string(deps_str: str) -> str:
|
|
586
|
+
"""Remove inline Python expressions and clean up a dependency string."""
|
|
587
|
+
# Remove ${@...} inline Python expressions (can be nested)
|
|
588
|
+
# Handle nested braces by iterating
|
|
589
|
+
while '${@' in deps_str:
|
|
590
|
+
start = deps_str.find('${@')
|
|
591
|
+
if start == -1:
|
|
592
|
+
break
|
|
593
|
+
# Find matching closing brace
|
|
594
|
+
depth = 0
|
|
595
|
+
end = start
|
|
596
|
+
for i, c in enumerate(deps_str[start:], start):
|
|
597
|
+
if c == '{':
|
|
598
|
+
depth += 1
|
|
599
|
+
elif c == '}':
|
|
600
|
+
depth -= 1
|
|
601
|
+
if depth == 0:
|
|
602
|
+
end = i
|
|
603
|
+
break
|
|
604
|
+
if end > start:
|
|
605
|
+
deps_str = deps_str[:start] + deps_str[end + 1:]
|
|
606
|
+
else:
|
|
607
|
+
# Malformed expression, just remove from ${@ onwards
|
|
608
|
+
deps_str = deps_str[:start]
|
|
609
|
+
break
|
|
610
|
+
|
|
611
|
+
# Also remove simple ${...} variable references
|
|
612
|
+
deps_str = re.sub(r'\$\{[^}]*\}', ' ', deps_str)
|
|
613
|
+
|
|
614
|
+
return deps_str
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
def _parse_recipe_depends(filepath: str) -> Tuple[List[str], List[str]]:
|
|
618
|
+
"""
|
|
619
|
+
Extract DEPENDS and RDEPENDS from a recipe file.
|
|
620
|
+
|
|
621
|
+
Returns (depends, rdepends) as lists of package names.
|
|
622
|
+
"""
|
|
623
|
+
depends = []
|
|
624
|
+
rdepends = []
|
|
625
|
+
|
|
626
|
+
try:
|
|
627
|
+
with open(filepath, "r", encoding="utf-8", errors="ignore") as f:
|
|
628
|
+
content = f.read()
|
|
629
|
+
except (OSError, IOError):
|
|
630
|
+
return depends, rdepends
|
|
631
|
+
|
|
632
|
+
# Handle line continuations
|
|
633
|
+
content = content.replace("\\\n", " ")
|
|
634
|
+
|
|
635
|
+
# Extract DEPENDS
|
|
636
|
+
# Match DEPENDS = "...", DEPENDS += "...", DEPENDS:append = "..."
|
|
637
|
+
depends_patterns = [
|
|
638
|
+
r'^DEPENDS\s*[+:]?=\s*"([^"]*)"',
|
|
639
|
+
r"^DEPENDS\s*[+:]?=\s*'([^']*)'",
|
|
640
|
+
]
|
|
641
|
+
for pattern in depends_patterns:
|
|
642
|
+
for match in re.finditer(pattern, content, re.MULTILINE):
|
|
643
|
+
deps_str = _clean_deps_string(match.group(1))
|
|
644
|
+
for dep in deps_str.split():
|
|
645
|
+
if _is_valid_package_name(dep) and dep not in depends:
|
|
646
|
+
depends.append(dep)
|
|
647
|
+
|
|
648
|
+
# Extract RDEPENDS (can be RDEPENDS:${PN} or just RDEPENDS)
|
|
649
|
+
rdepends_patterns = [
|
|
650
|
+
r'^RDEPENDS[:\w${}\-]*\s*[+:]?=\s*"([^"]*)"',
|
|
651
|
+
r"^RDEPENDS[:\w${}\-]*\s*[+:]?=\s*'([^']*)'",
|
|
652
|
+
]
|
|
653
|
+
for pattern in rdepends_patterns:
|
|
654
|
+
for match in re.finditer(pattern, content, re.MULTILINE):
|
|
655
|
+
deps_str = _clean_deps_string(match.group(1))
|
|
656
|
+
for dep in deps_str.split():
|
|
657
|
+
if _is_valid_package_name(dep) and dep not in rdepends:
|
|
658
|
+
rdepends.append(dep)
|
|
659
|
+
|
|
660
|
+
return depends, rdepends
|
|
661
|
+
|
|
662
|
+
|
|
663
|
+
def _find_recipes(layer_paths: List[str], recipe_name: str) -> List[RecipeInfo]:
|
|
664
|
+
"""
|
|
665
|
+
Find recipes matching the given name across all layers.
|
|
666
|
+
|
|
667
|
+
Returns list of RecipeInfo for matching recipes.
|
|
668
|
+
"""
|
|
669
|
+
recipes = []
|
|
670
|
+
recipe_name_lower = recipe_name.lower()
|
|
671
|
+
|
|
672
|
+
for layer_path in layer_paths:
|
|
673
|
+
layer_name = os.path.basename(layer_path)
|
|
674
|
+
|
|
675
|
+
# Glob for recipes matching the name
|
|
676
|
+
patterns = [
|
|
677
|
+
os.path.join(layer_path, "recipes-*", "*", f"{recipe_name}_*.bb"),
|
|
678
|
+
os.path.join(layer_path, "recipes-*", "*", f"{recipe_name}.bb"),
|
|
679
|
+
os.path.join(layer_path, "recipes-*", f"{recipe_name}_*.bb"),
|
|
680
|
+
os.path.join(layer_path, "recipes-*", f"{recipe_name}.bb"),
|
|
681
|
+
]
|
|
682
|
+
|
|
683
|
+
for pattern in patterns:
|
|
684
|
+
for filepath in glob.glob(pattern):
|
|
685
|
+
filename = os.path.basename(filepath)
|
|
686
|
+
# Extract PN and PV from filename
|
|
687
|
+
if "_" in filename:
|
|
688
|
+
parts = filename.rsplit("_", 1)
|
|
689
|
+
pn = parts[0]
|
|
690
|
+
pv = parts[1].replace(".bb", "")
|
|
691
|
+
else:
|
|
692
|
+
pn = filename.replace(".bb", "")
|
|
693
|
+
pv = ""
|
|
694
|
+
|
|
695
|
+
depends, rdepends = _parse_recipe_depends(filepath)
|
|
696
|
+
|
|
697
|
+
recipes.append(RecipeInfo(
|
|
698
|
+
name=pn,
|
|
699
|
+
version=pv,
|
|
700
|
+
path=filepath,
|
|
701
|
+
layer=layer_name,
|
|
702
|
+
depends=depends,
|
|
703
|
+
rdepends=rdepends,
|
|
704
|
+
))
|
|
705
|
+
|
|
706
|
+
# Also try partial match if no exact matches
|
|
707
|
+
if not recipes:
|
|
708
|
+
for layer_path in layer_paths:
|
|
709
|
+
layer_name = os.path.basename(layer_path)
|
|
710
|
+
pattern = os.path.join(layer_path, "recipes-*", "*", "*.bb")
|
|
711
|
+
|
|
712
|
+
for filepath in glob.glob(pattern):
|
|
713
|
+
filename = os.path.basename(filepath)
|
|
714
|
+
pn = filename.split("_")[0] if "_" in filename else filename.replace(".bb", "")
|
|
715
|
+
|
|
716
|
+
if recipe_name_lower in pn.lower():
|
|
717
|
+
if "_" in filename:
|
|
718
|
+
parts = filename.rsplit("_", 1)
|
|
719
|
+
pv = parts[1].replace(".bb", "")
|
|
720
|
+
else:
|
|
721
|
+
pv = ""
|
|
722
|
+
|
|
723
|
+
depends, rdepends = _parse_recipe_depends(filepath)
|
|
724
|
+
|
|
725
|
+
recipes.append(RecipeInfo(
|
|
726
|
+
name=pn,
|
|
727
|
+
version=pv,
|
|
728
|
+
path=filepath,
|
|
729
|
+
layer=layer_name,
|
|
730
|
+
depends=depends,
|
|
731
|
+
rdepends=rdepends,
|
|
732
|
+
))
|
|
733
|
+
|
|
734
|
+
return recipes
|
|
735
|
+
|
|
736
|
+
|
|
737
|
+
def _render_recipe_tree_ascii(
|
|
738
|
+
recipe: RecipeInfo,
|
|
739
|
+
include_rdepends: bool = False,
|
|
740
|
+
) -> str:
|
|
741
|
+
"""Render recipe dependency tree as ASCII."""
|
|
742
|
+
lines = []
|
|
743
|
+
lines.append(f"{Colors.bold(recipe.name)}")
|
|
744
|
+
if recipe.version:
|
|
745
|
+
lines.append(f"Version: {recipe.version}")
|
|
746
|
+
lines.append(f"Layer: {recipe.layer}")
|
|
747
|
+
lines.append(f"Path: {recipe.path}")
|
|
748
|
+
lines.append("")
|
|
749
|
+
|
|
750
|
+
if recipe.depends:
|
|
751
|
+
lines.append(f"{Colors.cyan('Build dependencies (DEPENDS):')}")
|
|
752
|
+
for i, dep in enumerate(sorted(recipe.depends)):
|
|
753
|
+
is_last = (i == len(recipe.depends) - 1) and not (include_rdepends and recipe.rdepends)
|
|
754
|
+
connector = "\u2514\u2500\u2500 " if is_last else "\u251c\u2500\u2500 "
|
|
755
|
+
lines.append(f" {connector}{dep}")
|
|
756
|
+
else:
|
|
757
|
+
lines.append(f"{Colors.cyan('Build dependencies (DEPENDS):')} (none)")
|
|
758
|
+
|
|
759
|
+
if include_rdepends:
|
|
760
|
+
lines.append("")
|
|
761
|
+
if recipe.rdepends:
|
|
762
|
+
lines.append(f"{Colors.cyan('Runtime dependencies (RDEPENDS):')}")
|
|
763
|
+
for i, dep in enumerate(sorted(recipe.rdepends)):
|
|
764
|
+
is_last = (i == len(recipe.rdepends) - 1)
|
|
765
|
+
connector = "\u2514\u2500\u2500 " if is_last else "\u251c\u2500\u2500 "
|
|
766
|
+
lines.append(f" {connector}{dep}")
|
|
767
|
+
else:
|
|
768
|
+
lines.append(f"{Colors.cyan('Runtime dependencies (RDEPENDS):')} (none)")
|
|
769
|
+
|
|
770
|
+
return "\n".join(lines)
|
|
771
|
+
|
|
772
|
+
|
|
773
|
+
def _render_recipe_dot(
|
|
774
|
+
recipe: RecipeInfo,
|
|
775
|
+
include_rdepends: bool = False,
|
|
776
|
+
) -> str:
|
|
777
|
+
"""Render recipe dependencies as DOT graph."""
|
|
778
|
+
dot_lines = [
|
|
779
|
+
"digraph recipe_deps {",
|
|
780
|
+
' rankdir=TB;',
|
|
781
|
+
' node [shape=box, fontname="sans-serif"];',
|
|
782
|
+
f' "{recipe.name}" [style=bold, color=blue];',
|
|
783
|
+
]
|
|
784
|
+
|
|
785
|
+
for dep in recipe.depends:
|
|
786
|
+
dot_lines.append(f' "{recipe.name}" -> "{dep}";')
|
|
787
|
+
|
|
788
|
+
if include_rdepends:
|
|
789
|
+
for dep in recipe.rdepends:
|
|
790
|
+
dot_lines.append(f' "{recipe.name}" -> "{dep}" [style=dashed, color=gray];')
|
|
791
|
+
|
|
792
|
+
dot_lines.append("}")
|
|
793
|
+
return "\n".join(dot_lines)
|
|
794
|
+
|
|
795
|
+
|
|
796
|
+
# =============================================================================
|
|
797
|
+
# Main Entry Point
|
|
798
|
+
# =============================================================================
|
|
799
|
+
|
|
800
|
+
def run_deps(args) -> int:
|
|
801
|
+
"""Main entry point for deps command."""
|
|
802
|
+
deps_command = getattr(args, "deps_command", None)
|
|
803
|
+
layer_name = getattr(args, "layer", None)
|
|
804
|
+
reverse = getattr(args, "reverse", False)
|
|
805
|
+
output_format = getattr(args, "format", "tree")
|
|
806
|
+
list_mode = getattr(args, "list", False)
|
|
807
|
+
|
|
808
|
+
# Get layer paths from bblayers.conf
|
|
809
|
+
bblayers_path = resolve_bblayers_path(getattr(args, "bblayers", None))
|
|
810
|
+
if not bblayers_path:
|
|
811
|
+
print("Could not find bblayers.conf")
|
|
812
|
+
return 1
|
|
813
|
+
|
|
814
|
+
layer_paths = extract_layer_paths(bblayers_path)
|
|
815
|
+
if not layer_paths:
|
|
816
|
+
print("No layers found in bblayers.conf")
|
|
817
|
+
return 1
|
|
818
|
+
|
|
819
|
+
# Build dependency graph
|
|
820
|
+
layers = build_layer_dependency_graph(layer_paths)
|
|
821
|
+
if not layers:
|
|
822
|
+
print("No layer.conf files found")
|
|
823
|
+
return 1
|
|
824
|
+
|
|
825
|
+
# Handle subcommands
|
|
826
|
+
if deps_command == "layers" or deps_command is None:
|
|
827
|
+
# Layer dependencies
|
|
828
|
+
|
|
829
|
+
if layer_name:
|
|
830
|
+
# Specific layer requested
|
|
831
|
+
if layer_name not in layers:
|
|
832
|
+
# Try partial match
|
|
833
|
+
matches = [n for n in layers if layer_name in n]
|
|
834
|
+
if len(matches) == 1:
|
|
835
|
+
layer_name = matches[0]
|
|
836
|
+
elif matches:
|
|
837
|
+
print(f"Ambiguous layer name '{layer_name}'. Matches: {', '.join(matches)}")
|
|
838
|
+
return 1
|
|
839
|
+
else:
|
|
840
|
+
print(f"Layer '{layer_name}' not found. Available: {', '.join(sorted(layers.keys()))}")
|
|
841
|
+
return 1
|
|
842
|
+
|
|
843
|
+
if output_format == "dot":
|
|
844
|
+
print(render_dot(layers, layer_name, reverse))
|
|
845
|
+
elif output_format == "list":
|
|
846
|
+
print(render_list(layers, layer_name, reverse))
|
|
847
|
+
else:
|
|
848
|
+
# Tree format - use configured preference
|
|
849
|
+
direction = "is required by" if reverse else "depends on"
|
|
850
|
+
print(f"{Colors.bold(layer_name)} {direction}:\n")
|
|
851
|
+
print(render_tree_preferred(layers, layer_name, reverse))
|
|
852
|
+
return 0
|
|
853
|
+
|
|
854
|
+
# No specific layer - interactive or list mode
|
|
855
|
+
if list_mode or not fzf_available():
|
|
856
|
+
return _list_layers_text(layers, verbose=getattr(args, "verbose", False))
|
|
857
|
+
|
|
858
|
+
return _deps_fzf_browser(layers)
|
|
859
|
+
|
|
860
|
+
if deps_command == "recipe":
|
|
861
|
+
recipe_name = getattr(args, "recipe", None)
|
|
862
|
+
if not recipe_name:
|
|
863
|
+
print("Recipe name required")
|
|
864
|
+
return 1
|
|
865
|
+
|
|
866
|
+
include_rdepends = getattr(args, "rdepends", False)
|
|
867
|
+
|
|
868
|
+
# Find recipes matching the name
|
|
869
|
+
recipes = _find_recipes(layer_paths, recipe_name)
|
|
870
|
+
|
|
871
|
+
if not recipes:
|
|
872
|
+
print(f"No recipe found matching '{recipe_name}'")
|
|
873
|
+
return 1
|
|
874
|
+
|
|
875
|
+
# If multiple matches, let user pick or show all
|
|
876
|
+
if len(recipes) > 1:
|
|
877
|
+
print(f"Found {len(recipes)} recipes matching '{recipe_name}':")
|
|
878
|
+
for i, r in enumerate(recipes, 1):
|
|
879
|
+
ver_str = f" ({r.version})" if r.version else ""
|
|
880
|
+
print(f" {i}. {r.name}{ver_str} - {r.layer}")
|
|
881
|
+
print()
|
|
882
|
+
|
|
883
|
+
# Use first one for now, or could add interactive selection
|
|
884
|
+
recipe = recipes[0]
|
|
885
|
+
print(f"Showing dependencies for: {recipe.name} from {recipe.layer}\n")
|
|
886
|
+
else:
|
|
887
|
+
recipe = recipes[0]
|
|
888
|
+
|
|
889
|
+
if output_format == "dot":
|
|
890
|
+
print(_render_recipe_dot(recipe, include_rdepends))
|
|
891
|
+
elif output_format == "list":
|
|
892
|
+
# Simple list format
|
|
893
|
+
print(f"{recipe.name}:")
|
|
894
|
+
print(f" DEPENDS: {', '.join(recipe.depends) if recipe.depends else '(none)'}")
|
|
895
|
+
if include_rdepends:
|
|
896
|
+
print(f" RDEPENDS: {', '.join(recipe.rdepends) if recipe.rdepends else '(none)'}")
|
|
897
|
+
else:
|
|
898
|
+
# Tree format
|
|
899
|
+
print(_render_recipe_tree_ascii(recipe, include_rdepends))
|
|
900
|
+
|
|
901
|
+
return 0
|
|
902
|
+
|
|
903
|
+
return 0
|