使用STM32CUEBEIDE/S32DS 开发时,生成compile_commands.json 方便VSCODE智能提示

使用eclipse开发C项目,比如stm32cubeide、s32ds时,由IDE生成makefile。如果你在VSCODE编辑代码,那是一片飘红,几乎没有智能提示。所以需要生成compile_commands.json ,喂给VSCODE。

使用方法(前提你已经生成了elf):

python 复制代码
python gen_compile_commands_from_args.py 你的构建目录
比如
python gen_compile_commands_from_args.py App/Debug_FLASH #Debug_Flash是IDE已经生成好ELF的目录 这样 Debug_Flash目录下就有了compile_commands.json
python 复制代码
#!/usr/bin/env python3
import argparse
import json
import re
from pathlib import Path


COMPILER_RE = re.compile(r"\b(arm-none-eabi-g\+\+|arm-none-eabi-gcc)\b")
ARGS_RE = re.compile(r'"@([^"]+\.args)"|@([^\s"]+\.args)')
RULE_RE = re.compile(r"^([^\s:]+)\s*:\s*(.+)$")
DRIVE_PATH_RE = re.compile(r"([A-Za-z]:[\\/][^\s\"']+)", re.IGNORECASE)


def read_text(path: Path) -> str:
    return path.read_text(encoding="utf-8", errors="ignore")


def parse_source_lists(lines):
    source_lists = {
        "C_SRCS": [],
        "CPP_SRCS": [],
        "CXX_SRCS": [],
        "CC_SRCS": [],
        "C++_SRCS": [],
    }
    idx = 0
    while idx < len(lines):
        line = lines[idx].rstrip("\n")
        m = re.match(r"^(C_SRCS|CPP_SRCS|CXX_SRCS|CC_SRCS|C\+\+_SRCS)\s*\+=\s*\\?\s*$", line)
        if not m:
            idx += 1
            continue
        key = m.group(1)
        idx += 1
        while idx < len(lines):
            cur = lines[idx].rstrip("\n").strip()
            if not cur:
                break
            if cur.endswith("\\"):
                cur = cur[:-1].rstrip()
            source_lists[key].append(cur)
            idx += 1
        idx += 1
    return source_lists


def source_candidates_by_ext(source_lists):
    out = {}
    for key, values in source_lists.items():
        for src in values:
            ext = Path(src).suffix.lower()
            out.setdefault(ext, []).append(src)
    return out


def parse_args_file(args_path: Path):
    def clean_token(token: str) -> str:
        t = token.strip()
        if not t:
            return t

        # key="value" -> key=value
        if '="' in t and t.endswith('"'):
            left, right = t.split('="', 1)
            return f"{left}={right[:-1]}"

        # -I"dir" / -include"file" -> -Idir / -includefile
        for prefix in ("-I", "-isystem", "-iquote", "-include", "-imacros", "-MF", "-MT", "-MQ", "-o"):
            if t.startswith(prefix + '"') and t.endswith('"'):
                return prefix + t[len(prefix) + 1 : -1]

        # "path" -> path
        if len(t) >= 2 and t[0] == '"' and t[-1] == '"':
            return t[1:-1]

        return t

    tokens = []
    for raw in read_text(args_path).splitlines():
        line = raw.strip()
        if not line:
            continue
        tokens.append(clean_token(line))
    return tokens


def normalize_source_for_db(src: str, build_dir: Path) -> str:
    src = src.strip()
    if re.match(r"^[A-Za-z]:/", src):
        return src.replace("\\", "/")
    abs_path = (build_dir / src).resolve()
    return str(abs_path).replace("\\", "/")


def infer_lang_ext(prereq: str) -> str:
    prereq = prereq.strip().strip('"')
    if "%" in prereq:
        low = prereq.lower()
        if low.endswith("%.c"):
            return ".c"
        if low.endswith("%.cpp") or low.endswith("%.cc") or low.endswith("%.cxx"):
            return ".cpp"
        return ""
    return Path(prereq).suffix.lower()


def pick_map_file(build_dir: Path):
    expected_stem = build_dir.parent.name.lower()
    exact = build_dir / f"{build_dir.parent.name}.map"
    if exact.exists():
        return exact
    maps = list(build_dir.glob("*.map"))
    for item in maps:
        if item.stem.lower() == expected_stem:
            return item
    return maps[0] if maps else None


def resolve_toolchain_from_map(build_dir: Path):
    map_file = pick_map_file(build_dir)
    if not map_file:
        return None, None, None

    root = None
    for raw_line in read_text(map_file).splitlines():
        line = raw_line.strip()
        if not line:
            continue
        lower = line.lower().replace("\\", "/")
        marker = "bin/../lib/gcc/arm-none-eabi"
        idx = lower.find(marker)
        if idx < 0:
            continue
        prefix = line[:idx].replace("\\", "/").rstrip("/")
        m = DRIVE_PATH_RE.search(prefix)
        if not m:
            continue
        root = m.group(1).replace("\\", "/").rstrip("/")
        break

    if not root:
        return map_file, None, None

    gcc = f"{root}/bin/arm-none-eabi-gcc"
    gpp = f"{root}/bin/arm-none-eabi-g++"
    if Path(gcc + ".exe").exists():
        gcc += ".exe"
    if Path(gpp + ".exe").exists():
        gpp += ".exe"
    return map_file, gcc, gpp


def build_compiler_system_include_flags(gcc_path: str | None, gpp_path: str | None):
    if not gcc_path:
        return [], []

    gcc_bin = Path(gcc_path)
    toolchain_root = gcc_bin.parent.parent
    gcc_lib_root = toolchain_root / "lib" / "gcc" / "arm-none-eabi"
    c_flags = []
    cxx_flags = []

    def add_flag(flags, include_path: Path):
        if include_path.exists():
            flags.append(f"-isystem{str(include_path).replace('\\', '/')}")

    # Common C/C++ headers.
    add_flag(c_flags, toolchain_root / "arm-none-eabi" / "include")
    add_flag(cxx_flags, toolchain_root / "arm-none-eabi" / "include")

    if gcc_lib_root.exists():
        version_dirs = [p for p in gcc_lib_root.iterdir() if p.is_dir()]
        version_dirs.sort()
        for version_dir in version_dirs:
            add_flag(c_flags, version_dir / "include")
            add_flag(c_flags, version_dir / "include-fixed")

            add_flag(cxx_flags, version_dir / "include")
            add_flag(cxx_flags, version_dir / "include-fixed")

            # C++ standard library headers.
            cxx_root = toolchain_root / "arm-none-eabi" / "include" / "c++" / version_dir.name
            add_flag(cxx_flags, cxx_root)
            add_flag(cxx_flags, cxx_root / "arm-none-eabi")
            add_flag(cxx_flags, cxx_root / "backward")

    # Remove duplicates while preserving order.
    c_flags = list(dict.fromkeys(c_flags))
    cxx_flags = list(dict.fromkeys(cxx_flags))
    return c_flags, cxx_flags


def append_unique_flags(arg_tokens, extra_flags):
    if not extra_flags:
        return arg_tokens
    token_set = set(arg_tokens)
    merged = list(arg_tokens)
    for flag in extra_flags:
        if flag not in token_set:
            merged.append(flag)
            token_set.add(flag)
    return merged


def collect_entries(
    build_dir: Path,
    gcc_path: str | None = None,
    gpp_path: str | None = None,
    c_sys_flags=None,
    cxx_sys_flags=None,
):
    entries = []
    seen = set()

    for subdir_mk in build_dir.rglob("subdir.mk"):
        lines = read_text(subdir_mk).splitlines(keepends=True)
        source_lists = parse_source_lists(lines)
        by_ext = source_candidates_by_ext(source_lists)

        i = 0
        while i < len(lines):
            line = lines[i].rstrip("\n")
            rule_match = RULE_RE.match(line)
            if not rule_match:
                i += 1
                continue

            target = rule_match.group(1).strip()
            prereq_all = rule_match.group(2).strip()
            prereq_first = prereq_all.split()[0]

            j = i + 1
            compiler = None
            args_rel = None
            while j < len(lines) and lines[j].startswith("\t"):
                cmd = lines[j].strip()
                comp = COMPILER_RE.search(cmd)
                args_m = ARGS_RE.search(cmd)
                if comp and args_m:
                    compiler = comp.group(1)
                    if compiler == "arm-none-eabi-gcc" and gcc_path:
                        compiler = gcc_path
                    elif compiler == "arm-none-eabi-g++" and gpp_path:
                        compiler = gpp_path
                    args_rel = (args_m.group(1) or args_m.group(2)).replace("\\", "/")
                    break
                j += 1

            if not compiler or not args_rel:
                i = j
                continue

            args_path = build_dir / args_rel
            if not args_path.exists():
                i = j
                continue

            arg_tokens = parse_args_file(args_path)
            if compiler.endswith("arm-none-eabi-g++") or compiler.endswith("arm-none-eabi-g++.exe"):
                arg_tokens = append_unique_flags(arg_tokens, cxx_sys_flags or [])
            else:
                arg_tokens = append_unique_flags(arg_tokens, c_sys_flags or [])

            if "%" in target or "%" in prereq_first:
                ext = infer_lang_ext(prereq_first)
                for src in by_ext.get(ext, []):
                    file_path = normalize_source_for_db(src, build_dir)
                    if file_path in seen:
                        continue
                    seen.add(file_path)
                    entries.append(
                        {
                            "directory": str(build_dir).replace("\\", "/"),
                            "file": file_path,
                            "arguments": [compiler, *arg_tokens, file_path],
                        }
                    )
            else:
                file_path = normalize_source_for_db(prereq_first, build_dir)
                if file_path not in seen:
                    seen.add(file_path)
                    entries.append(
                        {
                            "directory": str(build_dir).replace("\\", "/"),
                            "file": file_path,
                            "arguments": [compiler, *arg_tokens, file_path],
                        }
                    )
            i = j

    entries.sort(key=lambda x: x["file"])
    return entries


def find_build_dirs(project_dir: Path):
    candidates = []
    for child in project_dir.iterdir():
        if not child.is_dir():
            continue
        if (child / "makefile").exists() and (child / "sources.mk").exists():
            subdir_count = len(list(child.rglob("subdir.mk")))
            candidates.append((child, subdir_count))
    candidates.sort(key=lambda item: item[1], reverse=True)
    return candidates


def main():
    parser = argparse.ArgumentParser(
        description="Generate compile_commands.json from S32DS subdir.mk and .args files."
    )
    parser.add_argument(
        "--build-dir",
        default="auto",
        help="S32DS build directory path, or 'auto' (default: auto)",
    )
    parser.add_argument(
        "--list-build-dirs",
        action="store_true",
        help="List detected S32DS build directories and exit",
    )
    parser.add_argument(
        "--output",
        default="compile_commands.json",
        help="Output file name or absolute path (default: compile_commands.json under build dir)",
    )
    args = parser.parse_args()

    script_dir = Path(__file__).resolve().parent
    detected = find_build_dirs(script_dir)

    if args.list_build_dirs:
        if not detected:
            print("No build directories detected.")
            return
        for item, count in detected:
            print(f"{item.name} (subdir.mk: {count})")
        return

    if args.build_dir.lower() == "auto":
        if not detected:
            raise SystemExit("No build directory detected. Use --build-dir <path>.")
        build_dir = detected[0][0]
        if len(detected) > 1:
            others = ", ".join(d[0].name for d in detected[1:4])
            print(f"Auto-selected build dir: {build_dir.name}")
            print(f"Other detected dirs: {others}")
        else:
            print(f"Auto-selected build dir: {build_dir.name}")
    else:
        candidate = Path(args.build_dir)
        build_dir = candidate if candidate.is_absolute() else (script_dir / candidate)
        build_dir = build_dir.resolve()
        if not build_dir.exists():
            raise SystemExit(f"Build directory not found: {build_dir}")

    output_arg = Path(args.output)
    if output_arg.is_absolute():
        output_path = output_arg
    else:
        output_path = (build_dir / output_arg).resolve()
    output_path.parent.mkdir(parents=True, exist_ok=True)

    map_file, gcc_path, gpp_path = resolve_toolchain_from_map(build_dir)
    if map_file:
        print(f"Map file: {map_file}")
        if gcc_path and gpp_path:
            print(f"Toolchain from map: {gcc_path}")
        else:
            print("Toolchain path not found in map, keeping arm-none-eabi-gcc/g++")
    else:
        print("Map file not found, keeping arm-none-eabi-gcc/g++")

    c_sys_flags, cxx_sys_flags = build_compiler_system_include_flags(gcc_path, gpp_path)
    if c_sys_flags or cxx_sys_flags:
        print(
            f"Added system include flags: C={len(c_sys_flags)} C++={len(cxx_sys_flags)}"
        )

    entries = collect_entries(
        build_dir,
        gcc_path=gcc_path,
        gpp_path=gpp_path,
        c_sys_flags=c_sys_flags,
        cxx_sys_flags=cxx_sys_flags,
    )
    output_path.write_text(json.dumps(entries, indent=2), encoding="utf-8")
    print(f"Generated {len(entries)} entries: {output_path}")


if __name__ == "__main__":
    main()
相关推荐
2501_9159214313 小时前
KXBuild CI 实操,Windows/Linux 服务器接入 Jenkins 打包 iOS IPA
服务器·windows·vscode·ios·ci/cd·个人开发·敏捷流程
Lhan.zzZ13 小时前
用 Qt 构建线程安全的通讯静态库:从线程亲和性到完整工程
c++·vscode·qt·tcp/ip·udp
Halo101314 小时前
VSCode设置护眼背景色
ide·vscode·编辑器
abc light14 小时前
ARM 裸机开发(6)——时钟树与定时器
arm开发·单片机·fpga开发
吠品1 天前
智谱开源 ZCode 之后,真正的考试才开始
单片机·嵌入式硬件·1024程序员节
光影6271 天前
Unittest单元测试基础速记
测试工具·单元测试·json·测试覆盖率
ZLongTeng1 天前
EIDE的launch.json文件
json
小李不想当小白1 天前
SPI通信外设与W25Q64 (硬件SPI)(STM32标准库学习笔记)
笔记·stm32·单片机·嵌入式硬件·学习·嵌入式
LCG米1 天前
STM32驱动W5500以太网模块实测:TCP吞吐瓶颈定位、TX指针回绕陷阱与断线重连保活落地
stm32·单片机·嵌入式硬件·tcp/ip
创界工坊工作室1 天前
63-基于STC89C52霍尔传感器的智能化电能表设计
stm32·单片机·嵌入式硬件·51单片机·proteus