forge-cpp-mcp 0.2.3__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.3 → forge_cpp_mcp-0.2.5}/PKG-INFO +62 -5
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/README.md +61 -4
- forge_cpp_mcp-0.2.5/frontend/cmake-run-test.html +12 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/frontend/package.json +1 -1
- forge_cpp_mcp-0.2.5/frontend/src/cmake-run-test.js +8 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/frontend/vite.config.js +1 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/hatch_build.py +1 -1
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/pyproject.toml +1 -1
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/__init__.py +1 -1
- {forge_cpp_mcp-0.2.3 → 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/errors.py +5 -0
- forge_cpp_mcp-0.2.5/src/forgemcp/cmake/file_api.py +229 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/cmake/models.py +30 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/cmake/profiles.py +21 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/cmake/service.py +295 -184
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/cmake/text.py +19 -0
- forge_cpp_mcp-0.2.5/src/forgemcp/cmake/workspace.py +122 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/process/service.py +2 -1
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/server.py +3 -0
- forge_cpp_mcp-0.2.3/src/forgemcp/cmake/errors.py +0 -5
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/.gitignore +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/LICENSE +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/frontend/clangd-result.html +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/frontend/cmake-build.html +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/frontend/cmake-configure.html +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/frontend/cmake-profiles.html +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/frontend/cmake-test.html +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/frontend/package-lock.json +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/frontend/process-details.html +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/frontend/process-overview.html +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/frontend/src/clangd-navigation-view.js +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/frontend/src/clangd-result.js +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/frontend/src/clangd-view.js +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/frontend/src/cmake-build.js +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/frontend/src/cmake-configure.js +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/frontend/src/cmake-profiles.js +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/frontend/src/cmake-test.js +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/frontend/src/cmake-view.js +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/frontend/src/process-details.js +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/frontend/src/process-overview.js +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/frontend/src/process-status.js +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/frontend/src/process.css +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/frontend/src/shared/app.js +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/frontend/src/shared/copy-icon.js +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/frontend/src/shared/presentation.js +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/frontend/src/shared/result-resources.js +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/frontend/src/shared/result-view.js +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/frontend/src/shared/widget.css +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/frontend/src/source-view.js +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/frontend/src/toolsets.js +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/frontend/src/workspace-file.js +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/frontend/src/workspace-result.js +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/frontend/src/workspace-search.js +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/frontend/src/workspace-tree.js +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/frontend/src/workspace-view.js +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/frontend/tests/result-resources.test.js +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/frontend/tests/result-view.test.js +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/frontend/tests/workspace.test.js +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/frontend/toolsets.html +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/frontend/workspace-file.html +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/frontend/workspace-result.html +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/frontend/workspace-search.html +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/frontend/workspace-tree.html +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/assets.py +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/clangd/__init__.py +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/clangd/errors.py +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/clangd/models.py +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/clangd/session.py +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/clangd/text.py +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/cmake/__init__.py +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/completion.py +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/icons/clangd.LICENSE +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/icons/clangd.svg +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/icons/cmake.svg +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/icons/process.svg +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/icons/toolchain.svg +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/icons/workspace-edit.svg +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/icons/workspace-file.svg +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/icons/workspace-search.svg +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/icons/workspace.svg +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/markdown.py +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/process/__init__.py +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/process/errors.py +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/process/models.py +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/process/text.py +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/process/transcript.py +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/progress.py +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/text.py +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/__init__.py +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/discovery.py +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/errors.py +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/loader.py +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/providers/__init__.py +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/providers/system.py +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/providers/user.py +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/providers/visual_studio.py +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/service.py +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/spec.py +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/tools/__init__.py +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/tools/clang.py +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/tools/clang_cl.py +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/tools/clangd.py +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/tools/clangxx.py +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/tools/cmake.py +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/tools/cppvsdbg.py +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/tools/ctest.py +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/tools/gcc.py +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/tools/gdb.py +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/tools/git.py +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/tools/gxx.py +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/tools/link.py +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/tools/lld.py +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/tools/lld_link.py +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/tools/lldb_dap.py +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/tools/make.py +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/tools/msbuild.py +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/tools/msvc.py +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/tools/ninja.py +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/workspace/__init__.py +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/workspace/diff.py +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/workspace/errors.py +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/workspace/metadata.py +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/workspace/models.py +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/workspace/path.py +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/workspace/providers.py +0 -0
- {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/workspace/service.py +0 -0
- {forge_cpp_mcp-0.2.3 → 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
|
|
@@ -44,7 +44,8 @@ debugger modules are planned.
|
|
|
44
44
|
configuration selections use all available contexts.
|
|
45
45
|
- No prompts are currently registered.
|
|
46
46
|
- CMake tools list operator profiles, configure projects, build targets, and run
|
|
47
|
-
CTest.
|
|
47
|
+
CTest. `cmake_run_test` directly runs a project/storage test executable with
|
|
48
|
+
literal command-line arguments. Each tool has its own widget with Fields/JSON views,
|
|
48
49
|
copying and per-test results. Command logs are available through `process_get`. CMake syntax highlighting remains deferred.
|
|
49
50
|
- Tool `processes_overview` shows running and completed external development tools,
|
|
50
51
|
including exit codes and timeout interruptions. `process_get(process_id)` returns
|
|
@@ -75,7 +76,9 @@ without making additional tool calls. Process times are readable to seconds; JSO
|
|
|
75
76
|
copying preserve the original precision. A toolsets list shows summaries, while a
|
|
76
77
|
`toolset_get` result shows the details returned by that call.
|
|
77
78
|
|
|
78
|
-
ForgeMCP
|
|
79
|
+
ForgeMCP can directly run an explicitly selected test executable inside the
|
|
80
|
+
project or storage root; it does not expose shell commands or external executable
|
|
81
|
+
paths over MCP. Feature services use
|
|
79
82
|
the shared process service internally; its public MCP surface is read-only.
|
|
80
83
|
|
|
81
84
|
## Requirements
|
|
@@ -93,7 +96,7 @@ and install it in a virtual environment:
|
|
|
93
96
|
|
|
94
97
|
```powershell
|
|
95
98
|
python -m venv .venv
|
|
96
|
-
.\.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
|
|
97
100
|
.\.venv\Scripts\forgemcp.exe --help
|
|
98
101
|
```
|
|
99
102
|
|
|
@@ -105,7 +108,7 @@ The distribution name is `forge-cpp-mcp`; the Python module and server command a
|
|
|
105
108
|
After the package is published to PyPI, the install command can instead be:
|
|
106
109
|
|
|
107
110
|
```powershell
|
|
108
|
-
.\.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
|
|
109
112
|
```
|
|
110
113
|
|
|
111
114
|
## Setup from source
|
|
@@ -319,6 +322,36 @@ no source preview because explicit external reads require confirmation.
|
|
|
319
322
|
|
|
320
323
|
## CMake profiles
|
|
321
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
|
+
|
|
322
355
|
`cmake_profiles` lists profiles. `cmake_configure`, `cmake_build`, and `cmake_test`
|
|
323
356
|
operate on all profiles unless a nonempty `profiles` list selects a subset.
|
|
324
357
|
Configuration must precede building, and building must precede testing. Profiles
|
|
@@ -332,6 +365,30 @@ Full process transcripts remain in the process module; CMake results contain no
|
|
|
332
365
|
transcript links. The default command timeout is 600 seconds per execution and
|
|
333
366
|
can be overridden with the existing `ProcessTimeout` shape (`total`, `idle`).
|
|
334
367
|
|
|
368
|
+
`cmake_run_test(executable, arguments=null, cwd="project/", timeout={"total":600})`
|
|
369
|
+
runs one already-built test executable directly, without a profile or CTest
|
|
370
|
+
registration. Executable and working-directory paths use `project/...` or
|
|
371
|
+
`storage/...` and must resolve inside their respective roots. The working directory
|
|
372
|
+
must exist; storage is not created automatically. On Windows, batch files are not
|
|
373
|
+
supported. Arguments are a list of literal strings, preserving spaces and empty
|
|
374
|
+
strings; NUL is rejected. For example:
|
|
375
|
+
|
|
376
|
+
```json
|
|
377
|
+
{
|
|
378
|
+
"executable": "project/build/ninja-debug/fixture_tests.exe",
|
|
379
|
+
"arguments": ["--fail"],
|
|
380
|
+
"cwd": "project/build/ninja-debug",
|
|
381
|
+
"timeout": {"total": 30}
|
|
382
|
+
}
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
The process inherits the server environment, closes stdin, and waits for completion.
|
|
386
|
+
It does not replay toolset/preset environments or CTest properties such as
|
|
387
|
+
`WILL_FAIL`, fixtures, or per-test timeouts. Exit zero means success; a nonzero
|
|
388
|
+
exit or process failure is reported in `error`. The single structured result
|
|
389
|
+
contains `executable`, `arguments`, `cwd`, `error`, and `process_id`; logs are read
|
|
390
|
+
through `process_get`. No JUnit or framework case report is inferred from output.
|
|
391
|
+
|
|
335
392
|
With no explicit profiles:
|
|
336
393
|
|
|
337
394
|
- If `CMakePresets.json` or `CMakeUserPresets.json` exists, the automatic `presets`
|
|
@@ -19,7 +19,8 @@ debugger modules are planned.
|
|
|
19
19
|
configuration selections use all available contexts.
|
|
20
20
|
- No prompts are currently registered.
|
|
21
21
|
- CMake tools list operator profiles, configure projects, build targets, and run
|
|
22
|
-
CTest.
|
|
22
|
+
CTest. `cmake_run_test` directly runs a project/storage test executable with
|
|
23
|
+
literal command-line arguments. Each tool has its own widget with Fields/JSON views,
|
|
23
24
|
copying and per-test results. Command logs are available through `process_get`. CMake syntax highlighting remains deferred.
|
|
24
25
|
- Tool `processes_overview` shows running and completed external development tools,
|
|
25
26
|
including exit codes and timeout interruptions. `process_get(process_id)` returns
|
|
@@ -50,7 +51,9 @@ without making additional tool calls. Process times are readable to seconds; JSO
|
|
|
50
51
|
copying preserve the original precision. A toolsets list shows summaries, while a
|
|
51
52
|
`toolset_get` result shows the details returned by that call.
|
|
52
53
|
|
|
53
|
-
ForgeMCP
|
|
54
|
+
ForgeMCP can directly run an explicitly selected test executable inside the
|
|
55
|
+
project or storage root; it does not expose shell commands or external executable
|
|
56
|
+
paths over MCP. Feature services use
|
|
54
57
|
the shared process service internally; its public MCP surface is read-only.
|
|
55
58
|
|
|
56
59
|
## Requirements
|
|
@@ -68,7 +71,7 @@ and install it in a virtual environment:
|
|
|
68
71
|
|
|
69
72
|
```powershell
|
|
70
73
|
python -m venv .venv
|
|
71
|
-
.\.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
|
|
72
75
|
.\.venv\Scripts\forgemcp.exe --help
|
|
73
76
|
```
|
|
74
77
|
|
|
@@ -80,7 +83,7 @@ The distribution name is `forge-cpp-mcp`; the Python module and server command a
|
|
|
80
83
|
After the package is published to PyPI, the install command can instead be:
|
|
81
84
|
|
|
82
85
|
```powershell
|
|
83
|
-
.\.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
|
|
84
87
|
```
|
|
85
88
|
|
|
86
89
|
## Setup from source
|
|
@@ -294,6 +297,36 @@ no source preview because explicit external reads require confirmation.
|
|
|
294
297
|
|
|
295
298
|
## CMake profiles
|
|
296
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
|
+
|
|
297
330
|
`cmake_profiles` lists profiles. `cmake_configure`, `cmake_build`, and `cmake_test`
|
|
298
331
|
operate on all profiles unless a nonempty `profiles` list selects a subset.
|
|
299
332
|
Configuration must precede building, and building must precede testing. Profiles
|
|
@@ -307,6 +340,30 @@ Full process transcripts remain in the process module; CMake results contain no
|
|
|
307
340
|
transcript links. The default command timeout is 600 seconds per execution and
|
|
308
341
|
can be overridden with the existing `ProcessTimeout` shape (`total`, `idle`).
|
|
309
342
|
|
|
343
|
+
`cmake_run_test(executable, arguments=null, cwd="project/", timeout={"total":600})`
|
|
344
|
+
runs one already-built test executable directly, without a profile or CTest
|
|
345
|
+
registration. Executable and working-directory paths use `project/...` or
|
|
346
|
+
`storage/...` and must resolve inside their respective roots. The working directory
|
|
347
|
+
must exist; storage is not created automatically. On Windows, batch files are not
|
|
348
|
+
supported. Arguments are a list of literal strings, preserving spaces and empty
|
|
349
|
+
strings; NUL is rejected. For example:
|
|
350
|
+
|
|
351
|
+
```json
|
|
352
|
+
{
|
|
353
|
+
"executable": "project/build/ninja-debug/fixture_tests.exe",
|
|
354
|
+
"arguments": ["--fail"],
|
|
355
|
+
"cwd": "project/build/ninja-debug",
|
|
356
|
+
"timeout": {"total": 30}
|
|
357
|
+
}
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
The process inherits the server environment, closes stdin, and waits for completion.
|
|
361
|
+
It does not replay toolset/preset environments or CTest properties such as
|
|
362
|
+
`WILL_FAIL`, fixtures, or per-test timeouts. Exit zero means success; a nonzero
|
|
363
|
+
exit or process failure is reported in `error`. The single structured result
|
|
364
|
+
contains `executable`, `arguments`, `cwd`, `error`, and `process_id`; logs are read
|
|
365
|
+
through `process_get`. No JUnit or framework case report is inferred from output.
|
|
366
|
+
|
|
310
367
|
With no explicit profiles:
|
|
311
368
|
|
|
312
369
|
- If `CMakePresets.json` or `CMakeUserPresets.json` exists, the automatic `presets`
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Test executable result</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<main id="widget" aria-label="Test executable result"></main>
|
|
10
|
+
<script type="module" src="/src/cmake-run-test.js"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"private": true,
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
|
-
"build": "vite build --mode workspace && vite build --mode file && vite build --mode search && vite build --mode result && vite build --mode process && vite build --mode process-details && vite build --mode toolsets && vite build --mode cmake-profiles && vite build --mode cmake-configure && vite build --mode cmake-build && vite build --mode cmake-test && vite build --mode clangd",
|
|
6
|
+
"build": "vite build --mode workspace && vite build --mode file && vite build --mode search && vite build --mode result && vite build --mode process && vite build --mode process-details && vite build --mode toolsets && vite build --mode cmake-profiles && vite build --mode cmake-configure && vite build --mode cmake-build && vite build --mode cmake-test && vite build --mode cmake-run-test && vite build --mode clangd",
|
|
7
7
|
"test": "node --test tests/*.test.js"
|
|
8
8
|
},
|
|
9
9
|
"dependencies": {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { connectWidget } from "./shared/app.js";
|
|
2
|
+
import { cmakePresentation, renderCMakeValue } from "./cmake-view.js";
|
|
3
|
+
|
|
4
|
+
await connectWidget({
|
|
5
|
+
toolName: "cmake_run_test",
|
|
6
|
+
describe: (data) => cmakePresentation(data, "cmake_run_test"),
|
|
7
|
+
renderValue: renderCMakeValue,
|
|
8
|
+
});
|
|
@@ -31,7 +31,7 @@ class CustomBuildHook(BuildHookInterface):
|
|
|
31
31
|
Path(self.root) / "src" / "forgemcp" / "assets" / "clangd-result.html",
|
|
32
32
|
*(
|
|
33
33
|
Path(self.root) / "src" / "forgemcp" / "assets" / f"cmake-{kind}.html"
|
|
34
|
-
for kind in ("profiles", "configure", "build", "test")
|
|
34
|
+
for kind in ("profiles", "configure", "build", "test", "run-test")
|
|
35
35
|
),
|
|
36
36
|
]
|
|
37
37
|
artifacts = build_data.setdefault("artifacts", [])
|
|
@@ -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
|