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,515 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright (C) 2025 Bruce Ashfield <bruce.ashfield@gmail.com>
|
|
3
|
+
#
|
|
4
|
+
# SPDX-License-Identifier: GPL-2.0-only
|
|
5
|
+
#
|
|
6
|
+
"""Init and bootstrap commands - setup OE/Yocto build environment."""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import shutil
|
|
11
|
+
import subprocess
|
|
12
|
+
import sys
|
|
13
|
+
import time
|
|
14
|
+
from typing import List, Optional
|
|
15
|
+
|
|
16
|
+
from ..core import Colors, get_fzf_color_args, load_defaults, save_defaults
|
|
17
|
+
from .common import add_extra_repo
|
|
18
|
+
|
|
19
|
+
def run_init(args) -> int:
|
|
20
|
+
"""Show OE/Yocto build environment setup command."""
|
|
21
|
+
layers_dir = args.layers_dir
|
|
22
|
+
|
|
23
|
+
# Find oe-init-build-env script
|
|
24
|
+
init_scripts = []
|
|
25
|
+
if os.path.isdir(layers_dir):
|
|
26
|
+
for root, dirs, files in os.walk(layers_dir):
|
|
27
|
+
# Don't descend too deep
|
|
28
|
+
depth = root[len(layers_dir):].count(os.sep)
|
|
29
|
+
if depth >= 3:
|
|
30
|
+
dirs[:] = []
|
|
31
|
+
continue
|
|
32
|
+
if "oe-init-build-env" in files:
|
|
33
|
+
init_scripts.append(os.path.join(root, "oe-init-build-env"))
|
|
34
|
+
|
|
35
|
+
if not init_scripts:
|
|
36
|
+
# Try current directory
|
|
37
|
+
if os.path.isfile("oe-init-build-env"):
|
|
38
|
+
init_scripts.append("./oe-init-build-env")
|
|
39
|
+
|
|
40
|
+
if not init_scripts:
|
|
41
|
+
print(f"Could not find oe-init-build-env in {layers_dir}/")
|
|
42
|
+
print("Try running from your OE/Yocto project root, or use --layers-dir")
|
|
43
|
+
return 1
|
|
44
|
+
|
|
45
|
+
init_script = init_scripts[0] # Use first found
|
|
46
|
+
|
|
47
|
+
# Find template directories (conf/templates/default pattern)
|
|
48
|
+
templates = []
|
|
49
|
+
if os.path.isdir(layers_dir):
|
|
50
|
+
for root, dirs, files in os.walk(layers_dir):
|
|
51
|
+
depth = root[len(layers_dir):].count(os.sep)
|
|
52
|
+
if depth >= 6:
|
|
53
|
+
dirs[:] = []
|
|
54
|
+
continue
|
|
55
|
+
# Check if this directory contains conf/templates/default
|
|
56
|
+
template_path = os.path.join(root, "conf", "templates", "default")
|
|
57
|
+
if os.path.isdir(template_path):
|
|
58
|
+
templates.append(template_path)
|
|
59
|
+
|
|
60
|
+
# Let user pick template if multiple
|
|
61
|
+
selected_template = None
|
|
62
|
+
if templates:
|
|
63
|
+
# Prefer meta-poky template if available
|
|
64
|
+
poky_templates = [t for t in templates if "meta-poky" in t]
|
|
65
|
+
if len(templates) == 1:
|
|
66
|
+
selected_template = templates[0]
|
|
67
|
+
elif poky_templates:
|
|
68
|
+
selected_template = poky_templates[0]
|
|
69
|
+
if len(templates) > 1:
|
|
70
|
+
print(f"Found {len(templates)} templates, using: {selected_template}")
|
|
71
|
+
print(f"Others: {', '.join(t for t in templates if t != selected_template)}")
|
|
72
|
+
else:
|
|
73
|
+
# Use fzf if available and we have a tty
|
|
74
|
+
if shutil.which("fzf") and sys.stdin.isatty():
|
|
75
|
+
try:
|
|
76
|
+
result = subprocess.run(
|
|
77
|
+
["fzf", "--height", "~10", "--header", "Select template (or Esc for none):"] + get_fzf_color_args(),
|
|
78
|
+
input="\n".join(templates),
|
|
79
|
+
stdout=subprocess.PIPE,
|
|
80
|
+
text=True,
|
|
81
|
+
)
|
|
82
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
83
|
+
selected_template = result.stdout.strip()
|
|
84
|
+
except Exception:
|
|
85
|
+
pass
|
|
86
|
+
|
|
87
|
+
if not selected_template:
|
|
88
|
+
# Fall back to numbered selection or first one
|
|
89
|
+
if sys.stdin.isatty():
|
|
90
|
+
print("Multiple templates found:")
|
|
91
|
+
for i, t in enumerate(templates, 1):
|
|
92
|
+
print(f" {i}. {t}")
|
|
93
|
+
print(" 0. (none)")
|
|
94
|
+
try:
|
|
95
|
+
choice = input("Select template number: ").strip()
|
|
96
|
+
if choice and choice != "0":
|
|
97
|
+
idx = int(choice) - 1
|
|
98
|
+
if 0 <= idx < len(templates):
|
|
99
|
+
selected_template = templates[idx]
|
|
100
|
+
except (ValueError, EOFError):
|
|
101
|
+
selected_template = templates[0]
|
|
102
|
+
else:
|
|
103
|
+
selected_template = templates[0]
|
|
104
|
+
|
|
105
|
+
# Build the command
|
|
106
|
+
print()
|
|
107
|
+
print(Colors.bold("Run this command to set up the build environment:"))
|
|
108
|
+
print()
|
|
109
|
+
|
|
110
|
+
if selected_template:
|
|
111
|
+
# Make paths relative to current dir
|
|
112
|
+
template_rel = os.path.relpath(selected_template)
|
|
113
|
+
init_rel = os.path.relpath(init_script)
|
|
114
|
+
cmd = f"TEMPLATECONF=$PWD/{template_rel} source ./{init_rel}"
|
|
115
|
+
else:
|
|
116
|
+
init_rel = os.path.relpath(init_script)
|
|
117
|
+
cmd = f"source ./{init_rel}"
|
|
118
|
+
|
|
119
|
+
print(f" {Colors.cyan(cmd)}")
|
|
120
|
+
print()
|
|
121
|
+
|
|
122
|
+
# Copy to clipboard if possible
|
|
123
|
+
try:
|
|
124
|
+
if shutil.which("xclip"):
|
|
125
|
+
subprocess.run(["xclip", "-selection", "clipboard"], input=cmd.encode(), check=True)
|
|
126
|
+
print(Colors.dim("(copied to clipboard)"))
|
|
127
|
+
elif shutil.which("xsel"):
|
|
128
|
+
subprocess.run(["xsel", "--clipboard", "--input"], input=cmd.encode(), check=True)
|
|
129
|
+
print(Colors.dim("(copied to clipboard)"))
|
|
130
|
+
except Exception:
|
|
131
|
+
pass
|
|
132
|
+
|
|
133
|
+
return 0
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def run_init_shell(args) -> int:
|
|
137
|
+
"""Start a new shell with OE/Yocto build environment sourced."""
|
|
138
|
+
import tempfile
|
|
139
|
+
|
|
140
|
+
layers_dir = args.layers_dir
|
|
141
|
+
|
|
142
|
+
# Find oe-init-build-env script (same logic as run_init)
|
|
143
|
+
init_scripts = []
|
|
144
|
+
if os.path.isdir(layers_dir):
|
|
145
|
+
for root, dirs, files in os.walk(layers_dir):
|
|
146
|
+
depth = root[len(layers_dir):].count(os.sep)
|
|
147
|
+
if depth >= 3:
|
|
148
|
+
dirs[:] = []
|
|
149
|
+
continue
|
|
150
|
+
if "oe-init-build-env" in files:
|
|
151
|
+
init_scripts.append(os.path.join(root, "oe-init-build-env"))
|
|
152
|
+
|
|
153
|
+
if not init_scripts:
|
|
154
|
+
if os.path.isfile("oe-init-build-env"):
|
|
155
|
+
init_scripts.append("./oe-init-build-env")
|
|
156
|
+
|
|
157
|
+
if not init_scripts:
|
|
158
|
+
print(f"Could not find oe-init-build-env in {layers_dir}/")
|
|
159
|
+
print("Try running from your OE/Yocto project root, or use --layers-dir")
|
|
160
|
+
return 1
|
|
161
|
+
|
|
162
|
+
init_script = os.path.abspath(init_scripts[0])
|
|
163
|
+
|
|
164
|
+
# Find template directories
|
|
165
|
+
templates = []
|
|
166
|
+
if os.path.isdir(layers_dir):
|
|
167
|
+
for root, dirs, files in os.walk(layers_dir):
|
|
168
|
+
depth = root[len(layers_dir):].count(os.sep)
|
|
169
|
+
if depth >= 6:
|
|
170
|
+
dirs[:] = []
|
|
171
|
+
continue
|
|
172
|
+
template_path = os.path.join(root, "conf", "templates", "default")
|
|
173
|
+
if os.path.isdir(template_path):
|
|
174
|
+
templates.append(template_path)
|
|
175
|
+
|
|
176
|
+
# Select template (prefer meta-poky)
|
|
177
|
+
selected_template = None
|
|
178
|
+
if templates:
|
|
179
|
+
poky_templates = [t for t in templates if "meta-poky" in t]
|
|
180
|
+
if len(templates) == 1:
|
|
181
|
+
selected_template = templates[0]
|
|
182
|
+
elif poky_templates:
|
|
183
|
+
selected_template = poky_templates[0]
|
|
184
|
+
elif shutil.which("fzf") and sys.stdin.isatty():
|
|
185
|
+
try:
|
|
186
|
+
result = subprocess.run(
|
|
187
|
+
["fzf", "--height", "~10", "--header", "Select template (or Esc for none):"] + get_fzf_color_args(),
|
|
188
|
+
input="\n".join(templates),
|
|
189
|
+
stdout=subprocess.PIPE,
|
|
190
|
+
text=True,
|
|
191
|
+
)
|
|
192
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
193
|
+
selected_template = result.stdout.strip()
|
|
194
|
+
except Exception:
|
|
195
|
+
pass
|
|
196
|
+
if not selected_template and templates:
|
|
197
|
+
selected_template = templates[0]
|
|
198
|
+
|
|
199
|
+
if selected_template:
|
|
200
|
+
selected_template = os.path.abspath(selected_template)
|
|
201
|
+
|
|
202
|
+
# Determine which shell to use
|
|
203
|
+
shell = os.environ.get("SHELL", "/bin/bash")
|
|
204
|
+
shell_name = os.path.basename(shell)
|
|
205
|
+
|
|
206
|
+
# Build the source command
|
|
207
|
+
cwd = os.getcwd()
|
|
208
|
+
if selected_template:
|
|
209
|
+
source_cmd = f'export TEMPLATECONF="{selected_template}" && source "{init_script}"'
|
|
210
|
+
else:
|
|
211
|
+
source_cmd = f'source "{init_script}"'
|
|
212
|
+
|
|
213
|
+
print(Colors.bold("Starting shell with OE/Yocto build environment..."))
|
|
214
|
+
print(f" Init script: {Colors.cyan(init_script)}")
|
|
215
|
+
if selected_template:
|
|
216
|
+
print(f" Template: {Colors.cyan(selected_template)}")
|
|
217
|
+
print()
|
|
218
|
+
|
|
219
|
+
# Create a temporary rcfile that sources the init script
|
|
220
|
+
# This ensures the environment is set up when the shell starts
|
|
221
|
+
if shell_name in ("bash", "sh"):
|
|
222
|
+
# For bash, create an rcfile that sources user's bashrc then the init script
|
|
223
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".bashrc", delete=False) as f:
|
|
224
|
+
f.write("# Temporary rcfile for bit init shell\n")
|
|
225
|
+
# Source user's bashrc first for their customizations
|
|
226
|
+
bashrc = os.path.expanduser("~/.bashrc")
|
|
227
|
+
if os.path.isfile(bashrc):
|
|
228
|
+
f.write(f'[ -f "{bashrc}" ] && source "{bashrc}"\n')
|
|
229
|
+
# Change to original directory (sourcing init script changes cwd)
|
|
230
|
+
f.write(f'cd "{cwd}"\n')
|
|
231
|
+
# Source the OE init script
|
|
232
|
+
f.write(f'{source_cmd}\n')
|
|
233
|
+
# Set a custom prompt to indicate we're in a bit shell
|
|
234
|
+
f.write('export PS1="(oe) $PS1"\n')
|
|
235
|
+
rcfile = f.name
|
|
236
|
+
|
|
237
|
+
try:
|
|
238
|
+
# Start bash with our custom rcfile
|
|
239
|
+
result = subprocess.run([shell, "--rcfile", rcfile])
|
|
240
|
+
return result.returncode
|
|
241
|
+
finally:
|
|
242
|
+
# Clean up temp file
|
|
243
|
+
try:
|
|
244
|
+
os.unlink(rcfile)
|
|
245
|
+
except Exception:
|
|
246
|
+
pass
|
|
247
|
+
|
|
248
|
+
elif shell_name == "zsh":
|
|
249
|
+
# For zsh, use ZDOTDIR to point to a temp directory with our .zshrc
|
|
250
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
251
|
+
zshrc = os.path.join(tmpdir, ".zshrc")
|
|
252
|
+
with open(zshrc, "w") as f:
|
|
253
|
+
f.write("# Temporary zshrc for bit init shell\n")
|
|
254
|
+
# Source user's zshrc first
|
|
255
|
+
user_zshrc = os.path.expanduser("~/.zshrc")
|
|
256
|
+
if os.path.isfile(user_zshrc):
|
|
257
|
+
f.write(f'[ -f "{user_zshrc}" ] && source "{user_zshrc}"\n')
|
|
258
|
+
f.write(f'cd "{cwd}"\n')
|
|
259
|
+
f.write(f'{source_cmd}\n')
|
|
260
|
+
f.write('export PS1="(oe) $PS1"\n')
|
|
261
|
+
|
|
262
|
+
env = os.environ.copy()
|
|
263
|
+
env["ZDOTDIR"] = tmpdir
|
|
264
|
+
result = subprocess.run([shell], env=env)
|
|
265
|
+
return result.returncode
|
|
266
|
+
|
|
267
|
+
else:
|
|
268
|
+
# Fallback: use bash -c to source and exec
|
|
269
|
+
print(f"Note: Using bash instead of {shell_name}")
|
|
270
|
+
bash_cmd = f'{source_cmd} && exec bash'
|
|
271
|
+
result = subprocess.run(["bash", "-c", bash_cmd])
|
|
272
|
+
return result.returncode
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def run_bootstrap(args) -> int:
|
|
276
|
+
"""Clone core Yocto/OE repositories for a new project."""
|
|
277
|
+
branch = args.branch
|
|
278
|
+
layers_dir = args.layers_dir
|
|
279
|
+
do_clone = args.clone
|
|
280
|
+
|
|
281
|
+
# Core repos for Yocto/OE setup
|
|
282
|
+
# See: https://docs.yoctoproject.org/dev-manual/poky-manual-setup.html
|
|
283
|
+
repos = [
|
|
284
|
+
{
|
|
285
|
+
"name": "bitbake",
|
|
286
|
+
"url": "https://git.openembedded.org/bitbake",
|
|
287
|
+
"desc": "Build engine",
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
"name": "openembedded-core",
|
|
291
|
+
"url": "https://git.openembedded.org/openembedded-core",
|
|
292
|
+
"desc": "Core metadata and recipes",
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
"name": "meta-yocto",
|
|
296
|
+
"url": "https://git.yoctoproject.org/meta-yocto",
|
|
297
|
+
"desc": "Yocto Project reference distribution (poky)",
|
|
298
|
+
},
|
|
299
|
+
]
|
|
300
|
+
|
|
301
|
+
# Check if layers_dir already exists and has content
|
|
302
|
+
if os.path.isdir(layers_dir) and os.listdir(layers_dir):
|
|
303
|
+
existing = os.listdir(layers_dir)
|
|
304
|
+
print(f"Warning: {layers_dir}/ already exists with: {', '.join(existing[:5])}")
|
|
305
|
+
if len(existing) > 5:
|
|
306
|
+
print(f" ... and {len(existing) - 5} more")
|
|
307
|
+
if do_clone:
|
|
308
|
+
try:
|
|
309
|
+
resp = input("Continue anyway? [y/N] ").strip().lower()
|
|
310
|
+
if resp != "y":
|
|
311
|
+
return 1
|
|
312
|
+
except (EOFError, KeyboardInterrupt):
|
|
313
|
+
return 1
|
|
314
|
+
|
|
315
|
+
print()
|
|
316
|
+
print(Colors.bold(f"Core Yocto/OE repositories (branch: {branch}):"))
|
|
317
|
+
print()
|
|
318
|
+
|
|
319
|
+
clone_cmds = []
|
|
320
|
+
for repo in repos:
|
|
321
|
+
target = os.path.join(layers_dir, repo["name"])
|
|
322
|
+
cmd = f"git clone -b {branch} {repo['url']} {target}"
|
|
323
|
+
clone_cmds.append(cmd)
|
|
324
|
+
print(f" {Colors.cyan(repo['name'])}: {repo['desc']}")
|
|
325
|
+
print(f" {Colors.dim(cmd)}")
|
|
326
|
+
print()
|
|
327
|
+
|
|
328
|
+
if do_clone:
|
|
329
|
+
# Create layers dir if needed
|
|
330
|
+
os.makedirs(layers_dir, exist_ok=True)
|
|
331
|
+
|
|
332
|
+
print(Colors.bold("Cloning repositories..."))
|
|
333
|
+
print()
|
|
334
|
+
|
|
335
|
+
for i, (repo, cmd) in enumerate(zip(repos, clone_cmds)):
|
|
336
|
+
target = os.path.join(layers_dir, repo["name"])
|
|
337
|
+
if os.path.isdir(target):
|
|
338
|
+
print(f" {Colors.yellow('skip')} {repo['name']} (already exists)")
|
|
339
|
+
continue
|
|
340
|
+
|
|
341
|
+
print(f" {Colors.cyan('clone')} {repo['name']}...")
|
|
342
|
+
result = subprocess.run(
|
|
343
|
+
["git", "clone", "-b", branch, repo["url"], target],
|
|
344
|
+
capture_output=True,
|
|
345
|
+
text=True,
|
|
346
|
+
)
|
|
347
|
+
if result.returncode != 0:
|
|
348
|
+
print(f" {Colors.red('failed')}: {result.stderr.strip()}")
|
|
349
|
+
return 1
|
|
350
|
+
print(f" {Colors.green('done')}")
|
|
351
|
+
|
|
352
|
+
# Add bitbake to tracked repos (it's not a layer, so won't be auto-discovered)
|
|
353
|
+
bitbake_path = os.path.abspath(os.path.join(layers_dir, "bitbake"))
|
|
354
|
+
if os.path.isdir(bitbake_path):
|
|
355
|
+
defaults_file = args.defaults_file
|
|
356
|
+
defaults = load_defaults(defaults_file)
|
|
357
|
+
add_extra_repo(defaults_file, defaults, bitbake_path)
|
|
358
|
+
|
|
359
|
+
print()
|
|
360
|
+
print(Colors.green("Bootstrap complete!"))
|
|
361
|
+
print()
|
|
362
|
+
print("Next steps:")
|
|
363
|
+
print(f" 1. Run: {Colors.cyan(f'bit init --layers-dir {layers_dir}')}")
|
|
364
|
+
print(" 2. Source the environment setup command shown")
|
|
365
|
+
print(" 3. Run: bitbake core-image-minimal")
|
|
366
|
+
else:
|
|
367
|
+
print(Colors.dim("Add --execute (or --clone) to execute, or copy/paste the commands manually."))
|
|
368
|
+
|
|
369
|
+
return 0
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def _get_layer_index_cache_path() -> str:
|
|
373
|
+
"""Get path to layer index cache file."""
|
|
374
|
+
cache_dir = os.path.expanduser("~/.cache/bit")
|
|
375
|
+
os.makedirs(cache_dir, exist_ok=True)
|
|
376
|
+
return os.path.join(cache_dir, "layer-index.json")
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def _fetch_layer_index(force: bool = False) -> Optional[list]:
|
|
380
|
+
"""Fetch layer index from API or cache. Returns None on error."""
|
|
381
|
+
import urllib.request
|
|
382
|
+
import urllib.error
|
|
383
|
+
|
|
384
|
+
cache_path = _get_layer_index_cache_path()
|
|
385
|
+
cache_max_age = 2 * 60 * 60 # 2 hours in seconds
|
|
386
|
+
|
|
387
|
+
# Check cache
|
|
388
|
+
if not force and os.path.isfile(cache_path):
|
|
389
|
+
try:
|
|
390
|
+
cache_age = time.time() - os.path.getmtime(cache_path)
|
|
391
|
+
if cache_age < cache_max_age:
|
|
392
|
+
with open(cache_path, "r") as f:
|
|
393
|
+
cache_data = json.load(f)
|
|
394
|
+
if cache_data.get("data"):
|
|
395
|
+
return cache_data["data"]
|
|
396
|
+
except (json.JSONDecodeError, OSError, KeyError):
|
|
397
|
+
pass # Cache invalid, fetch fresh
|
|
398
|
+
|
|
399
|
+
# Fetch from API
|
|
400
|
+
api_url = "https://layers.openembedded.org/layerindex/api/layers/?format=json"
|
|
401
|
+
try:
|
|
402
|
+
print(Colors.dim("Fetching layer index..."), end=" ", flush=True)
|
|
403
|
+
with urllib.request.urlopen(api_url, timeout=30) as resp:
|
|
404
|
+
data = json.loads(resp.read().decode())
|
|
405
|
+
print(Colors.dim("done"))
|
|
406
|
+
|
|
407
|
+
# Save to cache
|
|
408
|
+
try:
|
|
409
|
+
with open(cache_path, "w") as f:
|
|
410
|
+
json.dump({"timestamp": time.time(), "data": data}, f)
|
|
411
|
+
except OSError:
|
|
412
|
+
pass # Cache write failed, continue anyway
|
|
413
|
+
|
|
414
|
+
return data
|
|
415
|
+
except urllib.error.URLError as e:
|
|
416
|
+
print(Colors.dim("failed"))
|
|
417
|
+
# Try to use stale cache
|
|
418
|
+
if os.path.isfile(cache_path):
|
|
419
|
+
try:
|
|
420
|
+
with open(cache_path, "r") as f:
|
|
421
|
+
cache_data = json.load(f)
|
|
422
|
+
if cache_data.get("data"):
|
|
423
|
+
print(Colors.yellow(f"Using cached data (network error: {e})"))
|
|
424
|
+
return cache_data["data"]
|
|
425
|
+
except (json.JSONDecodeError, OSError, KeyError):
|
|
426
|
+
pass
|
|
427
|
+
print(f"Error fetching layer index: {e}")
|
|
428
|
+
return None
|
|
429
|
+
except json.JSONDecodeError as e:
|
|
430
|
+
print(Colors.dim("failed"))
|
|
431
|
+
print(f"Error parsing response: {e}")
|
|
432
|
+
return None
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def _fetch_layer_dependencies(layer_name: str, branch: str) -> List[dict]:
|
|
436
|
+
"""Fetch dependencies for a layer from the API. Returns list of {name, required} dicts."""
|
|
437
|
+
import urllib.request
|
|
438
|
+
import urllib.error
|
|
439
|
+
|
|
440
|
+
# First get the layerbranch ID for this layer+branch
|
|
441
|
+
try:
|
|
442
|
+
# Get all layerbranches for this layer
|
|
443
|
+
url = f"https://layers.openembedded.org/layerindex/api/layerBranches/?filter=layer__name:{layer_name}"
|
|
444
|
+
with urllib.request.urlopen(url, timeout=10) as resp:
|
|
445
|
+
layerbranches = json.loads(resp.read().decode())
|
|
446
|
+
if not layerbranches:
|
|
447
|
+
return []
|
|
448
|
+
|
|
449
|
+
# Get branch ID mapping
|
|
450
|
+
branch_url = "https://layers.openembedded.org/layerindex/api/branches/"
|
|
451
|
+
with urllib.request.urlopen(branch_url, timeout=10) as resp:
|
|
452
|
+
branches = json.loads(resp.read().decode())
|
|
453
|
+
branch_id = next((b["id"] for b in branches if b["name"] == branch), None)
|
|
454
|
+
if not branch_id:
|
|
455
|
+
return []
|
|
456
|
+
|
|
457
|
+
# Find layerbranch for the target branch
|
|
458
|
+
layerbranch_id = next((lb["id"] for lb in layerbranches if lb.get("branch") == branch_id), None)
|
|
459
|
+
if not layerbranch_id:
|
|
460
|
+
return []
|
|
461
|
+
except (urllib.error.URLError, json.JSONDecodeError, KeyError, IndexError, StopIteration):
|
|
462
|
+
return []
|
|
463
|
+
|
|
464
|
+
# Get dependencies for this layerbranch
|
|
465
|
+
try:
|
|
466
|
+
url = f"https://layers.openembedded.org/layerindex/api/layerDependencies/?filter=layerbranch:{layerbranch_id}"
|
|
467
|
+
with urllib.request.urlopen(url, timeout=10) as resp:
|
|
468
|
+
deps = json.loads(resp.read().decode())
|
|
469
|
+
except (urllib.error.URLError, json.JSONDecodeError):
|
|
470
|
+
return []
|
|
471
|
+
|
|
472
|
+
if not deps:
|
|
473
|
+
return []
|
|
474
|
+
|
|
475
|
+
# Get layer info for each dependency ID
|
|
476
|
+
dep_ids = [d.get("dependency") for d in deps if d.get("dependency")]
|
|
477
|
+
if not dep_ids:
|
|
478
|
+
return []
|
|
479
|
+
|
|
480
|
+
# Build lookup from cached layer data
|
|
481
|
+
layer_data = _fetch_layer_index(force=False)
|
|
482
|
+
if not layer_data:
|
|
483
|
+
return []
|
|
484
|
+
|
|
485
|
+
# Map layer IDs to names (layer ID is in layer.id)
|
|
486
|
+
id_to_name = {}
|
|
487
|
+
for entry in layer_data:
|
|
488
|
+
layer_info = entry.get("layer", {})
|
|
489
|
+
lid = layer_info.get("id")
|
|
490
|
+
name = layer_info.get("name")
|
|
491
|
+
if lid and name:
|
|
492
|
+
id_to_name[lid] = name
|
|
493
|
+
|
|
494
|
+
# Resolve dependency names
|
|
495
|
+
result = []
|
|
496
|
+
for d in deps:
|
|
497
|
+
dep_id = d.get("dependency")
|
|
498
|
+
required = d.get("required", True)
|
|
499
|
+
name = id_to_name.get(dep_id)
|
|
500
|
+
if name:
|
|
501
|
+
# Skip openembedded-core (always present)
|
|
502
|
+
if name == "openembedded-core":
|
|
503
|
+
continue
|
|
504
|
+
result.append({"name": name, "required": required})
|
|
505
|
+
|
|
506
|
+
# Dedupe and sort (required first)
|
|
507
|
+
seen = set()
|
|
508
|
+
unique = []
|
|
509
|
+
for d in result:
|
|
510
|
+
if d["name"] not in seen:
|
|
511
|
+
seen.add(d["name"])
|
|
512
|
+
unique.append(d)
|
|
513
|
+
return sorted(unique, key=lambda x: (not x["required"], x["name"]))
|
|
514
|
+
|
|
515
|
+
|