armbuilderx 0.1.1__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 @@
1
+ __version__ = "0.1.1"
armbuilderx/cli.py ADDED
@@ -0,0 +1,258 @@
1
+ from __future__ import annotations
2
+
3
+ import shutil
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ import click
8
+
9
+ from armbuilderx.cmsis import csolution, generator, mdk_project, toolbox
10
+ from armbuilderx.cmsis.csolution import CSolutionError
11
+ from armbuilderx.cmsis.mdk_project import MdkProjectError
12
+ from armbuilderx.paths import pack_root_dir
13
+ from armbuilderx.sdk import available_sdk_ids, get_sdk_adapter
14
+ from armbuilderx.toolchain.ensure import ensure_toolchain
15
+
16
+
17
+ def _echo(msg: str) -> None:
18
+ click.echo(msg)
19
+
20
+
21
+ @click.group()
22
+ def main() -> None:
23
+ """ArmBuilderX: build, flash and monitor Keil/MDK Arm projects with
24
+ CMake, CMSIS-Toolbox and pyOCD."""
25
+
26
+
27
+ @main.command()
28
+ @click.option("--pack", "pack_files", multiple=True, type=click.Path(exists=True, path_type=Path),
29
+ help="Vendor .pack file to install into ArmBuilderX's managed pack root. "
30
+ "Repeat for multiple packs.")
31
+ def init(pack_files: tuple[Path, ...]) -> None:
32
+ """Install CMSIS-Pack(s) needed to resolve device data."""
33
+ if not pack_files:
34
+ _echo(f"pack root: {pack_root_dir()}")
35
+ _echo("Nothing to do. Pass --pack <file> to install a vendor pack.")
36
+ return
37
+ for pack_file in pack_files:
38
+ _echo(f"installing {pack_file} ...")
39
+ toolbox.add_pack(pack_file)
40
+ _echo("done.")
41
+
42
+
43
+ @main.command()
44
+ @click.argument("project_dir", type=click.Path(exists=True, file_okay=False, path_type=Path))
45
+ @click.option("--sdk", type=click.Choice(available_sdk_ids()),
46
+ help="SDK adapter to use. Defaults to automatic project-layout detection.")
47
+ def generate(project_dir: Path, sdk: str | None) -> None:
48
+ """Generate/refresh GCC/*.csolution.yml + *.cproject.yml for PROJECT_DIR
49
+ from its existing MDK project."""
50
+ try:
51
+ result = generator.generate(project_dir, sdk=sdk)
52
+ except (MdkProjectError, generator.GenerateError) as exc:
53
+ raise click.ClickException(str(exc))
54
+ _echo(f"generated {result.csolution_yml}")
55
+ _echo(f"generated {result.cproject_yml}")
56
+
57
+
58
+ def _resolve_pyocd_exe() -> str:
59
+ """pyocd is installed as a console script in the same environment as
60
+ armbuilderx (it's a pip dependency), so look next to the running
61
+ interpreter first; fall back to PATH for the rare case of a
62
+ non-standard install layout.
63
+ """
64
+ exe_dir = Path(sys.executable).parent
65
+ for name in ("pyocd", "pyocd.exe"):
66
+ candidate = exe_dir / name
67
+ if candidate.is_file():
68
+ return str(candidate)
69
+ found = shutil.which("pyocd")
70
+ if found:
71
+ return found
72
+ raise click.ClickException(
73
+ "pyocd not found. It is a pip dependency of armbuilderx; reinstall with "
74
+ "'pip install armbuilderx'."
75
+ )
76
+
77
+
78
+ def _ensure_generated(project_dir: Path, sdk: str | None = None) -> Path:
79
+ gcc_dir = project_dir / "GCC"
80
+ selected_sdk = sdk
81
+ if selected_sdk is None:
82
+ try:
83
+ # SDK adapters own generated configuration such as startup files,
84
+ # ABI flags, and vendor linker inputs. Refresh it on every build;
85
+ # merely finding an existing csolution file must not retain stale
86
+ # adapter output from an earlier ArmBuilderX version.
87
+ selected_sdk = get_sdk_adapter(project_dir).sdk_id
88
+ except generator.GenerateError:
89
+ pass
90
+
91
+ if selected_sdk is not None:
92
+ _echo(f"refreshing project files with SDK adapter '{selected_sdk}'...")
93
+ generator.generate(project_dir, sdk=selected_sdk)
94
+ elif not list(gcc_dir.glob("*.csolution.yml")):
95
+ _echo("no GCC/*.csolution.yml found yet, generating it first...")
96
+ generator.generate(project_dir, sdk=sdk)
97
+ return gcc_dir
98
+
99
+
100
+ @main.command()
101
+ @click.argument("project_dir", type=click.Path(exists=True, file_okay=False, path_type=Path))
102
+ @click.option("--build-type", default="Debug", show_default=True, type=click.Choice(["Debug", "Release"]))
103
+ @click.option("--clean", is_flag=True, help="Remove intermediate/output directories before building.")
104
+ @click.option("--sdk", type=click.Choice(available_sdk_ids()),
105
+ help="SDK adapter to use if generation is needed.")
106
+ def build(project_dir: Path, build_type: str, clean: bool, sdk: str | None) -> None:
107
+ """Ensure the toolchain is available, then build PROJECT_DIR/GCC."""
108
+ project_dir = project_dir.resolve()
109
+ try:
110
+ gcc_dir = _ensure_generated(project_dir, sdk)
111
+ except (MdkProjectError, generator.GenerateError) as exc:
112
+ raise click.ClickException(str(exc))
113
+
114
+ csolution_files = list(gcc_dir.glob("*.csolution.yml"))
115
+ if not csolution_files:
116
+ raise click.ClickException(f"no *.csolution.yml found under {gcc_dir}")
117
+ csolution_yml = csolution_files[0]
118
+
119
+ tc_env = ensure_toolchain(notify=_echo)
120
+ env = tc_env.as_environ()
121
+ env["CMSIS_PACK_ROOT"] = str(pack_root_dir())
122
+
123
+ args = [str(csolution_yml), "--toolchain", "GCC", "--packs"]
124
+ # *.cproject.yml -> project name (strip the ".cproject.yml" suffix, not just ".yml")
125
+ contexts = [c.name[: -len(".cproject.yml")] for c in gcc_dir.glob("*.cproject.yml")]
126
+ for context in contexts:
127
+ args += ["-c", f"{context}.{build_type}"]
128
+ if clean:
129
+ args.append("--rebuild")
130
+
131
+ _echo(f"building {csolution_yml.name} ({build_type}) ...")
132
+ result = toolbox.run(toolbox.cbuild_exe(), args, cwd=gcc_dir, env=env)
133
+ click.echo(result.stdout)
134
+ if result.returncode != 0:
135
+ click.echo(result.stderr, err=True)
136
+ raise click.ClickException("build failed")
137
+ _echo("build succeeded.")
138
+
139
+
140
+ @main.command()
141
+ @click.argument("project_dir", type=click.Path(exists=True, file_okay=False, path_type=Path))
142
+ @click.option("--build-type", default="Debug", show_default=True, type=click.Choice(["Debug", "Release"]))
143
+ @click.option("--pack", "pack_file", type=click.Path(exists=True, path_type=Path),
144
+ help="Vendor .pack file pyOCD should use to resolve the target device.")
145
+ @click.option(
146
+ "--target",
147
+ help="pyOCD target type name (defaults to the device declared by the MDK or CMSIS solution project).",
148
+ )
149
+ @click.option("--unique-id", help="pyOCD probe unique ID, when multiple probes are attached.")
150
+ @click.option("--frequency", help="SWD/JTAG clock frequency, for example 100k or 1m.")
151
+ @click.option("--connect", "connect_mode", type=click.Choice(["halt", "pre-reset", "under-reset", "attach"]),
152
+ help="pyOCD target connection mode.")
153
+ def flash(project_dir: Path, build_type: str, pack_file, target, unique_id, frequency, connect_mode) -> None:
154
+ """Flash the ELF built for PROJECT_DIR onto the target with pyOCD."""
155
+ from armbuilderx import flash as flash_mod
156
+
157
+ project_dir = project_dir.resolve()
158
+ gcc_dir = project_dir / "GCC"
159
+ elf_path = flash_mod.find_elf(gcc_dir, build_type)
160
+
161
+ if pack_file is None or target is None:
162
+ try:
163
+ uvprojx = mdk_project.find_uvprojx(project_dir)
164
+ mdk = mdk_project.parse_uvprojx(uvprojx)
165
+ if target is None:
166
+ target = mdk.device.lower()
167
+ except MdkProjectError:
168
+ if target is None:
169
+ try:
170
+ target = csolution.pyocd_target_from_csolution(project_dir)
171
+ except CSolutionError:
172
+ pass
173
+
174
+ # Goodix links GR551x applications at 0x01002000, while its CMSIS-Pack
175
+ # .FLM file exposes a 0x00200000 flash range to pyOCD. The Goodix
176
+ # downloader performs target-specific initialisation/address mapping that
177
+ # pyOCD cannot infer from this Pack, so do not risk a misleading partial
178
+ # programming attempt through the generic pyOCD backend.
179
+ if target == "gr551x":
180
+ raise click.ClickException(
181
+ "GR551x flashing is not supported by the pyOCD backend: the Goodix Pack flash "
182
+ "address does not match the SDK ELF address. Use Goodix GProgrammer with a J-Link, "
183
+ "or add a verified GR551x J-Link/GProgrammer backend."
184
+ )
185
+
186
+ pyocd_exe = _resolve_pyocd_exe()
187
+
188
+ _echo(f"flashing {elf_path} ...")
189
+ try:
190
+ flash_mod.flash(
191
+ elf_path,
192
+ pack_file=pack_file,
193
+ target=target,
194
+ unique_id=unique_id,
195
+ frequency=frequency,
196
+ connect_mode=connect_mode,
197
+ pyocd_exe=pyocd_exe,
198
+ )
199
+ except flash_mod.FlashError as exc:
200
+ raise click.ClickException(str(exc))
201
+ _echo("flash succeeded.")
202
+
203
+
204
+ @main.command()
205
+ @click.option("--port", help="Serial device to open (skips auto-detect).")
206
+ @click.option("--unique-id", help="pyOCD probe unique ID to match against a serial port's serial number.")
207
+ @click.option("--baud", default=115200, show_default=True)
208
+ def monitor(port, unique_id, baud) -> None:
209
+ """Stream the DAPLink/CMSIS-DAP virtual COM port log."""
210
+ from armbuilderx.serial import monitor as monitor_mod
211
+
212
+ if port is None:
213
+ try:
214
+ if unique_id:
215
+ info = monitor_mod.find_port_for_probe(unique_id)
216
+ else:
217
+ info = monitor_mod.find_single_daplink_port()
218
+ except monitor_mod.SerialPortNotFoundError as exc:
219
+ raise click.ClickException(str(exc))
220
+ port = info.device
221
+ _echo(f"using serial port {port} ({info.description})")
222
+
223
+ _echo(f"streaming {port} @ {baud} baud, Ctrl+C to stop")
224
+ try:
225
+ for chunk in monitor_mod.stream(port, baudrate=baud):
226
+ sys.stdout.buffer.write(chunk)
227
+ sys.stdout.buffer.flush()
228
+ except KeyboardInterrupt:
229
+ _echo("\nstopped.")
230
+
231
+
232
+ @main.command()
233
+ @click.argument("project_dir", type=click.Path(exists=True, file_okay=False, path_type=Path))
234
+ @click.option("--build-type", default="Debug", show_default=True, type=click.Choice(["Debug", "Release"]))
235
+ @click.option("--skip-monitor", is_flag=True, help="Build and flash only; do not open the serial monitor.")
236
+ @click.option("--sdk", type=click.Choice(available_sdk_ids()),
237
+ help="SDK adapter to use if generation is needed.")
238
+ @click.pass_context
239
+ def run(ctx: click.Context, project_dir: Path, build_type: str, skip_monitor: bool,
240
+ sdk: str | None) -> None:
241
+ """Generate, build, flash, then stream the serial log — all in one shot."""
242
+ ctx.invoke(build, project_dir=project_dir, build_type=build_type, clean=False, sdk=sdk)
243
+ ctx.invoke(
244
+ flash,
245
+ project_dir=project_dir,
246
+ build_type=build_type,
247
+ pack_file=None,
248
+ target=None,
249
+ unique_id=None,
250
+ frequency=None,
251
+ connect_mode=None,
252
+ )
253
+ if not skip_monitor:
254
+ ctx.invoke(monitor, port=None, unique_id=None, baud=115200)
255
+
256
+
257
+ if __name__ == "__main__":
258
+ main()
File without changes
@@ -0,0 +1,46 @@
1
+ """Read the target identity declared by a CMSIS Solution file."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ import yaml
8
+
9
+
10
+ class CSolutionError(RuntimeError):
11
+ """A CMSIS Solution file cannot provide an unambiguous target."""
12
+
13
+
14
+ def find_csolution(project_dir: Path) -> Path:
15
+ """Return the sole GCC solution file for *project_dir*."""
16
+ candidates = sorted((project_dir / "GCC").glob("*.csolution.yml"))
17
+ if len(candidates) != 1:
18
+ raise CSolutionError(
19
+ f"expected exactly one GCC/*.csolution.yml under {project_dir}, "
20
+ f"found {len(candidates)}"
21
+ )
22
+ return candidates[0]
23
+
24
+
25
+ def pyocd_target_from_csolution(project_dir: Path) -> str:
26
+ """Return pyOCD's lowercase target name from ``target-types[0].device``.
27
+
28
+ CMSIS uses a qualified device name such as ``Goodix::GR551x`` while pyOCD
29
+ registers the same Pack target as ``gr551x``. The device declaration is
30
+ intentionally read from the project rather than maintained in a vendor
31
+ specific flash table.
32
+ """
33
+ path = find_csolution(project_dir)
34
+ try:
35
+ with path.open() as source:
36
+ data = yaml.safe_load(source)
37
+ except (OSError, yaml.YAMLError) as exc:
38
+ raise CSolutionError(f"could not read {path}: {exc}") from exc
39
+
40
+ try:
41
+ device = data["solution"]["target-types"][0]["device"]
42
+ except (KeyError, IndexError, TypeError) as exc:
43
+ raise CSolutionError(f"missing solution.target-types[0].device in {path}") from exc
44
+ if not isinstance(device, str) or not device.strip():
45
+ raise CSolutionError(f"invalid solution.target-types[0].device in {path}")
46
+ return device.rsplit("::", 1)[-1].lower()
@@ -0,0 +1,21 @@
1
+ """SDK-agnostic façade for CMSIS-Toolbox project generation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Mapping
7
+
8
+ from armbuilderx.cmsis.types import GenerateError, GeneratedProject
9
+ from armbuilderx.sdk.registry import get_sdk_adapter
10
+
11
+ __all__ = ["GenerateError", "GeneratedProject", "generate"]
12
+
13
+
14
+ def generate(
15
+ project_dir: Path,
16
+ pack_env: Mapping[str, str] | None = None,
17
+ sdk: str | None = None,
18
+ ) -> GeneratedProject:
19
+ """Generate a project through the explicitly selected or detected SDK adapter."""
20
+ project_dir = project_dir.resolve()
21
+ return get_sdk_adapter(project_dir, sdk).generate(project_dir, pack_env)
@@ -0,0 +1,130 @@
1
+ """Parse a Keil MDK (.uvprojx) project just enough to drive CMSIS-Toolbox:
2
+ device/vendor/pack identity, preprocessor defines, and the list of
3
+ source file groups. CMSIS-Toolbox (csolution/cbuild/cbuild2cmake) does
4
+ all of the real work of turning that into a CMake project; this module
5
+ only extracts what already exists in the Keil project file.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import xml.etree.ElementTree as ET
11
+ from dataclasses import dataclass, field
12
+ from pathlib import Path
13
+ from typing import Optional
14
+
15
+
16
+ @dataclass
17
+ class SourceFile:
18
+ path: Path # absolute path
19
+ is_source: bool # True for .c/.cpp/.s/.S that must be compiled; False for headers
20
+
21
+
22
+ @dataclass
23
+ class SourceGroup:
24
+ name: str
25
+ files: list[SourceFile] = field(default_factory=list)
26
+
27
+
28
+ @dataclass
29
+ class MdkTarget:
30
+ target_name: str
31
+ device: str
32
+ vendor: str
33
+ pack_id: str # e.g. "NOVOSENSE.NS800RT5XXX.1.3.0"
34
+ output_name: str
35
+ defines: list[str]
36
+ include_paths: list[Path]
37
+ groups: list[SourceGroup]
38
+
39
+
40
+ class MdkProjectError(RuntimeError):
41
+ pass
42
+
43
+
44
+ # File types compiled from source (Keil's <FileType>): 1=C, 2=Asm, 8=C++.
45
+ _COMPILABLE_FILE_TYPES = {"1", "2", "8"}
46
+
47
+
48
+ def find_uvprojx(project_dir: Path) -> Path:
49
+ mdk_dir = project_dir / "MDK"
50
+ if not mdk_dir.is_dir():
51
+ raise MdkProjectError(f"no MDK/ folder found under {project_dir}")
52
+ candidates = sorted(mdk_dir.glob("*.uvprojx"))
53
+ if not candidates:
54
+ raise MdkProjectError(f"no .uvprojx file found under {mdk_dir}")
55
+ return candidates[0]
56
+
57
+
58
+ def parse_pack_id(pack_id: str) -> tuple[str, str, str]:
59
+ """'NOVOSENSE.NS800RT5XXX.1.3.0' -> ('NOVOSENSE', 'NS800RT5XXX', '1.3.0')."""
60
+ parts = pack_id.split(".")
61
+ if len(parts) < 4:
62
+ raise MdkProjectError(f"unexpected PackID format: {pack_id!r}")
63
+ vendor, name = parts[0], parts[1]
64
+ version = ".".join(parts[2:])
65
+ return vendor, name, version
66
+
67
+
68
+ def parse_uvprojx(uvprojx_path: Path) -> MdkTarget:
69
+ tree = ET.parse(uvprojx_path)
70
+ root = tree.getroot()
71
+
72
+ target_el = root.find("./Targets/Target")
73
+ if target_el is None:
74
+ raise MdkProjectError(f"no <Target> found in {uvprojx_path}")
75
+
76
+ target_name = _text(target_el, "TargetName")
77
+ common = target_el.find("./TargetOption/TargetCommonOption")
78
+ if common is None:
79
+ raise MdkProjectError(f"no TargetCommonOption in {uvprojx_path}")
80
+
81
+ device = _text(common, "Device")
82
+ vendor = _text(common, "Vendor")
83
+ pack_id = _text(common, "PackID")
84
+ output_name = _text(common, "OutputName") or target_name
85
+
86
+ cads = target_el.find("./TargetOption/TargetArmAds/Cads/VariousControls")
87
+ defines: list[str] = []
88
+ include_paths: list[Path] = []
89
+ if cads is not None:
90
+ define_text = _text(cads, "Define")
91
+ defines = [d for d in define_text.split() if d]
92
+ include_text = _text(cads, "IncludePath")
93
+ project_dir = uvprojx_path.parent # .../MDK
94
+ for raw in include_text.split(";"):
95
+ raw = raw.strip()
96
+ if not raw:
97
+ continue
98
+ include_paths.append((project_dir / raw.replace("\\", "/")).resolve())
99
+
100
+ groups: list[SourceGroup] = []
101
+ project_dir = uvprojx_path.parent
102
+ for group_el in target_el.findall("./Groups/Group"):
103
+ group_name = _text(group_el, "GroupName") or "group"
104
+ files: list[SourceFile] = []
105
+ for file_el in group_el.findall("./Files/File"):
106
+ file_type = _text(file_el, "FileType")
107
+ file_path = _text(file_el, "FilePath")
108
+ if not file_path:
109
+ continue
110
+ abs_path = (project_dir / file_path.replace("\\", "/")).resolve()
111
+ files.append(SourceFile(path=abs_path, is_source=file_type in _COMPILABLE_FILE_TYPES))
112
+ groups.append(SourceGroup(name=group_name, files=files))
113
+
114
+ return MdkTarget(
115
+ target_name=target_name,
116
+ device=device,
117
+ vendor=vendor,
118
+ pack_id=pack_id,
119
+ output_name=output_name,
120
+ defines=defines,
121
+ include_paths=include_paths,
122
+ groups=groups,
123
+ )
124
+
125
+
126
+ def _text(parent: ET.Element, tag: str) -> str:
127
+ el = parent.find(tag)
128
+ if el is None or el.text is None:
129
+ return ""
130
+ return el.text.strip()
@@ -0,0 +1,69 @@
1
+ """Locate and invoke the CMSIS-Toolbox executables (csolution, cbuild,
2
+ cpackget). ArmBuilderX never re-implements pack parsing or CMake
3
+ generation itself; it only shells out to these official binaries.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import shutil
9
+ import subprocess
10
+ from pathlib import Path
11
+ from typing import Optional
12
+
13
+ from armbuilderx.paths import cmsis_toolbox_bin_dir, pack_root_dir
14
+
15
+
16
+ class CmsisToolboxNotFoundError(RuntimeError):
17
+ pass
18
+
19
+
20
+ def _resolve(exe_name: str) -> Path:
21
+ bin_dir = cmsis_toolbox_bin_dir()
22
+ if bin_dir and bin_dir.is_dir():
23
+ candidate = bin_dir / exe_name
24
+ candidate_exe = bin_dir / f"{exe_name}.exe"
25
+ if candidate.is_file():
26
+ return candidate
27
+ if candidate_exe.is_file():
28
+ return candidate_exe
29
+ found = shutil.which(exe_name)
30
+ if found:
31
+ return Path(found)
32
+ raise CmsisToolboxNotFoundError(
33
+ f"'{exe_name}' not found. Install CMSIS-Toolbox and either put it on "
34
+ f"PATH or set ARMBUILDERX_CMSIS_TOOLBOX_BIN to its bin directory."
35
+ )
36
+
37
+
38
+ def csolution_exe() -> Path:
39
+ return _resolve("csolution")
40
+
41
+
42
+ def cbuild_exe() -> Path:
43
+ return _resolve("cbuild")
44
+
45
+
46
+ def cpackget_exe() -> Path:
47
+ return _resolve("cpackget")
48
+
49
+
50
+ def run(exe: Path, args: list[str], cwd: Optional[Path] = None, env: Optional[dict] = None):
51
+ return subprocess.run(
52
+ [str(exe), *args],
53
+ cwd=str(cwd) if cwd else None,
54
+ env=env,
55
+ capture_output=True,
56
+ text=True,
57
+ )
58
+
59
+
60
+ def add_pack(pack_file: Path, env: Optional[dict] = None) -> None:
61
+ """Install a local .pack file into the managed pack root (idempotent)."""
62
+ pack_root_dir().mkdir(parents=True, exist_ok=True)
63
+ full_env = dict(env or {})
64
+ full_env.setdefault("CMSIS_PACK_ROOT", str(pack_root_dir()))
65
+ result = run(cpackget_exe(), ["add", str(pack_file)], env=full_env)
66
+ if result.returncode != 0 and "already installed" not in (result.stderr + result.stdout):
67
+ raise RuntimeError(
68
+ f"cpackget add failed for {pack_file}:\n{result.stdout}\n{result.stderr}"
69
+ )
@@ -0,0 +1,17 @@
1
+ """SDK-independent generation result and errors."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+
8
+
9
+ class GenerateError(RuntimeError):
10
+ """Raised when a project cannot be generated for the selected SDK."""
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class GeneratedProject:
15
+ gcc_dir: Path
16
+ csolution_yml: Path
17
+ cproject_yml: Path
armbuilderx/flash.py ADDED
@@ -0,0 +1,54 @@
1
+ """Flash a built ELF onto the target over CMSIS-DAP/DAPLink, using pyOCD.
2
+
3
+ ArmBuilderX shells out to the `pyocd` CLI (rather than re-implementing
4
+ flashing against pyOCD's Python API) so behavior always matches pyOCD's
5
+ own, well-tested command-line contract.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import subprocess
11
+ from pathlib import Path
12
+ from typing import Optional
13
+
14
+
15
+ class FlashError(RuntimeError):
16
+ pass
17
+
18
+
19
+ def find_elf(gcc_dir: Path, build_type: str = "Debug") -> Path:
20
+ """Locate the most recently built .elf under GCC/out/**/<build_type>/."""
21
+ candidates = sorted(
22
+ gcc_dir.glob(f"out/*/*/{build_type}/*.elf"), key=lambda p: p.stat().st_mtime, reverse=True
23
+ )
24
+ if not candidates:
25
+ raise FlashError(
26
+ f"no .elf found under {gcc_dir / 'out'} ({build_type}); build the project first"
27
+ )
28
+ return candidates[0]
29
+
30
+
31
+ def flash(
32
+ elf_path: Path,
33
+ pack_file: Optional[Path] = None,
34
+ target: Optional[str] = None,
35
+ unique_id: Optional[str] = None,
36
+ frequency: Optional[str] = None,
37
+ connect_mode: Optional[str] = None,
38
+ pyocd_exe: str = "pyocd",
39
+ ) -> None:
40
+ args = [pyocd_exe, "load", str(elf_path)]
41
+ if pack_file:
42
+ args += ["--pack", str(pack_file)]
43
+ if target:
44
+ args += ["-t", target]
45
+ if unique_id:
46
+ args += ["-u", unique_id]
47
+ if frequency:
48
+ args += ["-f", frequency]
49
+ if connect_mode:
50
+ args += ["--connect", connect_mode]
51
+
52
+ result = subprocess.run(args, capture_output=True, text=True)
53
+ if result.returncode != 0:
54
+ raise FlashError(f"pyocd load failed:\n{result.stdout}\n{result.stderr}")
armbuilderx/paths.py ADDED
@@ -0,0 +1,66 @@
1
+ """Well-known directories used by ArmBuilderX."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import sys
7
+ from pathlib import Path
8
+
9
+
10
+ def home_dir() -> Path:
11
+ """Root directory ArmBuilderX uses for its own cache/state."""
12
+ override = os.environ.get("ARMBUILDERX_HOME")
13
+ if override:
14
+ return Path(override)
15
+ return Path.home() / ".armbuilderx"
16
+
17
+
18
+ def tools_dir() -> Path:
19
+ """Directory where auto-downloaded toolchains are installed."""
20
+ return home_dir() / "tools"
21
+
22
+
23
+ def pack_root_dir() -> Path:
24
+ """CMSIS_PACK_ROOT used by cpackget/csolution/cbuild."""
25
+ override = os.environ.get("ARMBUILDERX_PACK_ROOT")
26
+ if override:
27
+ return Path(override)
28
+ return home_dir() / "packs"
29
+
30
+
31
+ def cmsis_toolbox_bin_dir() -> Path:
32
+ """Bundled or user-provided CMSIS-Toolbox bin directory.
33
+
34
+ Resolution order:
35
+ 1. ARMBUILDERX_CMSIS_TOOLBOX_BIN environment variable.
36
+ 2. A `cmsis-toolbox-<platform>/bin` directory bundled next to the
37
+ installed package (used for local/dev checkouts).
38
+ 3. `csolution`/`cbuild` already on PATH.
39
+ """
40
+ override = os.environ.get("ARMBUILDERX_CMSIS_TOOLBOX_BIN")
41
+ if override:
42
+ return Path(override)
43
+
44
+ candidates = []
45
+ if sys.platform == "darwin":
46
+ candidates.append("cmsis-toolbox-darwin-arm64")
47
+ candidates.append("cmsis-toolbox-darwin-x64")
48
+ elif sys.platform.startswith("linux"):
49
+ candidates.append("cmsis-toolbox-linux-x64")
50
+ candidates.append("cmsis-toolbox-linux-arm64")
51
+ elif sys.platform.startswith("win"):
52
+ candidates.append("cmsis-toolbox-windows-x64")
53
+
54
+ here = Path(__file__).resolve()
55
+ for parent in [here.parent, *here.parents]:
56
+ for name in candidates:
57
+ bin_dir = parent / name / "bin"
58
+ if bin_dir.is_dir():
59
+ return bin_dir
60
+ # also look one level up from a repo checkout (ArmBuilderX/<name>/bin)
61
+ for name in candidates:
62
+ bin_dir = parent.parent / name / "bin"
63
+ if bin_dir.is_dir():
64
+ return bin_dir
65
+
66
+ return Path() # empty -> caller falls back to PATH lookup
@@ -0,0 +1,10 @@
1
+ """SDK-specific project-generation adapters.
2
+
3
+ The command layer and CMSIS-Toolbox integration stay SDK agnostic. Each
4
+ vendor SDK contributes one adapter that understands only that SDK's layout
5
+ and build conventions.
6
+ """
7
+
8
+ from armbuilderx.sdk.registry import available_sdk_ids, get_sdk_adapter
9
+
10
+ __all__ = ["available_sdk_ids", "get_sdk_adapter"]