Skip to content
Merged
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
53 changes: 53 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,59 @@ jobs:
- name: Run nested static and shared installation round trips
run: cmake -P tests/install/test_nested_install.cmake

clangd:
name: clangd (${{ matrix.os }})
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
cxx: g++
compiler_id: GNU
- os: windows-latest
cxx: cl
compiler_id: MSVC
- os: macos-latest
cxx: clang++
compiler_id: AppleClang
runs-on: ${{ matrix.os }}
env:
CXX: ${{ matrix.cxx }}
steps:
- uses: actions/checkout@v7
- uses: ilammy/msvc-dev-cmd@0b201ec74fa43914dc39ae48a89fd1d8cb592756
if: runner.os == 'Windows'
- name: Locate Windows clangd
if: runner.os == 'Windows'
shell: pwsh
run: |
$llvm = Join-Path $env:ProgramFiles 'LLVM\bin'
if (-not (Test-Path (Join-Path $llvm 'clangd.exe'))) {
choco install llvm --yes --no-progress
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
}
$llvm | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
- name: Ensure Linux clangd
if: runner.os == 'Linux'
run: |
if ! command -v clangd >/dev/null; then
sudo apt-get update -qq
sudo apt-get install -y clangd
fi
- name: Ensure macOS clangd
if: runner.os == 'macOS'
run: |
if ! command -v clangd >/dev/null; then
brew install llvm
echo "$(brew --prefix llvm)/bin" >> "$GITHUB_PATH"
fi
- name: Check configure-only header commands and fresh clangd navigation
run: cmake -DCPP_LIBRARY_TEST_CLANGD=ON -DCPP_LIBRARY_TEST_COMPILER_ID=${{ matrix.compiler_id }} -P tests/setup/test_clangd.cmake
- name: Check existing setup behavior
run: cmake -P tests/setup/test_target_type.cmake
- name: Check existing package behavior
run: cmake -P tests/install/test_nested_install.cmake

integration-tests:
name: Integration Tests
runs-on: ubuntu-latest
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
.worktrees/
.claude/worktrees/
.tokensave
build/
54 changes: 0 additions & 54 deletions .vscode/tasks.json

This file was deleted.

132 changes: 132 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,47 @@ future toolkit-template changes. Prefer the shared template with only the
project-specific deltas above. Documentation is configured only for top-level
projects with `BUILD_DOCS` enabled.

The shared template uses `templates/DoxygenLayout.xml` for reference-first
navigation: grouped APIs appear under **Reference**, with namespace, class, file,
and other lookup pages under **Index**. Group descriptions precede member lists,
and group briefs remain visible even with `REPEAT_BRIEF = NO`. The compact
version-2 layout inherits unspecified page sections from Doxygen, avoiding a
version-specific copy of its entire default layout. Documentation generation
therefore requires **Doxygen 1.13.1 or newer**; non-documentation builds do not
require Doxygen.

`WARN_LAYOUT_FILE = NO` suppresses warnings about intentionally inherited layout
entries. It also suppresses other layout diagnostics, including malformed XML;
normal documentation builds therefore do not validate layout edits. Other
documentation warnings remain errors.

**Whenever the layout is updated**, temporarily enable layout warnings and review
all of them. Use these review-only overrides to see the complete warning list
rather than stopping at the first inherited-entry warning:

```cmake
DOCS_OPTIONS
"WARN_LAYOUT_FILE = YES"
"WARN_AS_ERROR = NO"
```

Reconfigure and build the `docs` target. Accept missing-entry warnings only for
sections intentionally inherited from Doxygen's defaults; fix malformed XML,
unknown entries, and other unexpected diagnostics. Then remove the review-only
overrides to restore `WARN_LAYOUT_FILE = NO` and `WARN_AS_ERROR = YES`.

The layout stays in the toolkit rather than being copied into each project.
Override it through the existing options when needed (quote paths containing
spaces):

```cmake
DOCS_OPTIONS
"LAYOUT_FILE = \"${CMAKE_CURRENT_SOURCE_DIR}/docs/doxygen/DoxygenLayout.xml\""
```

A project's custom `docs/Doxyfile` retains its own layout configuration; the shared
layout is not appended to or imposed on that override.

### `cpp_library_copy_runtime_dlls`

```cmake
Expand Down Expand Up @@ -686,6 +727,91 @@ The dependency provider (CMake 3.24+) tracks `find_package()` and `CPMAddPackage

**Solution**: This is a known clang-tidy issue ([CMake #22979](https://gitlab.kitware.com/cmake/cmake/-/issues/22979)) where clang-tidy doesn't properly recognize MSVC's `/EHsc` exception handling flag. cpp-library automatically detects this scenario and adds `--extra-arg=/EHsc` to `CMAKE_CXX_CLANG_TIDY` when both MSVC and clang-tidy are enabled. This workaround is applied transparently and only on MSVC platforms.

## clangd and VSCode

With `CMAKE_EXPORT_COMPILE_COMMANDS=ON` (already enabled by the supplied Ninja
presets), `cpp_library_setup()` registers each public header with an exact C++
consumer command in CMake's ordinary `compile_commands.json`. This also works for
header-only and nested dependency libraries without tests or implementation
sources. Commands use the owner's public/transitive usage requirements, including
requirements added after setup; private implementation flags and DLL-export
definitions are not copied. Dependencies never consume their downstream owners.

Indexing OBJECT targets are excluded from normal builds and are not installed or
exported. They do not change the production target's sources or public-header
FILE_SET. No database postprocessor or configure-time clangd dependency is needed.
Set `CPP_LIBRARY_ENABLE_CLANGD=OFF` to opt out. With database export disabled,
registration still validates ownership but creates no indexing targets or warnings.

For manually created local INTERFACE, STATIC or SHARED libraries, register explicit
absolute paths (a local alias is also accepted):

```cmake
cpp_library_add_clangd_headers(
TARGET my_library
HEADERS "${CMAKE_CURRENT_SOURCE_DIR}/include/my_library/api.hpp"
)
```

Repeated registration with one owner is idempotent; additional headers extend its
registration. Registering a header with two different owners fails with both names
and the path, rather than leaving clangd to choose an arbitrary command. Imported
targets and generator expressions in header paths are not supported. Explicit
generated headers can be registered before they exist, provided they are declared
`GENERATED`; headers needed immediately by the editor must actually be generated
during configure (for example with `configure_file()`). Registering a build-time
header does not run its generator or add default-build prerequisites.

### Editor setup and migration

Install CMake 3.24+, Ninja, the selected compiler/SDK, clangd, VSCode CMake Tools,
and the clangd extension. Dependencies must be available and configuration must
succeed. Select a configure preset with **CMake: Select Configure Preset**, then
run **CMake: Configure**. No build or previously opened includer is required:
restart clangd if necessary and open a registered public header first.

New projects receive `.vscode/settings.json` with:

```json
{
"cmake.useCMakePresets": "always",
"cmake.configureOnOpen": true,
"cmake.copyCompileCommands": "${workspaceFolder}/compile_commands.json"
}
```

CMake Tools copies the selected build tree's unmodified database to the workspace
root after successful configuration. Selecting another preset and configuring
refreshes that copy. No `cmake.postConfigureTask` is added, and clangd is not pinned
to any preset directory. CLI-only `cmake --preset ...` configuration generates the
build-tree database but does **not** invoke CMake Tools' root-copy action.

Ordinary configuration preserves an existing settings file byte-for-byte, including
custom copy destinations, post-configure hooks, and unrelated settings. For an
existing workspace, add the three keys above manually, reconciling your current
copy destination/hook rather than adding a competing database writer. The `init`
preset / `CPP_LIBRARY_FORCE_INIT=ON` intentionally regenerates templates, **including
settings.json**; back up custom settings before using it.

### Supported boundaries

Use a single-configuration Ninja or Makefile generator. Visual Studio and Xcode
generators are not supported for database export. Active multi-configuration
generators (including Ninja Multi-Config) are rejected: their duplicate per-header
configuration entries do not select clangd's active variant. Use single-config
Ninja or disable this feature instead.

Normal configuration-dependent usage requirements are evaluated by CMake at
generation time. Target-context-sensitive expressions evaluate in the private
indexing **consumer** context, not the producer's: custom consumer properties are
not automatically copied from the library. This is not a compilation-database
merger or a standalone-header validation build.

The focused CI regression covers Windows/MSVC, Linux/GCC and macOS/Apple Clang.
Local evidence from this change is Windows/MSVC; the other platforms require their
CI runs. Protocol checks use fresh clangd processes and require Python only for the
test harness, never for consuming projects.

## Development

To update cpp-library dependencies search for `[DEPENDENCY]`. Embed the marker in an HTML
Expand Down Expand Up @@ -715,9 +841,15 @@ cmake -P tests/install/CMakeLists.txt
cmake -P tests/install/test_provider_merge.cmake
cmake -P tests/install/test_nested_install.cmake
cmake -P tests/setup/test_target_type.cmake
cmake -P tests/setup/test_clangd.cmake
cmake -P tests/setup/test_setup_version_resolution.cmake
```

To require fresh-clangd protocol checks as CI does, run
`cmake -DCPP_LIBRARY_TEST_CLANGD=ON -P tests/setup/test_clangd.cmake`.
This requires clangd on `PATH` and a Python 3 interpreter; otherwise the default
script reports unavailable protocol checks as skipped.

The nested-install regression configures, builds, installs, and runs a downstream consumer,
checks independent and legacy installation controls, and verifies package-local dependency validation.
The consumer links only the parent target and calls its API, which requires a compiled static
Expand Down
9 changes: 9 additions & 0 deletions cmake/clangd/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# SPDX-License-Identifier: BSL-1.0
# A separate directory keeps header language overrides off production targets.
cmake_minimum_required(VERSION 3.24)
set_source_files_properties(${headers} PROPERTIES LANGUAGE CXX HEADER_FILE_ONLY FALSE)
add_library("${index_target}" OBJECT EXCLUDE_FROM_ALL ${headers})
set_target_properties("${index_target}" PROPERTIES
UNITY_BUILD OFF
EXPORT_COMPILE_COMMANDS ON)
target_link_libraries("${index_target}" PRIVATE "${owner}")
86 changes: 86 additions & 0 deletions cmake/cpp-library-clangd.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# SPDX-License-Identifier: BSL-1.0
include_guard(GLOBAL)

option(CPP_LIBRARY_ENABLE_CLANGD "Generate public-header consumer commands for clangd" ON)

# Register absolute public header paths, including declared generated headers.
function(cpp_library_add_clangd_headers)
cmake_parse_arguments(PARSE_ARGV 0 ARG "" "TARGET" "HEADERS")
if(ARG_UNPARSED_ARGUMENTS OR ARG_KEYWORDS_MISSING_VALUES OR
NOT ARG_TARGET OR NOT ARG_HEADERS)
message(FATAL_ERROR
"cpp_library_add_clangd_headers: expected TARGET <local-library> HEADERS <absolute-path>...")
endif()
if(NOT TARGET "${ARG_TARGET}")
message(FATAL_ERROR "cpp_library_add_clangd_headers: target '${ARG_TARGET}' does not exist")
endif()
get_target_property(owner "${ARG_TARGET}" ALIASED_TARGET)
if(NOT owner)
set(owner "${ARG_TARGET}")
endif()
get_target_property(imported "${owner}" IMPORTED)
get_target_property(type "${owner}" TYPE)
if(imported OR NOT type MATCHES "^(INTERFACE|STATIC|SHARED)_LIBRARY$")
message(FATAL_ERROR
"cpp_library_add_clangd_headers: '${owner}' must be a local INTERFACE, STATIC or SHARED library (not imported)")
endif()

set(headers)
foreach(header IN LISTS ARG_HEADERS)
if(NOT IS_ABSOLUTE "${header}" OR header MATCHES "\\$<")
message(FATAL_ERROR
"cpp_library_add_clangd_headers: '${header}' must be an absolute header path, not a generator expression")
endif()
cmake_path(NORMAL_PATH header)
set(key_path "${header}")
if(WIN32)
string(TOLOWER "${key_path}" key_path)
endif()
string(SHA256 key "${key_path}")
get_property(previous GLOBAL PROPERTY "_CPP_LIBRARY_CLANGD_OWNER_${key}")
if(previous AND NOT previous STREQUAL owner)
message(FATAL_ERROR
"cpp_library_add_clangd_headers: header '${header}' is registered to both '${previous}' and '${owner}'; assign a single owner")
endif()
set_property(GLOBAL PROPERTY "_CPP_LIBRARY_CLANGD_OWNER_${key}" "${owner}")
list(APPEND headers "${header}")
endforeach()
list(REMOVE_DUPLICATES headers)

if(NOT CPP_LIBRARY_ENABLE_CLANGD OR NOT CMAKE_EXPORT_COMPILE_COMMANDS)
return()
endif()
if(CMAKE_CONFIGURATION_TYPES)
message(FATAL_ERROR
"cpp_library_add_clangd_headers: multi-configuration generators produce ambiguous header commands; use single-config Ninja/Makefiles or set CPP_LIBRARY_ENABLE_CLANGD=OFF")
endif()
if(NOT CMAKE_GENERATOR MATCHES "(Ninja|Makefiles|WMake)")
message(WARNING
"cpp_library_add_clangd_headers: '${CMAKE_GENERATOR}' cannot export supported header commands; use a Ninja/Makefile generator or set CPP_LIBRARY_ENABLE_CLANGD=OFF")
return()
endif()

get_property(index_target TARGET "${owner}" PROPERTY _CPP_LIBRARY_CLANGD_TARGET)
if(NOT index_target)
string(SHA256 owner_key "${owner}")
string(SUBSTRING "${owner_key}" 0 12 owner_key)
set(index_target "_cpp_library_clangd_${owner_key}")
if(TARGET "${index_target}")
message(FATAL_ERROR
"cpp_library_add_clangd_headers: reserved indexing target '${index_target}' already exists")
endif()
set(index_binary "${CMAKE_CURRENT_BINARY_DIR}/clangd/${owner_key}")
set_property(TARGET "${owner}" PROPERTY _CPP_LIBRARY_CLANGD_TARGET "${index_target}")
set_property(TARGET "${owner}" PROPERTY _CPP_LIBRARY_CLANGD_DIRECTORY "${index_binary}")
add_subdirectory("${CMAKE_CURRENT_FUNCTION_LIST_DIR}/clangd" "${index_binary}" EXCLUDE_FROM_ALL)
else()
get_property(index_binary TARGET "${owner}" PROPERTY _CPP_LIBRARY_CLANGD_DIRECTORY)
get_target_property(registered "${index_target}" SOURCES)
list(REMOVE_ITEM headers ${registered})
if(headers)
set_source_files_properties(${headers} DIRECTORY "${index_binary}"
PROPERTIES LANGUAGE CXX HEADER_FILE_ONLY FALSE)
target_sources("${index_target}" PRIVATE ${headers})
endif()
endif()
endfunction()
Loading
Loading