Skip to content

Commit 02fea99

Browse files
feat: add dry-run and fix symbol repetition
1 parent 593f6f5 commit 02fea99

File tree

22 files changed

+1015
-251
lines changed

22 files changed

+1015
-251
lines changed
Lines changed: 54 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,59 @@
11
name: generate-docs
22

33
on:
4-
push:
5-
branches: ["main"]
6-
pull_request:
7-
workflow_dispatch:
4+
push:
5+
branches: ["main"]
6+
pull_request:
7+
workflow_dispatch:
88

99
jobs:
10-
generate:
11-
runs-on: windows-2025
12-
permissions:
13-
contents: write
14-
models: read
15-
16-
steps:
17-
- name: Checkout clore
18-
uses: actions/checkout@v6
19-
20-
- name: Setup pixi
21-
uses: prefix-dev/setup-pixi@v0.9.4
22-
with:
23-
pixi-version: v0.67.0
24-
activate-environment: true
25-
cache: true
26-
locked: true
27-
28-
- name: Restore compiler cache
29-
uses: actions/cache@v5
30-
with:
31-
path: .cache/sccache
32-
key: Windows-sccache-${{ github.sha }}
33-
restore-keys: |
34-
Windows-sccache-
35-
36-
- name: Build
37-
run: pixi run build
38-
39-
- name: Generate documentation
40-
env:
41-
OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
42-
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
43-
run: |
44-
./build/RelWithDebInfo/bin/clore `
45-
--compile-commands build/RelWithDebInfo/compile_commands.json `
46-
--project-root . `
47-
--output generated `
48-
--llm-model moonshotai/kimi-k2.5 `
49-
--config example.toml `
50-
--log-level info
51-
52-
- name: Package documentation
53-
run: Compress-Archive -Path generated/* -DestinationPath clore-docs.zip
54-
55-
- name: Upload documentation artifact
56-
uses: actions/upload-artifact@v7
57-
with:
58-
name: clore-docs
59-
path: clore-docs.zip
60-
retention-days: 30
61-
62-
- name: Stop sccache
63-
if: always()
64-
run: pixi run -- sccache --stop-server 2>$null
10+
generate:
11+
runs-on: windows-2025
12+
permissions:
13+
contents: write
14+
15+
steps:
16+
- name: Checkout clore
17+
uses: actions/checkout@v6
18+
19+
- name: Setup pixi
20+
uses: prefix-dev/setup-pixi@v0.9.4
21+
with:
22+
pixi-version: v0.67.0
23+
activate-environment: true
24+
cache: true
25+
locked: true
26+
27+
- name: Restore compiler cache
28+
uses: actions/cache@v5
29+
with:
30+
path: .cache/sccache
31+
key: Windows-sccache-${{ github.sha }}
32+
restore-keys: |
33+
Windows-sccache-
34+
35+
- name: Build
36+
run: pixi run build
37+
38+
- name: Generate documentation
39+
run: |
40+
./build/RelWithDebInfo/bin/clore `
41+
--compile-commands build/RelWithDebInfo/compile_commands.json `
42+
--project-root src `
43+
--output generated `
44+
--log-level info `
45+
--dry-run
46+
47+
- name: Package documentation
48+
run: Compress-Archive -Path generated/* -DestinationPath clore-docs.zip
49+
50+
- name: Upload documentation artifact
51+
uses: actions/upload-artifact@v7
52+
with:
53+
name: clore-docs
54+
path: clore-docs.zip
55+
retention-days: 30
56+
57+
- name: Stop sccache
58+
if: always()
59+
run: pixi run -- sccache --stop-server 2>$null

CMakeLists.txt

Lines changed: 20 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,14 @@ set(CMAKE_CXX_SCAN_FOR_MODULES OFF)
88
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
99

1010
include(GNUInstallDirs)
11+
include(CheckIPOSupported)
1112
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/lib")
1213
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/lib")
1314
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/bin")
1415

15-
option(CLORE_ENABLE_LTO "Enable ThinLTO for all targets" OFF)
16+
option(CLORE_ENABLE_LTO "Enable ThinLTO for all targets" ON)
1617
option(CLORE_OFFLINE_BUILD "Disable network downloads during configuration" OFF)
17-
option(CLORE_ENABLE_TEST "Build unit tests" OFF)
18+
option(CLORE_ENABLE_TEST "Build unit tests" ON)
1819

1920
# Global flags that apply to all targets (including FetchContent dependencies).
2021
if(NOT MSVC)
@@ -39,47 +40,31 @@ else()
3940
string(APPEND CMAKE_SHARED_LINKER_FLAGS " -Wl,--gc-sections")
4041
endif()
4142

43+
set(CLORE_ENABLE_IPO OFF)
44+
set(CLORE_USE_LTO_ARTIFACT OFF)
4245
if(CLORE_ENABLE_LTO)
43-
string(APPEND CMAKE_C_FLAGS " -flto=thin")
44-
string(APPEND CMAKE_CXX_FLAGS " -flto=thin")
45-
string(APPEND CMAKE_EXE_LINKER_FLAGS " -flto=thin")
46-
string(APPEND CMAKE_SHARED_LINKER_FLAGS " -flto=thin")
47-
string(APPEND CMAKE_MODULE_LINKER_FLAGS " -flto=thin")
46+
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
47+
message(STATUS "Interprocedural optimization disabled for Debug builds.")
48+
else()
49+
check_ipo_supported(RESULT CLORE_IPO_SUPPORTED OUTPUT CLORE_IPO_ERROR LANGUAGES C CXX)
50+
if(CLORE_IPO_SUPPORTED)
51+
set(CLORE_ENABLE_IPO ON)
52+
set(CLORE_USE_LTO_ARTIFACT ON)
53+
message(STATUS "Interprocedural optimization enabled for ${CMAKE_BUILD_TYPE} builds.")
54+
else()
55+
message(WARNING "LTO requested but IPO is not supported by the active toolchain: ${CLORE_IPO_ERROR}")
56+
endif()
57+
endif()
4858
endif()
4959

5060
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
51-
add_compile_options(-fsanitize=address)
52-
53-
if(CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC")
54-
# clang-cl (MSVC frontend): manually link ASan runtime since clang-cl
55-
# doesn't handle -fsanitize=address linking automatically.
56-
execute_process(
57-
COMMAND ${CMAKE_CXX_COMPILER} --print-resource-dir
58-
OUTPUT_VARIABLE CLANG_RESOURCE_DIR
59-
OUTPUT_STRIP_TRAILING_WHITESPACE
60-
)
61-
set(ASAN_LIB_PATH "${CLANG_RESOURCE_DIR}/lib/windows")
62-
link_directories(${ASAN_LIB_PATH})
63-
64-
set(ASAN_LINK_FLAGS "")
65-
list(APPEND ASAN_LINK_FLAGS "clang_rt.asan_dynamic-x86_64.lib")
66-
list(APPEND ASAN_LINK_FLAGS "/wholearchive:clang_rt.asan_dynamic_runtime_thunk-x86_64.lib")
67-
68-
foreach(flag ${ASAN_LINK_FLAGS})
69-
string(APPEND CMAKE_EXE_LINKER_FLAGS " ${flag}")
70-
string(APPEND CMAKE_SHARED_LINKER_FLAGS " ${flag}")
71-
string(APPEND CMAKE_MODULE_LINKER_FLAGS " ${flag}")
72-
endforeach()
61+
if(WIN32)
62+
message(STATUS "AddressSanitizer disabled on Windows Debug builds: the current MSVC-targeting clang toolchain and prebuilt LLVM static libraries are not ASan-safe in this repo.")
7363
else()
64+
add_compile_options(-fsanitize=address)
7465
string(APPEND CMAKE_EXE_LINKER_FLAGS " -fsanitize=address")
7566
string(APPEND CMAKE_SHARED_LINKER_FLAGS " -fsanitize=address")
7667
endif()
77-
78-
if(WIN32)
79-
# Disable Identical COMDAT Folding in Debug to avoid ASan ODR false positives.
80-
string(APPEND CMAKE_EXE_LINKER_FLAGS " -Wl,/OPT:NOICF")
81-
string(APPEND CMAKE_SHARED_LINKER_FLAGS " -Wl,/OPT:NOICF")
82-
endif()
8368
endif()
8469

8570
include("${PROJECT_SOURCE_DIR}/cmake/package.cmake")

clore.toml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[clore]
2+
language = "简体中文"
3+
4+
[filter]
5+
include = ["src/"]
6+
exclude = []
7+
8+
[[frontmatter.fields]]
9+
key = "sidebar_label"
10+
value = "Clore API"

cmake/llvm.cmake

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,21 @@ function(setup_llvm LLVM_VERSION)
55

66
set(LLVM_SETUP_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/.llvm/setup-llvm.json")
77
set(LLVM_SETUP_SCRIPT "${PROJECT_SOURCE_DIR}/scripts/setup-llvm.py")
8+
set(LLVM_ARTIFACT_BUILD_TYPE "${CMAKE_BUILD_TYPE}")
9+
if(WIN32 AND CMAKE_BUILD_TYPE STREQUAL "Debug")
10+
set(LLVM_ARTIFACT_BUILD_TYPE "RelWithDebInfo")
11+
message(STATUS "Windows Debug builds use the non-ASan RelWithDebInfo LLVM artifact because ASan is disabled for this toolchain.")
12+
endif()
813
set(LLVM_SETUP_ARGS
914
"--version" "${LLVM_VERSION}"
1015
"--build-type" "${CMAKE_BUILD_TYPE}"
16+
"--artifact-build-type" "${LLVM_ARTIFACT_BUILD_TYPE}"
1117
"--binary-dir" "${CMAKE_CURRENT_BINARY_DIR}"
1218
"--manifest" "${PROJECT_SOURCE_DIR}/config/llvm-manifest.json"
1319
"--output" "${LLVM_SETUP_OUTPUT}"
1420
)
1521

16-
if(CLORE_ENABLE_LTO)
22+
if(CLORE_USE_LTO_ARTIFACT)
1723
list(APPEND LLVM_SETUP_ARGS "--enable-lto")
1824
endif()
1925

scripts/setup-llvm.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,7 @@ def main() -> None:
259259
parser = argparse.ArgumentParser(description="Setup LLVM dependencies for CMake")
260260
parser.add_argument("--version", required=True)
261261
parser.add_argument("--build-type", required=True)
262+
parser.add_argument("--artifact-build-type")
262263
parser.add_argument("--binary-dir", required=True)
263264
parser.add_argument("--manifest", required=True)
264265
parser.add_argument("--install-path")
@@ -270,13 +271,18 @@ def main() -> None:
270271
log(
271272
"Args: "
272273
f"version={args.version}, build_type={args.build_type}, "
274+
f"artifact_build_type={args.artifact_build_type or args.build_type}, "
273275
f"binary_dir={args.binary_dir}, install_path={args.install_path or '(auto)'}, "
274276
f"enable_lto={args.enable_lto}, offline={args.offline}"
275277
)
276278
token = os.environ.get("GH_TOKEN") or os.environ.get("GITHUB_TOKEN")
277279
build_type = args.build_type
280+
artifact_build_type = args.artifact_build_type or build_type
278281
platform_name = detect_platform()
279-
log(f"Platform detected: {platform_name}, normalized build type: {build_type}")
282+
log(
283+
f"Platform detected: {platform_name}, normalized build type: {build_type}, "
284+
f"artifact build type: {artifact_build_type}"
285+
)
280286
manifest = read_manifest(Path(args.manifest))
281287

282288
binary_dir = Path(args.binary_dir).resolve()
@@ -304,7 +310,7 @@ def main() -> None:
304310
if install_path is None:
305311
needs_install = True
306312
artifact = pick_artifact(
307-
manifest, args.version, build_type, args.enable_lto, platform_name
313+
manifest, args.version, artifact_build_type, args.enable_lto, platform_name
308314
)
309315
log(f"Selected artifact: {artifact.get('filename')} for download")
310316
filename = artifact["filename"]
@@ -317,7 +323,7 @@ def main() -> None:
317323
install_path = install_root
318324
elif needs_install:
319325
artifact = pick_artifact(
320-
manifest, args.version, build_type, args.enable_lto, platform_name
326+
manifest, args.version, artifact_build_type, args.enable_lto, platform_name
321327
)
322328
log(f"Selected artifact: {artifact.get('filename')} for download")
323329
filename = artifact["filename"]

src/CMakeLists.txt

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ install(
3838

3939
if(CLORE_ENABLE_TEST)
4040
file(GLOB_RECURSE CLORE_TEST_SOURCES CONFIGURE_DEPENDS
41-
"${PROJECT_SOURCE_DIR}/tests/unit/*/*_tests.cpp"
41+
"${PROJECT_SOURCE_DIR}/tests/unit/*/*.cpp"
4242
)
4343

4444
add_executable(unit_tests
@@ -51,3 +51,11 @@ if(CLORE_ENABLE_TEST)
5151
)
5252
target_link_libraries(unit_tests PRIVATE clore::core eventide::zest eventide::deco)
5353
endif()
54+
55+
if(CLORE_ENABLE_IPO)
56+
set_property(TARGET clore-core PROPERTY INTERPROCEDURAL_OPTIMIZATION TRUE)
57+
set_property(TARGET clore PROPERTY INTERPROCEDURAL_OPTIMIZATION TRUE)
58+
if(CLORE_ENABLE_TEST)
59+
set_property(TARGET unit_tests PROPERTY INTERPROCEDURAL_OPTIMIZATION TRUE)
60+
endif()
61+
endif()

src/config/load.cpp

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,11 @@ auto load_config(std::string_view path) -> std::expected<TaskConfig, ConfigError
110110
namespace fs = std::filesystem;
111111

112112
auto config_path = fs::path(path);
113+
if(config_path.is_relative()) {
114+
config_path = fs::absolute(config_path);
115+
}
116+
config_path = config_path.lexically_normal();
117+
113118
if(!fs::exists(config_path)) {
114119
return std::unexpected(ConfigError{
115120
.message = std::format("configuration file not found: {}", path)});
@@ -123,7 +128,14 @@ auto load_config(std::string_view path) -> std::expected<TaskConfig, ConfigError
123128

124129
std::ostringstream ss;
125130
ss << file.rdbuf();
126-
return load_config_from_string(ss.str());
131+
132+
auto config = load_config_from_string(ss.str());
133+
if(!config.has_value()) {
134+
return config;
135+
}
136+
137+
config->workspace_root = config_path.parent_path().string();
138+
return config;
127139
}
128140

129141
auto load_config_from_string(std::string_view toml_content) -> std::expected<TaskConfig, ConfigError> {

src/config/normalize.cpp

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,29 @@ auto normalize(TaskConfig& config) -> std::expected<void, NormalizeError> {
1111
// Reject empty required path fields before any filesystem operations.
1212
// fs::absolute("") silently resolves to cwd, which would bypass validation.
1313
auto make_absolute = [](std::string& path,
14-
std::string_view field) -> std::expected<void, NormalizeError> {
14+
std::string_view field,
15+
const std::optional<fs::path>& base = std::nullopt)
16+
-> std::expected<void, NormalizeError> {
1517
if(path.empty()) {
1618
return std::unexpected(
1719
NormalizeError{.message = std::format("{} must not be empty", field)});
1820
}
1921
auto p = fs::path(path);
2022
if(p.is_relative()) {
21-
path = fs::absolute(p).string();
23+
p = base.has_value() ? (*base / p) : fs::absolute(p);
2224
}
23-
path = fs::path(path).lexically_normal().string();
25+
path = p.lexically_normal().string();
2426
return {};
2527
};
2628

29+
if(config.workspace_root.empty()) {
30+
config.workspace_root = fs::current_path().string();
31+
}
32+
if(auto r = make_absolute(config.workspace_root, "workspace_root"); !r.has_value()) {
33+
return r;
34+
}
35+
auto workspace_root = fs::path(config.workspace_root);
36+
2737
if(auto r = make_absolute(config.compile_commands_path, "compile_commands_path");
2838
!r.has_value()) {
2939
return r;
@@ -36,14 +46,18 @@ auto normalize(TaskConfig& config) -> std::expected<void, NormalizeError> {
3646
}
3747

3848
auto make_absolute_opt = [&](std::optional<std::string>& opt,
39-
std::string_view field) -> std::expected<void, NormalizeError> {
49+
std::string_view field,
50+
const std::optional<fs::path>& base = std::nullopt)
51+
-> std::expected<void, NormalizeError> {
4052
if(opt.has_value()) {
41-
return make_absolute(*opt, field);
53+
return make_absolute(*opt, field, base);
4254
}
4355
return {};
4456
};
4557

46-
if(auto r = make_absolute_opt(config.frontmatter.template_path, "frontmatter.template_path");
58+
if(auto r = make_absolute_opt(config.frontmatter.template_path,
59+
"frontmatter.template_path",
60+
workspace_root);
4761
!r.has_value()) {
4862
return r;
4963
}
@@ -60,8 +74,12 @@ auto normalize(TaskConfig& config) -> std::expected<void, NormalizeError> {
6074
normalize_separators(config.compile_commands_path);
6175
normalize_separators(config.project_root);
6276
normalize_separators(config.output_root);
77+
normalize_separators(config.workspace_root);
6378
for(auto& p : config.filter.include) normalize_separators(p);
6479
for(auto& p : config.filter.exclude) normalize_separators(p);
80+
if(config.frontmatter.template_path.has_value()) {
81+
normalize_separators(*config.frontmatter.template_path);
82+
}
6583

6684
return {};
6785
}

0 commit comments

Comments
 (0)