bspctl 0.0.2__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.
bspctl/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """bspctl: NXP i.MX and TI Sitara BSP build orchestrator."""
2
+
3
+ __version__ = "0.0.2"
bspctl/__main__.py ADDED
@@ -0,0 +1,6 @@
1
+ """Entry point for `python -m bspctl`."""
2
+
3
+ from bspctl.cli import app
4
+
5
+ if __name__ == "__main__":
6
+ app()
bspctl/bsp_detect.py ADDED
@@ -0,0 +1,154 @@
1
+ """Inspect a kas YAML and classify the BSP family.
2
+
3
+ Used by the BYO (Form A) path of ``bspctl build``: when the user hands
4
+ bspctl a kas YAML directly, we cannot rely on the manifest filename
5
+ regex in :func:`bspctl.bsp_model.detect_bsp_family`. Instead the
6
+ classifier reads ``machine:`` and ``repos:`` from the YAML and applies
7
+ a small first-match-wins rule set:
8
+
9
+ * machine starts with ``imx`` -> ``nxp``
10
+ * machine starts with ``am`` / ``k3-`` / ``j7-`` -> ``ti``
11
+ * repos contains ``meta-imx`` / ``meta-freescale*`` / ``meta-nxp*`` -> ``nxp``
12
+ * repos contains ``meta-ti-bsp`` / ``meta-ti`` / ``meta-arago`` -> ``ti``
13
+ * parseable YAML with at least ``machine:`` or ``repos:`` -> ``generic``
14
+ * unparseable / empty -> ``unknown``
15
+
16
+ The ``generic`` classification is the BSP-agnostic fallback for kas
17
+ YAMLs that look like real builds but do not target an NXP/TI SoM
18
+ (e.g. qemuarm64 + poky + meta-arm). Callers layer the
19
+ ``bspctl-tuning-generic.yml`` overlay - which carries only the
20
+ BSP-agnostic optimizations (ccache, MIRRORS, PREMIRRORS, FETCHCMD_wget,
21
+ PYTHONMALLOC) - onto these YAMLs.
22
+
23
+ The function never raises - I/O on the YAML is wrapped defensively so
24
+ ``bspctl build my.yml`` can fail with a single typer.Exit(2) instead of
25
+ a Python traceback.
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ from typing import TYPE_CHECKING, Any, Literal
31
+
32
+ import yaml
33
+
34
+ if TYPE_CHECKING:
35
+ from pathlib import Path
36
+
37
+ # Repo-name substrings that identify each BSP. Order matters within a
38
+ # family: the first matching substring wins. The lists are kept short
39
+ # and explicit so a future BSP addition does not collide silently.
40
+ _NXP_REPO_NAMES: tuple[str, ...] = (
41
+ "meta-imx",
42
+ "meta-freescale",
43
+ "meta-nxp",
44
+ "meta-variscite-bsp-imx",
45
+ "meta-variscite-sdk-imx",
46
+ )
47
+
48
+ _TI_REPO_NAMES: tuple[str, ...] = (
49
+ "meta-ti-bsp",
50
+ "meta-ti-extras",
51
+ "meta-ti",
52
+ "meta-tisdk",
53
+ "meta-arago",
54
+ "meta-variscite-bsp-ti",
55
+ "meta-variscite-sdk-ti",
56
+ )
57
+
58
+
59
+ def _machine_family(machine: str) -> Literal["nxp", "ti", "unknown"]:
60
+ if not machine:
61
+ return "unknown"
62
+ name = machine.lower()
63
+ if name.startswith("imx"):
64
+ return "nxp"
65
+ if name.startswith(("am", "k3-", "j7-", "j72", "j78", "j784")):
66
+ return "ti"
67
+ return "unknown"
68
+
69
+
70
+ def _repos_family(repos: dict[str, Any] | None) -> Literal["nxp", "ti", "unknown"]:
71
+ if not isinstance(repos, dict):
72
+ return "unknown"
73
+ names = set(repos.keys())
74
+ for hit in _NXP_REPO_NAMES:
75
+ if hit in names:
76
+ return "nxp"
77
+ for hit in _TI_REPO_NAMES:
78
+ if hit in names:
79
+ return "ti"
80
+ return "unknown"
81
+
82
+
83
+ def detect_bsp_from_yaml(yaml_path: Path) -> Literal["nxp", "ti", "generic", "unknown"]:
84
+ """Inspect a kas YAML and classify the BSP family.
85
+
86
+ Pure function over a parsed YAML dict. Returns ``"generic"`` for a
87
+ kas YAML that parses cleanly but lacks NXP/TI markers; callers
88
+ use that to layer the BSP-agnostic tuning overlay. Returns
89
+ ``"unknown"`` only for unparseable, empty, or shape-incomplete
90
+ YAMLs - those exit with a typer.Exit(2) and a hint.
91
+ """
92
+ if yaml_path is None or not yaml_path.is_file():
93
+ return "unknown"
94
+ try:
95
+ with yaml_path.open("r", encoding="utf-8") as fh:
96
+ data = yaml.safe_load(fh)
97
+ except (OSError, yaml.YAMLError):
98
+ return "unknown"
99
+ if not isinstance(data, dict):
100
+ return "unknown"
101
+
102
+ machine = data.get("machine", "")
103
+ machine_hit = _machine_family(machine if isinstance(machine, str) else "")
104
+ if machine_hit != "unknown":
105
+ return machine_hit
106
+
107
+ repos_hit = _repos_family(data.get("repos"))
108
+ if repos_hit != "unknown":
109
+ return repos_hit
110
+
111
+ # No NXP/TI markers but the YAML has at least a machine string or a
112
+ # repos block - treat as a generic kas build. Reject only YAMLs
113
+ # that lack both anchors (typo or empty file).
114
+ has_machine = isinstance(machine, str) and bool(machine.strip())
115
+ has_repos = isinstance(data.get("repos"), dict) and bool(data["repos"])
116
+ if has_machine or has_repos:
117
+ return "generic"
118
+ return "unknown"
119
+
120
+
121
+ def is_meta_avocado_yaml(yaml_path: Path) -> bool:
122
+ """Return True if the YAML lives inside a meta-avocado repository.
123
+
124
+ Walks the resolved path and checks whether any ancestor directory is
125
+ named ``meta-avocado``. This is how bspctl detects that a generic kas
126
+ YAML belongs to the Avocado OS build system and needs the
127
+ ``init-build``-style build-directory setup before kas can run.
128
+ """
129
+ try:
130
+ return "meta-avocado" in yaml_path.resolve().parts
131
+ except Exception:
132
+ return False
133
+
134
+
135
+ def detect_kas_workspace(yaml_path: Path) -> Path:
136
+ """Return the effective workspace root for a generic kas YAML.
137
+
138
+ For meta-avocado YAMLs the YAML sits several levels deep inside the
139
+ ``meta-avocado`` repository (e.g. ``sources/meta-avocado/kas/machine/
140
+ qemux86-64.yml``). kas must run from a build directory that is a
141
+ *sibling* of ``meta-avocado/`` (e.g. ``sources/build-qemux86-64/``),
142
+ not from inside the repo. This function walks up from the YAML to
143
+ find the ``meta-avocado`` boundary and returns its parent
144
+ (e.g. ``sources/``) so :func:`bspctl.config.BuildConfig.bsp_root`
145
+ can derive the correct build-directory path.
146
+
147
+ For every other generic kas YAML the workspace is simply the YAML's
148
+ parent directory (preserving the existing behavior).
149
+ """
150
+ resolved = yaml_path.resolve()
151
+ for parent in resolved.parents:
152
+ if parent.name == "meta-avocado":
153
+ return parent.parent
154
+ return resolved.parent
bspctl/bsp_model.py ADDED
@@ -0,0 +1,258 @@
1
+ """Per-BSP model for the bspctl CLI.
2
+
3
+ NXP i.MX and TI Sitara BSP families are supported.
4
+ Each family has its own toolchain (Google ``repo`` + ``var-setup-release.sh``
5
+ for NXP; ``varigit/oe-layersetup`` shell wrapper for TI), its own
6
+ manifest format (``imx-A.B.C-X.Y.Z.xml`` vs ``processor-sdk-...-config_var<N>.txt``),
7
+ its own static tuning overlay (``overlays/bspctl-tuning-<bsp>.yml``),
8
+ and its own pre-flight checks.
9
+
10
+ This module exports:
11
+
12
+ * :func:`detect_bsp_family` - classify a manifest filename. Pure regex,
13
+ no I/O. The return value drives both the dispatcher in
14
+ :mod:`bspctl.cli` and the ``check_host_tools`` decision.
15
+ * :func:`infer_bsp_branch` - synthesize the
16
+ ``meta-variscite-bsp-ti`` branch suffix from a TI config filename.
17
+ * :class:`BspModel` - dataclass + registry that hold every per-BSP
18
+ knob: defaults, kas template, sync/setup steps, doctor extras.
19
+ * :func:`get_model` - factory returning the dispatched model. Imports
20
+ are lazy so :mod:`bspctl.config` (which imports
21
+ :func:`infer_bsp_branch` for TI branch fallback) can keep its
22
+ circular dep-free top-level import.
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import re
28
+ from collections.abc import Callable
29
+ from dataclasses import dataclass, replace
30
+ from typing import TYPE_CHECKING, Any, Literal
31
+
32
+ from bspctl.vendor_config import load_vendors
33
+
34
+ if TYPE_CHECKING:
35
+ from pathlib import Path
36
+
37
+ from bspctl.kas import KasTemplate
38
+
39
+
40
+ # ---------------------------------------------------------------------------
41
+ # Manifest-shape detection (no I/O on the regex path)
42
+ # ---------------------------------------------------------------------------
43
+
44
+
45
+ # NXP manifest filename: imx-<kernel>-<bsp>.xml
46
+ # Examples: imx-6.6.52-2.2.2.xml, imx-6.12.49-2.2.0.xml
47
+ _NXP_MANIFEST_RE = re.compile(r"^imx-\d+\.\d+\.\d+-\d+\.\d+\.\d+\.xml$")
48
+
49
+ # TI Processor SDK config filename:
50
+ # processor-sdk-<poky>-<flavour>-<sdk>-config_<var>.txt
51
+ # where <poky> is the LTS code name (scarthgap, walnascar, ...),
52
+ # <flavour> is "chromium" / "non-chromium" / etc,
53
+ # <sdk> is a 4-part TI SDK version (11.00.09.04),
54
+ # <var> is "var01" / "var02" / ...
55
+ _TI_PROCESSOR_SDK_RE = re.compile(
56
+ r"^processor-sdk-"
57
+ r"(?P<poky>[A-Za-z]\w*)-"
58
+ r".*?-"
59
+ r"(?P<sdk>\d+\.\d+\.\d+\.\d+)-"
60
+ r"config_(?P<var>var\d+)\.txt$"
61
+ )
62
+
63
+ # TI legacy/Arago manifest filename: arago-<anything>.txt
64
+ # Kept as alternation for forward compatibility with older
65
+ # config naming conventions.
66
+ _TI_ARAGO_RE = re.compile(r"^arago-.*\.txt$")
67
+
68
+ # Layer name string used as a fallback heuristic when a config file is
69
+ # present but does not match either regex.
70
+ _TI_LAYER_PREFIX = "meta-variscite-bsp-ti"
71
+
72
+
73
+ def detect_bsp_family(
74
+ manifest_path: Path | None,
75
+ config_file: Path | None = None,
76
+ ) -> Literal["nxp", "ti", "unknown"]:
77
+ """Detect the BSP family from a manifest filename and optional config.
78
+
79
+ Only the filename (``path.name``) is inspected; the file need not
80
+ exist on disk. Pass ``None`` for either argument to skip that
81
+ check. The function never raises - I/O on ``config_file`` is
82
+ wrapped defensively.
83
+ """
84
+ if manifest_path is not None:
85
+ name = manifest_path.name
86
+ for vendor in load_vendors():
87
+ if re.match(vendor.manifest_regex, name):
88
+ return vendor.family # type: ignore[return-value]
89
+ if _NXP_MANIFEST_RE.match(name):
90
+ return "nxp"
91
+ if _TI_PROCESSOR_SDK_RE.match(name) or _TI_ARAGO_RE.match(name):
92
+ return "ti"
93
+
94
+ if config_file is not None:
95
+ try:
96
+ text = config_file.read_text(encoding="utf-8", errors="replace")
97
+ if _TI_LAYER_PREFIX in text:
98
+ return "ti"
99
+ except OSError:
100
+ pass
101
+
102
+ return "unknown"
103
+
104
+
105
+ def infer_bsp_branch(config_filename: str) -> str:
106
+ """Derive the ``meta-variscite-bsp-ti`` branch from a TI config name.
107
+
108
+ The canonical filename
109
+ ``processor-sdk-scarthgap-chromium-11.00.09.04-config_var01.txt``
110
+ parses to ``(poky=scarthgap, sdk=11.00.09.04, var=var01)`` and
111
+ yields ``scarthgap_11.00.09.04_var01`` - the actual branch name on
112
+ the layer repo.
113
+
114
+ Returns ``"<unknown>"`` for inputs that do not match the regex
115
+ (legacy ``arago-*.txt`` configs go through the legacy versioning
116
+ scheme, which is out of scope for this helper).
117
+ """
118
+ m = _TI_PROCESSOR_SDK_RE.match(config_filename)
119
+ if not m:
120
+ return "<unknown>"
121
+ return f"{m.group('poky')}_{m.group('sdk')}_{m.group('var')}"
122
+
123
+
124
+ # ---------------------------------------------------------------------------
125
+ # BspModel registry
126
+ # ---------------------------------------------------------------------------
127
+
128
+
129
+ # Type aliases. ``Callable[..., None]`` is intentionally permissive so
130
+ # both the NXP sync step (``init_and_sync(cfg, log, *, force_init=...)``)
131
+ # and the TI sync step (``populate(cfg, log, *, force_init=...)``) fit.
132
+ SyncStep = Callable[..., None]
133
+ SetupEnvStep = Callable[..., None]
134
+ DoctorCheck = Callable[..., Any]
135
+
136
+
137
+ @dataclass(frozen=True)
138
+ class BspModel:
139
+ """Per-BSP knobs consumed by the dispatch layer.
140
+
141
+ Every field that varies between NXP and TI lives here. The CLI's
142
+ ``_dispatch_bsp`` returns one of two singleton instances; downstream
143
+ code reads the fields instead of switching on ``cfg.bsp_family``.
144
+ """
145
+
146
+ family: Literal["nxp", "ti"]
147
+ workspace_subdir: str # "nxp" or "ti"
148
+ kas_yaml_filename: str # "kas-nxp.yml" or "kas-ti.yml"
149
+ tuning_overlay_filename: str # "bspctl-tuning-nxp.yml" or "bspctl-tuning-ti.yml"
150
+ manifest_kind: Literal["repo-xml", "oe-layertool-config"]
151
+ default_machine: str
152
+ default_distro: str
153
+ default_image: str
154
+ default_manifest: str
155
+ default_branch: str
156
+ required_host_tools: tuple[str, ...]
157
+ sync_step: SyncStep
158
+ setup_env_step: SetupEnvStep
159
+ kas_template: KasTemplate
160
+ doctor_extras: tuple[DoctorCheck, ...]
161
+
162
+
163
+ def get_model(family: Literal["nxp", "ti"]) -> BspModel:
164
+ """Return the BspModel singleton for ``family``.
165
+
166
+ Imports are intentionally lazy. ``bsp_model`` is imported from
167
+ ``config.py`` (``infer_bsp_branch``) and from ``cli.py``; deferring
168
+ the heavy imports until ``get_model`` is actually called keeps the
169
+ import graph clean and lets unit tests construct dummy models
170
+ without dragging in diagnostics or steps.
171
+ """
172
+ # Lazy: avoid pulling diagnostics/steps/kas into every consumer of
173
+ # detect_bsp_family or infer_bsp_branch.
174
+ from bspctl import config as cfg_mod
175
+ from bspctl.diagnostics import (
176
+ check_forks_linux_imx,
177
+ check_forks_ti_linux_kernel,
178
+ check_forks_ti_u_boot,
179
+ check_git_object_cache,
180
+ check_manifest_consistency,
181
+ check_ti_layertool_config_consistency,
182
+ check_ti_layertool_present,
183
+ )
184
+ from bspctl.kas import NXP_KAS_TEMPLATE, TI_KAS_TEMPLATE
185
+ from bspctl.steps import repo as step_repo
186
+ from bspctl.steps import setup_env as step_setup
187
+ from bspctl.steps import ti_layertool as step_ti_layertool
188
+ from bspctl.steps import ti_setup_env as step_ti_setup
189
+
190
+ if family == "nxp":
191
+ model = BspModel(
192
+ family="nxp",
193
+ workspace_subdir="nxp",
194
+ kas_yaml_filename="kas-nxp.yml",
195
+ tuning_overlay_filename="bspctl-tuning-nxp.yml",
196
+ manifest_kind="repo-xml",
197
+ default_machine=cfg_mod.DEFAULT_NXP_MACHINE,
198
+ default_distro=cfg_mod.DEFAULT_NXP_DISTRO,
199
+ default_image=cfg_mod.DEFAULT_NXP_IMAGE,
200
+ default_manifest=cfg_mod.DEFAULT_NXP_MANIFEST,
201
+ default_branch=cfg_mod.DEFAULT_NXP_REPO_BRANCH,
202
+ required_host_tools=("repo", "kas-container", "docker", "python3"),
203
+ sync_step=step_repo.init_and_sync,
204
+ setup_env_step=step_setup.run,
205
+ kas_template=NXP_KAS_TEMPLATE,
206
+ doctor_extras=(
207
+ check_forks_linux_imx,
208
+ check_manifest_consistency,
209
+ check_git_object_cache,
210
+ ),
211
+ )
212
+ elif family == "ti":
213
+ model = BspModel(
214
+ family="ti",
215
+ workspace_subdir="ti",
216
+ kas_yaml_filename="kas-ti.yml",
217
+ tuning_overlay_filename="bspctl-tuning-ti.yml",
218
+ manifest_kind="oe-layertool-config",
219
+ default_machine=cfg_mod.DEFAULT_TI_MACHINE,
220
+ default_distro=cfg_mod.DEFAULT_TI_DISTRO,
221
+ default_image=cfg_mod.DEFAULT_TI_IMAGE,
222
+ default_manifest=cfg_mod.DEFAULT_TI_MANIFEST,
223
+ default_branch=cfg_mod.DEFAULT_TI_REPO_BRANCH,
224
+ required_host_tools=("git", "kas-container", "docker", "python3"),
225
+ sync_step=step_ti_layertool.populate,
226
+ setup_env_step=step_ti_setup.run,
227
+ kas_template=TI_KAS_TEMPLATE,
228
+ doctor_extras=(
229
+ check_ti_layertool_present,
230
+ check_ti_layertool_config_consistency,
231
+ check_forks_ti_linux_kernel,
232
+ check_forks_ti_u_boot,
233
+ ),
234
+ )
235
+ else:
236
+ raise ValueError(f"Unknown BSP family: {family!r}")
237
+
238
+ # Apply optional VendorEntry overrides for the first matching vendor.
239
+ for entry in load_vendors():
240
+ if entry.family == family:
241
+ overrides: dict[str, Any] = {}
242
+ if entry.default_machine is not None:
243
+ overrides["default_machine"] = entry.default_machine
244
+ if entry.default_distro is not None:
245
+ overrides["default_distro"] = entry.default_distro
246
+ if entry.default_image is not None:
247
+ overrides["default_image"] = entry.default_image
248
+ if entry.default_manifest is not None:
249
+ overrides["default_manifest"] = entry.default_manifest
250
+ if entry.default_branch is not None:
251
+ overrides["default_branch"] = entry.default_branch
252
+ if entry.tuning_overlay is not None:
253
+ overrides["tuning_overlay_filename"] = entry.tuning_overlay
254
+ if overrides:
255
+ model = replace(model, **overrides)
256
+ break
257
+
258
+ return model