diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..a85e1d4 --- /dev/null +++ b/.clang-format @@ -0,0 +1,139 @@ +AccessModifierOffset: -4 +AlignAfterOpenBracket: BlockIndent +AlignArrayOfStructures: Left +AlignConsecutiveAssignments: false +AlignConsecutiveBitFields: AcrossEmptyLinesAndComments +AlignConsecutiveDeclarations: false +AlignConsecutiveMacros: AcrossEmptyLinesAndComments +AlignEscapedNewlines: Left +AlignOperands: Align +AlignTrailingComments: + Kind: Leave +AllowAllParametersOfDeclarationOnNextLine: false +AllowShortBlocksOnASingleLine: Always +AllowShortCaseLabelsOnASingleLine: true +AllowShortEnumsOnASingleLine: true +AllowShortFunctionsOnASingleLine: Inline +AllowShortIfStatementsOnASingleLine: WithoutElse +AllowShortLambdasOnASingleLine: Inline +AllowShortLoopsOnASingleLine: true +AlwaysBreakAfterDefinitionReturnType: None +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: true +AlwaysBreakTemplateDeclarations: Yes +BinPackArguments: false +BinPackParameters: false +BitFieldColonSpacing: Both +BraceWrapping: + AfterClass: true + AfterControlStatement: true + AfterEnum: true + AfterFunction: true + AfterNamespace: false + AfterObjCDeclaration: true + AfterStruct: true + AfterUnion: true + BeforeCatch: true + BeforeElse: true + IndentBraces: false + SplitEmptyFunction: true + SplitEmptyNamespace: true + SplitEmptyRecord: true +BreakAfterAttributes: Never +BreakBeforeBinaryOperators: NonAssignment +BreakBeforeBraces: Custom +BreakBeforeConceptDeclarations: Always +BreakBeforeInlineASMColon: OnlyMultiline +BreakBeforeTernaryOperators: true +BreakConstructorInitializers: BeforeComma +BreakInheritanceList: AfterComma +BreakStringLiterals: true +ColumnLimit: 128 +CommentPragmas: '^ IWYU pragma:' +CompactNamespaces: true +ConstructorInitializerIndentWidth: 2 +ContinuationIndentWidth: 4 +Cpp11BracedListStyle: true +DerivePointerAlignment: false +DisableFormat: false +EmptyLineAfterAccessModifier: Never +EmptyLineBeforeAccessModifier: Always +FixNamespaceComments: true +ForEachMacros: +- foreach +- Q_FOREACH +- BOOST_FOREACH +IncludeBlocks: Preserve +IncludeCategories: +- Priority: 2 + Regex: ^"(llvm|llvm-c|clang|clang-c)/ +- Priority: 3 + Regex: ^(<|"(gtest|gmock|isl|json)/) +- Priority: 1 + Regex: .* +IncludeIsMainRegex: (Test)?$ +IndentAccessModifiers: false +IndentCaseBlocks: false +IndentCaseLabels: false +IndentExternBlock: NoIndent +IndentGotoLabels: false +IndentPPDirectives: AfterHash +IndentRequiresClause: false +IndentWidth: 4 +IndentWrappedFunctionNames: true +InsertBraces: true +InsertNewlineAtEOF: true +IntegerLiteralSeparator: + Binary: 8 + Decimal: 3 + Hex: 4 +KeepEmptyLinesAtTheStartOfBlocks: false +LambdaBodyIndentation: Signature +Language: Cpp +LineEnding: DeriveLF +MacroBlockBegin: '' +MacroBlockEnd: '' +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: None +PPIndentWidth: 1 +PackConstructorInitializers: CurrentLine +PointerAlignment: Left +QualifierAlignment: Custom +QualifierOrder: ['static', 'inline', 'type', 'const', 'volatile'] +ReferenceAlignment: Pointer +ReflowComments: true +RemoveBracesLLVM: false +RemoveSemicolon: true +RequiresClausePosition: OwnLine +RequiresExpressionIndentation: OuterScope +SeparateDefinitionBlocks: Always +ShortNamespaceLines: 0 +SortIncludes: CaseSensitive +SortUsingDeclarations: true +SpaceAfterCStyleCast: true +SpaceAfterLogicalNot: false +SpaceAfterTemplateKeyword: true +SpaceAroundPointerQualifiers: Default +SpaceBeforeAssignmentOperators: true +SpaceBeforeCaseColon: false +SpaceBeforeCpp11BracedList: false +SpaceBeforeCtorInitializerColon: true +SpaceBeforeInheritanceColon: true +SpaceBeforeParens: ControlStatements +SpaceBeforeRangeBasedForLoopColon: true +SpaceBeforeSquareBrackets: false +SpaceInEmptyBlock: false +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 1 +SpacesInAngles: false +SpacesInCStyleCastParentheses: false +SpacesInConditionalStatement: false +SpacesInContainerLiterals: false +SpacesInLineCommentPrefix: + Minimum: 1 + Maximum: 1 +SpacesInParentheses: false +SpacesInSquareBrackets: false +Standard: c++20 +TabWidth: 8 +UseTab: Never diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 0000000..b0ad0ff --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,73 @@ +Checks: >- + *, + -abseil-*, + -altera-*, + -android-*, + -boost-*, + -fuchsia-*, + -google-*, + -llvm*, + -modernize-use-trailing-return-type, + -zircon-*, + -readability-else-after-return, + -readability-static-accessed-through-instance, + -readability-avoid-const-params-in-decls, + -cppcoreguidelines-non-private-member-variables-in-classes, + -cppcoreguidelines-pro-bounds-constant-array-index, + -performance-enum-size, + -misc-non-private-member-variables-in-classes, + -misc-include-cleaner +WarningsAsErrors: '*' +HeaderFilterRegex: '' +FormatStyle: 'file' +CheckOptions: +- key: readability-identifier-naming.ClassCase + value: CamelCase +- key: readability-identifier-naming.ClassConstantCase + value: lower_case +- key: readability-identifier-naming.ClassMemberCase + value: lower_case +- key: readability-identifier-naming.ClassMethodCase + value: lower_case +- key: readability-identifier-naming.ConceptCase + value: CamelCase +- key: readability-identifier-naming.EnumCase + value: CamelCase +- key: readability-identifier-naming.EnumConstantCase + value: lower_case +- key: readability-identifier-naming.FunctionCase + value: lower_case +- key: readability-identifier-naming.GlobalConstantCase + value: lower_case +- key: readability-identifier-naming.GlobalFunctionCase + value: lower_case +- key: readability-identifier-naming.GlobalVariableCase + value: lower_case +- key: readability-identifier-naming.IgnoreMainLikeFunctions + value: true +- key: readability-identifier-naming.MacroDefinitionCase + value: UPPER_CASE +- key: readability-identifier-naming.MemberCase + value: lower_case +- key: readability-identifier-naming.MethodCase + value: lower_case +- key: readability-identifier-naming.NamespaceCase + value: lower_case +- key: readability-identifier-naming.ParameterCase + value: lower_case +- key: readability-identifier-naming.PrivateMemberSuffix + value: _ +- key: readability-identifier-naming.ProtectedMemberSuffix + value: _ +- key: readability-identifier-naming.PublicMemberSuffix + value: '' +- key: readability-identifier-naming.StructCase + value: CamelCase +- key: readability-identifier-naming.TemplateParameterCase + value: CamelCase +- key: readability-identifier-naming.TypeAliasCase + value: CamelCase +- key: readability-identifier-naming.TypedefCase + value: CamelCase +- key: readability-identifier-naming.VariableCase + value: lower_case diff --git a/.clangd b/.clangd new file mode 100644 index 0000000..b85f560 --- /dev/null +++ b/.clangd @@ -0,0 +1,6 @@ +InlayHints: + BlockEnd: No + Designators: Yes + ParameterNames: Yes + DeducedTypes: Yes + DefaultArguments: No diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..7000872 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,55 @@ +{ + "name": "C++ Devcontainer", + "image": "dhglennvl/cpp-devcontainer:latest", + "capAdd": [ + "SYS_PTRACE" + ], + "securityOpt": [ + "seccomp=unconfined" + ], + "customizations": { + "vscode": { + "settings": { + "C_Cpp.intelliSenseEngine": "disabled", + "C_Cpp.default.configurationProvider": "llvm-vs-code-extensions.vscode-clangd", + "clangd.arguments": [ + "--log=verbose", + "--pretty", + "--background-index", + "--clang-tidy", + "--header-insertion=never", + "--completion-style=detailed", + "--compile-commands-dir=${workspaceFolder}/build/", + "--enable-config" + ], + "cmake.debugConfig": { + "name": "Debug with LLDB", + "type": "lldb-dap", + "request": "launch", + "runInTerminal": true, + "program": "${command:cmake.launchTargetPath}", + "args": [], + "cwd": "${workspaceFolder}", + "stopOnEntry": false, + "env": { + "LSAN_OPTIONS": "detect_leaks=0" + }, + "initCommands": [ + "command source ${workspaceFolder}/.lldbinit", + "breakpoint set --name main" + ] + }, + "plantuml.server": "https://www.plantuml.com/plantuml" + }, + "extensions": [ + "ms-vscode.cmake-tools", + "llvm-vs-code-extensions.lldb-dap", + "llvm-vs-code-extensions.vscode-clangd", + "cheshirekow.cmake-format", + "DavidAnson.vscode-markdownlint", + "jebbs.plantuml" + ] + } + }, + "remoteUser": "user" +} diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..1e2c9d2 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,37 @@ +############################### +# Git Line Endings # +############################### + +* text=auto eol=lf +*.{cmd,[cC][mM][dD]} text eol=crlf +*.{bat,[bB][aA][tT]} text eol=crlf +*.{vcxproj,vcxproj.filters} text eol=crlf + +############################### +# Git Large File System (LFS) # +############################### + +# Archives +#*.7z filter=lfs diff=lfs merge=lfs -text +#*.br filter=lfs diff=lfs merge=lfs -text +#*.gz filter=lfs diff=lfs merge=lfs -text +#*.tar filter=lfs diff=lfs merge=lfs -text +#*.zip filter=lfs diff=lfs merge=lfs -text + +# Documents +#*.pdf filter=lfs diff=lfs merge=lfs -text + +# Images +#*.gif filter=lfs diff=lfs merge=lfs -text +#*.ico filter=lfs diff=lfs merge=lfs -text +#*.jpg filter=lfs diff=lfs merge=lfs -text +#*.pdf filter=lfs diff=lfs merge=lfs -text +#*.png filter=lfs diff=lfs merge=lfs -text +#*.psd filter=lfs diff=lfs merge=lfs -text +#*.webp filter=lfs diff=lfs merge=lfs -text + +# Fonts +#*.woff2 filter=lfs diff=lfs merge=lfs -text + +# Other +#*.exe filter=lfs diff=lfs merge=lfs -text diff --git a/.github/workflows/build-test.yaml b/.github/workflows/build-test.yaml new file mode 100644 index 0000000..e703d0d --- /dev/null +++ b/.github/workflows/build-test.yaml @@ -0,0 +1,117 @@ +name: Build and Test +on: + push: + branches: + - main + pull_request: + branches: + - main +env: + GHCR_URL_WITH_UPPERCASE: ghcr.io/${{ github.repository }} +jobs: + build-devcontainer-image: + name: Build devcontainer + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - name: Checkout repository + uses: actions/checkout@v6 + - name: Convert GHCR url to lower case + run: echo "GHCR_URL=${GHCR_URL_WITH_UPPERCASE@L}" >> ${GITHUB_ENV} + - name: Login to Github Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Pre-build devcontainer image + uses: devcontainers/ci@v0.3 + with: + imageName: ${{ env.GHCR_URL }} + cacheFrom: ${{ env.GHCR_URL }} + push: always + make-release-build: + name: Make release build + needs: build-devcontainer-image + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v6 + - name: Convert GHCR url to lower case + run: echo "GHCR_URL=${GHCR_URL_WITH_UPPERCASE@L}" >> ${GITHUB_ENV} + - name: Login to Github Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Make release build + uses: devcontainers/ci@v0.3 + with: + cacheFrom: ${{ env.GHCR_URL }} + push: never + runCmd: | + cmake --preset clang-release + cmake --build --preset clang-release-build + make-debug-build: + name: Make debug build + needs: build-devcontainer-image + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v6 + - name: Convert GHCR url to lower case + run: echo "GHCR_URL=${GHCR_URL_WITH_UPPERCASE@L}" >> ${GITHUB_ENV} + - name: Login to Github Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Make debug build + uses: devcontainers/ci@v0.3 + with: + cacheFrom: ${{ env.GHCR_URL }} + push: never + runCmd: | + cmake --preset clang-debug + cmake --build --preset clang-debug-build + - name: Archive debug build + run: tar cf clang-debug.tar build/clang-debug + - name: Upload debug build artifact + uses: actions/upload-artifact@v6 + with: + name: clang-debug-build + path: clang-debug.tar + retention-days: 1 + include-hidden-files: true + run-tests: + name: Run tests + needs: make-debug-build + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v6 + - name: Convert GHCR url to lower case + run: echo "GHCR_URL=${GHCR_URL_WITH_UPPERCASE@L}" >> ${GITHUB_ENV} + - name: Login to Github Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Download debug build artifact + uses: actions/download-artifact@v7 + with: + name: clang-debug-build + - name: Extract debug build + run: tar xf clang-debug.tar + - name: Run tests + uses: devcontainers/ci@v0.3 + with: + cacheFrom: ${{ env.GHCR_URL }} + push: never + runCmd: | + ctest --preset clang-debug-test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f4a2026 --- /dev/null +++ b/.gitignore @@ -0,0 +1,38 @@ +# Build directories and binary files +build/ +out/ +cmake-build-*/ +conan-cache/ +vcpkg_installed/ +venv/ +Testing/ + +# User specific settings +CMakeUserPresets.json + +# IDE files +.vs/ +.idea/ +.vscode/* +!.vscode/tasks.json +!.vscode/launch.json +*.swp +*~ +_ReSharper* +*.log + +# OS Generated Files +.DS_Store +.AppleDouble +.LSOverride +._* +.Spotlight-V100 +.Trashes +.Trash-* +$RECYCLE.BIN/ +.TemporaryItems +ehthumbs.db +Thumbs.db + +# Generated files +.lldbinit diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..0c0b1fe --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,22 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Debug Tests with LLDB", + "type": "lldb-dap", + "request": "launch", + "presentation": { + "hidden": true + }, + "program": "${cmake.testProgram}", + "args": [ + "${cmake.testArgs}" + ], + "cwd": "${cmake.testWorkingDirectory}", + "stopOnEntry": true, + "initCommands": [ + "command source ${workspaceFolder}/.lldbinit" + ] + } + ] +} diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..1a5ec44 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,15 @@ +cmake_minimum_required(VERSION 4.2 FATAL_ERROR) + +# workaround clang bug +set(CMAKE_CXX_STDLIB_MODULES_JSON "/usr/lib/llvm-21/lib/libc++.modules.json") + +include("${CMAKE_CURRENT_LIST_DIR}/cmake/cpprog.cmake") + +cpprog_init() + +project(CppProjectTemplate VERSION 0.0.1 LANGUAGES C CXX) + +cpprog_configure_project() + +add_subdirectory(src) +add_subdirectory(tests) diff --git a/CMakePresets.json b/CMakePresets.json new file mode 100644 index 0000000..5d883c6 --- /dev/null +++ b/CMakePresets.json @@ -0,0 +1,59 @@ +{ + "version": 6, + "cmakeMinimumRequired": { + "major": 4, + "minor": 2, + "patch": 0 + }, + "configurePresets": [ + { + "name": "vcpkg", + "hidden": true, + "generator": "Ninja", + "binaryDir": "${sourceDir}/build/${presetName}", + "toolchainFile": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake", + "cacheVariables": { + "VCPKG_OVERLAY_TRIPLETS": "${sourceDir}/cmake" + } + }, + { + "name": "x64-linux-clang", + "hidden": true, + "inherits": "vcpkg", + "cacheVariables": { + "VCPKG_TARGET_TRIPLET": "x64-linux-clang-libcxx", + "VCPKG_CHAINLOAD_TOOLCHAIN_FILE": "${sourceDir}/cmake/x64-linux-clang-libcxx.toolchain.cmake" + } + }, + { + "name": "clang-debug", + "inherits": "x64-linux-clang", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug" + } + }, + { + "name": "clang-release", + "inherits": "x64-linux-clang", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release" + } + } + ], + "buildPresets": [ + { + "name": "clang-debug-build", + "configurePreset": "clang-debug" + }, + { + "name": "clang-release-build", + "configurePreset": "clang-release" + } + ], + "testPresets": [ + { + "name": "clang-debug-test", + "configurePreset": "clang-debug" + } + ] +} diff --git a/README.md b/README.md index 01d49f7..7904838 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,91 @@ -# cpp-project-template -C++ Project Template +# C++ Project Template + +## Devcontainer + +Basic C and C++ devcontainer with cmake, compiler warnings, clang-tidy and clang-format. + +## Installed tools + +* ccache +* clang +* clang-tidy +* clang-format +* cmake +* cppcheck +* lldb +* vcpkg + +### Supported IDEs + +* vscode +* clion + +## Adding libraries and executables + +* Create a new directory for each module in the `src` directory +* Add `CMakeLists.txt` in the new directory + +```cmake +cpprog_add_library( + TARGET exercise_1_lib # library will be called exercise_1_lib + CXX_MODULES # module source files here + "my_module_1.cpp" + "my_module_2.cpp" + DEPENDENCIES # libraries on which the library depends + datetime +) +``` + +```cmake +cpprog_add_executable( + TARGET exercise_1 # executable will be called exercise_1 + CXX_MODULES # module source files here + "my_module_1.cpp" + "my_module_2.cpp" + CXX_SOURCES # old-style source files here + "main.cpp" + DEPENDENCIES # libraries on which the exercise depends + exercise_1_lib +) +``` + +* Add the new directory to the `src/CMakeLists.txt` file after the line `add_subdirectory(cpprog)` + +```cmake +add_subdirectory(my_new_directory) +``` + +* Configure/build the project using the buttons in the vscode status bar +* Select the target to run in vscode + +```text +View > Command Palette... > CMake: Set Launch/Debug Target +``` + +* Run/Debug the selected target executable using the buttons in the vscode status bar +* Add unittest source files to the `test` directory +* Add unittests to the `test/CMakeListst.txt` file + +```cmake +cpprog_add_test( + TARGET test_exercise_1 # test will be called test_exercise_1 + CXX_SOURCES # unittest source files + "my_module_1.test.cpp" + DEPENDENCIES # libraries on which the test depends + exercise_1_lib +) +``` + +* Run test from the `Testing` activity in the vscode action bar +* View test results in the `Test Results` tab in the vscode bottom panel + +## Using CLion instead of vscode + +1. **Settings > Build, Execution, Deployment > Toolchains** + * **CMake**: Change from **Bundled** to **Custom CMake executable** with value **/usr/local/bin/cmake** + * **Debugger**: Change from **Bundled GDB** to **Custom LLDB executable** with value **/usr/bin/lldb** +2. **Settings > Build, Execution, Deployment > CMake** + * Delete **Debug** preset + * Enable **clang-debug** and **clang-release** presets +3. **Settings > Build, Execution, Deployment > Dynamic Analysis Tools > Sanitizers** + * **LeakSanitizer**: Set **LSAN_OPTIONS** field to **detect_leaks=0** (disables leak detection, required for running with the debugger) diff --git a/cmake/cpprog.cmake b/cmake/cpprog.cmake new file mode 100644 index 0000000..1a35398 --- /dev/null +++ b/cmake/cpprog.cmake @@ -0,0 +1,249 @@ +include_guard() + +macro(cpprog_init) + _cpprog_generate_compile_commands() + _cpprog_enable_cxx_modules() + _cpprog_set_language_standards() + _cpprog_enable_lto() + _cpprog_find_clang_tidy() + _cpprog_generate_debuginit() +endmacro() + +macro(cpprog_configure_project) + _cpprog_clion_clangd_workaround() + _cpprog_enable_testing() +endmacro() + +macro(_cpprog_generate_compile_commands) + set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + + cmake_path(RELATIVE_PATH CMAKE_CURRENT_BINARY_DIR BASE_DIRECTORY "${CMAKE_SOURCE_DIR}/build" OUTPUT_VARIABLE cpprog_RELATIVE_BINARY_DIR) + + execute_process( + COMMAND "${CMAKE_COMMAND}" -E create_symlink + "${cpprog_RELATIVE_BINARY_DIR}/compile_commands.json" + "compile_commands.json" + WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}/build" + RESULT_VARIABLE cpprog_SYMLINK_RESULT + OUTPUT_QUIET + ERROR_QUIET + ) + + if(NOT cpprog_SYMLINK_RESULT EQUAL 0) + message(WARNING "[cpprog] Failed to create symlink for ${cpprog_RELATIVE_BINARY_DIR}/compile_commands.json!") + else() + message(STATUS "[cpprog] Created symlink for ${cpprog_RELATIVE_BINARY_DIR}/compile_commands.json.") + endif() +endmacro() + +macro(_cpprog_enable_cxx_modules) + set(CMAKE_EXPERIMENTAL_CXX_IMPORT_STD "d0edc3af-4c50-42ea-a356-e2862fe7a444") + set(CMAKE_CXX_MODULE_STD ON) +endmacro() + +macro(_cpprog_set_language_standards) + set(CMAKE_C_STANDARD 23) + set(CMAKE_C_STANDARD_REQUIRED ON) + set(CMAKE_C_EXTENSIONS OFF) + set(CMAKE_CXX_STANDARD 26) + set(CMAKE_CXX_STANDARD_REQUIRED ON) + set(CMAKE_CXX_EXTENSIONS OFF) +endmacro() + +macro(_cpprog_enable_lto) + set(CMAKE_INTERPROCEDURAL_OPTIMIZATION_RELEASE ON) +endmacro() + +macro(_cpprog_find_clang_tidy) + find_program(CLANG_TIDY NAMES clang-tidy REQUIRED) +endmacro() + +function(_cpprog_generate_debuginit) + configure_file("${CMAKE_SOURCE_DIR}/lldbinit.in" "${CMAKE_SOURCE_DIR}/.lldbinit") +endfunction() + +function(_cpprog_clion_clangd_workaround) + if($ENV{CLION_IDE}) + message(STATUS "[cpprog] Detected clion, applying workaround for module std.") + set(cpprog_LIBCXX_DIR "/usr/lib/llvm-20/share/libc++/v1") + if(NOT EXISTS "${cpprog_LIBCXX_DIR}") + message(FATAL_ERROR "[cpprog] libc++ not found at ${cpprog_LIBCXX_DIR}") + endif() + add_library(clion_workaround_std_target STATIC EXCLUDE_FROM_ALL) + target_sources(clion_workaround_std_target + PRIVATE FILE_SET CXX_MODULES + BASE_DIRS "${cpprog_LIBCXX_DIR}" + FILES "${cpprog_LIBCXX_DIR}/std.cppm" "${cpprog_LIBCXX_DIR}/std.compat.cppm" + ) + endif() +endfunction() + +macro(_cpprog_enable_testing) + include(CTest) + enable_testing() + find_package(Catch2 CONFIG REQUIRED) + include(Catch) +endmacro() + +function(cpprog_generate_version_info) + set(options) + set(oneValueArgs TARGET INPUT_FILE OUTPUT_FILE) + set(multiValueArgs) + cmake_parse_arguments(PARSE_ARGV 0 arg "${OPTIONS}" "${oneValueArgs}" "${multiValueArgs}") + + if(NOT TARGET "${arg_TARGET}") + message(FATAL_ERROR "[cpprog] Target ${arg_TARGET} does not exist.") + endif() + + cmake_path(ABSOLUTE_PATH arg_INPUT_FILE BASE_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" NORMALIZE OUTPUT_VARIABLE cpprog_INPUT_FILE) + cmake_path(ABSOLUTE_PATH arg_OUTPUT_FILE BASE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}" NORMALIZE OUTPUT_VARIABLE cpprog_OUTPUT_FILE) + + set(cpprog_VERSION_TARGET "${cpprog_OUTPUT_FILE}") + string(REPLACE "/" "_" cpprog_VERSION_TARGET "${cpprog_VERSION_TARGET}") + + add_custom_target( + "${cpprog_VERSION_TARGET}" ALL + COMMAND "${CMAKE_COMMAND}" + -DPROJECT_ROOT="${CMAKE_SOURCE_DIR}" + -DINPUT_FILE="${cpprog_INPUT_FILE}" + -DOUTPUT_FILE="${cpprog_OUTPUT_FILE}" + -DVERSION_MAJOR="${${CMAKE_PROJECT_NAME}_VERSION_MAJOR}" + -DVERSION_MINOR="${${CMAKE_PROJECT_NAME}_VERSION_MINOR}" + -DVERSION_PATCH="${${CMAKE_PROJECT_NAME}_VERSION_PATCH}" + -P "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/generate_version_info.cmake" + BYPRODUCTS "${cpprog_OUTPUT_FILE}" + COMMENT "[cpprog] Generating version info." + ) + + add_dependencies("${arg_TARGET}" "${cpprog_VERSION_TARGET}") + target_sources("${arg_TARGET}" PUBLIC FILE_SET CXX_MODULES BASE_DIRS "${CMAKE_CURRENT_BINARY_DIR}" FILES "${cpprog_OUTPUT_FILE}") +endfunction() + +function(cpprog_add_executable) + set(options) + set(oneValueArgs TARGET) + set(multiValueArgs CXX_MODULES CXX_SOURCES DEPENDENCIES) + cmake_parse_arguments(PARSE_ARGV 0 arg "${OPTIONS}" "${oneValueArgs}" "${multiValueArgs}") + + if(NOT arg_TARGET) + message(FATAL_ERROR "[cpprog] Missing argument TARGET. Executable name is required!") + endif() + if(NOT arg_CXX_SOURCES) + message(FATAL_ERROR "[cpprog] Missing argument CXX_SOURCES. At least a source file with main function is required!") + endif() + + list(LENGTH arg_CXX_SOURCES cpprog_NUM_SOURCES) + if(cpprog_NUM_SOURCES GREATER 1) + message(NOTICE "[cpprog] Prefer modules when writing C++ code.") + endif() + + add_executable("${arg_TARGET}") + _cpprog_configure_target("${arg_TARGET}" "${arg_CXX_MODULES}" "${arg_CXX_SOURCES}" "" "${arg_DEPENDENCIES}") +endfunction() + +function(cpprog_add_library) + set(options) + set(oneValueArgs TARGET) + set(multiValueArgs CXX_MODULES CXX_SOURCES CXX_HEADERS DEPENDENCIES) + cmake_parse_arguments(PARSE_ARGV 0 arg "${OPTIONS}" "${oneValueArgs}" "${multiValueArgs}") + + if(NOT arg_TARGET) + message(FATAL_ERROR "[cpprog] Missing argument TARGET. Library name is required!") + endif() + if ((NOT arg_CXX_MODULES) OR arg_CXX_SOURCES) + message(NOTICE "[cpprog] Prefer modules when writing C++ code.") + endif() + + add_library("${arg_TARGET}") + add_library("${CMAKE_PROJECT_NAME}::${arg_TARGET}" ALIAS "${arg_TARGET}") + _cpprog_configure_target("${arg_TARGET}" "${arg_CXX_MODULES}" "${arg_CXX_SOURCES}" "${arg_CXX_HEADERS}" "${arg_DEPENDENCIES}") +endfunction() + +function(cpprog_add_test) + set(options) + set(oneValueArgs TARGET) + set(multiValueArgs CXX_MODULES CXX_SOURCES DEPENDENCIES) + cmake_parse_arguments(PARSE_ARGV 0 arg "${OPTIONS}" "${oneValueArgs}" "${multiValueArgs}") + + if(NOT arg_TARGET) + message(FATAL_ERROR "[cpprog] Missing argument TARGET. Test name is required!") + endif() + if(NOT arg_CXX_SOURCES) + message(FATAL_ERROR "[cpprog] Missing argument CXX_SOURCES. At least one source file is required!") + endif() + if(arg_CXX_MODULES) + message(NOTICE "[cpprog] Prefer moving module files with reusable code to a separate library.") + endif() + + list(APPEND arg_DEPENDENCIES Catch2::Catch2WithMain) + + add_executable("${arg_TARGET}") + _cpprog_configure_target("${arg_TARGET}" "${arg_CXX_MODULES}" "${arg_CXX_SOURCES}" "" "${arg_DEPENDENCIES}") + catch_discover_tests("${arg_TARGET}" TEST_PREFIX "${arg_TARGET}." REPORTER compact) +endfunction() + +function(_cpprog_configure_target target_name modules sources headers dependencies) + if(NOT "${target_name}" STREQUAL "cpprog") + list(APPEND dependencies cpprog) + endif() + + target_sources("${target_name}" + PUBLIC FILE_SET HEADERS FILES ${headers} + PUBLIC FILE_SET CXX_MODULES FILES ${modules} + PRIVATE ${sources} + ) + + target_link_libraries("${target_name}" PRIVATE ${dependencies}) + + _cpprog_set_compiler_options("${target_name}") + _cpprog_enable_sanitizers("${target_name}") + _cpprog_enable_clangtidy("${target_name}" "${dependencies}") +endfunction() + +function(_cpprog_set_compiler_options target_name) + set(cpprog_COMMON_WARNINGS + "-Wall;-Wextra;-Wpedantic;-Wshadow;-Wconversion;-Wsign-conversion;-Wdouble-promotion;" + "-Wcast-align;-Wunused;-Wnull-dereference;-Wimplicit-fallthrough;-Wformat=2;-Werror") + + set(cpprog_C_WARNINGS "${cpprog_COMMON_WARNINGS}") + set(cpprog_CXX_WARNINGS "${cpprog_COMMON_WARNINGS};" + "-Wnon-virtual-dtor;-Wold-style-cast;-Woverloaded-virtual;-Wextra-semi") + + target_compile_options("${target_name}" PRIVATE + "-ffile-prefix-map=${CMAKE_SOURCE_DIR}=/project_root" + "$<$:${cpprog_C_WARNINGS}>" + "$<$:${cpprog_CXX_WARNINGS}>" + ) +endfunction() + +function(_cpprog_enable_sanitizers target_name) + set(cpprog_SANITIZERS "address,undefined") + + target_compile_options("${target_name}" PRIVATE + "$<$:-fsanitize=${cpprog_SANITIZERS};-fno-omit-frame-pointer>" + ) + target_link_options("${target_name}" PRIVATE + "$<$:-fsanitize=${cpprog_SANITIZERS}>" + ) +endfunction() + +function(_cpprog_enable_clangtidy target_name dependencies) + get_target_property(cpprog_CXX_STANDARD "${target_name}" CXX_STANDARD) + + set(cpprog_C_CLANG_TIDY "${CLANG_TIDY}") + set(cpprog_CXX_CLANG_TIDY + "${CLANG_TIDY}" + "--extra-arg=-fprebuilt-module-path=${CMAKE_BINARY_DIR}/CMakeFiles/__cmake_cxx${cpprog_CXX_STANDARD}.dir" + "--extra-arg=-fprebuilt-module-path=${CMAKE_CURRENT_BINARY_DIR}/CMakeFiles/${target_name}.dir" + ) + + foreach(cpprog_DEP IN LISTS dependencies) + if(TARGET "${cpprog_DEP}") + get_target_property(cpprog_DEP_DIR "${cpprog_DEP}" BINARY_DIR) + list(APPEND cpprog_CXX_CLANG_TIDY "--extra-arg=-fprebuilt-module-path=${cpprog_DEP_DIR}/CMakeFiles/${cpprog_DEP}.dir") + endif() + endforeach() + + set_target_properties("${target_name}" PROPERTIES C_CLANG_TIDY "${cpprog_C_CLANG_TIDY}") + set_target_properties("${target_name}" PROPERTIES CXX_CLANG_TIDY "${cpprog_CXX_CLANG_TIDY}") +endfunction() diff --git a/cmake/generate_version_info.cmake b/cmake/generate_version_info.cmake new file mode 100644 index 0000000..253d184 --- /dev/null +++ b/cmake/generate_version_info.cmake @@ -0,0 +1,40 @@ +if(NOT PROJECT_ROOT) + message(FATAL_ERROR "[cpprog] Missing argument PROJECT_ROOT.") +endif() + +if(NOT INPUT_FILE) + message(FATAL_ERROR "[cpprog] Missing argument INPUT_FILE.") +endif() + +if(NOT OUTPUT_FILE) + message(FATAL_ERROR "[cpprog] Missing argument OUTPUT_FILE.") +endif() + +if(NOT VERSION_MAJOR) + set(VERSION_MAJOR 0) +endif() +if(NOT VERSION_MINOR) + set(VERSION_MINOR 0) +endif() +if(NOT VERSION_PATCH) + set(VERSION_PATCH 0) +endif() + +execute_process( + COMMAND git rev-parse HEAD + WORKING_DIRECTORY "${PROJECT_ROOT}" + OUTPUT_VARIABLE GIT_COMMIT_HASH + OUTPUT_STRIP_TRAILING_WHITESPACE + RESULT_VARIABLE GIT_REV_PARSE_RESULT + ERROR_QUIET +) + +if(NOT GIT_REV_PARSE_RESULT EQUAL 0) + message(STATUS "[cpprog] Not a git repository.") + set(GIT_COMMIT_HASH "unknown") +endif() + +message(STATUS "[cpprog] Generating ${OUTPUT_FILE}.") +message(STATUS "[cpprog] VERSION=${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_PATCH}, GIT_COMMIT_HASH=${GIT_COMMIT_HASH}") + +configure_file("${INPUT_FILE}" "${OUTPUT_FILE}" @ONLY) diff --git a/cmake/x64-linux-clang-libcxx.cmake b/cmake/x64-linux-clang-libcxx.cmake new file mode 100644 index 0000000..1181826 --- /dev/null +++ b/cmake/x64-linux-clang-libcxx.cmake @@ -0,0 +1,6 @@ +set(VCPKG_TARGET_ARCHITECTURE x64) +set(VCPKG_CRT_LINKAGE dynamic) +set(VCPKG_LIBRARY_LINKAGE static) +set(VCPKG_CMAKE_SYSTEM_NAME Linux) + +set(VCPKG_CHAINLOAD_TOOLCHAIN_FILE "${CMAKE_CURRENT_LIST_DIR}/x64-linux-clang-libcxx.toolchain.cmake") diff --git a/cmake/x64-linux-clang-libcxx.toolchain.cmake b/cmake/x64-linux-clang-libcxx.toolchain.cmake new file mode 100644 index 0000000..c9996e2 --- /dev/null +++ b/cmake/x64-linux-clang-libcxx.toolchain.cmake @@ -0,0 +1,9 @@ +set(CMAKE_C_COMPILER "/usr/bin/clang" CACHE STRING "") +set(CMAKE_CXX_COMPILER "/usr/bin/clang++" CACHE STRING "") + +set(CMAKE_C_FLAGS_DEBUG_INIT "-Og -glldb") +set(CMAKE_CXX_FLAGS_INIT "-stdlib=libc++") +set(CMAKE_CXX_FLAGS_DEBUG_INIT "-D_LIBCPP_HARDENING_MODE=_LIBCPP_HARDENING_MODE_DEBUG -Og -glldb") +set(CMAKE_CXX_FLAGS_RELEASE_INIT "-D_LIBCPP_HARDENING_MODE=_LIBCPP_HARDENING_MODE_FAST") +set(CMAKE_EXE_LINKER_FLAGS_INIT "-lc++ -lc++abi") +set(CMAKE_SHARED_LINKER_FLAGS_INIT "-lc++ -lc++abi") diff --git a/lldbinit.in b/lldbinit.in new file mode 100644 index 0000000..c66911f --- /dev/null +++ b/lldbinit.in @@ -0,0 +1,3 @@ +settings set target.env-vars LSAN_OPTIONS=detect_leaks=0 +settings set target.source-map /project_root ${CMAKE_SOURCE_DIR} +settings set target.process.thread.step-avoid-regexp ^std::|^Catch:: diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 0000000..483dfd7 --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory(cpprog) diff --git a/src/cpprog/CMakeLists.txt b/src/cpprog/CMakeLists.txt new file mode 100644 index 0000000..5be5663 --- /dev/null +++ b/src/cpprog/CMakeLists.txt @@ -0,0 +1,12 @@ +cpprog_add_library( + TARGET cpprog + CXX_MODULES + "cpprog.cpp" + "cpprog_utils.cpp" +) + +cpprog_generate_version_info( + TARGET cpprog + INPUT_FILE "cpprog_version.cpp.in" + OUTPUT_FILE "cpprog_version.cpp" +) diff --git a/src/cpprog/cpprog.cpp b/src/cpprog/cpprog.cpp new file mode 100644 index 0000000..1501528 --- /dev/null +++ b/src/cpprog/cpprog.cpp @@ -0,0 +1,4 @@ +export module cpprog; + +export import :utils; +export import :version; diff --git a/src/cpprog/cpprog_utils.cpp b/src/cpprog/cpprog_utils.cpp new file mode 100644 index 0000000..3efe312 --- /dev/null +++ b/src/cpprog/cpprog_utils.cpp @@ -0,0 +1,44 @@ +export module cpprog:utils; + +import std; + +namespace cpprog { + +export struct ExpectError final : std::runtime_error +{ + using std::runtime_error::runtime_error; +}; + +export constexpr void expect( + std::predicate auto&& cond, + std::string_view msg, + std::source_location const location = std::source_location::current() +) +{ + if (!cond()) [[unlikely]] + { + throw ExpectError{std::format( + "ExpectError @ {}({}:{}) `{}`: {}", + location.file_name(), + location.line(), + location.column(), + location.function_name(), + msg + )}; + } +} + +export struct NarrowingError final : std::exception +{ + [[nodiscard]] char const* what() const noexcept override { return "NarrowingError"; } +}; + +export template +[[nodiscard]] constexpr TO narrow_cast(FROM value) +{ + auto const result = static_cast(value); + if (static_cast(result) != value) { throw NarrowingError{}; } + return result; +} + +} // namespace cpprog diff --git a/src/cpprog/cpprog_version.cpp.in b/src/cpprog/cpprog_version.cpp.in new file mode 100644 index 0000000..91101ba --- /dev/null +++ b/src/cpprog/cpprog_version.cpp.in @@ -0,0 +1,31 @@ +export module cpprog:version; + +import std; + +namespace cpprog::version { + +export struct VersionInfo +{ + int major{}; + int minor{}; + int patch{}; + + [[nodiscard]] constexpr auto operator<=>(VersionInfo const&) const = default; +}; + +export [[nodiscard]] constexpr VersionInfo get() +{ + return { .major = @VERSION_MAJOR@, .minor = @VERSION_MINOR@, .patch = @VERSION_PATCH@ }; +} + +export [[nodiscard]] constexpr std::string_view as_string() +{ + return "@VERSION_MAJOR@.@VERSION_MINOR@.@VERSION_PATCH@"; +} + +export [[nodiscard]] constexpr std::string_view git_commit_hash() +{ + return "@GIT_COMMIT_HASH@"; +} + +} // namespace cpprog::version diff --git a/tests/.clang-tidy b/tests/.clang-tidy new file mode 100644 index 0000000..1d37283 --- /dev/null +++ b/tests/.clang-tidy @@ -0,0 +1,6 @@ +Checks: >- + -cppcoreguidelines-avoid-magic-numbers, + -readability-magic-numbers, + -readability-function-cognitive-complexity, + -readability-redundant-declaration +InheritParentConfig: true diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..2a879f8 --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,5 @@ +cpprog_add_test( + TARGET test_cpprog + CXX_SOURCES + "cpprog.test.cpp" +) diff --git a/tests/cpprog.test.cpp b/tests/cpprog.test.cpp new file mode 100644 index 0000000..e89d8ec --- /dev/null +++ b/tests/cpprog.test.cpp @@ -0,0 +1,59 @@ +#include + +import cpprog; +import std; + +TEST_CASE("test cpprog::version", "[cpprog][version]") +{ + SECTION("get version") + { + auto const expected = cpprog::version::as_string(); + auto const actual = + std::format("{}.{}.{}", cpprog::version::get().major, cpprog::version::get().minor, cpprog::version::get().patch); + + REQUIRE(actual == expected); + } + + SECTION("compare versions") + { + using cpprog::version::VersionInfo; + + STATIC_REQUIRE(VersionInfo{1, 2, 3} == VersionInfo{1, 2, 3}); + STATIC_REQUIRE(VersionInfo{1, 2, 3} != VersionInfo{3, 2, 1}); + STATIC_REQUIRE(VersionInfo{1, 2, 3} < VersionInfo{2, 2, 3}); + STATIC_REQUIRE(VersionInfo{1, 2, 3} < VersionInfo{1, 3, 3}); + STATIC_REQUIRE(VersionInfo{1, 2, 3} < VersionInfo{1, 2, 4}); + } +} + +TEST_CASE("test cpprog::expect", "[cpprog][expect]") +{ + SECTION("expect does not throw when predicate is true") + { + REQUIRE_NOTHROW(cpprog::expect([] noexcept { return true; }, "should not throw")); + } + + SECTION("expect throws when predicate is false") + { + REQUIRE_THROWS_MATCHES( + cpprog::expect([] noexcept { return false; }, "should throw"), + cpprog::ExpectError, + Catch::Matchers::MessageMatches(Catch::Matchers::EndsWith("should throw")) + ); + } +} + +TEST_CASE("test cpprog::narrow_cast", "[cpprog][narrow_cast]") +{ + SECTION("narrowing does not throw if no information is lost") + { + REQUIRE_NOTHROW(cpprog::narrow_cast(3.0)); + REQUIRE_NOTHROW(cpprog::narrow_cast(127)); + } + + SECTION("narowing throws when information is lost") + { + REQUIRE_THROWS_AS(cpprog::narrow_cast(3.14), cpprog::NarrowingError); + REQUIRE_THROWS_AS(cpprog::narrow_cast(1'024), cpprog::NarrowingError); + } +} diff --git a/vcpkg.json b/vcpkg.json new file mode 100644 index 0000000..b74b1a2 --- /dev/null +++ b/vcpkg.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://raw.githubusercontent.com/microsoft/vcpkg-tool/main/docs/vcpkg.schema.json", + "dependencies": [ + "catch2" + ] +}