使用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()
相关推荐
lanhuazui102 小时前
vscode打不开终端窗口
ide·vscode·编辑器
fengjay012 小时前
AI Coding——VsCode
ide·vscode·编辑器
qq_402995752 小时前
RS485通信设计
stm32·单片机·mcu
串口哑火达人2 小时前
(七)RT-Thread物联网实战--MQTT-cJSON-OneNET
c语言·单片机·嵌入式硬件·mcu·物联网
qingcyb2 小时前
JacksonUtilTest
windows·json
普中科技2 小时前
【普中STM32F1xx开发攻略--标准库版】-- 第 34 章 RTC 实时时钟实验
stm32·单片机·嵌入式硬件·开发板·rtc·实时时钟·普中科技
我在人间贩卖青春2 小时前
NVIC相关寄存器
单片机·嵌入式硬件·中断·nvic
Silicore_Emma2 小时前
芯谷科技—79MXX系列三端负电压稳压器
单片机·运算放大器·线性稳压器·消费电子·芯谷科技·通讯设备系统·便携式车载音响
玩转单片机与嵌入式2 小时前
Keil 最反人类的设计是什么?答:加文件!(附一键添加工具)
单片机