k2c 1.5.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.
- k2c-1.5.1.dist-info/METADATA +168 -0
- k2c-1.5.1.dist-info/RECORD +9 -0
- k2c-1.5.1.dist-info/WHEEL +5 -0
- k2c-1.5.1.dist-info/entry_points.txt +2 -0
- k2c-1.5.1.dist-info/top_level.txt +1 -0
- keil2compilecommands/__init__.py +1 -0
- keil2compilecommands/cli.py +90 -0
- keil2compilecommands/k2c_utils.py +512 -0
- keil2compilecommands/version.py +24 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: k2c
|
|
3
|
+
Version: 1.5.1
|
|
4
|
+
Summary: Keil .uvprojx to compile_commands.json Converter
|
|
5
|
+
Requires-Python: >=3.13
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: json5>=0.12.0
|
|
8
|
+
|
|
9
|
+
# Keil to Compile Commands Converter
|
|
10
|
+
|
|
11
|
+
这是一个 Python/uv 工具,用于解析 Keil MDK 项目文件 (.uvprojx、.uvproj) 并生成 `compile_commands.json` 文件。这个文件可以被 Clangd 等语言服务器使用,以提供更准确的代码补全、导航和诊断功能。
|
|
12
|
+
|
|
13
|
+
## 功能
|
|
14
|
+
|
|
15
|
+
* 解析 Keil `.uvprojx` XML 文件。
|
|
16
|
+
* 提取项目中包含的 C/C++ 源文件 (`.c`) 和汇编文件 (`.s`)。
|
|
17
|
+
* 提取在 Keil 项目设置中定义的宏 (`-D` 标志)。
|
|
18
|
+
* 提取在 Keil 项目设置中指定的包含路径 (`-I` 标志)。
|
|
19
|
+
* 支持 Target 选择和 Target 列表查看。
|
|
20
|
+
* 合并 Target、Group、File 级别的宏、包含路径和部分编译选项。
|
|
21
|
+
* 支持 C、C++、汇编源文件 (`.c`, `.cc`, `.cpp`, `.cxx`, `.s`, `.S`)。
|
|
22
|
+
* 从 Keil CPU/FPU 配置推导常用 clang 参数,如 `-mcpu`、`-mthumb`、`-mfpu`。
|
|
23
|
+
* 自动处理包含路径的相对路径转换
|
|
24
|
+
* 统一路径分隔符为 `/` 格式
|
|
25
|
+
* 支持从 VSCode Clangd 设置中自动获取编译器路径
|
|
26
|
+
* 支持检查缺失的源文件和包含目录。
|
|
27
|
+
* 根据提取的信息生成 `compile_commands.json` 文件。
|
|
28
|
+
|
|
29
|
+
## 依赖
|
|
30
|
+
|
|
31
|
+
* uv
|
|
32
|
+
* Python 3.13+
|
|
33
|
+
* 标准库: `xml.etree.ElementTree`, `json`, `os`, `sys` (无需额外安装)
|
|
34
|
+
* `json5` (由 uv 按 `pyproject.toml` 自动安装,用于读取带注释的VSCode设置文件)
|
|
35
|
+
|
|
36
|
+
## 使用方法
|
|
37
|
+
|
|
38
|
+
### 安装
|
|
39
|
+
|
|
40
|
+
通过 pip 安装 Python 包:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pip install k2c
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
安装后可以直接运行:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
k2c --version
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
也可以从 GitHub Release 下载 `k2c.exe` 后直接运行。
|
|
53
|
+
|
|
54
|
+
### 本地开发运行
|
|
55
|
+
|
|
56
|
+
首次使用先同步运行环境:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
uv sync
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
日常使用建议加 `--no-sync`,避免每次运行前重复检查和同步环境:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
uv run --no-sync k2c <path_to_your_keil_project.uvprojx> [options]
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**参数说明:**
|
|
69
|
+
- `--version`: 输出当前版本号
|
|
70
|
+
- `-d`: 可选参数,创建clangd缓存目录
|
|
71
|
+
- 不带参数值: 默认创建`.cache`目录
|
|
72
|
+
- 带参数值: 创建指定名称的目录
|
|
73
|
+
- `-o, --output`: 指定输出文件路径,默认 `compile_commands.json`
|
|
74
|
+
- `--target`: 指定要解析的 Keil Target 名称
|
|
75
|
+
- `--list-targets`: 列出工程中的 Target 名称后退出
|
|
76
|
+
- `--compiler`: 显式指定编译器路径,优先级高于 VSCode 设置
|
|
77
|
+
- `--dry-run`: 只解析并打印摘要,不写入文件
|
|
78
|
+
- `--verbose`: 打印解析摘要
|
|
79
|
+
- `--check-missing-files`: 检查缺失的源文件和 include 路径
|
|
80
|
+
|
|
81
|
+
**示例:**
|
|
82
|
+
|
|
83
|
+
基本用法:
|
|
84
|
+
```bash
|
|
85
|
+
uv run --no-sync k2c C:/Path/To/Your/Project/YourProject.uvprojx
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
查看 Target:
|
|
89
|
+
```bash
|
|
90
|
+
uv run --no-sync k2c ../../MyKeilProject/MyProject.uvprojx --list-targets
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
查看版本:
|
|
94
|
+
```bash
|
|
95
|
+
uv run --no-sync k2c --version
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
指定 Target 和编译器:
|
|
99
|
+
```bash
|
|
100
|
+
uv run --no-sync k2c ../../MyKeilProject/MyProject.uvprojx --target Debug --compiler C:/Keil_v5/ARM/ARMCLANG/bin/armclang.exe
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
指定输出路径:
|
|
104
|
+
```bash
|
|
105
|
+
uv run --no-sync k2c ../../MyKeilProject/MyProject.uvprojx -o build/compile_commands.json
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
检查解析结果:
|
|
109
|
+
```bash
|
|
110
|
+
uv run --no-sync k2c ../../MyKeilProject/MyProject.uvprojx --dry-run --verbose --check-missing-files
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
创建默认缓存目录:
|
|
114
|
+
```bash
|
|
115
|
+
uv run --no-sync k2c ../../MyKeilProject/MyProject.uvprojx -d
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
创建自定义缓存目录:
|
|
119
|
+
```bash
|
|
120
|
+
uv run --no-sync k2c ../../MyKeilProject/MyProject.uvprojx -d my_cache
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
当然你也可以使用打包好的exe可执行文件 [Release](https://github.com/liuyu80/keil2CompileCommands/releases/latest)
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
k2c ../../MyKeilProject/MyProject.uvprojx -d
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
工具默认将在当前工作目录下生成一个名为 `compile_commands.json` 的文件。
|
|
130
|
+
|
|
131
|
+
## 编译器路径设置
|
|
132
|
+
|
|
133
|
+
为了获取正确的编译器路径,脚本会按以下顺序查找:
|
|
134
|
+
1. 当前项目目录下的 `.vscode/settings.json`
|
|
135
|
+
2. 全局 VSCode 用户设置 (`AppData/Code/User/settings.json`)
|
|
136
|
+
|
|
137
|
+
如果找不到编译器路径,脚本会提醒您添加以下配置到 VSCode 设置中:
|
|
138
|
+
```json
|
|
139
|
+
"clangd.arguments": ["--query-driver=<absolute_path_to_compiler>"]
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## `compile_commands.json`
|
|
143
|
+
|
|
144
|
+
生成的 `compile_commands.json` 文件包含一个 JSON 数组,其中每个对象代表项目中的一个源文件及其编译参数。结构如下:
|
|
145
|
+
|
|
146
|
+
```json
|
|
147
|
+
[
|
|
148
|
+
{
|
|
149
|
+
"directory": "/path/to/source/file/directory",
|
|
150
|
+
"arguments": [
|
|
151
|
+
"<absolute_path_to_compiler>",
|
|
152
|
+
"-Iinclude/path1",
|
|
153
|
+
"-Iinclude/path2",
|
|
154
|
+
"-DMACRO1",
|
|
155
|
+
"-DMACRO2"
|
|
156
|
+
],
|
|
157
|
+
"file": "/path/to/source/file/filename.c"
|
|
158
|
+
},
|
|
159
|
+
...
|
|
160
|
+
]
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
这个文件可以被许多开发工具(如 VS Code 配合 Clangd 插件)使用,以增强代码编辑体验。
|
|
164
|
+
|
|
165
|
+
Clangd LSP 安装包: https://github.com/clangd/clangd/releases/latest
|
|
166
|
+
|
|
167
|
+
VS Code 的 Clangd 插件: https://github.com/clangd/vscode-clangd/releases/latest
|
|
168
|
+
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
keil2compilecommands/__init__.py,sha256=BfQ2MCyweriP8ve0GQfXLgIdl7HJ4f_tsi0oNaKA4lc,55
|
|
2
|
+
keil2compilecommands/cli.py,sha256=0TfGUncyEsK8s1_MtgeuE6wms5mpb8x1Hf82IBsWSak,3450
|
|
3
|
+
keil2compilecommands/k2c_utils.py,sha256=i8Hl4gBDEXotyK73NhDlXe0r5ILn32K3eJhm991KHQM,17206
|
|
4
|
+
keil2compilecommands/version.py,sha256=TFIXKBGtjU6i9bBeiQzQdTCmzBpGkKPXrezObldiZrQ,531
|
|
5
|
+
k2c-1.5.1.dist-info/METADATA,sha256=g9SufFtCeBRSYqDtxFQ28ntcDC5Abz1Y7QSIfRYpLyc,4958
|
|
6
|
+
k2c-1.5.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
7
|
+
k2c-1.5.1.dist-info/entry_points.txt,sha256=mx9n8xKKecNasNyMB19o61LJ8h2MJ5J160TH_7xszGk,54
|
|
8
|
+
k2c-1.5.1.dist-info/top_level.txt,sha256=UZtSAEj8jqduhZ8cuyNJqom-EFbleLKa1OpbrOsmnTg,21
|
|
9
|
+
k2c-1.5.1.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
keil2compilecommands
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Keil project to compile_commands.json converter."""
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from .k2c_utils import (
|
|
8
|
+
build_compile_commands,
|
|
9
|
+
check_missing_paths,
|
|
10
|
+
create_clangd_directory,
|
|
11
|
+
get_clangd_query_driver,
|
|
12
|
+
list_target_names,
|
|
13
|
+
write_compile_commands,
|
|
14
|
+
)
|
|
15
|
+
from .version import get_version
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
19
|
+
parser = argparse.ArgumentParser(
|
|
20
|
+
description="Parse Keil project file and generate compile_commands.json"
|
|
21
|
+
)
|
|
22
|
+
parser.add_argument("--version", action="version", version=f"k2c {get_version()}")
|
|
23
|
+
parser.add_argument("project_file", type=str, help="Path to Keil project file (.uvprojx .uvproj)")
|
|
24
|
+
parser.add_argument("-d", nargs="?", const=".cache", metavar="CACHE_DIR",
|
|
25
|
+
help="Create clangd cache directory (default: .cache)")
|
|
26
|
+
parser.add_argument("-o", "--output", default="compile_commands.json",
|
|
27
|
+
help="Output compile_commands.json path")
|
|
28
|
+
parser.add_argument("--target", help="Keil target name to parse")
|
|
29
|
+
parser.add_argument("--list-targets", action="store_true",
|
|
30
|
+
help="List target names and exit")
|
|
31
|
+
parser.add_argument("--compiler", help="Compiler path used as argv[0] in compile commands")
|
|
32
|
+
parser.add_argument("--dry-run", action="store_true",
|
|
33
|
+
help="Parse and print a summary without writing output")
|
|
34
|
+
parser.add_argument("--verbose", action="store_true",
|
|
35
|
+
help="Print parsing summary")
|
|
36
|
+
parser.add_argument("--check-missing-files", action="store_true",
|
|
37
|
+
help="Warn about missing source and include paths")
|
|
38
|
+
return parser
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def main(argv: list[str] | None = None) -> int:
|
|
42
|
+
parser = build_parser()
|
|
43
|
+
args = parser.parse_args(argv)
|
|
44
|
+
|
|
45
|
+
project_path = Path(args.project_file)
|
|
46
|
+
if not project_path.exists():
|
|
47
|
+
parser.error(f"The specified Keil project file does not exist: {args.project_file}")
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
if args.list_targets:
|
|
51
|
+
for target_name in list_target_names(project_path):
|
|
52
|
+
print(target_name)
|
|
53
|
+
return 0
|
|
54
|
+
|
|
55
|
+
compiler = args.compiler or get_clangd_query_driver(project_path.resolve().parent)
|
|
56
|
+
compile_info = build_compile_commands(
|
|
57
|
+
project_path,
|
|
58
|
+
compiler=compiler,
|
|
59
|
+
target_name=args.target,
|
|
60
|
+
output_dir=Path(args.output).resolve().parent,
|
|
61
|
+
verbose=args.verbose,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
if args.check_missing_files:
|
|
65
|
+
report = check_missing_paths(project_path, target_name=args.target)
|
|
66
|
+
if report.missing_sources:
|
|
67
|
+
print(f"Missing source files: {len(report.missing_sources)}")
|
|
68
|
+
for missing in report.missing_sources:
|
|
69
|
+
print(f" {missing}")
|
|
70
|
+
if report.missing_includes:
|
|
71
|
+
print(f"Missing include paths: {len(report.missing_includes)}")
|
|
72
|
+
for missing in report.missing_includes:
|
|
73
|
+
print(f" {missing}")
|
|
74
|
+
|
|
75
|
+
if args.dry_run:
|
|
76
|
+
print(f"Would write {len(compile_info)} compile command entries to {args.output}")
|
|
77
|
+
else:
|
|
78
|
+
write_compile_commands(compile_info, args.output)
|
|
79
|
+
|
|
80
|
+
if args.d is not None:
|
|
81
|
+
create_clangd_directory(args.d)
|
|
82
|
+
except Exception as error:
|
|
83
|
+
print(f"Error: {error}", file=sys.stderr)
|
|
84
|
+
return 1
|
|
85
|
+
|
|
86
|
+
return 0
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
if __name__ == "__main__":
|
|
90
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Iterable, Optional
|
|
8
|
+
import xml.etree.ElementTree as ET
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
SOURCE_SUFFIXES = {".c", ".cc", ".cpp", ".cxx", ".s"}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class KeilControls:
|
|
16
|
+
defines: list[str] = field(default_factory=list)
|
|
17
|
+
include_paths: list[str] = field(default_factory=list)
|
|
18
|
+
misc_controls: list[str] = field(default_factory=list)
|
|
19
|
+
optimization: Optional[str] = None
|
|
20
|
+
language: Optional[str] = None
|
|
21
|
+
language_cpp: Optional[str] = None
|
|
22
|
+
warning_level: Optional[str] = None
|
|
23
|
+
warnings_as_errors: Optional[str] = None
|
|
24
|
+
rtti: Optional[str] = None
|
|
25
|
+
short_enums: Optional[str] = None
|
|
26
|
+
short_wchar: Optional[str] = None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True)
|
|
30
|
+
class KeilSource:
|
|
31
|
+
path: str
|
|
32
|
+
controls: KeilControls = field(default_factory=KeilControls)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class KeilTarget:
|
|
37
|
+
name: str
|
|
38
|
+
element: ET.Element
|
|
39
|
+
controls: KeilControls
|
|
40
|
+
cpu_text: str
|
|
41
|
+
sources: list[KeilSource]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(frozen=True)
|
|
45
|
+
class MissingPathReport:
|
|
46
|
+
missing_sources: list[str]
|
|
47
|
+
missing_includes: list[str]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def normalize_path(path: Path | str) -> str:
|
|
51
|
+
return str(path).replace("\\", "/")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def relative_or_absolute(path: Path, start: Path) -> str:
|
|
55
|
+
try:
|
|
56
|
+
return normalize_path(os.path.relpath(path, start))
|
|
57
|
+
except ValueError:
|
|
58
|
+
return normalize_path(path)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def split_list(value: Optional[str], separator: str) -> list[str]:
|
|
62
|
+
if not value:
|
|
63
|
+
return []
|
|
64
|
+
return [item.strip() for item in value.split(separator) if item.strip()]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def split_shell_words(value: Optional[str]) -> list[str]:
|
|
68
|
+
if not value:
|
|
69
|
+
return []
|
|
70
|
+
import shlex
|
|
71
|
+
|
|
72
|
+
return shlex.split(value, posix=False)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def unique_preserving_order(values: Iterable[str]) -> list[str]:
|
|
76
|
+
seen: set[str] = set()
|
|
77
|
+
result: list[str] = []
|
|
78
|
+
for value in values:
|
|
79
|
+
if value not in seen:
|
|
80
|
+
seen.add(value)
|
|
81
|
+
result.append(value)
|
|
82
|
+
return result
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def parse_controls(container: Optional[ET.Element]) -> KeilControls:
|
|
86
|
+
if container is None:
|
|
87
|
+
return KeilControls()
|
|
88
|
+
|
|
89
|
+
various = container.find(".//VariousControls")
|
|
90
|
+
define_text = text_or_empty(various.find("Define") if various is not None else None)
|
|
91
|
+
include_text = text_or_empty(various.find("IncludePath") if various is not None else None)
|
|
92
|
+
misc_text = text_or_empty(various.find("MiscControls") if various is not None else None)
|
|
93
|
+
optimization = text_or_empty(container.find("Optim")) or None
|
|
94
|
+
|
|
95
|
+
return KeilControls(
|
|
96
|
+
defines=split_list(define_text, ","),
|
|
97
|
+
include_paths=split_list(include_text, ";"),
|
|
98
|
+
misc_controls=split_shell_words(misc_text),
|
|
99
|
+
optimization=optimization,
|
|
100
|
+
language=_language_standard(container),
|
|
101
|
+
language_cpp=_cpp_language_standard(container),
|
|
102
|
+
warning_level=_field_text(container, "wLevel"),
|
|
103
|
+
warnings_as_errors=_field_text(container, "v6WtE"),
|
|
104
|
+
rtti=_field_text(container, "v6Rtti"),
|
|
105
|
+
short_enums=_field_text(container, "vShortEn"),
|
|
106
|
+
short_wchar=_field_text(container, "vShortWch"),
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def merge_controls(*controls: KeilControls) -> KeilControls:
|
|
111
|
+
return KeilControls(
|
|
112
|
+
defines=unique_preserving_order(
|
|
113
|
+
item for control in controls for item in control.defines
|
|
114
|
+
),
|
|
115
|
+
include_paths=unique_preserving_order(
|
|
116
|
+
item for control in controls for item in control.include_paths
|
|
117
|
+
),
|
|
118
|
+
misc_controls=[
|
|
119
|
+
item for control in controls for item in control.misc_controls
|
|
120
|
+
],
|
|
121
|
+
optimization=next(
|
|
122
|
+
(control.optimization for control in reversed(controls) if control.optimization),
|
|
123
|
+
None,
|
|
124
|
+
),
|
|
125
|
+
language=next(
|
|
126
|
+
(control.language for control in reversed(controls) if control.language),
|
|
127
|
+
None,
|
|
128
|
+
),
|
|
129
|
+
language_cpp=next(
|
|
130
|
+
(control.language_cpp for control in reversed(controls) if control.language_cpp),
|
|
131
|
+
None,
|
|
132
|
+
),
|
|
133
|
+
warning_level=next(
|
|
134
|
+
(control.warning_level for control in reversed(controls) if control.warning_level),
|
|
135
|
+
None,
|
|
136
|
+
),
|
|
137
|
+
warnings_as_errors=next(
|
|
138
|
+
(control.warnings_as_errors for control in reversed(controls) if control.warnings_as_errors),
|
|
139
|
+
None,
|
|
140
|
+
),
|
|
141
|
+
rtti=next(
|
|
142
|
+
(control.rtti for control in reversed(controls) if control.rtti),
|
|
143
|
+
None,
|
|
144
|
+
),
|
|
145
|
+
short_enums=next(
|
|
146
|
+
(control.short_enums for control in reversed(controls) if control.short_enums),
|
|
147
|
+
None,
|
|
148
|
+
),
|
|
149
|
+
short_wchar=next(
|
|
150
|
+
(control.short_wchar for control in reversed(controls) if control.short_wchar),
|
|
151
|
+
None,
|
|
152
|
+
),
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def text_or_empty(element: Optional[ET.Element]) -> str:
|
|
157
|
+
if element is None or element.text is None:
|
|
158
|
+
return ""
|
|
159
|
+
return element.text.strip()
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _field_text(container: ET.Element, name: str) -> Optional[str]:
|
|
163
|
+
value = text_or_empty(container.find(name))
|
|
164
|
+
if not value or value == "2":
|
|
165
|
+
return None
|
|
166
|
+
return value
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _language_standard(container: ET.Element) -> Optional[str]:
|
|
170
|
+
v6_lang = _field_text(container, "v6Lang")
|
|
171
|
+
if v6_lang:
|
|
172
|
+
mapping = {
|
|
173
|
+
"0": "c90",
|
|
174
|
+
"1": "c99",
|
|
175
|
+
"2": "c11",
|
|
176
|
+
"3": "gnu90",
|
|
177
|
+
"4": "gnu99",
|
|
178
|
+
"5": "gnu11",
|
|
179
|
+
}
|
|
180
|
+
return mapping.get(v6_lang)
|
|
181
|
+
|
|
182
|
+
if _field_text(container, "uGnu") == "1":
|
|
183
|
+
return "gnu99" if _field_text(container, "uC99") == "1" else "gnu90"
|
|
184
|
+
if _field_text(container, "uC99") == "1":
|
|
185
|
+
return "c99"
|
|
186
|
+
return None
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _cpp_language_standard(container: ET.Element) -> Optional[str]:
|
|
190
|
+
v6_lang_cpp = _field_text(container, "v6LangP")
|
|
191
|
+
if not v6_lang_cpp:
|
|
192
|
+
return None
|
|
193
|
+
mapping = {
|
|
194
|
+
"0": "c++03",
|
|
195
|
+
"1": "gnu++03",
|
|
196
|
+
"2": "c++11",
|
|
197
|
+
"3": "gnu++11",
|
|
198
|
+
"4": "c++14",
|
|
199
|
+
"5": "gnu++14",
|
|
200
|
+
"6": "c++17",
|
|
201
|
+
"7": "gnu++17",
|
|
202
|
+
}
|
|
203
|
+
return mapping.get(v6_lang_cpp)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def find_targets(project_file: str | Path) -> list[KeilTarget]:
|
|
207
|
+
tree = ET.parse(project_file)
|
|
208
|
+
root = tree.getroot()
|
|
209
|
+
targets = root.findall(".//Target")
|
|
210
|
+
return [_parse_target(target) for target in targets]
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def list_target_names(project_file: str | Path) -> list[str]:
|
|
214
|
+
return [target.name for target in find_targets(project_file)]
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _parse_target(target: ET.Element) -> KeilTarget:
|
|
218
|
+
name = text_or_empty(target.find("TargetName")) or "<unnamed>"
|
|
219
|
+
target_option = target.find(".//TargetOption")
|
|
220
|
+
controls = parse_controls(target_option.find(".//Cads") if target_option is not None else None)
|
|
221
|
+
cpu_text = text_or_empty(target_option.find(".//Cpu") if target_option is not None else None)
|
|
222
|
+
sources = _parse_sources(target, controls)
|
|
223
|
+
return KeilTarget(name=name, element=target, controls=controls, cpu_text=cpu_text, sources=sources)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _parse_sources(target: ET.Element, target_controls: KeilControls) -> list[KeilSource]:
|
|
227
|
+
sources: list[KeilSource] = []
|
|
228
|
+
for group in target.findall(".//Group"):
|
|
229
|
+
group_controls = parse_controls(group.find(".//GroupOption/Cads"))
|
|
230
|
+
for file_element in group.findall("./Files/File"):
|
|
231
|
+
file_type = _parse_int(text_or_empty(file_element.find("FileType")))
|
|
232
|
+
file_path = text_or_empty(file_element.find("FilePath"))
|
|
233
|
+
if file_type not in {1, 2} or not file_path:
|
|
234
|
+
continue
|
|
235
|
+
if not _is_supported_source(file_path):
|
|
236
|
+
continue
|
|
237
|
+
|
|
238
|
+
file_controls = parse_controls(file_element.find(".//FileOption/Cads"))
|
|
239
|
+
controls = merge_controls(target_controls, group_controls, file_controls)
|
|
240
|
+
sources.append(KeilSource(path=file_path, controls=controls))
|
|
241
|
+
return sources
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _parse_int(value: str) -> Optional[int]:
|
|
245
|
+
if not value:
|
|
246
|
+
return None
|
|
247
|
+
try:
|
|
248
|
+
return int(value)
|
|
249
|
+
except ValueError:
|
|
250
|
+
return None
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _is_supported_source(path: str) -> bool:
|
|
254
|
+
suffix = Path(path).suffix
|
|
255
|
+
return suffix.lower() in SOURCE_SUFFIXES or suffix == ".S"
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def select_target(targets: list[KeilTarget], target_name: Optional[str]) -> KeilTarget:
|
|
259
|
+
if not targets:
|
|
260
|
+
raise ValueError("Target element not found in Keil project file.")
|
|
261
|
+
if target_name is None:
|
|
262
|
+
return targets[0]
|
|
263
|
+
|
|
264
|
+
matches = [target for target in targets if target.name == target_name]
|
|
265
|
+
if not matches:
|
|
266
|
+
available = ", ".join(target.name for target in targets)
|
|
267
|
+
raise ValueError(f"Target '{target_name}' not found. Available targets: {available}")
|
|
268
|
+
return matches[0]
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def keil_cpu_arguments(cpu_text: str) -> list[str]:
|
|
272
|
+
args: list[str] = []
|
|
273
|
+
lowered = cpu_text.lower()
|
|
274
|
+
|
|
275
|
+
cpu = _extract_between(cpu_text, 'CPUTYPE("', '")')
|
|
276
|
+
if cpu:
|
|
277
|
+
cpu_map = {
|
|
278
|
+
"cortex-m0": "cortex-m0",
|
|
279
|
+
"cortex-m0+": "cortex-m0plus",
|
|
280
|
+
"cortex-m3": "cortex-m3",
|
|
281
|
+
"cortex-m4": "cortex-m4",
|
|
282
|
+
"cortex-m7": "cortex-m7",
|
|
283
|
+
"cortex-m23": "cortex-m23",
|
|
284
|
+
"cortex-m33": "cortex-m33",
|
|
285
|
+
"cortex-m55": "cortex-m55",
|
|
286
|
+
}
|
|
287
|
+
mapped = cpu_map.get(cpu.lower(), cpu.lower())
|
|
288
|
+
args.extend(["-mcpu=" + mapped, "-mthumb"])
|
|
289
|
+
elif "cortex-m" in lowered:
|
|
290
|
+
args.append("-mthumb")
|
|
291
|
+
|
|
292
|
+
if "fpu" in lowered or "dfpu" in lowered or "spfu" in lowered:
|
|
293
|
+
if "cortex-m7" in lowered:
|
|
294
|
+
args.extend(["-mfpu=fpv5-d16", "-mfloat-abi=hard"])
|
|
295
|
+
elif "cortex-m4" in lowered:
|
|
296
|
+
args.extend(["-mfpu=fpv4-sp-d16", "-mfloat-abi=hard"])
|
|
297
|
+
|
|
298
|
+
if "elittle" in lowered:
|
|
299
|
+
args.append("-mlittle-endian")
|
|
300
|
+
elif "ebig" in lowered:
|
|
301
|
+
args.append("-mbig-endian")
|
|
302
|
+
|
|
303
|
+
return unique_preserving_order(args)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def optimization_argument(optimization: Optional[str]) -> list[str]:
|
|
307
|
+
if optimization is None:
|
|
308
|
+
return []
|
|
309
|
+
mapping = {
|
|
310
|
+
"0": "-O0",
|
|
311
|
+
"1": "-O1",
|
|
312
|
+
"2": "-O2",
|
|
313
|
+
"3": "-O3",
|
|
314
|
+
"4": "-Os",
|
|
315
|
+
}
|
|
316
|
+
return [mapping.get(optimization, f"-O{optimization}")]
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def control_arguments(controls: KeilControls, source_path: str) -> list[str]:
|
|
320
|
+
args: list[str] = []
|
|
321
|
+
standard = controls.language_cpp if _is_cpp_source(source_path) else controls.language
|
|
322
|
+
if standard:
|
|
323
|
+
args.append(f"-std={standard}")
|
|
324
|
+
|
|
325
|
+
args.extend(optimization_argument(controls.optimization))
|
|
326
|
+
args.extend(warning_arguments(controls.warning_level))
|
|
327
|
+
|
|
328
|
+
if controls.warnings_as_errors == "1":
|
|
329
|
+
args.append("-Werror")
|
|
330
|
+
if controls.short_enums == "1":
|
|
331
|
+
args.append("-fshort-enums")
|
|
332
|
+
if controls.short_wchar == "1":
|
|
333
|
+
args.append("-fshort-wchar")
|
|
334
|
+
if controls.rtti == "0" and _is_cpp_source(source_path):
|
|
335
|
+
args.append("-fno-rtti")
|
|
336
|
+
elif controls.rtti == "1" and _is_cpp_source(source_path):
|
|
337
|
+
args.append("-frtti")
|
|
338
|
+
|
|
339
|
+
args.extend(controls.misc_controls)
|
|
340
|
+
return args
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def warning_arguments(warning_level: Optional[str]) -> list[str]:
|
|
344
|
+
if warning_level is None:
|
|
345
|
+
return []
|
|
346
|
+
mapping = {
|
|
347
|
+
"0": ["-w"],
|
|
348
|
+
"1": ["-Wall"],
|
|
349
|
+
"2": ["-Wall"],
|
|
350
|
+
"3": ["-Wall", "-Wextra"],
|
|
351
|
+
"4": ["-Wall", "-Wextra"],
|
|
352
|
+
}
|
|
353
|
+
return mapping.get(warning_level, [])
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def _is_cpp_source(path: str) -> bool:
|
|
357
|
+
return Path(path).suffix.lower() in {".cc", ".cpp", ".cxx"}
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _extract_between(value: str, prefix: str, suffix: str) -> Optional[str]:
|
|
361
|
+
start = value.find(prefix)
|
|
362
|
+
if start < 0:
|
|
363
|
+
return None
|
|
364
|
+
start += len(prefix)
|
|
365
|
+
end = value.find(suffix, start)
|
|
366
|
+
if end < 0:
|
|
367
|
+
return None
|
|
368
|
+
return value[start:end]
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def build_compile_commands(
|
|
372
|
+
project_file: str | Path,
|
|
373
|
+
*,
|
|
374
|
+
compiler: str,
|
|
375
|
+
target_name: Optional[str] = None,
|
|
376
|
+
output_dir: str | Path | None = None,
|
|
377
|
+
verbose: bool = False,
|
|
378
|
+
) -> list[dict[str, Any]]:
|
|
379
|
+
project_path = Path(os.path.abspath(project_file))
|
|
380
|
+
project_dir = project_path.parent
|
|
381
|
+
output_path = Path(os.path.abspath(Path.cwd() if output_dir is None else output_dir))
|
|
382
|
+
|
|
383
|
+
targets = find_targets(project_path)
|
|
384
|
+
target = select_target(targets, target_name)
|
|
385
|
+
if verbose and target_name is None and len(targets) > 1:
|
|
386
|
+
print(f"Using first target '{target.name}'. Use --target to select another target.")
|
|
387
|
+
|
|
388
|
+
base_args = keil_cpu_arguments(target.cpu_text)
|
|
389
|
+
compile_commands: list[dict[str, Any]] = []
|
|
390
|
+
include_cache: dict[str, str] = {}
|
|
391
|
+
|
|
392
|
+
def include_argument(include: str) -> str:
|
|
393
|
+
cached = include_cache.get(include)
|
|
394
|
+
if cached is not None:
|
|
395
|
+
return cached
|
|
396
|
+
abs_include = Path(os.path.abspath(project_dir / include))
|
|
397
|
+
cached = "-I" + relative_or_absolute(abs_include, output_path)
|
|
398
|
+
include_cache[include] = cached
|
|
399
|
+
return cached
|
|
400
|
+
|
|
401
|
+
for source in target.sources:
|
|
402
|
+
abs_source = Path(os.path.abspath(project_dir / source.path))
|
|
403
|
+
rel_source = relative_or_absolute(abs_source, output_path)
|
|
404
|
+
includes = [include_argument(include) for include in source.controls.include_paths]
|
|
405
|
+
macros = [f"-D{define}" for define in source.controls.defines]
|
|
406
|
+
arguments = (
|
|
407
|
+
[compiler]
|
|
408
|
+
+ base_args
|
|
409
|
+
+ includes
|
|
410
|
+
+ macros
|
|
411
|
+
+ control_arguments(source.controls, source.path)
|
|
412
|
+
+ [rel_source]
|
|
413
|
+
)
|
|
414
|
+
compile_commands.append(
|
|
415
|
+
{
|
|
416
|
+
"arguments": arguments,
|
|
417
|
+
"directory": str(output_path),
|
|
418
|
+
"file": rel_source,
|
|
419
|
+
}
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
if verbose:
|
|
423
|
+
print(
|
|
424
|
+
f"Parsed target '{target.name}': "
|
|
425
|
+
f"{len(target.controls.include_paths)} target includes, "
|
|
426
|
+
f"{len(target.controls.defines)} target macros, "
|
|
427
|
+
f"{len(target.sources)} source files."
|
|
428
|
+
)
|
|
429
|
+
return compile_commands
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def write_compile_commands(
|
|
433
|
+
compile_commands: list[dict[str, Any]],
|
|
434
|
+
output_file: str | Path = "compile_commands.json",
|
|
435
|
+
) -> None:
|
|
436
|
+
output_path = Path(output_file)
|
|
437
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
438
|
+
with open(output_path, "w", encoding="utf-8") as file:
|
|
439
|
+
json.dump(compile_commands, file, indent=4, ensure_ascii=False)
|
|
440
|
+
print(f"Successfully wrote compile commands to {output_path}")
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def create_clangd_directory(cache_dir: Optional[str] = None) -> None:
|
|
444
|
+
cache_path = Path(".cache" if cache_dir is None else cache_dir)
|
|
445
|
+
cache_path.mkdir(parents=True, exist_ok=True)
|
|
446
|
+
(cache_path / ".gitignore").write_text("*", encoding="utf-8")
|
|
447
|
+
print(f"Successfully created {cache_path} directory and .gitignore file")
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def check_missing_paths(
|
|
451
|
+
project_file: str | Path,
|
|
452
|
+
*,
|
|
453
|
+
target_name: Optional[str] = None,
|
|
454
|
+
) -> MissingPathReport:
|
|
455
|
+
project_path = Path(os.path.abspath(project_file))
|
|
456
|
+
project_dir = project_path.parent
|
|
457
|
+
target = select_target(find_targets(project_path), target_name)
|
|
458
|
+
missing_sources: list[str] = []
|
|
459
|
+
missing_includes: list[str] = []
|
|
460
|
+
|
|
461
|
+
for source in target.sources:
|
|
462
|
+
source_path = Path(os.path.abspath(project_dir / source.path))
|
|
463
|
+
if not source_path.exists():
|
|
464
|
+
missing_sources.append(normalize_path(source.path))
|
|
465
|
+
for include in source.controls.include_paths:
|
|
466
|
+
include_path = Path(os.path.abspath(project_dir / include))
|
|
467
|
+
normalized = normalize_path(include)
|
|
468
|
+
if not include_path.exists() and normalized not in missing_includes:
|
|
469
|
+
missing_includes.append(normalized)
|
|
470
|
+
|
|
471
|
+
return MissingPathReport(missing_sources=missing_sources, missing_includes=missing_includes)
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def get_clangd_query_driver(project_dir: str | Path | None = None) -> str:
|
|
475
|
+
def read_json_file(file_path: Path) -> dict[str, Any]:
|
|
476
|
+
try:
|
|
477
|
+
import json5
|
|
478
|
+
except ModuleNotFoundError:
|
|
479
|
+
with open(file_path, "r", encoding="utf-8") as file:
|
|
480
|
+
return json.load(file)
|
|
481
|
+
with open(file_path, "r", encoding="utf-8") as file:
|
|
482
|
+
return json5.load(file)
|
|
483
|
+
|
|
484
|
+
def find_compiler_in_settings(settings_path: Path) -> Optional[str]:
|
|
485
|
+
if not settings_path.exists():
|
|
486
|
+
return None
|
|
487
|
+
data = read_json_file(settings_path)
|
|
488
|
+
arguments = data.get("clangd.arguments", [])
|
|
489
|
+
if not isinstance(arguments, list):
|
|
490
|
+
return None
|
|
491
|
+
for arg in arguments:
|
|
492
|
+
if isinstance(arg, str) and arg.startswith("--query-driver="):
|
|
493
|
+
compilers = split_list(arg.split("=", 1)[1], ",")
|
|
494
|
+
if compilers:
|
|
495
|
+
print(f"Found compiler: {compilers}")
|
|
496
|
+
return compilers[0]
|
|
497
|
+
return None
|
|
498
|
+
|
|
499
|
+
search_dir = Path.cwd() if project_dir is None else Path(project_dir)
|
|
500
|
+
compiler = find_compiler_in_settings(search_dir / ".vscode" / "settings.json")
|
|
501
|
+
if compiler:
|
|
502
|
+
return compiler
|
|
503
|
+
|
|
504
|
+
app_data = os.getenv("AppData")
|
|
505
|
+
if app_data:
|
|
506
|
+
compiler = find_compiler_in_settings(Path(app_data) / "Code" / "User" / "settings.json")
|
|
507
|
+
if compiler:
|
|
508
|
+
return compiler
|
|
509
|
+
|
|
510
|
+
print("Please add the following to your VSCode settings.json (use absolute path):")
|
|
511
|
+
print('"clangd.arguments": ["--query-driver=<absolute_path_to_compiler>"]')
|
|
512
|
+
return "<compilerPath>"
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from importlib import metadata
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
PACKAGE_NAME = "k2c"
|
|
8
|
+
DEFAULT_VERSION = "0.1.0"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def normalize_version(version: str) -> str:
|
|
12
|
+
version = version.strip()
|
|
13
|
+
return version[1:] if version.startswith("v") else version
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_version() -> str:
|
|
17
|
+
env_version = os.getenv("K2C_VERSION")
|
|
18
|
+
if env_version:
|
|
19
|
+
return normalize_version(env_version)
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
return metadata.version(PACKAGE_NAME)
|
|
23
|
+
except metadata.PackageNotFoundError:
|
|
24
|
+
return DEFAULT_VERSION
|