forge-cpp-mcp 0.2.4__tar.gz → 0.2.5__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.
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/PKG-INFO +33 -3
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/README.md +32 -2
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/pyproject.toml +1 -1
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/__init__.py +1 -1
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/clangd/service.py +18 -5
- forge_cpp_mcp-0.2.5/src/forgemcp/cmake/build_tree.py +67 -0
- forge_cpp_mcp-0.2.5/src/forgemcp/cmake/file_api.py +229 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/cmake/models.py +17 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/cmake/profiles.py +21 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/cmake/service.py +218 -200
- forge_cpp_mcp-0.2.5/src/forgemcp/cmake/workspace.py +122 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/server.py +2 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/.gitignore +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/LICENSE +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/frontend/clangd-result.html +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/frontend/cmake-build.html +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/frontend/cmake-configure.html +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/frontend/cmake-profiles.html +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/frontend/cmake-run-test.html +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/frontend/cmake-test.html +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/frontend/package-lock.json +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/frontend/package.json +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/frontend/process-details.html +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/frontend/process-overview.html +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/frontend/src/clangd-navigation-view.js +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/frontend/src/clangd-result.js +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/frontend/src/clangd-view.js +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/frontend/src/cmake-build.js +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/frontend/src/cmake-configure.js +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/frontend/src/cmake-profiles.js +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/frontend/src/cmake-run-test.js +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/frontend/src/cmake-test.js +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/frontend/src/cmake-view.js +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/frontend/src/process-details.js +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/frontend/src/process-overview.js +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/frontend/src/process-status.js +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/frontend/src/process.css +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/frontend/src/shared/app.js +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/frontend/src/shared/copy-icon.js +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/frontend/src/shared/presentation.js +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/frontend/src/shared/result-resources.js +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/frontend/src/shared/result-view.js +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/frontend/src/shared/widget.css +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/frontend/src/source-view.js +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/frontend/src/toolsets.js +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/frontend/src/workspace-file.js +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/frontend/src/workspace-result.js +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/frontend/src/workspace-search.js +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/frontend/src/workspace-tree.js +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/frontend/src/workspace-view.js +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/frontend/tests/result-resources.test.js +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/frontend/tests/result-view.test.js +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/frontend/tests/workspace.test.js +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/frontend/toolsets.html +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/frontend/vite.config.js +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/frontend/workspace-file.html +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/frontend/workspace-result.html +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/frontend/workspace-search.html +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/frontend/workspace-tree.html +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/hatch_build.py +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/assets.py +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/clangd/__init__.py +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/clangd/errors.py +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/clangd/models.py +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/clangd/session.py +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/clangd/text.py +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/cmake/__init__.py +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/cmake/errors.py +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/cmake/text.py +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/completion.py +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/icons/clangd.LICENSE +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/icons/clangd.svg +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/icons/cmake.svg +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/icons/process.svg +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/icons/toolchain.svg +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/icons/workspace-edit.svg +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/icons/workspace-file.svg +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/icons/workspace-search.svg +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/icons/workspace.svg +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/markdown.py +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/process/__init__.py +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/process/errors.py +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/process/models.py +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/process/service.py +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/process/text.py +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/process/transcript.py +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/progress.py +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/text.py +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/__init__.py +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/discovery.py +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/errors.py +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/loader.py +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/providers/__init__.py +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/providers/system.py +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/providers/user.py +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/providers/visual_studio.py +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/service.py +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/spec.py +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/tools/__init__.py +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/tools/clang.py +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/tools/clang_cl.py +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/tools/clangd.py +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/tools/clangxx.py +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/tools/cmake.py +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/tools/cppvsdbg.py +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/tools/ctest.py +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/tools/gcc.py +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/tools/gdb.py +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/tools/git.py +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/tools/gxx.py +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/tools/link.py +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/tools/lld.py +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/tools/lld_link.py +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/tools/lldb_dap.py +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/tools/make.py +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/tools/msbuild.py +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/tools/msvc.py +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/tools/ninja.py +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/workspace/__init__.py +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/workspace/diff.py +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/workspace/errors.py +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/workspace/metadata.py +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/workspace/models.py +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/workspace/path.py +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/workspace/providers.py +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/workspace/service.py +0 -0
- {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/workspace/text.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: forge-cpp-mcp
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.5
|
|
4
4
|
Summary: A safe MCP server for structured C++ project development workflows.
|
|
5
5
|
Project-URL: Homepage, https://github.com/hardened-steel/ForgeMCP
|
|
6
6
|
Project-URL: Repository, https://github.com/hardened-steel/ForgeMCP
|
|
@@ -96,7 +96,7 @@ and install it in a virtual environment:
|
|
|
96
96
|
|
|
97
97
|
```powershell
|
|
98
98
|
python -m venv .venv
|
|
99
|
-
.\.venv\Scripts\python.exe -m pip install .\forge_cpp_mcp-0.2.
|
|
99
|
+
.\.venv\Scripts\python.exe -m pip install .\forge_cpp_mcp-0.2.5-py3-none-any.whl
|
|
100
100
|
.\.venv\Scripts\forgemcp.exe --help
|
|
101
101
|
```
|
|
102
102
|
|
|
@@ -108,7 +108,7 @@ The distribution name is `forge-cpp-mcp`; the Python module and server command a
|
|
|
108
108
|
After the package is published to PyPI, the install command can instead be:
|
|
109
109
|
|
|
110
110
|
```powershell
|
|
111
|
-
.\.venv\Scripts\python.exe -m pip install forge-cpp-mcp==0.2.
|
|
111
|
+
.\.venv\Scripts\python.exe -m pip install forge-cpp-mcp==0.2.5
|
|
112
112
|
```
|
|
113
113
|
|
|
114
114
|
## Setup from source
|
|
@@ -322,6 +322,36 @@ no source preview because explicit external reads require confirmation.
|
|
|
322
322
|
|
|
323
323
|
## CMake profiles
|
|
324
324
|
|
|
325
|
+
CMake owns which configurations can supply clangd compilation contexts. After a
|
|
326
|
+
successful configure/generate, ForgeMCP reads `cmakeFiles-v1` from a new CMake
|
|
327
|
+
File API reply and records SHA-256 hashes of that build directory's configuration
|
|
328
|
+
inputs in `project/` and `storage/`. Hashes and dependency lists live in memory;
|
|
329
|
+
restart the server and run `cmake_configure` to confirm contexts again. Existing
|
|
330
|
+
File API replies and `compile_commands.json` alone do not confirm current inputs.
|
|
331
|
+
|
|
332
|
+
Workspace writes, edits, moves, deletions, and directory creation check known
|
|
333
|
+
inputs at the operation boundary. Actual input changes withdraw affected contexts
|
|
334
|
+
and notify clangd through the CMake subscription; identical content and ordinary
|
|
335
|
+
source/header edits keep contexts available unless those files are CMake inputs.
|
|
336
|
+
Known preset files are treated conservatively without resolving preset inheritance
|
|
337
|
+
or includes. CONFIGURE_DEPENDS globs conservatively invalidate on structural
|
|
338
|
+
changes; older File API replies without glob information use this policy for all
|
|
339
|
+
configurations. No project-wide dependency scan or CMake/glob parser is used.
|
|
340
|
+
|
|
341
|
+
Run `cmake_configure` to restore outdated or unknown contexts. `cmake_build` is
|
|
342
|
+
also allowed for outdated configurations: a new successful File API generation
|
|
343
|
+
can restore them even when later compilation fails. A successful build without
|
|
344
|
+
regeneration does not refresh the input baseline. Failed configure/generate cannot
|
|
345
|
+
reactivate old artifacts, and invalidation leaves build trees and databases intact.
|
|
346
|
+
Unchanged contexts and database contents retain their clangd sessions.
|
|
347
|
+
|
|
348
|
+
Checks occur only during Workspace mutations, configure, and build. There are no
|
|
349
|
+
filesystem watchers, polling, or background scans, and no guarantee of detecting
|
|
350
|
+
external editor/process changes. External `root/` dependencies are omitted from
|
|
351
|
+
hash tracking without elicitation or reads; changes to those inputs require an
|
|
352
|
+
explicit configure. Native preset contexts still require a known managed build
|
|
353
|
+
directory and a compilation database.
|
|
354
|
+
|
|
325
355
|
`cmake_profiles` lists profiles. `cmake_configure`, `cmake_build`, and `cmake_test`
|
|
326
356
|
operate on all profiles unless a nonempty `profiles` list selects a subset.
|
|
327
357
|
Configuration must precede building, and building must precede testing. Profiles
|
|
@@ -71,7 +71,7 @@ and install it in a virtual environment:
|
|
|
71
71
|
|
|
72
72
|
```powershell
|
|
73
73
|
python -m venv .venv
|
|
74
|
-
.\.venv\Scripts\python.exe -m pip install .\forge_cpp_mcp-0.2.
|
|
74
|
+
.\.venv\Scripts\python.exe -m pip install .\forge_cpp_mcp-0.2.5-py3-none-any.whl
|
|
75
75
|
.\.venv\Scripts\forgemcp.exe --help
|
|
76
76
|
```
|
|
77
77
|
|
|
@@ -83,7 +83,7 @@ The distribution name is `forge-cpp-mcp`; the Python module and server command a
|
|
|
83
83
|
After the package is published to PyPI, the install command can instead be:
|
|
84
84
|
|
|
85
85
|
```powershell
|
|
86
|
-
.\.venv\Scripts\python.exe -m pip install forge-cpp-mcp==0.2.
|
|
86
|
+
.\.venv\Scripts\python.exe -m pip install forge-cpp-mcp==0.2.5
|
|
87
87
|
```
|
|
88
88
|
|
|
89
89
|
## Setup from source
|
|
@@ -297,6 +297,36 @@ no source preview because explicit external reads require confirmation.
|
|
|
297
297
|
|
|
298
298
|
## CMake profiles
|
|
299
299
|
|
|
300
|
+
CMake owns which configurations can supply clangd compilation contexts. After a
|
|
301
|
+
successful configure/generate, ForgeMCP reads `cmakeFiles-v1` from a new CMake
|
|
302
|
+
File API reply and records SHA-256 hashes of that build directory's configuration
|
|
303
|
+
inputs in `project/` and `storage/`. Hashes and dependency lists live in memory;
|
|
304
|
+
restart the server and run `cmake_configure` to confirm contexts again. Existing
|
|
305
|
+
File API replies and `compile_commands.json` alone do not confirm current inputs.
|
|
306
|
+
|
|
307
|
+
Workspace writes, edits, moves, deletions, and directory creation check known
|
|
308
|
+
inputs at the operation boundary. Actual input changes withdraw affected contexts
|
|
309
|
+
and notify clangd through the CMake subscription; identical content and ordinary
|
|
310
|
+
source/header edits keep contexts available unless those files are CMake inputs.
|
|
311
|
+
Known preset files are treated conservatively without resolving preset inheritance
|
|
312
|
+
or includes. CONFIGURE_DEPENDS globs conservatively invalidate on structural
|
|
313
|
+
changes; older File API replies without glob information use this policy for all
|
|
314
|
+
configurations. No project-wide dependency scan or CMake/glob parser is used.
|
|
315
|
+
|
|
316
|
+
Run `cmake_configure` to restore outdated or unknown contexts. `cmake_build` is
|
|
317
|
+
also allowed for outdated configurations: a new successful File API generation
|
|
318
|
+
can restore them even when later compilation fails. A successful build without
|
|
319
|
+
regeneration does not refresh the input baseline. Failed configure/generate cannot
|
|
320
|
+
reactivate old artifacts, and invalidation leaves build trees and databases intact.
|
|
321
|
+
Unchanged contexts and database contents retain their clangd sessions.
|
|
322
|
+
|
|
323
|
+
Checks occur only during Workspace mutations, configure, and build. There are no
|
|
324
|
+
filesystem watchers, polling, or background scans, and no guarantee of detecting
|
|
325
|
+
external editor/process changes. External `root/` dependencies are omitted from
|
|
326
|
+
hash tracking without elicitation or reads; changes to those inputs require an
|
|
327
|
+
explicit configure. Native preset contexts still require a known managed build
|
|
328
|
+
directory and a compilation database.
|
|
329
|
+
|
|
300
330
|
`cmake_profiles` lists profiles. `cmake_configure`, `cmake_build`, and `cmake_test`
|
|
301
331
|
operate on all profiles unless a nonempty `profiles` list selects a subset.
|
|
302
332
|
Configuration must precede building, and building must precede testing. Profiles
|
|
@@ -273,21 +273,32 @@ class ClangdService(ResultProvider[WorkspaceContext]):
|
|
|
273
273
|
async def configurations(self) -> list[CompilationContext]:
|
|
274
274
|
"""Return independent copies of currently usable compilation contexts."""
|
|
275
275
|
self.check_ready()
|
|
276
|
-
|
|
276
|
+
current = {context.id: context for context in await self.cmake.compilation_contexts()}
|
|
277
|
+
return [
|
|
278
|
+
context.model_copy(deep=True)
|
|
279
|
+
for context in self.contexts.values()
|
|
280
|
+
if current.get(context.id) == context
|
|
281
|
+
]
|
|
277
282
|
|
|
278
283
|
async def selection(self, configurations: Sequence[str]) -> list[CompilationContext]:
|
|
279
284
|
"""Validate requested configuration IDs, using all contexts for an empty selection."""
|
|
280
285
|
contexts = await self.configurations()
|
|
281
|
-
unknown = set(configurations) -
|
|
286
|
+
unknown = set(configurations) - {context.id for context in contexts}
|
|
282
287
|
if unknown:
|
|
283
|
-
raise ClangdError(
|
|
288
|
+
raise ClangdError(
|
|
289
|
+
f"Unavailable clangd configurations: {', '.join(sorted(unknown))}. "
|
|
290
|
+
"CMake inputs may be outdated or unconfirmed; run cmake_configure."
|
|
291
|
+
)
|
|
284
292
|
selected = [
|
|
285
293
|
context
|
|
286
294
|
for context in contexts
|
|
287
295
|
if not configurations or context.id in configurations
|
|
288
296
|
]
|
|
289
297
|
if not selected:
|
|
290
|
-
raise ClangdError(
|
|
298
|
+
raise ClangdError(
|
|
299
|
+
"No confirmed CMake configuration has both a compilation database and clangd. "
|
|
300
|
+
"CMake inputs may be outdated or unconfirmed; run cmake_configure."
|
|
301
|
+
)
|
|
291
302
|
return selected
|
|
292
303
|
|
|
293
304
|
@asynccontextmanager
|
|
@@ -867,7 +878,9 @@ class ClangdService(ResultProvider[WorkspaceContext]):
|
|
|
867
878
|
databases = {context.compilation_database for context in self.contexts.values()}
|
|
868
879
|
if any(change.path in databases for change in changes):
|
|
869
880
|
await self.cmake.publish_configurations(refresh=True)
|
|
870
|
-
|
|
881
|
+
# CMake's provider may still be checking this mutation. It never waits
|
|
882
|
+
# for analysis while holding its operation lock, so this order is safe.
|
|
883
|
+
await self.update_configurations(await self.cmake.compilation_contexts())
|
|
871
884
|
notifications: list[JsonValue] = [
|
|
872
885
|
{
|
|
873
886
|
"uri": self.workspace.resolve_workspace_path(change.path).as_uri(),
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Managed CMake build trees and direct test execution paths."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import shutil
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from forgemcp.workspace.path import WorkspacePath
|
|
8
|
+
|
|
9
|
+
from .errors import CMakeError
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def clear_build_directory(
|
|
13
|
+
directory: Path,
|
|
14
|
+
root: Path,
|
|
15
|
+
storage: Path,
|
|
16
|
+
protected: set[Path],
|
|
17
|
+
) -> None:
|
|
18
|
+
"""Remove an owned tree only after root, protected subtree, and link checks succeed."""
|
|
19
|
+
if (
|
|
20
|
+
directory == root
|
|
21
|
+
or not directory.resolve().is_relative_to(root)
|
|
22
|
+
or storage.is_relative_to(directory)
|
|
23
|
+
or any(path.is_relative_to(directory) for path in protected)
|
|
24
|
+
):
|
|
25
|
+
raise CMakeError(
|
|
26
|
+
"Build directory contains a protected path or escaped its root."
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
def raise_walk_error(error: OSError) -> None:
|
|
30
|
+
"""Reject incomplete pre-removal inspection rather than ignoring traversal failures."""
|
|
31
|
+
raise error
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
if not directory.exists():
|
|
35
|
+
return
|
|
36
|
+
for parent, directories, files in os.walk(
|
|
37
|
+
directory,
|
|
38
|
+
followlinks=False,
|
|
39
|
+
onerror=raise_walk_error,
|
|
40
|
+
):
|
|
41
|
+
for name in (*directories, *files):
|
|
42
|
+
item = Path(parent) / name
|
|
43
|
+
if item.is_symlink() or item.is_junction():
|
|
44
|
+
raise CMakeError("Build directory contains a link or junction.")
|
|
45
|
+
if not directory.resolve().is_relative_to(root):
|
|
46
|
+
raise CMakeError("Build directory escaped its root.")
|
|
47
|
+
shutil.rmtree(directory)
|
|
48
|
+
except OSError as error:
|
|
49
|
+
raise CMakeError("Cannot clear build directory.") from error
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def execution_path(reference: WorkspacePath, project: Path, storage: Path) -> Path:
|
|
53
|
+
"""Resolve a test executable or working directory without escaping its managed root."""
|
|
54
|
+
if reference.area == "root":
|
|
55
|
+
raise CMakeError("Test execution paths must be inside project/ or storage/.")
|
|
56
|
+
base = project if reference.area == "project" else storage
|
|
57
|
+
try:
|
|
58
|
+
if os.name == "nt" and any(
|
|
59
|
+
os.path.isreserved(part) for part in Path(reference.relative).parts
|
|
60
|
+
):
|
|
61
|
+
raise CMakeError(f"Test execution path contains a reserved name: {reference}.")
|
|
62
|
+
path = (base / reference.relative).resolve()
|
|
63
|
+
if not path.is_relative_to(base):
|
|
64
|
+
raise CMakeError(f"Test execution path resolves outside its root: {reference}.")
|
|
65
|
+
return path
|
|
66
|
+
except OSError as error:
|
|
67
|
+
raise CMakeError(f"Cannot resolve test execution path: {reference}.") from error
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
"""Read bounded CMake-owned replies and fingerprint managed configuration inputs."""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
from collections.abc import Iterable
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from forgemcp.workspace.path import WorkspacePath
|
|
11
|
+
|
|
12
|
+
from .errors import CMakeError
|
|
13
|
+
|
|
14
|
+
MAX_REPLY_BYTES = 16 * 1024 * 1024
|
|
15
|
+
MAX_INPUTS = 100_000
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class WorkspaceChange:
|
|
20
|
+
"""Known input hashes and path kinds needed to verify one controlled mutation."""
|
|
21
|
+
|
|
22
|
+
inputs: dict[WorkspacePath, str | None]
|
|
23
|
+
kinds: dict[WorkspacePath, str]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class ConfigurationInputs:
|
|
28
|
+
"""One generation's input hashes and conservative structural invalidation policy."""
|
|
29
|
+
|
|
30
|
+
checksums: dict[WorkspacePath, str | None] = field(default_factory=dict)
|
|
31
|
+
structural: bool = False
|
|
32
|
+
current: bool = False
|
|
33
|
+
|
|
34
|
+
def verify(self, project: Path, storage: Path) -> None:
|
|
35
|
+
"""Withdraw a confirmed baseline when an input changes or cannot safely be read."""
|
|
36
|
+
if not self.current:
|
|
37
|
+
return
|
|
38
|
+
try:
|
|
39
|
+
self.current = self.checksums == input_checksums(
|
|
40
|
+
self.checksums,
|
|
41
|
+
project,
|
|
42
|
+
storage,
|
|
43
|
+
)
|
|
44
|
+
except (CMakeError, OSError):
|
|
45
|
+
self.current = False
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def managed_path(path: Path, project: Path, storage: Path) -> WorkspacePath | None:
|
|
49
|
+
"""Qualify a path only when both its lexical and resolved targets stay managed."""
|
|
50
|
+
for area, base in (("storage", storage), ("project", project)):
|
|
51
|
+
if path.is_relative_to(base) and path.resolve().is_relative_to(base):
|
|
52
|
+
relative = path.relative_to(base).as_posix()
|
|
53
|
+
return WorkspacePath(f"{area}/{'' if relative == '.' else relative}")
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def checksum(path: Path) -> str | None:
|
|
58
|
+
"""Hash a file incrementally, representing a missing input without retaining contents."""
|
|
59
|
+
try:
|
|
60
|
+
with path.open("rb") as stream:
|
|
61
|
+
return hashlib.file_digest(stream, "sha256").hexdigest()
|
|
62
|
+
except FileNotFoundError:
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def read_reply(path: Path, project: Path, storage: Path) -> dict:
|
|
67
|
+
"""Read a size-bounded managed JSON reply without following references outside the roots."""
|
|
68
|
+
if managed_path(path, project, storage) is None:
|
|
69
|
+
raise CMakeError("CMake File API reply is outside managed roots.")
|
|
70
|
+
with path.open("rb") as stream:
|
|
71
|
+
data = stream.read(MAX_REPLY_BYTES + 1)
|
|
72
|
+
if len(data) > MAX_REPLY_BYTES:
|
|
73
|
+
raise CMakeError("CMake File API reply exceeds the size limit.")
|
|
74
|
+
value = json.loads(data)
|
|
75
|
+
if not isinstance(value, dict):
|
|
76
|
+
raise CMakeError("Expected a CMake File API object.")
|
|
77
|
+
return value
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def generation(directory: Path) -> str | None:
|
|
81
|
+
"""Identify the latest successful index, rejecting a newer CMake 4.1 error index."""
|
|
82
|
+
try:
|
|
83
|
+
reply = directory / ".cmake/api/v1/reply"
|
|
84
|
+
current = reply
|
|
85
|
+
while current != directory:
|
|
86
|
+
if current.is_symlink() or current.is_junction():
|
|
87
|
+
raise CMakeError("CMake File API cannot traverse links or junctions.")
|
|
88
|
+
current = current.parent
|
|
89
|
+
if not reply.exists():
|
|
90
|
+
return None
|
|
91
|
+
indexes = []
|
|
92
|
+
errors = []
|
|
93
|
+
for count, path in enumerate(reply.iterdir()):
|
|
94
|
+
if count >= MAX_INPUTS:
|
|
95
|
+
raise CMakeError("Too many CMake File API reply files.")
|
|
96
|
+
if path.name.startswith("index-") and path.suffix == ".json":
|
|
97
|
+
indexes.append(path.name)
|
|
98
|
+
elif path.name.startswith("error-") and path.suffix == ".json":
|
|
99
|
+
errors.append(path.name)
|
|
100
|
+
latest = max(indexes, default=None)
|
|
101
|
+
if latest is None or (errors and max(errors)[6:] >= latest[6:]):
|
|
102
|
+
return None
|
|
103
|
+
return latest
|
|
104
|
+
except OSError as error:
|
|
105
|
+
raise CMakeError("Cannot inspect CMake File API generation.") from error
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def read_inputs(
|
|
109
|
+
directory: Path,
|
|
110
|
+
index: str,
|
|
111
|
+
project: Path,
|
|
112
|
+
storage: Path,
|
|
113
|
+
) -> ConfigurationInputs:
|
|
114
|
+
"""Follow cmakeFiles-v1 from this generation and hash only project/storage inputs."""
|
|
115
|
+
try:
|
|
116
|
+
reply = directory / ".cmake/api/v1/reply"
|
|
117
|
+
reference = read_reply(reply / index, project, storage)["reply"][
|
|
118
|
+
"client-forgemcp"
|
|
119
|
+
]["cmakeFiles-v1"]
|
|
120
|
+
filename = reference["jsonFile"]
|
|
121
|
+
if not isinstance(filename, str) or Path(filename).name != filename:
|
|
122
|
+
raise CMakeError("Invalid CMake File API reply reference.")
|
|
123
|
+
files = read_reply(reply / filename, project, storage)
|
|
124
|
+
if files["kind"] != "cmakeFiles" or files["version"]["major"] != 1:
|
|
125
|
+
raise CMakeError("Unsupported CMake configuration inputs reply.")
|
|
126
|
+
if (
|
|
127
|
+
Path(files["paths"]["source"]).resolve() != project
|
|
128
|
+
or Path(files["paths"]["build"]).resolve() != directory.resolve()
|
|
129
|
+
):
|
|
130
|
+
raise CMakeError(
|
|
131
|
+
"CMake File API reply belongs to a different source/build tree."
|
|
132
|
+
)
|
|
133
|
+
inputs = files["inputs"]
|
|
134
|
+
if not isinstance(inputs, list) or not inputs or len(inputs) > MAX_INPUTS:
|
|
135
|
+
raise CMakeError("Invalid or excessive CMake configuration inputs.")
|
|
136
|
+
checksums = {}
|
|
137
|
+
for item in inputs:
|
|
138
|
+
native = Path(item["path"])
|
|
139
|
+
if not native.is_absolute():
|
|
140
|
+
native = project / native
|
|
141
|
+
native = Path(os.path.abspath(native))
|
|
142
|
+
qualified = managed_path(native, project, storage)
|
|
143
|
+
if qualified is not None:
|
|
144
|
+
checksums[qualified] = checksum(native)
|
|
145
|
+
for filename in ("CMakePresets.json", "CMakeUserPresets.json"):
|
|
146
|
+
native = project / filename
|
|
147
|
+
qualified = managed_path(native, project, storage)
|
|
148
|
+
if qualified is not None:
|
|
149
|
+
checksums[qualified] = checksum(native)
|
|
150
|
+
# Older replies cannot tell us whether CONFIGURE_DEPENDS globs exist.
|
|
151
|
+
return ConfigurationInputs(
|
|
152
|
+
checksums,
|
|
153
|
+
structural=files["version"].get("minor", 0) < 1
|
|
154
|
+
or bool(files.get("globsDependent")),
|
|
155
|
+
current=True,
|
|
156
|
+
)
|
|
157
|
+
except (OSError, ValueError, KeyError, TypeError) as error:
|
|
158
|
+
raise CMakeError(
|
|
159
|
+
"Cannot confirm CMake configuration inputs; run cmake_configure."
|
|
160
|
+
) from error
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def input_path(reference: WorkspacePath, project: Path, storage: Path) -> Path:
|
|
164
|
+
"""Resolve an input within its selected managed root without accessing external targets."""
|
|
165
|
+
if reference.area == "root":
|
|
166
|
+
raise CMakeError("Configuration inputs must be managed paths.")
|
|
167
|
+
base = project if reference.area == "project" else storage
|
|
168
|
+
path = base / reference.relative
|
|
169
|
+
if not path.resolve().is_relative_to(base):
|
|
170
|
+
raise CMakeError(f"Configuration input escaped its root: {reference}.")
|
|
171
|
+
return path
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def input_checksums(
|
|
175
|
+
paths: Iterable[WorkspacePath],
|
|
176
|
+
project: Path,
|
|
177
|
+
storage: Path,
|
|
178
|
+
) -> dict[WorkspacePath, str | None]:
|
|
179
|
+
"""Capture only hashes for the supplied managed input names."""
|
|
180
|
+
return {path: checksum(input_path(path, project, storage)) for path in paths}
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def path_kind(path: Path) -> str:
|
|
184
|
+
"""Capture a mutation target's existence and kind without traversing its subtree."""
|
|
185
|
+
if path.is_dir():
|
|
186
|
+
return "directory"
|
|
187
|
+
return "file" if path.exists() else "missing"
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def prepare_queries(query: Path) -> None:
|
|
191
|
+
"""Create owned File API queries in a validated build-tree directory."""
|
|
192
|
+
try:
|
|
193
|
+
query.mkdir(parents=True, exist_ok=True)
|
|
194
|
+
for name in ("codemodel-v2", "cache-v2", "toolchains-v1", "cmakeFiles-v1"):
|
|
195
|
+
target = query / name
|
|
196
|
+
if target.is_symlink() or target.is_junction():
|
|
197
|
+
raise CMakeError("CMake File API queries cannot be links.")
|
|
198
|
+
target.write_text("", encoding="utf-8")
|
|
199
|
+
except OSError as error:
|
|
200
|
+
raise CMakeError("Cannot prepare CMake File API queries.") from error
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def compilation_database(
|
|
204
|
+
directory: WorkspacePath,
|
|
205
|
+
native: Path,
|
|
206
|
+
) -> WorkspacePath | None:
|
|
207
|
+
"""Return the qualified compilation database name when that build tree contains one."""
|
|
208
|
+
path = native / "compile_commands.json"
|
|
209
|
+
return (
|
|
210
|
+
WorkspacePath(f"{directory}/compile_commands.json") if path.is_file() else None
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def read_generation(
|
|
215
|
+
directory: Path,
|
|
216
|
+
previous: str | None,
|
|
217
|
+
project: Path,
|
|
218
|
+
storage: Path,
|
|
219
|
+
) -> ConfigurationInputs | None:
|
|
220
|
+
"""Read inputs only when a command produced a new successful File API index."""
|
|
221
|
+
try:
|
|
222
|
+
latest = generation(directory)
|
|
223
|
+
if latest is None or latest == previous:
|
|
224
|
+
return None
|
|
225
|
+
return read_inputs(directory, latest, project, storage)
|
|
226
|
+
except OSError as error:
|
|
227
|
+
raise CMakeError(
|
|
228
|
+
"Cannot inspect CMake generation; run cmake_configure."
|
|
229
|
+
) from error
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""CMake profiles and typed execution results shared with analysis and presentation."""
|
|
2
2
|
|
|
3
3
|
from typing import Literal
|
|
4
|
+
from urllib.parse import quote
|
|
4
5
|
|
|
5
6
|
from pydantic import BaseModel, Field
|
|
6
7
|
|
|
@@ -22,6 +23,22 @@ class CMakeProfile(BaseModel):
|
|
|
22
23
|
generator: str | None = None
|
|
23
24
|
|
|
24
25
|
|
|
26
|
+
@property
|
|
27
|
+
def compilation_context(self) -> "CompilationContext | None":
|
|
28
|
+
"""Derive a candidate identity only for an unambiguous managed build directory."""
|
|
29
|
+
if self.build_directory is None or len(self.configure_presets) > 1:
|
|
30
|
+
return None
|
|
31
|
+
identifier = quote(self.name, safe="")
|
|
32
|
+
if self.configure_presets:
|
|
33
|
+
identifier += "/" + quote(self.configure_presets[0], safe="")
|
|
34
|
+
return CompilationContext(
|
|
35
|
+
id=identifier,
|
|
36
|
+
toolset_id=self.toolset_id,
|
|
37
|
+
build_directory=self.build_directory,
|
|
38
|
+
compilation_database=WorkspacePath(f"{self.build_directory}/compile_commands.json"),
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
25
42
|
class CompilationContext(BaseModel):
|
|
26
43
|
"""A successful build configuration with its toolset and compilation database."""
|
|
27
44
|
|
|
@@ -96,3 +96,24 @@ def parse_profiles(
|
|
|
96
96
|
)
|
|
97
97
|
profiles.append(profile)
|
|
98
98
|
return tuple(profiles)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def require_configuration(
|
|
102
|
+
cache: dict[str, str],
|
|
103
|
+
configuration: str | None,
|
|
104
|
+
) -> None:
|
|
105
|
+
"""Validate the requested configuration against single- or multi-config cache settings."""
|
|
106
|
+
choices = cache.get("CMAKE_CONFIGURATION_TYPES", "").split(";")
|
|
107
|
+
if choices != [""] and configuration not in choices:
|
|
108
|
+
raise CMakeError(
|
|
109
|
+
"Select an available multi-config configuration in the operator "
|
|
110
|
+
f"profile or preset: {', '.join(choices)}."
|
|
111
|
+
)
|
|
112
|
+
if (
|
|
113
|
+
choices == [""]
|
|
114
|
+
and configuration is not None
|
|
115
|
+
and cache.get("CMAKE_BUILD_TYPE") != configuration
|
|
116
|
+
):
|
|
117
|
+
raise CMakeError(
|
|
118
|
+
"The build directory has a different configuration; configure this profile first."
|
|
119
|
+
)
|