diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..4a6e50a --- /dev/null +++ b/.clang-format @@ -0,0 +1,135 @@ +--- +Language: Cpp +AccessModifierOffset: -4 +AlignAfterOpenBracket: Align +AlignConsecutiveMacros: false +AlignConsecutiveAssignments: false +AlignConsecutiveDeclarations: false +AlignEscapedNewlines: Right +AlignOperands: true +AlignTrailingComments: true +AllowAllArgumentsOnNextLine: false +AllowAllConstructorInitializersOnNextLine: true +AllowAllParametersOfDeclarationOnNextLine: false +AllowShortBlocksOnASingleLine: Never +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: None +AllowShortLambdasOnASingleLine: All +AllowShortIfStatementsOnASingleLine: Never +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterDefinitionReturnType: None +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: false +AlwaysBreakTemplateDeclarations: Yes +BinPackArguments: false +BinPackParameters: false +BraceWrapping: + AfterCaseLabel: true + AfterClass: true + AfterControlStatement: true + AfterEnum: true + AfterFunction: true + AfterNamespace: true + AfterObjCDeclaration: true + AfterStruct: true + AfterUnion: true + AfterExternBlock: true + BeforeCatch: true + BeforeElse: true + IndentBraces: false + SplitEmptyFunction: true + SplitEmptyRecord: true + SplitEmptyNamespace: true +BreakBeforeBinaryOperators: None +BreakBeforeBraces: Custom +BreakBeforeInheritanceComma: false +BreakInheritanceList: BeforeColon +BreakBeforeTernaryOperators: true +BreakConstructorInitializersBeforeComma: false +BreakConstructorInitializers: BeforeColon +BreakAfterJavaFieldAnnotations: false +BreakStringLiterals: true +ColumnLimit: 80 +CommentPragmas: '^ IWYU pragma:' +CompactNamespaces: false +ConstructorInitializerAllOnOneLineOrOnePerLine: false +ConstructorInitializerIndentWidth: 4 +ContinuationIndentWidth: 4 +Cpp11BracedListStyle: true +DeriveLineEnding: true +DerivePointerAlignment: false +DisableFormat: false +ExperimentalAutoDetectBinPacking: false +FixNamespaceComments: true +ForEachMacros: + - foreach + - Q_FOREACH + - BOOST_FOREACH +IncludeBlocks: Preserve +IncludeCategories: + - Regex: '^"(llvm|llvm-c|clang|clang-c)/' + Priority: 2 + SortPriority: 0 + - Regex: '^(<|"(gtest|gmock|isl|json)/)' + Priority: 3 + SortPriority: 0 + - Regex: '.*' + Priority: 1 + SortPriority: 0 +IncludeIsMainRegex: '(Test)?$' +IncludeIsMainSourceRegex: '' +IndentCaseLabels: false +IndentGotoLabels: true +IndentPPDirectives: None +IndentWidth: 4 +IndentWrappedFunctionNames: false +JavaScriptQuotes: Leave +JavaScriptWrapImports: true +KeepEmptyLinesAtTheStartOfBlocks: true +MacroBlockBegin: '' +MacroBlockEnd: '' +MaxEmptyLinesToKeep: 2 +NamespaceIndentation: None +ObjCBinPackProtocolList: Auto +ObjCBlockIndentWidth: 2 +ObjCSpaceAfterProperty: false +ObjCSpaceBeforeProtocolList: true +PenaltyBreakAssignment: 2 +PenaltyBreakBeforeFirstCallParameter: 19 +PenaltyBreakComment: 300 +PenaltyBreakFirstLessLess: 120 +PenaltyBreakString: 1000 +PenaltyBreakTemplateDeclaration: 10 +PenaltyExcessCharacter: 1000000 +PenaltyReturnTypeOnItsOwnLine: 1000 +PointerAlignment: Right +ReflowComments: false +SortIncludes: true +SortUsingDeclarations: true +SpaceAfterCStyleCast: false +SpaceAfterLogicalNot: false +SpaceAfterTemplateKeyword: true +SpaceBeforeAssignmentOperators: true +SpaceBeforeCpp11BracedList: false +SpaceBeforeCtorInitializerColon: true +SpaceBeforeInheritanceColon: true +SpaceBeforeParens: ControlStatements +SpaceBeforeRangeBasedForLoopColon: true +SpaceInEmptyBlock: false +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 1 +SpacesInAngles: false +SpacesInConditionalStatement: false +SpacesInContainerLiterals: true +SpacesInCStyleCastParentheses: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +SpaceBeforeSquareBrackets: false +Standard: Latest +StatementMacros: + - Q_UNUSED + - QT_REQUIRE_VERSION +TabWidth: 4 +UseCRLF: false +UseTab: Never +... diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 0000000..77ab2c9 --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,7 @@ +--- +Checks: 'clang-analyzer-*,cppcoreguidelines-*,modernize-*,bugprone-*,performance-*,readability-non-const-parameter,misc-const-correctness,misc-use-anonymous-namespace,google-explicit-constructor,-modernize-use-trailing-return-type,-bugprone-exception-escape,-cppcoreguidelines-pro-bounds-constant-array-index,-cppcoreguidelines-avoid-magic-numbers,-bugprone-easily-swappable-parameters' +WarningsAsErrors: '' +HeaderFilterRegex: '\(src|app\)\/*.\(h|hpp\)' +AnalyzeTemporaryDtors: false +FormatStyle: none +... diff --git a/.cmake-format.yaml b/.cmake-format.yaml new file mode 100644 index 0000000..f5d8dc3 --- /dev/null +++ b/.cmake-format.yaml @@ -0,0 +1,245 @@ +_help_parse: Options affecting listfile parsing +parse: + _help_additional_commands: + - Specify structure for custom cmake functions + additional_commands: + foo: + flags: + - BAR + - BAZ + kwargs: + HEADERS: '*' + SOURCES: '*' + DEPENDS: '*' + _help_override_spec: + - Override configurations per-command where available + override_spec: {} + _help_vartags: + - Specify variable tags. + vartags: [] + _help_proptags: + - Specify property tags. + proptags: [] +_help_format: Options affecting formatting. +format: + _help_disable: + - Disable formatting entirely, making cmake-format a no-op + disable: false + _help_line_width: + - How wide to allow formatted cmake files + line_width: 80 + _help_tab_size: + - How many spaces to tab for indent + tab_size: 4 + _help_use_tabchars: + - If true, lines are indented using tab characters (utf-8 + - 0x09) instead of space characters (utf-8 0x20). + - In cases where the layout would require a fractional tab + - character, the behavior of the fractional indentation is + - governed by + use_tabchars: false + _help_fractional_tab_policy: + - If is True, then the value of this variable + - indicates how fractional indentions are handled during + - whitespace replacement. If set to 'use-space', fractional + - indentation is left as spaces (utf-8 0x20). If set to + - '`round-up` fractional indentation is replaced with a single' + - tab character (utf-8 0x09) effectively shifting the column + - to the next tabstop + fractional_tab_policy: use-space + _help_max_subgroups_hwrap: + - If an argument group contains more than this many sub-groups + - (parg or kwarg groups) then force it to a vertical layout. + max_subgroups_hwrap: 2 + _help_max_pargs_hwrap: + - If a positional argument group contains more than this many + - arguments, then force it to a vertical layout. + max_pargs_hwrap: 3 + _help_max_rows_cmdline: + - If a cmdline positional group consumes more than this many + - lines without nesting, then invalidate the layout (and nest) + max_rows_cmdline: 2 + _help_separate_ctrl_name_with_space: + - If true, separate flow control names from their parentheses + - with a space + separate_ctrl_name_with_space: false + _help_separate_fn_name_with_space: + - If true, separate function names from parentheses with a + - space + separate_fn_name_with_space: false + _help_dangle_parens: + - If a statement is wrapped to more than one line, than dangle + - the closing parenthesis on its own line. + dangle_parens: false + _help_dangle_align: + - If the trailing parenthesis must be 'dangled' on its on + - 'line, then align it to this reference: `prefix`: the start' + - 'of the statement, `prefix-indent`: the start of the' + - 'statement, plus one indentation level, `child`: align to' + - the column of the arguments + dangle_align: prefix + _help_min_prefix_chars: + - If the statement spelling length (including space and + - parenthesis) is smaller than this amount, then force reject + - nested layouts. + min_prefix_chars: 4 + _help_max_prefix_chars: + - If the statement spelling length (including space and + - parenthesis) is larger than the tab width by more than this + - amount, then force reject un-nested layouts. + max_prefix_chars: 10 + _help_max_lines_hwrap: + - If a candidate layout is wrapped horizontally but it exceeds + - this many lines, then reject the layout. + max_lines_hwrap: 1 + _help_line_ending: + - What style line endings to use in the output. + line_ending: unix + _help_command_case: + - Format command names consistently as 'lower' or 'upper' case + command_case: canonical + _help_keyword_case: + - Format keywords consistently as 'lower' or 'upper' case + keyword_case: unchanged + _help_always_wrap: + - A list of command names which should always be wrapped + always_wrap: [] + _help_enable_sort: + - If true, the argument lists which are known to be sortable + - will be sorted lexicographicall + enable_sort: true + _help_autosort: + - If true, the parsers may infer whether or not an argument + - list is sortable (without annotation). + autosort: false + _help_require_valid_layout: + - By default, if cmake-format cannot successfully fit + - everything into the desired linewidth it will apply the + - last, most agressive attempt that it made. If this flag is + - True, however, cmake-format will print error, exit with non- + - zero status code, and write-out nothing + require_valid_layout: false + _help_layout_passes: + - A dictionary mapping layout nodes to a list of wrap + - decisions. See the documentation for more information. + layout_passes: {} +_help_markup: Options affecting comment reflow and formatting. +markup: + _help_bullet_char: + - What character to use for bulleted lists + bullet_char: '*' + _help_enum_char: + - What character to use as punctuation after numerals in an + - enumerated list + enum_char: . + _help_first_comment_is_literal: + - If comment markup is enabled, don't reflow the first comment + - block in each listfile. Use this to preserve formatting of + - your copyright/license statements. + first_comment_is_literal: true + _help_literal_comment_pattern: + - If comment markup is enabled, don't reflow any comment block + - which matches this (regex) pattern. Default is `None` + - (disabled). + literal_comment_pattern: .* + _help_fence_pattern: + - Regular expression to match preformat fences in comments + - default= ``r'^\s*([`~]{3}[`~]*)(.*)$'`` + fence_pattern: ^\s*([`~]{3}[`~]*)(.*)$ + _help_ruler_pattern: + - Regular expression to match rulers in comments default= + - '``r''^\s*[^\w\s]{3}.*[^\w\s]{3}$''``' + ruler_pattern: ^\s*[^\w\s]{3}.*[^\w\s]{3}$ + _help_explicit_trailing_pattern: + - If a comment line matches starts with this pattern then it + - is explicitly a trailing comment for the preceeding + - argument. Default is '#<' + explicit_trailing_pattern: '#<' + _help_hashruler_min_length: + - If a comment line starts with at least this many consecutive + - hash characters, then don't lstrip() them off. This allows + - for lazy hash rulers where the first hash char is not + - separated by space + hashruler_min_length: 10 + _help_canonicalize_hashrulers: + - If true, then insert a space between the first hash char and + - remaining hash chars in a hash ruler, and normalize its + - length to fill the column + canonicalize_hashrulers: true + _help_enable_markup: + - enable comment markup parsing and reflow + enable_markup: true +_help_lint: Options affecting the linter +lint: + _help_disabled_codes: + - a list of lint codes to disable + disabled_codes: [] + _help_function_pattern: + - regular expression pattern describing valid function names + function_pattern: '[0-9a-z_]+' + _help_macro_pattern: + - regular expression pattern describing valid macro names + macro_pattern: '[0-9A-Z_]+' + _help_global_var_pattern: + - regular expression pattern describing valid names for + - variables with global (cache) scope + global_var_pattern: '[A-Z][0-9A-Z_]+' + _help_internal_var_pattern: + - regular expression pattern describing valid names for + - variables with global scope (but internal semantic) + internal_var_pattern: _[A-Z][0-9A-Z_]+ + _help_local_var_pattern: + - regular expression pattern describing valid names for + - variables with local scope + local_var_pattern: '[a-z][a-z0-9_]+' + _help_private_var_pattern: + - regular expression pattern describing valid names for + - privatedirectory variables + private_var_pattern: _[0-9a-z_]+ + _help_public_var_pattern: + - regular expression pattern describing valid names for public + - directory variables + public_var_pattern: '[A-Z][0-9A-Z_]+' + _help_argument_var_pattern: + - regular expression pattern describing valid names for + - function/macro arguments and loop variables. + argument_var_pattern: '[a-z][a-z0-9_]+' + _help_keyword_pattern: + - regular expression pattern describing valid names for + - keywords used in functions or macros + keyword_pattern: '[A-Z][0-9A-Z_]+' + _help_max_conditionals_custom_parser: + - In the heuristic for C0201, how many conditionals to match + - within a loop in before considering the loop a parser. + max_conditionals_custom_parser: 2 + _help_min_statement_spacing: + - Require at least this many newlines between statements + min_statement_spacing: 1 + _help_max_statement_spacing: + - Require no more than this many newlines between statements + max_statement_spacing: 2 + max_returns: 6 + max_branches: 12 + max_arguments: 5 + max_localvars: 15 + max_statements: 50 +_help_encode: Options affecting file encoding +encode: + _help_emit_byteorder_mark: + - If true, emit the unicode byte-order mark (BOM) at the start + - of the file + emit_byteorder_mark: false + _help_input_encoding: + - Specify the encoding of the input file. Defaults to utf-8 + input_encoding: utf-8 + _help_output_encoding: + - Specify the encoding of the output file. Defaults to utf-8. + - Note that cmake only claims to support utf-8 so be careful + - when using anything else + output_encoding: utf-8 +_help_misc: Miscellaneous configurations options. +misc: + _help_per_command: + - A dictionary containing any per-command configuration + - overrides. Currently only `command_case` is supported. + per_command: {} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..7fc84ba --- /dev/null +++ b/.editorconfig @@ -0,0 +1,17 @@ +# EditorConfig is awesome: http://EditorConfig.org + +root = true + +[*] +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true +insert_final_newline = true +end_of_line = lf + +[*.md] +trim_trailing_whitespace = false + +[Makefile] +indent_style = tab +trim_trailing_whitespace = false diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..c9a5f2b --- /dev/null +++ b/.gitattributes @@ -0,0 +1,11 @@ +# Set the default behavior for all files. +* text=auto eol=lf + +# Normalized and converts to native line endings on checkout. +*.c text +*.cc text +*.cxx +*.cpp text +*.h text +*.hxx text +*.hpp text diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..dcf0b11 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,40 @@ +name: "CodeQL" + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + schedule: + - cron: '36 4 * * 6' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'cpp' ] + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 + with: + category: "/language:${{matrix.language}}" diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml new file mode 100644 index 0000000..d741a19 --- /dev/null +++ b/.github/workflows/documentation.yml @@ -0,0 +1,34 @@ +name: Documentation + +on: + push: + tags: + - "*" + branches: [ main, master ] + +jobs: + build: + name: Build and publish documentation + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + - name: Install Docs + run: | + sudo apt-get install doxygen + pip install jinja2 Pygments + - name: prepare + run: | + make prepare + - name: configure + run: | + cmake -H. -Bbuild -G "Unix Makefiles" -DCMAKE_BUILD_TYPE="Debug" + - name: building + run: | + cmake --build build --config Debug --target docs -j4 + - name: Deploy to GitHub Pages + uses: Cecilapp/GitHub-Pages-deploy@v3 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + build_dir: ./docs/html diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml new file mode 100644 index 0000000..a817641 --- /dev/null +++ b/.github/workflows/macos.yml @@ -0,0 +1,24 @@ +name: MacOS CI Test + +on: + push: + branches: [ main, master, dev ] + pull_request: + branches: [ main, master, dev ] + +jobs: + build: + runs-on: macos-latest + steps: + - uses: actions/checkout@v2 + - name: prepare + run: | + make prepare + - name: configure + run: | + cmake -H. -Bbuild -DCMAKE_BUILD_TYPE="Debug" + - name: building and testing + run: | + cmake --build build --config Debug + cd build + ctest . diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml new file mode 100644 index 0000000..5b607b2 --- /dev/null +++ b/.github/workflows/pre-commit.yml @@ -0,0 +1,15 @@ +name: pre-commit + +on: + push: + branches: [ main, master, dev ] + pull_request: + branches: [ main, master, dev ] + +jobs: + pre-commit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + - uses: pre-commit/action@v2.0.0 diff --git a/.github/workflows/ubuntu.yml b/.github/workflows/ubuntu.yml new file mode 100644 index 0000000..d035f68 --- /dev/null +++ b/.github/workflows/ubuntu.yml @@ -0,0 +1,45 @@ +name: Ubuntu CI Test + +on: + push: + branches: [ main, master, dev ] + pull_request: + branches: [ main, master, dev ] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: install + run: | + pip install --user -U gcovr + - name: prepare + run: | + make prepare + - name: configure + run: | + cmake -H. -Bbuild -DCMAKE_BUILD_TYPE=Debug -DENABLE_COVERAGE=On + - name: building and testing + run: | + cmake --build build --config Debug --target coverage -j4 + cd build + ctest . + - name: Code Coverage Report + uses: irongut/CodeCoverageSummary@v1.3.0 + with: + filename: build/coverage/coverage.cobertura.xml + badge: true + fail_below_min: true + format: markdown + hide_branch_rate: false + hide_complexity: true + indicators: true + output: both + thresholds: '60 80' + - name: Add Coverage PR Comment + uses: marocchino/sticky-pull-request-comment@v2 + if: github.event_name == 'pull_request' + with: + recreate: true + path: code-coverage-results.md diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml new file mode 100644 index 0000000..5cce786 --- /dev/null +++ b/.github/workflows/windows.yml @@ -0,0 +1,24 @@ +name: Windows CI Test + +on: + push: + branches: [ main, master, dev ] + pull_request: + branches: [ main, master, dev ] + +jobs: + build: + runs-on: windows-latest + steps: + - uses: actions/checkout@v2 + - name: prepare + run: | + make prepare + - name: configure + run: | + cmake -H"." -Bbuild -T host=x86 -A x64 -DCMAKE_BUILD_TYPE="Debug" + - name: building and testing + run: | + cmake --build build --config Debug + cd build + ctest . diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0dce4f1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,146 @@ +################################ +########### FILES ############ +################################ +*.pptx + +################################ +########### FOLDERS ############ +################################ +build/ +build-*/ +html/ + +################################ +############ C/C++ ############# +################################ +# Prerequisites +*.d +# Compiled Object files +*.slo +*.lo +*.o +*.obj +*.ko +*.elf +# Precompiled Headers +*.gch +*.pch +# Compiled Dynamic libraries +*.so +*.dylib +*.dll +# Fortran module files +*.mod +*.smod +# Compiled Static libraries +*.lai +*.la +*.a +*.lib +# Executables +*.i*86 +*.x86_64 +*.hex +*.app +*.stackdump +*.exe +*.out +# Linker output +*.ilk +*.map +*.exp +# Debug files +*.dSYM/ +*.su +*.idb +*.pdb + +################################ +########### VS CODE ############ +################################ +.vscode/settings.json +.vscode/c_cpp_properties.json +*.code-workspace +.history + +################################ +########### VS ############ +################################ + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.meta +*.iobj +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..63ff859 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "external/vcpkg"] + path = external/vcpkg + url = https://github.com/microsoft/vcpkg.git diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..a59ff07 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,16 @@ +fail_fast: false +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: check-yaml + - id: check-json + exclude: .vscode + - id: end-of-file-fixer + - id: trailing-whitespace + +- repo: https://github.com/pre-commit/mirrors-clang-format + rev: 'v16.0.2' + hooks: + - id: clang-format + exclude_types: [javascript, json] diff --git a/.vscode/cmake-kits.json b/.vscode/cmake-kits.json new file mode 100644 index 0000000..ec65e53 --- /dev/null +++ b/.vscode/cmake-kits.json @@ -0,0 +1,37 @@ +[ + { + "name": "linux-default", + "compilers": { + "C": "gcc", + "CXX": "g++" + } + }, + { + "name": "mac-default", + "compilers": { + "C": "clang", + "CXX": "clang++" + } + }, + { + "name": "win32-default", + "visualStudio": "1ad26ee0", + "visualStudioArchitecture": "x86_amd64" + }, + { + "name": "arm32-cross", + "toolchainFile": "${workspaceFolder}/cmake/toolchains/arm32-cross-toolchain.cmake" + }, + { + "name": "arm32-native", + "toolchainFile": "${workspaceFolder}/cmake/toolchains/arm32-native-toolchain.cmake" + }, + { + "name": "x86-64-mingw", + "toolchainFile": "${workspaceFolder}/cmake/toolchains/x86-64-mingw-toolchain.cmake" + }, + { + "name": "x86-64-native", + "toolchainFile": "${workspaceFolder}/cmake/toolchains/x86-64-native-toolchain.cmake" + } +] diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..74e2a9f --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,62 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "(gdb) Launch", + "type": "cppdbg", + "request": "launch", + "program": "${command:cmake.launchTargetPath}", + "args": [], + "stopAtEntry": false, + "cwd": "${workspaceFolder}", + "environment": [ + { + "name": "PATH", + "value": "${env:PATH}:${command:cmake.getLaunchTargetDirectory}" + } + ], + "externalConsole": true, + "MIMode": "gdb", + "setupCommands": [ + { + "description": "Enable pretty-printing for gdb", + "text": "-enable-pretty-printing", + "ignoreFailures": true + } + ] + }, + { + "name": "(lldb) Launch", + "type": "cppdbg", + "request": "launch", + "program": "${command:cmake.launchTargetPath}", + "args": [], + "stopAtEntry": false, + "cwd": "${workspaceFolder}", + "environment": [ + { + "name": "PATH", + "value": "${env:PATH}:${command:cmake.getLaunchTargetDirectory}" + } + ], + "externalConsole": true, + "MIMode": "lldb" + }, + { + "name": "(msvc) Launch", + "type": "cppvsdbg", + "request": "launch", + "program": "${command:cmake.launchTargetPath}", + "args": [], + "stopAtEntry": false, + "cwd": "${workspaceFolder}", + "environment": [ + { + "name": "PATH", + "value": "${env:PATH}:${command:cmake.getLaunchTargetDirectory}" + } + ], + "externalConsole": true + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..07bd84e --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,23 @@ +{ + "C_Cpp.default.configurationProvider": "ms-vscode.cmake-tools", + "cmake.debugConfig": { + "args": [ + // "--verbose", + // "true", + // "--filename", + // "${workspaceFolder}/app/test.json" + ], + "externalConsole": true + }, + // "cmake.buildDirectory": "${workspaceFolder}/build-${buildKit}", + "files.associations": { + "*.pyx": "cython", + ".clang*": "yaml", + "*.gpj": "jsonc", + "*.gvw": "jsonc", + "*.hpp.in": "cpp", + "string": "cpp", + "chrono": "cpp", + "filesystem": "cpp" + } +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..a8edcb7 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,28 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Executable Call", + "type": "shell", + "command": [ + // MSVC + "${workspaceFolder}/build/app/Debug/main" + // Unix + // "${workspaceFolder}/build/bin/main" + ], + "args": [ + // "--verbose", + // "true", + // "--filename", + // "${workspaceFolder}/app/test.json" + ], + "group": { + "kind": "build", + "isDefault": true + }, + "problemMatcher": [ + "$gcc" + ] + } + ] +} diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..8aa5731 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,163 @@ +cmake_minimum_required(VERSION 3.22) + +project( + "CppTemplate" + VERSION 1.0.0 + LANGUAGES C CXX) + +# Global CMake variables are set here +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +# Options +option(USE_CONAN "Whether to use Conan." OFF) +option(USE_VCPKG "Whether to use VCPKG." OFF) +option(USE_CPM "Whether to use CPM." ON) + +option(ENABLE_WARNINGS "Enable to add warnings to a target." ON) +option(ENABLE_WARNINGS_AS_ERRORS "Enable to treat warnings as errors." OFF) + +option(ENABLE_TESTING "Enable a Unit Testing build." ON) +option(ENABLE_COVERAGE "Enable a Code Coverage build." OFF) + +option(ENABLE_CLANG_TIDY "Enable to add clang tidy." OFF) + +option(ENABLE_SANITIZE_ADDR "Enable address sanitize." OFF) +option(ENABLE_SANITIZE_UNDEF "Enable undefined sanitize." OFF) +option(ENABLE_SANITIZE_LEAK "Enable leak sanitize (Gcc/Clang only)." OFF) +option(ENABLE_SANITIZE_THREAD "Enable thread sanitize (Gcc/Clang only)." OFF) + +option(ENABLE_CLANG_FORMAT "Enable to add clang-format." OFF) +option(ENABLE_CMAKE_FORMAT "Enable to add cmake-format." OFF) + +option(ENABLE_LTO "Enable to add Link Time Optimization." ON) + +# Project/Library Names +set(EXECUTABLE_NAME "main") + +# CMAKE MODULES +set(CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake/) +include(ConfigSafeGuards) +include(AddGitSubmodule) +include(Docs) +include(Tools) + +if(ENABLE_WARNINGS) + include(Warnings) +endif() + +add_cmake_format_target() +add_clang_format_target() + +if(ENABLE_SANITIZE_ADDR + OR ENABLE_SANITIZE_UNDEF + OR ENABLE_SANITIZE_LEAK + OR ENABLE_SANITIZE_THREAD) + include(Sanitizer) + add_sanitizer_flags() +endif() + +if(ENABLE_TESTING AND ENABLE_COVERAGE) + include(CodeCoverage) + append_coverage_compiler_flags() +endif() + +if(ENABLE_LTO) + include(LTO) +endif() + +# EXTERNAL LIBRARIES + +if(USE_CPM) + message(STATUS "Using CPM") + include(CPM) + cpmaddpackage("gh:nlohmann/json#v3.11.3") + cpmaddpackage("gh:fmtlib/fmt#9.1.0") + cpmaddpackage("gh:gabime/spdlog#v1.13.0") + cpmaddpackage("gh:jarro2783/cxxopts#v3.1.1") + if(ENABLE_TESTING) + cpmaddpackage("gh:catchorg/Catch2#v3.5.3") + list(APPEND CMAKE_MODULE_PATH ${Catch2_SOURCE_DIR}/extras) + endif() +elseif(USE_CONAN OR USE_VCPKG) + if(USE_CONAN) + message(STATUS "Using Conan") + include(${CMAKE_BINARY_DIR}/conan_toolchain.cmake) + elseif(USE_VCPKG) + message(STATUS "Using VCPKG") + include( + ${CMAKE_SOURCE_DIR}/external/vcpkg/scripts/buildsystems/vcpkg.cmake) + endif() + find_package(nlohmann_json REQUIRED) + find_package(fmt REQUIRED) + find_package(spdlog REQUIRED) + find_package(cxxopts REQUIRED) + if(ENABLE_TESTING) + find_package(Catch2 REQUIRED) + list(APPEND CMAKE_MODULE_PATH ${Catch2_SOURCE_DIR}/extras) + endif() +else() + message(STATUS "Using FetchContent") + FetchContent_Declare( + nlohmann_json + GIT_REPOSITORY https://github.com/nlohmann/json + GIT_TAG v3.11.3 + GIT_SHALLOW TRUE) + FetchContent_MakeAvailable(nlohmann_json) + + FetchContent_Declare( + fmt + GIT_REPOSITORY https://github.com/fmtlib/fmt + GIT_TAG 10.0.0 + GIT_SHALLOW TRUE) + FetchContent_MakeAvailable(fmt) + + FetchContent_Declare( + spdlog + GIT_REPOSITORY https://github.com/gabime/spdlog + GIT_TAG v1.13.0 + GIT_SHALLOW TRUE) + FetchContent_MakeAvailable(spdlog) + + FetchContent_Declare( + cxxopts + GIT_REPOSITORY https://github.com/jarro2783/cxxopts + GIT_TAG v3.1.1 + GIT_SHALLOW TRUE) + FetchContent_MakeAvailable(cxxopts) + + if(ENABLE_TESTING) + FetchContent_Declare( + Catch2 + GIT_REPOSITORY https://github.com/catchorg/Catch2 + GIT_TAG v3.5.3 + GIT_SHALLOW TRUE) + FetchContent_MakeAvailable(Catch2) + list(APPEND CMAKE_MODULE_PATH ${catch2_SOURCE_DIR}/extras) + endif() +endif() + +add_subdirectory(configured) +add_subdirectory(external) +add_subdirectory(src) +add_subdirectory(app) +if(ENABLE_TESTING) + include(CTest) + enable_testing() + add_subdirectory(tests) +endif() + +# INSTALL TARGETS + +install( + TARGETS ${EXECUTABLE_NAME} + ARCHIVE DESTINATION lib + LIBRARY DESTINATION lib + RUNTIME DESTINATION bin) + +install( + TARGETS "LibFoo" "LibBar" + ARCHIVE DESTINATION lib + LIBRARY DESTINATION lib) diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..1f287b4 --- /dev/null +++ b/LICENSE @@ -0,0 +1,16 @@ +Copyright 2023, Jan Schaffranek. + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT +NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d22e4dc --- /dev/null +++ b/Makefile @@ -0,0 +1,18 @@ +all: prepare + +install: + sudo apt-get install gcc g++ cmake make doxygen git llvm pkg-config curl zip unzip tar python3-dev clang-format clang-tidy + +prepare: + rm -rf build + mkdir build + +conan_d: + rm -rf build + mkdir build + cd build && conan install .. -s build_type=Debug --output-folder=. --build missing -s compiler.cppstd=17 + +conan_r: + rm -rf build + mkdir build + cd build && conan install .. -s build_type=Release --output-folder=. --build missing -s compiler.cppstd=17 diff --git a/README.md b/README.md new file mode 100644 index 0000000..97a3f1a --- /dev/null +++ b/README.md @@ -0,0 +1,102 @@ +# Template For C++ Projects + +![C++](https://img.shields.io/badge/C%2B%2B-11%2F14%2F17%2F20%2F23-blue) +![License](https://img.shields.io/github/license/franneck94/CppProjectTemplate) +![Linux Build](https://github.com/franneck94/CppProjectTemplate/workflows/Ubuntu%20CI%20Test/badge.svg) + +This is a template for C++ projects. What you get: + +- Library, executable and test code separated in distinct folders +- Use of modern CMake for building and compiling +- External libraries installed and managed by + - [CPM](https://github.com/cpm-cmake/CPM.cmake) Package Manager **OR** + - [Conan](https://conan.io/) Package Manager **OR** + - [VCPKG](https://github.com/microsoft/vcpkg) Package Manager +- Unit testing using [Catch2](https://github.com/catchorg/Catch2) v2 +- General purpose libraries: [JSON](https://github.com/nlohmann/json), [spdlog](https://github.com/gabime/spdlog), [cxxopts](https://github.com/jarro2783/cxxopts) and [fmt](https://github.com/fmtlib/fmt) +- Continuous integration testing with Github Actions and [pre-commit](https://pre-commit.com/) +- Code documentation with [Doxygen](https://doxygen.nl/) and [Github Pages](https://franneck94.github.io/CppProjectTemplate/) +- Tooling: Clang-Format, Cmake-Format, Clang-tidy, Sanitizers + +## Structure + +``` text +├── CMakeLists.txt +├── app +│ ├── CMakesLists.txt +│ └── main.cc +├── cmake +│ └── cmake modules +├── docs +│ ├── Doxyfile +│ └── html/ +├── external +│ ├── CMakesLists.txt +│ ├── ... +├── src +│ ├── CMakesLists.txt +│ ├── foo/... +│ └── bar/... +└── tests + ├── CMakeLists.txt + └── test_*.cc +``` + +Library code goes into [src/](src/), main program code in [app/](app) and tests go in [tests/](tests/). + +## Software Requirements + +- CMake 3.21+ +- GNU Makefile +- Doxygen +- Conan or VCPKG +- MSVC 2017 (or higher), G++9 (or higher), Clang++9 (or higher) +- Optional: Code Coverage (only on GNU|Clang): gcovr +- Optional: Makefile, Doxygen, Conan, VCPKG + +## Building + +First, clone this repo and do the preliminary work: + +```shell +git clone --recursive https://github.com/franneck94/CppProjectTemplate +mkdir build +``` + +- App Executable + +```shell +cd build +cmake -DCMAKE_BUILD_TYPE=Release .. +cmake --build . --config Release --target main +cd app +./main +``` + +- Unit testing + +```shell +cmake -H. -Bbuild -DCMAKE_BUILD_TYPE="Debug" +cmake --build build --config Debug +cd build +ctest . +``` + +- Documentation + +```shell +cd build +cmake -DCMAKE_BUILD_TYPE=Debug .. +cmake --build . --config Debug --target docs +``` + +- Code Coverage (Unix only) + +```shell +cmake -H. -Bbuild -DCMAKE_BUILD_TYPE=Debug -DENABLE_COVERAGE=On +cmake --build build --config Debug --target coverage -j4 +cd build +ctest . +``` + +For more info about CMake see [here](./README_cmake.md). diff --git a/README_cmake.md b/README_cmake.md new file mode 100644 index 0000000..a53663a --- /dev/null +++ b/README_cmake.md @@ -0,0 +1,162 @@ +# CMake Tutorial + +## Generating a Project + +```bash +cmake [] -S -B +``` + +Assuming that a CMakeLists.txt is in the root directory, you can generate a project like the following. + +```bash +mkdir build +cd build +cmake -S .. -B . # Option 1 +cmake .. # Option 2 +``` + +Assuming that you have already built the CMake project, you can update the generated project. + +```bash +cd build +cmake . +``` + +## Generator for GCC and Clang + +```bash +cd build +cmake -S .. -B . -G "Unix Makefiles" # Option 1 +cmake .. -G "Unix Makefiles" # Option 2 +``` + +## Generator for MSVC + +```bash +cd build +cmake -S .. -B . -G "Visual Studio 16 2019" # Option 1 +cmake .. -G "Visual Studio 16 2019" # Option 2 +``` + +## Specify the Build Type + +Per default, the standard type is in most cases the debug type. +If you want to generate the project, for example, in release mode you have to set the build type. + +```bash +cd build +cmake -DCMAKE_BUILD_TYPE=Release .. +``` + +## Passing Options + +If you have set some options in the CMakeLists, you can pass values in the command line. + +```bash +cd build +cmake -DMY_OPTION=[ON|OFF] .. +``` + +## Specify the Build Target (Option 1) + +The standard build command would build all created targets within the CMakeLists. +If you want to build a specific target, you can do so. + +```bash +cd build +cmake --build . --target ExternalLibraries_Executable +``` + +The target *ExternalLibraries_Executable* is just an example of a possible target name. +Note: All dependent targets will be built beforehand. + +## Specify the Build Target (Option 2) + +Besides setting the target within the cmake build command, you could also run the previously generated Makefile (from the generating step). +If you want to build the *ExternalLibraries_Executable*, you could do the following. + +```bash +cd build +make ExternalLibraries_Executable +``` + +## Run the Executable + +After generating the project and building a specific target you might want to run the executable. +In the default case, the executable is stored in *build/5_ExternalLibraries/app/ExternalLibraries_Executable*, assuming that you are building the project *5_ExternalLibraries* and the main file of the executable is in the *app* dir. + +```bash +cd build +./bin/ExternalLibraries_Executable +``` + +## Different Linking Types + +```cmake +target_link_libraries(A PUBLIC fmt) +target_link_libraries(B PRIVATE spdlog) +``` + +```cmake +target_link_libraries(C PUBLIC/PRIVATE A) +target_link_libraries(C PUBLIC/PRIVATE B) +``` + +### PUBLIC + +When A links fmt as *PUBLIC*, it says that A uses fmt in its implementation, and fmt is also used in A's public API. +Hence, C can use fmt since it is part of the public API of A. + +### PRIVATE + +When B links spdlog as *PRIVATE*, it is saying that B uses spdlog in its +implementation, but spdlog is not used in any part of B's public API. + +### INTERFACE + +```cmake +add_library(D INTERFACE) +target_include_directories(D INTERFACE {CMAKE_CURRENT_SOURCE_DIR}/include) +``` + +In general, used for header-only libraries. + +## Different Library Types + +### Shared + +Shared libraries reduce the amount of code that is duplicated in each program that makes use of the library, keeping the binaries small. It also allows you to replace the shared object with one that is functionally equivalent, without needing to recompile the program that makes use of it. Shared libraries will however have a small additional cost for the execution. + +### Static + +Static libraries increase the overall size of the binary, but it means that you don't need to carry along a copy of the library that is being used. As the code is connected at compile time there are not any additional run-time loading costs. The code is simply there. + +## Cross Compilation with Toolchain Files + +## ARM 32 Cross + +```shell +cmake -B build_arm32 -DCMAKE_TOOLCHAIN_FILE=cmake/toolchains/arm32-cross-toolchain.cmake +cmake --build build_arm32 -j8 +``` + +## ARM 32 Native + +```shell +cmake -B build -DCMAKE_TOOLCHAIN_FILE=cmake/toolchains/arm32-native-toolchain.cmake +cmake --build build -j8 +``` + +## x86 64 MingW + +```shell +cmake -B build_mingw -DCMAKE_TOOLCHAIN_FILE=cmake/toolchains/x86-64-mingw-toolchain.cmake +cmake --build build_mingw -j8 +``` + +## x86 64 Native + +```shell +cmake -B build -DCMAKE_TOOLCHAIN_FILE=cmake/toolchains/x86-64-native-toolchain.cmake +cmake --build build -j8 +``` diff --git a/README_install.md b/README_install.md new file mode 100644 index 0000000..fba8f95 --- /dev/null +++ b/README_install.md @@ -0,0 +1,76 @@ +# Software Installation + +## How to install VCPKG + +Official Link: + +```cmd +cd external +git clone https://github.com/Microsoft/vcpkg.git +.\vcpkg\bootstrap-vcpkg.bat # windows +./vcpkg/bootstrap-vcpkg.sh # Unix +``` + +## How to install the Conan Package Manager + +Official installation guide is [here](https://docs.conan.io/2/). + +The conan database is [here](https://conan.io/center/). + +### Installation Steps + +1. Install Python (3.7+) +2. Type ``pip install --user -U conan`` into the terminal + 1. Unix: Append conan to the PATH by: ``source ~/.profile`` +3. Run the conan command: ``conan`` +4. conan profile detect --force +5. conan profile path default + +## Formatter and Static Analyzer + +### Tooling + +Clang-Format: Formatting tool for your C/C++ code: + +- Documentation for Clang-Format: [Link](https://clang.llvm.org/docs/ClangFormat.html) + +Clang-Tidy: Static linting tool for your C/C++ code: + +- Documentation for Clang-Tidy: [Link](https://clang.llvm.org/extra/clang-tidy/) + +Cmake-Format: + +```bash +pip install cmake-format # python 3.7+ +``` + +### Coverage Tools + +```bash +sudo apt-get install gcovr +pip install -U gcovr +``` + +### Install Clang Tools + +It's included in the LLVM toolchain, but also installable by apt, brew, winget etc. + +https://github.com/llvm/llvm-project/releases/tag/llvmorg-16.0.0 + +## Cross Compiler as an Example + +### Install ARM Compiler on x86 64 Ubuntu + +```shell +sudo apt update +sudo apt install libc6-armel-cross libc6-dev-armel-cross binutils-arm-linux-gnueabi libncurses5-dev build-essential bison flex libssl-dev bc + +sudo apt install gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf +sudo apt install gcc-arm-linux-gnueabi g++-arm-linux-gnueabi +``` + +### Install MingW Cross Compiler on x86 64 Ubuntu + +```shell +sudo apt-get install mingw-w64 +``` diff --git a/README_tools.md b/README_tools.md new file mode 100644 index 0000000..20e3c0e --- /dev/null +++ b/README_tools.md @@ -0,0 +1,10 @@ +# Tools + +To sum up all the tools we use: + +- Compiler warnings: fast checks while compiling the code, for the all target. +- Clang-tidy, CppCheck: linters, can be manually run at any time after their specific targets are built. +- Sanitizers: shows memory leaks in runtime. Built with the all target. +- LTO: applies linking optimization in release mode. Automatically works at compile/linking time for all target +- Doxygen: generates HTML documentation. It can be run apart after build its specific target. +- Clang-format and Cmake-format: allows automatically format the code and CMake files. They can be run apart after build their specific targets. diff --git a/_CMakePresets.json b/_CMakePresets.json new file mode 100644 index 0000000..132b67e --- /dev/null +++ b/_CMakePresets.json @@ -0,0 +1,128 @@ +{ + "version": 3, + "configurePresets": [ + { + "name": "base", + "hidden": true, + "generator": "Ninja", + "binaryDir": "${sourceDir}/build-${presetName}", + "installDir": "${sourceDir}/install/${presetName}", + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Windows" + } + }, + { + "name": "x64-debug", + "displayName": "x64 Debug", + "description": "Sets debug build type and x64 arch", + "inherits": "base", + "architecture": { + "value": "x64", + "strategy": "external" + }, + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug" + } + }, + { + "name": "x64-release", + "displayName": "x64 Release", + "description": "Sets release build type", + "inherits": "x64-debug", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release" + } + }, + { + "name": "x86-debug", + "displayName": "x86 Debug ", + "description": "Sets debug build type and x86 arch", + "inherits": "base", + "architecture": { + "value": "x86", + "strategy": "external" + }, + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug" + } + }, + { + "name": "x86-release", + "displayName": "x86 Release", + "description": "Sets release build type", + "inherits": "x86-debug", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release" + } + }, + { + "name": "linux-debug", + "displayName": "Linux Debug", + "description": "Sets GCC", + "inherits": "base", + "cacheVariables": { + "CMAKE_C_COMPILER": "gcc", + "CMAKE_CXX_COMPILER": "g++", + "CMAKE_BUILD_TYPE": "Debug" + }, + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Linux" + } + }, + { + "name": "linux-release", + "displayName": "Linux Release", + "description": "Sets release build type", + "inherits": "linux-debug", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release" + } + } + ], + "buildPresets": [ + { + "name": "default-build-linux", + "displayName": "Default", + "configurePreset": "linux-debug", + "description": "Vanilla build" + }, + { + "name": "default-build-windows", + "displayName": "Default", + "configurePreset": "x64-debug", + "description": "Vanilla build" + }, + { + "name": "verbose-build-linux", + "displayName": "Verbose Build", + "description": "Passes -v to Ninja", + "configurePreset": "linux-debug", + "nativeToolOptions": [ "-v" ] + }, + { + "name": "verbose-build-windows", + "displayName": "Verbose Build", + "configurePreset": "x64-debug", + "inherits": "verbose-build-linux" + } + ], + "testPresets": [ + { + "name": "core-test-linux", + "description": "Enable output on failure", + "configurePreset": "linux-debug", + "output": { + "outputOnFailure": true + } + }, + { + "name": "core-test-windows", + "inherits": "core-test-linux", + "configurePreset": "x64-debug" + } + ] + } diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt new file mode 100644 index 0000000..7431c37 --- /dev/null +++ b/app/CMakeLists.txt @@ -0,0 +1,34 @@ +set(APP_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/main.cc") + +add_executable(${EXECUTABLE_NAME} ${APP_SOURCES}) + +target_link_libraries( + ${EXECUTABLE_NAME} + PRIVATE "LibFoo" + "LibBar" + nlohmann_json::nlohmann_json + fmt::fmt + spdlog::spdlog + cxxopts::cxxopts) + +if(${ENABLE_WARNINGS}) + target_set_warnings( + TARGET + ${EXECUTABLE_NAME} + ENABLE + ${ENABLE_WARNINGS} + AS_ERRORS + ${ENABLE_WARNINGS_AS_ERRORS}) +endif() + +if(${ENABLE_LTO}) + target_enable_lto( + TARGET + ${EXECUTABLE_NAME} + ENABLE + ON) +endif() + +if(${ENABLE_CLANG_TIDY}) + add_clang_tidy_to_target(${EXECUTABLE_NAME}) +endif() diff --git a/app/main.cc b/app/main.cc new file mode 100644 index 0000000..ec9a4ef --- /dev/null +++ b/app/main.cc @@ -0,0 +1,93 @@ +#include +#include +#include + +#include +#include +#include +#include + +#include "config.hpp" +#include "foo.h" + +using json = nlohmann::json; +namespace fs = std::filesystem; + +int main(int argc, char **argv) +{ + std::cout << "JSON: " << NLOHMANN_JSON_VERSION_MAJOR << "." + << NLOHMANN_JSON_VERSION_MINOR << "." + << NLOHMANN_JSON_VERSION_PATCH << '\n'; + std::cout << "FMT: " << FMT_VERSION << '\n'; + std::cout << "CXXOPTS: " << CXXOPTS__VERSION_MAJOR << "." + << CXXOPTS__VERSION_MINOR << "." << CXXOPTS__VERSION_PATCH + << '\n'; + std::cout << "SPDLOG: " << SPDLOG_VER_MAJOR << "." << SPDLOG_VER_MINOR + << "." << SPDLOG_VER_PATCH << '\n'; + std::cout << "\n\nUsage Example:\n"; + + // Compiler Warning and clang tidy error + // std::int32_t i = 0; + + // Adress Sanitizer should see this + // char x[10]; + // x[11] = 1; + + const auto welcome_message = + fmt::format("Welcome to {} v{}\n", project_name, project_version); + spdlog::info(welcome_message); + + cxxopts::Options options(project_name.data(), welcome_message); + + options.add_options("arguments")("h,help", "Print usage")( + "f,filename", + "File name", + cxxopts::value())( + "v,verbose", + "Verbose output", + cxxopts::value()->default_value("false")); + + auto result = options.parse(argc, argv); + + if (argc == 1 || result.count("help")) + { + std::cout << options.help() << '\n'; + return 0; + } + + auto filename = std::string{}; + auto verbose = false; + + if (result.count("filename")) + { + filename = result["filename"].as(); + } + else + { + return 1; + } + + verbose = result["verbose"].as(); + + if (verbose) + { + fmt::print("Opening file: {}\n", filename); + } + + auto ifs = std::ifstream{filename}; + + if (!ifs.is_open()) + { + return 1; + } + + const auto parsed_data = json::parse(ifs); + + if (verbose) + { + const auto name = parsed_data["name"]; + fmt::print("Name: {}\n", name); + } + + return 0; +} diff --git a/app/test.json b/app/test.json new file mode 100644 index 0000000..7c33b38 --- /dev/null +++ b/app/test.json @@ -0,0 +1,3 @@ +{ + "name": "Jan" +} diff --git a/cmake/AddGitSubmodule.cmake b/cmake/AddGitSubmodule.cmake new file mode 100644 index 0000000..ae62fe1 --- /dev/null +++ b/cmake/AddGitSubmodule.cmake @@ -0,0 +1,18 @@ +function(add_git_submodule relative_dir) + find_package(Git REQUIRED) + + set(FULL_DIR ${CMAKE_SOURCE_DIR}/${relative_dir}) + + if (NOT EXISTS ${FULL_DIR}/CMakeLists.txt) + execute_process(COMMAND ${GIT_EXECUTABLE} + submodule update --init --recursive -- ${relative_dir} + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}) + endif() + + if (EXISTS ${FULL_DIR}/CMakeLists.txt) + message("Submodule is CMake Project: ${FULL_DIR}/CMakeLists.txt") + add_subdirectory(${FULL_DIR}) + else() + message("Submodule is NO CMake Project: ${FULL_DIR}") + endif() +endfunction(add_git_submodule) diff --git a/cmake/CPM.cmake b/cmake/CPM.cmake new file mode 100644 index 0000000..0025365 --- /dev/null +++ b/cmake/CPM.cmake @@ -0,0 +1,1287 @@ +# CPM.cmake - CMake's missing package manager +# =========================================== +# See https://github.com/cpm-cmake/CPM.cmake for usage and update instructions. +# +# MIT License +# ----------- +#[[ + Copyright (c) 2019-2022 Lars Melchior and contributors + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +]] + +cmake_minimum_required(VERSION 3.14 FATAL_ERROR) + +# Initialize logging prefix +if(NOT CPM_INDENT) + set(CPM_INDENT + "CPM:" + CACHE INTERNAL "") +endif() + +if(NOT COMMAND cpm_message) + function(cpm_message) + message(${ARGV}) + endfunction() +endif() + +set(CURRENT_CPM_VERSION 0.38.1) + +get_filename_component(CPM_CURRENT_DIRECTORY "${CMAKE_CURRENT_LIST_DIR}" + REALPATH) +if(CPM_DIRECTORY) + if(NOT + CPM_DIRECTORY + STREQUAL + CPM_CURRENT_DIRECTORY) + if(CPM_VERSION VERSION_LESS CURRENT_CPM_VERSION) + message( + AUTHOR_WARNING + "${CPM_INDENT} \ +A dependency is using a more recent CPM version (${CURRENT_CPM_VERSION}) than the current project (${CPM_VERSION}). \ +It is recommended to upgrade CPM to the most recent version. \ +See https://github.com/cpm-cmake/CPM.cmake for more information.") + endif() + if(${CMAKE_VERSION} VERSION_LESS "3.17.0") + include(FetchContent) + endif() + return() + endif() + + get_property( + CPM_INITIALIZED GLOBAL "" + PROPERTY CPM_INITIALIZED + SET) + if(CPM_INITIALIZED) + return() + endif() +endif() + +if(CURRENT_CPM_VERSION MATCHES "development-version") + message( + WARNING + "${CPM_INDENT} Your project is using an unstable development version of CPM.cmake. \ +Please update to a recent release if possible. \ +See https://github.com/cpm-cmake/CPM.cmake for details.") +endif() + +set_property(GLOBAL PROPERTY CPM_INITIALIZED true) + +macro(cpm_set_policies) + # the policy allows us to change options without caching + cmake_policy(SET CMP0077 NEW) + set(CMAKE_POLICY_DEFAULT_CMP0077 NEW) + + # the policy allows us to change set(CACHE) without caching + if(POLICY CMP0126) + cmake_policy(SET CMP0126 NEW) + set(CMAKE_POLICY_DEFAULT_CMP0126 NEW) + endif() + + # The policy uses the download time for timestamp, instead of the timestamp in the archive. This + # allows for proper rebuilds when a projects url changes + if(POLICY CMP0135) + cmake_policy(SET CMP0135 NEW) + set(CMAKE_POLICY_DEFAULT_CMP0135 NEW) + endif() +endmacro() +cpm_set_policies() + +option( + CPM_USE_LOCAL_PACKAGES + "Always try to use `find_package` to get dependencies" + $ENV{CPM_USE_LOCAL_PACKAGES}) +option(CPM_LOCAL_PACKAGES_ONLY "Only use `find_package` to get dependencies" + $ENV{CPM_LOCAL_PACKAGES_ONLY}) +option(CPM_DOWNLOAD_ALL "Always download dependencies from source" + $ENV{CPM_DOWNLOAD_ALL}) +option( + CPM_DONT_UPDATE_MODULE_PATH + "Don't update the module path to allow using find_package" + $ENV{CPM_DONT_UPDATE_MODULE_PATH}) +option( + CPM_DONT_CREATE_PACKAGE_LOCK + "Don't create a package lock file in the binary path" + $ENV{CPM_DONT_CREATE_PACKAGE_LOCK}) +option( + CPM_INCLUDE_ALL_IN_PACKAGE_LOCK + "Add all packages added through CPM.cmake to the package lock" + $ENV{CPM_INCLUDE_ALL_IN_PACKAGE_LOCK}) +option( + CPM_USE_NAMED_CACHE_DIRECTORIES + "Use additional directory of package name in cache on the most nested level." + $ENV{CPM_USE_NAMED_CACHE_DIRECTORIES}) + +set(CPM_VERSION + ${CURRENT_CPM_VERSION} + CACHE INTERNAL "") +set(CPM_DIRECTORY + ${CPM_CURRENT_DIRECTORY} + CACHE INTERNAL "") +set(CPM_FILE + ${CMAKE_CURRENT_LIST_FILE} + CACHE INTERNAL "") +set(CPM_PACKAGES + "" + CACHE INTERNAL "") +set(CPM_DRY_RUN + OFF + CACHE INTERNAL "Don't download or configure dependencies (for testing)") + +if(DEFINED ENV{CPM_SOURCE_CACHE}) + set(CPM_SOURCE_CACHE_DEFAULT $ENV{CPM_SOURCE_CACHE}) +else() + set(CPM_SOURCE_CACHE_DEFAULT OFF) +endif() + +set(CPM_SOURCE_CACHE + ${CPM_SOURCE_CACHE_DEFAULT} + CACHE PATH "Directory to download CPM dependencies") + +if(NOT CPM_DONT_UPDATE_MODULE_PATH) + set(CPM_MODULE_PATH + "${CMAKE_BINARY_DIR}/CPM_modules" + CACHE INTERNAL "") + # remove old modules + file(REMOVE_RECURSE ${CPM_MODULE_PATH}) + file(MAKE_DIRECTORY ${CPM_MODULE_PATH}) + # locally added CPM modules should override global packages + set(CMAKE_MODULE_PATH "${CPM_MODULE_PATH};${CMAKE_MODULE_PATH}") +endif() + +if(NOT CPM_DONT_CREATE_PACKAGE_LOCK) + set(CPM_PACKAGE_LOCK_FILE + "${CMAKE_BINARY_DIR}/cpm-package-lock.cmake" + CACHE INTERNAL "") + file( + WRITE ${CPM_PACKAGE_LOCK_FILE} + "# CPM Package Lock\n# This file should be committed to version control\n\n" + ) +endif() + +include(FetchContent) + +# Try to infer package name from git repository uri (path or url) +function(cpm_package_name_from_git_uri URI RESULT) + if("${URI}" MATCHES "([^/:]+)/?.git/?$") + set(${RESULT} + ${CMAKE_MATCH_1} + PARENT_SCOPE) + else() + unset(${RESULT} PARENT_SCOPE) + endif() +endfunction() + +# Try to infer package name and version from a url +function( + cpm_package_name_and_ver_from_url + url + outName + outVer) + if(url MATCHES + "[/\\?]([a-zA-Z0-9_\\.-]+)\\.(tar|tar\\.gz|tar\\.bz2|zip|ZIP)(\\?|/|$)") + # We matched an archive + set(filename "${CMAKE_MATCH_1}") + + if(filename MATCHES + "([a-zA-Z0-9_\\.-]+)[_-]v?(([0-9]+\\.)*[0-9]+[a-zA-Z0-9]*)") + # We matched - (ie foo-1.2.3) + set(${outName} + "${CMAKE_MATCH_1}" + PARENT_SCOPE) + set(${outVer} + "${CMAKE_MATCH_2}" + PARENT_SCOPE) + elseif(filename MATCHES "(([0-9]+\\.)+[0-9]+[a-zA-Z0-9]*)") + # We couldn't find a name, but we found a version + # + # In many cases (which we don't handle here) the url would look something like + # `irrelevant/ACTUAL_PACKAGE_NAME/irrelevant/1.2.3.zip`. In such a case we can't possibly + # distinguish the package name from the irrelevant bits. Moreover if we try to match the + # package name from the filename, we'd get bogus at best. + unset(${outName} PARENT_SCOPE) + set(${outVer} + "${CMAKE_MATCH_1}" + PARENT_SCOPE) + else() + # Boldly assume that the file name is the package name. + # + # Yes, something like `irrelevant/ACTUAL_NAME/irrelevant/download.zip` will ruin our day, but + # such cases should be quite rare. No popular service does this... we think. + set(${outName} + "${filename}" + PARENT_SCOPE) + unset(${outVer} PARENT_SCOPE) + endif() + else() + # No ideas yet what to do with non-archives + unset(${outName} PARENT_SCOPE) + unset(${outVer} PARENT_SCOPE) + endif() +endfunction() + +function(cpm_find_package NAME VERSION) + string( + REPLACE " " + ";" + EXTRA_ARGS + "${ARGN}") + find_package( + ${NAME} + ${VERSION} + ${EXTRA_ARGS} + QUIET) + if(${CPM_ARGS_NAME}_FOUND) + if(DEFINED ${CPM_ARGS_NAME}_VERSION) + set(VERSION ${${CPM_ARGS_NAME}_VERSION}) + endif() + cpm_message( + STATUS + "${CPM_INDENT} Using local package ${CPM_ARGS_NAME}@${VERSION}") + cpmregisterpackage(${CPM_ARGS_NAME} "${VERSION}") + set(CPM_PACKAGE_FOUND + YES + PARENT_SCOPE) + else() + set(CPM_PACKAGE_FOUND + NO + PARENT_SCOPE) + endif() +endfunction() + +# Create a custom FindXXX.cmake module for a CPM package This prevents `find_package(NAME)` from +# finding the system library +function(cpm_create_module_file Name) + if(NOT CPM_DONT_UPDATE_MODULE_PATH) + # erase any previous modules + file(WRITE ${CPM_MODULE_PATH}/Find${Name}.cmake + "include(\"${CPM_FILE}\")\n${ARGN}\nset(${Name}_FOUND TRUE)") + endif() +endfunction() + +# Find a package locally or fallback to CPMAddPackage +function(CPMFindPackage) + set(oneValueArgs + NAME + VERSION + GIT_TAG + FIND_PACKAGE_ARGUMENTS) + + cmake_parse_arguments( + CPM_ARGS + "" + "${oneValueArgs}" + "" + ${ARGN}) + + if(NOT DEFINED CPM_ARGS_VERSION) + if(DEFINED CPM_ARGS_GIT_TAG) + cpm_get_version_from_git_tag("${CPM_ARGS_GIT_TAG}" CPM_ARGS_VERSION) + endif() + endif() + + set(downloadPackage ${CPM_DOWNLOAD_ALL}) + if(DEFINED CPM_DOWNLOAD_${CPM_ARGS_NAME}) + set(downloadPackage ${CPM_DOWNLOAD_${CPM_ARGS_NAME}}) + elseif(DEFINED ENV{CPM_DOWNLOAD_${CPM_ARGS_NAME}}) + set(downloadPackage $ENV{CPM_DOWNLOAD_${CPM_ARGS_NAME}}) + endif() + if(downloadPackage) + cpmaddpackage(${ARGN}) + cpm_export_variables(${CPM_ARGS_NAME}) + return() + endif() + + cpm_check_if_package_already_added(${CPM_ARGS_NAME} "${CPM_ARGS_VERSION}") + if(CPM_PACKAGE_ALREADY_ADDED) + cpm_export_variables(${CPM_ARGS_NAME}) + return() + endif() + + cpm_find_package(${CPM_ARGS_NAME} "${CPM_ARGS_VERSION}" + ${CPM_ARGS_FIND_PACKAGE_ARGUMENTS}) + + if(NOT CPM_PACKAGE_FOUND) + cpmaddpackage(${ARGN}) + cpm_export_variables(${CPM_ARGS_NAME}) + endif() + +endfunction() + +# checks if a package has been added before +function(cpm_check_if_package_already_added CPM_ARGS_NAME CPM_ARGS_VERSION) + if("${CPM_ARGS_NAME}" IN_LIST CPM_PACKAGES) + cpmgetpackageversion(${CPM_ARGS_NAME} CPM_PACKAGE_VERSION) + if("${CPM_PACKAGE_VERSION}" VERSION_LESS "${CPM_ARGS_VERSION}") + message( + WARNING + "${CPM_INDENT} Requires a newer version of ${CPM_ARGS_NAME} (${CPM_ARGS_VERSION}) than currently included (${CPM_PACKAGE_VERSION})." + ) + endif() + cpm_get_fetch_properties(${CPM_ARGS_NAME}) + set(${CPM_ARGS_NAME}_ADDED NO) + set(CPM_PACKAGE_ALREADY_ADDED + YES + PARENT_SCOPE) + cpm_export_variables(${CPM_ARGS_NAME}) + else() + set(CPM_PACKAGE_ALREADY_ADDED + NO + PARENT_SCOPE) + endif() +endfunction() + +# Parse the argument of CPMAddPackage in case a single one was provided and convert it to a list of +# arguments which can then be parsed idiomatically. For example gh:foo/bar@1.2.3 will be converted +# to: GITHUB_REPOSITORY;foo/bar;VERSION;1.2.3 +function(cpm_parse_add_package_single_arg arg outArgs) + # Look for a scheme + if("${arg}" MATCHES "^([a-zA-Z]+):(.+)$") + string(TOLOWER "${CMAKE_MATCH_1}" scheme) + set(uri "${CMAKE_MATCH_2}") + + # Check for CPM-specific schemes + if(scheme STREQUAL "gh") + set(out "GITHUB_REPOSITORY;${uri}") + set(packageType "git") + elseif(scheme STREQUAL "gl") + set(out "GITLAB_REPOSITORY;${uri}") + set(packageType "git") + elseif(scheme STREQUAL "bb") + set(out "BITBUCKET_REPOSITORY;${uri}") + set(packageType "git") + # A CPM-specific scheme was not found. Looks like this is a generic URL so try to determine + # type + elseif(arg MATCHES ".git/?(@|#|$)") + set(out "GIT_REPOSITORY;${arg}") + set(packageType "git") + else() + # Fall back to a URL + set(out "URL;${arg}") + set(packageType "archive") + + # We could also check for SVN since FetchContent supports it, but SVN is so rare these days. + # We just won't bother with the additional complexity it will induce in this function. SVN is + # done by multi-arg + endif() + else() + if(arg MATCHES ".git/?(@|#|$)") + set(out "GIT_REPOSITORY;${arg}") + set(packageType "git") + else() + # Give up + message( + FATAL_ERROR + "${CPM_INDENT} Can't determine package type of '${arg}'") + endif() + endif() + + # For all packages we interpret @... as version. Only replace the last occurrence. Thus URIs + # containing '@' can be used + string( + REGEX + REPLACE "@([^@]+)$" + ";VERSION;\\1" + out + "${out}") + + # Parse the rest according to package type + if(packageType STREQUAL "git") + # For git repos we interpret #... as a tag or branch or commit hash + string( + REGEX + REPLACE "#([^#]+)$" + ";GIT_TAG;\\1" + out + "${out}") + elseif(packageType STREQUAL "archive") + # For archives we interpret #... as a URL hash. + string( + REGEX + REPLACE "#([^#]+)$" + ";URL_HASH;\\1" + out + "${out}") + # We don't try to parse the version if it's not provided explicitly. cpm_get_version_from_url + # should do this at a later point + else() + # We should never get here. This is an assertion and hitting it means there's a bug in the code + # above. A packageType was set, but not handled by this if-else. + message( + FATAL_ERROR + "${CPM_INDENT} Unsupported package type '${packageType}' of '${arg}'" + ) + endif() + + set(${outArgs} + ${out} + PARENT_SCOPE) +endfunction() + +# Check that the working directory for a git repo is clean +function( + cpm_check_git_working_dir_is_clean + repoPath + gitTag + isClean) + + find_package(Git REQUIRED) + + if(NOT GIT_EXECUTABLE) + # No git executable, assume directory is clean + set(${isClean} + TRUE + PARENT_SCOPE) + return() + endif() + + # check for uncommitted changes + execute_process( + COMMAND ${GIT_EXECUTABLE} status --porcelain + RESULT_VARIABLE resultGitStatus + OUTPUT_VARIABLE repoStatus + OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET + WORKING_DIRECTORY ${repoPath}) + if(resultGitStatus) + # not supposed to happen, assume clean anyway + message( + WARNING + "${CPM_INDENT} Calling git status on folder ${repoPath} failed") + set(${isClean} + TRUE + PARENT_SCOPE) + return() + endif() + + if(NOT + "${repoStatus}" + STREQUAL + "") + set(${isClean} + FALSE + PARENT_SCOPE) + return() + endif() + + # check for committed changes + execute_process( + COMMAND ${GIT_EXECUTABLE} diff -s --exit-code ${gitTag} + RESULT_VARIABLE resultGitDiff + OUTPUT_STRIP_TRAILING_WHITESPACE OUTPUT_QUIET + WORKING_DIRECTORY ${repoPath}) + + if(${resultGitDiff} EQUAL 0) + set(${isClean} + TRUE + PARENT_SCOPE) + else() + set(${isClean} + FALSE + PARENT_SCOPE) + endif() + +endfunction() + +# method to overwrite internal FetchContent properties, to allow using CPM.cmake to overload +# FetchContent calls. As these are internal cmake properties, this method should be used carefully +# and may need modification in future CMake versions. Source: +# https://github.com/Kitware/CMake/blob/dc3d0b5a0a7d26d43d6cfeb511e224533b5d188f/Modules/FetchContent.cmake#L1152 +function(cpm_override_fetchcontent contentName) + cmake_parse_arguments( + PARSE_ARGV + 1 + arg + "" + "SOURCE_DIR;BINARY_DIR" + "") + if(NOT + "${arg_UNPARSED_ARGUMENTS}" + STREQUAL + "") + message( + FATAL_ERROR + "${CPM_INDENT} Unsupported arguments: ${arg_UNPARSED_ARGUMENTS}" + ) + endif() + + string(TOLOWER ${contentName} contentNameLower) + set(prefix "_FetchContent_${contentNameLower}") + + set(propertyName "${prefix}_sourceDir") + define_property( + GLOBAL + PROPERTY ${propertyName} + BRIEF_DOCS "Internal implementation detail of FetchContent_Populate()" + FULL_DOCS "Details used by FetchContent_Populate() for ${contentName}") + set_property(GLOBAL PROPERTY ${propertyName} "${arg_SOURCE_DIR}") + + set(propertyName "${prefix}_binaryDir") + define_property( + GLOBAL + PROPERTY ${propertyName} + BRIEF_DOCS "Internal implementation detail of FetchContent_Populate()" + FULL_DOCS "Details used by FetchContent_Populate() for ${contentName}") + set_property(GLOBAL PROPERTY ${propertyName} "${arg_BINARY_DIR}") + + set(propertyName "${prefix}_populated") + define_property( + GLOBAL + PROPERTY ${propertyName} + BRIEF_DOCS "Internal implementation detail of FetchContent_Populate()" + FULL_DOCS "Details used by FetchContent_Populate() for ${contentName}") + set_property(GLOBAL PROPERTY ${propertyName} TRUE) +endfunction() + +# Download and add a package from source +function(CPMAddPackage) + cpm_set_policies() + + list(LENGTH ARGN argnLength) + if(argnLength EQUAL 1) + cpm_parse_add_package_single_arg("${ARGN}" ARGN) + + # The shorthand syntax implies EXCLUDE_FROM_ALL and SYSTEM + set(ARGN "${ARGN};EXCLUDE_FROM_ALL;YES;SYSTEM;YES;") + endif() + + set(oneValueArgs + NAME + FORCE + VERSION + GIT_TAG + DOWNLOAD_ONLY + GITHUB_REPOSITORY + GITLAB_REPOSITORY + BITBUCKET_REPOSITORY + GIT_REPOSITORY + SOURCE_DIR + DOWNLOAD_COMMAND + FIND_PACKAGE_ARGUMENTS + NO_CACHE + SYSTEM + GIT_SHALLOW + EXCLUDE_FROM_ALL + SOURCE_SUBDIR) + + set(multiValueArgs URL OPTIONS) + + cmake_parse_arguments( + CPM_ARGS + "" + "${oneValueArgs}" + "${multiValueArgs}" + "${ARGN}") + + # Set default values for arguments + + if(NOT DEFINED CPM_ARGS_VERSION) + if(DEFINED CPM_ARGS_GIT_TAG) + cpm_get_version_from_git_tag("${CPM_ARGS_GIT_TAG}" CPM_ARGS_VERSION) + endif() + endif() + + if(CPM_ARGS_DOWNLOAD_ONLY) + set(DOWNLOAD_ONLY ${CPM_ARGS_DOWNLOAD_ONLY}) + else() + set(DOWNLOAD_ONLY NO) + endif() + + if(DEFINED CPM_ARGS_GITHUB_REPOSITORY) + set(CPM_ARGS_GIT_REPOSITORY + "https://github.com/${CPM_ARGS_GITHUB_REPOSITORY}.git") + elseif(DEFINED CPM_ARGS_GITLAB_REPOSITORY) + set(CPM_ARGS_GIT_REPOSITORY + "https://gitlab.com/${CPM_ARGS_GITLAB_REPOSITORY}.git") + elseif(DEFINED CPM_ARGS_BITBUCKET_REPOSITORY) + set(CPM_ARGS_GIT_REPOSITORY + "https://bitbucket.org/${CPM_ARGS_BITBUCKET_REPOSITORY}.git") + endif() + + if(DEFINED CPM_ARGS_GIT_REPOSITORY) + list( + APPEND + CPM_ARGS_UNPARSED_ARGUMENTS + GIT_REPOSITORY + ${CPM_ARGS_GIT_REPOSITORY}) + if(NOT DEFINED CPM_ARGS_GIT_TAG) + set(CPM_ARGS_GIT_TAG v${CPM_ARGS_VERSION}) + endif() + + # If a name wasn't provided, try to infer it from the git repo + if(NOT DEFINED CPM_ARGS_NAME) + cpm_package_name_from_git_uri(${CPM_ARGS_GIT_REPOSITORY} + CPM_ARGS_NAME) + endif() + endif() + + set(CPM_SKIP_FETCH FALSE) + + if(DEFINED CPM_ARGS_GIT_TAG) + list( + APPEND + CPM_ARGS_UNPARSED_ARGUMENTS + GIT_TAG + ${CPM_ARGS_GIT_TAG}) + # If GIT_SHALLOW is explicitly specified, honor the value. + if(DEFINED CPM_ARGS_GIT_SHALLOW) + list( + APPEND + CPM_ARGS_UNPARSED_ARGUMENTS + GIT_SHALLOW + ${CPM_ARGS_GIT_SHALLOW}) + endif() + endif() + + if(DEFINED CPM_ARGS_URL) + # If a name or version aren't provided, try to infer them from the URL + list( + GET + CPM_ARGS_URL + 0 + firstUrl) + cpm_package_name_and_ver_from_url(${firstUrl} nameFromUrl verFromUrl) + # If we fail to obtain name and version from the first URL, we could try other URLs if any. + # However multiple URLs are expected to be quite rare, so for now we won't bother. + + # If the caller provided their own name and version, they trump the inferred ones. + if(NOT DEFINED CPM_ARGS_NAME) + set(CPM_ARGS_NAME ${nameFromUrl}) + endif() + if(NOT DEFINED CPM_ARGS_VERSION) + set(CPM_ARGS_VERSION ${verFromUrl}) + endif() + + list( + APPEND + CPM_ARGS_UNPARSED_ARGUMENTS + URL + "${CPM_ARGS_URL}") + endif() + + # Check for required arguments + + if(NOT DEFINED CPM_ARGS_NAME) + message( + FATAL_ERROR + "${CPM_INDENT} 'NAME' was not provided and couldn't be automatically inferred for package added with arguments: '${ARGN}'" + ) + endif() + + # Check if package has been added before + cpm_check_if_package_already_added(${CPM_ARGS_NAME} "${CPM_ARGS_VERSION}") + if(CPM_PACKAGE_ALREADY_ADDED) + cpm_export_variables(${CPM_ARGS_NAME}) + return() + endif() + + # Check for manual overrides + if(NOT CPM_ARGS_FORCE + AND NOT + "${CPM_${CPM_ARGS_NAME}_SOURCE}" + STREQUAL + "") + set(PACKAGE_SOURCE ${CPM_${CPM_ARGS_NAME}_SOURCE}) + set(CPM_${CPM_ARGS_NAME}_SOURCE "") + cpmaddpackage( + NAME + "${CPM_ARGS_NAME}" + SOURCE_DIR + "${PACKAGE_SOURCE}" + EXCLUDE_FROM_ALL + "${CPM_ARGS_EXCLUDE_FROM_ALL}" + SYSTEM + "${CPM_ARGS_SYSTEM}" + OPTIONS + "${CPM_ARGS_OPTIONS}" + SOURCE_SUBDIR + "${CPM_ARGS_SOURCE_SUBDIR}" + DOWNLOAD_ONLY + "${DOWNLOAD_ONLY}" + FORCE + True) + cpm_export_variables(${CPM_ARGS_NAME}) + return() + endif() + + # Check for available declaration + if(NOT CPM_ARGS_FORCE + AND NOT + "${CPM_DECLARATION_${CPM_ARGS_NAME}}" + STREQUAL + "") + set(declaration ${CPM_DECLARATION_${CPM_ARGS_NAME}}) + set(CPM_DECLARATION_${CPM_ARGS_NAME} "") + cpmaddpackage(${declaration}) + cpm_export_variables(${CPM_ARGS_NAME}) + # checking again to ensure version and option compatibility + cpm_check_if_package_already_added(${CPM_ARGS_NAME} + "${CPM_ARGS_VERSION}") + return() + endif() + + if(NOT CPM_ARGS_FORCE) + if(CPM_USE_LOCAL_PACKAGES OR CPM_LOCAL_PACKAGES_ONLY) + cpm_find_package(${CPM_ARGS_NAME} "${CPM_ARGS_VERSION}" + ${CPM_ARGS_FIND_PACKAGE_ARGUMENTS}) + + if(CPM_PACKAGE_FOUND) + cpm_export_variables(${CPM_ARGS_NAME}) + return() + endif() + + if(CPM_LOCAL_PACKAGES_ONLY) + message( + SEND_ERROR + "${CPM_INDENT} ${CPM_ARGS_NAME} not found via find_package(${CPM_ARGS_NAME} ${CPM_ARGS_VERSION})" + ) + endif() + endif() + endif() + + cpmregisterpackage("${CPM_ARGS_NAME}" "${CPM_ARGS_VERSION}") + + if(DEFINED CPM_ARGS_GIT_TAG) + set(PACKAGE_INFO "${CPM_ARGS_GIT_TAG}") + elseif(DEFINED CPM_ARGS_SOURCE_DIR) + set(PACKAGE_INFO "${CPM_ARGS_SOURCE_DIR}") + else() + set(PACKAGE_INFO "${CPM_ARGS_VERSION}") + endif() + + if(DEFINED FETCHCONTENT_BASE_DIR) + # respect user's FETCHCONTENT_BASE_DIR if set + set(CPM_FETCHCONTENT_BASE_DIR ${FETCHCONTENT_BASE_DIR}) + else() + set(CPM_FETCHCONTENT_BASE_DIR ${CMAKE_BINARY_DIR}/_deps) + endif() + + if(DEFINED CPM_ARGS_DOWNLOAD_COMMAND) + list( + APPEND + CPM_ARGS_UNPARSED_ARGUMENTS + DOWNLOAD_COMMAND + ${CPM_ARGS_DOWNLOAD_COMMAND}) + elseif(DEFINED CPM_ARGS_SOURCE_DIR) + list( + APPEND + CPM_ARGS_UNPARSED_ARGUMENTS + SOURCE_DIR + ${CPM_ARGS_SOURCE_DIR}) + if(NOT IS_ABSOLUTE ${CPM_ARGS_SOURCE_DIR}) + # Expand `CPM_ARGS_SOURCE_DIR` relative path. This is important because EXISTS doesn't work + # for relative paths. + get_filename_component( + source_directory + ${CPM_ARGS_SOURCE_DIR} + REALPATH + BASE_DIR + ${CMAKE_CURRENT_BINARY_DIR}) + else() + set(source_directory ${CPM_ARGS_SOURCE_DIR}) + endif() + if(NOT EXISTS ${source_directory}) + string(TOLOWER ${CPM_ARGS_NAME} lower_case_name) + # remove timestamps so CMake will re-download the dependency + file(REMOVE_RECURSE + "${CPM_FETCHCONTENT_BASE_DIR}/${lower_case_name}-subbuild") + endif() + elseif(CPM_SOURCE_CACHE AND NOT CPM_ARGS_NO_CACHE) + string(TOLOWER ${CPM_ARGS_NAME} lower_case_name) + set(origin_parameters ${CPM_ARGS_UNPARSED_ARGUMENTS}) + list(SORT origin_parameters) + if(CPM_USE_NAMED_CACHE_DIRECTORIES) + string(SHA1 origin_hash + "${origin_parameters};NEW_CACHE_STRUCTURE_TAG") + set(download_directory + ${CPM_SOURCE_CACHE}/${lower_case_name}/${origin_hash}/${CPM_ARGS_NAME} + ) + else() + string(SHA1 origin_hash "${origin_parameters}") + set(download_directory + ${CPM_SOURCE_CACHE}/${lower_case_name}/${origin_hash}) + endif() + # Expand `download_directory` relative path. This is important because EXISTS doesn't work for + # relative paths. + get_filename_component(download_directory ${download_directory} + ABSOLUTE) + list( + APPEND + CPM_ARGS_UNPARSED_ARGUMENTS + SOURCE_DIR + ${download_directory}) + + if(CPM_SOURCE_CACHE) + file(LOCK ${download_directory}/../cmake.lock) + endif() + + if(EXISTS ${download_directory}) + if(CPM_SOURCE_CACHE) + file(LOCK ${download_directory}/../cmake.lock RELEASE) + endif() + + cpm_store_fetch_properties( + ${CPM_ARGS_NAME} "${download_directory}" + "${CPM_FETCHCONTENT_BASE_DIR}/${lower_case_name}-build") + cpm_get_fetch_properties("${CPM_ARGS_NAME}") + + if(DEFINED CPM_ARGS_GIT_TAG AND NOT (PATCH_COMMAND IN_LIST + CPM_ARGS_UNPARSED_ARGUMENTS)) + # warn if cache has been changed since checkout + cpm_check_git_working_dir_is_clean(${download_directory} + ${CPM_ARGS_GIT_TAG} IS_CLEAN) + if(NOT ${IS_CLEAN}) + message( + WARNING + "${CPM_INDENT} Cache for ${CPM_ARGS_NAME} (${download_directory}) is dirty" + ) + endif() + endif() + + cpm_add_subdirectory( + "${CPM_ARGS_NAME}" + "${DOWNLOAD_ONLY}" + "${${CPM_ARGS_NAME}_SOURCE_DIR}/${CPM_ARGS_SOURCE_SUBDIR}" + "${${CPM_ARGS_NAME}_BINARY_DIR}" + "${CPM_ARGS_EXCLUDE_FROM_ALL}" + "${CPM_ARGS_SYSTEM}" + "${CPM_ARGS_OPTIONS}") + set(PACKAGE_INFO "${PACKAGE_INFO} at ${download_directory}") + + # As the source dir is already cached/populated, we override the call to FetchContent. + set(CPM_SKIP_FETCH TRUE) + cpm_override_fetchcontent( + "${lower_case_name}" + SOURCE_DIR + "${${CPM_ARGS_NAME}_SOURCE_DIR}/${CPM_ARGS_SOURCE_SUBDIR}" + BINARY_DIR + "${${CPM_ARGS_NAME}_BINARY_DIR}") + + else() + # Enable shallow clone when GIT_TAG is not a commit hash. Our guess may not be accurate, but + # it should guarantee no commit hash get mis-detected. + if(NOT DEFINED CPM_ARGS_GIT_SHALLOW) + cpm_is_git_tag_commit_hash("${CPM_ARGS_GIT_TAG}" IS_HASH) + if(NOT ${IS_HASH}) + list( + APPEND + CPM_ARGS_UNPARSED_ARGUMENTS + GIT_SHALLOW + TRUE) + endif() + endif() + + # remove timestamps so CMake will re-download the dependency + file(REMOVE_RECURSE + ${CPM_FETCHCONTENT_BASE_DIR}/${lower_case_name}-subbuild) + set(PACKAGE_INFO "${PACKAGE_INFO} to ${download_directory}") + endif() + endif() + + cpm_create_module_file(${CPM_ARGS_NAME} "CPMAddPackage(\"${ARGN}\")") + + if(CPM_PACKAGE_LOCK_ENABLED) + if((CPM_ARGS_VERSION AND NOT CPM_ARGS_SOURCE_DIR) + OR CPM_INCLUDE_ALL_IN_PACKAGE_LOCK) + cpm_add_to_package_lock(${CPM_ARGS_NAME} "${ARGN}") + elseif(CPM_ARGS_SOURCE_DIR) + cpm_add_comment_to_package_lock(${CPM_ARGS_NAME} "local directory") + else() + cpm_add_comment_to_package_lock(${CPM_ARGS_NAME} "${ARGN}") + endif() + endif() + + cpm_message( + STATUS + "${CPM_INDENT} Adding package ${CPM_ARGS_NAME}@${CPM_ARGS_VERSION} (${PACKAGE_INFO})" + ) + + if(NOT CPM_SKIP_FETCH) + cpm_declare_fetch( + "${CPM_ARGS_NAME}" + "${CPM_ARGS_VERSION}" + "${PACKAGE_INFO}" + "${CPM_ARGS_UNPARSED_ARGUMENTS}") + cpm_fetch_package("${CPM_ARGS_NAME}" populated) + if(CPM_CACHE_SOURCE AND download_directory) + file(LOCK ${download_directory}/../cmake.lock RELEASE) + endif() + if(${populated}) + cpm_add_subdirectory( + "${CPM_ARGS_NAME}" + "${DOWNLOAD_ONLY}" + "${${CPM_ARGS_NAME}_SOURCE_DIR}/${CPM_ARGS_SOURCE_SUBDIR}" + "${${CPM_ARGS_NAME}_BINARY_DIR}" + "${CPM_ARGS_EXCLUDE_FROM_ALL}" + "${CPM_ARGS_SYSTEM}" + "${CPM_ARGS_OPTIONS}") + endif() + cpm_get_fetch_properties("${CPM_ARGS_NAME}") + endif() + + set(${CPM_ARGS_NAME}_ADDED YES) + cpm_export_variables("${CPM_ARGS_NAME}") +endfunction() + +# Fetch a previously declared package +macro(CPMGetPackage Name) + if(DEFINED "CPM_DECLARATION_${Name}") + cpmaddpackage(NAME ${Name}) + else() + message( + SEND_ERROR + "${CPM_INDENT} Cannot retrieve package ${Name}: no declaration available" + ) + endif() +endmacro() + +# export variables available to the caller to the parent scope expects ${CPM_ARGS_NAME} to be set +macro(cpm_export_variables name) + set(${name}_SOURCE_DIR + "${${name}_SOURCE_DIR}" + PARENT_SCOPE) + set(${name}_BINARY_DIR + "${${name}_BINARY_DIR}" + PARENT_SCOPE) + set(${name}_ADDED + "${${name}_ADDED}" + PARENT_SCOPE) + set(CPM_LAST_PACKAGE_NAME + "${name}" + PARENT_SCOPE) +endmacro() + +# declares a package, so that any call to CPMAddPackage for the package name will use these +# arguments instead. Previous declarations will not be overridden. +macro(CPMDeclarePackage Name) + if(NOT DEFINED "CPM_DECLARATION_${Name}") + set("CPM_DECLARATION_${Name}" "${ARGN}") + endif() +endmacro() + +function(cpm_add_to_package_lock Name) + if(NOT CPM_DONT_CREATE_PACKAGE_LOCK) + cpm_prettify_package_arguments(PRETTY_ARGN false ${ARGN}) + file(APPEND ${CPM_PACKAGE_LOCK_FILE} + "# ${Name}\nCPMDeclarePackage(${Name}\n${PRETTY_ARGN})\n") + endif() +endfunction() + +function(cpm_add_comment_to_package_lock Name) + if(NOT CPM_DONT_CREATE_PACKAGE_LOCK) + cpm_prettify_package_arguments(PRETTY_ARGN true ${ARGN}) + file( + APPEND ${CPM_PACKAGE_LOCK_FILE} + "# ${Name} (unversioned)\n# CPMDeclarePackage(${Name}\n${PRETTY_ARGN}#)\n" + ) + endif() +endfunction() + +# includes the package lock file if it exists and creates a target `cpm-update-package-lock` to +# update it +macro(CPMUsePackageLock file) + if(NOT CPM_DONT_CREATE_PACKAGE_LOCK) + get_filename_component(CPM_ABSOLUTE_PACKAGE_LOCK_PATH ${file} ABSOLUTE) + if(EXISTS ${CPM_ABSOLUTE_PACKAGE_LOCK_PATH}) + include(${CPM_ABSOLUTE_PACKAGE_LOCK_PATH}) + endif() + if(NOT TARGET cpm-update-package-lock) + add_custom_target( + cpm-update-package-lock + COMMAND ${CMAKE_COMMAND} -E copy ${CPM_PACKAGE_LOCK_FILE} + ${CPM_ABSOLUTE_PACKAGE_LOCK_PATH}) + endif() + set(CPM_PACKAGE_LOCK_ENABLED true) + endif() +endmacro() + +# registers a package that has been added to CPM +function(CPMRegisterPackage PACKAGE VERSION) + list(APPEND CPM_PACKAGES ${PACKAGE}) + set(CPM_PACKAGES + ${CPM_PACKAGES} + CACHE INTERNAL "") + set("CPM_PACKAGE_${PACKAGE}_VERSION" + ${VERSION} + CACHE INTERNAL "") +endfunction() + +# retrieve the current version of the package to ${OUTPUT} +function(CPMGetPackageVersion PACKAGE OUTPUT) + set(${OUTPUT} + "${CPM_PACKAGE_${PACKAGE}_VERSION}" + PARENT_SCOPE) +endfunction() + +# declares a package in FetchContent_Declare +function( + cpm_declare_fetch + PACKAGE + VERSION + INFO) + if(${CPM_DRY_RUN}) + cpm_message(STATUS "${CPM_INDENT} Package not declared (dry run)") + return() + endif() + + FetchContent_Declare(${PACKAGE} ${ARGN}) +endfunction() + +# returns properties for a package previously defined by cpm_declare_fetch +function(cpm_get_fetch_properties PACKAGE) + if(${CPM_DRY_RUN}) + return() + endif() + + set(${PACKAGE}_SOURCE_DIR + "${CPM_PACKAGE_${PACKAGE}_SOURCE_DIR}" + PARENT_SCOPE) + set(${PACKAGE}_BINARY_DIR + "${CPM_PACKAGE_${PACKAGE}_BINARY_DIR}" + PARENT_SCOPE) +endfunction() + +function( + cpm_store_fetch_properties + PACKAGE + source_dir + binary_dir) + if(${CPM_DRY_RUN}) + return() + endif() + + set(CPM_PACKAGE_${PACKAGE}_SOURCE_DIR + "${source_dir}" + CACHE INTERNAL "") + set(CPM_PACKAGE_${PACKAGE}_BINARY_DIR + "${binary_dir}" + CACHE INTERNAL "") +endfunction() + +# adds a package as a subdirectory if viable, according to provided options +function( + cpm_add_subdirectory + PACKAGE + DOWNLOAD_ONLY + SOURCE_DIR + BINARY_DIR + EXCLUDE + SYSTEM + OPTIONS) + + if(NOT DOWNLOAD_ONLY AND EXISTS ${SOURCE_DIR}/CMakeLists.txt) + set(addSubdirectoryExtraArgs "") + if(EXCLUDE) + list(APPEND addSubdirectoryExtraArgs EXCLUDE_FROM_ALL) + endif() + if("${SYSTEM}" AND "${CMAKE_VERSION}" VERSION_GREATER_EQUAL "3.25") + # https://cmake.org/cmake/help/latest/prop_dir/SYSTEM.html#prop_dir:SYSTEM + list(APPEND addSubdirectoryExtraArgs SYSTEM) + endif() + if(OPTIONS) + foreach(OPTION ${OPTIONS}) + cpm_parse_option("${OPTION}") + set(${OPTION_KEY} "${OPTION_VALUE}") + endforeach() + endif() + set(CPM_OLD_INDENT "${CPM_INDENT}") + set(CPM_INDENT "${CPM_INDENT} ${PACKAGE}:") + add_subdirectory(${SOURCE_DIR} ${BINARY_DIR} + ${addSubdirectoryExtraArgs}) + set(CPM_INDENT "${CPM_OLD_INDENT}") + endif() +endfunction() + +# downloads a previously declared package via FetchContent and exports the variables +# `${PACKAGE}_SOURCE_DIR` and `${PACKAGE}_BINARY_DIR` to the parent scope +function(cpm_fetch_package PACKAGE populated) + set(${populated} + FALSE + PARENT_SCOPE) + if(${CPM_DRY_RUN}) + cpm_message(STATUS + "${CPM_INDENT} Package ${PACKAGE} not fetched (dry run)") + return() + endif() + + FetchContent_GetProperties(${PACKAGE}) + + string(TOLOWER "${PACKAGE}" lower_case_name) + + if(NOT ${lower_case_name}_POPULATED) + FetchContent_Populate(${PACKAGE}) + set(${populated} + TRUE + PARENT_SCOPE) + endif() + + cpm_store_fetch_properties( + ${CPM_ARGS_NAME} ${${lower_case_name}_SOURCE_DIR} + ${${lower_case_name}_BINARY_DIR}) + + set(${PACKAGE}_SOURCE_DIR + ${${lower_case_name}_SOURCE_DIR} + PARENT_SCOPE) + set(${PACKAGE}_BINARY_DIR + ${${lower_case_name}_BINARY_DIR} + PARENT_SCOPE) +endfunction() + +# splits a package option +function(cpm_parse_option OPTION) + string( + REGEX MATCH + "^[^ ]+" + OPTION_KEY + "${OPTION}") + string(LENGTH "${OPTION}" OPTION_LENGTH) + string(LENGTH "${OPTION_KEY}" OPTION_KEY_LENGTH) + if(OPTION_KEY_LENGTH STREQUAL OPTION_LENGTH) + # no value for key provided, assume user wants to set option to "ON" + set(OPTION_VALUE "ON") + else() + math(EXPR OPTION_KEY_LENGTH "${OPTION_KEY_LENGTH}+1") + string( + SUBSTRING "${OPTION}" + "${OPTION_KEY_LENGTH}" + "-1" + OPTION_VALUE) + endif() + set(OPTION_KEY + "${OPTION_KEY}" + PARENT_SCOPE) + set(OPTION_VALUE + "${OPTION_VALUE}" + PARENT_SCOPE) +endfunction() + +# guesses the package version from a git tag +function(cpm_get_version_from_git_tag GIT_TAG RESULT) + string(LENGTH ${GIT_TAG} length) + if(length EQUAL 40) + # GIT_TAG is probably a git hash + set(${RESULT} + 0 + PARENT_SCOPE) + else() + string( + REGEX MATCH + "v?([0123456789.]*).*" + _ + ${GIT_TAG}) + set(${RESULT} + ${CMAKE_MATCH_1} + PARENT_SCOPE) + endif() +endfunction() + +# guesses if the git tag is a commit hash or an actual tag or a branch name. +function(cpm_is_git_tag_commit_hash GIT_TAG RESULT) + string(LENGTH "${GIT_TAG}" length) + # full hash has 40 characters, and short hash has at least 7 characters. + if(length LESS 7 OR length GREATER 40) + set(${RESULT} + 0 + PARENT_SCOPE) + else() + if(${GIT_TAG} MATCHES "^[a-fA-F0-9]+$") + set(${RESULT} + 1 + PARENT_SCOPE) + else() + set(${RESULT} + 0 + PARENT_SCOPE) + endif() + endif() +endfunction() + +function(cpm_prettify_package_arguments OUT_VAR IS_IN_COMMENT) + set(oneValueArgs + NAME + FORCE + VERSION + GIT_TAG + DOWNLOAD_ONLY + GITHUB_REPOSITORY + GITLAB_REPOSITORY + GIT_REPOSITORY + SOURCE_DIR + DOWNLOAD_COMMAND + FIND_PACKAGE_ARGUMENTS + NO_CACHE + SYSTEM + GIT_SHALLOW) + set(multiValueArgs OPTIONS) + cmake_parse_arguments( + CPM_ARGS + "" + "${oneValueArgs}" + "${multiValueArgs}" + ${ARGN}) + + foreach(oneArgName ${oneValueArgs}) + if(DEFINED CPM_ARGS_${oneArgName}) + if(${IS_IN_COMMENT}) + string(APPEND PRETTY_OUT_VAR "#") + endif() + if(${oneArgName} STREQUAL "SOURCE_DIR") + string( + REPLACE ${CMAKE_SOURCE_DIR} + "\${CMAKE_SOURCE_DIR}" + CPM_ARGS_${oneArgName} + ${CPM_ARGS_${oneArgName}}) + endif() + string(APPEND PRETTY_OUT_VAR + " ${oneArgName} ${CPM_ARGS_${oneArgName}}\n") + endif() + endforeach() + foreach(multiArgName ${multiValueArgs}) + if(DEFINED CPM_ARGS_${multiArgName}) + if(${IS_IN_COMMENT}) + string(APPEND PRETTY_OUT_VAR "#") + endif() + string(APPEND PRETTY_OUT_VAR " ${multiArgName}\n") + foreach(singleOption ${CPM_ARGS_${multiArgName}}) + if(${IS_IN_COMMENT}) + string(APPEND PRETTY_OUT_VAR "#") + endif() + string(APPEND PRETTY_OUT_VAR " \"${singleOption}\"\n") + endforeach() + endif() + endforeach() + + if(NOT + "${CPM_ARGS_UNPARSED_ARGUMENTS}" + STREQUAL + "") + if(${IS_IN_COMMENT}) + string(APPEND PRETTY_OUT_VAR "#") + endif() + string(APPEND PRETTY_OUT_VAR " ") + foreach(CPM_ARGS_UNPARSED_ARGUMENT ${CPM_ARGS_UNPARSED_ARGUMENTS}) + string(APPEND PRETTY_OUT_VAR " ${CPM_ARGS_UNPARSED_ARGUMENT}") + endforeach() + string(APPEND PRETTY_OUT_VAR "\n") + endif() + + set(${OUT_VAR} + ${PRETTY_OUT_VAR} + PARENT_SCOPE) + +endfunction() diff --git a/cmake/CodeCoverage.cmake b/cmake/CodeCoverage.cmake new file mode 100644 index 0000000..21f39bc --- /dev/null +++ b/cmake/CodeCoverage.cmake @@ -0,0 +1,191 @@ +# Copyright (c) 2012 - 2017, Lars Bilke All rights reserved. +# Edited by Jan Schaffranek (2024). +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# 1. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 1. Neither the name of the copyright holder nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# + +include(CMakeParseArguments) + +if(CMAKE_C_COMPILER_ID MATCHES "Clang" OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") + set(IS_CLANG TRUE) +else() + set(IS_CLANG FALSE) +endif() +if(CMAKE_C_COMPILER_ID MATCHES "GNU" OR CMAKE_CXX_COMPILER_ID MATCHES "GNU") + set(IS_GCC TRUE) +else() + set(IS_GCC FALSE) +endif() + +if(NOT ${IS_CLANG} AND NOT ${IS_GCC}) + message(FATAL_ERROR "Compiler is not gcc/clang! Aborting...") +endif() + +find_program(GCOVR_PATH gcovr) + +set(COVERAGE_COMPILER_FLAGS "-g3 -O0 --coverage") +set(CMAKE_CXX_FLAGS_COVERAGE ${COVERAGE_COMPILER_FLAGS} FORCE) +set(CMAKE_C_FLAGS_COVERAGE ${COVERAGE_COMPILER_FLAGS} FORCE) +set(CMAKE_EXE_LINKER_FLAGS_COVERAGE "-lgcov" FORCE) +set(CMAKE_SHARED_LINKER_FLAGS_COVERAGE "" FORCE) +mark_as_advanced( + CMAKE_CXX_FLAGS_COVERAGE + CMAKE_C_FLAGS_COVERAGE + CMAKE_EXE_LINKER_FLAGS_COVERAGE + CMAKE_SHARED_LINKER_FLAGS_COVERAGE) + +if(NOT + CMAKE_BUILD_TYPE + STREQUAL + "Debug") + message(WARNING "Cov results with non-Debug build may be misleading") +endif() + +if(${IS_GCC}) + link_libraries(gcov) +endif() + +# Defines a target for running and collection code coverage information Builds +# dependencies, runs the given executable and outputs reports. NOTE! The +# executable should always have a ZERO as exit code otherwise the coverage +# generation will not complete. +function(setup_target_for_coverage_gcovr_html) + if(NOT GCOVR_PATH) + message(FATAL_ERROR "gcovr not found! Aborting...") + endif() # NOT GCOVR_PATH + + set(options NONE) + set(oneValueArgs BASE_DIRECTORY NAME) + set(multiValueArgs + EXCLUDE + EXECUTABLE + EXECUTABLE_ARGS + DEPENDENCIES) + cmake_parse_arguments( + Coverage + "${options}" + "${oneValueArgs}" + "${multiValueArgs}" + ${ARGN}) + + # Set base directory (as absolute path), or default to PROJECT_SOURCE_DIR + if(DEFINED Coverage_BASE_DIRECTORY) + get_filename_component(BASEDIR ${Coverage_BASE_DIRECTORY} ABSOLUTE) + else() + set(BASEDIR ${PROJECT_SOURCE_DIR}) + endif() + + # Collect excludes (CMake 3.4+: Also compute absolute paths) + set(GCOVR_EXCLUDES "") + foreach(EXCLUDE ${Coverage_EXCLUDE} ${COVERAGE_EXCLUDES} + ${COVERAGE_GCOVR_EXCLUDES}) + if(CMAKE_VERSION VERSION_GREATER 3.4) + get_filename_component( + EXCLUDE + ${EXCLUDE} + ABSOLUTE + BASE_DIR + ${BASEDIR}) + endif() + list(APPEND GCOVR_EXCLUDES "${EXCLUDE}") + endforeach() + list(REMOVE_DUPLICATES GCOVR_EXCLUDES) + + # Combine excludes to several -e arguments + set(GCOVR_EXCLUDE_ARGS "") + foreach(EXCLUDE ${GCOVR_EXCLUDES}) + list(APPEND GCOVR_EXCLUDE_ARGS "-e") + list(APPEND GCOVR_EXCLUDE_ARGS "${EXCLUDE}") + endforeach() + + # Set up commands which will be run to generate coverage data Run tests + set(GCOVR_HTML_EXEC_TESTS_CMD ${Coverage_EXECUTABLE} + ${Coverage_EXECUTABLE_ARGS}) + # Create folder + set(GCOVR_HTML_FOLDER_CMD + ${CMAKE_COMMAND} + -E + make_directory + ${PROJECT_BINARY_DIR}/${Coverage_NAME}) + # Running gcovr + set(GCOVR_EXTRA_FLAGS + --json-summary + --json-summary-pretty + --html-theme + github.dark-green) + set(GCOVR_HTML_CMD + ${GCOVR_PATH} + ${GCOVR_EXTRA_FLAGS} + --html + ${Coverage_NAME}/index.html + --html-details + --json-summary + ${Coverage_NAME}/summary.json + --json-summary-pretty + --cobertura + ${Coverage_NAME}/coverage.cobertura.xml + --cobertura-pretty + --decisions + -r + ${BASEDIR} + ${GCOVR_ADDITIONAL_ARGS} + ${GCOVR_EXCLUDE_ARGS} + --object-directory=${PROJECT_BINARY_DIR}) + + add_custom_target( + ${Coverage_NAME} + COMMAND ${GCOVR_HTML_EXEC_TESTS_CMD} + COMMAND ${GCOVR_HTML_FOLDER_CMD} + COMMAND ${GCOVR_HTML_CMD} + BYPRODUCTS ${PROJECT_BINARY_DIR}/${Coverage_NAME}/index.html # report + # directory + WORKING_DIRECTORY ${PROJECT_BINARY_DIR} + DEPENDS ${Coverage_DEPENDENCIES} + VERBATIM # Protect arguments to commands + COMMENT "Running gcovr to produce HTML code coverage report.") + + # Show info where to find the report + add_custom_command( + TARGET ${Coverage_NAME} + POST_BUILD + COMMAND ; + COMMENT + "Open ./${Coverage_NAME}/index.html in your browser to view the coverage report." + ) +endfunction() # setup_target_for_coverage_gcovr_html + +function(append_coverage_compiler_flags) + set(CMAKE_C_FLAGS + "${CMAKE_C_FLAGS} ${COVERAGE_COMPILER_FLAGS}" + PARENT_SCOPE) + set(CMAKE_CXX_FLAGS + "${CMAKE_CXX_FLAGS} ${COVERAGE_COMPILER_FLAGS}" + PARENT_SCOPE) + message( + STATUS + "Appending code coverage compiler flags: ${COVERAGE_COMPILER_FLAGS}" + ) +endfunction() diff --git a/cmake/ConfigSafeGuards.cmake b/cmake/ConfigSafeGuards.cmake new file mode 100644 index 0000000..fe7240f --- /dev/null +++ b/cmake/ConfigSafeGuards.cmake @@ -0,0 +1,10 @@ +if(${CMAKE_SOURCE_DIR} STREQUAL ${CMAKE_BINARY_DIR}) + message( + FATAL_ERROR + "In-source builds not allowed. Please make a build directory.") +endif() + +if(NOT CMAKE_BUILD_TYPE) + message(STATUS "No build type selected, default to Debug") + set(CMAKE_BUILD_TYPE "Debug") +endif() diff --git a/cmake/Docs.cmake b/cmake/Docs.cmake new file mode 100644 index 0000000..fef49b8 --- /dev/null +++ b/cmake/Docs.cmake @@ -0,0 +1,8 @@ +find_package(Doxygen) + +if(DOXYGEN_FOUND) + add_custom_target( + docs + ${DOXYGEN_EXECUTABLE} + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/docs) +endif() diff --git a/cmake/LTO.cmake b/cmake/LTO.cmake new file mode 100644 index 0000000..eb42a33 --- /dev/null +++ b/cmake/LTO.cmake @@ -0,0 +1,19 @@ +function(target_enable_lto) + set(oneValueArgs TARGET ENABLE) + cmake_parse_arguments( + LTO + "${options}" + "${oneValueArgs}" + "${multiValueArgs}" + ${ARGN}) + + include(CheckIPOSupported) + check_ipo_supported(RESULT result OUTPUT output) + if(result) + message(STATUS "IPO/LTO is supported: ${LTO_TARGET}") + set_property(TARGET ${LTO_TARGET} PROPERTY INTERPROCEDURAL_OPTIMIZATION + ${LTO_ENABLE}) + else() + message(WARNING "IPO/LTO is not supported: ${LTO_TARGET}") + endif() +endfunction() diff --git a/cmake/Sanitizer.cmake b/cmake/Sanitizer.cmake new file mode 100644 index 0000000..760ffb7 --- /dev/null +++ b/cmake/Sanitizer.cmake @@ -0,0 +1,57 @@ +function(add_sanitizer_flags) + if(NOT ENABLE_SANITIZE_ADDR AND NOT ENABLE_SANITIZE_UNDEF) + return() + endif() + + if(CMAKE_CXX_COMPILER_ID MATCHES "Clang" OR CMAKE_CXX_COMPILER_ID MATCHES + "GNU") + add_compile_options("-fno-omit-frame-pointer") + add_link_options("-fno-omit-frame-pointer") + + if(ENABLE_SANITIZE_ADDR) + message(STATUS "Activating Address Sanitizer") + add_compile_options("-fsanitize=address") + add_link_options("-fsanitize=address") + endif() + + if(ENABLE_SANITIZE_UNDEF) + message(STATUS "Activating Undefined Sanitizer") + add_compile_options("-fsanitize=undefined") + add_link_options("-fsanitize=undefined") + endif() + + if(ENABLE_SANITIZE_LEAK) + add_compile_options("-fsanitize=leak") + add_link_options("-fsanitize=leak") + endif() + + if(ENABLE_SANITIZE_THREAD) + if(ENABLE_SANITIZE_ADDR OR ENABLE_SANITIZE_LEAK) + message(WARNING "thread does not work with: address and leak") + endif() + message(STATUS "Activating Thread Sanitizer") + add_compile_options("-fsanitize=thread") + add_link_options("-fsanitize=thread") + endif() + elseif(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") + if(ENABLE_SANITIZE_ADDR) + message(STATUS "Activating Address Sanitizer") + add_compile_options("/fsanitize=address") + endif() + + if(ENABLE_SANITIZE_UNDEF) + message(STATUS "sanitize=undefined not avail. for MSVC") + endif() + + if(ENABLE_SANITIZE_LEAK) + message(STATUS "sanitize=leak not avail. for MSVC") + endif() + + if(ENABLE_SANITIZE_THREAD) + message(STATUS "sanitize=thread not avail. for MSVC") + endif() + else() + message(WARNING "This sanitizer not supported in this environment") + return() + endif() +endfunction(add_sanitizer_flags) diff --git a/cmake/Tools.cmake b/cmake/Tools.cmake new file mode 100644 index 0000000..bcc18d8 --- /dev/null +++ b/cmake/Tools.cmake @@ -0,0 +1,120 @@ +function(add_cmake_format_target) + if(NOT ${ENABLE_CMAKE_FORMAT}) + return() + endif() + set(ROOT_CMAKE_FILES "${CMAKE_SOURCE_DIR}/CMakeLists.txt") + file(GLOB_RECURSE CMAKE_FILES_TXT "*/CMakeLists.txt") + file(GLOB_RECURSE CMAKE_FILES_C "cmake/*.cmake") + list( + FILTER + CMAKE_FILES_TXT + EXCLUDE + REGEX + "${CMAKE_SOURCE_DIR}/(build|external)/.*") + set(CMAKE_FILES ${ROOT_CMAKE_FILES} ${CMAKE_FILES_TXT} ${CMAKE_FILES_C}) + find_program(CMAKE_FORMAT cmake-format) + if(CMAKE_FORMAT) + message(STATUS "Added Cmake Format") + set(FORMATTING_COMMANDS) + foreach(cmake_file ${CMAKE_FILES}) + list( + APPEND + FORMATTING_COMMANDS + COMMAND + cmake-format + -c + ${CMAKE_SOURCE_DIR}/.cmake-format.yaml + -i + ${cmake_file}) + endforeach() + add_custom_target( + run_cmake_format + COMMAND ${FORMATTING_COMMANDS} + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}) + else() + message(WARNING "CMAKE_FORMAT NOT FOUND") + endif() +endfunction() + +function(add_clang_format_target) + if(NOT ${ENABLE_CLANG_FORMAT}) + return() + endif() + find_package(Python3 COMPONENTS Interpreter) + if(NOT ${Python_FOUND}) + return() + endif() + file(GLOB_RECURSE CMAKE_FILES_CC "*/*.cc") + file(GLOB_RECURSE CMAKE_FILES_CPP "*/*.cpp") + file(GLOB_RECURSE CMAKE_FILES_H "*/*.h") + file(GLOB_RECURSE CMAKE_FILES_HPP "*/*.hpp") + set(CPP_FILES + ${CMAKE_FILES_CC} + ${CMAKE_FILES_CPP} + ${CMAKE_FILES_H} + ${CMAKE_FILES_HPP}) + list( + FILTER + CPP_FILES + EXCLUDE + REGEX + "${CMAKE_SOURCE_DIR}/(build|external)/.*") + find_program(CLANGFORMAT clang-format) + if(CLANGFORMAT) + message(STATUS "Added Clang Format") + add_custom_target( + run_clang_format + COMMAND + ${Python3_EXECUTABLE} + ${CMAKE_SOURCE_DIR}/tools/run-clang-format.py ${CPP_FILES} + --in-place + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + USES_TERMINAL) + else() + message(WARNING "CLANGFORMAT NOT FOUND") + endif() +endfunction() + +function(add_clang_tidy_to_target target) + get_target_property(TARGET_SOURCES ${target} SOURCES) + list( + FILTER + TARGET_SOURCES + INCLUDE + REGEX + ".*.(cc|h|cpp|hpp)") + + find_package(Python3 COMPONENTS Interpreter) + if(NOT ${Python_FOUND}) + message(WARNING "Python3 needed for Clang-Tidy") + return() + endif() + + find_program(CLANGTIDY clang-tidy) + if(CLANGTIDY) + if(CMAKE_CXX_COMPILER_ID MATCHES "MSVC") + message(STATUS "Added MSVC ClangTidy (VS GUI only) for: ${target}") + set_target_properties( + ${target} PROPERTIES VS_GLOBAL_EnableMicrosoftCodeAnalysis + false) + set_target_properties( + ${target} PROPERTIES VS_GLOBAL_EnableClangTidyCodeAnalysis true) + else() + message(STATUS "Added Clang Tidy for Target: ${target}") + add_custom_target( + ${target}_clangtidy + COMMAND + ${Python3_EXECUTABLE} + ${CMAKE_SOURCE_DIR}/tools/run-clang-tidy.py + ${TARGET_SOURCES} + -config-file=${CMAKE_SOURCE_DIR}/.clang-tidy + -extra-arg-before=-std=${CMAKE_CXX_STANDARD} + -header-filter="\(src|app\)\/*.\(h|hpp\)" + -p=${CMAKE_BINARY_DIR} + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + USES_TERMINAL) + endif() + else() + message(WARNING "CLANGTIDY NOT FOUND") + endif() +endfunction() diff --git a/cmake/Warnings.cmake b/cmake/Warnings.cmake new file mode 100644 index 0000000..1c349e5 --- /dev/null +++ b/cmake/Warnings.cmake @@ -0,0 +1,84 @@ +function(target_set_warnings) + set(oneValueArgs TARGET ENABLE AS_ERRORS) + cmake_parse_arguments( + TARGET_SET_WARNINGS + "${options}" + "${oneValueArgs}" + "${multiValueArgs}" + ${ARGN}) + + if(NOT ${TARGET_SET_WARNINGS_ENABLE}) + message(STATUS "Warnings Disabled for: ${TARGET_SET_WARNINGS_TARGET}") + return() + endif() + message(STATUS "Warnings Active for: ${TARGET_SET_WARNINGS_TARGET}") + message(STATUS "Warnings as Errors: ${TARGET_SET_WARNINGS_AS_ERRORS}") + + set(MSVC_WARNINGS + # Baseline + /W4 # Baseline reasonable warnings + /permissive- # standards conformance mode for MSVC compiler + # C and C++ Warnings + /w14242 # conversion from 'type1' to 'type1', possible loss of data + /w14254 # 'operator': conversion from 't1:field_bits' to 't2:field_bits' + /w14287 # unsigned/negative constant mismatch + /w14296 # expression is always 'boolean_value' + /w14311 # pointer truncation from 'type1' to 'type2' + /w44062 # enumerator in a switch of enum 'enumeration' is not handled + /w44242 # conversion from 'type1' to 'type2', possible loss of data + /w14826 # Conversion from 'type1' to 'type_2' is sign-extended + /w14905 # wide string literal cast to 'LPSTR' + /w14906 # string literal cast to 'LPWSTR' + # C++ Only + /w14263 # function does not override any base class virtual function + /w14265 # class has virtual functions, but destructor is not virtual + /w14640 # Enable warning on thread un-safe static member initialization + /w14928 # more than one implicitly user-defined conversion + /we4289 # nonstandard extension used: 'variable' + ) + + set(CLANG_WARNINGS + # Baseline + -Wall + -Wextra # reasonable and standard + -Wshadow # if a variable declaration shadows one from a parent context + -Wpedantic # warn if non-standard is used + # C and C++ Warnings + -Wunused # warn on anything being unused + -Wformat=2 # warn on security issues around functions that format output + -Wcast-align # warn for potential performance problem casts + -Wconversion # warn on type conversions that may lose data + -Wsign-conversion # warn on sign conversions + -Wnull-dereference # warn if a null dereference is detected + -Wdouble-promotion # warn if float is implicit promoted to double + # C++ Warnings + -Wnon-virtual-dtor # if a class with virtual func has a non-virtual dest + -Wold-style-cast # warn for c-style casts + -Woverloaded-virtual # if you overload (not override) a virtual function + -Weffc++ # violations from Scott Meyers’ Effective C++ + ) + + set(GCC_WARNINGS + ${CLANG_WARNINGS} + -Wduplicated-cond # warn if if / else chain has duplicated conditions + -Wduplicated-branches # warn if if / else branches have duplicated code + -Wlogical-op # warn about logical operations being used where bitwise were probably wanted + ) + + if(${TARGET_SET_WARNINGS_AS_ERRORS}) + set(CLANG_WARNINGS ${CLANG_WARNINGS} -Werror) + set(GCC_WARNINGS ${GCC_WARNINGS} -Werror) + set(MSVC_WARNINGS ${MSVC_WARNINGS} /WX) + endif() + + if(CMAKE_CXX_COMPILER_ID MATCHES "MSVC") + set(WARNINGS ${MSVC_WARNINGS}) + elseif(CMAKE_CXX_COMPILER_ID MATCHES "Clang") + set(WARNINGS ${CLANG_WARNINGS}) + elseif(CMAKE_CXX_COMPILER_ID MATCHES "GNU") + set(WARNINGS ${GCC_WARNINGS}) + endif() + + target_compile_options(${TARGET_SET_WARNINGS_TARGET} PRIVATE ${WARNINGS}) + +endfunction(target_set_warnings) diff --git a/cmake/toolchains/arm32-cross-toolchain.cmake b/cmake/toolchains/arm32-cross-toolchain.cmake new file mode 100644 index 0000000..f18f49d --- /dev/null +++ b/cmake/toolchains/arm32-cross-toolchain.cmake @@ -0,0 +1,12 @@ +set(CMAKE_SYSTEM_NAME Linux) +set(CMAKE_SYSTEM_PROCESSOR arm) + +set(CMAKE_C_COMPILER arm-linux-gnueabihf-gcc) +set(CMAKE_C_FLAGS -static) +set(CMAKE_CXX_COMPILER arm-linux-gnueabihf-g++) +set(CMAKE_CXX_FLAGS -static) + +set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) +set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) diff --git a/cmake/toolchains/arm32-native-toolchain.cmake b/cmake/toolchains/arm32-native-toolchain.cmake new file mode 100644 index 0000000..29887e8 --- /dev/null +++ b/cmake/toolchains/arm32-native-toolchain.cmake @@ -0,0 +1,12 @@ +set(CMAKE_SYSTEM_NAME Linux) +set(CMAKE_SYSTEM_PROCESSOR arm) + +set(CMAKE_C_COMPILER gcc) +set(CMAKE_C_FLAGS -static) +set(CMAKE_CXX_COMPILER g++) +set(CMAKE_CXX_FLAGS -static) + +set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) +set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) diff --git a/cmake/toolchains/x86-64-mingw-toolchain.cmake b/cmake/toolchains/x86-64-mingw-toolchain.cmake new file mode 100644 index 0000000..f47acad --- /dev/null +++ b/cmake/toolchains/x86-64-mingw-toolchain.cmake @@ -0,0 +1,14 @@ +set(CMAKE_SYSTEM_NAME Windows) +set(CMAKE_SYSTEM_PROCESSOR x86_64) + +set(TOOLCHAIN_PREFIX x86_64-w64-mingw32) + +set(CMAKE_C_COMPILER ${TOOLCHAIN_PREFIX}-gcc-posix) +set(CMAKE_C_FLAGS "-static") +set(CMAKE_CXX_COMPILER ${TOOLCHAIN_PREFIX}-g++-posix) +set(CMAKE_CXX_FLAGS "-static") + +set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) +set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) diff --git a/cmake/toolchains/x86-64-native-toolchain.cmake b/cmake/toolchains/x86-64-native-toolchain.cmake new file mode 100644 index 0000000..05c64de --- /dev/null +++ b/cmake/toolchains/x86-64-native-toolchain.cmake @@ -0,0 +1,10 @@ +set(CMAKE_SYSTEM_NAME Linux) +set(CMAKE_SYSTEM_PROCESSOR x86_64) + +set(CMAKE_C_COMPILER gcc) +set(CMAKE_CXX_COMPILER g++) + +set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) +set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..a7b3849 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,10 @@ +coverage: + precision: 0 + round: nearest + range: 40...100 + +ignore: + - "app/*" + - "external/*" + - "external/**/*" + - "tests/*" diff --git a/conanfile.py b/conanfile.py new file mode 100644 index 0000000..e0c411a --- /dev/null +++ b/conanfile.py @@ -0,0 +1,19 @@ +from conan import ConanFile +from conan.tools.cmake import CMakeToolchain + + +class CompressorRecipe(ConanFile): + settings = "os", "compiler", "build_type", "arch" + generators = "CMakeDeps" + + def requirements(self): + self.requires("nlohmann_json/3.11.3") + self.requires("fmt/10.0.0") + self.requires("spdlog/1.13.0") + self.requires("catch2/3.5.3") + self.requires("cxxopts/3.1.1") + + def generate(self): + tc = CMakeToolchain(self) + tc.user_presets_path = False + tc.generate() diff --git a/configured/CMakeLists.txt b/configured/CMakeLists.txt new file mode 100644 index 0000000..83e7f3d --- /dev/null +++ b/configured/CMakeLists.txt @@ -0,0 +1,3 @@ +configure_file( + "config.hpp.in" "${CMAKE_BINARY_DIR}/configured_files/include/config.hpp" + ESCAPE_QUOTES) diff --git a/configured/config.hpp.in b/configured/config.hpp.in new file mode 100644 index 0000000..18df7ae --- /dev/null +++ b/configured/config.hpp.in @@ -0,0 +1,11 @@ +#pragma once + +#include +#include + +static constexpr std::string_view project_name = "@PROJECT_NAME@"; +static constexpr std::string_view project_version = "@PROJECT_VERSION@"; +static constexpr std::int32_t project_version_major{ @PROJECT_VERSION_MAJOR@ }; +static constexpr std::int32_t project_version_minor{ @PROJECT_VERSION_MINOR@ }; +static constexpr std::int32_t project_version_patch{ @PROJECT_VERSION_PATCH@ }; +static constexpr std::string_view git_sha = "@GIT_SHA@"; diff --git a/docs/Doxyfile b/docs/Doxyfile new file mode 100644 index 0000000..4d78309 --- /dev/null +++ b/docs/Doxyfile @@ -0,0 +1,37 @@ +# Configuration for Doxygen for use with CMake +# Only options that deviate from the default are included +# To create a new Doxyfile containing all available options, call `doxygen -g` + +#--------------------------------------------------------------------------- +# Project related configuration options +#--------------------------------------------------------------------------- +DOXYFILE_ENCODING = UTF-8 +PROJECT_NAME = "C++ Project Template" +PROJECT_NUMBER = 1.0.0 +PROJECT_BRIEF = +PROJECT_LOGO = +OUTPUT_DIRECTORY = ./ +OUTPUT_LANGUAGE = English +MARKDOWN_SUPPORT = YES + +#--------------------------------------------------------------------------- +# Build related configuration options +#--------------------------------------------------------------------------- +EXTRACT_ALL = YES +RECURSIVE = YES +GENERATE_HTML = YES +GENERATE_LATEX = NO + +#--------------------------------------------------------------------------- +# Configuration options related to the input files +#--------------------------------------------------------------------------- +INPUT = ../src \ +INPUT ../include +INPUT_ENCODING = UTF-8 +FILE_PATTERNS = *.c \ + *.cc \ + *.cpp \ + *.h \ + *.hpp \ + *.md \ + *.txt diff --git a/external/CMakeLists.txt b/external/CMakeLists.txt new file mode 100644 index 0000000..e69de29 diff --git a/external/vcpkg b/external/vcpkg new file mode 160000 index 0000000..4cb4a5c --- /dev/null +++ b/external/vcpkg @@ -0,0 +1 @@ +Subproject commit 4cb4a5c5ddcb9de0c83c85837ee6974c8333f032 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 0000000..97b3359 --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,2 @@ +add_subdirectory(foo) +add_subdirectory(bar) diff --git a/src/bar/CMakeLists.txt b/src/bar/CMakeLists.txt new file mode 100644 index 0000000..e0d5e7a --- /dev/null +++ b/src/bar/CMakeLists.txt @@ -0,0 +1,34 @@ +set(LIBRARY_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/bar.cc") +set(LIBRARY_HEADERS "${CMAKE_CURRENT_SOURCE_DIR}/bar.h") +set(LIBRARY_INCLUDES "./" "${CMAKE_BINARY_DIR}/configured_files/include") + +add_library("LibBar" STATIC ${LIBRARY_SOURCES} ${LIBRARY_HEADERS}) +target_include_directories("LibBar" PUBLIC ${LIBRARY_INCLUDES}) +target_link_libraries( + "LibBar" + PRIVATE nlohmann_json::nlohmann_json + fmt::fmt + spdlog::spdlog + cxxopts::cxxopts) + +if(${ENABLE_WARNINGS}) + target_set_warnings( + TARGET + "LibBar" + ENABLE + ${ENABLE_WARNINGS} + AS_ERRORS + ${ENABLE_WARNINGS_AS_ERRORS}) +endif() + +if(${ENABLE_LTO}) + target_enable_lto( + TARGET + "LibBar" + ENABLE + ON) +endif() + +if(${ENABLE_CLANG_TIDY}) + add_clang_tidy_to_target("LibBar") +endif() diff --git a/src/bar/bar.cc b/src/bar/bar.cc new file mode 100644 index 0000000..937d9ab --- /dev/null +++ b/src/bar/bar.cc @@ -0,0 +1,13 @@ +#include + +#include "nlohmann/json.hpp" + +#include "bar.h" + +int fn_branch(bool do_branch1, bool do_branch2) +{ + if (do_branch1 || do_branch2) + return 0; + + return 1; +} diff --git a/src/bar/bar.h b/src/bar/bar.h new file mode 100644 index 0000000..2bb392b --- /dev/null +++ b/src/bar/bar.h @@ -0,0 +1,9 @@ +#pragma once + +template +T summing(T number1, T number2) +{ + return number1 + number2; +} + +int fn_branch(bool do_branch1, bool do_branch2); diff --git a/src/foo/CMakeLists.txt b/src/foo/CMakeLists.txt new file mode 100644 index 0000000..d66927d --- /dev/null +++ b/src/foo/CMakeLists.txt @@ -0,0 +1,34 @@ +set(LIBRARY_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/foo.cc") +set(LIBRARY_HEADERS "${CMAKE_CURRENT_SOURCE_DIR}/foo.h") +set(LIBRARY_INCLUDES "./" "${CMAKE_BINARY_DIR}/configured_files/include") + +add_library("LibFoo" STATIC ${LIBRARY_SOURCES} ${LIBRARY_HEADERS}) +target_include_directories("LibFoo" PUBLIC ${LIBRARY_INCLUDES}) +target_link_libraries( + "LibFoo" + PRIVATE nlohmann_json::nlohmann_json + fmt::fmt + spdlog::spdlog + cxxopts::cxxopts) + +if(${ENABLE_WARNINGS}) + target_set_warnings( + TARGET + "LibFoo" + ENABLE + ${ENABLE_WARNINGS} + AS_ERRORS + ${ENABLE_WARNINGS_AS_ERRORS}) +endif() + +if(${ENABLE_LTO}) + target_enable_lto( + TARGET + "LibFoo" + ENABLE + ON) +endif() + +if(${ENABLE_CLANG_TIDY}) + add_clang_tidy_to_target("LibFoo") +endif() diff --git a/src/foo/foo.cc b/src/foo/foo.cc new file mode 100644 index 0000000..b4ccff7 --- /dev/null +++ b/src/foo/foo.cc @@ -0,0 +1,18 @@ +#include + +#include "nlohmann/json.hpp" + +#include "foo.h" + +int print_hello_world() +{ + std::cout << "Cout: Hello World" << '\n'; + std::cout << NLOHMANN_JSON_VERSION_MAJOR << '\n'; + + return 1; +} + +unsigned int factorial(unsigned int number) +{ + return number <= 1 ? 1 : factorial(number - 1) * number; +} diff --git a/src/foo/foo.h b/src/foo/foo.h new file mode 100644 index 0000000..e54fbe9 --- /dev/null +++ b/src/foo/foo.h @@ -0,0 +1,15 @@ +#pragma once + +/** + * @brief Prints hello world the console. + * + */ +int print_hello_world(); + +/** + * @brief Compute n faculty (n!) + * + * @param number Input number n + * @return Faculty of n + */ +unsigned int factorial(unsigned int number); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..3de49f9 --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,53 @@ +include(Catch) + +add_executable("UnitTestFoo" "test_foo.cc") +target_link_libraries("UnitTestFoo" PUBLIC "LibFoo") +target_link_libraries("UnitTestFoo" PRIVATE Catch2::Catch2WithMain) + +add_executable("UnitTestBar" "test_bar.cc") +target_link_libraries("UnitTestBar" PUBLIC "LibBar") +target_link_libraries("UnitTestBar" PRIVATE Catch2::Catch2WithMain) + +if(${ENABLE_WARNINGS}) + target_set_warnings( + TARGET + "UnitTestFoo" + ENABLE + ${ENABLE_WARNINGS} + AS_ERRORS + ${ENABLE_WARNINGS_AS_ERRORS}) + target_set_warnings( + TARGET + "UnitTestBar" + ENABLE + ${ENABLE_WARNINGS} + AS_ERRORS + ${ENABLE_WARNINGS_AS_ERRORS}) +endif() + +catch_discover_tests("UnitTestFoo") +catch_discover_tests("UnitTestBar") + +if(ENABLE_COVERAGE) + set(COVERAGE_MAIN "coverage") + set(COVERAGE_EXCLUDES + "${PROJECT_SOURCE_DIR}/app/*" + "${PROJECT_SOURCE_DIR}/cmake/*" + "${PROJECT_SOURCE_DIR}/docs/*" + "${PROJECT_SOURCE_DIR}/external/*" + "${PROJECT_SOURCE_DIR}/tests/*" + "${PROJECT_SOURCE_DIR}/build/*" + "/usr/include/*") + set(COVERAGE_EXTRA_FLAGS) + set(COVERAGE_DEPENDENCIES "UnitTestFoo" "UnitTestBar") + + setup_target_for_coverage_gcovr_html( + NAME + ${COVERAGE_MAIN} + EXECUTABLE + ctest + DEPENDENCIES + ${COVERAGE_DEPENDENCIES} + BASE_DIRECTORY + ${CMAKE_SOURCE_DIR}) +endif() diff --git a/tests/test_bar.cc b/tests/test_bar.cc new file mode 100644 index 0000000..1a6b61d --- /dev/null +++ b/tests/test_bar.cc @@ -0,0 +1,17 @@ +#include + +#include "bar.h" + +TEST_CASE("sum1") +{ + REQUIRE(summing(0, 1) == 1); + REQUIRE(summing(1, 0) == 1); +} + +TEST_CASE("branch1") +{ + REQUIRE(fn_branch(true, false) == 0); + REQUIRE(fn_branch(true, true) == 0); + REQUIRE(fn_branch(false, true) == 0); + REQUIRE(fn_branch(false, false) == 1); +} diff --git a/tests/test_foo.cc b/tests/test_foo.cc new file mode 100644 index 0000000..e35b861 --- /dev/null +++ b/tests/test_foo.cc @@ -0,0 +1,17 @@ +#include + +#include "foo.h" + +TEST_CASE("factorial1") +{ + REQUIRE(factorial(0) == 1); + REQUIRE(factorial(1) == 1); + REQUIRE(factorial(2) == 2); + REQUIRE(factorial(3) == 6); + REQUIRE(factorial(10) == 3628800); +} + +TEST_CASE("helloworld1") +{ + REQUIRE(print_hello_world() == 1); +} diff --git a/tools/run-clang-format.py b/tools/run-clang-format.py new file mode 100644 index 0000000..1dea8ab --- /dev/null +++ b/tools/run-clang-format.py @@ -0,0 +1,408 @@ +#!/usr/bin/env python +"""A wrapper script around clang-format, suitable for linting multiple files +and to use for continuous integration. + +This is an alternative API for the clang-format command line. +It runs over multiple files and directories in parallel. +A diff output is produced and a sensible exit code is returned. + +""" + +from __future__ import print_function, unicode_literals + +import argparse +import codecs +import difflib +import fnmatch +import io +import errno +import multiprocessing +import os +import signal +import subprocess +import sys +import traceback + +from functools import partial + +try: + from subprocess import DEVNULL # py3k +except ImportError: + DEVNULL = open(os.devnull, "wb") + + +DEFAULT_EXTENSIONS = 'c,h,C,H,cpp,hpp,cc,hh,c++,h++,cxx,hxx' +DEFAULT_CLANG_FORMAT_IGNORE = '.clang-format-ignore' + + +class ExitStatus: + SUCCESS = 0 + DIFF = 1 + TROUBLE = 2 + +def excludes_from_file(ignore_file): + excludes = [] + try: + with io.open(ignore_file, 'r', encoding='utf-8') as f: + for line in f: + if line.startswith('#'): + # ignore comments + continue + pattern = line.rstrip() + if not pattern: + # allow empty lines + continue + excludes.append(pattern) + except EnvironmentError as e: + if e.errno != errno.ENOENT: + raise + return excludes + +def list_files(files, recursive=False, extensions=None, exclude=None): + if extensions is None: + extensions = [] + if exclude is None: + exclude = [] + + out = [] + for file in files: + if recursive and os.path.isdir(file): + for dirpath, dnames, fnames in os.walk(file): + fpaths = [os.path.join(dirpath, fname) for fname in fnames] + for pattern in exclude: + # os.walk() supports trimming down the dnames list + # by modifying it in-place, + # to avoid unnecessary directory listings. + dnames[:] = [ + x for x in dnames + if + not fnmatch.fnmatch(os.path.join(dirpath, x), pattern) + ] + fpaths = [ + x for x in fpaths if not fnmatch.fnmatch(x, pattern) + ] + for f in fpaths: + ext = os.path.splitext(f)[1][1:] + if ext in extensions: + out.append(f) + else: + out.append(file) + return out + + +def make_diff(file, original, reformatted): + return list( + difflib.unified_diff( + original, + reformatted, + fromfile='{}\t(original)'.format(file), + tofile='{}\t(reformatted)'.format(file), + n=3)) + + +class DiffError(Exception): + def __init__(self, message, errs=None): + super(DiffError, self).__init__(message) + self.errs = errs or [] + + +class UnexpectedError(Exception): + def __init__(self, message, exc=None): + super(UnexpectedError, self).__init__(message) + self.formatted_traceback = traceback.format_exc() + self.exc = exc + + +def run_clang_format_diff_wrapper(args, file): + try: + ret = run_clang_format_diff(args, file) + return ret + except DiffError: + raise + except Exception as e: + raise UnexpectedError('{}: {}: {}'.format(file, e.__class__.__name__, + e), e) + + +def run_clang_format_diff(args, file): + try: + with io.open(file, 'r', encoding='utf-8') as f: + original = f.readlines() + except IOError as exc: + raise DiffError(str(exc)) + + if args.in_place: + invocation = [args.clang_format_executable, '-i', file] + else: + invocation = [args.clang_format_executable, file] + + if args.style: + invocation.extend(['--style', args.style]) + + if args.dry_run: + print(" ".join(invocation)) + return [], [] + + # Use of utf-8 to decode the process output. + # + # Hopefully, this is the correct thing to do. + # + # It's done due to the following assumptions (which may be incorrect): + # - clang-format will returns the bytes read from the files as-is, + # without conversion, and it is already assumed that the files use utf-8. + # - if the diagnostics were internationalized, they would use utf-8: + # > Adding Translations to Clang + # > + # > Not possible yet! + # > Diagnostic strings should be written in UTF-8, + # > the client can translate to the relevant code page if needed. + # > Each translation completely replaces the format string + # > for the diagnostic. + # > -- http://clang.llvm.org/docs/InternalsManual.html#internals-diag-translation + # + # It's not pretty, due to Python 2 & 3 compatibility. + encoding_py3 = {} + if sys.version_info[0] >= 3: + encoding_py3['encoding'] = 'utf-8' + + try: + proc = subprocess.Popen( + invocation, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + **encoding_py3) + except OSError as exc: + raise DiffError( + "Command '{}' failed to start: {}".format( + subprocess.list2cmdline(invocation), exc + ) + ) + proc_stdout = proc.stdout + proc_stderr = proc.stderr + if sys.version_info[0] < 3: + # make the pipes compatible with Python 3, + # reading lines should output unicode + encoding = 'utf-8' + proc_stdout = codecs.getreader(encoding)(proc_stdout) + proc_stderr = codecs.getreader(encoding)(proc_stderr) + # hopefully the stderr pipe won't get full and block the process + outs = list(proc_stdout.readlines()) + errs = list(proc_stderr.readlines()) + proc.wait() + if proc.returncode: + raise DiffError( + "Command '{}' returned non-zero exit status {}".format( + subprocess.list2cmdline(invocation), proc.returncode + ), + errs, + ) + if args.in_place: + return [], errs + return make_diff(file, original, outs), errs + + +def bold_red(s): + return '\x1b[1m\x1b[31m' + s + '\x1b[0m' + + +def colorize(diff_lines): + def bold(s): + return '\x1b[1m' + s + '\x1b[0m' + + def cyan(s): + return '\x1b[36m' + s + '\x1b[0m' + + def green(s): + return '\x1b[32m' + s + '\x1b[0m' + + def red(s): + return '\x1b[31m' + s + '\x1b[0m' + + for line in diff_lines: + if line[:4] in ['--- ', '+++ ']: + yield bold(line) + elif line.startswith('@@ '): + yield cyan(line) + elif line.startswith('+'): + yield green(line) + elif line.startswith('-'): + yield red(line) + else: + yield line + + +def print_diff(diff_lines, use_color): + if use_color: + diff_lines = colorize(diff_lines) + if sys.version_info[0] < 3: + sys.stdout.writelines((l.encode('utf-8') for l in diff_lines)) + else: + sys.stdout.writelines(diff_lines) + + +def print_trouble(prog, message, use_colors): + error_text = 'error:' + if use_colors: + error_text = bold_red(error_text) + print("{}: {} {}".format(prog, error_text, message), file=sys.stderr) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + '--clang-format-executable', + metavar='EXECUTABLE', + help='path to the clang-format executable', + default='clang-format') + parser.add_argument( + '--extensions', + help='comma separated list of file extensions (default: {})'.format( + DEFAULT_EXTENSIONS), + default=DEFAULT_EXTENSIONS) + parser.add_argument( + '-r', + '--recursive', + action='store_true', + help='run recursively over directories') + parser.add_argument( + '-d', + '--dry-run', + action='store_true', + help='just print the list of files') + parser.add_argument( + '-i', + '--in-place', + action='store_true', + help='format file instead of printing differences') + parser.add_argument('files', metavar='file', nargs='+') + parser.add_argument( + '-q', + '--quiet', + action='store_true', + help="disable output, useful for the exit code") + parser.add_argument( + '-j', + metavar='N', + type=int, + default=0, + help='run N clang-format jobs in parallel' + ' (default number of cpus + 1)') + parser.add_argument( + '--color', + default='auto', + choices=['auto', 'always', 'never'], + help='show colored diff (default: auto)') + parser.add_argument( + '-e', + '--exclude', + metavar='PATTERN', + action='append', + default=[], + help='exclude paths matching the given glob-like pattern(s)' + ' from recursive search') + parser.add_argument( + '--style', + help='formatting style to apply (LLVM, Google, Chromium, Mozilla, WebKit)') + + args = parser.parse_args() + + # use default signal handling, like diff return SIGINT value on ^C + # https://bugs.python.org/issue14229#msg156446 + signal.signal(signal.SIGINT, signal.SIG_DFL) + try: + signal.SIGPIPE + except AttributeError: + # compatibility, SIGPIPE does not exist on Windows + pass + else: + signal.signal(signal.SIGPIPE, signal.SIG_DFL) + + colored_stdout = False + colored_stderr = False + if args.color == 'always': + colored_stdout = True + colored_stderr = True + elif args.color == 'auto': + colored_stdout = sys.stdout.isatty() + colored_stderr = sys.stderr.isatty() + + version_invocation = [args.clang_format_executable, str("--version")] + try: + subprocess.check_call(version_invocation, stdout=DEVNULL) + except subprocess.CalledProcessError as e: + print_trouble(parser.prog, str(e), use_colors=colored_stderr) + return ExitStatus.TROUBLE + except OSError as e: + print_trouble( + parser.prog, + "Command '{}' failed to start: {}".format( + subprocess.list2cmdline(version_invocation), e + ), + use_colors=colored_stderr, + ) + return ExitStatus.TROUBLE + + retcode = ExitStatus.SUCCESS + + excludes = excludes_from_file(DEFAULT_CLANG_FORMAT_IGNORE) + excludes.extend(args.exclude) + + files = list_files( + args.files, + recursive=args.recursive, + exclude=excludes, + extensions=args.extensions.split(',')) + + if not files: + return + + njobs = args.j + if njobs == 0: + njobs = multiprocessing.cpu_count() + 1 + njobs = min(len(files), njobs) + + if njobs == 1: + # execute directly instead of in a pool, + # less overhead, simpler stacktraces + it = (run_clang_format_diff_wrapper(args, file) for file in files) + pool = None + else: + pool = multiprocessing.Pool(njobs) + it = pool.imap_unordered( + partial(run_clang_format_diff_wrapper, args), files) + pool.close() + while True: + try: + outs, errs = next(it) + except StopIteration: + break + except DiffError as e: + print_trouble(parser.prog, str(e), use_colors=colored_stderr) + retcode = ExitStatus.TROUBLE + sys.stderr.writelines(e.errs) + except UnexpectedError as e: + print_trouble(parser.prog, str(e), use_colors=colored_stderr) + sys.stderr.write(e.formatted_traceback) + retcode = ExitStatus.TROUBLE + # stop at the first unexpected error, + # something could be very wrong, + # don't process all files unnecessarily + if pool: + pool.terminate() + break + else: + sys.stderr.writelines(errs) + if outs == []: + continue + if not args.quiet: + print_diff(outs, use_color=colored_stdout) + if retcode == ExitStatus.SUCCESS: + retcode = ExitStatus.DIFF + if pool: + pool.join() + return retcode + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/tools/run-clang-tidy.py b/tools/run-clang-tidy.py new file mode 100644 index 0000000..815c2ea --- /dev/null +++ b/tools/run-clang-tidy.py @@ -0,0 +1,493 @@ +#!/usr/bin/env python3 +# +# ===- run-clang-tidy.py - Parallel clang-tidy runner --------*- python -*--===# +# +# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +# See https://llvm.org/LICENSE.txt for license information. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +# +# ===-----------------------------------------------------------------------===# +# FIXME: Integrate with clang-tidy-diff.py + + +""" +Parallel clang-tidy runner +========================== + +Runs clang-tidy over all files in a compilation database. Requires clang-tidy +and clang-apply-replacements in $PATH. + +Example invocations. +- Run clang-tidy on all files in the current working directory with a default + set of checks and show warnings in the cpp files and all project headers. + run-clang-tidy.py $PWD + +- Fix all header guards. + run-clang-tidy.py -fix -checks=-*,llvm-header-guard + +- Fix all header guards included from clang-tidy and header guards + for clang-tidy headers. + run-clang-tidy.py -fix -checks=-*,llvm-header-guard extra/clang-tidy \ + -header-filter=extra/clang-tidy + +Compilation database setup: +http://clang.llvm.org/docs/HowToSetupToolingForLLVM.html +""" + +from __future__ import print_function + +import argparse +import glob +import json +import multiprocessing +import os +import queue +import re +import shutil +import subprocess +import sys +import tempfile +import threading +import traceback + +try: + import yaml +except ImportError: + yaml = None + + +def strtobool(val): + """Convert a string representation of truth to a bool following LLVM's CLI argument parsing.""" + + val = val.lower() + if val in ["", "true", "1"]: + return True + elif val in ["false", "0"]: + return False + + # Return ArgumentTypeError so that argparse does not substitute its own error message + raise argparse.ArgumentTypeError( + "'{}' is invalid value for boolean argument! Try 0 or 1.".format(val) + ) + + +def find_compilation_database(path): + """Adjusts the directory until a compilation database is found.""" + result = os.path.realpath("./") + while not os.path.isfile(os.path.join(result, path)): + parent = os.path.dirname(result) + if result == parent: + print("Error: could not find compilation database!") + sys.exit(1) + result = parent + return result + + +def make_absolute(f, directory): + if os.path.isabs(f): + return f + return os.path.normpath(os.path.join(directory, f)) + + +def get_tidy_invocation( + f, + clang_tidy_binary, + checks, + tmpdir, + build_path, + header_filter, + allow_enabling_alpha_checkers, + extra_arg, + extra_arg_before, + quiet, + config_file_path, + config, + line_filter, + use_color, + plugins, +): + """Gets a command line for clang-tidy.""" + start = [clang_tidy_binary] + if allow_enabling_alpha_checkers: + start.append("-allow-enabling-analyzer-alpha-checkers") + if header_filter is not None: + start.append("-header-filter=" + header_filter) + if line_filter is not None: + start.append("-line-filter=" + line_filter) + if use_color is not None: + if use_color: + start.append("--use-color") + else: + start.append("--use-color=false") + if checks: + start.append("-checks=" + checks) + if tmpdir is not None: + start.append("-export-fixes") + # Get a temporary file. We immediately close the handle so clang-tidy can + # overwrite it. + (handle, name) = tempfile.mkstemp(suffix=".yaml", dir=tmpdir) + os.close(handle) + start.append(name) + for arg in extra_arg: + start.append("-extra-arg=%s" % arg) + for arg in extra_arg_before: + start.append("-extra-arg-before=%s" % arg) + start.append("-p=" + build_path) + if quiet: + start.append("-quiet") + if config_file_path: + start.append("--config-file=" + config_file_path) + elif config: + start.append("-config=" + config) + for plugin in plugins: + start.append("-load=" + plugin) + start.append(f) + return start + + +def merge_replacement_files(tmpdir, mergefile): + """Merge all replacement files in a directory into a single file""" + # The fixes suggested by clang-tidy >= 4.0.0 are given under + # the top level key 'Diagnostics' in the output yaml files + mergekey = "Diagnostics" + merged = [] + for replacefile in glob.iglob(os.path.join(tmpdir, "*.yaml")): + content = yaml.safe_load(open(replacefile, "r")) + if not content: + continue # Skip empty files. + merged.extend(content.get(mergekey, [])) + + if merged: + # MainSourceFile: The key is required by the definition inside + # include/clang/Tooling/ReplacementsYaml.h, but the value + # is actually never used inside clang-apply-replacements, + # so we set it to '' here. + output = {"MainSourceFile": "", mergekey: merged} + with open(mergefile, "w") as out: + yaml.safe_dump(output, out) + else: + # Empty the file: + open(mergefile, "w").close() + + +def find_binary(arg, name, build_path): + """Get the path for a binary or exit""" + if arg: + if shutil.which(arg): + return arg + else: + raise SystemExit( + "error: passed binary '{}' was not found or is not executable".format( + arg + ) + ) + + built_path = os.path.join(build_path, "bin", name) + binary = shutil.which(name) or shutil.which(built_path) + if binary: + return binary + else: + raise SystemExit( + "error: failed to find {} in $PATH or at {}".format(name, built_path) + ) + + +def apply_fixes(args, clang_apply_replacements_binary, tmpdir): + """Calls clang-apply-fixes on a given directory.""" + invocation = [clang_apply_replacements_binary] + invocation.append("-ignore-insert-conflict") + if args.format: + invocation.append("-format") + if args.style: + invocation.append("-style=" + args.style) + invocation.append(tmpdir) + subprocess.call(invocation) + + +def run_tidy(args, clang_tidy_binary, tmpdir, build_path, queue, lock, failed_files): + """Takes filenames out of queue and runs clang-tidy on them.""" + while True: + name = queue.get() + invocation = get_tidy_invocation( + name, + clang_tidy_binary, + args.checks, + tmpdir, + build_path, + args.header_filter, + args.allow_enabling_alpha_checkers, + args.extra_arg, + args.extra_arg_before, + args.quiet, + args.config_file, + args.config, + args.line_filter, + args.use_color, + args.plugins, + ) + + proc = subprocess.Popen( + invocation, stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) + output, err = proc.communicate() + if proc.returncode != 0: + if proc.returncode < 0: + msg = "%s: terminated by signal %d\n" % (name, -proc.returncode) + err += msg.encode("utf-8") + failed_files.append(name) + with lock: + sys.stdout.write(" ".join(invocation) + "\n" + output.decode("utf-8")) + if len(err) > 0: + sys.stdout.flush() + sys.stderr.write(err.decode("utf-8")) + queue.task_done() + + +def main(): + parser = argparse.ArgumentParser( + description="Runs clang-tidy over all files " + "in a compilation database. Requires " + "clang-tidy and clang-apply-replacements in " + "$PATH or in your build directory." + ) + parser.add_argument( + "-allow-enabling-alpha-checkers", + action="store_true", + help="allow alpha checkers from " "clang-analyzer.", + ) + parser.add_argument( + "-clang-tidy-binary", metavar="PATH", help="path to clang-tidy binary" + ) + parser.add_argument( + "-clang-apply-replacements-binary", + metavar="PATH", + help="path to clang-apply-replacements binary", + ) + parser.add_argument( + "-checks", + default=None, + help="checks filter, when not specified, use clang-tidy " "default", + ) + config_group = parser.add_mutually_exclusive_group() + config_group.add_argument( + "-config", + default=None, + help="Specifies a configuration in YAML/JSON format: " + " -config=\"{Checks: '*', " + ' CheckOptions: {x: y}}" ' + "When the value is empty, clang-tidy will " + "attempt to find a file named .clang-tidy for " + "each source file in its parent directories.", + ) + config_group.add_argument( + "-config-file", + default=None, + help="Specify the path of .clang-tidy or custom config " + "file: e.g. -config-file=/some/path/myTidyConfigFile. " + "This option internally works exactly the same way as " + "-config option after reading specified config file. " + "Use either -config-file or -config, not both.", + ) + parser.add_argument( + "-header-filter", + default=None, + help="regular expression matching the names of the " + "headers to output diagnostics from. Diagnostics from " + "the main file of each translation unit are always " + "displayed.", + ) + parser.add_argument( + "-line-filter", + default=None, + help="List of files with line ranges to filter the" "warnings.", + ) + if yaml: + parser.add_argument( + "-export-fixes", + metavar="filename", + dest="export_fixes", + help="Create a yaml file to store suggested fixes in, " + "which can be applied with clang-apply-replacements.", + ) + parser.add_argument( + "-j", + type=int, + default=0, + help="number of tidy instances to be run in parallel.", + ) + parser.add_argument( + "files", nargs="*", default=[".*"], help="files to be processed (regex on path)" + ) + parser.add_argument("-fix", action="store_true", help="apply fix-its") + parser.add_argument( + "-format", action="store_true", help="Reformat code " "after applying fixes" + ) + parser.add_argument( + "-style", + default="file", + help="The style of reformat " "code after applying fixes", + ) + parser.add_argument( + "-use-color", + type=strtobool, + nargs="?", + const=True, + help="Use colors in diagnostics, overriding clang-tidy's" + " default behavior. This option overrides the 'UseColor" + "' option in .clang-tidy file, if any.", + ) + parser.add_argument( + "-p", dest="build_path", help="Path used to read a compile command database." + ) + parser.add_argument( + "-extra-arg", + dest="extra_arg", + action="append", + default=[], + help="Additional argument to append to the compiler " "command line.", + ) + parser.add_argument( + "-extra-arg-before", + dest="extra_arg_before", + action="append", + default=[], + help="Additional argument to prepend to the compiler " "command line.", + ) + parser.add_argument( + "-quiet", action="store_true", help="Run clang-tidy in quiet mode" + ) + parser.add_argument( + "-load", + dest="plugins", + action="append", + default=[], + help="Load the specified plugin in clang-tidy.", + ) + args = parser.parse_args() + + db_path = "compile_commands.json" + + if args.build_path is not None: + build_path = args.build_path + else: + # Find our database + build_path = find_compilation_database(db_path) + + clang_tidy_binary = find_binary(args.clang_tidy_binary, "clang-tidy", build_path) + + tmpdir = None + if args.fix or (yaml and args.export_fixes): + clang_apply_replacements_binary = find_binary( + args.clang_apply_replacements_binary, "clang-apply-replacements", build_path + ) + tmpdir = tempfile.mkdtemp() + + try: + invocation = get_tidy_invocation( + "", + clang_tidy_binary, + args.checks, + None, + build_path, + args.header_filter, + args.allow_enabling_alpha_checkers, + args.extra_arg, + args.extra_arg_before, + args.quiet, + args.config_file, + args.config, + args.line_filter, + args.use_color, + args.plugins, + ) + invocation.append("-list-checks") + invocation.append("-") + if args.quiet: + # Even with -quiet we still want to check if we can call clang-tidy. + with open(os.devnull, "w") as dev_null: + subprocess.check_call(invocation, stdout=dev_null) + else: + subprocess.check_call(invocation) + except: + print("Unable to run clang-tidy.", file=sys.stderr) + sys.exit(1) + + # Load the database and extract all files. + database = json.load(open(os.path.join(build_path, db_path))) + files = set( + [make_absolute(entry["file"], entry["directory"]) for entry in database] + ) + + max_task = args.j + if max_task == 0: + max_task = multiprocessing.cpu_count() + + # Build up a big regexy filter from all command line arguments. + file_name_re = re.compile("|".join(args.files)) + + return_code = 0 + try: + # Spin up a bunch of tidy-launching threads. + task_queue = queue.Queue(max_task) + # List of files with a non-zero return code. + failed_files = [] + lock = threading.Lock() + for _ in range(max_task): + t = threading.Thread( + target=run_tidy, + args=( + args, + clang_tidy_binary, + tmpdir, + build_path, + task_queue, + lock, + failed_files, + ), + ) + t.daemon = True + t.start() + + # Fill the queue with files. + for name in files: + if file_name_re.search(name): + task_queue.put(name) + + # Wait for all threads to be done. + task_queue.join() + if len(failed_files): + return_code = 1 + + except KeyboardInterrupt: + # This is a sad hack. Unfortunately subprocess goes + # bonkers with ctrl-c and we start forking merrily. + print("\nCtrl-C detected, goodbye.") + if tmpdir: + shutil.rmtree(tmpdir) + os.kill(0, 9) + + if yaml and args.export_fixes: + print("Writing fixes to " + args.export_fixes + " ...") + try: + merge_replacement_files(tmpdir, args.export_fixes) + except: + print("Error exporting fixes.\n", file=sys.stderr) + traceback.print_exc() + return_code = 1 + + if args.fix: + print("Applying fixes ...") + try: + apply_fixes(args, clang_apply_replacements_binary, tmpdir) + except: + print("Error applying fixes.\n", file=sys.stderr) + traceback.print_exc() + return_code = 1 + + if tmpdir: + shutil.rmtree(tmpdir) + sys.exit(return_code) + + +if __name__ == "__main__": + main() diff --git a/vcpkg.json b/vcpkg.json new file mode 100644 index 0000000..84acbd3 --- /dev/null +++ b/vcpkg.json @@ -0,0 +1,33 @@ +{ + "name": "cpptemplateproject", + "version-string": "1.0.0", + "dependencies": [ + { + "name": "cxxopts", + "version>=": "3.1.1" + }, + { + "name": "fmt", + "version>=": "9.1.0" + }, + { + "name": "nlohmann-json", + "version>=": "3.11.3" + }, + { + "name": "spdlog", + "version>=": "1.13.0" + }, + { + "name": "catch2", + "version>=": "3.5.2" + } + ], + "overrides": [ + { + "name": "catch2", + "version": "3.5.2" + } + ], + "builtin-baseline": "40619a55c3e76dc4005c8d1b7395071471bb8b96" +}