uv2compdb 0.3.0__tar.gz
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.
- uv2compdb-0.3.0/LICENSE +21 -0
- uv2compdb-0.3.0/PKG-INFO +118 -0
- uv2compdb-0.3.0/README.md +96 -0
- uv2compdb-0.3.0/pyproject.toml +39 -0
- uv2compdb-0.3.0/src/uv2compdb/__init__.py +5 -0
- uv2compdb-0.3.0/src/uv2compdb/__main__.py +4 -0
- uv2compdb-0.3.0/src/uv2compdb/main.py +84 -0
- uv2compdb-0.3.0/src/uv2compdb/parser.py +531 -0
uv2compdb-0.3.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 xbin-xu
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
uv2compdb-0.3.0/PKG-INFO
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: uv2compdb
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Generate Compilation Database by parse Keil µVision project
|
|
5
|
+
Keywords: keil,MDK,µVision,clangd,Compilation Database,compiled_commands.json
|
|
6
|
+
Author: xbin
|
|
7
|
+
Author-email: xbin <xbin.xu@qq.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Topic :: Software Development :: Embedded Systems
|
|
17
|
+
Classifier: Topic :: Utilities
|
|
18
|
+
Requires-Python: >=3.8
|
|
19
|
+
Project-URL: Homepage, https://github.com/xbin-xu/uv2compdb
|
|
20
|
+
Project-URL: Issues, https://github.com/xbin-xu/uv2compdb/issues
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# uv2compdb
|
|
24
|
+
|
|
25
|
+
Generate [Compilation Database] by parse Keil µVision project.
|
|
26
|
+
|
|
27
|
+
## Features
|
|
28
|
+
|
|
29
|
+
+ Parse strategy (dep/build_log -> XML)
|
|
30
|
+
+ Extract toolchain predefined macros with `-p` option
|
|
31
|
+
+ VariousControls hierarchical merge (Target -> Group -> File)
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
pip install uv2compdb
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Usage
|
|
40
|
+
|
|
41
|
+
### Basic Usage
|
|
42
|
+
|
|
43
|
+
Generate `compile_commands.json` in the current directory for the first target if the project has multiple targets.
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
uv2compdb /path/to/project
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Specify target and output
|
|
50
|
+
|
|
51
|
+
Generate `compile_commands.json` for a specific target and output
|
|
52
|
+
|
|
53
|
+
```sh
|
|
54
|
+
uv2compdb /path/to/project -t target -o /path/to/compile_commands.json
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Help
|
|
58
|
+
|
|
59
|
+
```sh
|
|
60
|
+
usage: uv2compdb [-h] [-a ARGUMENTS] [-b] [-t TARGET] [-o OUTPUT] [-p] project
|
|
61
|
+
|
|
62
|
+
Generate compile_commands.json by parse Keil µVision project
|
|
63
|
+
|
|
64
|
+
positional arguments:
|
|
65
|
+
project path to .uvproj[x] file
|
|
66
|
+
|
|
67
|
+
options:
|
|
68
|
+
-h, --help show this help message and exit
|
|
69
|
+
-a, --arguments ARGUMENTS
|
|
70
|
+
add extra arguments
|
|
71
|
+
-b, --build try to build while dep/build_log files don't not exist
|
|
72
|
+
-t, --target TARGET target name
|
|
73
|
+
-o, --output OUTPUT output dir/file path (default: compile_commands.json)
|
|
74
|
+
-p, --predefined try to add predefined macros
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Limit
|
|
78
|
+
|
|
79
|
+
+ [ ] Not support C51
|
|
80
|
+
+ [x] Not parsed `"Options" -> "C/C++" -> "Language / Code Generation"`
|
|
81
|
+
+ [x] Not parsed `"Options" -> "ASM"`, so Asm file use same options with C file
|
|
82
|
+
+ [x] Can't parse **RTE** components
|
|
83
|
+
+ [x] Can't add toolchain predefined macros and include path
|
|
84
|
+
+ [ ] The support for ARMCC (AC5) not well
|
|
85
|
+
+ need config `.clangd` manually
|
|
86
|
+
|
|
87
|
+
## [Clangd]
|
|
88
|
+
|
|
89
|
+
[.clangd config]
|
|
90
|
+
|
|
91
|
+
```yaml
|
|
92
|
+
CompileFlags:
|
|
93
|
+
CompilationDatabase: /path/to/compile-commands-dir
|
|
94
|
+
Compiler: arm-none-eabi-gcc # use arm-neon-eabi-gcc instead of armcc
|
|
95
|
+
Add:
|
|
96
|
+
- -fdeclspec # fix '__declspec' if use arm-none-eabi-gcc instead of armcc
|
|
97
|
+
|
|
98
|
+
Diagnostics:
|
|
99
|
+
UnusedIncludes: None # Strict(default), None
|
|
100
|
+
# Suppress:
|
|
101
|
+
# - no_member
|
|
102
|
+
# - no_member_suggest
|
|
103
|
+
# - no_template
|
|
104
|
+
# - undeclared_var_use
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## References
|
|
108
|
+
|
|
109
|
+
+ [keil2clangd]
|
|
110
|
+
+ [uvConvertor]
|
|
111
|
+
+ [a3750/uvconvertor]
|
|
112
|
+
|
|
113
|
+
[Compilation Database]: <https://clang.llvm.org/docs/JSONCompilationDatabase.html>
|
|
114
|
+
[Clangd]: <https://clangd.llvm.org/>
|
|
115
|
+
[.clangd config]: <https://clangd.llvm.org/config>
|
|
116
|
+
[keil2clangd]: <https://github.com/huiyi-li/keil2clangd>
|
|
117
|
+
[uvConvertor]: <https://github.com/vankubo/uvConvertor>
|
|
118
|
+
[a3750/uvconvertor]: <https://github.com/a3750/uvconvertor>
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# uv2compdb
|
|
2
|
+
|
|
3
|
+
Generate [Compilation Database] by parse Keil µVision project.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
+ Parse strategy (dep/build_log -> XML)
|
|
8
|
+
+ Extract toolchain predefined macros with `-p` option
|
|
9
|
+
+ VariousControls hierarchical merge (Target -> Group -> File)
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
pip install uv2compdb
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
### Basic Usage
|
|
20
|
+
|
|
21
|
+
Generate `compile_commands.json` in the current directory for the first target if the project has multiple targets.
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
uv2compdb /path/to/project
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Specify target and output
|
|
28
|
+
|
|
29
|
+
Generate `compile_commands.json` for a specific target and output
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
uv2compdb /path/to/project -t target -o /path/to/compile_commands.json
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Help
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
usage: uv2compdb [-h] [-a ARGUMENTS] [-b] [-t TARGET] [-o OUTPUT] [-p] project
|
|
39
|
+
|
|
40
|
+
Generate compile_commands.json by parse Keil µVision project
|
|
41
|
+
|
|
42
|
+
positional arguments:
|
|
43
|
+
project path to .uvproj[x] file
|
|
44
|
+
|
|
45
|
+
options:
|
|
46
|
+
-h, --help show this help message and exit
|
|
47
|
+
-a, --arguments ARGUMENTS
|
|
48
|
+
add extra arguments
|
|
49
|
+
-b, --build try to build while dep/build_log files don't not exist
|
|
50
|
+
-t, --target TARGET target name
|
|
51
|
+
-o, --output OUTPUT output dir/file path (default: compile_commands.json)
|
|
52
|
+
-p, --predefined try to add predefined macros
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Limit
|
|
56
|
+
|
|
57
|
+
+ [ ] Not support C51
|
|
58
|
+
+ [x] Not parsed `"Options" -> "C/C++" -> "Language / Code Generation"`
|
|
59
|
+
+ [x] Not parsed `"Options" -> "ASM"`, so Asm file use same options with C file
|
|
60
|
+
+ [x] Can't parse **RTE** components
|
|
61
|
+
+ [x] Can't add toolchain predefined macros and include path
|
|
62
|
+
+ [ ] The support for ARMCC (AC5) not well
|
|
63
|
+
+ need config `.clangd` manually
|
|
64
|
+
|
|
65
|
+
## [Clangd]
|
|
66
|
+
|
|
67
|
+
[.clangd config]
|
|
68
|
+
|
|
69
|
+
```yaml
|
|
70
|
+
CompileFlags:
|
|
71
|
+
CompilationDatabase: /path/to/compile-commands-dir
|
|
72
|
+
Compiler: arm-none-eabi-gcc # use arm-neon-eabi-gcc instead of armcc
|
|
73
|
+
Add:
|
|
74
|
+
- -fdeclspec # fix '__declspec' if use arm-none-eabi-gcc instead of armcc
|
|
75
|
+
|
|
76
|
+
Diagnostics:
|
|
77
|
+
UnusedIncludes: None # Strict(default), None
|
|
78
|
+
# Suppress:
|
|
79
|
+
# - no_member
|
|
80
|
+
# - no_member_suggest
|
|
81
|
+
# - no_template
|
|
82
|
+
# - undeclared_var_use
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## References
|
|
86
|
+
|
|
87
|
+
+ [keil2clangd]
|
|
88
|
+
+ [uvConvertor]
|
|
89
|
+
+ [a3750/uvconvertor]
|
|
90
|
+
|
|
91
|
+
[Compilation Database]: <https://clang.llvm.org/docs/JSONCompilationDatabase.html>
|
|
92
|
+
[Clangd]: <https://clangd.llvm.org/>
|
|
93
|
+
[.clangd config]: <https://clangd.llvm.org/config>
|
|
94
|
+
[keil2clangd]: <https://github.com/huiyi-li/keil2clangd>
|
|
95
|
+
[uvConvertor]: <https://github.com/vankubo/uvConvertor>
|
|
96
|
+
[a3750/uvconvertor]: <https://github.com/a3750/uvconvertor>
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "uv2compdb"
|
|
3
|
+
version = "0.3.0"
|
|
4
|
+
description = "Generate Compilation Database by parse Keil µVision project"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [{ name = "xbin", email = "xbin.xu@qq.com" }]
|
|
7
|
+
requires-python = ">=3.8"
|
|
8
|
+
classifiers = [
|
|
9
|
+
"Development Status :: 4 - Beta",
|
|
10
|
+
"Environment :: Console",
|
|
11
|
+
"Intended Audience :: Developers",
|
|
12
|
+
"License :: OSI Approved :: MIT License",
|
|
13
|
+
"Operating System :: OS Independent",
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"Topic :: Software Development :: Embedded Systems",
|
|
16
|
+
"Topic :: Utilities",
|
|
17
|
+
]
|
|
18
|
+
license = "MIT"
|
|
19
|
+
license-files = ["LICEN[CS]E*"]
|
|
20
|
+
keywords = [
|
|
21
|
+
"keil",
|
|
22
|
+
"MDK",
|
|
23
|
+
"µVision",
|
|
24
|
+
"clangd",
|
|
25
|
+
"Compilation Database",
|
|
26
|
+
"compiled_commands.json",
|
|
27
|
+
]
|
|
28
|
+
dependencies = []
|
|
29
|
+
|
|
30
|
+
[project.scripts]
|
|
31
|
+
uv2compdb = "uv2compdb:main"
|
|
32
|
+
|
|
33
|
+
[build-system]
|
|
34
|
+
requires = ["uv_build>=0.9.25,<0.10.0"]
|
|
35
|
+
build-backend = "uv_build"
|
|
36
|
+
|
|
37
|
+
[project.urls]
|
|
38
|
+
Homepage = "https://github.com/xbin-xu/uv2compdb"
|
|
39
|
+
Issues = "https://github.com/xbin-xu/uv2compdb/issues"
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Generate Compilation Database by parse Keil µVision project.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import argparse
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from uv2compdb.parser import UV2CompDB, _split_and_strip, generate_compile_commands
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
logging.basicConfig(
|
|
13
|
+
level=logging.INFO,
|
|
14
|
+
format="[%(levelname).1s] %(message)s",
|
|
15
|
+
# format="[%(levelname).1s] [%(asctime)s] [%(filename)s:%(lineno)d] %(message)s",
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def main() -> int:
|
|
20
|
+
parser = argparse.ArgumentParser(
|
|
21
|
+
description="Generate compile_commands.json by parse Keil µVision project"
|
|
22
|
+
)
|
|
23
|
+
parser.add_argument("-a", "--arguments", default=None, help="add extra arguments")
|
|
24
|
+
parser.add_argument(
|
|
25
|
+
"-b",
|
|
26
|
+
"--build",
|
|
27
|
+
action="store_true",
|
|
28
|
+
help="try to build while dep/build_log files don't not exist",
|
|
29
|
+
)
|
|
30
|
+
parser.add_argument("-t", "--target", default=None, help="target name")
|
|
31
|
+
parser.add_argument(
|
|
32
|
+
"-o",
|
|
33
|
+
"--output",
|
|
34
|
+
default="compile_commands.json",
|
|
35
|
+
help="output dir/file path (default: compile_commands.json)",
|
|
36
|
+
)
|
|
37
|
+
parser.add_argument(
|
|
38
|
+
"-p",
|
|
39
|
+
"--predefined",
|
|
40
|
+
action="store_true",
|
|
41
|
+
help="try to add predefined macros",
|
|
42
|
+
)
|
|
43
|
+
parser.add_argument("project", type=Path, help="path to .uvproj[x] file")
|
|
44
|
+
|
|
45
|
+
args = parser.parse_args()
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
uv2compdb = UV2CompDB(args.project)
|
|
49
|
+
|
|
50
|
+
if not (targets := list(uv2compdb.targets.keys())):
|
|
51
|
+
logger.error("No targets found in project")
|
|
52
|
+
return 1
|
|
53
|
+
|
|
54
|
+
if not args.target:
|
|
55
|
+
args.target = targets[0]
|
|
56
|
+
logger.warning(
|
|
57
|
+
f"Project has multi targets: {targets}, use the first {args.target}"
|
|
58
|
+
)
|
|
59
|
+
elif args.target not in targets:
|
|
60
|
+
logger.error(f"Not found target: {args.target}")
|
|
61
|
+
return 1
|
|
62
|
+
|
|
63
|
+
output_path = Path(args.output)
|
|
64
|
+
if args.output.endswith(("/", "\\")) or (
|
|
65
|
+
output_path.exists() and output_path.is_dir()
|
|
66
|
+
):
|
|
67
|
+
args.output = output_path / "compile_commands.json"
|
|
68
|
+
else:
|
|
69
|
+
args.output = output_path
|
|
70
|
+
|
|
71
|
+
target_setting = uv2compdb.parse(args.target, args.build)
|
|
72
|
+
command_objects = uv2compdb.generate_command_objects(
|
|
73
|
+
target_setting,
|
|
74
|
+
_split_and_strip(args.arguments, delimiter=" ") if args.arguments else [],
|
|
75
|
+
args.predefined,
|
|
76
|
+
)
|
|
77
|
+
if not generate_compile_commands(command_objects, args.output):
|
|
78
|
+
return 1
|
|
79
|
+
logger.info(f"Generate at {args.output.resolve().as_posix()}")
|
|
80
|
+
except Exception as e:
|
|
81
|
+
logger.exception(f"Unexpected error: {e}")
|
|
82
|
+
return 1
|
|
83
|
+
|
|
84
|
+
return 0
|
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
import json
|
|
5
|
+
import shlex
|
|
6
|
+
import shutil
|
|
7
|
+
import logging
|
|
8
|
+
import subprocess
|
|
9
|
+
import xml.etree.ElementTree as ET
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Callable
|
|
12
|
+
from functools import partial, cached_property, lru_cache
|
|
13
|
+
from dataclasses import dataclass, field, asdict
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
PREDEFINED_REGEX = re.compile(r"^#define\s+(\S+)(?:\s+(.*))?")
|
|
18
|
+
TOOLCHAIN_REGEX = re.compile(
|
|
19
|
+
r"Toolchain Path:\s+([^\n]+)\nC Compiler:\s+(\S+)[^\n]+\nAssembler:\s+(\S+)"
|
|
20
|
+
)
|
|
21
|
+
DEP_F_REGEX = re.compile(r"F\s\(([^)]+)\)\([^)]+\)\(([^)]+)\)")
|
|
22
|
+
DEP_I_REGEX = re.compile(r"I\s\(([^)]+)\)\([^)]+\)")
|
|
23
|
+
C_VERSION_REGEX = re.compile(r"^--c(\d+)$")
|
|
24
|
+
ARMCC_UNKNOWN_ARGUMENT_REGEX = [
|
|
25
|
+
(re.compile(r"^--gnu$"), False),
|
|
26
|
+
(re.compile(r"^--c\d+$"), False),
|
|
27
|
+
(re.compile(r"^--cpp$"), False),
|
|
28
|
+
(re.compile(r"^--cpu$"), True),
|
|
29
|
+
(re.compile(r"^--apcs="), False),
|
|
30
|
+
(re.compile(r"^--split_sections$"), False),
|
|
31
|
+
(re.compile(r"^--omf_browse$"), True),
|
|
32
|
+
(re.compile(r"^--depend$"), True),
|
|
33
|
+
(re.compile(r"^--diag_suppress="), True),
|
|
34
|
+
]
|
|
35
|
+
PREDEFINED_FILTER_ARGUMENT_REGEX = [
|
|
36
|
+
(re.compile(r"^-o$"), True),
|
|
37
|
+
(re.compile(r"^--omf_browse$"), True),
|
|
38
|
+
(re.compile(r"^--depend$"), True),
|
|
39
|
+
(re.compile(r"^-I"), False),
|
|
40
|
+
(re.compile(r"^-D"), False),
|
|
41
|
+
(re.compile(r"^-MD$"), False),
|
|
42
|
+
(re.compile(r"^-MMD$"), False),
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _to_posix_path(path: str) -> str:
|
|
47
|
+
"""Convert Windows path separators to POSIX format."""
|
|
48
|
+
return path.replace("\\", "/")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _split_and_strip(text: str, delimiter: str) -> list[str]:
|
|
52
|
+
"""Split text by delimiter and strip whitespace from each part."""
|
|
53
|
+
return [striped for item in text.split(delimiter) if (striped := item.strip())]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass(frozen=True)
|
|
57
|
+
class Toolchain:
|
|
58
|
+
path: str
|
|
59
|
+
compiler: str
|
|
60
|
+
assembler: str
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass
|
|
64
|
+
class FileObject:
|
|
65
|
+
file: str
|
|
66
|
+
arguments: list[str] = field(default_factory=list)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass
|
|
70
|
+
class TargetSetting:
|
|
71
|
+
name: str
|
|
72
|
+
toolchain: Toolchain
|
|
73
|
+
file_objects: list[FileObject] = field(default_factory=list)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass(frozen=True)
|
|
77
|
+
class CommandObject:
|
|
78
|
+
directory: str
|
|
79
|
+
file: str
|
|
80
|
+
arguments: list[str] = field(default_factory=list)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@dataclass(frozen=True)
|
|
84
|
+
class VariousControls:
|
|
85
|
+
"""
|
|
86
|
+
Various Controls Levels: Target, Group, File
|
|
87
|
+
|
|
88
|
+
Various Controls Rules:
|
|
89
|
+
OPTIONS = INCLUDE_PATH + MISC + DEFINE
|
|
90
|
+
INCLUDE_PATH = File.include_path + Group.include_path
|
|
91
|
+
+ Target.include_path
|
|
92
|
+
MISC = Target.misc_controls + Group.misc_controls
|
|
93
|
+
+ File.misc_controls
|
|
94
|
+
DEFINE = Target.undefine + Target.define
|
|
95
|
+
+ Group.undefine + Group.define
|
|
96
|
+
+ File.undefine + File.define
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
misc_controls: list[str] = field(default_factory=list)
|
|
100
|
+
define: list[str] = field(default_factory=list)
|
|
101
|
+
undefine: list[str] = field(default_factory=list)
|
|
102
|
+
include_path: list[str] = field(default_factory=list)
|
|
103
|
+
|
|
104
|
+
def __str__(self) -> str:
|
|
105
|
+
return " ".join(self.get_options())
|
|
106
|
+
|
|
107
|
+
def get_options(self) -> list[str]:
|
|
108
|
+
# 'MBEDTLS_CONFIG_FILE=/\"config-aes-cbc.h/\"'
|
|
109
|
+
# => '-DMBEDTLS_CONFIG_FILE="config-aes-cbc.h"'
|
|
110
|
+
return (
|
|
111
|
+
[f"-I{_to_posix_path(x)}" for x in self.include_path]
|
|
112
|
+
+ [f"{_to_posix_path(x)}" for x in self.misc_controls]
|
|
113
|
+
+ [f"-U{_to_posix_path(x)}" for x in self.undefine]
|
|
114
|
+
+ ["-D" + _to_posix_path(x.replace(r'\\"', '"')) for x in self.define]
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
@classmethod
|
|
118
|
+
def merge(cls, parent: VariousControls, child: VariousControls) -> VariousControls:
|
|
119
|
+
return cls(
|
|
120
|
+
misc_controls=parent.misc_controls + child.misc_controls,
|
|
121
|
+
define=parent.undefine + parent.define + child.undefine + child.define,
|
|
122
|
+
undefine=[],
|
|
123
|
+
include_path=child.include_path + parent.include_path,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class UV2CompDB:
|
|
128
|
+
"""Keil µVision project parser."""
|
|
129
|
+
|
|
130
|
+
# TODO: how to deal with delimiters inside text (e.g., -DFOO="(1, 2)")
|
|
131
|
+
UV_VARIOUS_CONTROLS_MAP: dict[str, tuple[str, Callable[[str], list[str]]]] = {
|
|
132
|
+
"MiscControls": ("misc_controls", partial(_split_and_strip, delimiter=" ")),
|
|
133
|
+
"Define": ("define", partial(_split_and_strip, delimiter=",")),
|
|
134
|
+
"Undefine": ("undefine", partial(_split_and_strip, delimiter=",")),
|
|
135
|
+
"IncludePath": ("include_path", partial(_split_and_strip, delimiter=";")),
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
UV_TOOLCHAIN_MAP: dict[str, Toolchain] = {
|
|
139
|
+
"0x00": Toolchain("", "c51", ""),
|
|
140
|
+
"0x40": Toolchain("", "armcc", "armasm"),
|
|
141
|
+
"0x41": Toolchain("", "armclang", "armasm"),
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
UV_CLI_ERRORLEVEL_MAP: dict[int, str] = {
|
|
145
|
+
0: "No Errors or Warnings",
|
|
146
|
+
1: "Warnings Only",
|
|
147
|
+
2: "Errors",
|
|
148
|
+
3: "Fatal Errors",
|
|
149
|
+
11: "Cannot open project file for writing",
|
|
150
|
+
12: "Device with given name is not found in database",
|
|
151
|
+
13: "Error writing project file",
|
|
152
|
+
15: "Error reading import XML file",
|
|
153
|
+
20: "Error converting project",
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
def __init__(self, project_path: Path) -> None:
|
|
157
|
+
self.project_path: Path = project_path
|
|
158
|
+
|
|
159
|
+
@cached_property
|
|
160
|
+
def root(self) -> ET.Element:
|
|
161
|
+
tree = ET.parse(self.project_path)
|
|
162
|
+
return tree.getroot()
|
|
163
|
+
|
|
164
|
+
@cached_property
|
|
165
|
+
def targets(self) -> dict[str, ET.Element]:
|
|
166
|
+
return {
|
|
167
|
+
target_name: target
|
|
168
|
+
for target in self.root.findall(".//Target")
|
|
169
|
+
if (target_name := self._get_text(target.find("TargetName")))
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
def _get_text(self, elem: ET.Element | None) -> str | None:
|
|
173
|
+
if elem is None or elem.text is None:
|
|
174
|
+
return None
|
|
175
|
+
return elem.text
|
|
176
|
+
|
|
177
|
+
def get_various_controls(self, elem: ET.Element | None) -> VariousControls | None:
|
|
178
|
+
if elem is None:
|
|
179
|
+
return None
|
|
180
|
+
|
|
181
|
+
# None: True, "0": False, "1": True, "2": inherit
|
|
182
|
+
if self._get_text(elem.find(".//CommonProperty/IncludeInBuild")) == "0":
|
|
183
|
+
return None
|
|
184
|
+
|
|
185
|
+
result = {}
|
|
186
|
+
for name, (var_name, pred) in self.UV_VARIOUS_CONTROLS_MAP.items():
|
|
187
|
+
text = self._get_text(elem.find(f".//Cads/VariousControls/{name}"))
|
|
188
|
+
result[var_name] = pred(text) if text else []
|
|
189
|
+
return VariousControls(**result)
|
|
190
|
+
|
|
191
|
+
def try_build(self, target: ET.Element | None) -> bool:
|
|
192
|
+
if target is None:
|
|
193
|
+
return False
|
|
194
|
+
|
|
195
|
+
if not (target_name := self._get_text(target.find("TargetName"))):
|
|
196
|
+
return False
|
|
197
|
+
|
|
198
|
+
# See: https://developer.arm.com/documentation/101407/0543/Command-Line
|
|
199
|
+
if not (uv4_path := shutil.which("uv4")):
|
|
200
|
+
return False
|
|
201
|
+
|
|
202
|
+
cmd = f"{uv4_path} -b -t {target_name} {self.project_path.resolve().as_posix()} -j0"
|
|
203
|
+
logger.info(f"Run: `{cmd}`")
|
|
204
|
+
try:
|
|
205
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
206
|
+
logger.info(
|
|
207
|
+
f"Exit Code: {result.returncode}({self.UV_CLI_ERRORLEVEL_MAP.get(result.returncode)})"
|
|
208
|
+
)
|
|
209
|
+
return result.returncode in [0, 1]
|
|
210
|
+
except (FileNotFoundError, OSError) as e:
|
|
211
|
+
logger.warning(f"Failed to invoke compiler: {e}")
|
|
212
|
+
return False
|
|
213
|
+
|
|
214
|
+
def get_build_log_path(self, target: ET.Element | None) -> Path | None:
|
|
215
|
+
if target is None:
|
|
216
|
+
return None
|
|
217
|
+
|
|
218
|
+
if not (output_directory := self._get_text(target.find(".//OutputDirectory"))):
|
|
219
|
+
return None
|
|
220
|
+
if not (output_name := self._get_text(target.find(".//OutputName"))):
|
|
221
|
+
return None
|
|
222
|
+
return (
|
|
223
|
+
self.project_path.parent / output_directory / f"{output_name}.build_log.htm"
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
def get_toolchain_from_build_log(
|
|
227
|
+
self, target: ET.Element | None, try_build: bool = False
|
|
228
|
+
) -> Toolchain | None:
|
|
229
|
+
if target is None:
|
|
230
|
+
return None
|
|
231
|
+
|
|
232
|
+
if (build_log_path := self.get_build_log_path(target)) is None:
|
|
233
|
+
return None
|
|
234
|
+
|
|
235
|
+
if try_build and not build_log_path.exists():
|
|
236
|
+
logger.warning("Not found build_log, try build ...")
|
|
237
|
+
self.try_build(target)
|
|
238
|
+
if not build_log_path.exists():
|
|
239
|
+
return None
|
|
240
|
+
|
|
241
|
+
text = build_log_path.read_text(encoding="utf-8", errors="ignore")
|
|
242
|
+
if not (m := TOOLCHAIN_REGEX.search(text)):
|
|
243
|
+
return None
|
|
244
|
+
|
|
245
|
+
toolchain_path = _to_posix_path(m.group(1))
|
|
246
|
+
return Toolchain(
|
|
247
|
+
path=toolchain_path,
|
|
248
|
+
compiler=f"{toolchain_path}/{m.group(2)}",
|
|
249
|
+
assembler=f"{toolchain_path}/{m.group(3)}",
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
def get_toolchain_from_xml(self, target: ET.Element | None) -> Toolchain | None:
|
|
253
|
+
if target is None:
|
|
254
|
+
return None
|
|
255
|
+
|
|
256
|
+
if not (toolset_number := self._get_text(target.find("ToolsetNumber"))):
|
|
257
|
+
return None
|
|
258
|
+
|
|
259
|
+
uac6 = self._get_text(target.find("uAC6")) or ""
|
|
260
|
+
key = toolset_number + uac6
|
|
261
|
+
if not (toolchain := self.UV_TOOLCHAIN_MAP.get(key)):
|
|
262
|
+
return None
|
|
263
|
+
|
|
264
|
+
compiler_path = shutil.which(toolchain.compiler)
|
|
265
|
+
return Toolchain(
|
|
266
|
+
path=(
|
|
267
|
+
Path(compiler_path).parent.resolve().as_posix()
|
|
268
|
+
if compiler_path
|
|
269
|
+
else toolchain.path
|
|
270
|
+
),
|
|
271
|
+
compiler=(
|
|
272
|
+
_to_posix_path(compiler_path) if compiler_path else toolchain.compiler
|
|
273
|
+
),
|
|
274
|
+
assembler=(
|
|
275
|
+
(Path(compiler_path).parent / toolchain.assembler).resolve().as_posix()
|
|
276
|
+
if compiler_path
|
|
277
|
+
else toolchain.assembler
|
|
278
|
+
),
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
def get_toolchain(
|
|
282
|
+
self, target: ET.Element | None, try_build: bool = False
|
|
283
|
+
) -> Toolchain | None:
|
|
284
|
+
if target is None:
|
|
285
|
+
return None
|
|
286
|
+
|
|
287
|
+
if toolchain := self.get_toolchain_from_build_log(target, try_build):
|
|
288
|
+
return toolchain
|
|
289
|
+
logger.warning("Not found build_log, fallback to parse xml")
|
|
290
|
+
return self.get_toolchain_from_xml(target)
|
|
291
|
+
|
|
292
|
+
def get_dep_path(self, target: ET.Element | None) -> Path | None:
|
|
293
|
+
if target is None:
|
|
294
|
+
return None
|
|
295
|
+
|
|
296
|
+
if not (target_name := self._get_text(target.find("TargetName"))):
|
|
297
|
+
return None
|
|
298
|
+
if not (output_directory := self._get_text(target.find(".//OutputDirectory"))):
|
|
299
|
+
return None
|
|
300
|
+
return (
|
|
301
|
+
self.project_path.parent
|
|
302
|
+
/ output_directory
|
|
303
|
+
/ f"{self.project_path.stem}_{target_name}.dep"
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
def parse_dep(
|
|
307
|
+
self, target: ET.Element | None, try_build: bool = False
|
|
308
|
+
) -> list[FileObject]:
|
|
309
|
+
if target is None:
|
|
310
|
+
return []
|
|
311
|
+
|
|
312
|
+
if (dep_path := self.get_dep_path(target)) is None:
|
|
313
|
+
return []
|
|
314
|
+
|
|
315
|
+
if try_build and not dep_path.exists():
|
|
316
|
+
logger.warning("Not Found dep file, try build ...")
|
|
317
|
+
self.try_build(target)
|
|
318
|
+
if not dep_path.exists():
|
|
319
|
+
return []
|
|
320
|
+
|
|
321
|
+
content = (
|
|
322
|
+
re.sub(r'\\(?!")', "/", dep_path.read_text(encoding="utf-8"))
|
|
323
|
+
.replace("-I ", "-I") # avoid "-I ./inc" split to two line
|
|
324
|
+
.replace("\n", " ") # to one line
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
# Header directory: parse "I (header)(hex)"
|
|
328
|
+
header_dirs = sorted(
|
|
329
|
+
{Path(m.group(1)).parent.as_posix() for m in DEP_I_REGEX.finditer(content)}
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
# Source file: parse "F (source)(hex)(arguments)"
|
|
333
|
+
file_objects = []
|
|
334
|
+
for m in DEP_F_REGEX.finditer(content):
|
|
335
|
+
file, args = m.group(1), shlex.split(m.group(2))
|
|
336
|
+
|
|
337
|
+
# Add missing include path
|
|
338
|
+
existing = {arg[2:] for arg in args if arg.startswith("-I")}
|
|
339
|
+
args.extend([f"-I{d}" for d in header_dirs if d not in existing])
|
|
340
|
+
file_objects.append(FileObject(file=file, arguments=args))
|
|
341
|
+
return file_objects
|
|
342
|
+
|
|
343
|
+
def parse_xml(self, target: ET.Element | None) -> list[FileObject]:
|
|
344
|
+
if target is None:
|
|
345
|
+
return []
|
|
346
|
+
|
|
347
|
+
if (target_vc := self.get_various_controls(target)) is None:
|
|
348
|
+
logger.warning("Not found target_controls in target")
|
|
349
|
+
return []
|
|
350
|
+
|
|
351
|
+
file_objects = []
|
|
352
|
+
for group in target.findall(".//Group"):
|
|
353
|
+
if (group_vc := self.get_various_controls(group)) is None:
|
|
354
|
+
continue
|
|
355
|
+
|
|
356
|
+
current_vc = VariousControls.merge(target_vc, group_vc)
|
|
357
|
+
for file in group.findall(".//File"):
|
|
358
|
+
file_path = self._get_text(file.find("FilePath"))
|
|
359
|
+
# file_type = self._get_text(file.find("FileType"))
|
|
360
|
+
|
|
361
|
+
if not file_path or not file_path.endswith(
|
|
362
|
+
(".s", ".c", ".cpp", ".cc", ".cx", ".cxx")
|
|
363
|
+
):
|
|
364
|
+
continue
|
|
365
|
+
|
|
366
|
+
if (file_controls := self.get_various_controls(file)) is None:
|
|
367
|
+
continue
|
|
368
|
+
|
|
369
|
+
file_objects.append(
|
|
370
|
+
FileObject(
|
|
371
|
+
file=_to_posix_path(file_path),
|
|
372
|
+
arguments=VariousControls.merge(
|
|
373
|
+
current_vc, file_controls
|
|
374
|
+
).get_options(),
|
|
375
|
+
)
|
|
376
|
+
)
|
|
377
|
+
# logger.debug(f"file_object: {file_objects[-1]}")
|
|
378
|
+
return file_objects
|
|
379
|
+
|
|
380
|
+
def parse(self, target_name: str, try_build: bool = False) -> TargetSetting | None:
|
|
381
|
+
if (target := self.targets.get(target_name)) is None:
|
|
382
|
+
logger.warning(f"Not found target: {target_name}")
|
|
383
|
+
return None
|
|
384
|
+
|
|
385
|
+
if (toolchain := self.get_toolchain(target, try_build)) is None:
|
|
386
|
+
logger.warning("Not found toolchain")
|
|
387
|
+
return None
|
|
388
|
+
logger.info(f"Toolchain: {toolchain}")
|
|
389
|
+
|
|
390
|
+
if not (file_objects := self.parse_dep(target, try_build)):
|
|
391
|
+
logger.warning("Not found dep file, fallback to parse xml")
|
|
392
|
+
file_objects = self.parse_xml(target)
|
|
393
|
+
|
|
394
|
+
return TargetSetting(
|
|
395
|
+
name=target_name,
|
|
396
|
+
toolchain=toolchain,
|
|
397
|
+
file_objects=file_objects,
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
@staticmethod
|
|
401
|
+
@lru_cache(maxsize=32)
|
|
402
|
+
def _get_predefined_macros_cached(compiler: str, args_str: str) -> tuple[str, ...]:
|
|
403
|
+
"""Get predefined macros from compiler with caching."""
|
|
404
|
+
if "armcc" in compiler.lower():
|
|
405
|
+
cmd = f"{compiler} {args_str} --list_macros"
|
|
406
|
+
elif "armclang" in compiler.lower():
|
|
407
|
+
cmd = f"{compiler} {args_str} --target=arm-arm-none-eabi -dM -E -"
|
|
408
|
+
else:
|
|
409
|
+
return ()
|
|
410
|
+
|
|
411
|
+
logger.info(f"Get predefined macro by: `{cmd}`")
|
|
412
|
+
try:
|
|
413
|
+
result = subprocess.run(cmd, capture_output=True, text=True, input="")
|
|
414
|
+
if result.returncode != 0:
|
|
415
|
+
logger.warning(
|
|
416
|
+
f"Exited with code {result.returncode}: {result.stderr.strip()}"
|
|
417
|
+
)
|
|
418
|
+
return ()
|
|
419
|
+
except (FileNotFoundError, OSError) as e:
|
|
420
|
+
logger.warning(f"Failed to invoke compiler: {e}")
|
|
421
|
+
return ()
|
|
422
|
+
|
|
423
|
+
return tuple(
|
|
424
|
+
f"-D{name}={value}"
|
|
425
|
+
for line in result.stdout.splitlines()
|
|
426
|
+
if (m := PREDEFINED_REGEX.match(line.strip()))
|
|
427
|
+
for name, value in [m.groups()]
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
def get_predefined_macros(
|
|
431
|
+
self, toolchain: Toolchain | None, args: list[str] | None = None
|
|
432
|
+
) -> list[str]:
|
|
433
|
+
if toolchain is None or not args:
|
|
434
|
+
return []
|
|
435
|
+
|
|
436
|
+
filtered_args = []
|
|
437
|
+
args_iter = iter(args)
|
|
438
|
+
for arg in args_iter:
|
|
439
|
+
gen = (
|
|
440
|
+
skip for pat, skip in PREDEFINED_FILTER_ARGUMENT_REGEX if pat.match(arg)
|
|
441
|
+
)
|
|
442
|
+
if (skip := next(gen, None)) is None:
|
|
443
|
+
filtered_args.append(arg)
|
|
444
|
+
elif skip:
|
|
445
|
+
next(args_iter, None)
|
|
446
|
+
|
|
447
|
+
return list(
|
|
448
|
+
self._get_predefined_macros_cached(
|
|
449
|
+
toolchain.compiler, " ".join(filtered_args)
|
|
450
|
+
)
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
def filter_unknown_argument(
|
|
454
|
+
self, toolchain: Toolchain | None, arguments: list[str]
|
|
455
|
+
) -> list[str]:
|
|
456
|
+
if toolchain is None or not arguments:
|
|
457
|
+
return []
|
|
458
|
+
|
|
459
|
+
if "armcc" not in toolchain.compiler.lower():
|
|
460
|
+
return arguments
|
|
461
|
+
|
|
462
|
+
filtered_args = []
|
|
463
|
+
args = iter(arguments)
|
|
464
|
+
for arg in args:
|
|
465
|
+
gen = (skip for pat, skip in ARMCC_UNKNOWN_ARGUMENT_REGEX if pat.match(arg))
|
|
466
|
+
if (skip := next(gen, None)) is None:
|
|
467
|
+
filtered_args.append(arg)
|
|
468
|
+
elif skip:
|
|
469
|
+
next(args, None)
|
|
470
|
+
|
|
471
|
+
return filtered_args
|
|
472
|
+
|
|
473
|
+
def generate_command_objects(
|
|
474
|
+
self,
|
|
475
|
+
target_setting: TargetSetting | None,
|
|
476
|
+
extra_args: list[str] | None = None,
|
|
477
|
+
predefined_macros: bool = False,
|
|
478
|
+
) -> list[CommandObject]:
|
|
479
|
+
if target_setting is None:
|
|
480
|
+
return []
|
|
481
|
+
|
|
482
|
+
extra_args = extra_args or []
|
|
483
|
+
command_objects = []
|
|
484
|
+
directory = self.project_path.parent.resolve().as_posix()
|
|
485
|
+
for file_object in target_setting.file_objects:
|
|
486
|
+
toolchain_args = (
|
|
487
|
+
self.get_predefined_macros(
|
|
488
|
+
target_setting.toolchain, file_object.arguments
|
|
489
|
+
)
|
|
490
|
+
if predefined_macros and not file_object.file.endswith(".s")
|
|
491
|
+
else []
|
|
492
|
+
)
|
|
493
|
+
arguments = self.filter_unknown_argument(
|
|
494
|
+
target_setting.toolchain, file_object.arguments
|
|
495
|
+
)
|
|
496
|
+
command_objects.append(
|
|
497
|
+
CommandObject(
|
|
498
|
+
directory=directory,
|
|
499
|
+
file=file_object.file,
|
|
500
|
+
arguments=(
|
|
501
|
+
[
|
|
502
|
+
target_setting.toolchain.compiler
|
|
503
|
+
if not file_object.file.endswith(".s")
|
|
504
|
+
else target_setting.toolchain.assembler
|
|
505
|
+
]
|
|
506
|
+
+ toolchain_args
|
|
507
|
+
+ arguments
|
|
508
|
+
+ extra_args
|
|
509
|
+
),
|
|
510
|
+
)
|
|
511
|
+
)
|
|
512
|
+
return command_objects
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def generate_compile_commands(
|
|
516
|
+
command_objects: list[CommandObject], output: Path
|
|
517
|
+
) -> bool:
|
|
518
|
+
if not command_objects:
|
|
519
|
+
logger.warning("No command objects")
|
|
520
|
+
return False
|
|
521
|
+
|
|
522
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
523
|
+
with open(output, "w", encoding="utf-8") as f:
|
|
524
|
+
json.dump(
|
|
525
|
+
[asdict(obj) for obj in command_objects],
|
|
526
|
+
f,
|
|
527
|
+
indent=4,
|
|
528
|
+
ensure_ascii=False,
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
return True
|