uv2compdb 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
uv2compdb/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ __version__ = "0.3.0"
2
+
3
+ from uv2compdb.main import main
4
+
5
+ __all__ = ["main"]
uv2compdb/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ from uv2compdb import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
uv2compdb/main.py ADDED
@@ -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
uv2compdb/parser.py ADDED
@@ -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
@@ -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,9 @@
1
+ uv2compdb/__init__.py,sha256=QGf8GNYJg7LAumsXASUDPORucTmkrjVVyBPqa9JrBSA,80
2
+ uv2compdb/__main__.py,sha256=6nm32QqZwc9URAzRAwrgJTyoLM-nGu0r9wH5iZfOKuE,70
3
+ uv2compdb/main.py,sha256=vIqI6CKChSzZfN40wTjXhUS3OvIqeH2DKFD-mSQcRY8,2756
4
+ uv2compdb/parser.py,sha256=BN-MHujtrSiL9wibFpPGIu8le9WEWZhLtAnXdUXFeig,18927
5
+ uv2compdb-0.3.0.dist-info/licenses/LICENSE,sha256=jBXhW_dm3ZqVqD8CitzmaDMGPonkPOmqWSDaIEUO30M,1085
6
+ uv2compdb-0.3.0.dist-info/WHEEL,sha256=XjEbIc5-wIORjWaafhI6vBtlxDBp7S9KiujWF1EM7Ak,79
7
+ uv2compdb-0.3.0.dist-info/entry_points.txt,sha256=O6qqKx-ZWjCetvl-PiycCpx92c9Xwm-EN9_UZEMHo04,46
8
+ uv2compdb-0.3.0.dist-info/METADATA,sha256=9-41mNUKZg29dpXTBMh7khDZwMO6DGAh7ZHg4It97BU,3433
9
+ uv2compdb-0.3.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.9.25
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ uv2compdb = uv2compdb:main
3
+
@@ -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.