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.
@@ -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
+