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.
- armbuilderx/__init__.py +1 -0
- armbuilderx/cli.py +258 -0
- armbuilderx/cmsis/__init__.py +0 -0
- armbuilderx/cmsis/csolution.py +46 -0
- armbuilderx/cmsis/generator.py +21 -0
- armbuilderx/cmsis/mdk_project.py +130 -0
- armbuilderx/cmsis/toolbox.py +69 -0
- armbuilderx/cmsis/types.py +17 -0
- armbuilderx/flash.py +54 -0
- armbuilderx/paths.py +66 -0
- armbuilderx/sdk/__init__.py +10 -0
- armbuilderx/sdk/base.py +26 -0
- armbuilderx/sdk/gr551x.py +390 -0
- armbuilderx/sdk/ns800rt5xxx.py +171 -0
- armbuilderx/sdk/registry.py +42 -0
- armbuilderx/serial/__init__.py +0 -0
- armbuilderx/serial/monitor.py +84 -0
- armbuilderx/toolchain/__init__.py +0 -0
- armbuilderx/toolchain/detect.py +97 -0
- armbuilderx/toolchain/ensure.py +106 -0
- armbuilderx/toolchain/install.py +199 -0
- armbuilderx-0.1.1.dist-info/METADATA +97 -0
- armbuilderx-0.1.1.dist-info/RECORD +26 -0
- armbuilderx-0.1.1.dist-info/WHEEL +5 -0
- armbuilderx-0.1.1.dist-info/entry_points.txt +2 -0
- armbuilderx-0.1.1.dist-info/top_level.txt +1 -0
armbuilderx/__init__.py
ADDED
|
@@ -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"]
|