bitp 1.0.6__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.6.dist-info/METADATA +401 -0
- bitp-1.0.6.dist-info/RECORD +24 -0
- bitp-1.0.6.dist-info/WHEEL +5 -0
- bitp-1.0.6.dist-info/entry_points.txt +3 -0
- bitp-1.0.6.dist-info/licenses/COPYING +338 -0
- bitp-1.0.6.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,884 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright (C) 2025 Bruce Ashfield <bruce.ashfield@gmail.com>
|
|
3
|
+
#
|
|
4
|
+
# SPDX-License-Identifier: GPL-2.0-only
|
|
5
|
+
#
|
|
6
|
+
"""Fragment command - browse and manage OE configuration fragments."""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import re
|
|
11
|
+
import shutil
|
|
12
|
+
import subprocess
|
|
13
|
+
import tempfile
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from typing import Dict, List, Optional, Tuple
|
|
16
|
+
|
|
17
|
+
from ..core import Colors, fzf_available, get_fzf_color_args, get_fzf_preview_resize_bindings, terminal_color
|
|
18
|
+
from .common import resolve_bblayers_path, extract_layer_paths
|
|
19
|
+
from .projects import get_preview_window_arg
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# =============================================================================
|
|
23
|
+
# Data Classes
|
|
24
|
+
# =============================================================================
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class Fragment:
|
|
28
|
+
"""Represents a configuration fragment."""
|
|
29
|
+
name: str # e.g., "meta/yocto/root-login-with-empty-password"
|
|
30
|
+
path: str # Full filesystem path to .conf file (empty for builtin)
|
|
31
|
+
layer: str # Layer name (e.g., "meta")
|
|
32
|
+
domain: str # Domain path (e.g., "yocto")
|
|
33
|
+
summary: str # BB_CONF_FRAGMENT_SUMMARY value
|
|
34
|
+
description: str # BB_CONF_FRAGMENT_DESCRIPTION value
|
|
35
|
+
is_enabled: bool # Whether in OE_FRAGMENTS
|
|
36
|
+
is_builtin: bool # Whether this is a built-in fragment (machine/distro)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class BuiltinPrefix:
|
|
41
|
+
"""Represents a built-in fragment prefix (machine:MACHINE, distro:DISTRO)."""
|
|
42
|
+
prefix: str # e.g., "machine"
|
|
43
|
+
variable: str # e.g., "MACHINE"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# =============================================================================
|
|
47
|
+
# Constants
|
|
48
|
+
# =============================================================================
|
|
49
|
+
|
|
50
|
+
# Default built-in fragment prefixes from OE-core bitbake.conf
|
|
51
|
+
DEFAULT_BUILTIN_PREFIXES = [
|
|
52
|
+
BuiltinPrefix("machine", "MACHINE"),
|
|
53
|
+
BuiltinPrefix("distro", "DISTRO"),
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# =============================================================================
|
|
58
|
+
# toolcfg.conf Parsing
|
|
59
|
+
# =============================================================================
|
|
60
|
+
|
|
61
|
+
def _get_toolcfg_path(bblayers_path: Optional[str] = None) -> str:
|
|
62
|
+
"""
|
|
63
|
+
Find toolcfg.conf relative to build directory.
|
|
64
|
+
|
|
65
|
+
Returns path to conf/toolcfg.conf relative to the build directory
|
|
66
|
+
containing bblayers.conf.
|
|
67
|
+
"""
|
|
68
|
+
if bblayers_path:
|
|
69
|
+
# toolcfg.conf is in the same conf/ directory as bblayers.conf
|
|
70
|
+
conf_dir = os.path.dirname(bblayers_path)
|
|
71
|
+
return os.path.join(conf_dir, "toolcfg.conf")
|
|
72
|
+
|
|
73
|
+
# Default locations
|
|
74
|
+
candidates = ["conf/toolcfg.conf", "build/conf/toolcfg.conf"]
|
|
75
|
+
for cand in candidates:
|
|
76
|
+
parent_conf = os.path.dirname(cand)
|
|
77
|
+
if os.path.isdir(parent_conf):
|
|
78
|
+
return cand
|
|
79
|
+
|
|
80
|
+
return "conf/toolcfg.conf"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _parse_enabled_fragments(toolcfg_path: str) -> List[str]:
|
|
84
|
+
"""
|
|
85
|
+
Parse OE_FRAGMENTS from toolcfg.conf using regex.
|
|
86
|
+
|
|
87
|
+
Handles:
|
|
88
|
+
- OE_FRAGMENTS = "..."
|
|
89
|
+
- OE_FRAGMENTS += "..."
|
|
90
|
+
- Line continuations with backslash
|
|
91
|
+
"""
|
|
92
|
+
if not os.path.isfile(toolcfg_path):
|
|
93
|
+
return []
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
with open(toolcfg_path, "r") as f:
|
|
97
|
+
content = f.read()
|
|
98
|
+
except (OSError, IOError):
|
|
99
|
+
return []
|
|
100
|
+
|
|
101
|
+
# Handle line continuations
|
|
102
|
+
content = content.replace("\\\n", " ")
|
|
103
|
+
|
|
104
|
+
# Match OE_FRAGMENTS = "..." or OE_FRAGMENTS += "..."
|
|
105
|
+
# Collect all values (multiple assignments get concatenated)
|
|
106
|
+
fragments = []
|
|
107
|
+
pattern = re.compile(r'OE_FRAGMENTS\s*\+?=\s*"([^"]*)"', re.MULTILINE)
|
|
108
|
+
|
|
109
|
+
for match in pattern.finditer(content):
|
|
110
|
+
value = match.group(1).strip()
|
|
111
|
+
if value:
|
|
112
|
+
fragments.extend(value.split())
|
|
113
|
+
|
|
114
|
+
return fragments
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _ensure_toolcfg_exists(toolcfg_path: str) -> None:
|
|
118
|
+
"""Create toolcfg.conf with default content if it doesn't exist."""
|
|
119
|
+
if os.path.exists(toolcfg_path):
|
|
120
|
+
return
|
|
121
|
+
|
|
122
|
+
# Create conf directory if needed
|
|
123
|
+
conf_dir = os.path.dirname(toolcfg_path)
|
|
124
|
+
if conf_dir and not os.path.isdir(conf_dir):
|
|
125
|
+
os.makedirs(conf_dir, exist_ok=True)
|
|
126
|
+
|
|
127
|
+
with open(toolcfg_path, "w") as f:
|
|
128
|
+
f.write("""# Automated config file controlled by bit/bitbake-config-build
|
|
129
|
+
#
|
|
130
|
+
# Run 'bit fragment enable <fragment-name>' to enable additional fragments
|
|
131
|
+
# or replace built-in ones (e.g. machine/<name> or distro/<name> to change MACHINE or DISTRO).
|
|
132
|
+
|
|
133
|
+
OE_FRAGMENTS += ""
|
|
134
|
+
""")
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _update_oe_fragments(
|
|
138
|
+
toolcfg_path: str,
|
|
139
|
+
to_add: List[str],
|
|
140
|
+
to_remove: List[str],
|
|
141
|
+
builtin_prefixes: List[BuiltinPrefix],
|
|
142
|
+
) -> bool:
|
|
143
|
+
"""
|
|
144
|
+
Update OE_FRAGMENTS variable in toolcfg.conf.
|
|
145
|
+
|
|
146
|
+
- Adds new fragments
|
|
147
|
+
- Removes specified fragments
|
|
148
|
+
- For builtin fragments (machine/, distro/), removes existing value with same prefix
|
|
149
|
+
|
|
150
|
+
Returns True if file was modified.
|
|
151
|
+
"""
|
|
152
|
+
_ensure_toolcfg_exists(toolcfg_path)
|
|
153
|
+
|
|
154
|
+
with open(toolcfg_path, "r") as f:
|
|
155
|
+
content = f.read()
|
|
156
|
+
|
|
157
|
+
# Parse current fragments
|
|
158
|
+
current = _parse_enabled_fragments(toolcfg_path)
|
|
159
|
+
|
|
160
|
+
# Get builtin prefix names for special handling
|
|
161
|
+
builtin_prefix_names = {p.prefix for p in builtin_prefixes}
|
|
162
|
+
|
|
163
|
+
# Process removals
|
|
164
|
+
for fragment in to_remove:
|
|
165
|
+
if fragment in current:
|
|
166
|
+
current.remove(fragment)
|
|
167
|
+
# Also handle prefix-based removal (e.g., "machine/" removes all machine/*)
|
|
168
|
+
if fragment.endswith("/"):
|
|
169
|
+
current = [f for f in current if not f.startswith(fragment)]
|
|
170
|
+
|
|
171
|
+
# Process additions
|
|
172
|
+
for fragment in to_add:
|
|
173
|
+
if fragment in current:
|
|
174
|
+
continue
|
|
175
|
+
|
|
176
|
+
# For builtin fragments, remove existing with same prefix first
|
|
177
|
+
prefix = fragment.split("/", 1)[0]
|
|
178
|
+
if prefix in builtin_prefix_names:
|
|
179
|
+
current = [f for f in current if not f.startswith(f"{prefix}/")]
|
|
180
|
+
|
|
181
|
+
current.append(fragment)
|
|
182
|
+
|
|
183
|
+
# Update the file
|
|
184
|
+
new_value = " ".join(current)
|
|
185
|
+
|
|
186
|
+
# Check if OE_FRAGMENTS line exists
|
|
187
|
+
pattern = re.compile(r'(OE_FRAGMENTS\s*\+?=\s*)"[^"]*"', re.MULTILINE)
|
|
188
|
+
|
|
189
|
+
if pattern.search(content):
|
|
190
|
+
# Replace existing line
|
|
191
|
+
new_content = pattern.sub(rf'\1"{new_value}"', content, count=1)
|
|
192
|
+
else:
|
|
193
|
+
# Add new line
|
|
194
|
+
new_content = content.rstrip() + f'\n\nOE_FRAGMENTS += "{new_value}"\n'
|
|
195
|
+
|
|
196
|
+
if new_content == content:
|
|
197
|
+
return False
|
|
198
|
+
|
|
199
|
+
with open(toolcfg_path, "w") as f:
|
|
200
|
+
f.write(new_content)
|
|
201
|
+
|
|
202
|
+
return True
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
# =============================================================================
|
|
206
|
+
# Fragment Discovery
|
|
207
|
+
# =============================================================================
|
|
208
|
+
|
|
209
|
+
def _parse_fragment_metadata(conf_path: str) -> Tuple[str, str]:
|
|
210
|
+
"""
|
|
211
|
+
Extract BB_CONF_FRAGMENT_SUMMARY and BB_CONF_FRAGMENT_DESCRIPTION from a .conf file.
|
|
212
|
+
|
|
213
|
+
Returns (summary, description).
|
|
214
|
+
"""
|
|
215
|
+
summary = ""
|
|
216
|
+
description = ""
|
|
217
|
+
|
|
218
|
+
try:
|
|
219
|
+
with open(conf_path, "r", encoding="utf-8", errors="ignore") as f:
|
|
220
|
+
content = f.read()
|
|
221
|
+
except (OSError, IOError):
|
|
222
|
+
return summary, description
|
|
223
|
+
|
|
224
|
+
# Parse BB_CONF_FRAGMENT_SUMMARY = "..."
|
|
225
|
+
summary_match = re.search(r'BB_CONF_FRAGMENT_SUMMARY\s*=\s*"([^"]*)"', content)
|
|
226
|
+
if summary_match:
|
|
227
|
+
summary = summary_match.group(1).strip()
|
|
228
|
+
|
|
229
|
+
# Parse BB_CONF_FRAGMENT_DESCRIPTION = "..."
|
|
230
|
+
# This can be multiline with backslash continuation
|
|
231
|
+
desc_match = re.search(r'BB_CONF_FRAGMENT_DESCRIPTION\s*=\s*"([^"]*(?:\\\n[^"]*)*)"', content)
|
|
232
|
+
if desc_match:
|
|
233
|
+
description = desc_match.group(1).strip()
|
|
234
|
+
# Clean up line continuations
|
|
235
|
+
description = description.replace("\\\n", " ").replace(" ", " ")
|
|
236
|
+
|
|
237
|
+
return summary, description
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _get_layer_name(layer_path: str) -> str:
|
|
241
|
+
"""
|
|
242
|
+
Get layer name from layer.conf BBFILE_COLLECTIONS.
|
|
243
|
+
Falls back to directory name.
|
|
244
|
+
"""
|
|
245
|
+
layer_conf = os.path.join(layer_path, "conf", "layer.conf")
|
|
246
|
+
|
|
247
|
+
if os.path.isfile(layer_conf):
|
|
248
|
+
try:
|
|
249
|
+
with open(layer_conf, "r") as f:
|
|
250
|
+
content = f.read()
|
|
251
|
+
match = re.search(r'BBFILE_COLLECTIONS\s*\+?=\s*"([^"]*)"', content)
|
|
252
|
+
if match:
|
|
253
|
+
# Take first collection name
|
|
254
|
+
names = match.group(1).strip().split()
|
|
255
|
+
if names:
|
|
256
|
+
return names[0]
|
|
257
|
+
except (OSError, IOError):
|
|
258
|
+
pass
|
|
259
|
+
|
|
260
|
+
return os.path.basename(layer_path)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _discover_fragments(layer_paths: List[str], progress: bool = True) -> List[Fragment]:
|
|
264
|
+
"""
|
|
265
|
+
Scan layers for fragments in conf/fragments/<domain>/*.conf.
|
|
266
|
+
|
|
267
|
+
Returns list of Fragment objects.
|
|
268
|
+
"""
|
|
269
|
+
fragments = []
|
|
270
|
+
|
|
271
|
+
if progress:
|
|
272
|
+
print(Colors.dim("Scanning fragments..."), end=" ", flush=True)
|
|
273
|
+
|
|
274
|
+
for layer_path in layer_paths:
|
|
275
|
+
layer_name = _get_layer_name(layer_path)
|
|
276
|
+
fragments_dir = os.path.join(layer_path, "conf", "fragments")
|
|
277
|
+
|
|
278
|
+
if not os.path.isdir(fragments_dir):
|
|
279
|
+
continue
|
|
280
|
+
|
|
281
|
+
for root, dirs, files in os.walk(fragments_dir):
|
|
282
|
+
# Get domain relative to fragments_dir
|
|
283
|
+
domain = os.path.relpath(root, fragments_dir)
|
|
284
|
+
if domain == ".":
|
|
285
|
+
domain = ""
|
|
286
|
+
|
|
287
|
+
for conf_file in sorted(files):
|
|
288
|
+
if not conf_file.endswith(".conf") or conf_file.startswith("."):
|
|
289
|
+
continue
|
|
290
|
+
|
|
291
|
+
conf_path = os.path.join(root, conf_file)
|
|
292
|
+
name_base = conf_file[:-5] # Remove .conf
|
|
293
|
+
|
|
294
|
+
# Build fragment name: layer/domain/name
|
|
295
|
+
if domain:
|
|
296
|
+
fragment_name = f"{layer_name}/{domain}/{name_base}"
|
|
297
|
+
else:
|
|
298
|
+
fragment_name = f"{layer_name}/{name_base}"
|
|
299
|
+
|
|
300
|
+
# Normalize path separators
|
|
301
|
+
fragment_name = fragment_name.replace("\\", "/")
|
|
302
|
+
|
|
303
|
+
summary, description = _parse_fragment_metadata(conf_path)
|
|
304
|
+
|
|
305
|
+
fragments.append(Fragment(
|
|
306
|
+
name=fragment_name,
|
|
307
|
+
path=conf_path,
|
|
308
|
+
layer=layer_name,
|
|
309
|
+
domain=domain,
|
|
310
|
+
summary=summary or "(no summary)",
|
|
311
|
+
description=description or "",
|
|
312
|
+
is_enabled=False,
|
|
313
|
+
is_builtin=False,
|
|
314
|
+
))
|
|
315
|
+
|
|
316
|
+
if progress:
|
|
317
|
+
print(Colors.dim(f"{len(fragments)} fragments found"))
|
|
318
|
+
|
|
319
|
+
return fragments
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def _get_enabled_builtin_values(
|
|
323
|
+
enabled_fragments: List[str],
|
|
324
|
+
prefixes: List[BuiltinPrefix],
|
|
325
|
+
) -> Dict[str, str]:
|
|
326
|
+
"""
|
|
327
|
+
Extract current values for builtin fragments.
|
|
328
|
+
|
|
329
|
+
E.g., if OE_FRAGMENTS contains "machine/qemuarm64", return {"machine": "qemuarm64"}
|
|
330
|
+
"""
|
|
331
|
+
result = {}
|
|
332
|
+
prefix_names = {p.prefix for p in prefixes}
|
|
333
|
+
|
|
334
|
+
for fragment in enabled_fragments:
|
|
335
|
+
if "/" in fragment:
|
|
336
|
+
prefix, value = fragment.split("/", 1)
|
|
337
|
+
if prefix in prefix_names:
|
|
338
|
+
result[prefix] = value
|
|
339
|
+
|
|
340
|
+
return result
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
# =============================================================================
|
|
344
|
+
# FZF Browser
|
|
345
|
+
# =============================================================================
|
|
346
|
+
|
|
347
|
+
def _build_fragment_menu(
|
|
348
|
+
fragments: List[Fragment],
|
|
349
|
+
builtin_prefixes: List[BuiltinPrefix],
|
|
350
|
+
enabled_builtins: Dict[str, str],
|
|
351
|
+
toolcfg_path: str = "",
|
|
352
|
+
) -> str:
|
|
353
|
+
"""Build fzf menu input with fragments grouped by layer/domain."""
|
|
354
|
+
lines = []
|
|
355
|
+
|
|
356
|
+
# Calculate column widths
|
|
357
|
+
max_name_len = max(len(f.name) for f in fragments) if fragments else 30
|
|
358
|
+
max_name_len = min(max_name_len, 50)
|
|
359
|
+
|
|
360
|
+
# Add config file entry at the top
|
|
361
|
+
if toolcfg_path:
|
|
362
|
+
config_name = "⚙ toolcfg.conf"
|
|
363
|
+
config_exists = os.path.isfile(toolcfg_path)
|
|
364
|
+
status = Colors.cyan("[config]") if config_exists else Colors.dim("[config]")
|
|
365
|
+
summary = toolcfg_path if config_exists else "(will be created)"
|
|
366
|
+
lines.append(f"__CONFIG__\t{Colors.cyan(f'{config_name:<{max_name_len}}')} {status} {summary}")
|
|
367
|
+
lines.append(f"---\t{Colors.dim('── Fragments ──')}")
|
|
368
|
+
|
|
369
|
+
# Show enabled fragments first
|
|
370
|
+
enabled = [f for f in fragments if f.is_enabled]
|
|
371
|
+
disabled = [f for f in fragments if not f.is_enabled]
|
|
372
|
+
|
|
373
|
+
# Add builtin prefixes as special entries
|
|
374
|
+
for prefix in builtin_prefixes:
|
|
375
|
+
current_value = enabled_builtins.get(prefix.prefix, "")
|
|
376
|
+
if current_value:
|
|
377
|
+
name = f"{prefix.prefix}/{current_value}"
|
|
378
|
+
status = terminal_color("fragment_enabled", "[enabled]")
|
|
379
|
+
summary = f"Sets {prefix.variable} = {current_value}"
|
|
380
|
+
colored_name = terminal_color("fragment_enabled", f"{name:<{max_name_len}}")
|
|
381
|
+
line = f"{name}\t{colored_name} {status} {summary}"
|
|
382
|
+
else:
|
|
383
|
+
name = f"{prefix.prefix}/..."
|
|
384
|
+
status = Colors.dim("[builtin]")
|
|
385
|
+
summary = f"Sets {prefix.variable} = <value>"
|
|
386
|
+
line = f"{name}\t{Colors.dim(f'{name:<{max_name_len}}')} {status} {summary}"
|
|
387
|
+
lines.append(line)
|
|
388
|
+
|
|
389
|
+
if builtin_prefixes:
|
|
390
|
+
lines.append(f"---\t{Colors.dim('── File Fragments ──')}")
|
|
391
|
+
|
|
392
|
+
# Enabled fragments
|
|
393
|
+
if enabled:
|
|
394
|
+
for f in enabled:
|
|
395
|
+
status = terminal_color("fragment_enabled", "[enabled]")
|
|
396
|
+
colored_name = terminal_color("fragment_enabled", f"{f.name:<{max_name_len}}")
|
|
397
|
+
line = f"{f.name}\t{colored_name} {status} {f.summary[:50]}"
|
|
398
|
+
lines.append(line)
|
|
399
|
+
|
|
400
|
+
# Disabled fragments
|
|
401
|
+
if disabled:
|
|
402
|
+
if enabled:
|
|
403
|
+
lines.append(f"---\t{Colors.dim('── Available ──')}")
|
|
404
|
+
for f in disabled:
|
|
405
|
+
status = Colors.dim("[ ]")
|
|
406
|
+
line = f"{f.name}\t{f.name:<{max_name_len}} {status} {f.summary[:50]}"
|
|
407
|
+
lines.append(line)
|
|
408
|
+
|
|
409
|
+
return "\n".join(lines)
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def _fragment_fzf_browser(
|
|
413
|
+
fragments: List[Fragment],
|
|
414
|
+
builtin_prefixes: List[BuiltinPrefix],
|
|
415
|
+
enabled_builtins: Dict[str, str],
|
|
416
|
+
toolcfg_path: str,
|
|
417
|
+
) -> int:
|
|
418
|
+
"""
|
|
419
|
+
Interactive fzf browser for fragments.
|
|
420
|
+
|
|
421
|
+
Key bindings:
|
|
422
|
+
- Enter: Toggle enable/disable
|
|
423
|
+
- Tab: Multi-select fragments
|
|
424
|
+
- e: Enable all selected
|
|
425
|
+
- d: Disable all selected
|
|
426
|
+
- v: View fragment file content
|
|
427
|
+
- c: View/edit toolcfg.conf
|
|
428
|
+
- ?: Toggle preview
|
|
429
|
+
- q: Quit
|
|
430
|
+
"""
|
|
431
|
+
if not fragments and not builtin_prefixes:
|
|
432
|
+
print("No fragments found.")
|
|
433
|
+
return 1
|
|
434
|
+
|
|
435
|
+
if not fzf_available():
|
|
436
|
+
# Fall back to text list
|
|
437
|
+
_list_fragments_text(fragments, builtin_prefixes, enabled_builtins, verbose=False)
|
|
438
|
+
return 0
|
|
439
|
+
|
|
440
|
+
# Build preview script
|
|
441
|
+
preview_data = {f.name: {
|
|
442
|
+
"name": f.name,
|
|
443
|
+
"path": f.path,
|
|
444
|
+
"layer": f.layer,
|
|
445
|
+
"domain": f.domain,
|
|
446
|
+
"summary": f.summary,
|
|
447
|
+
"description": f.description,
|
|
448
|
+
"is_enabled": f.is_enabled,
|
|
449
|
+
"is_builtin": f.is_builtin,
|
|
450
|
+
} for f in fragments}
|
|
451
|
+
|
|
452
|
+
# Add builtin info
|
|
453
|
+
for prefix in builtin_prefixes:
|
|
454
|
+
value = enabled_builtins.get(prefix.prefix, "")
|
|
455
|
+
if value:
|
|
456
|
+
key = f"{prefix.prefix}/{value}"
|
|
457
|
+
preview_data[key] = {
|
|
458
|
+
"name": key,
|
|
459
|
+
"path": "",
|
|
460
|
+
"layer": "",
|
|
461
|
+
"domain": "",
|
|
462
|
+
"summary": f"Sets {prefix.variable} = {value}",
|
|
463
|
+
"description": f"Built-in fragment that sets the {prefix.variable} variable.",
|
|
464
|
+
"is_enabled": True,
|
|
465
|
+
"is_builtin": True,
|
|
466
|
+
}
|
|
467
|
+
# Also add the generic entry
|
|
468
|
+
key = f"{prefix.prefix}/..."
|
|
469
|
+
preview_data[key] = {
|
|
470
|
+
"name": key,
|
|
471
|
+
"path": "",
|
|
472
|
+
"layer": "",
|
|
473
|
+
"domain": "",
|
|
474
|
+
"summary": f"Sets {prefix.variable} = <value>",
|
|
475
|
+
"description": f"Built-in fragment. Use '{prefix.prefix}/<value>' to set {prefix.variable}.",
|
|
476
|
+
"is_enabled": False,
|
|
477
|
+
"is_builtin": True,
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
# Add config file entry
|
|
481
|
+
preview_data["__CONFIG__"] = {
|
|
482
|
+
"name": "toolcfg.conf",
|
|
483
|
+
"path": toolcfg_path,
|
|
484
|
+
"layer": "",
|
|
485
|
+
"domain": "",
|
|
486
|
+
"summary": "OE configuration file for fragments",
|
|
487
|
+
"description": "This file stores your enabled fragments (OE_FRAGMENTS variable).",
|
|
488
|
+
"is_enabled": False,
|
|
489
|
+
"is_builtin": False,
|
|
490
|
+
"is_config": True,
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
preview_data_file = tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False)
|
|
494
|
+
json.dump(preview_data, preview_data_file)
|
|
495
|
+
preview_data_file.close()
|
|
496
|
+
|
|
497
|
+
# Create preview script
|
|
498
|
+
preview_script = tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False)
|
|
499
|
+
preview_script.write(f'''#!/usr/bin/env python3
|
|
500
|
+
import json
|
|
501
|
+
import sys
|
|
502
|
+
import os
|
|
503
|
+
|
|
504
|
+
key = sys.argv[1] if len(sys.argv) > 1 else ""
|
|
505
|
+
key = key.strip("'\\"")
|
|
506
|
+
|
|
507
|
+
with open("{preview_data_file.name}", "r") as f:
|
|
508
|
+
data = json.load(f)
|
|
509
|
+
|
|
510
|
+
frag = data.get(key, {{}})
|
|
511
|
+
if not frag:
|
|
512
|
+
print(f"No data for: {{key}}")
|
|
513
|
+
sys.exit(0)
|
|
514
|
+
|
|
515
|
+
# Special handling for config file entry
|
|
516
|
+
if frag.get('is_config'):
|
|
517
|
+
path = frag.get('path', '')
|
|
518
|
+
print("\\033[1mtoolcfg.conf\\033[0m")
|
|
519
|
+
print(f"Path: {{path}}")
|
|
520
|
+
print()
|
|
521
|
+
if path and os.path.isfile(path):
|
|
522
|
+
print("\\033[36mContent:\\033[0m")
|
|
523
|
+
print("-" * 40)
|
|
524
|
+
with open(path, 'r') as f:
|
|
525
|
+
print(f.read())
|
|
526
|
+
else:
|
|
527
|
+
print("\\033[33mFile does not exist yet.\\033[0m")
|
|
528
|
+
print("It will be created when you enable a fragment.")
|
|
529
|
+
sys.exit(0)
|
|
530
|
+
|
|
531
|
+
print(f"\\033[1m{{frag.get('name', '')}}\\033[0m")
|
|
532
|
+
if frag.get('is_enabled'):
|
|
533
|
+
print("\\033[32mStatus: Enabled\\033[0m")
|
|
534
|
+
else:
|
|
535
|
+
print("Status: Disabled")
|
|
536
|
+
print()
|
|
537
|
+
|
|
538
|
+
if frag.get('path'):
|
|
539
|
+
print(f"Path: {{frag['path']}}")
|
|
540
|
+
print(f"Layer: {{frag.get('layer', '')}}")
|
|
541
|
+
if frag.get('domain'):
|
|
542
|
+
print(f"Domain: {{frag['domain']}}")
|
|
543
|
+
print()
|
|
544
|
+
|
|
545
|
+
print("\\033[36mSummary:\\033[0m")
|
|
546
|
+
print(f" {{frag.get('summary', '')}}")
|
|
547
|
+
print()
|
|
548
|
+
|
|
549
|
+
if frag.get('description'):
|
|
550
|
+
print("\\033[36mDescription:\\033[0m")
|
|
551
|
+
for line in frag['description'].split('\\n'):
|
|
552
|
+
print(f" {{line}}")
|
|
553
|
+
print()
|
|
554
|
+
|
|
555
|
+
# Show file content for non-builtin fragments
|
|
556
|
+
if frag.get('path') and os.path.isfile(frag['path']):
|
|
557
|
+
print("\\033[36mContent:\\033[0m")
|
|
558
|
+
print("-" * 40)
|
|
559
|
+
with open(frag['path'], 'r') as f:
|
|
560
|
+
print(f.read())
|
|
561
|
+
''')
|
|
562
|
+
preview_script.close()
|
|
563
|
+
|
|
564
|
+
preview_cmd = f'python3 "{preview_script.name}" {{1}}'
|
|
565
|
+
|
|
566
|
+
# Header (two lines)
|
|
567
|
+
header = "Enter=toggle | Tab=select | +/e=enable | -/d=disable\nv=view | c=edit conf | ?=preview | q=quit"
|
|
568
|
+
|
|
569
|
+
preview_window = get_preview_window_arg("50%")
|
|
570
|
+
|
|
571
|
+
fzf_args = [
|
|
572
|
+
"fzf",
|
|
573
|
+
"--multi",
|
|
574
|
+
"--ansi",
|
|
575
|
+
"--height", "100%",
|
|
576
|
+
"--layout=reverse-list",
|
|
577
|
+
"--header", header,
|
|
578
|
+
"--prompt", "Fragment: ",
|
|
579
|
+
"--with-nth", "2..",
|
|
580
|
+
"--delimiter", "\t",
|
|
581
|
+
"--preview", preview_cmd,
|
|
582
|
+
"--preview-window", preview_window,
|
|
583
|
+
"--bind", "?:toggle-preview",
|
|
584
|
+
"--bind", "pgup:preview-page-up",
|
|
585
|
+
"--bind", "pgdn:preview-page-down",
|
|
586
|
+
"--bind", "esc:abort",
|
|
587
|
+
"--expect", "e,d,v,c,q,+,-",
|
|
588
|
+
]
|
|
589
|
+
|
|
590
|
+
fzf_args.extend(get_fzf_preview_resize_bindings())
|
|
591
|
+
fzf_args.extend(get_fzf_color_args())
|
|
592
|
+
|
|
593
|
+
next_selection = None # Track item to select after enable/disable
|
|
594
|
+
|
|
595
|
+
try:
|
|
596
|
+
while True:
|
|
597
|
+
# Rebuild menu with current state
|
|
598
|
+
menu_input = _build_fragment_menu(fragments, builtin_prefixes, enabled_builtins, toolcfg_path)
|
|
599
|
+
|
|
600
|
+
# Build position binding if we have a selection to restore
|
|
601
|
+
run_args = fzf_args.copy()
|
|
602
|
+
if next_selection:
|
|
603
|
+
lines = menu_input.split('\n')
|
|
604
|
+
for i, line in enumerate(lines):
|
|
605
|
+
if line.startswith(next_selection + '\t'):
|
|
606
|
+
run_args.extend(["--bind", f"load:pos({i + 1})"])
|
|
607
|
+
break
|
|
608
|
+
next_selection = None
|
|
609
|
+
|
|
610
|
+
result = subprocess.run(
|
|
611
|
+
run_args,
|
|
612
|
+
input=menu_input,
|
|
613
|
+
stdout=subprocess.PIPE,
|
|
614
|
+
text=True,
|
|
615
|
+
)
|
|
616
|
+
|
|
617
|
+
if result.returncode != 0 or not result.stdout.strip():
|
|
618
|
+
break
|
|
619
|
+
|
|
620
|
+
# Parse output
|
|
621
|
+
lines = result.stdout.strip().split("\n")
|
|
622
|
+
key = lines[0].strip() if lines else ""
|
|
623
|
+
selected = [l.split("\t")[0].strip() for l in lines[1:] if l.strip() and not l.startswith("---")]
|
|
624
|
+
|
|
625
|
+
if key == "q" or not selected:
|
|
626
|
+
break
|
|
627
|
+
|
|
628
|
+
if key == "v":
|
|
629
|
+
# View fragment content
|
|
630
|
+
for name in selected:
|
|
631
|
+
frag = next((f for f in fragments if f.name == name), None)
|
|
632
|
+
if frag and frag.path and os.path.isfile(frag.path):
|
|
633
|
+
pager = os.environ.get("PAGER", "less")
|
|
634
|
+
subprocess.run([pager, frag.path])
|
|
635
|
+
continue
|
|
636
|
+
|
|
637
|
+
if key == "c":
|
|
638
|
+
# View/edit toolcfg.conf
|
|
639
|
+
if os.path.isfile(toolcfg_path):
|
|
640
|
+
editor = os.environ.get("EDITOR", os.environ.get("PAGER", "less"))
|
|
641
|
+
subprocess.run([editor, toolcfg_path])
|
|
642
|
+
# Refresh state after potential edits
|
|
643
|
+
enabled_list = _parse_enabled_fragments(toolcfg_path)
|
|
644
|
+
for f in fragments:
|
|
645
|
+
f.is_enabled = f.name in enabled_list
|
|
646
|
+
enabled_builtins.clear()
|
|
647
|
+
enabled_builtins.update(_get_enabled_builtin_values(enabled_list, builtin_prefixes))
|
|
648
|
+
else:
|
|
649
|
+
print(f"No toolcfg.conf yet (will be created when you enable a fragment)")
|
|
650
|
+
input("Press Enter to continue...")
|
|
651
|
+
continue
|
|
652
|
+
|
|
653
|
+
# Handle enable/disable
|
|
654
|
+
to_enable = []
|
|
655
|
+
to_disable = []
|
|
656
|
+
|
|
657
|
+
for name in selected:
|
|
658
|
+
# Skip separator lines and config entry
|
|
659
|
+
if name == "---" or name.startswith("──") or name == "__CONFIG__":
|
|
660
|
+
continue
|
|
661
|
+
|
|
662
|
+
# Check if this is a builtin prompt (e.g., "machine/...")
|
|
663
|
+
if name.endswith("/..."):
|
|
664
|
+
prefix = name[:-4]
|
|
665
|
+
# Prompt for value
|
|
666
|
+
try:
|
|
667
|
+
value = input(f"Enter value for {prefix}/: ").strip()
|
|
668
|
+
if value:
|
|
669
|
+
to_enable.append(f"{prefix}/{value}")
|
|
670
|
+
except (EOFError, KeyboardInterrupt):
|
|
671
|
+
print()
|
|
672
|
+
continue
|
|
673
|
+
|
|
674
|
+
frag = next((f for f in fragments if f.name == name), None)
|
|
675
|
+
is_enabled = frag.is_enabled if frag else name in enabled_builtins.values()
|
|
676
|
+
|
|
677
|
+
if key in ("e", "+") or (key == "" and not is_enabled):
|
|
678
|
+
to_enable.append(name)
|
|
679
|
+
elif key in ("d", "-") or (key == "" and is_enabled):
|
|
680
|
+
to_disable.append(name)
|
|
681
|
+
|
|
682
|
+
# Update toolcfg.conf
|
|
683
|
+
if to_enable or to_disable:
|
|
684
|
+
modified = _update_oe_fragments(toolcfg_path, to_enable, to_disable, builtin_prefixes)
|
|
685
|
+
if modified:
|
|
686
|
+
# Refresh state
|
|
687
|
+
enabled_list = _parse_enabled_fragments(toolcfg_path)
|
|
688
|
+
for f in fragments:
|
|
689
|
+
f.is_enabled = f.name in enabled_list
|
|
690
|
+
enabled_builtins = _get_enabled_builtin_values(enabled_list, builtin_prefixes)
|
|
691
|
+
|
|
692
|
+
# Preserve selection on the first modified item
|
|
693
|
+
if selected:
|
|
694
|
+
next_selection = selected[0]
|
|
695
|
+
|
|
696
|
+
finally:
|
|
697
|
+
# Clean up temp files
|
|
698
|
+
for f in [preview_data_file.name, preview_script.name]:
|
|
699
|
+
try:
|
|
700
|
+
os.unlink(f)
|
|
701
|
+
except OSError:
|
|
702
|
+
pass
|
|
703
|
+
|
|
704
|
+
return 0
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
# =============================================================================
|
|
708
|
+
# Text Output
|
|
709
|
+
# =============================================================================
|
|
710
|
+
|
|
711
|
+
def _list_fragments_text(
|
|
712
|
+
fragments: List[Fragment],
|
|
713
|
+
builtin_prefixes: List[BuiltinPrefix],
|
|
714
|
+
enabled_builtins: Dict[str, str],
|
|
715
|
+
verbose: bool = False,
|
|
716
|
+
) -> None:
|
|
717
|
+
"""Print fragments as text output."""
|
|
718
|
+
# Print builtin info
|
|
719
|
+
print("Built-in fragments:")
|
|
720
|
+
for prefix in builtin_prefixes:
|
|
721
|
+
value = enabled_builtins.get(prefix.prefix, "")
|
|
722
|
+
if value:
|
|
723
|
+
status = terminal_color("fragment_enabled", "[enabled]")
|
|
724
|
+
print(f" {prefix.prefix}/{value} {status} Sets {prefix.variable} = {value}")
|
|
725
|
+
else:
|
|
726
|
+
print(f" {prefix.prefix}/... [builtin] Sets {prefix.variable} = <value>")
|
|
727
|
+
print()
|
|
728
|
+
|
|
729
|
+
# Group by layer
|
|
730
|
+
by_layer: Dict[str, List[Fragment]] = {}
|
|
731
|
+
for f in fragments:
|
|
732
|
+
by_layer.setdefault(f.layer, []).append(f)
|
|
733
|
+
|
|
734
|
+
for layer, layer_fragments in sorted(by_layer.items()):
|
|
735
|
+
print(f"Layer: {layer}")
|
|
736
|
+
for f in sorted(layer_fragments, key=lambda x: x.name):
|
|
737
|
+
if f.is_enabled:
|
|
738
|
+
status = terminal_color("fragment_enabled", "[enabled]")
|
|
739
|
+
else:
|
|
740
|
+
status = "[ ]"
|
|
741
|
+
if verbose:
|
|
742
|
+
print(f" {f.name}")
|
|
743
|
+
if f.is_enabled:
|
|
744
|
+
print(f" Status: {terminal_color('fragment_enabled', 'Enabled')}")
|
|
745
|
+
else:
|
|
746
|
+
print(f" Status: Disabled")
|
|
747
|
+
print(f" Path: {f.path}")
|
|
748
|
+
print(f" Summary: {f.summary}")
|
|
749
|
+
if f.description:
|
|
750
|
+
print(f" Description: {f.description}")
|
|
751
|
+
print()
|
|
752
|
+
else:
|
|
753
|
+
print(f" {status} {f.name:<40} {f.summary[:40]}")
|
|
754
|
+
print()
|
|
755
|
+
|
|
756
|
+
|
|
757
|
+
def _show_fragment(name: str, fragments: List[Fragment]) -> int:
|
|
758
|
+
"""Show content of a fragment."""
|
|
759
|
+
frag = next((f for f in fragments if f.name == name), None)
|
|
760
|
+
|
|
761
|
+
if not frag:
|
|
762
|
+
print(f"Fragment not found: {name}")
|
|
763
|
+
return 1
|
|
764
|
+
|
|
765
|
+
if not frag.path or not os.path.isfile(frag.path):
|
|
766
|
+
print(f"Fragment has no file: {name}")
|
|
767
|
+
return 1
|
|
768
|
+
|
|
769
|
+
print(f"Fragment: {frag.name}")
|
|
770
|
+
print(f"Path: {frag.path}")
|
|
771
|
+
print(f"Summary: {frag.summary}")
|
|
772
|
+
print()
|
|
773
|
+
print("-" * 60)
|
|
774
|
+
with open(frag.path, "r") as f:
|
|
775
|
+
print(f.read())
|
|
776
|
+
|
|
777
|
+
return 0
|
|
778
|
+
|
|
779
|
+
|
|
780
|
+
# =============================================================================
|
|
781
|
+
# Main Entry Point
|
|
782
|
+
# =============================================================================
|
|
783
|
+
|
|
784
|
+
def run_fragment(args) -> int:
|
|
785
|
+
"""
|
|
786
|
+
Main entry point for fragment command.
|
|
787
|
+
|
|
788
|
+
Args:
|
|
789
|
+
args: Parsed command line arguments with:
|
|
790
|
+
- fragment_command: Subcommand (list, enable, disable, show)
|
|
791
|
+
- fragmentname: Fragment name(s) for enable/disable/show
|
|
792
|
+
- confpath: Path to toolcfg.conf
|
|
793
|
+
- verbose: Show descriptions in list mode
|
|
794
|
+
"""
|
|
795
|
+
bblayers = getattr(args, "bblayers", None)
|
|
796
|
+
confpath = getattr(args, "confpath", None)
|
|
797
|
+
fragment_command = getattr(args, "fragment_command", None)
|
|
798
|
+
fragmentnames = getattr(args, "fragmentname", None)
|
|
799
|
+
verbose = getattr(args, "verbose", False)
|
|
800
|
+
list_mode = getattr(args, "list", False)
|
|
801
|
+
|
|
802
|
+
# Resolve bblayers.conf
|
|
803
|
+
bblayers_path = resolve_bblayers_path(bblayers)
|
|
804
|
+
if not bblayers_path:
|
|
805
|
+
print("No bblayers.conf found. Run from a Yocto/OE project directory.")
|
|
806
|
+
return 1
|
|
807
|
+
|
|
808
|
+
# Get layer paths
|
|
809
|
+
try:
|
|
810
|
+
layer_paths = extract_layer_paths(bblayers_path)
|
|
811
|
+
except SystemExit:
|
|
812
|
+
return 1
|
|
813
|
+
|
|
814
|
+
# Determine toolcfg.conf path
|
|
815
|
+
toolcfg_path = confpath or _get_toolcfg_path(bblayers_path)
|
|
816
|
+
|
|
817
|
+
# Get enabled fragments
|
|
818
|
+
enabled_list = _parse_enabled_fragments(toolcfg_path) if os.path.exists(toolcfg_path) else []
|
|
819
|
+
|
|
820
|
+
# Discover fragments
|
|
821
|
+
fragments = _discover_fragments(layer_paths)
|
|
822
|
+
|
|
823
|
+
# Mark enabled fragments
|
|
824
|
+
for f in fragments:
|
|
825
|
+
f.is_enabled = f.name in enabled_list
|
|
826
|
+
|
|
827
|
+
# Get builtin prefixes
|
|
828
|
+
builtin_prefixes = DEFAULT_BUILTIN_PREFIXES
|
|
829
|
+
enabled_builtins = _get_enabled_builtin_values(enabled_list, builtin_prefixes)
|
|
830
|
+
|
|
831
|
+
# Handle subcommands
|
|
832
|
+
if fragment_command == "list" or list_mode:
|
|
833
|
+
_list_fragments_text(fragments, builtin_prefixes, enabled_builtins, verbose)
|
|
834
|
+
return 0
|
|
835
|
+
|
|
836
|
+
if fragment_command == "enable":
|
|
837
|
+
if not fragmentnames:
|
|
838
|
+
print("No fragment names specified.")
|
|
839
|
+
return 1
|
|
840
|
+
|
|
841
|
+
# Validate fragments exist
|
|
842
|
+
valid_fragments = []
|
|
843
|
+
for name in fragmentnames:
|
|
844
|
+
# Check if it's a builtin prefix
|
|
845
|
+
prefix = name.split("/", 1)[0] if "/" in name else ""
|
|
846
|
+
is_builtin = prefix in {p.prefix for p in builtin_prefixes}
|
|
847
|
+
|
|
848
|
+
if is_builtin:
|
|
849
|
+
valid_fragments.append(name)
|
|
850
|
+
elif any(f.name == name for f in fragments):
|
|
851
|
+
valid_fragments.append(name)
|
|
852
|
+
else:
|
|
853
|
+
print(f"Fragment not found: {name}")
|
|
854
|
+
return 1
|
|
855
|
+
|
|
856
|
+
modified = _update_oe_fragments(toolcfg_path, valid_fragments, [], builtin_prefixes)
|
|
857
|
+
if modified:
|
|
858
|
+
for name in valid_fragments:
|
|
859
|
+
print(f"Enabled: {name}")
|
|
860
|
+
# Show summary for file fragments
|
|
861
|
+
frag = next((f for f in fragments if f.name == name), None)
|
|
862
|
+
if frag:
|
|
863
|
+
print(f" Summary: {frag.summary}")
|
|
864
|
+
return 0
|
|
865
|
+
|
|
866
|
+
if fragment_command == "disable":
|
|
867
|
+
if not fragmentnames:
|
|
868
|
+
print("No fragment names specified.")
|
|
869
|
+
return 1
|
|
870
|
+
|
|
871
|
+
modified = _update_oe_fragments(toolcfg_path, [], fragmentnames, builtin_prefixes)
|
|
872
|
+
if modified:
|
|
873
|
+
for name in fragmentnames:
|
|
874
|
+
print(f"Disabled: {name}")
|
|
875
|
+
return 0
|
|
876
|
+
|
|
877
|
+
if fragment_command == "show":
|
|
878
|
+
if not fragmentnames:
|
|
879
|
+
print("No fragment name specified.")
|
|
880
|
+
return 1
|
|
881
|
+
return _show_fragment(fragmentnames, fragments)
|
|
882
|
+
|
|
883
|
+
# Default: interactive browser
|
|
884
|
+
return _fragment_fzf_browser(fragments, builtin_prefixes, enabled_builtins, toolcfg_path)
|