Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions build_wheel.bat
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
@echo off
REM ============================================================================
REM build_wheel.bat - One-shot: clean, build, and verify the graphify wheel.
REM
REM Produces dist\graphifyy-<version>-py3-none-any.whl and checks it actually
REM contains the cpp_deep package + the [cpp] extra before declaring success.
REM
REM Usage: build_wheel.bat
REM ============================================================================
setlocal
cd /d "%~dp0"

echo [1/4] Cleaning old build artifacts...
if exist dist rmdir /s /q dist
if exist build rmdir /s /q build
for /d %%D in (*.egg-info) do rmdir /s /q "%%D"
for /d %%D in (graphify\*.egg-info) do rmdir /s /q "%%D"

echo [2/4] Ensuring the 'build' backend is available...
python -c "import build" >nul 2>&1 || python -m pip install build -q

echo [3/4] Building wheel...
python -m build --wheel
if errorlevel 1 (
echo [ERROR] wheel build failed.
exit /b 1
)

echo [4/4] Verifying wheel contents...
python -c "import glob,os,sys,zipfile; whls=sorted(glob.glob('dist/graphifyy-*.whl'));_=sys.exit('no wheel') if not whls else None; whl=whls[-1]; zf=zipfile.ZipFile(whl); names=zf.namelist(); cpp=[n for n in names if n.startswith('graphify/cpp_deep/') and n.endswith('.py')]; meta=zf.read([n for n in names if n.endswith('METADATA')][0]).decode('utf-8','replace'); skills=[n for n in names if n.endswith('skill.md') or '/skills/' in n]; checks=[('cpp_deep modules', len(cpp)>=10, str(len(cpp))+' py'),('[cpp] extra libclang','libclang' in meta,''),('skill docs',len(skills)>=1,str(len(skills))),('resolver.py','graphify/cpp_deep/resolver.py' in names,'')]; [print(' [%s] %s %s'%('OK ' if c else 'FAIL',l,d)) for l,c,d in checks]; bad=[l for l,c,d in checks if not c]; sys.exit('INCOMPLETE wheel: '+', '.join(bad)) if bad else print('\n[DONE] '+whl); print(' Copy this wheel + install.bat to the target machine.')"
if errorlevel 1 (
echo [ERROR] wheel verification failed - do not distribute.
exit /b 1
)
endlocal
63 changes: 63 additions & 0 deletions build_wheel.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
#!/usr/bin/env bash
# =============================================================================
# build_wheel.sh - One-shot: clean, build, and verify the graphify wheel.
#
# Produces dist/graphifyy-<version>-py3-none-any.whl and checks it actually
# contains the cpp_deep package + the [cpp] extra before declaring success.
#
# Usage: ./build_wheel.sh
# =============================================================================
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
PY="${PYTHON:-python}"

echo "[1/4] Cleaning old build artifacts..."
rm -rf dist/ build/ ./*.egg-info graphify/*.egg-info 2>/dev/null || true

echo "[2/4] Ensuring the 'build' backend is available..."
if ! "$PY" -c "import build" >/dev/null 2>&1; then
if command -v uv >/dev/null 2>&1; then
uv pip install build -q
else
"$PY" -m pip install build -q
fi
fi

echo "[3/4] Building wheel..."
"$PY" -m build --wheel

echo "[4/4] Verifying wheel contents..."
"$PY" - <<'PYEOF'
import glob, os, sys, zipfile

whls = sorted(glob.glob("dist/graphifyy-*.whl"))
if not whls:
print(" [ERROR] no wheel produced in dist/", file=sys.stderr); sys.exit(1)
whl = whls[-1]
zf = zipfile.ZipFile(whl)
names = zf.namelist()

cpp = [n for n in names if n.startswith("graphify/cpp_deep/") and n.endswith(".py")]
meta = zf.read([n for n in names if n.endswith("METADATA")][0]).decode("utf-8", "replace")
skills = [n for n in names if n.endswith("skill.md") or "/skills/" in n]

ok = True
def check(label, cond, detail=""):
global ok
mark = "OK " if cond else "FAIL"
if not cond: ok = False
print(f" [{mark}] {label}{(' - ' + detail) if detail else ''}")

check(f"wheel present ({os.path.basename(whl)}, {os.path.getsize(whl)//1024} KB)", True)
check("cpp_deep modules bundled", len(cpp) >= 10, f"{len(cpp)} .py files")
check("[cpp] extra -> libclang", "libclang" in meta)
check("skill docs bundled", len(skills) >= 1, f"{len(skills)} files")
check("cpp-deep resolver present", "graphify/cpp_deep/resolver.py" in names)

if not ok:
print("\n Wheel is INCOMPLETE - do not distribute.", file=sys.stderr); sys.exit(1)
print(f"\n[DONE] {whl}")
print(" Copy this wheel + install.sh/install.bat to the target machine.")
PYEOF
116 changes: 116 additions & 0 deletions docs/cpp-deep-enhancement-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# Graphify C/C++ 增强设计方案

> 目标:让 Graphify 对 C/C++ 生成的知识图谱能够 (a) 完整遍历顶层→底层调用链,含**宏展开**与**跨 DLL/模块**调用;(b) 社区聚合度更高;(c) 更少重复节点(头文件分离导致的重复)。

## 决策(已确认)

- 深层引擎:**libclang (`clang.cindex`)** — 直接拿完全预处理后的 AST(宏已实例化)+ 语义级 USR 符号身份。
- 工程类型:**Windows `.sln` / `.vcxproj`**,先转换/提取为 `compile_commands.json`(含 include 路径与宏定义)。
- 跨 DLL:**所有 DLL 均有源码** → 做**源码级跨模块链接**(caller → 其它模块内的定义),按模块/DLL 分组成调用链。
- 落地:**opt-in 增强模块 + 新 flag**,默认行为不变;libclang 缺失时优雅回退到纯 tree-sitter。

## 现状关键事实(研究结论)

- C/C++ 全走通用 `_extract_generic`(`_C_CONFIG`/`_CPP_CONFIG`);无专用 C/C++ 提取体。
- **宏是最大缺口**:tree-sitter 不展开宏;`preproc_def`/`preproc_function_def` 不产生任何节点;函数式宏调用点变成解析不到的悬空 raw_call。
- **代码节点去重只按 ID**(`dedup.py` 对 `file_type=="code"` 跳过 label 合并)。重复头文件符号只有共享 canonical ID 才会合并。ID 由 `ids.py make_id` + `_file_stem`(去扩展名)决定。
- 已有干净扩展点:`resolver_registry.register(LanguageResolver(name, suffixes, resolve_fn))`,`resolve_fn(per_file, all_nodes, all_edges)` 在 `extract.py:16827` 自动运行。现有 `_resolve_cpp_member_calls` 即样板。
- 跨模块外部符号唯一 seam:`global_graph.py:121-142` 按 label 合并 `source_file`-空的 external 节点;C 家族在 `build.py:596 _LANG_FAMILY` 同一桶,impl→header 的 INFERRED calls 可存活。
- 内聚度 = 社区内边密度(`cluster.py:257`)。加入**真实且类型正确**的跨文件/跨模块边会直接提升内聚度。
- 环境:`clang.cindex` 当前未安装、PATH 无 clang;Python 3.14。CLI 为手写 `sys.argv` 解析。

## 总体架构:四阶段,新增独立模块

新增包 `graphify/cpp_deep/`(不污染 16939 行的 `extract.py`),通过 resolver 注册接入:

```
graphify/cpp_deep/
__init__.py # register_cpp_deep_resolver(); 特性开关与 libclang 探测
compile_db.py # .sln/.vcxproj → compile_commands.json;include/宏推断回退
clang_index.py # libclang 封装:TU 解析、USR、宏实例化、调用/引用游标
macro.py # 宏节点 + 宏展开边(定义→展开目标,调用点→真实 callee)
crossmod.py # 跨模块/DLL 调用链:按 USR 的源码级链接 + 模块分组
merge.py # 把 clang 结果并入 all_nodes/all_edges,ID 对齐 tree-sitter
usr_id.py # USR → 稳定 canonical ID(TU 无关,消重复头文件节点)
```

### Stage 1 — tree-sitter(保持不变)
初步语法解析,产出现有节点/边。作为 libclang 不可用时的回退基线。

### Stage 2 — libclang 深层解析(新)
1. **compile_commands.json 获取**(`compile_db.py`):
- 若已存在 → 直接用。
- 否则解析 `.sln` 找 `.vcxproj`;从 vcxproj 读 `AdditionalIncludeDirectories`、`PreprocessorDefinitions`、`AdditionalOptions`、语言标准,合成每个 TU 的编译命令(MSVC 模式 `clang-cl` 兼容参数)。
- 都没有 → 启发式:扫描工程推断 include 根目录 + 常见宏,尽力解析,失败文件回退 tree-sitter。
2. **TU 解析**(`clang_index.py`):对每个翻译单元 `clang.cindex.Index.parse(..., options=DETAILED_PREPROCESSING_RECORD | ...)`。
- 用 **USR**(Unified Symbol Resolution)作为符号的 TU 无关身份 → 解决“头文件分散、重复引用”导致的重复节点。
- 采集:函数/方法定义与声明、`CALL_EXPR` 调用边(游标级,精确到被调用定义的 USR)、类型引用。

### Stage 3 — 宏展开(新,`macro.py`)
- 为 `MACRO_DEFINITION` 建节点(object-like / function-like)。
- 用 `DETAILED_PREPROCESSING_RECORD` 拿 `MACRO_INSTANTIATION`:在每个展开点,把**宏 → 展开后真实调用的函数**连边(`expands_to` / `calls`),并把宏节点挂到调用者上。
- 对“宏生成的函数定义”(如 `DEFINE_HANDLER(foo)`):libclang 看到的是**展开后**的真实 `FUNCTION_DECL`,直接建函数节点与其内部调用边 — 补上 tree-sitter 完全丢失的部分。
- 条件编译(`#ifdef`):libclang 只解析激活分支 → 消除 tree-sitter 双分支重复/矛盾节点。

### Stage 4 — 合并 + 调优(`merge.py` / `crossmod.py` + 复用现有 dedup/cluster)
1. **ID 对齐**(`usr_id.py`):USR → canonical ID,规则与 `_file_stem`/`make_id` 对齐,使 clang 节点与 tree-sitter 节点在同一符号上**同 ID**,从而:
- 复用现有 `_merge_decl_def_classes` + 按-ID 去重,自动折叠“头文件声明 + 源文件定义 + N 个包含点”的重复。
- 头文件符号 ID 锚定到**声明所在头文件**的路径(TU 无关),彻底消除跨库重复头引用节点。
2. **跨模块/DLL 调用链**(`crossmod.py`):clang 的 CALL_EXPR 已指向被调用定义的 USR;按 USR 在**全工程**内解析(即使定义在另一个 DLL 的源码中)→ EXTRACTED `calls` 边。给节点标注 `module`/`dll`(来自 vcxproj 输出目标),供调用链按 DLL 分组遍历与可视化。
3. **调优**:
- 边优先 EXTRACTED(clang 精确),覆盖/增强 tree-sitter 的 INFERRED。
- 通过更多真实跨文件/跨模块边提升社区内聚度;宏与 include 折叠减少重复节点。
- 复用现有 `cluster`(Leiden/Louvain)与 `dedup`,不改其算法,仅喂给它更干净的输入。

## 接入方式(opt-in)

- 新 flag:`--cpp-deep`(在 `/graphify` 与 `graphify extract` 都识别)。可选 `--compile-db <path>`、`--cpp-deep-macros`。
- `graphify/cpp_deep/__init__.py` 在**导入时探测** libclang;`register` 仅在 `--cpp-deep` 且探测成功时激活 resolver。缺失时打印一次提示并回退,绝不阻塞。
- 依赖:`pyproject.toml` 增加 optional extra `cpp = ["libclang"]`(`pip install 'graphifyy[cpp]'`)。

## 落地步骤(增量、可验证)

1. **脚手架 + 探测**:建 `cpp_deep/` 包、`--cpp-deep` flag、libclang 探测与回退;不产出边时全链路仍正常。
2. **compile_db.py**:`.sln`/`.vcxproj` → `compile_commands.json`;单测覆盖一个样例 vcxproj。
3. **clang_index.py + usr_id.py**:TU 解析、USR→ID、函数/调用采集;与 tree-sitter 节点 ID 对齐的单测。
4. **macro.py**:宏节点 + 展开边 + 宏生成函数;样例含函数式宏与 `#ifdef`。
5. **crossmod.py**:跨模块 `calls` 边 + `module`/`dll` 标注;多 vcxproj 样例形成跨 DLL 调用链。
6. **merge.py**:并入 `all_nodes/all_edges`,跑通现有 dedup/cluster;端到端验证顶层→底层链路可 `graphify query`/`path` 遍历。
7. **样例工程 + 集成测试**:`tests/fixtures/cpp_multi_dll/`(2~3 个互相调用、含宏、头文件分离的 DLL 源码工程),断言:宏展开链、跨 DLL 链、无重复头节点、内聚度提升。

## 风险与回退

- libclang 版本/DLL 定位:`clang_index.py` 统一处理 `Config.set_library_file`,失败即回退 tree-sitter。
- MSVC 参数兼容:优先 `clang-cl` 驱动模式解析 vcxproj 参数。
- 解析慢/大工程:TU 级缓存(复用现有 per-file cache 思路,key 含编译命令 hash)。
- 全程 opt-in:任何阶段失败都不影响现有 `/graphify` 默认行为。

## 实现状态(已完成)

包 `graphify/cpp_deep/` 已实现并通过测试(24 个测试全绿,含跨 DLL 集成测试):

| 模块 | 行数 | 职责 |
|---|---|---|
| `__init__.py` | 105 | 特性开关 `GRAPHIFY_CPP_DEEP` + libclang 探测 + resolver 注册 |
| `resolver.py` | 53 | 注册入口;**运行时**再次校验开关(防跨调用泄漏) |
| `pipeline.py` | 106 | 从 baseline 节点重建 C/C++ 文件集与 root,编排四阶段 |
| `compile_db.py` | 372 | compile_commands.json / .vcxproj / 启发式 三级回退 |
| `clang_index.py` | 417 | libclang 定位 + TU 解析 + 游标遍历(符号/调用/宏) |
| `records.py` | 76 | 阶段间中间记录(不泄漏 clang 类型) |
| `usr_id.py` | 76 | USR/位置 → 与 tree-sitter 对齐的 canonical ID |
| `macro.py` | 106 | 宏节点 + `contains`/`uses` 边 |
| `crossmod.py` | 116 | 按 USR 的跨模块调用解析 + 模块归属(定义方优先) |
| `merge.py` | 165 | 折叠进 all_nodes/all_edges;EXTRACTED 覆盖 INFERRED |

**接入点**:`extract.py` 在 `run_language_resolvers` 前调用 `cpp_deep.maybe_register()`(失败隔离);CLI `--cpp-deep` / `--compile-db` 设置环境变量;`pyproject.toml` 新增 `cpp = ["libclang>=16"]`。

**已验证的关键成果**:
- 宏 `DOUBLE` 成为一等节点(baseline 完全没有);`compute() --uses--> DOUBLE` 记录展开出处。
- 函数式宏展开后的真实调用由 libclang 自动捕获(post-expansion AST)。
- 头文件声明 + 源文件定义 = **单一节点**(ID 对齐);跨 TU 重复消除。
- 跨 DLL 调用 `AppExe::compute --calls--> UtilLib::helper` 标为 `cross_module_call`(EXTRACTED),节点带 `module` 标签,归属取**定义方**模块。
- baseline 的 INFERRED 调用被精确 EXTRACTED 边取代(去重、提升社区内聚度)。

**测试**:`tests/test_cpp_deep.py`(gate/ID 对齐/compile_db,无需 libclang)+ `tests/test_cpp_deep_integration.py`(多 DLL 端到端,libclang 缺失时自动 skip)。

**已知后续项**:`.sln` 顺序解析(当前用 rglob 全量扫 vcxproj,功能等价)、TU 级解析缓存、二进制 PE 导入/导出表(当前仅源码级,符合"全部有源码"的前提)。
Loading