alloy-embedded 0.1.0__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.
- alloy_cli/__init__.py +3 -0
- alloy_cli/build.py +201 -0
- alloy_cli/cli.py +351 -0
- alloy_cli/debug.py +61 -0
- alloy_cli/emit/__init__.py +89 -0
- alloy_cli/emit/board.py +433 -0
- alloy_cli/emit/boot.py +31 -0
- alloy_cli/emit/common.py +55 -0
- alloy_cli/emit/device.py +211 -0
- alloy_cli/emit/ip.py +125 -0
- alloy_cli/emit/linker.py +190 -0
- alloy_cli/emit/vectors.py +133 -0
- alloy_cli/flash.py +277 -0
- alloy_cli/monitor.py +104 -0
- alloy_cli/payload/boards/esp32_devkit/board.json +62 -0
- alloy_cli/payload/boards/esp_wrover_kit/board.json +62 -0
- alloy_cli/payload/boards/nucleo_f722ze/board.json +38 -0
- alloy_cli/payload/boards/nucleo_g071rb/board.json +54 -0
- alloy_cli/payload/boards/nucleo_g0b1re/board.json +54 -0
- alloy_cli/payload/boards/raspberry_pi_pico/board.json +23 -0
- alloy_cli/payload/boards/rp2040_zero/board.json +24 -0
- alloy_cli/payload/boards/same70_xplained/board.json +60 -0
- alloy_cli/payload/src/alloy/adc.hpp +78 -0
- alloy_cli/payload/src/alloy/arch/cortex_m/irq_dispatch.cpp +27 -0
- alloy_cli/payload/src/alloy/arch/cortex_m/nvic.hpp +41 -0
- alloy_cli/payload/src/alloy/arch/cortex_m/startup.cpp +56 -0
- alloy_cli/payload/src/alloy/arch/cortex_m/systick.cpp +77 -0
- alloy_cli/payload/src/alloy/arch/cortex_m/systick.hpp +20 -0
- alloy_cli/payload/src/alloy/arch/irq.hpp +26 -0
- alloy_cli/payload/src/alloy/arch/xtensa/irq_ctrl.cpp +112 -0
- alloy_cli/payload/src/alloy/arch/xtensa/startup.S +47 -0
- alloy_cli/payload/src/alloy/arch/xtensa/startup.cpp +39 -0
- alloy_cli/payload/src/alloy/arch/xtensa/systick.cpp +58 -0
- alloy_cli/payload/src/alloy/arch/xtensa/systick.hpp +22 -0
- alloy_cli/payload/src/alloy/arch/xtensa/vectors.S +467 -0
- alloy_cli/payload/src/alloy/concepts.hpp +73 -0
- alloy_cli/payload/src/alloy/core/mmio.hpp +79 -0
- alloy_cli/payload/src/alloy/core/routes.hpp +51 -0
- alloy_cli/payload/src/alloy/core/types.hpp +82 -0
- alloy_cli/payload/src/alloy/dma.hpp +112 -0
- alloy_cli/payload/src/alloy/drivers/at24.hpp +132 -0
- alloy_cli/payload/src/alloy/drivers/spi_flash.hpp +156 -0
- alloy_cli/payload/src/alloy/drivers/ws2812.hpp +89 -0
- alloy_cli/payload/src/alloy/gpio.hpp +63 -0
- alloy_cli/payload/src/alloy/hal/adc/adc_impl.hpp +12 -0
- alloy_cli/payload/src/alloy/hal/adc/microchip_afec_v1.hpp +89 -0
- alloy_cli/payload/src/alloy/hal/adc/st_adc_v2.hpp +95 -0
- alloy_cli/payload/src/alloy/hal/clock_program.hpp +53 -0
- alloy_cli/payload/src/alloy/hal/dma/dma_impl.hpp +12 -0
- alloy_cli/payload/src/alloy/hal/dma/microchip_xdmac_v1.hpp +132 -0
- alloy_cli/payload/src/alloy/hal/dma/st_dma_v1.hpp +130 -0
- alloy_cli/payload/src/alloy/hal/gpio/espressif_gpio_v1.hpp +123 -0
- alloy_cli/payload/src/alloy/hal/gpio/microchip_pio_v1.hpp +80 -0
- alloy_cli/payload/src/alloy/hal/gpio/pin_impl.hpp +14 -0
- alloy_cli/payload/src/alloy/hal/gpio/raspberrypi_io_bank0.hpp +80 -0
- alloy_cli/payload/src/alloy/hal/gpio/st_gpio_v2.hpp +78 -0
- alloy_cli/payload/src/alloy/hal/i2c/espressif_i2c_v1.hpp +179 -0
- alloy_cli/payload/src/alloy/hal/i2c/i2c_impl.hpp +12 -0
- alloy_cli/payload/src/alloy/hal/i2c/microchip_twihs_v1.hpp +148 -0
- alloy_cli/payload/src/alloy/hal/i2c/st_i2c_v2.hpp +117 -0
- alloy_cli/payload/src/alloy/hal/pwm/espressif_ledc_v1.hpp +82 -0
- alloy_cli/payload/src/alloy/hal/pwm/pwm_impl.hpp +12 -0
- alloy_cli/payload/src/alloy/hal/pwm/st_tim_gp16.hpp +96 -0
- alloy_cli/payload/src/alloy/hal/spi/microchip_spi_v1.hpp +69 -0
- alloy_cli/payload/src/alloy/hal/spi/spi_impl.hpp +12 -0
- alloy_cli/payload/src/alloy/hal/spi/st_spi_v2.hpp +65 -0
- alloy_cli/payload/src/alloy/hal/uart/espressif_uart_v1.hpp +99 -0
- alloy_cli/payload/src/alloy/hal/uart/microchip_usart_v1.hpp +107 -0
- alloy_cli/payload/src/alloy/hal/uart/raspberrypi_uart_pl011.hpp +100 -0
- alloy_cli/payload/src/alloy/hal/uart/st_usart_v3.hpp +109 -0
- alloy_cli/payload/src/alloy/hal/uart/st_usart_v4.hpp +110 -0
- alloy_cli/payload/src/alloy/hal/uart/uart_impl.hpp +13 -0
- alloy_cli/payload/src/alloy/i2c.hpp +106 -0
- alloy_cli/payload/src/alloy/irq.hpp +66 -0
- alloy_cli/payload/src/alloy/pwm.hpp +100 -0
- alloy_cli/payload/src/alloy/spi.hpp +97 -0
- alloy_cli/payload/src/alloy/time.hpp +24 -0
- alloy_cli/payload/src/alloy/uart.hpp +162 -0
- alloy_cli/ports.py +52 -0
- alloy_cli/project.py +137 -0
- alloy_cli/toolchains.json +139 -0
- alloy_cli/toolchains.py +195 -0
- alloy_embedded-0.1.0.dist-info/METADATA +44 -0
- alloy_embedded-0.1.0.dist-info/RECORD +87 -0
- alloy_embedded-0.1.0.dist-info/WHEEL +4 -0
- alloy_embedded-0.1.0.dist-info/entry_points.txt +2 -0
- alloy_embedded-0.1.0.dist-info/licenses/LICENSE +21 -0
alloy_cli/__init__.py
ADDED
alloy_cli/build.py
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""Render the CMake build tree and drive cmake + ninja.
|
|
2
|
+
|
|
3
|
+
Users never see CMake: the tree lives in .alloy/build-tree (gitignored,
|
|
4
|
+
regenerated every run). `alloy export cmake` will later emit a standalone
|
|
5
|
+
copy for people who want to own their build.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import shutil
|
|
11
|
+
import subprocess
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from .emit.common import EmitError
|
|
16
|
+
from .project import Project
|
|
17
|
+
|
|
18
|
+
# Core name -> GCC -mcpu flag; fallback per architecture.
|
|
19
|
+
_CPU_BY_CORE = {
|
|
20
|
+
"cm0": "cortex-m0",
|
|
21
|
+
"cm0plus": "cortex-m0plus",
|
|
22
|
+
"cm3": "cortex-m3",
|
|
23
|
+
"cm4": "cortex-m4",
|
|
24
|
+
"cm7": "cortex-m7",
|
|
25
|
+
"cm33": "cortex-m33",
|
|
26
|
+
}
|
|
27
|
+
_CPU_BY_ARCH = {
|
|
28
|
+
"armv6m": "cortex-m0plus",
|
|
29
|
+
"armv7m": "cortex-m3",
|
|
30
|
+
"armv7em": "cortex-m4",
|
|
31
|
+
"armv8m_base": "cortex-m23",
|
|
32
|
+
"armv8m_main": "cortex-m33",
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _arch_ns(chip: dict[str, Any]) -> str:
|
|
37
|
+
arch = chip["cores"][0]["arch"]
|
|
38
|
+
if arch.startswith("armv"):
|
|
39
|
+
return "cortex_m"
|
|
40
|
+
if arch.startswith("xtensa"):
|
|
41
|
+
return "xtensa"
|
|
42
|
+
raise EmitError(f"unsupported architecture {arch}")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _xtensa_prefix() -> str:
|
|
46
|
+
if found := shutil.which("xtensa-esp-elf-gcc"):
|
|
47
|
+
return str(Path(found).with_name("xtensa-esp-elf-"))
|
|
48
|
+
candidate = Path.home() / ".alloy/tools/xtensa-esp-elf/bin/xtensa-esp-elf-gcc"
|
|
49
|
+
if candidate.exists():
|
|
50
|
+
return str(candidate.with_name("xtensa-esp-elf-"))
|
|
51
|
+
raise EmitError(
|
|
52
|
+
"xtensa-esp-elf toolchain not found — looked on PATH and ~/.alloy/tools/xtensa-esp-elf"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _cpu_flags(chip: dict[str, Any]) -> str:
|
|
57
|
+
if _arch_ns(chip) == "xtensa":
|
|
58
|
+
# The unified xtensa-esp-elf toolchain is multi-core: -mdynconfig
|
|
59
|
+
# selects the concrete core (ESP32 = LX6 little-endian); without it
|
|
60
|
+
# GCC emits generic BIG-endian objects that esptool rejects. The flag
|
|
61
|
+
# must stay RELATIVE to match the multilib table (esp32/libgcc.a);
|
|
62
|
+
# macOS hardened dlopen then needs XTENSA_GNU_CONFIG (absolute) set
|
|
63
|
+
# in the environment — see _build_env().
|
|
64
|
+
return f"-mdynconfig=xtensa_{chip['family']}.so -mlongcalls"
|
|
65
|
+
core = chip["cores"][0]
|
|
66
|
+
cpu = _CPU_BY_CORE.get(core["name"]) or _CPU_BY_ARCH.get(core["arch"])
|
|
67
|
+
if cpu is None:
|
|
68
|
+
raise EmitError(f"no CPU mapping for core {core['name']} / arch {core['arch']}")
|
|
69
|
+
# Soft-float everywhere for now, even on FPU parts: hard-float needs a
|
|
70
|
+
# CPACR enable in the arch startup before the first FP instruction.
|
|
71
|
+
# TODO(arch): enable CP10/CP11 on armv7em, then honor core.fpu here.
|
|
72
|
+
return f"-mcpu={cpu} -mthumb -mfloat-abi=soft"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _toolchain_cmake(chip: dict[str, Any], cpu_flags: str) -> str:
|
|
76
|
+
if _arch_ns(chip) == "xtensa":
|
|
77
|
+
prefix = _xtensa_prefix()
|
|
78
|
+
processor = "xtensa"
|
|
79
|
+
cc, cxx = f"{prefix}gcc", f"{prefix}g++"
|
|
80
|
+
else:
|
|
81
|
+
processor = "arm"
|
|
82
|
+
cc, cxx = "arm-none-eabi-gcc", "arm-none-eabi-g++"
|
|
83
|
+
return f"""# GENERATED by alloy — DO NOT EDIT
|
|
84
|
+
set(CMAKE_SYSTEM_NAME Generic)
|
|
85
|
+
set(CMAKE_SYSTEM_PROCESSOR {processor})
|
|
86
|
+
set(CMAKE_C_COMPILER "{cc}")
|
|
87
|
+
set(CMAKE_CXX_COMPILER "{cxx}")
|
|
88
|
+
set(CMAKE_ASM_COMPILER "{cc}")
|
|
89
|
+
set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY)
|
|
90
|
+
set(CMAKE_C_FLAGS_INIT "{cpu_flags}")
|
|
91
|
+
set(CMAKE_CXX_FLAGS_INIT "{cpu_flags}")
|
|
92
|
+
set(CMAKE_ASM_FLAGS_INIT "{cpu_flags}")
|
|
93
|
+
set(CMAKE_EXE_LINKER_FLAGS_INIT "{cpu_flags}")
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _cmakelists(project: Project, chip: dict[str, Any], sources: list[Path],
|
|
98
|
+
runtime_sources: list[Path]) -> str:
|
|
99
|
+
gen = project.gen_dir
|
|
100
|
+
gen_sources = [gen / "board.cpp"]
|
|
101
|
+
for optional in ("vector_table.c", "boot2.c", "irq_data.c"):
|
|
102
|
+
if (gen / optional).exists():
|
|
103
|
+
gen_sources.append(gen / optional)
|
|
104
|
+
src_list = "\n ".join(
|
|
105
|
+
f'"{p}"' for p in [*sources, *gen_sources, *runtime_sources]
|
|
106
|
+
)
|
|
107
|
+
# newlib nano/nosys specs are an ARM-newlib convention; the xtensa
|
|
108
|
+
# toolchain links its own newlib without them.
|
|
109
|
+
specs = "" if _arch_ns(chip) == "xtensa" else """
|
|
110
|
+
--specs=nano.specs
|
|
111
|
+
--specs=nosys.specs"""
|
|
112
|
+
return f"""# GENERATED by alloy — DO NOT EDIT
|
|
113
|
+
cmake_minimum_required(VERSION 3.25)
|
|
114
|
+
project({project.name} C CXX ASM)
|
|
115
|
+
|
|
116
|
+
set(CMAKE_C_STANDARD 11)
|
|
117
|
+
set(CMAKE_CXX_STANDARD 23)
|
|
118
|
+
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
|
119
|
+
set(CMAKE_C_EXTENSIONS OFF)
|
|
120
|
+
set(CMAKE_CXX_EXTENSIONS OFF)
|
|
121
|
+
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
|
122
|
+
|
|
123
|
+
add_executable({project.name}.elf
|
|
124
|
+
{src_list}
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
target_include_directories({project.name}.elf PRIVATE
|
|
128
|
+
"{project.alloy_root / 'src'}"
|
|
129
|
+
"{gen}"
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
target_compile_options({project.name}.elf PRIVATE
|
|
133
|
+
-Os -g3
|
|
134
|
+
-ffunction-sections -fdata-sections
|
|
135
|
+
-Wall -Wextra
|
|
136
|
+
$<$<COMPILE_LANGUAGE:CXX>:-fno-exceptions>
|
|
137
|
+
$<$<COMPILE_LANGUAGE:CXX>:-fno-rtti>
|
|
138
|
+
$<$<COMPILE_LANGUAGE:CXX>:-fno-threadsafe-statics>
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
target_link_options({project.name}.elf PRIVATE
|
|
142
|
+
-T "{gen / 'linker.ld'}"
|
|
143
|
+
-nostartfiles
|
|
144
|
+
-Wl,--gc-sections
|
|
145
|
+
-Wl,-Map={project.name}.map{specs}
|
|
146
|
+
)
|
|
147
|
+
"""
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def build(project: Project, chip: dict[str, Any]) -> Path:
|
|
151
|
+
sources = sorted((project.root / "src").rglob("*.cpp")) + \
|
|
152
|
+
sorted((project.root / "src").rglob("*.c"))
|
|
153
|
+
if not sources:
|
|
154
|
+
raise EmitError(f"no sources under {project.root / 'src'}")
|
|
155
|
+
|
|
156
|
+
arch_dir = project.alloy_root / "src" / "alloy" / "arch" / _arch_ns(chip)
|
|
157
|
+
runtime_sources = sorted(arch_dir.glob("*.cpp")) + sorted(arch_dir.glob("*.S"))
|
|
158
|
+
|
|
159
|
+
tree = project.build_dir
|
|
160
|
+
tree.mkdir(parents=True, exist_ok=True)
|
|
161
|
+
(tree / "toolchain.cmake").write_text(_toolchain_cmake(chip, _cpu_flags(chip)))
|
|
162
|
+
(tree / "CMakeLists.txt").write_text(_cmakelists(project, chip, sources, runtime_sources))
|
|
163
|
+
|
|
164
|
+
env = None
|
|
165
|
+
if _arch_ns(chip) == "xtensa":
|
|
166
|
+
import os # noqa: PLC0415
|
|
167
|
+
|
|
168
|
+
dynconfig = (Path(_xtensa_prefix()).parent.parent / "lib" /
|
|
169
|
+
f"xtensa_{chip['family']}.so")
|
|
170
|
+
if not dynconfig.exists():
|
|
171
|
+
raise EmitError(f"toolchain dynconfig missing: {dynconfig}")
|
|
172
|
+
env = os.environ | {"XTENSA_GNU_CONFIG": str(dynconfig)}
|
|
173
|
+
|
|
174
|
+
out = tree / "out"
|
|
175
|
+
subprocess.run(
|
|
176
|
+
["cmake", "-G", "Ninja", "-S", str(tree), "-B", str(out),
|
|
177
|
+
f"-DCMAKE_TOOLCHAIN_FILE={tree / 'toolchain.cmake'}",
|
|
178
|
+
"-DCMAKE_BUILD_TYPE=MinSizeRel"],
|
|
179
|
+
check=True, env=env,
|
|
180
|
+
)
|
|
181
|
+
subprocess.run(["cmake", "--build", str(out)], check=True, env=env)
|
|
182
|
+
|
|
183
|
+
elf = out / f"{project.name}.elf"
|
|
184
|
+
if not elf.exists():
|
|
185
|
+
raise EmitError(f"build finished but {elf} is missing")
|
|
186
|
+
|
|
187
|
+
size_tool = (
|
|
188
|
+
f"{_xtensa_prefix()}size" if _arch_ns(chip) == "xtensa"
|
|
189
|
+
else shutil.which("arm-none-eabi-size")
|
|
190
|
+
)
|
|
191
|
+
if size_tool:
|
|
192
|
+
subprocess.run([size_tool, str(elf)], check=False)
|
|
193
|
+
|
|
194
|
+
# clangd support out of the box.
|
|
195
|
+
cc_json = out / "compile_commands.json"
|
|
196
|
+
link = project.root / "compile_commands.json"
|
|
197
|
+
if cc_json.exists():
|
|
198
|
+
link.unlink(missing_ok=True)
|
|
199
|
+
link.symlink_to(cc_json)
|
|
200
|
+
|
|
201
|
+
return elf
|
alloy_cli/cli.py
ADDED
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
"""alloy CLI.
|
|
2
|
+
|
|
3
|
+
Commands:
|
|
4
|
+
new scaffold a portable project (the scaffold IS the CI-built blink)
|
|
5
|
+
boards list boards (--json for IDE integrations)
|
|
6
|
+
gen regenerate .alloy/generated from the device database
|
|
7
|
+
build gen + render CMake tree + compile
|
|
8
|
+
flash build + program the board
|
|
9
|
+
monitor bidirectional serial monitor
|
|
10
|
+
run flash + monitor
|
|
11
|
+
clean remove per-board build trees
|
|
12
|
+
set-board change the board in alloy.toml
|
|
13
|
+
setup verify/install toolchains into ~/.alloy/tools
|
|
14
|
+
debug-info debug-server facts for a board (--json)
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import argparse
|
|
20
|
+
import subprocess
|
|
21
|
+
import sys
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
from alloy_devices.loader import load_database
|
|
25
|
+
|
|
26
|
+
from . import __version__
|
|
27
|
+
from .build import build
|
|
28
|
+
from .emit import generate
|
|
29
|
+
from .emit.common import EmitError
|
|
30
|
+
from .project import Project, ProjectError, load_project
|
|
31
|
+
|
|
32
|
+
_MAIN_CPP = """\
|
|
33
|
+
// Portable hello — blink AND echo, so the board never looks dead after a
|
|
34
|
+
// flash. Identical bytes on every supported board; zero #ifdefs.
|
|
35
|
+
#include <alloy/board.hpp>
|
|
36
|
+
|
|
37
|
+
#include <cstdint>
|
|
38
|
+
|
|
39
|
+
int main() {
|
|
40
|
+
board::init();
|
|
41
|
+
auto uart = board::debug_uart::open({.baud = board::debug_uart_baud});
|
|
42
|
+
uart.write("alloy hello: blinking + echoing\\r\\n");
|
|
43
|
+
|
|
44
|
+
std::uint32_t last_toggle = alloy::uptime_ms();
|
|
45
|
+
while (true) {
|
|
46
|
+
std::uint8_t byte{};
|
|
47
|
+
if (uart.read(byte)) {
|
|
48
|
+
uart.write(byte);
|
|
49
|
+
}
|
|
50
|
+
if (alloy::uptime_ms() - last_toggle >= 500u) {
|
|
51
|
+
board::led.toggle();
|
|
52
|
+
last_toggle = alloy::uptime_ms();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
_ALLOY_TOML = """\
|
|
59
|
+
[project]
|
|
60
|
+
name = "{name}"
|
|
61
|
+
|
|
62
|
+
[board]
|
|
63
|
+
id = "{board}"
|
|
64
|
+
|
|
65
|
+
[alloy]
|
|
66
|
+
root = "{root}"
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
_GITIGNORE = """\
|
|
70
|
+
.alloy/
|
|
71
|
+
compile_commands.json
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _project(args: argparse.Namespace) -> Project:
|
|
76
|
+
return load_project(
|
|
77
|
+
Path(getattr(args, "project", ".") or "."),
|
|
78
|
+
board_override=getattr(args, "board", None),
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def cmd_new(args: argparse.Namespace) -> int:
|
|
83
|
+
from .project import _find_alloy_root # noqa: PLC0415
|
|
84
|
+
|
|
85
|
+
target = Path(args.name)
|
|
86
|
+
if target.exists():
|
|
87
|
+
print(f"error: {target} already exists", file=sys.stderr)
|
|
88
|
+
return 1
|
|
89
|
+
alloy_root = _find_alloy_root(Path.cwd())
|
|
90
|
+
boards_dir = alloy_root / "boards"
|
|
91
|
+
if not (boards_dir / args.board / "board.json").exists():
|
|
92
|
+
known = sorted(p.name for p in boards_dir.iterdir() if (p / "board.json").exists())
|
|
93
|
+
print(f"error: unknown board '{args.board}' — known: {', '.join(known)}", file=sys.stderr)
|
|
94
|
+
return 1
|
|
95
|
+
(target / "src").mkdir(parents=True)
|
|
96
|
+
from .project import packaged_alloy_root # noqa: PLC0415
|
|
97
|
+
|
|
98
|
+
if alloy_root == packaged_alloy_root():
|
|
99
|
+
# Wheel installs are relocatable: resolution finds the package at
|
|
100
|
+
# runtime; baking a version-specific site-packages path would rot.
|
|
101
|
+
toml_text = _ALLOY_TOML.split("[alloy]")[0].format(
|
|
102
|
+
name=target.name, board=args.board
|
|
103
|
+
)
|
|
104
|
+
else:
|
|
105
|
+
toml_text = _ALLOY_TOML.format(
|
|
106
|
+
name=target.name, board=args.board, root=alloy_root
|
|
107
|
+
)
|
|
108
|
+
(target / "alloy.toml").write_text(toml_text)
|
|
109
|
+
(target / "src" / "main.cpp").write_text(_MAIN_CPP)
|
|
110
|
+
(target / ".gitignore").write_text(_GITIGNORE)
|
|
111
|
+
print(f"created {target}/ (board: {args.board}, framework: {alloy_root})")
|
|
112
|
+
print(f"next: cd {target} && alloy run")
|
|
113
|
+
return 0
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def cmd_boards(args: argparse.Namespace) -> int:
|
|
117
|
+
import json # noqa: PLC0415
|
|
118
|
+
|
|
119
|
+
project_dir = Path(getattr(args, "project", ".") or ".")
|
|
120
|
+
try:
|
|
121
|
+
project = load_project(project_dir)
|
|
122
|
+
boards_dir = project.alloy_root / "boards"
|
|
123
|
+
except ProjectError:
|
|
124
|
+
from .project import _find_alloy_root # noqa: PLC0415
|
|
125
|
+
|
|
126
|
+
boards_dir = _find_alloy_root(project_dir.resolve()) / "boards"
|
|
127
|
+
rows = []
|
|
128
|
+
for board_json in sorted(boards_dir.glob("*/board.json")):
|
|
129
|
+
board = json.loads(board_json.read_text())
|
|
130
|
+
rows.append(board)
|
|
131
|
+
if getattr(args, "json", False):
|
|
132
|
+
print(json.dumps({
|
|
133
|
+
"schema": "alloy.boards.v1",
|
|
134
|
+
"boards": [
|
|
135
|
+
{
|
|
136
|
+
"id": b["id"],
|
|
137
|
+
"name": b.get("name", ""),
|
|
138
|
+
"chip": b["chip"],
|
|
139
|
+
"probe": b.get("probe", {}).get("kind"),
|
|
140
|
+
"roles": sorted(b.get("roles", {})),
|
|
141
|
+
}
|
|
142
|
+
for b in rows
|
|
143
|
+
],
|
|
144
|
+
}, indent=2))
|
|
145
|
+
return 0
|
|
146
|
+
for b in rows:
|
|
147
|
+
print(f"{b['id']:24} {b.get('name', ''):32} chip={b['chip']}")
|
|
148
|
+
return 0
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def cmd_gen(args: argparse.Namespace) -> int:
|
|
152
|
+
project = _project(args)
|
|
153
|
+
written = generate(project)
|
|
154
|
+
print(f"generated {len(written)} file(s) -> {project.gen_dir}")
|
|
155
|
+
return 0
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def cmd_build(args: argparse.Namespace) -> int:
|
|
159
|
+
project = _project(args)
|
|
160
|
+
generate(project)
|
|
161
|
+
board = project.load_board()
|
|
162
|
+
db = load_database(project.devices_root)
|
|
163
|
+
chip = db.chips[board["chip"]]
|
|
164
|
+
elf = build(project, chip)
|
|
165
|
+
print(f"\nbuilt {elf}")
|
|
166
|
+
return 0
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def cmd_flash(args: argparse.Namespace) -> int:
|
|
170
|
+
from .flash import flash # noqa: PLC0415
|
|
171
|
+
|
|
172
|
+
project = _project(args)
|
|
173
|
+
generate(project)
|
|
174
|
+
board = project.load_board()
|
|
175
|
+
db = load_database(project.devices_root)
|
|
176
|
+
chip = db.chips[board["chip"]]
|
|
177
|
+
elf = build(project, chip)
|
|
178
|
+
runner = flash(board, chip, elf)
|
|
179
|
+
print(f"\nflashed {elf.name} via {runner}")
|
|
180
|
+
return 0
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def cmd_monitor(args: argparse.Namespace) -> int:
|
|
184
|
+
from .monitor import monitor # noqa: PLC0415
|
|
185
|
+
|
|
186
|
+
project = _project(args)
|
|
187
|
+
monitor(project.load_board())
|
|
188
|
+
return 0
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def cmd_run(args: argparse.Namespace) -> int:
|
|
192
|
+
from .emit.common import EmitError # noqa: PLC0415
|
|
193
|
+
from .monitor import monitor # noqa: PLC0415
|
|
194
|
+
|
|
195
|
+
rc = cmd_flash(args)
|
|
196
|
+
if rc != 0:
|
|
197
|
+
return rc
|
|
198
|
+
project = _project(args)
|
|
199
|
+
try:
|
|
200
|
+
monitor(project.load_board())
|
|
201
|
+
except EmitError as exc:
|
|
202
|
+
print(f"note: {exc}")
|
|
203
|
+
return 0
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def cmd_clean(args: argparse.Namespace) -> int:
|
|
207
|
+
import shutil # noqa: PLC0415
|
|
208
|
+
|
|
209
|
+
root = Path(getattr(args, "project", ".") or ".").resolve()
|
|
210
|
+
if not (root / "alloy.toml").exists():
|
|
211
|
+
print(f"error: {root} is not an alloy project (no alloy.toml)", file=sys.stderr)
|
|
212
|
+
return 1
|
|
213
|
+
base = root / ".alloy"
|
|
214
|
+
targets = [base] if args.all else (
|
|
215
|
+
[base / "build-tree" / args.board, base / "generated" / args.board]
|
|
216
|
+
if getattr(args, "board", None)
|
|
217
|
+
else [base / "build-tree"]
|
|
218
|
+
)
|
|
219
|
+
removed = 0
|
|
220
|
+
for target in targets:
|
|
221
|
+
if target.exists():
|
|
222
|
+
shutil.rmtree(target)
|
|
223
|
+
removed += 1
|
|
224
|
+
print(f"removed {target}")
|
|
225
|
+
if removed == 0:
|
|
226
|
+
print("nothing to clean")
|
|
227
|
+
return 0
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def cmd_set_board(args: argparse.Namespace) -> int:
|
|
231
|
+
import re # noqa: PLC0415
|
|
232
|
+
|
|
233
|
+
root = Path(getattr(args, "project", ".") or ".").resolve()
|
|
234
|
+
toml_path = root / "alloy.toml"
|
|
235
|
+
if not toml_path.exists():
|
|
236
|
+
print(f"error: {root} is not an alloy project (no alloy.toml)", file=sys.stderr)
|
|
237
|
+
return 1
|
|
238
|
+
# Validate against known boards first.
|
|
239
|
+
project = load_project(root)
|
|
240
|
+
boards_dir = project.alloy_root / "boards"
|
|
241
|
+
if not (boards_dir / args.board_id / "board.json").exists():
|
|
242
|
+
known = sorted(p.name for p in boards_dir.iterdir() if (p / "board.json").exists())
|
|
243
|
+
print(f"error: unknown board '{args.board_id}' — known: {', '.join(known)}",
|
|
244
|
+
file=sys.stderr)
|
|
245
|
+
return 1
|
|
246
|
+
text = toml_path.read_text()
|
|
247
|
+
new_text, n = re.subn(
|
|
248
|
+
r'(\[board\]\s*\n\s*id\s*=\s*")[^"]*(")',
|
|
249
|
+
rf"\g<1>{args.board_id}\g<2>",
|
|
250
|
+
text,
|
|
251
|
+
count=1,
|
|
252
|
+
)
|
|
253
|
+
if n != 1:
|
|
254
|
+
print("error: could not locate [board] id in alloy.toml", file=sys.stderr)
|
|
255
|
+
return 1
|
|
256
|
+
toml_path.write_text(new_text)
|
|
257
|
+
print(f"board set to {args.board_id}")
|
|
258
|
+
return 0
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def cmd_setup(args: argparse.Namespace) -> int:
|
|
262
|
+
from .toolchains import setup # noqa: PLC0415
|
|
263
|
+
|
|
264
|
+
families = set(args.family) if args.family else None
|
|
265
|
+
return setup(families, check_only=args.check,
|
|
266
|
+
json_out=args.json, json_progress=args.json_progress)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def cmd_debug_info(args: argparse.Namespace) -> int:
|
|
270
|
+
import json # noqa: PLC0415
|
|
271
|
+
|
|
272
|
+
from .debug import debug_info # noqa: PLC0415
|
|
273
|
+
|
|
274
|
+
project = _project(args)
|
|
275
|
+
board = project.load_board()
|
|
276
|
+
db = load_database(project.devices_root)
|
|
277
|
+
chip = db.chips[board["chip"]]
|
|
278
|
+
elf = project.build_dir / "out" / f"{project.name}.elf"
|
|
279
|
+
info = debug_info(board, chip, elf if elf.exists() else None)
|
|
280
|
+
info["schema"] = "alloy.debug_info.v1"
|
|
281
|
+
if getattr(args, "json", False):
|
|
282
|
+
print(json.dumps(info, indent=2))
|
|
283
|
+
else:
|
|
284
|
+
for key, value in info.items():
|
|
285
|
+
print(f"{key:14} {value}")
|
|
286
|
+
return 0
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def main() -> None:
|
|
290
|
+
parser = argparse.ArgumentParser(prog="alloy", description=__doc__)
|
|
291
|
+
parser.add_argument("--version", action="version", version=__version__)
|
|
292
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
293
|
+
|
|
294
|
+
p_new = sub.add_parser("new", help="scaffold a new project")
|
|
295
|
+
p_new.add_argument("name")
|
|
296
|
+
p_new.add_argument("--board", required=True)
|
|
297
|
+
p_new.set_defaults(func=cmd_new)
|
|
298
|
+
|
|
299
|
+
p_boards = sub.add_parser("boards", help="list known boards")
|
|
300
|
+
p_boards.add_argument("--project", default=".")
|
|
301
|
+
p_boards.add_argument("--json", action="store_true")
|
|
302
|
+
p_boards.set_defaults(func=cmd_boards)
|
|
303
|
+
|
|
304
|
+
p_clean = sub.add_parser("clean", help="remove per-board build trees")
|
|
305
|
+
p_clean.add_argument("--project", default=".")
|
|
306
|
+
p_clean.add_argument("--board", help="clean only this board's trees")
|
|
307
|
+
p_clean.add_argument("--all", action="store_true", help="remove the whole .alloy/")
|
|
308
|
+
p_clean.set_defaults(func=cmd_clean)
|
|
309
|
+
|
|
310
|
+
p_setb = sub.add_parser("set-board", help="change the board in alloy.toml")
|
|
311
|
+
p_setb.add_argument("board_id")
|
|
312
|
+
p_setb.add_argument("--project", default=".")
|
|
313
|
+
p_setb.set_defaults(func=cmd_set_board)
|
|
314
|
+
|
|
315
|
+
p_setup = sub.add_parser("setup", help="verify/install toolchains")
|
|
316
|
+
p_setup.add_argument("--family", action="append",
|
|
317
|
+
help="limit to a chip family (repeatable)")
|
|
318
|
+
p_setup.add_argument("--check", action="store_true", help="report only")
|
|
319
|
+
p_setup.add_argument("--json", action="store_true")
|
|
320
|
+
p_setup.add_argument("--json-progress", action="store_true")
|
|
321
|
+
p_setup.set_defaults(func=cmd_setup)
|
|
322
|
+
|
|
323
|
+
p_dbg = sub.add_parser("debug-info", help="debug-server facts for a board")
|
|
324
|
+
p_dbg.add_argument("--project", default=".")
|
|
325
|
+
p_dbg.add_argument("--board")
|
|
326
|
+
p_dbg.add_argument("--json", action="store_true")
|
|
327
|
+
p_dbg.set_defaults(func=cmd_debug_info)
|
|
328
|
+
|
|
329
|
+
for cmd, func in (("gen", cmd_gen), ("build", cmd_build), ("flash", cmd_flash),
|
|
330
|
+
("monitor", cmd_monitor), ("run", cmd_run)):
|
|
331
|
+
p = sub.add_parser(cmd)
|
|
332
|
+
p.add_argument("--project", default=".")
|
|
333
|
+
p.add_argument("--board", help="override the board declared in alloy.toml")
|
|
334
|
+
p.set_defaults(func=func)
|
|
335
|
+
|
|
336
|
+
args = parser.parse_args()
|
|
337
|
+
try:
|
|
338
|
+
sys.exit(args.func(args))
|
|
339
|
+
except (ProjectError, EmitError) as exc:
|
|
340
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
341
|
+
sys.exit(1)
|
|
342
|
+
except subprocess.CalledProcessError as exc:
|
|
343
|
+
cmd = exc.cmd[0] if isinstance(exc.cmd, list) else str(exc.cmd)
|
|
344
|
+
print(f"error: {cmd} failed (exit {exc.returncode})", file=sys.stderr)
|
|
345
|
+
sys.exit(1)
|
|
346
|
+
except KeyboardInterrupt:
|
|
347
|
+
sys.exit(130)
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
if __name__ == "__main__":
|
|
351
|
+
main()
|
alloy_cli/debug.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Debug-server facts per board — consumed by IDE integrations.
|
|
2
|
+
|
|
3
|
+
The openocd interface/target maps live HERE (flash.py imports them): one
|
|
4
|
+
source of truth for tool-integration knowledge. `debug_info` emits what a
|
|
5
|
+
Cortex-Debug launch.json needs; unsupported families say so honestly.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
# OpenOCD script maps (tool integration, not silicon facts).
|
|
14
|
+
OPENOCD_TARGET = {
|
|
15
|
+
"stm32g0": "stm32g0x",
|
|
16
|
+
"stm32f4": "stm32f4x",
|
|
17
|
+
"stm32f7": "stm32f7x",
|
|
18
|
+
"stm32g4": "stm32g4x",
|
|
19
|
+
"same70": "atsamv",
|
|
20
|
+
"rp2040": "rp2040",
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
OPENOCD_INTERFACE = {
|
|
24
|
+
"stlink": "stlink",
|
|
25
|
+
"cmsis-dap": "cmsis-dap",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def debug_info(board: dict[str, Any], chip: dict[str, Any],
|
|
30
|
+
elf: Path | None) -> dict[str, Any]:
|
|
31
|
+
probe = board.get("probe") or {}
|
|
32
|
+
kind = probe.get("kind", "")
|
|
33
|
+
interface = OPENOCD_INTERFACE.get(kind)
|
|
34
|
+
target = OPENOCD_TARGET.get(chip["family"])
|
|
35
|
+
|
|
36
|
+
info: dict[str, Any] = {
|
|
37
|
+
"board": board["id"],
|
|
38
|
+
"chip": f"{chip['vendor']}/{chip['part']}",
|
|
39
|
+
"elf": str(elf) if elf else None,
|
|
40
|
+
"svd": None, # SVD export is a future phase
|
|
41
|
+
}
|
|
42
|
+
if interface and target:
|
|
43
|
+
info.update(
|
|
44
|
+
supported=True,
|
|
45
|
+
servertype="openocd",
|
|
46
|
+
interface_cfg=f"interface/{interface}.cfg",
|
|
47
|
+
target_cfg=f"target/{target}.cfg",
|
|
48
|
+
device=chip["part"],
|
|
49
|
+
chip_id=probe.get("chip_id"),
|
|
50
|
+
gdb="arm-none-eabi-gdb",
|
|
51
|
+
)
|
|
52
|
+
else:
|
|
53
|
+
reason = (
|
|
54
|
+
"ESP32 interactive debug needs openocd-esp32 + xtensa gdb (future phase)"
|
|
55
|
+
if kind == "esptool"
|
|
56
|
+
else "BOOTSEL boards need a debug probe wired to SWD (picoprobe/debugprobe)"
|
|
57
|
+
if kind == "bootsel"
|
|
58
|
+
else f"no openocd mapping for probe kind '{kind}' / family '{chip['family']}'"
|
|
59
|
+
)
|
|
60
|
+
info.update(supported=False, reason=reason)
|
|
61
|
+
return info
|