
Eide
- 223 installs
- 534 repo stars
- Updated June 29, 2026
- zhinkgit/embeddedskills
Lets a firmware developer scan EIDE projects, pick a build config, and compile to ELF/HEX paths from the agent without manual unify_builder clicks.
About
eide is a Claude Code agent skill for developers who standardize on Embedded IDE (EIDE) inside VS Code. It discovers firmware workspaces by locating `.eide/eide.yml`, lists named build configurations, and drives the bundled unify_builder to incrementally compile, fully rebuild, or clean targets. After a successful run it surfaces artifact paths such as ELF and HEX so you can continue with J-Link, OpenOCD, or your own flash pipeline without retyping paths from the UI. The skill also runs toolchain size analysis on ELF output and turns noisy compiler logs into structured errors and warnings your agent can act on. You configure once via skill-level `config.json` (builder directory, optional `code_exe`, toolchain prefix, and safety-oriented operation_mode). Prerequisites are VS Code with the EIDE extension, an ARM AC5/AC6 or arm-none-eabi-gcc toolchain, Python 3, and PyYAML. It targets indie and small-team embedded builders who want repeatable agent-driven builds on Windows-first EIDE installs while staying compatible with a generic agent workflow.
- Scans directories for EIDE projects that contain `.eide/eide.yml`
- Enumerates ConfigName build profiles before compile or rebuild
- Supports incremental compile, full rebuild, and clean targets
- Returns `elf_file` and `hex_file` paths plus ELF text/data/bss size analysis
- Parses build logs into structured errors and warnings
Eide by the numbers
- 223 all-time installs (skills.sh)
- +22 installs in the week ending Jul 28, 2026 (Skillselion tracking)
- Ranked #383 of 1,435 DevOps & CI/CD skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/zhinkgit/embeddedskills --skill eideAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 223 |
|---|---|
| repo stars | ★ 534 |
| Last updated | June 29, 2026 |
| Repository | zhinkgit/embeddedskills ↗ |
What it does
Lets a firmware developer scan EIDE projects, pick a build config, and compile to ELF/HEX paths from the agent without manual unify_builder clicks.
Files
EIDE 嵌入式工程构建
本 skill 提供 EIDE (Embedded IDE) 工程的发现、构建配置枚举、增量编译、全量重建、清理和 ELF 大小分析能力,并返回可供 jlink/openocd 继续使用的固件产物路径。
EIDE 是 VS Code 下的嵌入式开发扩展,使用 ARM CC (AC5/AC6) 或 GCC 工具链,通过 unify_builder 统一构建后端驱动。
配置
环境级配置(skill/config.json)
skill 目录下的 config.json 包含环境级配置,首次使用前确认 builder_dir 路径正确:
{
"builder_dir": "C:\\Users\\<user>\\.vscode\\extensions\\cl.eide-<version>\\res\\tools\\win32\\unify_builder",
"builder_exe": "unify_builder.exe",
"code_exe": "code",
"toolchain_prefix": "arm-none-eabi-",
"operation_mode": 1
}builder_dir:EIDE unify_builder 所在目录(必填,位于 VS Code 扩展目录下)builder_exe:builder 可执行文件名,Windows 默认unify_builder.execode_exe:VS Code CLI 路径,默认从 PATH 查找codetoolchain_prefix:用于 size 分析的工具链前缀,默认arm-none-eabi-operation_mode:1直接执行 /2输出风险摘要但不阻塞 /3执行前确认
工程级配置(workspace/.embeddedskills/config.json)
工程级共享配置统一保存在工作区的 .embeddedskills/config.json 中:
{
"eide": {
"project": "",
"config": "",
"log_dir": ".embeddedskills/build"
}
}project:默认 EIDE 工程根目录(包含.eide/eide.yml的目录),构建成功后会自动更新config:默认构建配置名称(对应 eide.yml 中的 ConfigName),构建成功后会自动更新log_dir:构建日志输出目录,默认.embeddedskills/build
参数解析优先级
参数解析顺序(从高到低): 1. CLI 显式参数 2. 环境级配置(skill/config.json) 3. 工程级配置(.embeddedskills/config.json) 4. .embeddedskills/state.json(上次构建记录) 5. 搜索/询问
子命令
| 子命令 | 用途 | 风险 |
|---|---|---|
scan | 搜索当前目录下的 EIDE 工程(含 .eide/eide.yml 的目录) | 低 |
configs | 枚举工程中的构建配置 | 低 |
build | 增量编译 | 中 |
rebuild | 全量重建 | 中 |
clean | 清理构建产物 | 高 |
size | 分析 ELF 文件大小(text/data/bss 和内存使用) | 低 |
执行流程
1. 读取 config.json,确认 builder_dir 路径有效 2. 未指定子命令时默认执行 scan 3. 未提供工程路径时先执行 scan 搜索工程 4. 同时发现多个工程或多个配置时,列出选项让用户选择,绝不自动猜测 5. build/rebuild/clean 按 operation_mode 决定是否需要确认 6. build/rebuild 成功后,从构建目录解析 elf_file / hex_file 等产物路径 7. 所有构建命令基于 builder.params 调用 unify_builder,输出到日志文件后解析 8. size 默认分析最近一次构建产物的 .elf 文件
脚本调用
skill 目录下有 Python 脚本,使用标准库 + PyYAML 实现。
eide_project.py — 工程扫描与配置枚举
# 扫描工程
python <skill-dir>/scripts/eide_project.py scan --root <搜索目录> --json
# 枚举构建配置
python <skill-dir>/scripts/eide_project.py configs --project <工程目录> --jsoneide_build.py — 构建 / 重建 / 清理
python <skill-dir>/scripts/eide_build.py <build|rebuild|clean> \
--builder-dir <unify_builder目录> \
--project <工程根目录> \
--config <配置名称> \
--log-dir <日志目录> \
--jsonrebuild 额外支持 --clean-first 先清理再重建。
eide_size.py — ELF 大小分析
# 基本分析
python <skill-dir>/scripts/eide_size.py analyze \
--elf <elf文件路径> \
--toolchain-prefix arm-none-eabi- \
--json
# 对比分析
python <skill-dir>/scripts/eide_size.py compare \
--elf <elf文件1> \
--compare <elf文件2> \
--toolchain-prefix arm-none-eabi- \
--json输出格式
所有脚本以 JSON 格式返回,基础字段为 status(ok/error)、action、summary、details,并可能附带 context、artifacts、metrics、state、next_actions、timing。
成功示例:
{
"status": "ok",
"action": "build",
"summary": "build 成功,errors=0 warnings=2",
"details": {
"project": "Vendor/EIDE",
"config": "W20_Mainboard",
"build_dir": "build/W20_Mainboard",
"elf_file": "build/W20_Mainboard/MDK-ARM_F403A.elf",
"hex_file": "build/W20_Mainboard/MDK-ARM_F403A.hex",
"log_file": ".embeddedskills/build/MDK-ARM_F403A-W20_Mainboard-build.log"
},
"artifacts": {
"elf_file": "build/W20_Mainboard/MDK-ARM_F403A.elf",
"hex_file": "build/W20_Mainboard/MDK-ARM_F403A.hex",
"flash_file": "build/W20_Mainboard/MDK-ARM_F403A.hex",
"debug_file": "build/W20_Mainboard/MDK-ARM_F403A.elf"
},
"metrics": { "errors": 0, "warnings": 2, "flash_bytes": 32768, "ram_bytes": 8192 }
}错误示例:
{
"status": "error",
"action": "build",
"error": { "code": "builder_not_found", "message": "unify_builder.exe 不存在,请确认 EIDE 扩展已安装" }
}核心规则
- 不修改
.eide/eide.yml或任何 EIDE 工程配置文件 - 不自动猜测工程路径或构建配置,有歧义时必须询问用户
- 参数解析优先级详见上方"参数解析优先级"章节
- 构建成功后优先使用返回的
flash_file/debug_file与jlink/openocd串联 clean不在自动流程中隐式执行- 构建失败时优先展示首个错误和日志文件路径
- 结果回显中始终包含工程名、配置名、构建目录路径;构建成功时优先回显产物路径
- EIDE 工程以包含
.eide/eide.yml的目录为根目录
与 Keil 工程的关系
本项目中的 EIDE 工程与 Keil MDK 工程共享相同的源码和 ARM CC 工具链(D:\Keil_V543\ARM\ARMCLANG)。EIDE 工程通过 eide.yml 描述工程结构,builder.params 由 EIDE 自动生成并供 unify_builder 使用。两者的构建产物(.axf/.hex/.elf)格式兼容,可互换使用。
参考
- EIDE 扩展:在 VS Code 中搜索
cl.eide安装 eide.yml格式:见 EIDE 扩展文档builder.params:由 EIDE 自动生成,位于build/<ConfigName>/builder.params
{
"builder_dir": "C:\\Users\\Administrator\\.vscode\\extensions\\cl.eide-3.27.0\\res\\tools\\win32\\unify_builder",
"builder_exe": "unify_builder.exe",
"code_exe": "code",
"toolchain_prefix": "arm-none-eabi-",
"operation_mode": 1
}
eide
Claude Code skill,驱动 EIDE (Embedded IDE) 进行工程扫描、构建配置枚举、编译构建,并返回可交给 jlink/openocd 的产物路径。
功能
- 扫描目录下的 EIDE 工程(含
.eide/eide.yml的目录) - 枚举工程中的构建配置 (ConfigName)
- 增量编译 / 全量重建 / 清理
- 返回
elf_file/hex_file等产物路径,便于继续交给jlink/openocd - ELF 大小分析(text/data/bss 和内存使用)
- 解析构建日志,输出结构化错误/警告信息
环境要求
- VS Code — 提供
codeCLI - EIDE 扩展 — 提供
unify_builder - ARM CC (AC5/AC6) 或 arm-none-eabi-gcc — 工具链
- Python 3.x — 运行脚本(需要 PyYAML)
- PyYAML —
pip install pyyaml
配置
环境级配置(skill/config.json)
复制 config.example.json 为 config.json,根据实际安装路径修改:
{
"builder_dir": "C:\\Users\\<user>\\.vscode\\extensions\\cl.eide-3.27.0\\res\\tools\\win32\\unify_builder",
"builder_exe": "unify_builder.exe",
"code_exe": "code",
"toolchain_prefix": "arm-none-eabi-",
"operation_mode": 1
}| 字段 | 必填 | 说明 |
|---|---|---|
builder_dir | 是 | EIDE unify_builder 所在目录 |
builder_exe | 否 | builder 可执行文件名,默认 unify_builder.exe |
code_exe | 否 | VS Code CLI 路径,默认从 PATH 查找 |
toolchain_prefix | 否 | size 分析用的工具链前缀,默认 arm-none-eabi- |
operation_mode | 否 | 1 直接执行 / 2 输出风险摘要 / 3 执行前确认 |
工程级配置(workspace/.embeddedskills/config.json)
工程级共享配置保存在工作区的 .embeddedskills/config.json 中:
{
"eide": {
"project": "",
"config": "",
"log_dir": ".embeddedskills/build"
}
}| 字段 | 说明 |
|---|---|
project | 默认工程根目录(相对 workspace,包含 .eide/eide.yml) |
config | 默认构建配置名称 |
log_dir | 构建日志输出目录,默认 .embeddedskills/build |
参数解析优先级
参数解析顺序(从高到低): 1. CLI 显式参数 2. 环境级配置(skill/config.json) 3. 工程级配置(.embeddedskills/config.json) 4. state.json(上次构建记录) 5. 搜索/询问
EIDE unify_builder 参考
unify_builder CLI
unify_builder 是 EIDE 扩展的统一构建后端,位于 VS Code 扩展目录下:
<vscode-extensions>/cl.eide-<version>/res/tools/win32/unify_builder/命令
| 命令 | 说明 |
|---|---|
build | 增量编译 |
rebuild | 全量重建 |
clean | 清理构建产物 |
参数
| 参数 | 说明 |
|---|---|
--params <path> | builder.params 文件路径(必需) |
builder.params
builder.params 是由 EIDE 从 eide.yml 自动生成的 JSON 文件,包含完整构建配置:
name:项目名称target:配置名称(ConfigName)toolchain:工具链类型(AC5/AC6/GCC)toolchainLocation:工具链路径sourceList:源文件列表incDirs:include 路径defines:预定义宏options:编译/链接选项env:环境变量(含产物输出路径等)dumpPath/outDir:构建输出目录
构建产物
构建产物输出到 build/<ConfigName>/ 目录:
| 文件 | 说明 |
|---|---|
<ProjectName>.axf | Keil 兼容调试文件 |
<ProjectName>.elf | ELF 可执行文件 |
<ProjectName>.hex | Intel HEX 烧录文件 |
<ProjectName>.bin | 二进制烧录文件 |
<ProjectName>.s19 | Motorola S19 格式 |
<ProjectName>.map | 链接 Map 文件 |
compiler.log | 编译日志 |
unify_builder.log | 构建器日志 |
builder.params | 构建参数 |
常见编译错误
| 错误类型 | 可能原因 |
|---|---|
error: A1023E: missing "{" after #include | ARM 汇编文件使用了 C 风格 include |
error: L6218E: Undefined symbol | 链接阶段缺少源文件或库 |
Fatal error: L6002U: Could not open file | 链接脚本路径不存在 |
No such file or directory | include 路径或源文件路径不正确 |
command not found | 工具链未安装或路径配置错误 |
与 UV4 的关系
EIDE 项目可与 Keil MDK 项目共享同一套 ARM CC 工具链。两者产出的 .axf 和 .hex 文件格式完全兼容:
- EIDE
builder.params中的toolchainLocation指向 Keil 安装目录下的 ARM CC env.KEIL_OUTPUT_DIR环境变量指明了 Keil 期望的输出目录- 链接脚本(scatter file)与 Keil 工程共用同一份
"""EIDE build / rebuild / clean via unify_builder."""
from __future__ import annotations
import argparse
import json
import os
import re
import subprocess
import sys
import time
from pathlib import Path
ROOT_DIR = Path(__file__).resolve().parents[2]
if str(ROOT_DIR) not in sys.path:
sys.path.insert(0, str(ROOT_DIR))
from eide_runtime import ( # noqa: E402
build_artifacts,
get_state_entry,
hidden_subprocess_kwargs,
is_missing,
load_json_file,
load_local_config,
load_project_config,
load_workspace_state,
make_result,
make_timing,
normalize_path,
normalize_path_with_base,
now_iso,
output_json,
parameter_context,
resolve_param,
resolve_tool_param,
save_project_config,
update_state_entry,
workspace_root,
)
BUILDER_TIMEOUT_SEC = 1800
ARTIFACT_SUFFIXES = {
".axf": "axf_file",
".elf": "elf_file",
".hex": "hex_file",
".bin": "bin_file",
".s19": "s19_file",
".map": "map_file",
".htm": "htm_file",
}
def _find_builder_exe(builder_dir: str) -> str:
"""Locate unify_builder executable within the builder directory."""
dir_path = Path(builder_dir)
if not dir_path.is_dir():
return ""
candidates = [
dir_path / "unify_builder.exe",
dir_path / "unify_builder",
dir_path / "bin" / "unify_builder.exe",
dir_path / "bin" / "unify_builder",
]
for c in candidates:
if c.is_file():
return str(c.resolve())
for item in dir_path.iterdir():
name = item.name.lower()
if item.is_file() and (
name == "unify_builder.exe"
or name == "unify_builder"
or name.startswith("unify_builder")
):
return str(item.resolve())
return ""
def _find_builder_params(project_path: Path, config: str) -> Path | None:
"""Find builder.params for the given configuration."""
build_dir = project_path / "build" / config
bp = build_dir / "builder.params"
if bp.is_file():
return bp
# Try searching build subdirs
build_root = project_path / "build"
if build_root.is_dir():
for item in build_root.iterdir():
if item.is_dir():
bp = item / "builder.params"
if bp.is_file():
return bp
return None
def _collect_artifacts(project_path: Path, config: str) -> dict[str, str]:
"""Collect build output artifacts."""
build_dir = project_path / "build" / config
if not build_dir.is_dir():
return {}
details: dict[str, str] = {}
details["build_dir"] = str(build_dir.resolve())
# First pass: exact match from builder.params project name
bp_file = _find_builder_params(project_path, config)
project_name = ""
if bp_file:
try:
bp_data = json.loads(bp_file.read_text(encoding="utf-8"))
except (json.JSONDecodeError, OSError):
bp_data = {}
project_name = bp_data.get("name", "") or bp_data.get(
"ProjectName", ""
)
if not project_name:
for bp_file in build_dir.rglob("builder.params"):
try:
bp_data = json.loads(
bp_file.read_text(encoding="utf-8")
)
except (json.JSONDecodeError, OSError):
continue
n = bp_data.get("name", "") or bp_data.get("ProjectName", "")
if n:
project_name = n
break
if not project_name:
eide_yml = project_path / ".eide" / "eide.yml"
if eide_yml.is_file():
try:
import yaml # type: ignore[import-untyped]
with open(eide_yml, "r", encoding="utf-8") as f:
eide_data = yaml.safe_load(f) or {}
except Exception:
eide_data = {}
project_name = eide_data.get("name", project_path.name)
# Collect artifacts by name or by suffix
names_to_try = [project_name] if project_name else []
names_to_try.append(project_path.name)
for name in names_to_try:
for suffix, key in ARTIFACT_SUFFIXES.items():
candidate = build_dir / f"{name}{suffix}"
if candidate.is_file() and key not in details:
details[key] = str(candidate.resolve())
# Fallback: find by suffix in build dir
for suffix, key in ARTIFACT_SUFFIXES.items():
if key not in details:
matches = sorted(
build_dir.rglob(f"*{suffix}"),
key=lambda p: p.stat().st_mtime,
reverse=True,
)
for m in matches:
if m.is_file():
details[key] = str(m.resolve())
break
# Set convenience aliases
debug_file = details.get("elf_file") or details.get("axf_file")
flash_file = (
details.get("hex_file")
or details.get("bin_file")
or debug_file
)
if debug_file:
details["debug_file"] = debug_file
if flash_file:
details["flash_file"] = flash_file
return details
def _find_build_log(build_dir: Path) -> str:
"""Find the most relevant build log file."""
log_candidates = ["compiler.log", "unify_builder.log"]
for name in log_candidates:
log_file = build_dir / name
if log_file.is_file():
return str(log_file.resolve())
return ""
def parse_log(log_path: str) -> dict:
"""Parse build log for errors, warnings, and size info."""
metrics = {"errors": 0, "warnings": 0, "flash_bytes": 0, "ram_bytes": 0}
if not os.path.isfile(log_path):
return metrics
with open(log_path, "r", encoding="utf-8", errors="replace") as f:
content = f.read()
# ARM CC style: "N Error(s), M Warning(s)"
arm_match = re.search(
r"(\d+)\s+Error\(s\)\s*,\s*(\d+)\s+Warning\(s\)", content
)
if arm_match:
metrics["errors"] = int(arm_match.group(1))
metrics["warnings"] = int(arm_match.group(2))
# GCC style: "error:" count
if metrics["errors"] == 0:
err_count = len(
re.findall(r"(?:^|\s)error:\s", content, re.IGNORECASE)
)
warn_count = len(
re.findall(r"(?:^|\s)warning:\s", content, re.IGNORECASE)
)
if err_count > 0 or warn_count > 0:
metrics["errors"] = err_count
metrics["warnings"] = warn_count
# ARM CC Program Size line
size_match = re.search(
r"Program Size:\s+Code=(\d+)\s+RO-data=(\d+)\s+"
r"RW-data=(\d+)\s+ZI-data=(\d+)",
content,
)
if size_match:
code_size = int(size_match.group(1))
ro_data = int(size_match.group(2))
rw_data = int(size_match.group(3))
zi_data = int(size_match.group(4))
metrics["flash_bytes"] = code_size + ro_data + rw_data
metrics["ram_bytes"] = rw_data + zi_data
# GCC style size info: .text/.data/.bss
if metrics["flash_bytes"] == 0:
gcc_size = re.search(
r"\.text\s+(\d+)\s+.*?\.data\s+(\d+)\s+.*?\.bss\s+(\d+)",
content,
)
if gcc_size:
text_sz = int(gcc_size.group(1))
data_sz = int(gcc_size.group(2))
bss_sz = int(gcc_size.group(3))
metrics["flash_bytes"] = text_sz + data_sz
metrics["ram_bytes"] = data_sz + bss_sz
return metrics
def run_builder(
builder_exe: str,
action: str,
project: str,
config: str,
log_dir: str,
clean_first: bool = False,
) -> dict:
"""Execute unify_builder for build/rebuild/clean."""
project_path = Path(project).resolve()
if not project_path.is_dir():
return {
"status": "error",
"action": action,
"error": {
"code": "project_not_found",
"message": f"Project directory not found: {project_path}",
},
}
if not os.path.isfile(builder_exe):
return {
"status": "error",
"action": action,
"error": {
"code": "builder_not_found",
"message": (
f"unify_builder not found: {builder_exe}\n"
"Please install the EIDE VS Code extension and "
"configure builder_dir in config.json"
),
},
}
# Find builder.params
bp_file = _find_builder_params(project_path, config)
if not bp_file:
return {
"status": "error",
"action": action,
"error": {
"code": "params_not_found",
"message": (
f"builder.params not found for config '{config}'\n"
"Please ensure the project has been opened with "
"EIDE at least once to generate builder.params"
),
},
}
build_dir = bp_file.parent
log_path = Path(log_dir).resolve()
log_path.mkdir(parents=True, exist_ok=True)
project_name = project_path.name
log_file = (
log_path / f"{project_name}-{config}-{action}.log"
)
# unify_builder commands:
# build - incremental build
# rebuild - full rebuild
# clean - clean build outputs
action_map = {
"build": "build",
"rebuild": "rebuild",
"clean": "clean",
}
builder_action = action_map.get(action, action)
cmd = [builder_exe, builder_action, "--params-file", str(bp_file)]
if clean_first and action == "rebuild":
cmd = [builder_exe, "clean", "--params-file", str(bp_file)]
try:
proc = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=BUILDER_TIMEOUT_SEC,
cwd=str(project_path),
encoding="utf-8",
errors="replace",
**hidden_subprocess_kwargs(),
)
except subprocess.TimeoutExpired:
return {
"status": "error",
"action": action,
"error": {
"code": "timeout",
"message": f"Build timed out ({BUILDER_TIMEOUT_SEC}s)",
},
}
except Exception as exc:
return {
"status": "error",
"action": action,
"error": {"code": "exec_error", "message": str(exc)},
}
# Write log
log_content_parts = []
if proc.stdout:
log_content_parts.append(proc.stdout)
if proc.stderr:
log_content_parts.append("--- STDERR ---")
log_content_parts.append(proc.stderr)
build_log = _find_build_log(build_dir)
if build_log and os.path.isfile(build_log):
log_content_parts.append(f"--- BUILD LOG: {build_log} ---")
try:
with open(
build_log, "r", encoding="utf-8", errors="replace"
) as f:
log_content_parts.append(f.read())
except OSError:
pass
log_file.write_text(
"\n".join(log_content_parts), encoding="utf-8"
)
# If clean_first and action is rebuild, now run the actual build
if clean_first and action == "rebuild":
cmd = [builder_exe, "build", "--params-file", str(bp_file)]
try:
proc2 = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=BUILDER_TIMEOUT_SEC,
cwd=str(project_path),
encoding="utf-8",
errors="replace",
**hidden_subprocess_kwargs(),
)
except subprocess.TimeoutExpired:
return {
"status": "error",
"action": action,
"error": {
"code": "timeout",
"message": f"Build timed out ({BUILDER_TIMEOUT_SEC}s)",
},
}
except Exception as exc:
return {
"status": "error",
"action": action,
"error": {"code": "exec_error", "message": str(exc)},
}
proc = proc2
build_log = _find_build_log(build_dir)
if build_log and os.path.isfile(build_log):
try:
with open(build_log, "r", encoding="utf-8", errors="replace") as f:
log_file.write_text(
log_file.read_text(encoding="utf-8")
+ f"\n--- AFTER CLEAN ---\n{f.read()}",
encoding="utf-8",
)
except OSError:
pass
# Parse results
primary_log = build_log if build_log else str(log_file)
metrics = parse_log(primary_log)
rc = proc.returncode
is_error = rc != 0 or metrics["errors"] > 0
status = "error" if is_error else "ok"
details: dict[str, object] = {
"project": str(project_path),
"config": config,
"build_dir": str(build_dir),
"log_file": str(log_file.resolve()),
"builder_exit_code": rc,
**_collect_artifacts(project_path, config),
}
result: dict[str, object] = {
"status": status,
"action": action,
"metrics": metrics,
"details": details,
}
if status == "error":
error_msg = f"Build failed with exit code {rc}"
if proc.stderr:
stderr_lines = [
l for l in proc.stderr.splitlines() if l.strip()
]
if stderr_lines:
first_error = stderr_lines[0][:200]
error_msg = (
f"Build failed: {first_error}"
)
result["error"] = {
"code": "build_failed",
"message": error_msg,
}
return result
def _build_summary(
action: str, status: str, metrics: dict
) -> str:
errors = metrics.get("errors", 0)
warnings = metrics.get("warnings", 0)
if status == "error":
return f"{action} failed, errors={errors} warnings={warnings}"
if action in ("build", "rebuild"):
return f"{action} succeeded, errors={errors} warnings={warnings}"
return f"{action} succeeded"
def _next_actions(
action: str, artifacts: dict
) -> list[str]:
actions: list[str] = []
if action in ("build", "rebuild") and artifacts.get("flash_file"):
actions.append(
"artifacts.flash_file can be reused for flash via jlink/openocd"
)
if action in ("build", "rebuild") and artifacts.get("debug_file"):
actions.append(
"artifacts.debug_file can be reused for gdb debugging"
)
return actions
def _make_relative_to_workspace(
workspace: Path, path: str
) -> str:
try:
p = Path(path).resolve()
rel = p.relative_to(workspace.resolve())
return str(rel).replace("\\", "/")
except ValueError:
return path
def main() -> None:
parser = argparse.ArgumentParser(
description="EIDE build / rebuild / clean"
)
parser.add_argument(
"action", choices=["build", "rebuild", "clean"]
)
parser.add_argument(
"--builder-dir", default=None, help="unify_builder directory path"
)
parser.add_argument(
"--project", default=None, help="EIDE project root directory"
)
parser.add_argument(
"--config", default=None, help="Build configuration name"
)
parser.add_argument(
"--log-dir", default=None, help="Log output directory"
)
parser.add_argument(
"--clean-first", action="store_true",
help="Clean before rebuild"
)
parser.add_argument(
"--config-file", default=None, help="Path to skill config.json"
)
parser.add_argument(
"--workspace", default=None,
help="Workspace root directory, default cwd"
)
parser.add_argument(
"--json", action="store_true", dest="as_json"
)
args = parser.parse_args()
started_at = now_iso()
started_ts = time.time()
workspace = workspace_root(args.workspace)
local_config = load_local_config(__file__)
project_config = load_project_config(str(workspace))
state = load_workspace_state(str(workspace))
last_build = get_state_entry(state, "last_build")
parameter_sources: dict[str, str] = {}
try:
# builder_dir: CLI > config > auto-detect > required
builder_dir_val, parameter_sources["builder_dir"] = (
resolve_tool_param(
"builder_dir",
args.builder_dir,
local_config=local_config,
local_keys=["builder_dir"],
required=True,
)
)
builder_exe_name, _ = resolve_tool_param(
"builder_exe",
None,
local_config=local_config,
local_keys=["builder_exe"],
default="unify_builder.exe",
)
# Resolve the actual builder executable
builder_dir_path = Path(builder_dir_val)
builder_exe_candidate = str(
builder_dir_path / str(builder_exe_name)
)
if not os.path.isfile(builder_exe_candidate):
alt = _find_builder_exe(builder_dir_val)
if alt:
builder_exe_candidate = alt
builder_exe = builder_exe_candidate
# project: CLI > project_config > state > required
project, parameter_sources["project"] = resolve_param(
"project",
args.project,
config=local_config,
config_keys=["default_project"],
normalize_as_path=True,
workspace=str(workspace),
)
if is_missing(project) and not is_missing(
project_config.get("project")
):
project = normalize_path_with_base(
project_config.get("project"), workspace
)
parameter_sources["project"] = "project_config:project"
if is_missing(project) and not is_missing(
last_build.get("project")
):
project = normalize_path_with_base(
str(last_build.get("project")), workspace
)
parameter_sources["project"] = "state:project"
if is_missing(project):
raise ValueError("missing required param: project")
# config: CLI > project_config > state
config, parameter_sources["config"] = resolve_param(
"config",
args.config,
config=local_config,
config_keys=["default_config"],
)
if is_missing(config) and not is_missing(
project_config.get("config")
):
config = project_config.get("config")
parameter_sources["config"] = "project_config:config"
if is_missing(config) and not is_missing(
last_build.get("config")
):
config = last_build.get("config")
parameter_sources["config"] = "state:config"
# log_dir: CLI > project_config > local_config > default
log_dir_raw = (
args.log_dir
or project_config.get("log_dir")
or local_config.get("log_dir")
)
log_dir = normalize_path_with_base(
log_dir_raw or ".embeddedskills/build", workspace
)
if args.log_dir:
parameter_sources["log_dir"] = "cli"
elif project_config.get("log_dir"):
parameter_sources["log_dir"] = "project_config:log_dir"
elif local_config.get("log_dir"):
parameter_sources["log_dir"] = "config:log_dir"
else:
parameter_sources["log_dir"] = "default"
except ValueError as exc:
result = make_result(
status="error",
action=args.action,
summary=str(exc),
details={},
context=parameter_context(
provider="eide",
workspace=str(workspace),
parameter_sources=parameter_sources,
),
error={"code": "missing_param", "message": str(exc)},
timing=make_timing(
started_at, (time.time() - started_ts) * 1000
),
)
if args.as_json:
output_json(result)
else:
print(f"Error: {exc}", file=sys.stderr)
sys.exit(1)
raw_result = run_builder(
builder_exe=builder_exe,
action=args.action,
project=project,
config=config or "",
log_dir=log_dir,
clean_first=args.clean_first,
)
elapsed_ms = (time.time() - started_ts) * 1000
if raw_result["status"] == "error":
result = make_result(
status="error",
action=args.action,
summary=raw_result["error"]["message"],
details=raw_result.get("details", {}),
context=parameter_context(
provider="eide",
workspace=str(workspace),
parameter_sources=parameter_sources,
),
error=raw_result["error"],
timing=make_timing(started_at, elapsed_ms),
)
else:
details = raw_result["details"]
artifacts = build_artifacts(
elf_file=details.get("elf_file"),
hex_file=details.get("hex_file"),
bin_file=details.get("bin_file"),
axf_file=details.get("axf_file"),
flash_file=details.get("flash_file"),
debug_file=details.get("debug_file"),
build_dir=details.get("build_dir"),
log_file=details.get("log_file"),
)
summary = _build_summary(
args.action, raw_result["status"], raw_result["metrics"]
)
state_info = None
if raw_result["status"] == "ok":
state_info = update_state_entry(
"last_build",
{
"provider": "eide",
"action": args.action,
"project": project,
"config": config,
"log_dir": log_dir,
"artifacts": artifacts,
**artifacts,
},
str(workspace),
)
# Write back confirmed parameters to project config
project_rel = _make_relative_to_workspace(
workspace, project
)
save_project_config(
str(workspace),
{
"project": project_rel,
"config": config or "",
"log_dir": _make_relative_to_workspace(
workspace, log_dir
),
},
)
result = make_result(
status=raw_result["status"],
action=args.action,
summary=summary,
details=details,
context=parameter_context(
provider="eide",
workspace=str(workspace),
parameter_sources=parameter_sources,
),
artifacts=artifacts,
metrics=raw_result["metrics"],
state=state_info,
next_actions=_next_actions(args.action, artifacts),
timing=make_timing(started_at, elapsed_ms),
)
if args.as_json:
output_json(result)
return
if result["status"] == "ok":
print(f"[{args.action}] {result['summary']}")
if result.get("artifacts", {}).get("log_file"):
print(f" Log: {result['artifacts']['log_file']}")
if result.get("artifacts", {}).get("flash_file"):
print(f" Flash: {result['artifacts']['flash_file']}")
if result.get("artifacts", {}).get("debug_file"):
print(f" Debug: {result['artifacts']['debug_file']}")
if result.get("metrics", {}).get("flash_bytes"):
m = result["metrics"]
print(
f" Size: Flash={m['flash_bytes']}B "
f"RAM={m.get('ram_bytes', 0)}B"
)
else:
error = result.get("error", {})
print(
f"[{args.action}] Failed — "
f"{error.get('message', result['summary'])}",
file=sys.stderr,
)
if result.get("details", {}).get("log_file"):
print(
f" Log: {result['details']['log_file']}",
file=sys.stderr,
)
sys.exit(1)
if __name__ == "__main__":
main()
"""EIDE project scanner and configuration enumerator.
Parses .eide/eide.yml to discover projects and list build configurations.
"""
from __future__ import annotations
import argparse
import json
import os
import sys
from pathlib import Path
from typing import Any
EIDE_YML = ".eide" + os.sep + "eide.yml"
def _load_yaml(path: str) -> dict:
"""Load a YAML file, trying PyYAML first then falling back to a
built-in minimal parser for the subset of YAML that eide.yml uses."""
try:
import yaml # type: ignore[import-untyped]
with open(path, "r", encoding="utf-8") as f:
return yaml.safe_load(f) or {}
except ImportError:
pass
return _parse_simple_yaml(path)
def _parse_simple_yaml(path: str) -> dict:
"""Minimal YAML parser for eide.yml structure.
Handles the subset of YAML that EIDE uses: scalars, basic lists,
and nested maps. Not a general-purpose YAML parser.
"""
with open(path, "r", encoding="utf-8") as f:
lines = f.readlines()
result: dict[str, Any] = {}
stack: list[tuple[dict, int]] = [(result, -1)]
current_list: list | None = None
current_list_parent: dict | None = None
current_list_key: str | None = None
for line in lines:
stripped = line.rstrip("\n\r")
if not stripped or stripped.lstrip().startswith("#"):
continue
indent = len(line) - len(line.lstrip())
if stripped.lstrip().startswith("- "):
value_text = stripped.lstrip()[2:].strip()
parsed_value = _parse_yaml_value(value_text)
while stack and stack[-1][1] >= indent:
stack.pop()
if current_list is None:
parent, _ = stack[-1]
if parsed_value is not None:
if current_list_key and current_list_parent is not None:
if current_list_key not in current_list_parent:
current_list_parent[current_list_key] = []
current_list_parent[current_list_key].append(parsed_value)
else:
if parsed_value is not None:
current_list.append(parsed_value)
continue
if ":" in stripped:
colon_idx = stripped.index(":")
key = stripped[:colon_idx].strip()
value_text = stripped[colon_idx + 1:].strip()
parsed_value = _parse_yaml_value(value_text)
while stack and stack[-1][1] >= indent:
stack.pop()
if parsed_value is not None:
parent, _ = stack[-1] if stack else (result, -1)
parent[key] = parsed_value
current_list = None
current_list_key = None
current_list_parent = None
elif value_text == "" or not value_text:
parent, _ = stack[-1] if stack else (result, -1)
new_map: dict = {}
parent[key] = new_map
stack.append((new_map, indent))
current_list = None
current_list_key = None
current_list_parent = None
else:
parent, _ = stack[-1] if stack else (result, -1)
if value_text == "[]":
parent[key] = []
current_list = parent[key]
current_list_key = key
current_list_parent = parent
else:
parent[key] = _parse_yaml_value(value_text)
return result
def _parse_yaml_value(text: str) -> Any:
"""Parse a YAML scalar value."""
if not text:
return None
if text in ("true", "True", "TRUE", "yes", "Yes", "YES"):
return True
if text in ("false", "False", "FALSE", "no", "No", "NO"):
return False
if text in ("null", "Null", "NULL", "~"):
return None
if text == "[]":
return []
if text == "{}":
return {}
if text.startswith('"') and text.endswith('"'):
return text[1:-1]
if text.startswith("'") and text.endswith("'"):
return text[1:-1]
try:
return int(text)
except ValueError:
pass
try:
return float(text)
except ValueError:
pass
return text
def scan_projects(root: str) -> list[dict]:
"""Recursively search for directories containing .eide/eide.yml."""
root_path = Path(root).resolve()
projects = []
for p in root_path.rglob(EIDE_YML):
project_dir = p.parents[1]
try:
eide_data = _load_yaml(str(p))
except Exception:
eide_data = {}
name = eide_data.get("name", project_dir.name)
device = eide_data.get("deviceName", "")
proj_type = eide_data.get("type", "")
projects.append({
"path": str(project_dir),
"name": str(name),
"device": str(device),
"type": str(proj_type),
"yaml_file": str(p),
})
projects.sort(key=lambda x: x["path"])
return projects
def list_configs(project_path: str) -> list[dict]:
"""Extract build configurations from eide.yml.
Each config is identified by its ConfigName from the builder.params
env section, or derived from the project structure.
"""
p = Path(project_path).resolve()
yml_path = p / EIDE_YML
if not yml_path.exists():
raise FileNotFoundError(f"EIDE project file not found: {yml_path}")
# Check builder.params first for configuration info
builder_params_path = p / "build"
configs = []
if builder_params_path.exists():
for build_dir in sorted(builder_params_path.iterdir()):
bp_file = build_dir / "builder.params"
if bp_file.is_file():
try:
bp_data = load_json_file(str(bp_file))
config_name = build_dir.name
if bp_data:
config_name = bp_data.get("target", config_name)
configs.append({
"name": config_name,
"build_dir": str(build_dir),
"builder_params": str(bp_file),
})
except Exception:
pass
if not configs:
try:
eide_data = _load_yaml(str(yml_path))
except Exception:
eide_data = {}
name = eide_data.get("name", p.name)
configs.append({
"name": name,
"build_dir": "",
"builder_params": "",
})
return configs
def load_json_file(path: str | Path) -> dict:
p = Path(path)
if not p.exists():
return {}
try:
return json.loads(p.read_text(encoding="utf-8"))
except (json.JSONDecodeError, OSError):
return {}
def output_json(data: dict):
sys.stdout.reconfigure(encoding="utf-8")
print(json.dumps(data, ensure_ascii=False, indent=2))
def main():
parser = argparse.ArgumentParser(
description="EIDE project scanner and config enumeration"
)
sub = parser.add_subparsers(dest="command")
scan_p = sub.add_parser("scan", help="Search for EIDE projects")
scan_p.add_argument("--root", default=".", help="Search root directory")
scan_p.add_argument("--json", action="store_true", dest="as_json")
configs_p = sub.add_parser("configs", help="List build configurations")
configs_p.add_argument(
"--project", required=True, help="Project root directory"
)
configs_p.add_argument("--json", action="store_true", dest="as_json")
args = parser.parse_args()
if args.command == "scan":
projects = scan_projects(args.root)
result = {
"status": "ok",
"action": "scan",
"details": {"projects": projects, "count": len(projects)},
}
if args.as_json:
output_json(result)
else:
if not projects:
print("No EIDE projects found")
else:
print(f"Found {len(projects)} project(s):")
for i, p in enumerate(projects, 1):
extra = ""
if p.get("device"):
extra = f" [{p['device']}]"
print(f" {i}. {p['name']}{extra} — {p['path']}")
elif args.command == "configs":
try:
configs = list_configs(args.project)
result = {
"status": "ok",
"action": "configs",
"details": {
"project": args.project,
"configs": configs,
"count": len(configs),
},
}
if args.as_json:
output_json(result)
else:
if not configs:
print("No build configurations found")
else:
print(
f"Project {args.project} has "
f"{len(configs)} configuration(s):"
)
for i, c in enumerate(configs, 1):
print(f" {i}. {c['name']}")
except (FileNotFoundError, ValueError) as e:
result = {
"status": "error",
"action": "configs",
"error": {"code": "invalid_project", "message": str(e)},
}
if args.as_json:
output_json(result)
else:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
else:
parser.print_help()
sys.exit(1)
if __name__ == "__main__":
main()
"""eide skill runtime utilities."""
from __future__ import annotations
import json
import os
import subprocess
import sys
from datetime import datetime
from pathlib import Path
from shutil import which
from typing import Any
STATE_DIR_NAME = ".embeddedskills"
STATE_FILE_NAME = "state.json"
PROJECT_CONFIG_FILE_NAME = "config.json"
SKILL_NAME = "eide"
def now_iso() -> str:
return datetime.now().astimezone().isoformat(timespec="seconds")
def default_config_path(script_file: str) -> Path:
return Path(script_file).resolve().parents[1] / "config.json"
def load_local_config(script_file: str | None = None) -> dict:
if script_file is None:
import inspect
frame = inspect.currentframe()
if frame and frame.f_back:
script_file = frame.f_back.f_globals.get("__file__", "")
if not script_file:
return {}
config_path = default_config_path(script_file)
return load_json_file(config_path)
def save_local_config(data: dict, script_file: str | None = None) -> Path | None:
if script_file is None:
import inspect
frame = inspect.currentframe()
if frame and frame.f_back:
script_file = frame.f_back.f_globals.get("__file__", "")
if not script_file:
return None
config_path = default_config_path(script_file)
existing = load_json_file(config_path)
existing.update(data)
save_json_file(config_path, existing)
return config_path
def load_project_config(workspace: str | None = None) -> dict:
ws = workspace_root(workspace)
config_file = ws / STATE_DIR_NAME / PROJECT_CONFIG_FILE_NAME
data = load_json_file(config_file)
return data.get(SKILL_NAME, {})
def save_project_config(
workspace: str | None = None, values: dict | None = None
) -> Path | None:
if values is None:
values = {}
ws = workspace_root(workspace)
config_file = ws / STATE_DIR_NAME / PROJECT_CONFIG_FILE_NAME
data = load_json_file(config_file)
data[SKILL_NAME] = {**(data.get(SKILL_NAME, {})), **values}
save_json_file(config_file, data)
return config_file
def output_json(data: dict, *, indent: int = 2) -> None:
sys.stdout.reconfigure(encoding="utf-8")
print(json.dumps(data, ensure_ascii=False, indent=indent), flush=True)
def is_missing(value: Any) -> bool:
return value is None or value == ""
def normalize_path(value: str | None) -> str:
if is_missing(value):
return ""
return str(Path(str(value)).expanduser().resolve())
def normalize_path_with_base(
value: str | None, base: str | Path | None = None
) -> str:
if is_missing(value):
return ""
path = Path(str(value)).expanduser()
if base and not path.is_absolute():
path = Path(base) / path
return str(path.resolve())
def _serialize_state_value(value: Any, workspace: Path) -> Any:
if isinstance(value, dict):
return {
key: _serialize_state_value(item, workspace)
for key, item in value.items()
}
if isinstance(value, list):
return [_serialize_state_value(item, workspace) for item in value]
if not isinstance(value, str) or "://" in value:
return value
path = Path(value).expanduser()
if not path.is_absolute():
return value
try:
return Path(os.path.relpath(path.resolve(), workspace)).as_posix()
except ValueError:
return value
def hidden_subprocess_kwargs() -> dict:
if sys.platform != "win32":
return {}
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
startupinfo.wShowWindow = getattr(subprocess, "SW_HIDE", 0)
return {
"creationflags": getattr(subprocess, "CREATE_NO_WINDOW", 0),
"startupinfo": startupinfo,
}
def load_json_file(path: str | Path) -> dict:
file_path = Path(path)
if not file_path.exists():
return {}
try:
return json.loads(file_path.read_text(encoding="utf-8"))
except (json.JSONDecodeError, OSError):
return {}
def save_json_file(path: str | Path, data: dict) -> None:
file_path = Path(path)
file_path.parent.mkdir(parents=True, exist_ok=True)
file_path.write_text(
json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8"
)
def workspace_root(workspace: str | None = None) -> Path:
if not is_missing(workspace):
return Path(str(workspace)).expanduser().resolve()
return Path.cwd().resolve()
def load_workspace_state(workspace: str | None = None) -> dict:
return load_json_file(
workspace_root(workspace) / STATE_DIR_NAME / STATE_FILE_NAME
)
def save_workspace_state(state: dict, workspace: str | None = None) -> Path:
ws = workspace_root(workspace)
file_path = ws / STATE_DIR_NAME / STATE_FILE_NAME
save_json_file(file_path, _serialize_state_value(state, ws))
return file_path
def get_state_entry(state: dict | None, key: str) -> dict:
if not isinstance(state, dict):
return {}
value = state.get(key, {})
return value if isinstance(value, dict) else {}
def update_state_entry(
category: str, record: dict, workspace: str | None = None
) -> dict:
ws = workspace_root(workspace)
state = load_workspace_state(workspace)
state[category] = _serialize_state_value(
{**record, "timestamp": record.get("timestamp") or now_iso()}, ws
)
file_path = save_workspace_state(state, workspace)
return {
"workspace": str(ws),
"file": str(file_path),
"updated_keys": [category],
category: state[category],
}
def _first_resolved(mapping: dict, keys: list[str]) -> tuple[Any, str | None]:
for key in keys:
value = mapping.get(key)
if not is_missing(value):
return value, key
return None, None
def _auto_detect_builder_dir() -> str:
vscode_ext = os.environ.get("USERPROFILE", os.path.expanduser("~"))
base = Path(vscode_ext) / ".vscode" / "extensions"
if base.exists():
for d in sorted(base.iterdir(), reverse=True):
if d.is_dir() and d.name.startswith("cl.eide-"):
builder_dir = d / "res" / "tools" / "win32" / "unify_builder"
if builder_dir.exists():
return str(builder_dir.resolve())
return ""
def _auto_detect_code() -> str:
code = which("code")
if code:
return str(Path(code).resolve())
candidates = [
r"C:\Program Files\Microsoft VS Code\bin\code.cmd",
r"C:\Program Files (x86)\Microsoft VS Code\bin\code.cmd",
os.path.join(
os.environ.get("LOCALAPPDATA", ""),
"Programs",
"Microsoft VS Code",
"bin",
"code.cmd",
),
]
for c in candidates:
if c and Path(c).is_file():
return str(Path(c).resolve())
return ""
def resolve_tool_param(
name: str,
cli_value: Any,
*,
local_config: dict | None = None,
local_keys: list[str] | None = None,
path_candidates: list[str] | tuple[str, ...] | None = None,
default: Any = None,
required: bool = False,
) -> tuple[Any, str]:
if not is_missing(cli_value):
value = normalize_path(str(cli_value))
source = "cli"
else:
value = None
source = ""
if local_config and local_keys:
value, key = _first_resolved(local_config, local_keys)
if not is_missing(value):
value = normalize_path(str(value))
source = f"config:{key}"
if is_missing(value) and path_candidates:
for candidate in path_candidates:
if is_missing(candidate):
continue
resolved = which(str(candidate))
if resolved:
value = normalize_path(resolved)
source = f"path:{candidate}"
break
if is_missing(value) and not is_missing(default):
value = default
source = f"default:{default}" if isinstance(default, str) else "default"
if required and is_missing(value):
raise ValueError(f"missing required param: {name}")
return value, source
def resolve_param(
name: str,
cli_value: Any,
*,
config: dict | None = None,
config_keys: list[str] | None = None,
state_record: dict | None = None,
state_keys: list[str] | None = None,
required: bool = False,
normalize_as_path: bool = False,
workspace: str | None = None,
) -> tuple[Any, str]:
if not is_missing(cli_value):
value = cli_value
source = "cli"
else:
value = None
source = ""
if config and config_keys:
value, config_key = _first_resolved(config, config_keys)
if not is_missing(value):
source = f"config:{config_key}"
if is_missing(value) and state_record and state_keys:
value, state_key = _first_resolved(state_record, state_keys)
if not is_missing(value):
source = f"state:{state_key}"
if normalize_as_path and not is_missing(value):
value = normalize_path_with_base(
str(value), workspace_root(workspace)
)
if required and is_missing(value):
raise ValueError(f"missing required param: {name}")
return value, source
def compact_dict(data: dict | None) -> dict:
if not isinstance(data, dict):
return {}
return {
key: value
for key, value in data.items()
if value not in (None, "", [], {})
}
def build_artifacts(**paths: str) -> dict:
return {
key: normalize_path(str(value))
for key, value in paths.items()
if not is_missing(value)
}
def make_result(
*,
status: str,
action: str,
summary: str,
details: dict | None = None,
context: dict | None = None,
artifacts: dict | None = None,
metrics: dict | None = None,
state: dict | None = None,
next_actions: list[str] | None = None,
timing: dict | None = None,
error: dict | None = None,
) -> dict:
result = {
"status": status,
"action": action,
"summary": summary,
"details": compact_dict(details),
}
optional = {
"context": compact_dict(context),
"artifacts": compact_dict(artifacts),
"metrics": compact_dict(metrics),
"state": compact_dict(state),
"timing": compact_dict(timing),
}
for key, value in optional.items():
if value:
result[key] = value
if next_actions:
result["next_actions"] = [item for item in next_actions if item]
if error:
result["error"] = compact_dict(error)
return result
def make_timing(started_at: str, elapsed_ms: int | float) -> dict:
return {
"started_at": started_at,
"finished_at": now_iso(),
"elapsed_ms": int(elapsed_ms),
}
def parameter_context(
*,
provider: str,
workspace: str | None = None,
parameter_sources: dict | None = None,
config_path: str | None = None,
) -> dict:
context = {
"provider": provider,
"workspace": str(workspace_root(workspace)),
}
if parameter_sources:
context["parameter_sources"] = compact_dict(parameter_sources)
if not is_missing(config_path):
context["config_path"] = normalize_path(str(config_path))
return context
def _auto_detect_toolchain_prefix() -> str:
for prefix in ("arm-none-eabi-", "arm-eabi-", "arm-elf-"):
if which(prefix + "gcc") or which(prefix + "size"):
return prefix
return "arm-none-eabi-"
"""EIDE ELF size analysis using arm-none-eabi-size."""
from __future__ import annotations
import argparse
import json
import os
import re
import subprocess
import sys
from pathlib import Path
from shutil import which
ROOT_DIR = Path(__file__).resolve().parents[2]
if str(ROOT_DIR) not in sys.path:
sys.path.insert(0, str(ROOT_DIR))
from eide_runtime import ( # noqa: E402
compact_dict,
hidden_subprocess_kwargs,
is_missing,
load_local_config,
load_project_config,
load_workspace_state,
make_result,
make_timing,
normalize_path,
now_io,
resolve_tool_param,
workspace_root,
)
def output_json(data: dict):
sys.stdout.reconfigure(encoding="utf-8")
print(json.dumps(data, ensure_ascii=False, indent=2))
def _find_size_tool(prefix: str) -> str:
"""Find the size tool from the toolchain prefix."""
tool = which(prefix + "size")
if tool:
return str(Path(tool).resolve())
alt_prefixes = ["arm-none-eabi-", "arm-eabi-", "arm-elf-"]
for p in alt_prefixes:
tool = which(p + "size")
if tool:
return str(Path(tool).resolve())
return prefix + "size"
def run_size_analyze(
elf_path: str, size_tool: str
) -> dict:
"""Run size tool and parse output."""
elf = Path(elf_path).resolve()
if not elf.is_file():
return {
"status": "error",
"action": "size_analyze",
"error": {
"code": "elf_not_found",
"message": f"ELF file not found: {elf_path}",
},
}
try:
proc = subprocess.run(
[size_tool, str(elf)],
capture_output=True,
text=True,
timeout=30,
encoding="utf-8",
errors="replace",
**hidden_subprocess_kwargs(),
)
except subprocess.TimeoutExpired:
return {
"status": "error",
"action": "size_analyze",
"error": {
"code": "timeout",
"message": "size tool timed out",
},
}
except Exception as exc:
return {
"status": "error",
"action": "size_analyze",
"error": {"code": "exec_error", "message": str(exc)},
}
output = proc.stdout or ""
if proc.returncode != 0:
return {
"status": "error",
"action": "size_analyze",
"error": {
"code": "size_failed",
"message": (
proc.stderr or "size tool failed"
),
},
}
sections = _parse_size_output(output)
flash_bytes = (
sections.get("text", 0)
+ sections.get("data", 0)
)
ram_bytes = (
sections.get("data", 0) + sections.get("bss", 0)
)
total = flash_bytes + sections.get("bss", 0)
metrics = {
"text": sections.get("text", 0),
"data": sections.get("data", 0),
"bss": sections.get("bss", 0),
"dec": sections.get("dec", total),
"hex": sections.get("hex", ""),
"flash_bytes": flash_bytes,
"ram_bytes": ram_bytes,
}
if "filename" in sections:
metrics["filename"] = sections["filename"]
return {
"status": "ok",
"action": "size_analyze",
"details": {
"elf_file": str(elf),
"size_tool": size_tool,
},
"metrics": metrics,
}
def _parse_size_output(output: str) -> dict:
"""Parse GNU size output."""
lines = [l.strip() for l in output.splitlines() if l.strip()]
if not lines:
return {}
sections: dict = {}
for line in lines:
match = re.match(
r"^\s*(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+([0-9a-fA-F]+)\s+(.+)",
line,
)
if match:
sections["text"] = int(match.group(1))
sections["data"] = int(match.group(2))
sections["bss"] = int(match.group(3))
sections["dec"] = int(match.group(4))
sections["hex"] = match.group(5)
sections["filename"] = match.group(6).strip()
return sections
# Fallback: Berkeley format
for line in lines:
parts = line.split()
if len(parts) >= 3:
try:
sections["text"] = int(parts[0])
sections["data"] = int(parts[1])
sections["bss"] = int(parts[2])
if len(parts) >= 4:
sections["dec"] = int(parts[3])
if len(parts) >= 5:
sections["hex"] = parts[4]
if len(parts) >= 6:
sections["filename"] = parts[5]
return sections
except ValueError:
continue
return {}
def run_size_compare(
elf1_path: str, elf2_path: str, size_tool: str
) -> dict:
"""Compare two ELF files and return delta."""
r1 = run_size_analyze(elf1_path, size_tool)
r2 = run_size_analyze(elf2_path, size_tool)
if r1["status"] != "ok":
return {
"status": "error",
"action": "size_compare",
"error": {
"code": "analyze_failed",
"message": (
f"Failed to analyze {elf1_path}: "
f"{r1.get('error', {}).get('message', '')}"
),
},
}
if r2["status"] != "ok":
return {
"status": "error",
"action": "size_compare",
"error": {
"code": "analyze_failed",
"message": (
f"Failed to analyze {elf2_path}: "
f"{r2.get('error', {}).get('message', '')}"
),
},
}
m1 = r1.get("metrics", {})
m2 = r2.get("metrics", {})
def _delta(a: int, b: int) -> int:
return b - a
delta_metrics = {
"text_delta": _delta(
m1.get("text", 0), m2.get("text", 0)
),
"data_delta": _delta(
m1.get("data", 0), m2.get("data", 0)
),
"bss_delta": _delta(
m1.get("bss", 0), m2.get("bss", 0)
),
"flash_bytes_delta": _delta(
m1.get("flash_bytes", 0),
m2.get("flash_bytes", 0),
),
"ram_bytes_delta": _delta(
m1.get("ram_bytes", 0),
m2.get("ram_bytes", 0),
),
}
return {
"status": "ok",
"action": "size_compare",
"details": {
"elf1": elf1_path,
"elf2": elf2_path,
},
"metrics": {
"before": {
k: m1.get(k)
for k in ("text", "data", "bss", "flash_bytes", "ram_bytes")
},
"after": {
k: m2.get(k)
for k in ("text", "data", "bss", "flash_bytes", "ram_bytes")
},
"delta": delta_metrics,
},
}
def main() -> None:
parser = argparse.ArgumentParser(
description="EIDE ELF size analysis"
)
sub = parser.add_subparsers(dest="command")
analyze_p = sub.add_parser("analyze", help="Analyze ELF file size")
analyze_p.add_argument("--elf", required=True, help="ELF file path")
analyze_p.add_argument(
"--toolchain-prefix",
default=None,
help="Toolchain prefix, default arm-none-eabi-",
)
analyze_p.add_argument("--json", action="store_true", dest="as_json")
compare_p = sub.add_parser(
"compare", help="Compare two ELF files"
)
compare_p.add_argument("--elf", required=True, help="Primary ELF")
compare_p.add_argument(
"--compare", required=True, help="Comparison ELF"
)
compare_p.add_argument(
"--toolchain-prefix",
default=None,
help="Toolchain prefix",
)
compare_p.add_argument("--json", action="store_true", dest="as_json")
args = parser.parse_args()
local_config = load_local_config(__file__)
prefix = (
args.toolchain_prefix
or local_config.get("toolchain_prefix")
or "arm-none-eabi-"
)
size_tool = _find_size_tool(prefix)
if args.command == "analyze":
result = run_size_analyze(args.elf, size_tool)
if args.as_json:
output_json(result)
else:
if result["status"] == "ok":
m = result["metrics"]
print(f"ELF: {result['details']['elf_file']}")
print(
f" text={m.get('text', 0):>8} B"
)
print(
f" data={m.get('data', 0):>8} B"
)
print(
f" bss={m.get('bss', 0):>8} B"
)
print(
f" Flash={m.get('flash_bytes', 0):>8} B"
f" (text+data)"
)
print(
f" RAM={m.get('ram_bytes', 0):>8} B"
f" (data+bss)"
)
else:
print(
f"Error: {result['error']['message']}",
file=sys.stderr,
)
sys.exit(1)
elif args.command == "compare":
result = run_size_compare(
args.elf, args.compare, size_tool
)
if args.as_json:
output_json(result)
else:
if result["status"] == "ok":
m = result["metrics"]
print("ELF Size Comparison:")
print(
" Before After Delta"
)
for key, label in (
("text", "text "),
("data", "data "),
("bss", "bss "),
("flash_bytes", "Flash"),
("ram_bytes", "RAM "),
):
b = m["before"].get(key, 0)
a = m["after"].get(key, 0)
d = m["delta"].get(f"{key}_delta", a - b)
sign = "+" if d > 0 else ""
print(
f" {label}: {b:>8} {a:>8} "
f"{sign}{d:>8}"
)
else:
print(
f"Error: {result['error']['message']}",
file=sys.stderr,
)
sys.exit(1)
else:
parser.print_help()
sys.exit(1)
if __name__ == "__main__":
main()
# Minimal EIDE project template
# Copy this to .eide/eide.yml in your project root
version: "4.1"
name: MyProject
type: ARM
deviceName: -AT32F403AVGT7
packDir: .pack/Vendor/DeviceDFP.1.0.0
srcDirs: []
virtualFolder:
name: <virtual_root>
files: []
folders:
- name: Sources
files:
- path: ../../src/main.c
folders: []