├── .clang-format ├── .gitignore ├── .vscode ├── launch.json └── settings.json ├── CMakeLists.txt ├── LICENSE.txt ├── README.md ├── cmake ├── CMakeRC.cmake ├── FindBikeshed.cmake ├── FindFilesystem.cmake ├── FindYarn.cmake └── Pitchfork.cmake ├── conanfile.py ├── data ├── CMakeLists.txt ├── spec.bs └── templates │ ├── CMakeLists.txt │ ├── base │ ├── first_example.in.cpp │ ├── first_header.in.hpp │ ├── first_source.in.cpp │ └── first_test.in.cpp │ └── cmake │ ├── examples_cml.in.cmake │ ├── root_cml.in.cmake │ └── src_cml.in.cmake ├── examples ├── CMakeLists.txt └── example1.cpp ├── external ├── CMakeLists.txt ├── args │ ├── CMakeLists.txt │ └── include │ │ └── args.hxx └── mustache │ ├── CMakeLists.txt │ └── include │ └── kainjow │ └── mustache.hpp ├── extras ├── CMakeLists.txt ├── __init__.py ├── pf-cmake │ ├── auto.cmake │ └── entry.cmake ├── pf_conan │ ├── __init__.py │ ├── conanfile.py │ ├── pf_conan │ │ ├── __init__.py │ │ └── cmake.py │ └── test_package │ │ ├── CMakeLists.txt │ │ ├── conanfile.py │ │ └── src │ │ └── program.cpp └── vscode-pitchfork │ ├── .clang-format │ ├── .gitattributes │ ├── .gitignore │ ├── .vscode │ ├── extensions.json │ ├── launch.json │ ├── settings.json │ └── tasks.json │ ├── .vscodeignore │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ ├── cmake.ts │ ├── dirs.ts │ ├── extension.ts │ ├── files.ts │ ├── new.ts │ ├── pr.ts │ ├── settings.ts │ └── test │ │ ├── extension.test.ts │ │ └── index.ts │ ├── tsconfig.json │ ├── tslint.json │ ├── vsc-extension-quickstart.md │ └── yarn.lock ├── pmm.cmake ├── src ├── pf.cpp └── pf │ ├── existing.cpp │ ├── existing.hpp │ ├── existing │ ├── detect_base_dir.cpp │ ├── detect_base_dir.hpp │ ├── update_source_files.cpp │ └── update_source_files.hpp │ ├── file_template.cpp │ ├── file_template.hpp │ ├── fs.cpp │ ├── fs.hpp │ ├── fs │ ├── ascending_iterator.cpp │ ├── ascending_iterator.hpp │ ├── core.cpp │ ├── core.hpp │ ├── glob.cpp │ └── glob.hpp │ ├── new.cpp │ ├── new.hpp │ ├── new │ ├── cmake.cpp │ ├── cmake.hpp │ ├── dirs.cpp │ ├── dirs.hpp │ ├── files.cpp │ ├── files.hpp │ ├── params.hpp │ ├── project.cpp │ └── project.hpp │ ├── pitchfork.cpp │ └── pitchfork.hpp └── tests ├── CMakeLists.txt ├── compare_fs.cpp ├── compare_fs.hpp ├── existing ├── detect_base_dir.cpp ├── sample │ ├── parallel_build │ │ └── CMakeCache.txt.in │ └── project │ │ ├── CMakeLists.txt │ │ ├── build │ │ └── CMakeCache.txt.in │ │ ├── data │ │ └── some_file.txt │ │ └── src │ │ ├── CMakeLists.txt │ │ ├── CMakeLists.txt.after_update │ │ └── project │ │ ├── header1.h │ │ ├── header2.hh │ │ ├── header3.hpp │ │ ├── header4.hxx │ │ ├── header5.h++ │ │ ├── source1.c │ │ ├── source2.cc │ │ ├── source3.cpp │ │ ├── source4.cxx │ │ ├── source5.c++ │ │ └── subfolder │ │ ├── header1.h │ │ ├── header2.hh │ │ ├── header3.hpp │ │ ├── header4.hxx │ │ ├── header5.h++ │ │ ├── source1.c │ │ ├── source2.cc │ │ ├── source3.cpp │ │ ├── source4.cxx │ │ └── source5.c++ └── update_source_files.cpp ├── expected ├── simple-cmake │ ├── CMakeLists.txt │ ├── cmake │ │ └── ignore_in_diff │ ├── docs │ │ └── ignore_in_diff │ ├── examples │ │ ├── CMakeLists.txt │ │ └── example1.cpp │ ├── src │ │ ├── CMakeLists.txt │ │ └── simple │ │ │ ├── simple-cmake.cpp │ │ │ └── simple-cmake.hpp │ ├── tests │ │ └── my_test.cpp │ └── third_party │ │ └── ignore_in_diff └── simple │ ├── docs │ └── ignore_in_diff │ ├── examples │ └── example1.cpp │ ├── src │ └── simple │ │ ├── simple.cpp │ │ └── simple.hpp │ ├── tests │ └── my_test.cpp │ └── third_party │ └── ignore_in_diff ├── generate.cpp └── test-main.cpp /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | Language: Cpp 3 | # BasedOnStyle: WebKit 4 | # Access specifiers are best unindented, to keep them easy to spot: 5 | AccessModifierOffset: -4 6 | # Keep them together. Would be preferable to put all arguments on a new line 7 | AlignAfterOpenBracket: true 8 | # Newline escapes should be aligned, they're prettier that way 9 | AlignEscapedNewlinesLeft: false 10 | # Same with OpenBracket alignment. No need for that. 11 | AlignOperands: false 12 | # Purdy 13 | AlignTrailingComments: true 14 | AlignConsecutiveAssignments: true 15 | AlignConsecutiveDeclarations: true 16 | # Yuck: 17 | AllowAllParametersOfDeclarationOnNextLine: false 18 | AllowShortBlocksOnASingleLine: false 19 | AllowShortCaseLabelsOnASingleLine: false 20 | AllowShortIfStatementsOnASingleLine: false 21 | AllowShortLoopsOnASingleLine: false 22 | AllowShortFunctionsOnASingleLine: true 23 | # Ew, no. 24 | AlwaysBreakAfterDefinitionReturnType: false 25 | # Not mandatory... 26 | AlwaysBreakTemplateDeclarations: true 27 | # Readability: 28 | AlwaysBreakBeforeMultilineStrings: true 29 | # Nice... 30 | BreakBeforeBinaryOperators: All 31 | BreakBeforeTernaryOperators: true 32 | # Yessir 33 | BreakConstructorInitializersBeforeComma: true 34 | # Please, keep it readable: 35 | BinPackParameters: false 36 | BinPackArguments: false 37 | # Not a hard and fast rule, but yeah 38 | ColumnLimit: 100 39 | # Bleh 40 | ConstructorInitializerAllOnOneLineOrOnePerLine: false 41 | # This is a tough one... it can be confusing to seem them alligned with code 42 | ConstructorInitializerIndentWidth: 4 43 | # No, there's a One True Way to align these things 44 | DerivePointerAlignment: false 45 | # No need 46 | IndentCaseLabels: false 47 | # Gross 48 | IndentWrappedFunctionNames: false 49 | IndentFunctionDeclarationAfterType: false 50 | MaxEmptyLinesToKeep: 1 51 | KeepEmptyLinesAtTheStartOfBlocks: true 52 | # Never ever ever 53 | NamespaceIndentation: None 54 | # These feel about right... 55 | PenaltyBreakBeforeFirstCallParameter: 600 56 | PenaltyBreakComment: 300 57 | PenaltyBreakString: 1000 58 | PenaltyBreakFirstLessLess: 120 59 | PenaltyExcessCharacter: 1000000 60 | PenaltyReturnTypeOnItsOwnLine: 60 61 | PointerAlignment: Left 62 | # Keep it away: 63 | SpacesBeforeTrailingComments: 2 64 | Cpp11BracedListStyle: true 65 | Standard: Cpp11 66 | IndentWidth: 4 67 | TabWidth: 8 68 | UseTab: Never 69 | BreakBeforeBraces: Attach 70 | # Used to do this, not anymore 71 | SpacesInParentheses: false 72 | SpacesInSquareBrackets: false 73 | SpacesInAngles: false 74 | SpaceInEmptyParentheses: false 75 | SpacesInCStyleCastParentheses: false 76 | SpaceAfterCStyleCast: false 77 | SpacesInContainerLiterals: true 78 | SpaceBeforeAssignmentOperators: true 79 | ContinuationIndentWidth: 4 80 | CommentPragmas: '^ IWYU pragma:' 81 | ForEachMacros: [ foreach, Q_FOREACH, BOOST_FOREACH ] 82 | SpaceBeforeParens: ControlStatements 83 | DisableFormat: false 84 | ... 85 | --- 86 | Language: JavaScript 87 | # BasedOnStyle: WebKit 88 | # Keep them together. Would be preferable to put all arguments on a new line 89 | AlignAfterOpenBracket: true 90 | # Newline escapes should be aligned, they're prettier that way 91 | AlignEscapedNewlinesLeft: false 92 | # Same with OpenBracket alignment. No need for that. 93 | AlignOperands: false 94 | # Purdy 95 | AlignTrailingComments: true 96 | # Yuck: 97 | AllowAllParametersOfDeclarationOnNextLine: false 98 | AllowShortBlocksOnASingleLine: true 99 | AllowShortCaseLabelsOnASingleLine: false 100 | AllowShortIfStatementsOnASingleLine: false 101 | AllowShortLoopsOnASingleLine: false 102 | AllowShortFunctionsOnASingleLine: true 103 | # Ew, no. 104 | AlwaysBreakAfterDefinitionReturnType: false 105 | # Not mandatory... 106 | AlwaysBreakTemplateDeclarations: false 107 | # Readability: 108 | AlwaysBreakBeforeMultilineStrings: true 109 | # Nice... 110 | BreakBeforeBinaryOperators: All 111 | BreakBeforeTernaryOperators: true 112 | # Yessir 113 | BreakConstructorInitializersBeforeComma: true 114 | # Please, keep it readable: 115 | BinPackParameters: false 116 | BinPackArguments: false 117 | # Not a hard and fast rule, but yeah 118 | ColumnLimit: 120 119 | # No need 120 | IndentCaseLabels: false 121 | # Gross 122 | IndentWrappedFunctionNames: false 123 | IndentFunctionDeclarationAfterType: false 124 | # Two empty lines are useful as physical seperators 125 | MaxEmptyLinesToKeep: 2 126 | KeepEmptyLinesAtTheStartOfBlocks: true 127 | # These feel about right... 128 | PenaltyBreakBeforeFirstCallParameter: 600 129 | PenaltyBreakComment: 300 130 | PenaltyBreakString: 1000 131 | PenaltyBreakFirstLessLess: 120 132 | PenaltyExcessCharacter: 1000000 133 | PenaltyReturnTypeOnItsOwnLine: 60 134 | # Keep it away: 135 | SpacesBeforeTrailingComments: 2 136 | IndentWidth: 2 137 | TabWidth: 8 138 | UseTab: Never 139 | # Attach is the One True Brace Style 140 | BreakBeforeBraces: Attach 141 | # Used to do this, not anymore 142 | SpacesInParentheses: false 143 | SpacesInSquareBrackets: false 144 | SpacesInAngles: false 145 | SpaceInEmptyParentheses: false 146 | SpacesInCStyleCastParentheses: false 147 | SpaceAfterCStyleCast: false 148 | SpacesInContainerLiterals: false 149 | SpaceBeforeAssignmentOperators: true 150 | ContinuationIndentWidth: 4 151 | SpaceBeforeParens: ControlStatements 152 | JavaScriptWrapImports: true 153 | JavaScriptQuotes: Single 154 | DisableFormat: false 155 | ... 156 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | .python-version 3 | *.pyc 4 | _pmm/ -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "(gdb) CMake Launch", 10 | "type": "cppdbg", 11 | "request": "launch", 12 | "program": "${command:cmake.launchTargetPath}", 13 | "args": [], 14 | "stopAtEntry": false, 15 | "cwd": "${workspaceFolder}", 16 | "environment": [], 17 | "externalConsole": true, 18 | "MIMode": "gdb", 19 | "setupCommands": [ 20 | { 21 | "description": "Enable pretty-printing for gdb", 22 | "text": "-enable-pretty-printing", 23 | "ignoreFailures": true 24 | } 25 | ] 26 | }, 27 | { 28 | "name": "Pitchfork New Project", 29 | "type": "cppdbg", 30 | "request": "launch", 31 | "program": "${command:cmake.launchTargetPath}", 32 | "args": [ 33 | "--base-dir", 34 | "${workspaceFolder}/build/pf-dbg-root", 35 | "new" 36 | ], 37 | "stopAtEntry": false, 38 | "cwd": "${workspaceFolder}", 39 | "environment": [], 40 | "externalConsole": true, 41 | "MIMode": "gdb", 42 | "setupCommands": [ 43 | { 44 | "description": "Enable pretty-printing for gdb", 45 | "text": "-enable-pretty-printing", 46 | "ignoreFailures": true 47 | } 48 | ] 49 | }, 50 | { 51 | "name": "VSCode Extension", 52 | "type": "extensionHost", 53 | "request": "launch", 54 | "runtimeExecutable": "${execPath}", 55 | "args": [ 56 | "--extensionDevelopmentPath=${workspaceFolder}/extras/vscode-pitchfork" 57 | ], 58 | "outFiles": [ 59 | "${workspaceFolder}/extras/vscode-pitchfork/out/**/*.js" 60 | ], 61 | "preLaunchTask": "npm: watch" 62 | }, 63 | { 64 | "name": "VSCode Extension Tests", 65 | "type": "extensionHost", 66 | "request": "launch", 67 | "runtimeExecutable": "${execPath}", 68 | "args": [ 69 | "--extensionDevelopmentPath=${workspaceFolder}/extras/vscode-pitchfork", 70 | "--extensionTestsPath=${workspaceFolder}/extras/vscode-pitchfork/out/test" 71 | ], 72 | "outFiles": [ 73 | "${workspaceFolder}/extras/vscode-pitchfork/out/test/**/*.js" 74 | ], 75 | "preLaunchTask": "npm: watch" 76 | } 77 | ] 78 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "C_Cpp.default.configurationProvider": "vector-of-bool.cmake-tools", 3 | "files.associations": { 4 | "*.lix": "elixir", 5 | "array": "cpp", 6 | "string_view": "cpp", 7 | "deque": "cpp", 8 | "unordered_map": "cpp", 9 | "unordered_set": "cpp", 10 | "vector": "cpp", 11 | "initializer_list": "cpp", 12 | "bitset": "cpp", 13 | "*.tcc": "cpp", 14 | "cctype": "cpp", 15 | "chrono": "cpp", 16 | "clocale": "cpp", 17 | "cmath": "cpp", 18 | "codecvt": "cpp", 19 | "cstdarg": "cpp", 20 | "cstddef": "cpp", 21 | "cstdint": "cpp", 22 | "cstdio": "cpp", 23 | "cstdlib": "cpp", 24 | "cstring": "cpp", 25 | "ctime": "cpp", 26 | "cwchar": "cpp", 27 | "cwctype": "cpp", 28 | "list": "cpp", 29 | "exception": "cpp", 30 | "filesystem": "cpp", 31 | "optional": "cpp", 32 | "fstream": "cpp", 33 | "functional": "cpp", 34 | "iomanip": "cpp", 35 | "iosfwd": "cpp", 36 | "iostream": "cpp", 37 | "istream": "cpp", 38 | "limits": "cpp", 39 | "memory": "cpp", 40 | "mutex": "cpp", 41 | "ostream": "cpp", 42 | "numeric": "cpp", 43 | "ratio": "cpp", 44 | "sstream": "cpp", 45 | "stdexcept": "cpp", 46 | "streambuf": "cpp", 47 | "system_error": "cpp", 48 | "cinttypes": "cpp", 49 | "type_traits": "cpp", 50 | "tuple": "cpp", 51 | "typeinfo": "cpp", 52 | "utility": "cpp", 53 | "condition_variable": "cpp", 54 | "new": "cpp", 55 | "thread": "cpp", 56 | "algorithm": "cpp" 57 | } 58 | } -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.12) 2 | 3 | list(INSERT CMAKE_MODULE_PATH 0 "${CMAKE_CURRENT_SOURCE_DIR}/cmake") 4 | 5 | project(pitchfork 6 | VERSION 0.0.1 7 | LANGUAGES C CXX 8 | DESCRIPTION "Libraries and programs for working with Pitchfork Layout projects" 9 | ) 10 | 11 | option(PF_FORCE_TESTS_COLOR "Force colored output in tests" OFF) 12 | 13 | if(NOT CMAKE_CONFIGURATION_TYPES) 14 | set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/bin") 15 | endif() 16 | 17 | option(BUILD_SPEC "Build the Pitchfork document" ON) 18 | 19 | if(NOT CMAKE_CXX_STANDARD) 20 | set(CMAKE_CXX_STANDARD 17 CACHE STRING "") 21 | endif() 22 | 23 | if(CMAKE_CXX_STANDARD LESS 17) 24 | message(WARNING "Incompatible C++ standard ${CMAKE_CXX_STANDARD}. Pitchfork requires C++17") 25 | endif() 26 | 27 | if(CMAKE_CXX_STANDARD) 28 | set(conan_std cppstd=${CMAKE_CXX_STANDARD}) 29 | endif() 30 | 31 | include(pmm.cmake) 32 | pmm(CONAN SETTINGS ${conan_std}) 33 | conan_set_find_paths() 34 | 35 | # Find-package the Boost that Conan gave us 36 | find_package(Boost 1.68.0 REQUIRED) 37 | 38 | # Other deps not controlled by Conan 39 | find_package(Filesystem REQUIRED) 40 | if(NOT HAVE_STD_FILESYSTEM) 41 | message(SEND_ERROR "Pitchfork only builds with C++17 std::filesystem, not std::experimental::filesystem.") 42 | endif() 43 | 44 | if(BUILD_SPEC) 45 | find_package(Bikeshed) 46 | endif() 47 | find_package(Yarn) 48 | include(CMakeRC) 49 | 50 | if(NOT MSVC) 51 | add_compile_options(-Wall -Wextra) 52 | if(PROJECT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR) 53 | add_compile_options(-Werror) 54 | endif() 55 | else() 56 | add_compile_options(/permissive-) 57 | # We want to pass this flag to link.exe. In CMake 3.13, we can use add_link_options 58 | # /IGNORE:4221 # LNK4221 warns for empty cpp files. We use such files. 59 | endif() 60 | 61 | add_subdirectory(data) 62 | 63 | set(PF_URL "file://${PROJECT_SOURCE_DIR}/extras/pf-cmake" CACHE INTERNAL "") 64 | include(Pitchfork) 65 | file(GLOB_RECURSE pf_cmake_files CONFIGURE_DEPENDS extras/pf-cmake/*) 66 | set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${pf_cmake_files}) 67 | 68 | pf_auto( 69 | OUTPUT_NAME pf 70 | ALIAS pf::pitchfork 71 | LINK 72 | CXX::Filesystem 73 | Boost::boost 74 | CONAN_PKG::spdlog 75 | PRIVATE_LINK 76 | kainjow::mustache 77 | pf::templates 78 | EXE_LINK 79 | taywee::args 80 | ) 81 | 82 | include(CPack) 83 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 vector-of-bool 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pitchfork 2 | 3 | Pitchfork is a set of conventions for native C and C++ projects. The most 4 | prominent being [the project layout conventions](https://api.csswg.org/bikeshed/?force=1&url=https://raw.githubusercontent.com/vector-of-bool/pitchfork/develop/data/spec.bs). 5 | 6 | The layout specification document is available in `data/spec.bs`. 7 | 8 | 9 | ## Why the Name *Pitchfork*? 10 | 11 | The very first public unveiling, drafting, and discussion of these project 12 | conventions started with [a Reddit thread entitled "Prepare thy Pitchforks"](https://www.reddit.com/r/cpp/comments/996q8o/prepare_thy_pitchforks_a_de_facto_standard/). Until 13 | that point, I had not chosen any particular name for the conventions, but I 14 | felt "Pitchfork" was as apt a name as any. 15 | 16 | 17 | ## The `pf` Tool 18 | 19 | This repository also hosts a (currently experimental) tool that helps you 20 | create and work with Pitchfork-compliant projects. 21 | 22 | This project is still very young and has a while to go before being a useful 23 | developer tool. Once ready, this README will be updated with proper user 24 | documentation. 25 | 26 | 27 | ## The `pf` Library 28 | 29 | The `pf` tool mentioned above is built upon the `pf` library, also hosted in 30 | this repository. This library can be used to query and manipulate 31 | Pitchfork-compliant projects. 32 | -------------------------------------------------------------------------------- /cmake/CMakeRC.cmake: -------------------------------------------------------------------------------- 1 | # This block is executed when generating an intermediate resource file, not when 2 | # running in CMake configure mode 3 | if(_CMRC_GENERATE_MODE) 4 | # Read in the digits 5 | file(READ "${INPUT_FILE}" bytes HEX) 6 | # Format each pair into a character literal. Heuristics seem to favor doing 7 | # the conversion in groups of five for fastest conversion 8 | string(REGEX REPLACE "(..)(..)(..)(..)(..)" "'\\\\x\\1','\\\\x\\2','\\\\x\\3','\\\\x\\4','\\\\x\\5'," chars "${bytes}") 9 | # Since we did this in groups, we have some leftovers to clean up 10 | string(LENGTH "${bytes}" n_bytes2) 11 | math(EXPR n_bytes "${n_bytes2} / 2") 12 | math(EXPR remainder "${n_bytes} % 5") # <-- '5' is the grouping count from above 13 | set(cleanup_re "$") 14 | set(cleanup_sub ) 15 | while(remainder) 16 | set(cleanup_re "(..)${cleanup_re}") 17 | set(cleanup_sub "'\\\\x\\${remainder}',${cleanup_sub}") 18 | math(EXPR remainder "${remainder} - 1") 19 | endwhile() 20 | if(NOT cleanup_re STREQUAL "$") 21 | string(REGEX REPLACE "${cleanup_re}" "${cleanup_sub}" chars "${chars}") 22 | endif() 23 | string(CONFIGURE [[ 24 | namespace { const char file_array[] = { @chars@ }; } 25 | namespace cmrc { namespace @NAMESPACE@ { namespace res_chars { 26 | extern const char* const @SYMBOL@_begin = file_array; 27 | extern const char* const @SYMBOL@_end = file_array + @n_bytes@; 28 | }}} 29 | ]] code) 30 | file(WRITE "${OUTPUT_FILE}" "${code}") 31 | # Exit from the script. Nothing else needs to be processed 32 | return() 33 | endif() 34 | 35 | set(_version 2.0.0) 36 | 37 | cmake_minimum_required(VERSION 3.3) 38 | include(CMakeParseArguments) 39 | 40 | if(COMMAND cmrc_add_resource_library) 41 | if(NOT DEFINED _CMRC_VERSION OR NOT (_version STREQUAL _CMRC_VERSION)) 42 | message(WARNING "More than one CMakeRC version has been included in this project.") 43 | endif() 44 | # CMakeRC has already been included! Don't do anything 45 | return() 46 | endif() 47 | 48 | set(_CMRC_VERSION "${_version}" CACHE INTERNAL "CMakeRC version. Used for checking for conflicts") 49 | 50 | set(_CMRC_SCRIPT "${CMAKE_CURRENT_LIST_FILE}" CACHE INTERNAL "Path to CMakeRC script") 51 | 52 | function(_cmrc_normalize_path var) 53 | set(path "${${var}}") 54 | file(TO_CMAKE_PATH "${path}" path) 55 | while(path MATCHES "//") 56 | string(REPLACE "//" "/" path "${path}") 57 | endwhile() 58 | string(REGEX REPLACE "/+$" "" path "${path}") 59 | set("${var}" "${path}" PARENT_SCOPE) 60 | endfunction() 61 | 62 | get_filename_component(_inc_dir "${CMAKE_BINARY_DIR}/_cmrc/include" ABSOLUTE) 63 | set(CMRC_INCLUDE_DIR "${_inc_dir}" CACHE INTERNAL "Directory for CMakeRC include files") 64 | # Let's generate the primary include file 65 | file(MAKE_DIRECTORY "${CMRC_INCLUDE_DIR}/cmrc") 66 | set(hpp_content [==[ 67 | #ifndef CMRC_CMRC_HPP_INCLUDED 68 | #define CMRC_CMRC_HPP_INCLUDED 69 | 70 | #include 71 | #include 72 | #include 73 | #include 74 | #include 75 | #include 76 | #include 77 | #include 78 | #include 79 | 80 | namespace cmrc { namespace detail { struct dummy; } } 81 | 82 | #define CMRC_DECLARE(libid) \ 83 | namespace cmrc { namespace detail { \ 84 | struct dummy; \ 85 | static_assert(std::is_same::value, "CMRC_DECLARE() must only appear at the global namespace"); \ 86 | } } \ 87 | namespace cmrc { namespace libid { \ 88 | cmrc::embedded_filesystem get_filesystem(); \ 89 | } } static_assert(true, "") 90 | 91 | namespace cmrc { 92 | 93 | class file { 94 | const char* _begin = nullptr; 95 | const char* _end = nullptr; 96 | 97 | public: 98 | using iterator = const char*; 99 | using const_iterator = iterator; 100 | iterator begin() const noexcept { return _begin; } 101 | iterator cbegin() const noexcept { return _begin; } 102 | iterator end() const noexcept { return _end; } 103 | iterator cend() const noexcept { return _end; } 104 | std::size_t size() const { return std::distance(begin(), end()); } 105 | 106 | file() = default; 107 | file(iterator beg, iterator end) noexcept : _begin(beg), _end(end) {} 108 | }; 109 | 110 | class directory_entry; 111 | 112 | namespace detail { 113 | 114 | class directory; 115 | class file_data; 116 | 117 | class file_or_directory { 118 | union _data_t { 119 | class file_data* file_data; 120 | class directory* directory; 121 | } _data; 122 | bool _is_file = true; 123 | 124 | public: 125 | explicit file_or_directory(file_data& f) { 126 | _data.file_data = &f; 127 | } 128 | explicit file_or_directory(directory& d) { 129 | _data.directory = &d; 130 | _is_file = false; 131 | } 132 | bool is_file() const noexcept { 133 | return _is_file; 134 | } 135 | bool is_directory() const noexcept { 136 | return !is_file(); 137 | } 138 | const directory& as_directory() const noexcept { 139 | assert(!is_file()); 140 | return *_data.directory; 141 | } 142 | const file_data& as_file() const noexcept { 143 | assert(is_file()); 144 | return *_data.file_data; 145 | } 146 | }; 147 | 148 | class file_data { 149 | public: 150 | const char* begin_ptr; 151 | const char* end_ptr; 152 | file_data(const file_data&) = delete; 153 | file_data(const char* b, const char* e) : begin_ptr(b), end_ptr(e) {} 154 | }; 155 | 156 | inline std::pair split_path(const std::string& path) { 157 | auto first_sep = path.find("/"); 158 | if (first_sep == path.npos) { 159 | return std::make_pair(path, ""); 160 | } else { 161 | return std::make_pair(path.substr(0, first_sep), path.substr(first_sep + 1)); 162 | } 163 | } 164 | 165 | struct created_subdirectory { 166 | class directory& directory; 167 | class file_or_directory& index_entry; 168 | }; 169 | 170 | class directory { 171 | std::list _files; 172 | std::list _dirs; 173 | std::map _index; 174 | 175 | using base_iterator = std::map::const_iterator; 176 | 177 | public: 178 | 179 | directory() = default; 180 | directory(const directory&) = delete; 181 | 182 | created_subdirectory add_subdir(std::string name) & { 183 | _dirs.emplace_back(); 184 | auto& back = _dirs.back(); 185 | auto& fod = _index.emplace(name, back).first->second; 186 | return created_subdirectory{back, fod}; 187 | } 188 | 189 | file_or_directory* add_file(std::string name, const char* begin, const char* end) & { 190 | assert(_index.find(name) == _index.end()); 191 | _files.emplace_back(begin, end); 192 | return &_index.emplace(name, _files.back()).first->second; 193 | } 194 | 195 | const file_or_directory* get(const std::string& path) const { 196 | auto pair = split_path(path); 197 | auto child = _index.find(pair.first); 198 | if (child == _index.end()) { 199 | return nullptr; 200 | } 201 | auto& entry = child->second; 202 | if (pair.second.empty()) { 203 | // We're at the end of the path 204 | return &entry; 205 | } 206 | 207 | if (entry.is_file()) { 208 | // We can't traverse into a file. Stop. 209 | return nullptr; 210 | } 211 | // Keep going down 212 | return entry.as_directory().get(pair.second); 213 | } 214 | 215 | class iterator { 216 | base_iterator _base_iter; 217 | base_iterator _end_iter; 218 | public: 219 | using value_type = directory_entry; 220 | using difference_type = std::ptrdiff_t; 221 | using pointer = const value_type*; 222 | using reference = const value_type&; 223 | using iterator_category = std::input_iterator_tag; 224 | 225 | iterator() = default; 226 | explicit iterator(base_iterator iter, base_iterator end) : _base_iter(iter), _end_iter(end) {} 227 | 228 | iterator begin() const noexcept { 229 | return *this; 230 | } 231 | 232 | iterator end() const noexcept { 233 | return iterator(_end_iter, _end_iter); 234 | } 235 | 236 | inline value_type operator*() const noexcept; 237 | 238 | bool operator==(const iterator& rhs) const noexcept { 239 | return _base_iter == rhs._base_iter; 240 | } 241 | 242 | bool operator!=(const iterator& rhs) const noexcept { 243 | return !(*this == rhs); 244 | } 245 | 246 | iterator operator++() noexcept { 247 | auto cp = *this; 248 | ++_base_iter; 249 | return cp; 250 | } 251 | 252 | iterator& operator++(int) noexcept { 253 | ++_base_iter; 254 | return *this; 255 | } 256 | }; 257 | 258 | using const_iterator = iterator; 259 | 260 | iterator begin() const noexcept { 261 | return iterator(_index.begin(), _index.end()); 262 | } 263 | 264 | iterator end() const noexcept { 265 | return iterator(); 266 | } 267 | }; 268 | 269 | inline std::string normalize_path(std::string path) { 270 | while (path.find("/") == 0) { 271 | path.erase(path.begin()); 272 | } 273 | while (!path.empty() && (path.rfind("/") == path.size() - 1)) { 274 | path.pop_back(); 275 | } 276 | auto off = path.npos; 277 | while ((off = path.find("//")) != path.npos) { 278 | path.erase(path.begin() + off); 279 | } 280 | return path; 281 | } 282 | 283 | using index_type = std::map; 284 | 285 | } // detail 286 | 287 | class directory_entry { 288 | std::string _fname; 289 | const detail::file_or_directory* _item; 290 | 291 | public: 292 | directory_entry() = delete; 293 | explicit directory_entry(std::string filename, const detail::file_or_directory& item) 294 | : _fname(filename) 295 | , _item(&item) 296 | {} 297 | 298 | const std::string& filename() const & { 299 | return _fname; 300 | } 301 | std::string filename() const && { 302 | return std::move(_fname); 303 | } 304 | 305 | bool is_file() const { 306 | return _item->is_file(); 307 | } 308 | 309 | bool is_directory() const { 310 | return _item->is_directory(); 311 | } 312 | }; 313 | 314 | directory_entry detail::directory::iterator::operator*() const noexcept { 315 | assert(begin() != end()); 316 | return directory_entry(_base_iter->first, _base_iter->second); 317 | } 318 | 319 | using directory_iterator = detail::directory::iterator; 320 | 321 | class embedded_filesystem { 322 | // Never-null: 323 | const cmrc::detail::index_type* _index; 324 | const detail::file_or_directory* _get(std::string path) const { 325 | path = detail::normalize_path(path); 326 | auto found = _index->find(path); 327 | if (found == _index->end()) { 328 | return nullptr; 329 | } else { 330 | return found->second; 331 | } 332 | } 333 | 334 | public: 335 | explicit embedded_filesystem(const detail::index_type& index) 336 | : _index(&index) 337 | {} 338 | 339 | file open(const std::string& path) const { 340 | auto entry_ptr = _get(path); 341 | if (!entry_ptr || !entry_ptr->is_file()) { 342 | throw std::system_error(make_error_code(std::errc::no_such_file_or_directory), path); 343 | } 344 | auto& dat = entry_ptr->as_file(); 345 | return file{dat.begin_ptr, dat.end_ptr}; 346 | } 347 | 348 | bool is_file(const std::string& path) const noexcept { 349 | auto entry_ptr = _get(path); 350 | return entry_ptr && entry_ptr->is_file(); 351 | } 352 | 353 | bool is_directory(const std::string& path) const noexcept { 354 | auto entry_ptr = _get(path); 355 | return entry_ptr && entry_ptr->is_directory(); 356 | } 357 | 358 | bool exists(const std::string& path) const noexcept { 359 | return !!_get(path); 360 | } 361 | 362 | directory_iterator iterate_directory(const std::string& path) const { 363 | auto entry_ptr = _get(path); 364 | if (!entry_ptr) { 365 | throw std::system_error(make_error_code(std::errc::no_such_file_or_directory), path); 366 | } 367 | if (!entry_ptr->is_directory()) { 368 | throw std::system_error(make_error_code(std::errc::not_a_directory), path); 369 | } 370 | return entry_ptr->as_directory().begin(); 371 | } 372 | }; 373 | 374 | } 375 | 376 | #endif // CMRC_CMRC_HPP_INCLUDED 377 | ]==]) 378 | 379 | set(cmrc_hpp "${CMRC_INCLUDE_DIR}/cmrc/cmrc.hpp" CACHE INTERNAL "") 380 | set(_generate 1) 381 | if(EXISTS "${cmrc_hpp}") 382 | file(READ "${cmrc_hpp}" _current) 383 | if(_current STREQUAL hpp_content) 384 | set(_generate 0) 385 | endif() 386 | endif() 387 | file(GENERATE OUTPUT "${cmrc_hpp}" CONTENT "${hpp_content}" CONDITION ${_generate}) 388 | 389 | add_library(cmrc-base INTERFACE) 390 | target_include_directories(cmrc-base INTERFACE "${CMRC_INCLUDE_DIR}") 391 | # Signal a basic C++11 feature to require C++11. 392 | target_compile_features(cmrc-base INTERFACE cxx_nullptr) 393 | set_property(TARGET cmrc-base PROPERTY INTERFACE_CXX_EXTENSIONS OFF) 394 | add_library(cmrc::base ALIAS cmrc-base) 395 | 396 | function(cmrc_add_resource_library name) 397 | set(args ALIAS NAMESPACE) 398 | cmake_parse_arguments(ARG "" "${args}" "" "${ARGN}") 399 | # Generate the identifier for the resource library's namespace 400 | set(ns_re "[a-zA-Z_][a-zA-Z0-9_]*") 401 | if(NOT DEFINED ARG_NAMESPACE) 402 | # Check that the library name is also a valid namespace 403 | if(NOT name MATCHES "${ns_re}") 404 | message(SEND_ERROR "Library name is not a valid namespace. Specify the NAMESPACE argument") 405 | endif() 406 | set(ARG_NAMESPACE "${name}") 407 | else() 408 | if(NOT ARG_NAMESPACE MATCHES "${ns_re}") 409 | message(SEND_ERROR "NAMESPACE for ${name} is not a valid C++ namespace identifier (${ARG_NAMESPACE})") 410 | endif() 411 | endif() 412 | set(libname "${name}") 413 | # Generate a library with the compiled in character arrays. 414 | string(CONFIGURE [=[ 415 | #include 416 | #include 417 | #include 418 | 419 | namespace cmrc { 420 | namespace @ARG_NAMESPACE@ { 421 | 422 | namespace res_chars { 423 | // These are the files which are available in this resource library 424 | $, 425 | > 426 | } 427 | 428 | namespace { 429 | 430 | const cmrc::detail::index_type& 431 | get_root_index() { 432 | static cmrc::detail::directory root_directory_; 433 | static cmrc::detail::file_or_directory root_directory_fod{root_directory_}; 434 | static cmrc::detail::index_type root_index; 435 | root_index.emplace("", &root_directory_fod); 436 | struct dir_inl { 437 | class cmrc::detail::directory& directory; 438 | }; 439 | dir_inl root_directory_dir{root_directory_}; 440 | (void)root_directory_dir; 441 | $, 442 | > 443 | $, 444 | > 445 | return root_index; 446 | } 447 | 448 | } 449 | 450 | cmrc::embedded_filesystem get_filesystem() { 451 | static auto& index = get_root_index(); 452 | return cmrc::embedded_filesystem{index}; 453 | } 454 | 455 | } // @ARG_NAMESPACE@ 456 | } // cmrc 457 | ]=] cpp_content @ONLY) 458 | get_filename_component(libdir "${CMAKE_CURRENT_BINARY_DIR}/__cmrc_${name}" ABSOLUTE) 459 | get_filename_component(lib_tmp_cpp "${libdir}/lib_.cpp" ABSOLUTE) 460 | string(REPLACE "\n " "\n" cpp_content "${cpp_content}") 461 | file(GENERATE OUTPUT "${lib_tmp_cpp}" CONTENT "${cpp_content}") 462 | get_filename_component(libcpp "${libdir}/lib.cpp" ABSOLUTE) 463 | add_custom_command(OUTPUT "${libcpp}" 464 | DEPENDS "${lib_tmp_cpp}" "${cmrc_hpp}" 465 | COMMAND ${CMAKE_COMMAND} -E copy_if_different "${lib_tmp_cpp}" "${libcpp}" 466 | COMMENT "Generating ${name} resource loader" 467 | ) 468 | # Generate the actual static library. Each source file is just a single file 469 | # with a character array compiled in containing the contents of the 470 | # corresponding resource file. 471 | add_library(${name} STATIC ${libcpp}) 472 | set_property(TARGET ${name} PROPERTY CMRC_LIBDIR "${libdir}") 473 | set_property(TARGET ${name} PROPERTY CMRC_NAMESPACE "${ARG_NAMESPACE}") 474 | target_link_libraries(${name} PUBLIC cmrc::base) 475 | set_property(TARGET ${name} PROPERTY CMRC_IS_RESOURCE_LIBRARY TRUE) 476 | if(ARG_ALIAS) 477 | add_library("${ARG_ALIAS}" ALIAS ${name}) 478 | endif() 479 | cmrc_add_resources(${name} ${ARG_UNPARSED_ARGUMENTS}) 480 | endfunction() 481 | 482 | function(_cmrc_register_dirs name dirpath) 483 | if(dirpath STREQUAL "") 484 | return() 485 | endif() 486 | # Skip this dir if we have already registered it 487 | get_target_property(registered "${name}" _CMRC_REGISTERED_DIRS) 488 | if(dirpath IN_LIST registered) 489 | return() 490 | endif() 491 | # Register the parent directory first 492 | get_filename_component(parent "${dirpath}" DIRECTORY) 493 | if(NOT parent STREQUAL "") 494 | _cmrc_register_dirs("${name}" "${parent}") 495 | endif() 496 | # Now generate the registration 497 | set_property(TARGET "${name}" APPEND PROPERTY _CMRC_REGISTERED_DIRS "${dirpath}") 498 | _cm_encode_fpath(sym "${dirpath}") 499 | if(parent STREQUAL "") 500 | set(parent_sym root_directory) 501 | else() 502 | _cm_encode_fpath(parent_sym "${parent}") 503 | endif() 504 | get_filename_component(leaf "${dirpath}" NAME) 505 | set_property( 506 | TARGET "${name}" 507 | APPEND PROPERTY CMRC_MAKE_DIRS 508 | "static auto ${sym}_dir = ${parent_sym}_dir.directory.add_subdir(\"${leaf}\")\;" 509 | "root_index.emplace(\"${dirpath}\", &${sym}_dir.index_entry)\;" 510 | ) 511 | endfunction() 512 | 513 | function(cmrc_add_resources name) 514 | get_target_property(is_reslib ${name} CMRC_IS_RESOURCE_LIBRARY) 515 | if(NOT TARGET ${name} OR NOT is_reslib) 516 | message(SEND_ERROR "cmrc_add_resources called on target '${name}' which is not an existing resource library") 517 | return() 518 | endif() 519 | 520 | set(options) 521 | set(args WHENCE PREFIX) 522 | set(list_args) 523 | cmake_parse_arguments(ARG "${options}" "${args}" "${list_args}" "${ARGN}") 524 | 525 | if(NOT ARG_WHENCE) 526 | set(ARG_WHENCE ${CMAKE_CURRENT_SOURCE_DIR}) 527 | endif() 528 | _cmrc_normalize_path(ARG_WHENCE) 529 | get_filename_component(ARG_WHENCE "${ARG_WHENCE}" ABSOLUTE) 530 | 531 | # Generate the identifier for the resource library's namespace 532 | get_target_property(lib_ns "${name}" CMRC_NAMESPACE) 533 | 534 | get_target_property(libdir ${name} CMRC_LIBDIR) 535 | get_target_property(target_dir ${name} SOURCE_DIR) 536 | file(RELATIVE_PATH reldir "${target_dir}" "${CMAKE_CURRENT_SOURCE_DIR}") 537 | if(reldir MATCHES "^\\.\\.") 538 | message(SEND_ERROR "Cannot call cmrc_add_resources in a parent directory from the resource library target") 539 | return() 540 | endif() 541 | 542 | foreach(input IN LISTS ARG_UNPARSED_ARGUMENTS) 543 | _cmrc_normalize_path(input) 544 | get_filename_component(abs_in "${input}" ABSOLUTE) 545 | # Generate a filename based on the input filename that we can put in 546 | # the intermediate directory. 547 | file(RELATIVE_PATH relpath "${ARG_WHENCE}" "${abs_in}") 548 | if(relpath MATCHES "^\\.\\.") 549 | # For now we just error on files that exist outside of the soure dir. 550 | message(SEND_ERROR "Cannot add file '${input}': File must be in a subdirectory of ${ARG_WHENCE}") 551 | continue() 552 | endif() 553 | if(DEFINED ARG_PREFIX) 554 | _cmrc_normalize_path(ARG_PREFIX) 555 | endif() 556 | if(ARG_PREFIX AND NOT ARG_PREFIX MATCHES "/$") 557 | set(ARG_PREFIX "${ARG_PREFIX}/") 558 | endif() 559 | get_filename_component(dirpath "${ARG_PREFIX}${relpath}" DIRECTORY) 560 | _cmrc_register_dirs("${name}" "${dirpath}") 561 | get_filename_component(abs_out "${libdir}/intermediate/${relpath}.cpp" ABSOLUTE) 562 | # Generate a symbol name relpath the file's character array 563 | _cm_encode_fpath(sym "${relpath}") 564 | # Get the symbol name for the parent directory 565 | if(dirpath STREQUAL "") 566 | set(parent_sym root_directory) 567 | else() 568 | _cm_encode_fpath(parent_sym "${dirpath}") 569 | endif() 570 | # Generate the rule for the intermediate source file 571 | _cmrc_generate_intermediate_cpp(${lib_ns} ${sym} "${abs_out}" "${abs_in}") 572 | target_sources(${name} PRIVATE "${abs_out}") 573 | set_property(TARGET ${name} APPEND PROPERTY CMRC_EXTERN_DECLS 574 | "// Pointers to ${input}" 575 | "extern const char* const ${sym}_begin\;" 576 | "extern const char* const ${sym}_end\;" 577 | ) 578 | get_filename_component(leaf "${relpath}" NAME) 579 | set_property( 580 | TARGET ${name} 581 | APPEND PROPERTY CMRC_MAKE_FILES 582 | "root_index.emplace(" 583 | " \"${ARG_PREFIX}${relpath}\"," 584 | " ${parent_sym}_dir.directory.add_file(" 585 | " \"${leaf}\"," 586 | " res_chars::${sym}_begin," 587 | " res_chars::${sym}_end" 588 | " )" 589 | ")\;" 590 | ) 591 | endforeach() 592 | endfunction() 593 | 594 | function(_cmrc_generate_intermediate_cpp lib_ns symbol outfile infile) 595 | add_custom_command( 596 | # This is the file we will generate 597 | OUTPUT "${outfile}" 598 | # These are the primary files that affect the output 599 | DEPENDS "${infile}" "${_CMRC_SCRIPT}" 600 | COMMAND 601 | "${CMAKE_COMMAND}" 602 | -D_CMRC_GENERATE_MODE=TRUE 603 | -DNAMESPACE=${lib_ns} 604 | -DSYMBOL=${symbol} 605 | "-DINPUT_FILE=${infile}" 606 | "-DOUTPUT_FILE=${outfile}" 607 | -P "${_CMRC_SCRIPT}" 608 | COMMENT "Generating intermediate file for ${infile}" 609 | ) 610 | endfunction() 611 | 612 | function(_cm_encode_fpath var fpath) 613 | string(MAKE_C_IDENTIFIER "${fpath}" ident) 614 | string(MD5 hash "${fpath}") 615 | string(SUBSTRING "${hash}" 0 4 hash) 616 | set(${var} f_${hash}_${ident} PARENT_SCOPE) 617 | endfunction() 618 | -------------------------------------------------------------------------------- /cmake/FindBikeshed.cmake: -------------------------------------------------------------------------------- 1 | include(CMakeFindDependencyMacro) 2 | 3 | find_dependency(Python2 COMPONENTS Interpreter) 4 | 5 | macro(_fail_find reason) 6 | set(Bikeshed_FOUND FALSE CACHE BOOL "Was Bikeshed found?" FORCE) 7 | if(Bikeshed_FIND_REQUIRED) 8 | message(FATAL_ERROR "${reason}") 9 | endif() 10 | return() 11 | endmacro() 12 | 13 | if(NOT Python2_FOUND) 14 | _fail_find("Failed to find bikeshed: We require a Python 2 executable.") 15 | endif() 16 | 17 | get_target_property(py2 Python2::Interpreter LOCATION) 18 | 19 | if(NOT DEFINED PF_VIRTUALENV_MODULE) 20 | message(STATUS "Finding Python virtualenv module...") 21 | endif() 22 | if(NOT PF_VIRTUALENV_MODULE) 23 | set(found) 24 | foreach(try_mod IN ITEMS venv virtualenv NOTFOUND) 25 | execute_process( 26 | COMMAND "${py2}" -m "${try_mod}" --help 27 | OUTPUT_VARIABLE out 28 | ERROR_VARIABLE out 29 | RESULT_VARIABLE retc 30 | ) 31 | if(retc EQUAL 0) 32 | message(STATUS "Found virtualenv module: ${try_mod}") 33 | set(found "${try_mod}") 34 | break() 35 | endif() 36 | endforeach() 37 | if(found AND NOT PF_VIRTUALENV_MODULE) 38 | # We've found the mod after failing to find it. Clear the cached failure. 39 | unset(PF_VIRTUALENV_MODULE CACHE) 40 | endif() 41 | set(PF_VIRTUALENV_MODULE "${found}" CACHE STRING "Python 2 module containing virtualenv") 42 | endif() 43 | 44 | if(NOT PF_VIRTUALENV_MODULE) 45 | message(STATUS "${PF_VIRTUALENV_MODULE}") 46 | _fail_find("Failed to find Bikeshed: Python 2 installation must contain virtualenv") 47 | endif() 48 | 49 | get_filename_component(bs_venv_dir "${CMAKE_BINARY_DIR}/_bikeshed_venv" ABSOLUTE) 50 | get_filename_component(bs_stamp "${bs_venv_dir}/.stamp" ABSOLUTE) 51 | 52 | macro(_bikeshed_found) 53 | set(Bikeshed_FOUND TRUE CACHE BOOL "Was Bikeshed found?" FORCE) 54 | file(TOUCH "${bs_stamp}") 55 | find_program( 56 | _bs_exe 57 | NAMES bikeshed bikeshed.exe 58 | PATH_SUFFIXES Scripts bin 59 | NO_DEFAULT_PATH 60 | PATHS "${bs_venv_dir}" 61 | ) 62 | add_executable(Bikeshed::Bikeshed IMPORTED) 63 | set_target_properties(Bikeshed::Bikeshed PROPERTIES IMPORTED_LOCATION "${_bs_exe}") 64 | # Check if the bikeshed data needs to be updated 65 | execute_process( 66 | COMMAND "${_bs_exe}" -d update 67 | OUTPUT_VARIABLE out 68 | ) 69 | if(NOT out MATCHES "already up-to-date") 70 | message(STATUS "Updating Bikeshed local data...") 71 | execute_process(COMMAND "${_bs_exe}" --silent update) 72 | endif() 73 | unset(_bs_exe CACHE) 74 | return() 75 | endmacro() 76 | 77 | if(EXISTS "${bs_stamp}") 78 | _bikeshed_found() 79 | endif() 80 | 81 | # Download the latest Bikeshed from master 82 | include(FetchContent) 83 | FetchContent_Declare( 84 | bikeshed 85 | URL https://github.com/tabatkins/bikeshed/archive/master.zip 86 | ) 87 | FetchContent_GetProperties(bikeshed) 88 | if(NOT bikeshed_POPULATED) 89 | message(STATUS "Obtaining Bikeshed... (This may take a moment)") 90 | FetchContent_Populate(bikeshed) 91 | message(STATUS "Bikeshed is downloaded") 92 | endif() 93 | 94 | file(REMOVE_RECURSE "${bs_venv_dir}") 95 | 96 | message(STATUS "Creating virtualenv for Bikeshed...") 97 | execute_process( 98 | COMMAND "${py2}" -m "${PF_VIRTUALENV_MODULE}" "${bs_venv_dir}" 99 | OUTPUT_VARIABLE out 100 | ERROR_VARIABLE out 101 | RESULT_VARIABLE retc 102 | ) 103 | 104 | if(retc) 105 | message(WARNING "Failed to create virtualenv:\n${out}") 106 | _fail_find("Could not create virtualenv") 107 | endif() 108 | 109 | get_filename_component(py_filename "${py2}" NAME) 110 | find_program(_venv_py2 111 | NAMES ${py_filename} python2.7 python2 python 112 | PATH_SUFFIXES Scripts bin 113 | NO_DEFAULT_PATH 114 | PATHS "${bs_venv_dir}" 115 | ) 116 | set(venv_py2 "${_venv_py2}") 117 | unset(_venv_py2 CACHE) 118 | 119 | message(STATUS "Installing Bikeshed in local virtualenv") 120 | execute_process( 121 | COMMAND "${venv_py2}" -m pip install --editable "${bikeshed_SOURCE_DIR}" 122 | OUTPUT_VARIABLE out 123 | ERROR_VARIABLE out 124 | RESULT_VARIABLE retc 125 | ) 126 | 127 | if(retc) 128 | message(WARNING "Failed to install bikeshed in virtualenv [${retc}]:\n${out}") 129 | _fail_find("Could not install bikeshed in virtualenv") 130 | endif() 131 | 132 | 133 | _bikeshed_found() -------------------------------------------------------------------------------- /cmake/FindFilesystem.cmake: -------------------------------------------------------------------------------- 1 | include(CMakePushCheckState) 2 | include(CheckIncludeFileCXX) 3 | include(CheckCXXSourceCompiles) 4 | 5 | cmake_push_check_state(RESET) 6 | set(CMAKE_CXX_STANDARD 17) 7 | 8 | set(have_fs FALSE) 9 | 10 | check_include_file_cxx("filesystem" HAVE_STD_FILESYSTEM) 11 | check_include_file_cxx("experimental/filesystem" HAVE_STD_EXPERIMENTAL_FILESYSTEM) 12 | 13 | if(HAVE_STD_FILESYSTEM) 14 | set(have_fs TRUE) 15 | set(header filesystem) 16 | set(namespace std::filesystem) 17 | elseif(HAVE_STD_EXPERIMENTAL_FILESYSTEM) 18 | set(have_fs TRUE) 19 | set(header experimental/filesystem) 20 | set(namespace std::experimental::filesystem) 21 | endif() 22 | 23 | string(CONFIGURE [[ 24 | #include <@header@> 25 | 26 | int main() { 27 | auto cwd = @namespace@::current_path(); 28 | return cwd.string().size(); 29 | } 30 | ]] code @ONLY) 31 | 32 | check_cxx_source_compiles("${code}" CAN_COMPILE_FS_WITHOUT_LINK) 33 | 34 | if(NOT CAN_COMPILE_FS_WITHOUT_LINK) 35 | set(CMAKE_REQUIRED_LIBRARIES -lstdc++fs) 36 | check_cxx_source_compiles("${code}" CAN_COMPILE_FS_WITH_LINK) 37 | endif() 38 | 39 | cmake_pop_check_state() 40 | 41 | if(have_fs) 42 | add_library(CXX::Filesystem INTERFACE IMPORTED) 43 | target_compile_definitions(CXX::Filesystem INTERFACE STD_FS_IS_EXPERIMENTAL=$>) 44 | if(CAN_COMPILE_FS_WITHOUT_LINK) 45 | # Nothing to add... 46 | elseif(CAN_COMPILE_FS_WITH_LINK) 47 | target_link_libraries(CXX::Filesystem INTERFACE -lstdc++fs) 48 | else() 49 | message(WARNING "Failed to link a filesystem library, although we found the headers...?") 50 | endif() 51 | set(Filesystem_FOUND TRUE CACHE BOOL "" FORCE) 52 | else() 53 | set(Filesystem_FOUND FALSE CACHE BOOL "" FORCE) 54 | if(Filesystem_FIND_REQUIRED) 55 | message(FATAL_ERROR "No C++ support for std::filesystem") 56 | endif() 57 | endif() 58 | -------------------------------------------------------------------------------- /cmake/FindYarn.cmake: -------------------------------------------------------------------------------- 1 | set(_prev "${YARN_EXECUTABLE}") 2 | find_program(YARN_EXECUTABLE yarn DOC "Path to Yarn, the better NPM") 3 | 4 | if(YARN_EXECUTABLE) 5 | if(NOT _prev) 6 | message(STATUS "Found Yarn executable: ${YARN_EXECUTABLE}") 7 | endif() 8 | set(Yarn_FOUND TRUE CACHE INTERNAL "") 9 | else() 10 | set(Yarn_FOUND FALSE CACHE INTERNAL "") 11 | if(Yarn_FIND_REQUIRED) 12 | message(FATAL_ERROR "Failed to find a Yarn executable") 13 | endif() 14 | endif() 15 | -------------------------------------------------------------------------------- /cmake/Pitchfork.cmake: -------------------------------------------------------------------------------- 1 | set(_pf_url http://localhost:8000/pf-cmake) 2 | if(DEFINED PF_URL_BASE) 3 | set(_pf_url "${PF_URL_BASE}") 4 | endif() 5 | set(PF_URL_BASE "${_pf_url}" CACHE STRING "Base URL to download Pitchfork files") 6 | set(_PF_BASEDIR "${CMAKE_BINARY_DIR}/_pf" CACHE PATH "Directory for Pitchfork files") 7 | 8 | if(NOT DEFINED PF_VERSION) 9 | set(PF_VERSION 0.1.0) 10 | endif() 11 | 12 | set(_PF_DIR "${_PF_BASEDIR}/${PF_VERSION}" CACHE INTERNAL "Directory for Pitchfork files") 13 | set(_PF_ENTRY_FILE "${_PF_DIR}/entry.cmake") 14 | set(PF_URL "${PF_URL_BASE}/${PF_VERSION}" CACHE STRING "URL for Pitchfork at requested version") 15 | 16 | if(NOT EXISTS "${_PF_ENTRY_FILE}" OR TRUE) 17 | file( 18 | DOWNLOAD "${PF_URL}/entry.cmake" 19 | "${_PF_ENTRY_FILE}.tmp" 20 | STATUS pair 21 | ) 22 | list(GET pair 0 rc) 23 | list(GET pair 1 msg) 24 | if(NOT rc EQUAL 0) 25 | file(REMOVE "${_PF_ENTRY_FILE}") 26 | message(FATAL_ERROR "Failed to download Pitchfork modules [${rc}]: ${msg}") 27 | endif() 28 | file(RENAME "${_PF_ENTRY_FILE}.tmp" "${_PF_ENTRY_FILE}") 29 | endif() 30 | 31 | include("${_PF_ENTRY_FILE}") 32 | -------------------------------------------------------------------------------- /conanfile.py: -------------------------------------------------------------------------------- 1 | from extras.pf_conan.pf_conan import CMakeConanFile 2 | 3 | 4 | class PitchforkConanFile(CMakeConanFile): 5 | name = 'pf' 6 | version = '0.1.0' 7 | requires = ( 8 | 'catch2/2.3.0@bincrafters/stable', 9 | 'spdlog/1.1.0@bincrafters/stable', 10 | 'boost/1.68.0@conan/stable', 11 | ) 12 | build_args = ['-DBUILD_SPEC=NO'] 13 | 14 | @property 15 | def exports_sources(self): 16 | return super().exports_sources + ['!extras/vscode-pitchfork/node_modules/*'] 17 | -------------------------------------------------------------------------------- /data/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | if(NOT TARGET Bikeshed::Bikeshed) 2 | message(STATUS "No Bikeshed executable, so we will not generate the spec document") 3 | else() 4 | get_filename_component(bs_input "spec.bs" ABSOLUTE) 5 | get_filename_component(bs_html "${PROJECT_BINARY_DIR}/spec.html" ABSOLUTE) 6 | 7 | add_custom_command( 8 | OUTPUT "${bs_html}" 9 | SOURCES "${bs_input}" 10 | COMMAND Bikeshed::Bikeshed spec "${bs_input}" "${bs_html}" 11 | COMMENT "Rendering specification to ${bs_html}" 12 | ) 13 | add_custom_target(pf-spec DEPENDS "${bs_html}") 14 | add_custom_target(pf-spec-watch 15 | COMMAND Bikeshed::Bikeshed watch "${bs_input}" "${bs_html}" 16 | COMMENT "Watching ${bs_input} for changes" 17 | USES_TERMINAL 18 | ) 19 | endif() 20 | 21 | add_subdirectory(templates) -------------------------------------------------------------------------------- /data/templates/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | file( 2 | GLOB_RECURSE template_files 3 | CONFIGURE_DEPENDS 4 | base/* 5 | cmake/* 6 | ) 7 | cmrc_add_resource_library( 8 | pf_templates 9 | ALIAS pf::templates 10 | ${template_files} 11 | ) 12 | -------------------------------------------------------------------------------- /data/templates/base/first_example.in.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include <{{ns_path}}/{{first_stem}}.hpp> 4 | 5 | int main() { 6 | std::cout << "I am an example executable\n"; 7 | std::cout << "Let's calculate the value...\n"; 8 | const auto value = {{root_ns}}::calculate_value(); 9 | std::cout << "The value we got is " << value << '\n'; 10 | } 11 | -------------------------------------------------------------------------------- /data/templates/base/first_header.in.hpp: -------------------------------------------------------------------------------- 1 | #ifndef {{guard_def}} 2 | #define {{guard_def}} 3 | 4 | namespace {{root_ns}} { 5 | 6 | /** 7 | * Calculate the answer. Not sure what the question is, though... 8 | */ 9 | int calculate_value(); 10 | 11 | } // namespace {{root_ns}} 12 | 13 | #endif // {{guard_def}} -------------------------------------------------------------------------------- /data/templates/base/first_source.in.cpp: -------------------------------------------------------------------------------- 1 | #include <{{ns_path}}/{{first_stem}}.hpp> 2 | 3 | int {{root_ns}}::calculate_value() { 4 | // How many roads must a man walk down? 5 | return 6 * 7; 6 | } 7 | -------------------------------------------------------------------------------- /data/templates/base/first_test.in.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include <{{ns_path}}/{{first_stem}}.hpp> 4 | 5 | int main() { 6 | const auto value = {{root_ns}}::calculate_value(); 7 | if (value == 42) { 8 | std::cout << "We calculated the value correctly\n"; 9 | return 0; 10 | } else { 11 | std::cout << "The value was incorrect!\n"; 12 | return 1; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /data/templates/cmake/examples_cml.in.cmake: -------------------------------------------------------------------------------- 1 | add_executable(example1 example1.cpp) 2 | 3 | target_link_libraries(example1 PRIVATE {{alias_target}}) 4 | -------------------------------------------------------------------------------- /data/templates/cmake/root_cml.in.cmake: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.10) 2 | 3 | list(INSERT CMAKE_MODULE_PATH 0 "${CMAKE_CURRENT_SOURCE_DIR}/cmake") 4 | 5 | project({{project_name}} VERSION 0.0.1 DESCRIPTION "A great new project") 6 | 7 | {{ #gen_third_party %}} 8 | # Include third-party components we need for the build 9 | add_subdirectory(third_party) 10 | {{% /gen_third_party }} 11 | 12 | add_subdirectory(src) 13 | 14 | {{ #gen_tests %}} 15 | option(BUILD_TESTING "Build tests" ON) 16 | if(BUILD_TESTING AND (PROJECT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR)) 17 | enable_testing() 18 | add_subdirectory(tests) 19 | endif() 20 | {{% /gen_tests }} 21 | 22 | {{ #gen_examples %}} 23 | option(BUILD_EXAMPLES "Build examples" ON) 24 | if(BUILD_EXAMPLES AND (PROJECT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR)) 25 | add_subdirectory(examples) 26 | endif() 27 | {{% /gen_examples }} 28 | 29 | {{ #gen_extras %}} 30 | add_subdirectory(extras) 31 | {{% /gen_extras }} 32 | -------------------------------------------------------------------------------- /data/templates/cmake/src_cml.in.cmake: -------------------------------------------------------------------------------- 1 | add_library( 2 | {{project_name}} 3 | {{ns_path}}/{{first_stem}}.hpp 4 | {{ns_path}}/{{first_stem}}.cpp 5 | ) 6 | add_library({{alias_target}} ALIAS {{project_name}}) 7 | target_include_directories({{project_name}} 8 | {{ #separate_headers %}} 9 | PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} 10 | PUBLIC $ 11 | {{% /separate_headers %}} 12 | {{% ^separate_headers %}} 13 | PUBLIC $ 14 | {{% /separate_headers }} 15 | ) 16 | -------------------------------------------------------------------------------- /examples/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_executable(example1 example1.cpp) 2 | 3 | target_link_libraries(example1 PRIVATE pf::pitchfork) 4 | -------------------------------------------------------------------------------- /examples/example1.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | 5 | int main() { 6 | std::cout << "I am an example executable\n"; 7 | std::cout << "Let's calculate the value...\n"; 8 | const auto value = pf::pitchfork(); 9 | std::cout << "The value we got is " << value << '\n'; 10 | } 11 | -------------------------------------------------------------------------------- /external/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_subdirectory(args) 2 | add_subdirectory(mustache) -------------------------------------------------------------------------------- /external/args/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | if(NOT TARGET taywee::args) 2 | add_library(taywee::args IMPORTED INTERFACE GLOBAL) 3 | target_include_directories(taywee::args INTERFACE "${CMAKE_CURRENT_LIST_DIR}/include") 4 | endif() -------------------------------------------------------------------------------- /external/mustache/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | if(NOT TARGET kainjow::mustache) 2 | add_library(kainjow::mustache INTERFACE IMPORTED GLOBAL) 3 | target_include_directories(kainjow::mustache INTERFACE "${CMAKE_CURRENT_LIST_DIR}/include") 4 | target_compile_features(kainjow::mustache INTERFACE cxx_std_11) 5 | endif() -------------------------------------------------------------------------------- /extras/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | option(BUILD_VSCE "Build the Pitchfork VSCode extension" ON) 2 | if(NOT YARN_EXECUTABLE) 3 | message(STATUS "No yarn executable. Not building VSCode extension.") 4 | elseif(BUILD_VSCE) 5 | get_filename_component(stamp "${CMAKE_CURRENT_BINARY_DIR}/vsce.stamp" ABSOLUTE) 6 | get_filename_component(vsce_root vscode-pitchfork ABSOLUTE) 7 | file(GLOB_RECURSE vsce_sources LIST_DIRECTORIES false CONFIGURE_DEPENDS 8 | ${vsce_root}/src/** 9 | ${vsce_root}/package.json 10 | ) 11 | add_custom_command( 12 | OUTPUT "${stamp}" 13 | SOURCES ${vsce_sources} 14 | COMMAND "${YARN_EXECUTABLE}" 15 | COMMAND "${YARN_EXECUTABLE}" compile 16 | COMMAND "${CMAKE_COMMAND}" -E touch "${stamp}" 17 | COMMENT "Building VSCode extension" 18 | WORKING_DIRECTORY "${vsce_root}" 19 | ) 20 | add_custom_target(pf-vsce DEPENDS "${stamp}") 21 | endif() 22 | -------------------------------------------------------------------------------- /extras/__init__.py: -------------------------------------------------------------------------------- 1 | # This file is only here to satisfy Python 2 wanting to import extras.pf_conan.pf_conan 2 | -------------------------------------------------------------------------------- /extras/pf-cmake/auto.cmake: -------------------------------------------------------------------------------- 1 | include_guard(GLOBAL) 2 | 3 | include(CMakePackageConfigHelpers) 4 | 5 | function(_pf_auto_detect_enable_testing) 6 | set(options 7 | NO_INSTALL 8 | ) 9 | set(args 10 | LIBRARY_NAME 11 | ALIAS 12 | OUTPUT_NAME 13 | VERSION_COMPATIBILITY 14 | ) 15 | set(list_args 16 | LINK 17 | PRIVATE_LINK 18 | EXE_LINK 19 | SUBDIR_BEFORE 20 | SUBDIR_AFTER 21 | ) 22 | cmake_parse_arguments(PARSE_ARGV 0 ARG "${options}" "${args}" "${list_args}") 23 | 24 | foreach(arg IN LISTS ARG_UNPARSED_ARGUMENTS) 25 | message(WARNING "Unknown argument to pf_auto: ${arg}") 26 | endforeach() 27 | 28 | # Check that we were call correctly 29 | # Must have called project(): 30 | if(NOT DEFINED PROJECT_NAME) 31 | message(FATAL_ERROR "Please call project() before pf_auto()") 32 | endif() 33 | 34 | # Bool-ish for checking if we are the root project 35 | get_directory_property(pr_pardir "${PROJECT_SOURCE_DIR}" DIRECTORY) 36 | set(is_root_project TRUE) 37 | if(pr_pardir) 38 | set(is_root_project FALSE) 39 | endif() 40 | 41 | get_directory_property(already_subdirs SUBDIRECTORIES) 42 | # Add the tests subdirectory 43 | get_filename_component(tests_dir "${PROJECT_SOURCE_DIR}/tests" ABSOLUTE) 44 | option(BUILD_TESTING "Build the testing tree" ON) 45 | set(_PF_ADDED_TESTS FALSE PARENT_SCOPE) 46 | if(EXISTS "${tests_dir}/CMakeLists.txt" AND BUILD_TESTING AND is_root_project AND NOT tests_dir IN_LIST already_subdirs) 47 | set(_PF_ADDED_TESTS TRUE PARENT_SCOPE) 48 | endif() 49 | endfunction() 50 | 51 | # Real impl of `pf_auto()` 52 | function(_pf_auto) 53 | set(options 54 | NO_INSTALL 55 | ) 56 | set(args 57 | LIBRARY_NAME 58 | ALIAS 59 | OUTPUT_NAME 60 | VERSION_COMPATIBILITY 61 | ) 62 | set(list_args 63 | LINK 64 | PRIVATE_LINK 65 | EXE_LINK 66 | SUBDIR_BEFORE 67 | SUBDIR_AFTER 68 | ) 69 | cmake_parse_arguments(PARSE_ARGV 0 ARG "${options}" "${args}" "${list_args}") 70 | 71 | foreach(arg IN LISTS ARG_UNPARSED_ARGUMENTS) 72 | message(WARNING "Unknown argument to pf_auto: ${arg}") 73 | endforeach() 74 | 75 | # Default arguments 76 | if(NOT DEFINED ARG_LIBRARY_NAME) 77 | set(ARG_LIBRARY_NAME "${PROJECT_NAME}") 78 | endif() 79 | if(NOT DEFINED ARG_ALIAS) 80 | set(ARG_ALIAS "${PROJECT_NAME}::${ARG_LIBRARY_NAME}") 81 | endif() 82 | if(NOT DEFINED ARG_VERSION_COMPATIBILITY) 83 | set(ARG_VERSION_COMPATIBILITY ExactVersion) 84 | endif() 85 | 86 | # Check arguments 87 | if(NOT ARG_ALIAS MATCHES "::") 88 | message(SEND_ERROR "pf_auto(ALIAS) must contain a double-colon '::'") 89 | endif() 90 | 91 | # Check that we were call correctly 92 | # Must have called project(): 93 | if(NOT DEFINED PROJECT_NAME) 94 | message(FATAL_ERROR "Please call project() before pf_auto()") 95 | endif() 96 | 97 | # Before defining our project, add the subdirs 98 | foreach(subdir IN LISTS ARG_SUBDIR_BEFORE) 99 | add_subdirectory("${subdir}") 100 | endforeach() 101 | 102 | # Must be called in the same dir as a project(): 103 | get_filename_component(pr_dir "${PROJECT_SOURCE_DIR}" ABSOLUTE) 104 | get_filename_component(here_dir "${CMAKE_CURRENT_SOURCE_DIR}" ABSOLUTE) 105 | if(NOT pr_dir STREQUAL here_dir) 106 | message(FATAL_ERROR "pf_auto() must be called in the same directory as the matching project() call.") 107 | endif() 108 | 109 | # Some basic vars: 110 | set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/bin") 111 | set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/lib") 112 | set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/lib") 113 | 114 | # Bool-ish for checking if we are the root project 115 | get_directory_property(pr_pardir "${PROJECT_SOURCE_DIR}" DIRECTORY) 116 | set(is_root_project TRUE) 117 | if(pr_pardir) 118 | set(is_root_project FALSE) 119 | endif() 120 | 121 | # Should we install? 122 | set(do_install FALSE) 123 | if(is_root_project AND NOT ARG_NO_INSTALL) 124 | set(do_install TRUE) 125 | endif() 126 | if(CMAKE_SIZEOF_VOID_P EQUAL 8) 127 | set(arch x64) 128 | else() 129 | set(arch x86) 130 | endif() 131 | set(install_infix "lib/${PROJECT_NAME}-${PROJECT_VERSION}-${arch}") 132 | set(install_target_common 133 | EXPORT "${PROJECT_NAME}Targets" 134 | RUNTIME DESTINATION "${install_infix}/bin" 135 | LIBRARY DESTINATION "${install_infix}/lib" 136 | ARCHIVE DESTINATION "${install_infix}/lib" 137 | OBJECTS DESTINATION "${install_infix}/lib" 138 | INCLUDES DESTINATION "${install_infix}/include" 139 | ) 140 | 141 | # Calculate the public include directory 142 | set(pub_inc_dir "${PROJECT_SOURCE_DIR}/include") 143 | if(NOT IS_DIRECTORY "${pub_inc_dir}") 144 | set(pub_inc_dir "${PROJECT_SOURCE_DIR}/src") 145 | endif() 146 | 147 | # Private inc dir is always the same: 148 | set(priv_inc_dir "${PROJECT_SOURCE_DIR}/src") 149 | 150 | # Get our source files 151 | file(GLOB_RECURSE 152 | sources 153 | RELATIVE "${PROJECT_SOURCE_DIR}/src" 154 | CONFIGURE_DEPENDS 155 | "${PROJECT_SOURCE_DIR}/src/*" 156 | ) 157 | # Maintain three different source classifications 158 | set(exe_sources) 159 | set(lib_sources) 160 | set(test_sources) 161 | # Find them 162 | foreach(file IN LISTS sources) 163 | get_filename_component(fname "${file}" NAME) 164 | if(fname STREQUAL file) 165 | # File is not in subdirectory. It is an executable 166 | list(APPEND exe_sources "src/${file}") 167 | elseif(fname MATCHES "\\.test\\.[cC][a-zA-Z]+^") 168 | list(APPEND test_sources "src/${file}") 169 | else() 170 | list(APPEND lib_sources "src/${file}") 171 | endif() 172 | endforeach() 173 | # Add all headers to the library sources 174 | if(NOT pub_inc_dir STREQUAL priv_inc_dir) 175 | file(GLOB_RECURSE includes "${pub_inc_dir}/*") 176 | list(APPEND lib_sources ${includes}) 177 | endif() 178 | 179 | # Pull in external before we check for link targets 180 | get_filename_component(external_dir "${PROJECT_SOURCE_DIR}/external" ABSOLUTE) 181 | if(EXISTS "${external_dir}/CMakeLists.txt") 182 | add_subdirectory("${external_dir}") 183 | endif() 184 | 185 | foreach(link IN LISTS ARG_LINK ARG_PRIVATE_LINK ARG_EXE_LINK) 186 | if(NOT TARGET "${link}") 187 | message(SEND_ERROR "Requested linking to non-target: ${link}") 188 | endif() 189 | endforeach() 190 | 191 | # Define the library 192 | add_library("${ARG_LIBRARY_NAME}" ${lib_sources}) 193 | target_include_directories("${ARG_LIBRARY_NAME}" PUBLIC "$") 194 | if(NOT pub_inc_dir STREQUAL priv_inc_dir) 195 | target_include_directories("${ARG_LIBRARY_NAME}" PRIVATE "${priv_inc_dir}") 196 | endif() 197 | # Link dependencies 198 | target_link_libraries("${ARG_LIBRARY_NAME}" PUBLIC ${ARG_LINK}) 199 | foreach(priv IN LISTS ARG_PRIVATE_LINK) 200 | target_link_libraries("${ARG_LIBRARY_NAME}" PRIVATE $) 201 | endforeach() 202 | # Add an alias target 203 | add_library("${ARG_ALIAS}" ALIAS "${ARG_LIBRARY_NAME}") 204 | set_property(TARGET "${ARG_LIBRARY_NAME}" PROPERTY EXPORT_NAME "${ARG_ALIAS}") 205 | # Optional properies 206 | if(DEFINED ARG_OUTPUT_NAME) 207 | set_property(TARGET "${ARG_LIBRARY_NAME}" PROPERTY OUTPUT_NAME "${ARG_OUTPUT_NAME}") 208 | endif() 209 | 210 | if(do_install) 211 | install(TARGETS "${ARG_LIBRARY_NAME}" ${install_target_common}) 212 | install( 213 | DIRECTORY "${pub_inc_dir}" 214 | DESTINATION "${install_infix}/include" 215 | FILES_MATCHING 216 | PATTERN *.h 217 | PATTERN *.hpp 218 | PATTERN *.hh 219 | PATTERN *.h++ 220 | PATTERN *.hxx 221 | PATTERN *.H 222 | ) 223 | endif() 224 | 225 | # Add executables 226 | foreach(exe_source IN LISTS exe_sources) 227 | get_filename_component(exe_name "${exe_source}" NAME_WE) 228 | add_executable("${exe_name}" "${exe_source}") 229 | target_link_libraries("${exe_name}" PRIVATE "${ARG_ALIAS}" ${ARG_EXE_LINK}) 230 | if(do_install) 231 | install(TARGETS "${exe_name}" RUNTIME DESTINATION bin) 232 | endif() 233 | endforeach() 234 | 235 | get_directory_property(already_subdirs SUBDIRECTORIES) 236 | 237 | # Add the tests subdirectory 238 | get_filename_component(tests_dir "${PROJECT_SOURCE_DIR}/tests" ABSOLUTE) 239 | option(BUILD_TESTING "Build the testing tree" ON) 240 | set(_PF_ADDED_TESTS FALSE PARENT_SCOPE) 241 | if(EXISTS "${tests_dir}/CMakeLists.txt" AND BUILD_TESTING AND is_root_project AND NOT tests_dir IN_LIST already_subdirs) 242 | add_subdirectory("${tests_dir}") 243 | set(_PF_ADDED_TESTS TRUE PARENT_SCOPE) 244 | endif() 245 | 246 | # Add the examples subdirectory 247 | get_filename_component(examples_dir "${PROJECT_SOURCE_DIR}/examples" ABSOLUTE) 248 | option(BUILD_EXAMPLES "Build examples" ON) 249 | if(EXISTS "${examples_dir}/CMakeLists.txt" AND BUILD_EXAMPLES AND is_root_project AND NOT examples_dir IN_LIST already_subdirs) 250 | add_subdirectory("${examples_dir}") 251 | endif() 252 | 253 | # Add the extras subdirectory 254 | get_filename_component(extras_dir "${PROJECT_SOURCE_DIR}/extras" ABSOLUTE) 255 | if(EXISTS "${extras_dir}/CMakeLists.txt" AND NOT extras_dirs IN_LIST already_subdirs) 256 | add_subdirectory("${extras_dir}") 257 | endif() 258 | 259 | # Add the data subdirectory 260 | get_filename_component(data_dir "${PROJECT_SOURCE_DIR}/data" ABSOLUTE) 261 | if(EXISTS "${data_dir}/CMakeLists.txt" AND NOT data_dir IN_LIST already_subdirs) 262 | add_subdirectory("${data_dir}") 263 | endif() 264 | 265 | if(do_install) 266 | install( 267 | EXPORT "${PROJECT_NAME}Targets" 268 | DESTINATION "${install_infix}/cmake" 269 | FILE "${PROJECT_NAME}Config.cmake" 270 | ) 271 | write_basic_package_version_file( 272 | "${PROJECT_NAME}ConfigVersion.cmake" 273 | COMPATIBILITY ${ARG_VERSION_COMPATIBILITY} 274 | ) 275 | install( 276 | FILES "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}ConfigVersion.cmake" 277 | DESTINATION "${install_infix}/cmake" 278 | ) 279 | endif() 280 | endfunction() 281 | 282 | # Macro so we can do things in the parent scope 283 | macro(pf_auto) 284 | _pf_auto_detect_enable_testing(${ARGN}) 285 | # Maybe call enable_testing() in the caller's scope (important) 286 | if(_PF_ADDED_TESTS) 287 | enable_testing() 288 | endif() 289 | _pf_auto(${ARGN}) 290 | endmacro() 291 | -------------------------------------------------------------------------------- /extras/pf-cmake/entry.cmake: -------------------------------------------------------------------------------- 1 | function(_pf_download url dest) 2 | cmake_parse_arguments(ARG "NO_CHECK" "RESULT_VARIABLE" "" "${ARGN}") 3 | set(tmp "${dest}.tmp") 4 | file( 5 | DOWNLOAD "${url}" 6 | "${tmp}" 7 | STATUS st 8 | ) 9 | list(GET st 0 rc) 10 | list(GET st 1 msg) 11 | if(rc) 12 | file(REMOVE "${tmp}") 13 | if(NOT ARG_NO_CHECK) 14 | message(FATAL_ERROR "Error while downloading file [${rc}]: ${msg}") 15 | endif() 16 | if(ARG_RESULT_VARIABLE) 17 | set("${ARG_RESULT_VARIABLE}" FALSE PARENT_SCOPE) 18 | endif() 19 | else() 20 | file(RENAME "${tmp}" "${dest}") 21 | if(ARG_RESULT_VARIABLE) 22 | set("${ARG_RESULT_VARIABLE}" TRUE PARENT_SCOPE) 23 | endif() 24 | endif() 25 | endfunction() 26 | 27 | foreach(fname IN ITEMS auto.cmake) 28 | get_filename_component(_pf_dest "${_PF_DIR}/${fname}" ABSOLUTE) 29 | _pf_download("${PF_URL}/${fname}" "${_pf_dest}") 30 | include("${_pf_dest}") 31 | endforeach() 32 | -------------------------------------------------------------------------------- /extras/pf_conan/__init__.py: -------------------------------------------------------------------------------- 1 | # This file is only here to satisfy Python 2 wanting to import extras.pf_conan.pf_conan -------------------------------------------------------------------------------- /extras/pf_conan/conanfile.py: -------------------------------------------------------------------------------- 1 | from pf_conan import CMakeConanFile 2 | import conans 3 | 4 | 5 | class PitchforkConanFile(conans.ConanFile): 6 | name = 'pf-conan' 7 | version = '0.1.0' 8 | exports = 'pf_conan/*' 9 | # We have no settings 10 | settings = None 11 | # We have no options 12 | options = None 13 | 14 | def package_id(self): 15 | self.info.header_only() 16 | -------------------------------------------------------------------------------- /extras/pf_conan/pf_conan/__init__.py: -------------------------------------------------------------------------------- 1 | from .cmake import CMakeConanFile 2 | -------------------------------------------------------------------------------- /extras/pf_conan/pf_conan/cmake.py: -------------------------------------------------------------------------------- 1 | import conans 2 | from os import path 3 | import tempfile 4 | import subprocess 5 | 6 | NINJA_TEST = ''' 7 | build test-target: phony 8 | ''' 9 | 10 | 11 | def has_ninja(): 12 | with tempfile.NamedTemporaryFile(suffix='.ninja') as tmpfd: 13 | tmpfd.write(NINJA_TEST.encode('utf-8')) 14 | for exe in ('ninja-build', 'ninja'): 15 | try: 16 | subprocess.check_call([exe, '-f', tmpfd.name]) 17 | return True 18 | except FileNotFoundError: 19 | pass 20 | except subprocess.CalledProcessError: 21 | pass 22 | return False 23 | 24 | 25 | class CMakeConanFile(conans.ConanFile): 26 | generators = 'cmake_paths' 27 | no_copy_source = True 28 | settings = 'os', 'arch', 'compiler' 29 | build_args = [] 30 | exports_sources = '*', '!build/*', '!_build/*' 31 | 32 | def build(self): 33 | use_ninja = has_ninja() 34 | for bt in ('Debug', 'Release'): 35 | cmake = conans.CMake( 36 | self, 37 | generator='Ninja' if use_ninja else None, 38 | ) 39 | cmake.configure( 40 | source_folder=self.source_folder, 41 | args=list( 42 | [ 43 | '-DCMAKE_BUILD_TYPE={}'.format(bt), 44 | '--no-warn-unused-cli', 45 | ], 46 | *self.build_args 47 | ), 48 | ) 49 | cmake.build() 50 | 51 | @property 52 | def has_pub_include_dir(self): 53 | return path.exists(path.join(self.source_folder, 'include')) 54 | 55 | def package_info(self): 56 | if self.has_pub_include_dir: 57 | self.cpp_info.inclduedirs = ['include'] 58 | else: 59 | self.cpp_info.inclduedirs = ['src'] 60 | -------------------------------------------------------------------------------- /extras/pf_conan/test_package/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # This CMakeLists is for the pf-conan test package. 2 | cmake_minimum_required(VERSION 3.10) 3 | project(PFConanTest VERSION 0.1.0) 4 | 5 | add_executable(my_program src/program.cpp) 6 | -------------------------------------------------------------------------------- /extras/pf_conan/test_package/conanfile.py: -------------------------------------------------------------------------------- 1 | import conans 2 | import subprocess 3 | 4 | pf = conans.python_requires('pf-conan/[*]@test/test') 5 | 6 | 7 | class ConanFile(pf.CMakeConanFile): 8 | name = 'pf-conan-test' 9 | version = 'test' 10 | 11 | def test(self): 12 | subprocess.check_call([f'{self.build_folder}/my_program', 'Test string']) 13 | -------------------------------------------------------------------------------- /extras/pf_conan/test_package/src/program.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | int main(int argc, char** argv) { 4 | if (argc < 2) { 5 | return 1; 6 | } 7 | if (std::string("Test string") == argv[1]) { 8 | return 0; 9 | } else { 10 | return 1; 11 | } 12 | } -------------------------------------------------------------------------------- /extras/vscode-pitchfork/.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | Language: JavaScript 3 | # BasedOnStyle: WebKit 4 | # Keep them together. Would be preferable to put all arguments on a new line 5 | AlignAfterOpenBracket: true 6 | # Newline escapes should be aligned, they're prettier that way 7 | AlignEscapedNewlinesLeft: false 8 | # Same with OpenBracket alignment. No need for that. 9 | AlignOperands: false 10 | # Purdy 11 | AlignTrailingComments: true 12 | # Yuck: 13 | AllowAllParametersOfDeclarationOnNextLine: false 14 | AllowShortBlocksOnASingleLine: true 15 | AllowShortCaseLabelsOnASingleLine: false 16 | AllowShortIfStatementsOnASingleLine: false 17 | AllowShortLoopsOnASingleLine: false 18 | AllowShortFunctionsOnASingleLine: true 19 | # Ew, no. 20 | AlwaysBreakAfterDefinitionReturnType: false 21 | # Not mandatory... 22 | AlwaysBreakTemplateDeclarations: false 23 | # Readability: 24 | AlwaysBreakBeforeMultilineStrings: true 25 | # Nice... 26 | BreakBeforeBinaryOperators: All 27 | BreakBeforeTernaryOperators: true 28 | # Yessir 29 | BreakConstructorInitializersBeforeComma: true 30 | # Please, keep it readable: 31 | BinPackParameters: false 32 | BinPackArguments: false 33 | # Not a hard and fast rule, but yeah 34 | ColumnLimit: 120 35 | # No need 36 | IndentCaseLabels: false 37 | # Gross 38 | IndentWrappedFunctionNames: false 39 | IndentFunctionDeclarationAfterType: false 40 | # Two empty lines are useful as physical seperators 41 | MaxEmptyLinesToKeep: 2 42 | KeepEmptyLinesAtTheStartOfBlocks: true 43 | # These feel about right... 44 | PenaltyBreakBeforeFirstCallParameter: 600 45 | PenaltyBreakComment: 300 46 | PenaltyBreakString: 1000 47 | PenaltyBreakFirstLessLess: 120 48 | PenaltyExcessCharacter: 1000000 49 | PenaltyReturnTypeOnItsOwnLine: 60 50 | # Keep it away: 51 | SpacesBeforeTrailingComments: 2 52 | IndentWidth: 2 53 | TabWidth: 8 54 | UseTab: Never 55 | # Attach is the One True Brace Style 56 | BreakBeforeBraces: Attach 57 | # Used to do this, not anymore 58 | SpacesInParentheses: false 59 | SpacesInSquareBrackets: false 60 | SpacesInAngles: false 61 | SpaceInEmptyParentheses: false 62 | SpacesInCStyleCastParentheses: false 63 | SpaceAfterCStyleCast: false 64 | SpacesInContainerLiterals: false 65 | SpaceBeforeAssignmentOperators: true 66 | ContinuationIndentWidth: 4 67 | SpaceBeforeParens: ControlStatements 68 | JavaScriptWrapImports: true 69 | JavaScriptQuotes: Single 70 | DisableFormat: false 71 | ... 72 | -------------------------------------------------------------------------------- /extras/vscode-pitchfork/.gitattributes: -------------------------------------------------------------------------------- 1 | # Set default behavior to automatically normalize line endings. 2 | * text=auto 3 | 4 | -------------------------------------------------------------------------------- /extras/vscode-pitchfork/.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | node_modules 3 | .vscode-test/ 4 | *.vsix 5 | -------------------------------------------------------------------------------- /extras/vscode-pitchfork/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "eg2.tslint" 6 | ] 7 | } -------------------------------------------------------------------------------- /extras/vscode-pitchfork/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "runtimeExecutable": "${execPath}", 13 | "args": [ 14 | "--extensionDevelopmentPath=${workspaceFolder}" 15 | ], 16 | "outFiles": [ 17 | "${workspaceFolder}/out/**/*.js" 18 | ], 19 | "preLaunchTask": "npm: watch" 20 | }, 21 | { 22 | "name": "Extension Tests", 23 | "type": "extensionHost", 24 | "request": "launch", 25 | "runtimeExecutable": "${execPath}", 26 | "args": [ 27 | "--extensionDevelopmentPath=${workspaceFolder}", 28 | "--extensionTestsPath=${workspaceFolder}/out/test" 29 | ], 30 | "outFiles": [ 31 | "${workspaceFolder}/out/test/**/*.js" 32 | ], 33 | "preLaunchTask": "npm: watch" 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /extras/vscode-pitchfork/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false // set this to true to hide the "out" folder with the compiled JS files 5 | }, 6 | "search.exclude": { 7 | "out": true // set this to false to include "out" folder in search results 8 | }, 9 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts 10 | "typescript.tsc.autoDetect": "off" 11 | } -------------------------------------------------------------------------------- /extras/vscode-pitchfork/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "watch", 9 | "problemMatcher": "$tsc-watch", 10 | "isBackground": true, 11 | "presentation": { 12 | "reveal": "never" 13 | }, 14 | "group": { 15 | "kind": "build", 16 | "isDefault": true 17 | } 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /extras/vscode-pitchfork/.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | out/test/** 4 | out/**/*.map 5 | src/** 6 | .gitignore 7 | tsconfig.json 8 | vsc-extension-quickstart.md 9 | tslint.json -------------------------------------------------------------------------------- /extras/vscode-pitchfork/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to the "vscode-pitchfork" extension will be documented in this file. 3 | 4 | Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file. 5 | 6 | ## [Unreleased] 7 | - Initial release -------------------------------------------------------------------------------- /extras/vscode-pitchfork/README.md: -------------------------------------------------------------------------------- 1 | # vscode-pitchfork README 2 | 3 | This is the README for your extension "vscode-pitchfork". After writing up a brief description, we recommend including the following sections. 4 | 5 | ## Features 6 | 7 | Describe specific features of your extension including screenshots of your extension in action. Image paths are relative to this README file. 8 | 9 | For example if there is an image subfolder under your extension project workspace: 10 | 11 | \!\[feature X\]\(images/feature-x.png\) 12 | 13 | > Tip: Many popular extensions utilize animations. This is an excellent way to show off your extension! We recommend short, focused animations that are easy to follow. 14 | 15 | ## Requirements 16 | 17 | If you have any requirements or dependencies, add a section describing those and how to install and configure them. 18 | 19 | ## Extension Settings 20 | 21 | Include if your extension adds any VS Code settings through the `contributes.configuration` extension point. 22 | 23 | For example: 24 | 25 | This extension contributes the following settings: 26 | 27 | * `myExtension.enable`: enable/disable this extension 28 | * `myExtension.thing`: set to `blah` to do something 29 | 30 | ## Known Issues 31 | 32 | Calling out known issues can help limit users opening duplicate issues against your extension. 33 | 34 | ## Release Notes 35 | 36 | Users appreciate release notes as you update your extension. 37 | 38 | ### 1.0.0 39 | 40 | Initial release of ... 41 | 42 | ### 1.0.1 43 | 44 | Fixed issue #. 45 | 46 | ### 1.1.0 47 | 48 | Added features X, Y, and Z. 49 | 50 | ----------------------------------------------------------------------------------------------------------- 51 | 52 | ## Working with Markdown 53 | 54 | **Note:** You can author your README using Visual Studio Code. Here are some useful editor keyboard shortcuts: 55 | 56 | * Split the editor (`Cmd+\` on macOS or `Ctrl+\` on Windows and Linux) 57 | * Toggle preview (`Shift+CMD+V` on macOS or `Shift+Ctrl+V` on Windows and Linux) 58 | * Press `Ctrl+Space` (Windows, Linux) or `Cmd+Space` (macOS) to see a list of Markdown snippets 59 | 60 | ### For more information 61 | 62 | * [Visual Studio Code's Markdown Support](http://code.visualstudio.com/docs/languages/markdown) 63 | * [Markdown Syntax Reference](https://help.github.com/articles/markdown-basics/) 64 | 65 | **Enjoy!** 66 | -------------------------------------------------------------------------------- /extras/vscode-pitchfork/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vscode-pitchfork", 3 | "displayName": "Pitchfork", 4 | "description": "Pitchfork Support for VSCode", 5 | "version": "0.0.1", 6 | "keywords": [ 7 | "cmake", 8 | "c++", 9 | "native" 10 | ], 11 | "publisher": "vector-of-bool", 12 | "engines": { 13 | "vscode": "^1.26.0" 14 | }, 15 | "categories": [ 16 | "Other" 17 | ], 18 | "activationEvents": [ 19 | "onCommand:pf.newProject", 20 | "onCommand:pf.changeBaseDir", 21 | "workspaceContains:.pitchfork-init" 22 | ], 23 | "main": "./out/extension", 24 | "contributes": { 25 | "commands": [ 26 | { 27 | "command": "pf.changeBaseDir", 28 | "title": "Set the Project Base Directory", 29 | "category": "Pitchfork" 30 | }, 31 | { 32 | "command": "pf.newProject", 33 | "title": "Create a New Project", 34 | "category": "Pitchfork" 35 | } 36 | ] 37 | }, 38 | "scripts": { 39 | "vscode:prepublish": "yarn compile", 40 | "compile": "tsc -p ./", 41 | "watch": "tsc -watch -p ./", 42 | "postinstall": "node ./node_modules/vscode/bin/install", 43 | "test": "yarn compile && node ./node_modules/vscode/bin/test" 44 | }, 45 | "devDependencies": { 46 | "@types/mocha": "^2.2.42", 47 | "@types/node": "^8.10.25", 48 | "tslint": "^5.8.0", 49 | "typescript": "^2.6.1", 50 | "@types/es6-promisify": "^6.0.0", 51 | "vscode": "^1.1.21" 52 | }, 53 | "dependencies": { 54 | "es6-promisify": "^6.0.0" 55 | } 56 | } -------------------------------------------------------------------------------- /extras/vscode-pitchfork/src/cmake.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Module for manipulating CMake projects in Pitchfork projects 3 | */ /** */ 4 | 5 | import * as path from 'path'; 6 | 7 | import {dirForNamespace} from './dirs'; 8 | import {NewProjectParams} from './new'; 9 | import {fs} from './pr'; 10 | 11 | const CML_HEAD 12 | = `\ 13 | cmake_minimum_required(VERSION 3.10) 14 | 15 | list(APPEND CMAKE_MODULE_PATH "\${CMAKE_CURRENT_SOURCE_DIR}/cmake") 16 | 17 | project(<> VERSION 0.0.1 DESCRIPTION "A great new project") 18 | 19 | `; 20 | 21 | const CML_INCL_THIRD 22 | = `\ 23 | # Include third-party components we need for the build 24 | add_subdirectory(third_party) 25 | 26 | `; 27 | 28 | const SRC_CML 29 | = `\ 30 | add_library( 31 | <> 32 | <>/<>.hpp 33 | <>/<>.cpp 34 | ) 35 | add_library(<> ALIAS <>) 36 | `; 37 | 38 | const SRC_CML_WITH_INCLUDE_DIR 39 | = `\ 40 | target_include_directories( 41 | <> 42 | PRIVATE \${CMAKE_CURRENT_SOURCE_DIR} 43 | PUBLIC $ 44 | ) 45 | `; 46 | 47 | const SRC_CML_WITH_NO_INCLUDE_DIR 48 | = `\ 49 | target_include_directories( 50 | <> 51 | PUBLIC $ 52 | ) 53 | `; 54 | 55 | const FIRST_CPP 56 | = `\ 57 | #include 58 | 59 | #include "./<>.hpp" 60 | 61 | int <>::<>() { 62 | std::cout << "Calculating the answer...\\n"; 63 | return 42; 64 | } 65 | `; 66 | 67 | const FIRST_HPP 68 | = `\ 69 | #ifndef <>_HPP_INCLUDED 70 | #define <>_HPP_INCLUDED 71 | 72 | namespace <> { 73 | 74 | // Calculate the answer 75 | int <>(); 76 | 77 | } // <> 78 | 79 | #endif // <>_HPP_INCLUDED 80 | `; 81 | 82 | const FIRST_TEST_CPP 83 | = `\ 84 | #include 85 | 86 | #include <<>/<>.hpp> 87 | 88 | int main() { 89 | const auto value = <>::<>(); 90 | if (value == 42) { 91 | std::cout << "We calculated the value correctly\\n"; 92 | return 0; 93 | } else { 94 | std::cout << "The value was incorrect!\\n"; 95 | return 1; 96 | } 97 | } 98 | `; 99 | 100 | const TEST_CML 101 | = `\ 102 | add_executable(first-test my_test.cpp) 103 | target_link_libraries(first-test PRIVATE <>) 104 | add_test(NAME first-test COMMAND first-test) 105 | `; 106 | 107 | const CML_MIDDLE 108 | = `\ 109 | add_subdirectory(src) 110 | 111 | option(BUILD_TESTS "Build tests" ON) 112 | if(BUILD_TESTS AND (PROJECT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR)) 113 | enable_testing() 114 | add_subdirectory(tests) 115 | endif() 116 | 117 | `; 118 | 119 | const EXAMPLE1_CPP 120 | = `\ 121 | #include 122 | 123 | #include <<>/<>.hpp> 124 | 125 | int main() { 126 | std::cout << "I am an example executable\\n"; 127 | std::cout << "Let's calculate the value...\\n"; 128 | const auto value = <>::<>(); 129 | std::cout << "The value we got is " << value << '\\n'; 130 | } 131 | `; 132 | 133 | const EXAMPLES_CML 134 | = `\ 135 | add_executable(example1 example1.cpp) 136 | 137 | target_link_libraries(example1 PRIVATE <>) 138 | `; 139 | 140 | const CML_INCL_EXAMPLES 141 | = `\ 142 | option(BUILD_EXAMPLES "Build examples" ON) 143 | if(BUILD_EXAMPLES AND (PROJECT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR)) 144 | add_subdirectory(examples) 145 | endif() 146 | 147 | `; 148 | 149 | function expandTemplate(tmpl: string, params: NewProjectParams): string { 150 | return tmpl.replace(/<>/g, params.name) 151 | .replace(/<>/g, dirForNamespace(params.rootNamespace)) 152 | .replace(/<>/g, params.rootNamespace) 153 | .replace(/<>/g, params.rootNamespace.toUpperCase()) 154 | .replace(/<>/g, `${params.rootNamespace}::${params.name}`) 155 | .replace(/<>/g, params.name.replace(/-/g, '_')); 156 | } 157 | 158 | export async function createCMakeFiles(prDir: string, params: NewProjectParams) { 159 | const cml = path.join(prDir, 'CMakeLists.txt'); 160 | let acc = CML_HEAD; 161 | 162 | function expand(tmpl: string) { return expandTemplate(tmpl, params); } 163 | 164 | // Include third_party if needed 165 | if (params.generateThirdParty) { 166 | const thirdCML = path.join(prDir, 'third_party', 'CMakeLists.txt'); 167 | await fs.writeFile(thirdCML, '\n'); 168 | acc += CML_INCL_THIRD; 169 | } 170 | 171 | // Write the src/CMakeLists.txt 172 | let srcAcc = SRC_CML; 173 | if (params.separateHeaders) { 174 | srcAcc += SRC_CML_WITH_INCLUDE_DIR; 175 | } else { 176 | srcAcc += SRC_CML_WITH_NO_INCLUDE_DIR; 177 | } 178 | srcAcc = expand(srcAcc); 179 | await fs.writeFile(path.join(prDir, 'src/CMakeLists.txt'), srcAcc); 180 | 181 | const libDir = path.join(prDir, 'src', dirForNamespace(params.rootNamespace)); 182 | await fs.writeFile(path.join(libDir, `${params.name}.hpp`), expand(FIRST_HPP)); 183 | await fs.writeFile(path.join(libDir, `${params.name}.cpp`), expand(FIRST_CPP)); 184 | 185 | // Write an initial test 186 | const testsDir = path.join(prDir, 'tests'); 187 | await fs.writeFile(path.join(testsDir, 'my_test.cpp'), expand(FIRST_TEST_CPP)); 188 | await fs.writeFile(path.join(testsDir, 'CMakeLists.txt'), expand(TEST_CML)); 189 | 190 | // The middle of the top cml: 191 | acc += CML_MIDDLE; 192 | 193 | // Generate some example files 194 | if (params.generateExamples) { 195 | const examplesDir = path.join(prDir, 'examples'); 196 | await fs.writeFile(path.join(examplesDir, 'example1.cpp'), expand(EXAMPLE1_CPP)); 197 | await fs.writeFile(path.join(examplesDir, 'CMakeLists.txt'), expand(EXAMPLES_CML)); 198 | acc += CML_INCL_EXAMPLES; 199 | } 200 | 201 | // Write the root CMakeLists.txt 202 | acc = expandTemplate(acc, params); 203 | await fs.writeFile(cml, acc); 204 | } -------------------------------------------------------------------------------- /extras/vscode-pitchfork/src/dirs.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Module to create directories 3 | */ /** */ 4 | 5 | import * as path from 'path'; 6 | 7 | import {BuildSystem, NewProjectParams} from './new'; 8 | import {fs} from './pr'; 9 | 10 | export function dirForNamespace(ns: string): string { return ns.replace(/::/g, '/'); } 11 | 12 | export async function createDirectories(basePath: string, params: NewProjectParams) { 13 | const pfDir = path.join(basePath, params.name); 14 | // Create the directory where the project will live 15 | await fs.mkdir_p(pfDir); 16 | async function mkdir(subdir: string) { return fs.mkdir_p(path.join(pfDir, subdir)); } 17 | // Create the source directory 18 | const srcDir = path.join(pfDir, 'src'); 19 | // And all the subdirectories based on the namespace 20 | await fs.mkdir_p(path.join(srcDir, dirForNamespace(params.rootNamespace))); 21 | // Generate the include/ dir, if applicable 22 | if (params.separateHeaders) { 23 | const incDir = path.join(pfDir, 'include'); 24 | await fs.mkdir_p(path.join(incDir, dirForNamespace(params.rootNamespace))); 25 | } 26 | if (params.generateThirdParty) { 27 | await mkdir('third_party'); 28 | } 29 | if (params.generateExamples) { 30 | await mkdir('examples'); 31 | } 32 | await mkdir('tests'); 33 | await mkdir('doc'); 34 | if (params.buildSystem === BuildSystem.CMake) { 35 | await mkdir('cmake'); 36 | } 37 | } -------------------------------------------------------------------------------- /extras/vscode-pitchfork/src/extension.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import * as vscode from 'vscode'; 3 | import * as path from 'path'; 4 | import {SettingsAccess} from './settings'; 5 | import {getNewProjectParametersFromUI, createNewProject, NewProjectParams} from './new'; 6 | import { fs } from './pr'; 7 | 8 | 9 | class Extension { 10 | constructor(public readonly extensionContext: vscode.ExtensionContext) {} 11 | 12 | readonly settings = new SettingsAccess(this.extensionContext); 13 | 14 | get baseDirPath(): string|undefined { return this.settings.globalSettings.baseDirPath; } 15 | 16 | async changeBaseDir() { 17 | const old = this.baseDirPath; 18 | const chosen = await vscode.window.showOpenDialog({ 19 | canSelectFiles: false, 20 | canSelectFolders: true, 21 | canSelectMany: false, 22 | defaultUri: old ? vscode.Uri.file(old) : undefined, 23 | openLabel: 'Select', 24 | }); 25 | if (chosen === undefined) { 26 | return; 27 | } 28 | console.assert(chosen.length === 1, 'More than one file chosen?'); 29 | const newSettings = {...this.settings.globalSettings, baseDirPath: chosen[0].fsPath}; 30 | await this.settings.setGlobalSettings(newSettings); 31 | } 32 | 33 | async newProject() { 34 | let baseDir = this.baseDirPath; 35 | while (!baseDir) { 36 | const okayString = 'Okay'; 37 | const chosen = await vscode.window.showInformationMessage( 38 | 'Before creating a project, you must set the base directory for all projects', 39 | {modal: true}, 40 | okayString, 41 | ); 42 | if (chosen === undefined) { 43 | return; 44 | } 45 | console.assert(chosen === okayString, 'Clicked on other button?', chosen); 46 | await this.changeBaseDir(); 47 | baseDir = this.baseDirPath; 48 | } 49 | 50 | const params = await getNewProjectParametersFromUI(baseDir); 51 | if (!params) { 52 | return; 53 | } 54 | const uri = await createNewProject(baseDir, params); 55 | vscode.commands.executeCommand('vscode.openFolder', uri, true); 56 | } 57 | } 58 | 59 | export async function activate(context: vscode.ExtensionContext) { 60 | const ext = new Extension(context); 61 | context.subscriptions.push( 62 | vscode.commands.registerCommand( 63 | 'pf.changeBaseDir', 64 | async () => ext.changeBaseDir(), 65 | ), 66 | vscode.commands.registerCommand( 67 | 'pf.newProject', 68 | async () => ext.newProject(), 69 | ), 70 | ); 71 | if (vscode.workspace.workspaceFolders) { 72 | const first = vscode.workspace.workspaceFolders[0]; 73 | const pfInitPath = path.join(first.uri.fsPath, '.pitchfork-init'); 74 | const stat = await fs.tryStat(pfInitPath); 75 | if (stat && stat.isFile()) { 76 | try { 77 | const data: NewProjectParams = JSON.parse((await fs.readFile(pfInitPath)).toString()); 78 | await ext.settings.setLocalSettings({ 79 | rootNamespace: data.rootNamespace, 80 | }); 81 | // Remove the file now that we have loaded its contents 82 | await fs.unlink(pfInitPath); 83 | } catch (e) { 84 | vscode.window.showErrorMessage('The .pitchfork-init file in this directory is not valid JSON.'); 85 | } 86 | } 87 | } 88 | } 89 | 90 | // this method is called when your extension is deactivated 91 | export function deactivate() {} -------------------------------------------------------------------------------- /extras/vscode-pitchfork/src/files.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vector-of-bool/pitchfork/1f1d16549408dc29dae03fc1a479c93556239112/extras/vscode-pitchfork/src/files.ts -------------------------------------------------------------------------------- /extras/vscode-pitchfork/src/new.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Module concerning the generation of new Pitchfork projects 3 | */ /** */ 4 | 5 | import * as path from 'path'; 6 | import * as vscode from 'vscode'; 7 | 8 | import {createDirectories} from './dirs'; 9 | import {fs} from './pr'; 10 | import { createCMakeFiles } from './cmake'; 11 | 12 | 13 | enum WizardState { 14 | GetName = 1, 15 | GetRootNamespace, 16 | GetBuildSystem, 17 | GetOtherFlags, 18 | Finished, 19 | Cancelled, 20 | } 21 | 22 | enum NewProjectFlags { 23 | None = 0, 24 | GenerateThirdParty = 1 << 1, 25 | SeparateHeaders = 1 << 2, 26 | GenerateExamples = 1 << 3, 27 | } 28 | 29 | export enum BuildSystem { 30 | None = 0, 31 | CMake = 1, 32 | } 33 | 34 | export interface NewProjectParams { 35 | name: string; 36 | rootNamespace: string; 37 | buildSystem: BuildSystem; 38 | generateThirdParty: boolean; 39 | generateExamples: boolean; 40 | separateHeaders: boolean; 41 | } 42 | 43 | type Awaitable = T|Thenable; 44 | 45 | class NewProjectWizard { 46 | constructor(readonly baseDir: string) {} 47 | private _state: WizardState = WizardState.GetName; 48 | 49 | // Parameters for the new project. 50 | private _name = ''; 51 | private _rootNamespace = ''; 52 | private _buildSystem = BuildSystem.None; 53 | private _otherFlags = NewProjectFlags.None; 54 | 55 | async runToCompletion(): Promise { 56 | while (true) { 57 | await this._runNext(); 58 | if (this._state === WizardState.Finished) { 59 | break; 60 | } 61 | if (this._state === WizardState.Cancelled) { 62 | return undefined; 63 | } 64 | } 65 | return { 66 | name: this._name, 67 | rootNamespace: this._rootNamespace, 68 | buildSystem: this._buildSystem, 69 | generateThirdParty: !!(this._otherFlags & NewProjectFlags.GenerateThirdParty), 70 | generateExamples: !!(this._otherFlags & NewProjectFlags.GenerateExamples), 71 | separateHeaders: !!(this._otherFlags & NewProjectFlags.SeparateHeaders), 72 | }; 73 | } 74 | 75 | private _runNext() { 76 | switch (this._state) { 77 | case WizardState.GetName: 78 | return this._getName(); 79 | case WizardState.GetRootNamespace: 80 | return this._getRootNamespace(); 81 | case WizardState.GetBuildSystem: 82 | return this._getBuildSystem(); 83 | case WizardState.GetOtherFlags: 84 | return this._getOtherFlags(); 85 | default: 86 | debugger; 87 | console.assert(false, 'Bad wizard state', this._state); 88 | throw new Error('Bad wizard'); 89 | } 90 | } 91 | 92 | private _initQuickInput(input: vscode.QuickInput, title: string) { 93 | input.title = title; 94 | input.step = this._state as number; 95 | input.totalSteps = this._state as number; 96 | input.ignoreFocusOut = true; 97 | } 98 | 99 | private _createInputBox(name: string, prompt: string): vscode.InputBox { 100 | const box = vscode.window.createInputBox(); 101 | this._initQuickInput(box, name); 102 | box.prompt = prompt; 103 | return box; 104 | } 105 | 106 | private _createQuickPick(name: string): vscode.QuickPick { 107 | const pick = vscode.window.createQuickPick(); 108 | this._initQuickInput(pick, name); 109 | return pick; 110 | } 111 | 112 | private async _getInputString( 113 | opt: { 114 | title: string, 115 | prompt: string, 116 | defaultValue?: string, validate?(value: string): Awaitable, 117 | }, 118 | ): Promise { 119 | const box = this._createInputBox(opt.title, opt.prompt); 120 | if (opt.defaultValue) { 121 | box.value = opt.defaultValue; 122 | } 123 | try { 124 | let resolved = false; 125 | return await new Promise(resolve => { 126 | box.onDidAccept(async () => { 127 | if (opt.validate) { 128 | box.busy = true; 129 | box.validationMessage = await opt.validate(box.value); 130 | box.busy = false; 131 | } 132 | if (box.validationMessage) { 133 | return; 134 | } 135 | resolved = true; 136 | resolve(box.value); 137 | }); 138 | box.onDidChangeValue(async value => { 139 | if (opt.validate) { 140 | box.busy = true; 141 | box.validationMessage = await opt.validate(value); 142 | box.busy = false; 143 | } 144 | }); 145 | box.onDidHide(() => { 146 | if (!resolved) { 147 | resolve(undefined); 148 | } 149 | }); 150 | box.show(); 151 | }); 152 | } finally { 153 | // Close the box 154 | box.dispose(); 155 | } 156 | } 157 | 158 | private async _getName() { 159 | const name = await this._getInputString({ 160 | title: 'Project Name', 161 | prompt: 'Enter the name for your new project', 162 | validate: async newName => { 163 | if (newName === '') { 164 | return 'A project name is required'; 165 | } 166 | const newDirPath = path.join(this.baseDir, newName); 167 | const stat = await fs.tryStat(newDirPath); 168 | if (!stat) { 169 | return undefined; 170 | } else if (stat.isDirectory()) { 171 | return 'A project with this name already exists'; 172 | } else { 173 | return 'A file in the project directory already has this name'; 174 | } 175 | }, 176 | }); 177 | if (!name) { 178 | // User cancelled input 179 | this._state = WizardState.Cancelled; 180 | } else { 181 | this._name = name; 182 | this._state++; 183 | } 184 | } 185 | 186 | private async _getRootNamespace() { 187 | const ns = await this._getInputString({ 188 | title: 'Root Namespace', 189 | prompt: 'Enter the base root namespace for the new project', 190 | defaultValue: this._name, 191 | validate: newNS => { 192 | const items = newNS.split('::'); 193 | const badItem = items.find(elem => !/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(elem)); 194 | if (badItem || newNS.endsWith('::')) { 195 | return 'Invalid C++ namespace'; 196 | } 197 | return; 198 | } 199 | }); 200 | if (!ns) { 201 | this._state = WizardState.Cancelled; 202 | } else { 203 | this._rootNamespace = ns; 204 | this._state++; 205 | } 206 | } 207 | 208 | private async _getQuickPick(opt: { 209 | title: string, 210 | items: T[], 211 | canSelectMany?: boolean, 212 | }): Promise { 213 | const pick = this._createQuickPick(opt.title); 214 | if (opt.canSelectMany) { 215 | pick.canSelectMany = true; 216 | } 217 | pick.items = opt.items; 218 | try { 219 | let resolved = false; 220 | return await new Promise(resolve => { 221 | pick.onDidAccept(() => { 222 | if (!pick.selectedItems) { 223 | return; 224 | } 225 | resolved = true; 226 | resolve([...pick.selectedItems]); 227 | }); 228 | pick.onDidHide(() => { 229 | if (!resolved) { 230 | resolve(undefined); 231 | } 232 | }); 233 | pick.show(); 234 | }); 235 | } finally { 236 | // Dispose of the page 237 | pick.dispose(); 238 | } 239 | } 240 | 241 | private async _getBuildSystem() { 242 | interface BuildSystemItems { 243 | label: string; 244 | detail: string; 245 | buildSystem: BuildSystem; 246 | } 247 | const chosen = await this._getQuickPick({ 248 | title: 'Build System', 249 | items: [ 250 | { 251 | label: 'CMake', 252 | detail: 'Generate a CMake build system in the new project', 253 | buildSystem: BuildSystem.CMake, 254 | }, 255 | { 256 | label: 'None', 257 | detail: 'Do not generate a build system', 258 | buildSystem: BuildSystem.None, 259 | }, 260 | ], 261 | }); 262 | if (!chosen) { 263 | this._state = WizardState.Cancelled; 264 | } else { 265 | this._state++; 266 | this._buildSystem = chosen[0].buildSystem; 267 | } 268 | } 269 | 270 | private async _getOtherFlags() { 271 | interface OtherOptionItems { 272 | label: string; 273 | detail: string; 274 | flag: NewProjectFlags; 275 | picked: boolean; 276 | } 277 | const chosen = await this._getQuickPick({ 278 | title: 'Other Options', 279 | canSelectMany: true, 280 | items: [ 281 | { 282 | label: 'Separate Headers and Sources', 283 | detail: 'Put header files in a separate `include/` directory', 284 | flag: NewProjectFlags.SeparateHeaders, 285 | picked: false, 286 | }, 287 | { 288 | label: 'Generate a third_party/ directory', 289 | detail: 'Create a third_party/ directory for external libraries', 290 | flag: NewProjectFlags.GenerateThirdParty, 291 | picked: false, 292 | }, 293 | { 294 | label: 'Generate an examples/ directory', 295 | detail: 'Create an examples/ directory for library/program example usages', 296 | flag: NewProjectFlags.GenerateExamples, 297 | picked: true, 298 | }, 299 | ], 300 | }); 301 | if (!chosen) { 302 | this._state = WizardState.Cancelled; 303 | } else { 304 | this._otherFlags = chosen.reduce((acc, item) => acc | item.flag, NewProjectFlags.None); 305 | this._state++; 306 | } 307 | } 308 | } 309 | 310 | /** 311 | * Show the new project UI and accept input from the user. Returns the parameters 312 | * the user requested from the UI. 313 | * @param baseDir The directory where the project may be placed (only used for validation) 314 | */ 315 | export function getNewProjectParametersFromUI(baseDir: string): Promise { 316 | const wiz = new NewProjectWizard(baseDir); 317 | return wiz.runToCompletion(); 318 | } 319 | 320 | /** 321 | * Create a new Pitchfork file structure on disk. 322 | * @param baseDir The directory in which to create the project 323 | * @param params The parameters to use when creating the project 324 | */ 325 | export async function createNewProject(baseDir: string, params: NewProjectParams): Promise { 326 | await createDirectories(baseDir, params); 327 | const prDir = path.join(baseDir, params.name); 328 | if (params.buildSystem === BuildSystem.CMake) { 329 | await createCMakeFiles(prDir, params); 330 | } 331 | // Since we will be opening the project in a new VSCode window, we will lose 332 | // the parameters used to create the project. Write those parameters to a file 333 | // that will be loaded by the extension immediately when the project is opened 334 | // so that we can continue as usual. 335 | const pfInitPath = path.join(prDir, '.pitchfork-init'); 336 | await fs.writeFile(pfInitPath, JSON.stringify(params)); 337 | return vscode.Uri.file(prDir); 338 | } 339 | -------------------------------------------------------------------------------- /extras/vscode-pitchfork/src/pr.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This module promise-ifies some NodeJS APIs that are frequently used in this 3 | * ext. 4 | */ /** */ 5 | 6 | // import promisify_ = require('es6-promisify'); 7 | import * as util from 'util'; 8 | // VSCode doesn't ship with util.promisify yet, but we do have type definitions for it, so we'll 9 | // hack those type definitions onto the type of es6-promisify >:) 10 | const promisify = util.promisify; 11 | 12 | import * as fs_ from 'fs'; 13 | import * as path from 'path'; 14 | 15 | /** 16 | * Wrappers for the `fs` module. 17 | * 18 | * Also has a few utility functions 19 | */ 20 | export namespace fs { 21 | 22 | export function exists(fspath: string): Promise { 23 | return new Promise((resolve, _reject) => { fs_.exists(fspath, res => resolve(res)); }); 24 | } 25 | 26 | export const readFile = promisify(fs_.readFile); 27 | 28 | export const writeFile = promisify(fs_.writeFile); 29 | 30 | export const readdir = promisify(fs_.readdir); 31 | 32 | export const mkdir = promisify(fs_.mkdir); 33 | 34 | export const mkdtemp = promisify(fs_.mkdtemp); 35 | 36 | export const rename = promisify(fs_.rename); 37 | 38 | export const stat = promisify(fs_.stat); 39 | 40 | /** 41 | * Try and stat() a file. If stat() fails for *any reason*, returns `null`. 42 | * @param filePath The file to try and stat() 43 | */ 44 | export async function tryStat(filePath: fs_.PathLike): Promise { 45 | try { 46 | return await stat(filePath); 47 | } catch (_e) { 48 | // Don't even bother with the error. Any number of things might have gone 49 | // wrong. Probably one of: Non-existing file, bad permissions, bad path. 50 | return null; 51 | } 52 | } 53 | 54 | export const readlink = promisify(fs_.readlink); 55 | 56 | export const unlink = promisify(fs_.unlink); 57 | 58 | export const appendFile = promisify(fs_.appendFile); 59 | 60 | /** 61 | * Creates a directory and all parent directories recursively. If the file 62 | * already exists, and is not a directory, just return. 63 | * @param fspath The directory to create 64 | */ 65 | export async function mkdir_p(fspath: string): Promise { 66 | const parent = path.dirname(fspath); 67 | if (!await exists(parent)) { 68 | await mkdir_p(parent); 69 | } else { 70 | if (!(await stat(parent)).isDirectory()) { 71 | throw new Error('Cannot create ${fspath}: ${parent} is a non-directory'); 72 | } 73 | } 74 | if (!await exists(fspath)) { 75 | await mkdir(fspath); 76 | } else { 77 | if (!(await stat(fspath)).isDirectory()) { 78 | throw new Error('Cannot mkdir_p on ${fspath}. It exists, and is not a directory!'); 79 | } 80 | } 81 | } 82 | 83 | /** 84 | * Copy a file from one location to another. 85 | * @param inpath The input file 86 | * @param outpath The output file 87 | */ 88 | export function copyFile(inpath: string, outpath: string): Promise { 89 | return new Promise((resolve, reject) => { 90 | const reader = fs_.createReadStream(inpath); 91 | reader.on('error', e => reject(e)); 92 | reader.on('open', _fd => { 93 | const writer = fs_.createWriteStream(outpath); 94 | writer.on('error', e => reject(e)); 95 | writer.on('open', _fd2 => { reader.pipe(writer); }); 96 | writer.on('close', () => resolve()); 97 | }); 98 | }); 99 | } 100 | 101 | /** 102 | * Create a hard link of an existing file 103 | * @param inPath The existing file path 104 | * @param outPath The new path to the hard link 105 | */ 106 | export function hardLinkFile(inPath: string, outPath: string): Promise { 107 | return new Promise((resolve, reject) => { 108 | fs_.link(inPath, outPath, err => { 109 | if (err) { 110 | reject(err); 111 | } else { 112 | resolve(); 113 | } 114 | }); 115 | }); 116 | } 117 | } -------------------------------------------------------------------------------- /extras/vscode-pitchfork/src/settings.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Module for extension settings, even those not stored in `settings.json` 3 | */ /** */ 4 | 5 | import * as vscode from 'vscode'; 6 | 7 | export interface GlobalSettings { 8 | baseDirPath?: string; 9 | } 10 | 11 | export interface LocalSettings { 12 | rootNamespace?: string; 13 | } 14 | 15 | export class SettingsAccess { 16 | constructor(public readonly extensionContext: vscode.ExtensionContext) {} 17 | 18 | get globalSettings(): GlobalSettings { 19 | return this.extensionContext.globalState.get('settings', {}); 20 | } 21 | 22 | async setGlobalSettings(s: GlobalSettings): Promise { 23 | await this.extensionContext.globalState.update('settings', s); 24 | } 25 | 26 | get lobalSettings(): LocalSettings { 27 | return this.extensionContext.workspaceState.get('settings', {}); 28 | } 29 | 30 | async setLocalSettings(s: LocalSettings): Promise { 31 | return this.extensionContext.workspaceState.update('settings', s); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /extras/vscode-pitchfork/src/test/extension.test.ts: -------------------------------------------------------------------------------- 1 | // 2 | // Note: This example test is leveraging the Mocha test framework. 3 | // Please refer to their documentation on https://mochajs.org/ for help. 4 | // 5 | 6 | // The module 'assert' provides assertion methods from node 7 | import * as assert from 'assert'; 8 | 9 | // You can import and use all API from the 'vscode' module 10 | // as well as import your extension to test it 11 | // import * as vscode from 'vscode'; 12 | // import * as myExtension from '../extension'; 13 | 14 | // Defines a Mocha test suite to group tests of similar kind together 15 | suite("Extension Tests", function () { 16 | 17 | // Defines a Mocha unit test 18 | test("Something 1", function() { 19 | assert.equal(-1, [1, 2, 3].indexOf(5)); 20 | assert.equal(-1, [1, 2, 3].indexOf(0)); 21 | }); 22 | }); -------------------------------------------------------------------------------- /extras/vscode-pitchfork/src/test/index.ts: -------------------------------------------------------------------------------- 1 | // 2 | // PLEASE DO NOT MODIFY / DELETE UNLESS YOU KNOW WHAT YOU ARE DOING 3 | // 4 | // This file is providing the test runner to use when running extension tests. 5 | // By default the test runner in use is Mocha based. 6 | // 7 | // You can provide your own test runner if you want to override it by exporting 8 | // a function run(testRoot: string, clb: (error:Error) => void) that the extension 9 | // host can call to run the tests. The test runner is expected to use console.log 10 | // to report the results back to the caller. When the tests are finished, return 11 | // a possible error to the callback or null if none. 12 | 13 | import * as testRunner from 'vscode/lib/testrunner'; 14 | 15 | // You can directly control Mocha options by uncommenting the following lines 16 | // See https://github.com/mochajs/mocha/wiki/Using-mocha-programmatically#set-options for more info 17 | testRunner.configure({ 18 | ui: 'tdd', // the TDD UI is being used in extension.test.ts (suite, test, etc.) 19 | useColors: true // colored output from test results 20 | }); 21 | 22 | module.exports = testRunner; -------------------------------------------------------------------------------- /extras/vscode-pitchfork/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es2017", 5 | "outDir": "out", 6 | "lib": [ 7 | "es6" 8 | ], 9 | "sourceMap": true, 10 | "rootDir": "src", 11 | /* Strict Type-Checking Option */ 12 | "strict": true, /* enable all strict type-checking options */ 13 | /* Additional Checks */ 14 | "noUnusedLocals": true, /* Report errors on unused locals. */ 15 | "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 16 | "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 17 | "noUnusedParameters": true /* Report errors on unused parameters. */ 18 | }, 19 | "exclude": [ 20 | "node_modules", 21 | ".vscode-test" 22 | ] 23 | } -------------------------------------------------------------------------------- /extras/vscode-pitchfork/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-unused-expression": true, 4 | "no-duplicate-variable": true, 5 | "no-unused-variable": true, 6 | "curly": false, 7 | "class-name": true, 8 | "semicolon": [true, "always"], 9 | "prefer-for-of": true, 10 | "only-arrow-functions": [true, "allow-declarations"], 11 | "await-promise": [true, "Thenable", "PromisedAssertion"], 12 | "ban-comma-operator": true, 13 | "no-console": [true, "info", "error", "warn"], 14 | "no-floating-promises": [true, "PromisedAssertion"], 15 | /// Errors when we use `tsconfig` "paths" option. Check for a fix in newer TSLint: 16 | // "no-implicit-dependencies": [true, "dev"], 17 | "no-shadowed-variable": true, 18 | "no-return-await": true, 19 | "no-string-throw": true, 20 | "no-switch-case-fall-through": true, 21 | "no-unbound-method": true, 22 | "no-this-assignment": true, 23 | "no-unnecessary-class": true, 24 | "no-unsafe-finally": true, 25 | "no-var-keyword": true, 26 | "prefer-object-spread": true, 27 | "restrict-plus-operands": true, 28 | "strict-type-predicates": true, 29 | "prefer-const": true, 30 | "prefer-readonly": true, 31 | "arrow-parens": [true, "ban-single-arg-parens"], 32 | "arrow-return-shorthand": true, 33 | "no-boolean-literal-compare": true, 34 | "no-angle-bracket-type-assertion": true, 35 | "no-trailing-whitespace": true, 36 | "object-literal-shorthand": true, 37 | "object-literal-key-quotes": [true, "as-needed"], 38 | "prefer-template": [true, "allow-single-concat"] 39 | } 40 | } -------------------------------------------------------------------------------- /extras/vscode-pitchfork/vsc-extension-quickstart.md: -------------------------------------------------------------------------------- 1 | # Welcome to your VS Code Extension 2 | 3 | ## What's in the folder 4 | * This folder contains all of the files necessary for your extension. 5 | * `package.json` - this is the manifest file in which you declare your extension and command. 6 | The sample plugin registers a command and defines its title and command name. With this information 7 | VS Code can show the command in the command palette. It doesn’t yet need to load the plugin. 8 | * `src/extension.ts` - this is the main file where you will provide the implementation of your command. 9 | The file exports one function, `activate`, which is called the very first time your extension is 10 | activated (in this case by executing the command). Inside the `activate` function we call `registerCommand`. 11 | We pass the function containing the implementation of the command as the second parameter to 12 | `registerCommand`. 13 | 14 | ## Get up and running straight away 15 | * Press `F5` to open a new window with your extension loaded. 16 | * Run your command from the command palette by pressing (`Ctrl+Shift+P` or `Cmd+Shift+P` on Mac) and typing `Hello World`. 17 | * Set breakpoints in your code inside `src/extension.ts` to debug your extension. 18 | * Find output from your extension in the debug console. 19 | 20 | ## Make changes 21 | * You can relaunch the extension from the debug toolbar after changing code in `src/extension.ts`. 22 | * You can also reload (`Ctrl+R` or `Cmd+R` on Mac) the VS Code window with your extension to load your changes. 23 | 24 | ## Explore the API 25 | * You can open the full set of our API when you open the file `node_modules/vscode/vscode.d.ts`. 26 | 27 | ## Run tests 28 | * Open the debug viewlet (`Ctrl+Shift+D` or `Cmd+Shift+D` on Mac) and from the launch configuration dropdown pick `Launch Tests`. 29 | * Press `F5` to run the tests in a new window with your extension loaded. 30 | * See the output of the test result in the debug console. 31 | * Make changes to `test/extension.test.ts` or create new test files inside the `test` folder. 32 | * By convention, the test runner will only consider files matching the name pattern `**.test.ts`. 33 | * You can create folders inside the `test` folder to structure your tests any way you want. 34 | -------------------------------------------------------------------------------- /pmm.cmake: -------------------------------------------------------------------------------- 1 | ## MIT License 2 | ## 3 | ## Copyright (c) 2018 vector-of-bool 4 | ## 5 | ## Permission is hereby granted, free of charge, to any person obtaining a copy 6 | ## of this software and associated documentation files (the "Software"), to deal 7 | ## in the Software without restriction, including without limitation the rights 8 | ## to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | ## copies of the Software, and to permit persons to whom the Software is 10 | ## furnished to do so, subject to the following conditions: 11 | ## 12 | ## The above copyright notice and this permission notice shall be included in all 13 | ## copies or substantial portions of the Software. 14 | ## 15 | ## THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | ## IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | ## FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | ## AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | ## LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | ## OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | ## SOFTWARE. 22 | 23 | # Bump this version to change what PMM version is downloaded 24 | set(PMM_VERSION_INIT 1.3.1) 25 | 26 | # Helpful macro to set a variable if it isn't already set 27 | macro(_pmm_set_if_undef varname) 28 | if(NOT DEFINED "${varname}") 29 | set("${varname}" "${ARGN}") 30 | endif() 31 | endmacro() 32 | 33 | ## Variables used by this script 34 | # The version: 35 | _pmm_set_if_undef(PMM_VERSION ${PMM_VERSION_INIT}) 36 | # The base URL we download PMM from: 37 | _pmm_set_if_undef(PMM_URL_BASE "https://vector-of-bool.github.io/pmm") 38 | # The real URL we download from (Based on the version) 39 | _pmm_set_if_undef(PMM_URL "${PMM_URL_BASE}/${PMM_VERSION}") 40 | # The directory where we store our downloaded files 41 | _pmm_set_if_undef(PMM_DIR_BASE "${CMAKE_BINARY_DIR}/_pmm") 42 | _pmm_set_if_undef(PMM_DIR "${PMM_DIR_BASE}/${PMM_VERSION}") 43 | 44 | # The file that we first download 45 | set(_PMM_ENTRY_FILE "${PMM_DIR}/entry.cmake") 46 | 47 | if(NOT EXISTS "${_PMM_ENTRY_FILE}" OR PMM_ALWAYS_DOWNLOAD) 48 | file( 49 | DOWNLOAD "${PMM_URL}/entry.cmake" 50 | "${_PMM_ENTRY_FILE}.tmp" 51 | STATUS pair 52 | ) 53 | list(GET pair 0 rc) 54 | list(GET pair 1 msg) 55 | if(rc) 56 | message(FATAL_ERROR "Failed to download PMM entry file") 57 | endif() 58 | file(RENAME "${_PMM_ENTRY_FILE}.tmp" "${_PMM_ENTRY_FILE}") 59 | endif() 60 | 61 | # ^^^ DO NOT CHANGE THIS LINE vvv 62 | set(_PMM_BOOTSTRAP_VERSION 1) 63 | # ^^^ DO NOT CHANGE THIS LINE ^^^ 64 | 65 | include("${_PMM_ENTRY_FILE}") 66 | -------------------------------------------------------------------------------- /src/pf.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | namespace fs = pf::fs; 18 | 19 | namespace { 20 | 21 | using string_flag = args::ValueFlag; 22 | using path_flag = args::ValueFlag; 23 | 24 | class reached_eof : public std::exception {}; 25 | 26 | std::string get_input_line() { 27 | std::cout.flush(); 28 | std::cerr.flush(); 29 | std::string ret; 30 | std::getline(std::cin, ret); 31 | if (std::cin.eof()) { 32 | throw reached_eof(); 33 | } 34 | return ret; 35 | } 36 | 37 | fs::path default_base_dir() { 38 | auto ptr = std::getenv("PF_BASE_DIR"); 39 | if (ptr) { 40 | return ptr; 41 | } 42 | 43 | // Attempt to detect the base directory 44 | return pf::detect_base_dir() 45 | // Just use the cwd if cannot detect base dir 46 | .value_or(fs::current_path()); 47 | } 48 | 49 | struct cli_common { 50 | args::ArgumentParser& parser; 51 | std::shared_ptr console = spdlog::stdout_color_mt("console"); 52 | // Flags that are not subcommand-specific: 53 | args::HelpFlag help{parser, "help", "Print this help message", {'h', "help"}}; 54 | // base-dir determines where projects will live 55 | path_flag base_dir_arg{parser, 56 | "base_dir", 57 | "The base directory for projects\n[env: PF_BASE_DIR]", 58 | {'B', "base-dir"}, 59 | default_base_dir()}; 60 | 61 | args::Group cmd_group{parser, "Available Commands"}; 62 | 63 | explicit cli_common(args::ArgumentParser& args) 64 | : parser{args} {} 65 | 66 | fs::path get_base_dir() { 67 | auto base_dir = base_dir_arg.Get(); 68 | if (!base_dir.empty()) { 69 | return fs::absolute(base_dir); 70 | } 71 | // Just use the cwd if the base dir provided was empty 72 | return fs::current_path(); 73 | } 74 | }; 75 | 76 | class cmd_list { 77 | private: 78 | cli_common& _cli; 79 | args::Command _cmd{_cli.cmd_group, "list", "List projects"}; 80 | 81 | public: 82 | explicit cmd_list(cli_common& gl) 83 | : _cli{gl} {} 84 | 85 | explicit operator bool() const { return !!_cmd; } 86 | 87 | int run() { 88 | std::error_code ec; 89 | const auto base_dir = _cli.get_base_dir(); 90 | auto iter = fs::directory_iterator{base_dir, ec}; 91 | if (ec) { 92 | _cli.console->error("Failed to enumerate directory ({}): {}", base_dir, ec.message()); 93 | return 1; 94 | } 95 | 96 | for (fs::path child : fs::directory_iterator{base_dir}) { 97 | if (!fs::is_directory(child, ec)) { 98 | if (ec) { 99 | _cli.console->warn("Failed to enumerate item ({}): {}", child, ec.message()); 100 | } 101 | continue; 102 | } 103 | std::cout << child.filename().string() << '\n'; 104 | } 105 | return 0; 106 | } 107 | }; 108 | 109 | class toggle_flag : public args::Flag { 110 | public: 111 | enum state { 112 | unset, 113 | enabled, 114 | disabled, 115 | }; 116 | state _state = unset; 117 | 118 | private: 119 | std::string GetNameString(const args::HelpParams&) const override { return "--[no-]" + Name(); } 120 | 121 | args::FlagBase* Match(const args::EitherFlag& flag) override { 122 | auto ret = Flag::Match(flag); 123 | if (ret) { 124 | auto off = flag.longFlag.find("no-"); 125 | _state = (off == 0) ? disabled : enabled; 126 | } 127 | return ret; 128 | } 129 | 130 | public: 131 | toggle_flag(args::Group& grp, const std::string& name, const std::string& help) 132 | : Flag::Flag(grp, name, help, {"no-" + name, name}) {} 133 | 134 | enum state state() const noexcept { return _state; } 135 | }; 136 | 137 | template 138 | FlagType get_string_value(args::ValueFlag& flag, const std::string& message) { 139 | auto ret = flag.Get(); 140 | while (ret.empty()) { 141 | std::cout << message << ": "; 142 | ret = FlagType(get_input_line()); 143 | } 144 | return ret; 145 | } 146 | 147 | template 148 | FlagType get_string_value(args::ValueFlag& flag, 149 | const std::string& message, 150 | const FlagType& default_) { 151 | auto ret = flag.Get(); 152 | if (!ret.empty()) { 153 | return ret; 154 | } 155 | 156 | std::cout << message << " [" << default_ << "]: "; 157 | ret = FlagType(get_input_line()); 158 | if (ret.empty()) { 159 | return default_; 160 | } 161 | return ret; 162 | } 163 | 164 | bool get_toggle_value(const toggle_flag& flag, const std::string& message, bool default_) { 165 | if (flag.state() == flag.enabled) { 166 | return true; 167 | } else if (flag.state() == flag.disabled) { 168 | return false; 169 | } 170 | 171 | while (1) { 172 | std::cout << message; 173 | std::cout << (default_ ? " [Yn]: " : " [yN]: "); 174 | auto chosen = get_input_line(); 175 | if (chosen.empty()) { 176 | return default_; 177 | } 178 | auto c = chosen[0]; 179 | if (c == 'y' || c == 'Y') { 180 | return true; 181 | } else if (c == 'n' || c == 'N') { 182 | return false; 183 | } 184 | } 185 | } 186 | 187 | template 188 | typename Map::value_type::second_type 189 | get_map_value(const std::string& message, const Map& map, const std::string& default_ = "") { 190 | while (1) { 191 | std::cout << message << '\n'; 192 | std::cout << "Chose one of:\n"; 193 | for (auto& pair : map) { 194 | std::cout << " - " << pair.first << '\n'; 195 | } 196 | if (default_.empty()) { 197 | std::cout << "Selection: "; 198 | } else { 199 | std::cout << fmt::format("Selection [{}]: ", default_); 200 | } 201 | std::cout.flush(); 202 | auto chosen = get_input_line(); 203 | if (chosen.empty() && !default_.empty()) { 204 | chosen = default_; 205 | } 206 | auto found = map.find(chosen); 207 | if (found != map.end()) { 208 | return found->second; 209 | } 210 | } 211 | } 212 | 213 | class cmd_new { 214 | private: 215 | cli_common& _cli; 216 | args::Command _cmd{_cli.cmd_group, "new", "Create a new project"}; 217 | args::HelpFlag _help{_cmd, "help", "Print help for the `new` subcommand", {'h', "help"}}; 218 | string_flag _name{_cmd, "name", "Name for the new project", {"name"}}; 219 | string_flag _namespace{_cmd, "namespace", "The root namespace for the project", {"namespace"}}; 220 | toggle_flag _split_headers{_cmd, "split-headers", "Store headers separate from source files"}; 221 | toggle_flag _gen_tests{_cmd, "tests", "Generate a tests/ directory"}; 222 | toggle_flag _gen_third_party{_cmd, "third-party", "Generate a third_party/ directory"}; 223 | toggle_flag _gen_examples{_cmd, "examples", "Generate an examples/ directory"}; 224 | toggle_flag _gen_extras{_cmd, "extras", "Generate an extras/ directory"}; 225 | string_flag 226 | _first_file_stem{_cmd, 227 | "first-file", 228 | "Stem of the first file to create in the root namespace (No extension)", 229 | {"first-file"}}; 230 | 231 | std::unordered_map _bs_map{ 232 | {"none", pf::build_system::none}, 233 | {"cmake", pf::build_system::cmake}, 234 | }; 235 | args::MapFlag _build_system{_cmd, 236 | "build-system", 237 | "The build system to generate", 238 | {'b', "build-system"}, 239 | _bs_map}; 240 | 241 | public: 242 | explicit cmd_new(cli_common& gl) 243 | : _cli{gl} {} 244 | 245 | explicit operator bool() const { return !!_cmd; } 246 | 247 | int run() { 248 | 249 | // Get the project name 250 | auto pr_name = get_string_value(_name, "Name for the new project"); 251 | 252 | // Check on the directory which we will create 253 | auto new_pr_dir = fs::absolute(_cli.get_base_dir() / pr_name); 254 | std::error_code ec; 255 | if (fs::exists(new_pr_dir, ec)) { 256 | _cli.console->error( 257 | "Cannot create project: Destination path names an existing file or directory ({})", 258 | fs::canonical(new_pr_dir)); 259 | return 1; 260 | } 261 | if (ec) { 262 | _cli.console->error("Failed to check on directory ({}): {}", new_pr_dir, ec.message()); 263 | return 2; 264 | } 265 | 266 | // Fill out the parameters for the new project 267 | auto root_namespace 268 | = get_string_value(_namespace, "Initial namespace", pf::namespace_for_name(pr_name)); 269 | 270 | // Final stuff 271 | auto first_file_stem = get_string_value(_first_file_stem, 272 | "First file stem (No extension)", 273 | new_pr_dir.stem().string()); 274 | 275 | // We have enough for the initial params set 276 | pf::new_project_params params{pr_name, root_namespace, first_file_stem, new_pr_dir}; 277 | 278 | // Process toggles 279 | params.separate_headers 280 | = get_toggle_value(_split_headers, "Split headers and sources?", false); 281 | params.create_tests = get_toggle_value(_gen_tests, "Generate a tests/ directory?", true); 282 | params.create_third_party 283 | = get_toggle_value(_gen_third_party, "Generate a third_party/ directory?", true); 284 | params.create_examples 285 | = get_toggle_value(_gen_examples, "Generate an examples/ directory?", true); 286 | params.create_extras 287 | = get_toggle_value(_gen_extras, "Generate an extras/ directory?", false); 288 | 289 | auto bs = _build_system.Get(); 290 | if (bs == pf::build_system::unspecified) { 291 | bs = get_map_value("Build system to generate", _bs_map, "cmake"); 292 | } 293 | params.build_system = bs; 294 | 295 | // Create the project! 296 | try { 297 | pf::create_project(params); 298 | } catch (const std::system_error& e) { 299 | _cli.console->error("Failed to create project in {}: {}", new_pr_dir, ec.message()); 300 | return 1; 301 | } 302 | return 0; 303 | } 304 | }; 305 | 306 | class cmd_update { 307 | private: 308 | cli_common& _cli; 309 | args::Command _cmd{_cli.cmd_group, "update", "Update sources for existing project"}; 310 | args::HelpFlag _help{_cmd, "help", "Print help for the `update` subcommand", {'h', "help"}}; 311 | std::unordered_map _bs_map{ 312 | {"cmake", pf::build_system::cmake}, 313 | }; 314 | args::MapFlag _build_system{_cmd, 315 | "build-system", 316 | "The build system to update", 317 | {'b', "build-system"}, 318 | _bs_map}; 319 | 320 | public: 321 | explicit cmd_update(cli_common& gl) 322 | : _cli{gl} {} 323 | 324 | explicit operator bool() const { return !!_cmd; } 325 | 326 | int run() { 327 | auto bs = _build_system.Get(); 328 | if (bs == pf::build_system::unspecified) { 329 | bs = get_map_value("Build system whose files to update", _bs_map, "cmake"); 330 | } 331 | 332 | // Only CMake supported at this time 333 | if (bs != pf::build_system::cmake) { 334 | _cli.console->error( 335 | "CMake is the only supported build system for the `update` subcommand"); 336 | return 1; 337 | } 338 | 339 | // Update existing source files 340 | try { 341 | auto const base_dir = _cli.get_base_dir(); 342 | 343 | auto const src_dir = base_dir / "src"; 344 | std::vector sources = pf::glob_sources(src_dir); 345 | pf::update_source_files(src_dir / "CMakeLists.txt", sources); 346 | 347 | auto const tests_dir = base_dir / "tests"; 348 | if (fs::exists(tests_dir)) { 349 | std::vector test_sources = pf::glob_sources(tests_dir); 350 | pf::update_source_files(tests_dir / "CMakeLists.txt", test_sources); 351 | } 352 | } catch (const std::system_error& e) { 353 | _cli.console->error("Failed to update project in {}: {}", 354 | _cli.get_base_dir(), 355 | e.what()); 356 | return 1; 357 | } 358 | return 0; 359 | } 360 | }; 361 | 362 | class cmd_query { 363 | private: 364 | cli_common& _cli; 365 | args::Command _cmd{_cli.cmd_group, "query", "Query the project"}; 366 | args::HelpFlag _help{_cmd, "help", "Print help for the `query` subcommand", {'h', "help"}}; 367 | args::Positional _id{ 368 | _cmd, 369 | "id", 370 | "Obtain this information. Valid values:\n" 371 | "* project.root", 372 | }; 373 | 374 | public: 375 | explicit cmd_query(cli_common& gl) 376 | : _cli{gl} {} 377 | 378 | explicit operator bool() const { return !!_cmd; } 379 | 380 | int run() { 381 | static std::unordered_map> queries{ 382 | {"project.root", 383 | [](cmd_query const& cmd) { 384 | std::cout << cmd._cli.get_base_dir().string() << '\n'; 385 | return 0; 386 | }}, 387 | }; 388 | 389 | auto const id = _id.Get(); 390 | 391 | auto query = queries.find(id); 392 | if (query == queries.end()) { 393 | _cli.console->error( 394 | "Invalid id `{}`. Valid values:\n" 395 | "* project.root", 396 | id); 397 | return 1; 398 | } 399 | 400 | return query->second(*this); 401 | } 402 | }; 403 | 404 | } // namespace 405 | 406 | int main(int argc, char** argv) { 407 | args::ArgumentParser parser("Pitchfork: It's for your project."); 408 | cli_common args{parser}; 409 | 410 | // Subcommands 411 | cmd_list list{args}; 412 | cmd_new new_{args}; 413 | cmd_update update{args}; 414 | cmd_query query{args}; 415 | 416 | try { 417 | parser.ParseCLI(argc, argv); 418 | } catch (args::Help const&) { 419 | std::cout << parser; 420 | return 0; 421 | } catch (args::Error& e) { 422 | std::cerr << e.what() << '\n' << parser; 423 | return 1; 424 | } 425 | 426 | try { 427 | if (list) { 428 | return list.run(); 429 | } else if (new_) { 430 | return new_.run(); 431 | } else if (update) { 432 | return update.run(); 433 | } else if (query) { 434 | return query.run(); 435 | } else { 436 | assert(false && "No subcommand selected?"); 437 | std::terminate(); 438 | } 439 | } catch (const reached_eof&) { 440 | return 2; 441 | } 442 | } 443 | -------------------------------------------------------------------------------- /src/pf/existing.cpp: -------------------------------------------------------------------------------- 1 | #include "./existing.hpp" -------------------------------------------------------------------------------- /src/pf/existing.hpp: -------------------------------------------------------------------------------- 1 | #ifndef PF_EXISTING_HPP_INCLUDED 2 | #define PF_EXISTING_HPP_INCLUDED 3 | 4 | #include 5 | #include 6 | 7 | #endif // PF_EXISTING_HPP_INCLUDED 8 | -------------------------------------------------------------------------------- /src/pf/existing/detect_base_dir.cpp: -------------------------------------------------------------------------------- 1 | #include "./detect_base_dir.hpp" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | 10 | namespace fs = pf::fs; 11 | 12 | namespace { 13 | std::optional parse_cmakecache_homedir(fs::path const& cmakecache) { 14 | std::string line; 15 | 16 | std::fstream file = pf::open(cmakecache, std::ios::in); 17 | 18 | while (std::getline(file, line)) { 19 | // Equivalent to the non-existent: 20 | // if (line.starts_with("...")) 21 | if (line.rfind("CMAKE_HOME_DIRECTORY:", 0) == 0) { 22 | auto index = line.find('='); 23 | line.erase(line.begin(), line.begin() + index + 1); 24 | return fs::path{line}; 25 | } 26 | } 27 | 28 | return std::nullopt; 29 | } 30 | } // namespace 31 | 32 | std::optional pf::detect_base_dir(fs::path from_dir) { 33 | auto cur_dir = std::find_if(pf::ascending_iterator{from_dir}, 34 | pf::ascending_iterator{}, 35 | [](auto const& dir) { 36 | return fs::exists(dir / "CMakeLists.txt") 37 | || fs::exists(dir / "CMakeCache.txt"); 38 | }); 39 | 40 | if (cur_dir == pf::ascending_iterator{}) { 41 | return std::nullopt; 42 | } 43 | 44 | if (fs::exists(*cur_dir / "CMakeLists.txt")) { 45 | return *std::find_if(pf::ascending_iterator{*cur_dir}, 46 | pf::ascending_iterator{}, 47 | [](auto const& path) { 48 | return !fs::exists(path.parent_path() / "CMakeLists.txt"); 49 | }); 50 | } 51 | 52 | // There's a CMakeCache 53 | return ::parse_cmakecache_homedir(*cur_dir / "CMakeCache.txt"); 54 | } 55 | -------------------------------------------------------------------------------- /src/pf/existing/detect_base_dir.hpp: -------------------------------------------------------------------------------- 1 | #ifndef PF_EXISTING_DETECT_BASE_DIR_HPP_INCLUDED 2 | #define PF_EXISTING_DETECT_BASE_DIR_HPP_INCLUDED 3 | 4 | #include 5 | 6 | #include 7 | 8 | namespace pf { 9 | 10 | std::optional detect_base_dir(fs::path from_dir = fs::current_path()); 11 | 12 | } // namespace pf 13 | 14 | #endif // PF_EXISTING_DETECT_BASE_DIR_HPP_INCLUDED 15 | -------------------------------------------------------------------------------- /src/pf/existing/update_source_files.cpp: -------------------------------------------------------------------------------- 1 | #include "./update_source_files.hpp" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include 12 | #include 13 | 14 | namespace fs = pf::fs; 15 | 16 | namespace { 17 | 18 | std::vector relative_source_strings(std::vector const& source_files, 19 | fs::path const& base_dir) { 20 | std::vector source_strings; 21 | source_strings.reserve(source_files.size()); 22 | 23 | std::transform(source_files.begin(), 24 | source_files.end(), 25 | std::back_inserter(source_strings), 26 | [&base_dir](auto const& path) { 27 | auto result = fs::relative(path, base_dir).string(); 28 | // TODO: evaluate if replacing `\` in filenames might cause a problem 29 | std::replace(result.begin(), result.end(), '\\', '/'); 30 | 31 | return result; 32 | }); 33 | 34 | return source_strings; 35 | } 36 | 37 | constexpr std::string_view SourcesComment = "# sources\n"; 38 | 39 | auto find_next_function(std::string::iterator begin, std::string::iterator end) { 40 | // BUG: fails for ( in a comment. 41 | // TODO: use a proper CMake parser, or otherwise improve this 42 | return std::find(begin, end, '('); 43 | } 44 | 45 | auto find_end_function(std::string::iterator begin, std::string::iterator end) { 46 | return std::find(begin, end, ')'); 47 | } 48 | 49 | // Assumes we are in the root location of a CMakeLists.txt 50 | auto find_insertion_indicator_comment(std::string::iterator begin_fn, 51 | std::string::iterator end_fn) { 52 | auto const insertion_comment 53 | = std::search(begin_fn, end_fn, SourcesComment.begin(), SourcesComment.end()); 54 | 55 | return insertion_comment; 56 | } 57 | 58 | auto get_indent_at_indicator_comment(std::string::iterator insertion_comment, 59 | std::string::iterator begin_fn, 60 | [[maybe_unused]] std::string::iterator end_fn) { 61 | auto const begin_of_line = std::find_if(std::make_reverse_iterator(insertion_comment), 62 | std::make_reverse_iterator(begin_fn), 63 | [](char c) { return c == '\n' || !std::isspace(c); }) 64 | .base(); 65 | 66 | auto indent_begin = begin_of_line; 67 | auto indent_end = insertion_comment; 68 | 69 | return std::string{indent_begin, indent_end}; 70 | } 71 | 72 | auto find_insertion_location(std::string::iterator insertion_comment, 73 | [[maybe_unused]] std::string::iterator begin_fn, 74 | [[maybe_unused]] std::string::iterator end_fn) { 75 | return std::next(insertion_comment, SourcesComment.size()); 76 | } 77 | 78 | auto erase_existing_sources(std::string& cmakelists, 79 | std::string::iterator insertion_point, 80 | std::string::iterator begin_fn, 81 | std::string::iterator end_fn) { 82 | // We don't delete until the end of the ')', but until the last source-list character, 83 | // meaning: 84 | // ... 85 | // # sources 86 | // something 87 | // ) 88 | // ... Gets turned into: 89 | // # sources 90 | // ) 91 | auto last_source_list_char = std::find_if_not(std::make_reverse_iterator(end_fn), 92 | std::make_reverse_iterator(begin_fn), 93 | [](char c) { return std::isblank(c); }); 94 | if (*last_source_list_char == '\n') { 95 | ++last_source_list_char; 96 | } 97 | 98 | if (last_source_list_char.base() <= insertion_point) { 99 | // If the source list is empty, we need to ensure there's a newline separating it 100 | // from the rest 101 | return cmakelists.insert(insertion_point, '\n'); 102 | } 103 | return cmakelists.erase(insertion_point, last_source_list_char.base()); 104 | } 105 | 106 | auto insert_sources(std::string& cmakelists, 107 | std::string::iterator insertion_point, 108 | std::vector const& sources, 109 | std::string const& indent) { 110 | bool first_iteration = true; 111 | 112 | for (auto const& source : sources) { 113 | if (!first_iteration) { 114 | insertion_point = std::next(cmakelists.insert(insertion_point, '\n')); 115 | } else { 116 | first_iteration = false; 117 | } 118 | 119 | insertion_point 120 | = std::next(cmakelists.insert(insertion_point, indent.begin(), indent.end()), 121 | indent.size()); 122 | insertion_point 123 | = std::next(cmakelists.insert(insertion_point, source.begin(), source.end()), 124 | source.size()); 125 | } 126 | 127 | return insertion_point; 128 | } 129 | 130 | std::pair 131 | write_sources(std::string& cmakelists, 132 | std::vector const& source_strings, 133 | std::string::iterator insertion_comment, 134 | std::string::iterator begin_fn, 135 | std::string::iterator end_fn) { 136 | std::string const indent 137 | = ::get_indent_at_indicator_comment(insertion_comment, begin_fn, end_fn); 138 | auto insertion_point = ::find_insertion_location(insertion_comment, begin_fn, end_fn); 139 | 140 | // Note: invalidates other iterators 141 | insertion_point = ::erase_existing_sources(cmakelists, insertion_point, begin_fn, end_fn); 142 | 143 | auto const end_insertion 144 | = ::insert_sources(cmakelists, insertion_point, source_strings, indent); 145 | return std::pair{end_insertion, cmakelists.end()}; 146 | } 147 | 148 | } // namespace 149 | 150 | void pf::update_source_files(fs::path const& cmakelists_file, 151 | std::vector const& source_files) { 152 | if (!fs::exists(cmakelists_file)) { 153 | throw std::system_error{ 154 | std::make_error_code(std::errc::no_such_file_or_directory), 155 | cmakelists_file.string() + " does not exist", 156 | }; 157 | } 158 | 159 | std::string cmakelists = pf::slurp_file(cmakelists_file); 160 | 161 | std::vector const source_strings 162 | = ::relative_source_strings(source_files, cmakelists_file.parent_path()); 163 | 164 | std::string const cmakelists_cpy = cmakelists; 165 | 166 | auto begin = cmakelists.begin(); 167 | auto end = cmakelists.end(); 168 | 169 | while (begin != end) { 170 | auto const begin_fn = ::find_next_function(begin, end); 171 | auto const end_fn = ::find_end_function(begin_fn, end); 172 | auto const insertion_comment = ::find_insertion_indicator_comment(begin_fn, end_fn); 173 | 174 | if (insertion_comment == end_fn) { 175 | begin = end_fn; 176 | continue; 177 | } 178 | 179 | std::tie(begin, end) 180 | = ::write_sources(cmakelists, source_strings, insertion_comment, begin_fn, end_fn); 181 | } 182 | 183 | if (cmakelists != cmakelists_cpy) { 184 | std::fstream cmakelists_out = pf::open(cmakelists_file, std::ios::trunc | std::ios::out); 185 | cmakelists_out << cmakelists; 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /src/pf/existing/update_source_files.hpp: -------------------------------------------------------------------------------- 1 | #ifndef PF_EXISTING_UPDATE_SOURCE_FILES_HPP_INCLUDED 2 | #define PF_EXISTING_UPDATE_SOURCE_FILES_HPP_INCLUDED 3 | 4 | #include 5 | 6 | #include 7 | 8 | namespace pf { 9 | 10 | void update_source_files(fs::path const& cmakelists_file, std::vector const& sources); 11 | 12 | } // namespace pf 13 | 14 | #endif // PF_EXISTING_UPDATE_SOURCE_FILES_HPP_INCLUDED 15 | -------------------------------------------------------------------------------- /src/pf/file_template.cpp: -------------------------------------------------------------------------------- 1 | #include "./file_template.hpp" 2 | 3 | #include 4 | 5 | std::string pf::template_renderer::render(const std::string& inpath) const { 6 | auto res = _fs.open(inpath); 7 | auto mustache = kainjow::mustache::mustache{{res.begin(), res.end()}}; 8 | if (!mustache.is_valid()) { 9 | throw std::runtime_error( 10 | fmt::format("Error loading template file: {}: {}", inpath, mustache.error_message())); 11 | } 12 | return mustache.render(_template_data); 13 | } 14 | -------------------------------------------------------------------------------- /src/pf/file_template.hpp: -------------------------------------------------------------------------------- 1 | #ifndef FILE_TEMPLATE_HPP_INCLUDED 2 | #define FILE_TEMPLATE_HPP_INCLUDED 3 | 4 | #include 5 | 6 | #include 7 | #include 8 | 9 | #include 10 | 11 | namespace pf { 12 | 13 | class template_renderer { 14 | fs::path _base_dir; 15 | kainjow::mustache::data _template_data; 16 | cmrc::embedded_filesystem _fs; 17 | 18 | public: 19 | template_renderer(fs::path dir, cmrc::embedded_filesystem fs) 20 | : _base_dir(dir) 21 | , _fs(fs) {} 22 | 23 | template 24 | void set(Key&& k, What&& w) { 25 | _template_data.set(k, std::forward(w)); 26 | } 27 | 28 | std::string render(const std::string& inpath) const; 29 | void render_to_file(const std::string& respath, const fs::path& outpath) const { 30 | auto content = render(respath); 31 | pf::write_file(_base_dir / outpath, content); 32 | } 33 | }; 34 | 35 | } // namespace pf 36 | 37 | #endif // FILE_TEMPLATE_HPP_INCLUDED -------------------------------------------------------------------------------- /src/pf/fs.cpp: -------------------------------------------------------------------------------- 1 | #include "./fs.hpp" 2 | -------------------------------------------------------------------------------- /src/pf/fs.hpp: -------------------------------------------------------------------------------- 1 | #ifndef PF_FS_HPP_INCLUDED 2 | #define PF_FS_HPP_INCLUDED 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #endif // PF_FS_HPP_INCLUDED 9 | -------------------------------------------------------------------------------- /src/pf/fs/ascending_iterator.cpp: -------------------------------------------------------------------------------- 1 | #include "./ascending_iterator.hpp" 2 | -------------------------------------------------------------------------------- /src/pf/fs/ascending_iterator.hpp: -------------------------------------------------------------------------------- 1 | #ifndef PF_FS_ASCENDING_ITERATOR_HPP_INCLUDED 2 | #define PF_FS_ASCENDING_ITERATOR_HPP_INCLUDED 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | namespace pf { 10 | 11 | class ascending_iterator { 12 | public: 13 | using difference_type = std::ptrdiff_t; 14 | using value_type = fs::path; 15 | using pointer = value_type const*; 16 | using reference = value_type const&; 17 | using iterator_category = std::input_iterator_tag; 18 | 19 | ascending_iterator() = default; 20 | 21 | // Canonicalizing the path makes the `==` comparison valid 22 | explicit ascending_iterator(fs::path const& p) 23 | : current_{fs::weakly_canonical(p)} {} 24 | 25 | ascending_iterator& operator++() { 26 | current_ = current_.parent_path(); 27 | return *this; 28 | } 29 | 30 | ascending_iterator operator++(int) { 31 | auto cpy = *this; 32 | ++*this; 33 | return cpy; 34 | } 35 | 36 | reference operator*() const { return current_; } 37 | pointer operator->() const { return ¤t_; } 38 | 39 | friend bool operator==(ascending_iterator const& lhs, ascending_iterator const& rhs) { 40 | return lhs.current_ == rhs.current_; 41 | } 42 | 43 | friend bool operator!=(ascending_iterator const& lhs, ascending_iterator const& rhs) { 44 | return !(lhs == rhs); 45 | } 46 | 47 | private: 48 | fs::path current_ = {"/"}; 49 | }; 50 | 51 | } // namespace pf 52 | 53 | #endif // PF_FS_ASCENDING_ITERATOR_HPP_INCLUDED 54 | -------------------------------------------------------------------------------- /src/pf/fs/core.cpp: -------------------------------------------------------------------------------- 1 | #include "./core.hpp" 2 | 3 | #include 4 | 5 | namespace fs = pf::fs; 6 | 7 | std::fstream pf::open(const fs::path& filepath, std::ios::openmode mode, std::error_code& ec) { 8 | std::fstream ret; 9 | auto mask = ret.exceptions() | std::ios::failbit; 10 | ret.exceptions(mask); 11 | 12 | try { 13 | ret.open(filepath.string(), mode); 14 | } catch (const std::ios::failure& e) { 15 | ec = e.code(); 16 | } 17 | return ret; 18 | } 19 | 20 | std::string pf::slurp_file(const fs::path& path, std::error_code& ec) { 21 | auto file = pf::open(path, std::ios::in, ec); 22 | if (ec) { 23 | return std::string{}; 24 | } 25 | 26 | std::ostringstream out; 27 | out << file.rdbuf(); 28 | return std::move(out).str(); 29 | } 30 | -------------------------------------------------------------------------------- /src/pf/fs/core.hpp: -------------------------------------------------------------------------------- 1 | #ifndef PF_FS_CORE_HPP_INCLUDED 2 | #define PF_FS_CORE_HPP_INCLUDED 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | namespace pf { 10 | 11 | namespace fs = std::filesystem; 12 | 13 | /** 14 | * Open the given filepath with the given openmode. Fills out `ec` with an error code in case of 15 | * file open failure. 16 | */ 17 | std::fstream open(const fs::path&, std::ios::openmode, std::error_code& ec); 18 | /** 19 | * Open the given filepath, but throw std::system_error in case of failure to open 20 | */ 21 | inline std::fstream open(const fs::path& filepath, std::ios::openmode mode) { 22 | std::error_code ec; 23 | auto ret = open(filepath, mode, ec); 24 | if (ec) { 25 | throw std::system_error{ec, "Open file: " + filepath.string()}; 26 | } 27 | return ret; 28 | } 29 | 30 | /** 31 | * Write the contents of a file, creating parents directories if necessary. 32 | */ 33 | template 34 | void write_file(const fs::path& path, What&& w, std::error_code& ec) { 35 | fs::create_directories(path.parent_path(), ec); 36 | if (ec) { 37 | return; 38 | } 39 | auto strm = open(path, std::ios::out | std::ios::binary, ec); 40 | if (ec) { 41 | return; 42 | } 43 | strm << std::forward(w); 44 | ec = {}; 45 | } 46 | 47 | template 48 | void write_file(const fs::path& path, What&& w) { 49 | std::error_code ec; 50 | write_file(path, std::forward(w), ec); 51 | if (ec) { 52 | throw std::system_error{ec, "Failed to write file: " + path.string()}; 53 | } 54 | } 55 | 56 | /** 57 | * Slurp the entire contents of a file into a std::string. 58 | */ 59 | std::string slurp_file(const fs::path& path, std::error_code& ec); 60 | 61 | inline std::string slurp_file(const fs::path& path) { 62 | std::error_code ec; 63 | auto contents = pf::slurp_file(path, ec); 64 | if (ec) { 65 | throw std::system_error{ec, "Reading file: " + path.string()}; 66 | } 67 | return contents; 68 | } 69 | } // namespace pf 70 | 71 | #endif // PF_FS_CORE_HPP_INCLUDED 72 | -------------------------------------------------------------------------------- /src/pf/fs/glob.cpp: -------------------------------------------------------------------------------- 1 | #include "./glob.hpp" 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | namespace fs = pf::fs; 8 | 9 | std::vector pf::glob_sources(fs::path const& relative_to) { 10 | std::vector sources; 11 | 12 | struct path_hash { 13 | auto operator()(fs::path const& path) const { 14 | return fs::hash_value(path); // 15 | } 16 | }; 17 | std::unordered_set const SourceFileExtensions{ 18 | fs::path{".c"}, 19 | fs::path{".cc"}, 20 | fs::path{".cpp"}, 21 | fs::path{".cxx"}, 22 | fs::path{".c++"}, 23 | fs::path{".h"}, 24 | fs::path{".hh"}, 25 | fs::path{".hpp"}, 26 | fs::path{".hxx"}, 27 | fs::path{".h++"}, 28 | }; 29 | 30 | std::for_each(fs::directory_iterator{relative_to}, 31 | fs::directory_iterator{}, 32 | [&](fs::directory_entry const& top_level_entry) { 33 | if (top_level_entry.is_directory()) { 34 | std::copy_if(fs::recursive_directory_iterator{top_level_entry.path()}, 35 | fs::recursive_directory_iterator{}, 36 | std::back_inserter(sources), 37 | [&](fs::directory_entry const& entry) { 38 | return SourceFileExtensions.count( 39 | entry.path().extension()); 40 | }); 41 | } 42 | }); 43 | 44 | std::sort(sources.begin(), sources.end()); 45 | 46 | return sources; 47 | } 48 | -------------------------------------------------------------------------------- /src/pf/fs/glob.hpp: -------------------------------------------------------------------------------- 1 | #ifndef PF_FS_GLOB_HPP_INCLUDED 2 | #define PF_FS_GLOB_HPP_INCLUDED 3 | 4 | #include 5 | 6 | namespace pf { 7 | std::vector glob_sources(fs::path const& relative_to); 8 | } 9 | 10 | #endif // PF_FS_GLOB_HPP_INCLUDED 11 | -------------------------------------------------------------------------------- /src/pf/new.cpp: -------------------------------------------------------------------------------- 1 | #include "./new.hpp" -------------------------------------------------------------------------------- /src/pf/new.hpp: -------------------------------------------------------------------------------- 1 | #ifndef PF_NEW_HPP_INCLUDED 2 | #define PF_NEW_HPP_INCLUDED 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #endif // PF_NEW_HPP_INCLUDED -------------------------------------------------------------------------------- /src/pf/new/cmake.cpp: -------------------------------------------------------------------------------- 1 | #include "./cmake.hpp" 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | 8 | #include 9 | 10 | #include 11 | 12 | CMRC_DECLARE(pf_templates); 13 | 14 | void pf::create_cmake_files(const pf::new_project_params& params) { 15 | pf::template_renderer trr{params.directory, cmrc::pf_templates::get_filesystem()}; 16 | // Fill out the template data: 17 | const auto alias_target = params.root_namespace + "::" + params.name; 18 | trr.set("alias_target", alias_target); 19 | trr.set("root_ns", params.root_namespace); 20 | trr.set("project_name", params.name); 21 | trr.set("ns_path", path_for_namespace(params.root_namespace).string()); 22 | trr.set("gen_extras", params.create_extras); 23 | trr.set("gen_examples", params.create_examples); 24 | trr.set("gen_third_party", params.create_third_party); 25 | trr.set("gen_tests", params.create_tests); 26 | trr.set("separate_headers", params.separate_headers); 27 | trr.set("first_stem", params.first_file_stem); 28 | 29 | trr.render_to_file("cmake/src_cml.in.cmake", "src/CMakeLists.txt"); 30 | trr.render_to_file("cmake/root_cml.in.cmake", "CMakeLists.txt"); 31 | 32 | // Optional content 33 | if (params.create_examples) { 34 | trr.render_to_file("cmake/examples_cml.in.cmake", "examples/CMakeLists.txt"); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/pf/new/cmake.hpp: -------------------------------------------------------------------------------- 1 | #ifndef PF_NEW_CMAKE_HPP_INCLUDED 2 | #define PF_NEW_CMAKE_HPP_INCLUDED 3 | 4 | #include 5 | #include 6 | 7 | namespace pf { 8 | 9 | void create_cmake_files(const new_project_params& params); 10 | 11 | } // namespace pf 12 | 13 | #endif // PF_NEW_CMAKE_HPP_INCLUDED -------------------------------------------------------------------------------- /src/pf/new/dirs.cpp: -------------------------------------------------------------------------------- 1 | #include "./dirs.hpp" 2 | 3 | #include 4 | 5 | void pf::create_directories(const pf::new_project_params& params) { 6 | // Create the root directory 7 | fs::create_directories(params.directory); 8 | // Required subdirectories: 9 | fs::create_directories(params.directory / "src"); 10 | fs::create_directories(params.directory / "docs"); 11 | // Conditional subdirs: 12 | if (params.separate_headers) { 13 | fs::create_directories(params.directory / "include"); 14 | } 15 | if (params.create_third_party) { 16 | fs::create_directories(params.directory / "third_party"); 17 | } 18 | if (params.create_examples) { 19 | fs::create_directories(params.directory / "examples"); 20 | } 21 | if (params.create_extras) { 22 | fs::create_directories(params.directory / "extras"); 23 | } 24 | if (params.create_tests) { 25 | fs::create_directories(params.directory / "tests"); 26 | } 27 | // Build system 28 | if (params.build_system == pf::build_system::cmake) { 29 | fs::create_directories(params.directory / "cmake"); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/pf/new/dirs.hpp: -------------------------------------------------------------------------------- 1 | #ifndef PF_NEW_DIRS_HPP_INCLUDED 2 | #define PF_NEW_DIRS_HPP_INCLUDED 3 | 4 | #include 5 | 6 | namespace pf { 7 | 8 | void create_directories(const new_project_params& params); 9 | 10 | } // namespace pf 11 | 12 | #endif // PF_NEW_DIRS_HPP_INCLUDED -------------------------------------------------------------------------------- /src/pf/new/files.cpp: -------------------------------------------------------------------------------- 1 | #include "./files.hpp" 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | #include 10 | #include 11 | 12 | #include 13 | 14 | CMRC_DECLARE(pf_templates); 15 | 16 | void pf::create_files(const pf::new_project_params& params) { 17 | pf::template_renderer trr{params.directory, cmrc::pf_templates::get_filesystem()}; 18 | // The first file path will be based on the namespace root namespace 19 | auto ns_path = path_for_namespace(params.root_namespace); 20 | // The first file paths: 21 | auto first_src = "src" / ns_path / (params.first_file_stem + ".cpp"); 22 | auto first_header = (params.separate_headers ? "include" : "src") / ns_path 23 | / (params.first_file_stem + ".hpp"); 24 | 25 | // Prepare the include guard string by replacing non-ident elements with '_' 26 | auto guard = (ns_path.string() + "_" + params.first_file_stem + "_HPP_INCLUDED"); 27 | boost::replace_all(guard, "/", "_"); 28 | boost::replace_all(guard, "-", "_"); 29 | boost::replace_all(guard, ".", "_"); 30 | boost::to_upper(guard); 31 | 32 | // Set up the template render context 33 | trr.set("root_ns", params.root_namespace); 34 | trr.set("first_stem", params.first_file_stem); 35 | trr.set("ns_path", ns_path.string()); 36 | trr.set("guard_def", guard); 37 | 38 | // Base files 39 | trr.render_to_file("base/first_source.in.cpp", first_src); 40 | trr.render_to_file("base/first_header.in.hpp", first_header); 41 | 42 | // Optional files 43 | if (params.create_examples) { 44 | trr.render_to_file("base/first_example.in.cpp", "examples/example1.cpp"); 45 | } 46 | 47 | if (params.create_tests) { 48 | trr.render_to_file("base/first_test.in.cpp", "tests/my_test.cpp"); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/pf/new/files.hpp: -------------------------------------------------------------------------------- 1 | #ifndef PF_NEW_FILES_HPP_INCLUDED 2 | #define PF_NEW_FILES_HPP_INCLUDED 3 | 4 | #include 5 | 6 | namespace pf { 7 | 8 | void create_files(const pf::new_project_params& params); 9 | 10 | } // namespace pf 11 | 12 | #endif // PF_NEW_FILES_HPP_INCLUDED -------------------------------------------------------------------------------- /src/pf/new/params.hpp: -------------------------------------------------------------------------------- 1 | #ifndef PF_NEW_PARAMS_HPP_INCLUDED 2 | #define PF_NEW_PARAMS_HPP_INCLUDED 3 | 4 | #include 5 | 6 | namespace pf { 7 | 8 | enum class build_system { 9 | unspecified, // Used by the CLI when nothing has been specified 10 | none, 11 | cmake, 12 | }; 13 | 14 | struct new_project_params { 15 | std::string name; 16 | std::string root_namespace; 17 | std::string first_file_stem; 18 | fs::path directory; 19 | bool separate_headers = false; 20 | bool create_third_party = true; 21 | bool create_examples = true; 22 | bool create_extras = false; 23 | bool create_tests = true; 24 | enum build_system build_system = build_system::none; 25 | 26 | new_project_params(std::string name_, 27 | std::string root_ns, 28 | std::string first_file_stem_, 29 | fs::path directory_) 30 | : name(std::move(name_)) 31 | , root_namespace(std::move(root_ns)) 32 | , first_file_stem(std::move(first_file_stem_)) 33 | , directory(directory_) {} 34 | }; 35 | 36 | } // namespace pf 37 | 38 | #endif // PF_NEW_PARAMS_HPP_INCLUDED -------------------------------------------------------------------------------- /src/pf/new/project.cpp: -------------------------------------------------------------------------------- 1 | #include "./project.hpp" 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | #include 10 | 11 | pf::fs::path pf::path_for_namespace(const std::string& ns) { 12 | return boost::replace_all_copy(ns, "::", "/"); 13 | } 14 | 15 | std::string pf::namespace_for_name(const std::string& name) { 16 | auto ns = name; 17 | boost::replace_all(ns, "-", "::"); 18 | boost::replace_all(ns, ".", "::"); 19 | boost::replace_all(ns, "/", "::"); 20 | return ns; 21 | } 22 | 23 | void pf::create_project(const pf::new_project_params& params) { 24 | assert(!params.name.empty() && "No name for project"); 25 | assert(!params.root_namespace.empty() && "No namespace for project!"); 26 | pf::create_directories(params); 27 | pf::create_files(params); 28 | if (params.build_system == pf::build_system::cmake) { 29 | pf::create_cmake_files(params); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/pf/new/project.hpp: -------------------------------------------------------------------------------- 1 | #ifndef PF_NEW_PROJECT_HPP_INCLUDED 2 | #define PF_NEW_PROJECT_HPP_INCLUDED 3 | 4 | #include 5 | #include 6 | 7 | namespace pf { 8 | 9 | fs::path path_for_namespace(const std::string& ns); 10 | void create_project(const new_project_params& params); 11 | std::string namespace_for_name(const std::string& name); 12 | 13 | } // namespace pf 14 | 15 | #endif // PF_NEW_PROJECT_HPP_INCLUDED -------------------------------------------------------------------------------- /src/pf/pitchfork.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "./pitchfork.hpp" 4 | #include "./fs.hpp" 5 | 6 | int pf::pitchfork() { 7 | return 42; 8 | } 9 | -------------------------------------------------------------------------------- /src/pf/pitchfork.hpp: -------------------------------------------------------------------------------- 1 | #ifndef PF_HPP_INCLUDED 2 | #define PF_HPP_INCLUDED 3 | 4 | namespace pf { 5 | 6 | // Calculate the answer 7 | int pitchfork(); 8 | 9 | } // pf 10 | 11 | #endif // PF_HPP_INCLUDED 12 | -------------------------------------------------------------------------------- /tests/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | find_package(Catch2 2.3.0 REQUIRED) 2 | include(Catch) 3 | 4 | set(PF_TEST_BINDIR "${CMAKE_CURRENT_BINARY_DIR}") 5 | set(PF_TEST_SRCDIR "${CMAKE_CURRENT_SOURCE_DIR}") 6 | 7 | add_library(pf-test-lib STATIC test-main.cpp compare_fs.hpp compare_fs.cpp) 8 | target_link_libraries(pf-test-lib PUBLIC Catch2::Catch2 pf::pitchfork) 9 | target_compile_definitions(pf-test-lib 10 | PUBLIC 11 | "PF_TEST_BINDIR=\"${CMAKE_CURRENT_BINARY_DIR}\"" 12 | "PF_TEST_SRCDIR=\"${CMAKE_CURRENT_SOURCE_DIR}\"" 13 | ) 14 | 15 | function(configure_directory directory) 16 | file(GLOB_RECURSE directory_contents CONFIGURE_DEPENDS "${directory}/*") 17 | set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${directory_contents}) 18 | 19 | file(COPY "${directory}/" DESTINATION "${CMAKE_CURRENT_BINARY_DIR}/${directory}/") 20 | file(GLOB_RECURSE directory_templates RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}" "${directory}/*.in") 21 | 22 | foreach(file ${directory_templates}) 23 | string(LENGTH "${file}" len) 24 | math(EXPR without_dot_in "${len} - 3") 25 | string(SUBSTRING "${file}" 0 ${without_dot_in} template_out) 26 | configure_file("${file}" "${CMAKE_CURRENT_BINARY_DIR}/${template_out}") 27 | endforeach() 28 | endfunction() 29 | 30 | function(pf_add_test_exe name) 31 | add_executable(${ARGV}) 32 | target_link_libraries(${name} PRIVATE pf-test-lib) 33 | catch_discover_tests(${name} 34 | TEST_PREFIX ${name}:: 35 | EXTRA_ARGS $<$:--use-colour=yes>) 36 | endfunction() 37 | 38 | function(pf_add_query_test ID) 39 | set(options) 40 | set(list_args PASS_REGULAR_EXPRESSION FAIL_REGULAR_EXPRESSION) 41 | 42 | cmake_parse_arguments(PARSE_ARGV 1 ARG "${options}" "${args}" "${list_args}") 43 | 44 | add_test( 45 | NAME "query:${ID}" 46 | COMMAND pf query "${ID}" 47 | ) 48 | set_tests_properties("query:${ID}" 49 | PROPERTIES 50 | PASS_REGULAR_EXPRESSION "${ARG_PASS_REGULAR_EXPRESSION}" 51 | FAIL_REGULAR_EXPRESSION "${ARG_FAIL_REGULAR_EXPRESSION}" 52 | ) 53 | endfunction() 54 | 55 | pf_add_test_exe(generate generate.cpp) 56 | 57 | pf_add_test_exe(existing 58 | existing/detect_base_dir.cpp 59 | existing/update_source_files.cpp) 60 | configure_directory(existing/sample) 61 | 62 | pf_add_query_test(project.root 63 | PASS_REGULAR_EXPRESSION "${PROJECT_SOURCE_DIR}" 64 | ) 65 | -------------------------------------------------------------------------------- /tests/compare_fs.cpp: -------------------------------------------------------------------------------- 1 | #include "./compare_fs.hpp" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | using namespace pf; 10 | using namespace pf::test; 11 | 12 | namespace { 13 | 14 | // Filename to ignore in diffs. Allows us to have "empty" directories in git 15 | constexpr std::string_view IgnoreDiff = "ignore_in_diff"; 16 | 17 | using path_set = std::set; 18 | using dir_iter = pf::fs::directory_iterator; 19 | 20 | path_set children(fs::path basis, fs::path path) { 21 | path_set ret; 22 | std::error_code ec; 23 | auto iter = fs::directory_iterator{path, ec}; 24 | if (ec) { 25 | if (ec == std::errc::no_such_file_or_directory) { 26 | return ret; 27 | } 28 | throw std::system_error{ec, "Cannot get children of non-directory file: " + path.string()}; 29 | } 30 | for (fs::path child : iter) { 31 | if (child.stem().string() == IgnoreDiff) { 32 | continue; 33 | } 34 | ret.insert(fs::relative(child, basis)); 35 | } 36 | return ret; 37 | } 38 | 39 | template 40 | decltype(auto) difference(Container&& lhs, Container&& rhs) { 41 | std::decay_t ret; 42 | std::set_difference(lhs.begin(), 43 | lhs.end(), 44 | rhs.begin(), 45 | rhs.end(), 46 | std::inserter(ret, ret.begin())); 47 | return ret; 48 | } 49 | 50 | void acc_differences(fs_diff& out, 51 | const fs::path& in_root, 52 | const fs::path& exp_root, 53 | const fs::path child) { 54 | auto in_files = children(in_root, in_root / child); 55 | auto exp_files = children(exp_root, exp_root / child); 56 | auto unexpected = difference(in_files, exp_files); 57 | auto missing = difference(exp_files, in_files); 58 | out.unexpected_files.insert(unexpected.begin(), unexpected.end()); 59 | out.missing_files.insert(missing.begin(), missing.end()); 60 | 61 | path_set both_children; 62 | std::set_intersection(in_files.begin(), 63 | in_files.end(), 64 | exp_files.begin(), 65 | exp_files.end(), 66 | std::inserter(both_children, both_children.begin())); 67 | for (auto child : both_children) { 68 | auto in_stat = fs::status(in_root / child); 69 | auto exp_stat = fs::status(exp_root / child); 70 | if (in_stat.type() != exp_stat.type()) { 71 | out.different_files.insert(child); 72 | } else if (fs::is_regular_file(in_stat)) { 73 | assert(fs::is_regular_file(exp_stat)); 74 | std::ifstream in_strm{in_root / child, std::ios::binary}; 75 | std::ifstream exp_strm{exp_root / child, std::ios::binary}; 76 | using char_iter = std::istreambuf_iterator; 77 | bool are_equal 78 | = std::equal(char_iter{in_strm}, char_iter{}, char_iter{exp_strm}, char_iter{}); 79 | if (!are_equal) { 80 | out.different_files.insert(child); 81 | } 82 | } 83 | } 84 | 85 | path_set all_children = in_files; 86 | all_children.insert(exp_files.begin(), exp_files.end()); 87 | for (auto& child : all_children) { 88 | if (fs::is_directory(in_root / child) || fs::is_directory(exp_root / child)) { 89 | acc_differences(out, in_root, exp_root, child); 90 | } 91 | } 92 | } 93 | 94 | } // namespace 95 | 96 | pf::test::fs_diff pf::test::compare_fs_tree(pf::fs::path input, pf::fs::path expected) { 97 | fs_diff ret; 98 | acc_differences(ret, input, expected, ""); 99 | return ret; 100 | } 101 | -------------------------------------------------------------------------------- /tests/compare_fs.hpp: -------------------------------------------------------------------------------- 1 | #ifndef COMPARE_FS_HPP_INCLUDED 2 | #define COMPARE_FS_HPP_INCLUDED 3 | 4 | #include 5 | 6 | #include 7 | #include 8 | 9 | namespace pf::test { 10 | 11 | class fs_diff { 12 | public: 13 | std::set unexpected_files; 14 | std::set missing_files; 15 | std::set different_files; 16 | explicit operator bool() const noexcept { 17 | return !unexpected_files.empty() || !missing_files.empty() || !different_files.empty(); 18 | } 19 | }; 20 | 21 | fs_diff compare_fs_tree(fs::path input, fs::path expected); 22 | 23 | inline std::ostream& operator<<(std::ostream& o, const fs_diff& diff) { 24 | if (!diff.unexpected_files.empty()) { 25 | o << "Unexpected files in output directory:\n"; 26 | for (auto& child : diff.unexpected_files) { 27 | o << " + " << child.string() << '\n'; 28 | } 29 | } 30 | if (!diff.missing_files.empty()) { 31 | o << "Files missing from the output directory:\n"; 32 | for (auto& child : diff.missing_files) { 33 | o << " - " << child.string() << '\n'; 34 | } 35 | } 36 | if (!diff.different_files.empty()) { 37 | o << "Files that are not equivalent:\n"; 38 | for (auto& child : diff.different_files) { 39 | o << " != " << child.string() << '\n'; 40 | } 41 | } 42 | return o; 43 | } 44 | 45 | } // namespace pf::test 46 | 47 | #endif // COMPARE_FS_HPP_INCLUDED -------------------------------------------------------------------------------- /tests/existing/detect_base_dir.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | 5 | namespace fs = pf::fs; 6 | 7 | TEST_CASE("detect project root") { 8 | auto const cases = { 9 | fs::path{"existing/sample/project"}, 10 | fs::path{"existing/sample/project/data"}, 11 | fs::path{"existing/sample/project/build"}, 12 | fs::path{"existing/sample/parallel_build"}, 13 | 14 | // This is a harder case, since it actually does contain a CMakeLists.txt 15 | fs::path{"existing/sample/project/src"}, 16 | }; 17 | 18 | fs::path const expected = fs::path{PF_TEST_BINDIR} / fs::path{"existing/sample/project"}; 19 | 20 | for (auto const& working_dir : cases) { 21 | DYNAMIC_SECTION(working_dir) { 22 | CHECK(pf::detect_base_dir(fs::path{PF_TEST_BINDIR} / working_dir) == expected); 23 | } 24 | } 25 | } 26 | 27 | TEST_CASE("no project root") { 28 | auto path = fs::current_path().root_directory(); 29 | INFO("Base directory:"); 30 | INFO(path); 31 | auto basedir = pf::detect_base_dir(path); 32 | CHECK(basedir == std::nullopt); 33 | } 34 | -------------------------------------------------------------------------------- /tests/existing/sample/parallel_build/CMakeCache.txt.in: -------------------------------------------------------------------------------- 1 | //Source directory with the top level CMakeLists.txt file for this 2 | // project 3 | CMAKE_HOME_DIRECTORY:INTERNAL=${PF_TEST_BINDIR}/existing/sample/project 4 | -------------------------------------------------------------------------------- /tests/existing/sample/project/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | project(my_project) 2 | 3 | add_subdirectory(src) 4 | -------------------------------------------------------------------------------- /tests/existing/sample/project/build/CMakeCache.txt.in: -------------------------------------------------------------------------------- 1 | //Source directory with the top level CMakeLists.txt file for this 2 | // project 3 | CMAKE_HOME_DIRECTORY:INTERNAL=${PF_TEST_BINDIR}/existing/sample/project 4 | -------------------------------------------------------------------------------- /tests/existing/sample/project/data/some_file.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vector-of-bool/pitchfork/1f1d16549408dc29dae03fc1a479c93556239112/tests/existing/sample/project/data/some_file.txt -------------------------------------------------------------------------------- /tests/existing/sample/project/src/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | set(sources 2 | # sources 3 | project/source1.c 4 | project/source2.cc 5 | project/subfolder/source4.cxx 6 | ) 7 | 8 | set(headers 9 | # headers 10 | project/header1.h 11 | project/subfolder/header4.hxx) 12 | 13 | add_library(my_project_lib 14 | # sources 15 | project/source1.c 16 | project/source2.cc 17 | project/subfolder/source3.cpp 18 | ) 19 | 20 | add_library(my_project_lib2 21 | # sources 22 | project/source1.c 23 | project/header1.h 24 | ) 25 | 26 | target_sources(my_project_lib 27 | PRIVATE 28 | # headers 29 | project/header2.hh) 30 | 31 | add_library(my_weird_interface_lib INTERFACE) 32 | target_sources(my_weird_interface_lib 33 | INTERFACE 34 | # sources 35 | project/subfolder/header3.hpp 36 | project/subfolder/source3.cpp) 37 | 38 | # sources 39 | # headers 40 | -------------------------------------------------------------------------------- /tests/existing/sample/project/src/CMakeLists.txt.after_update: -------------------------------------------------------------------------------- 1 | set(sources 2 | # sources 3 | project/source1.c 4 | project/source2.cc 5 | project/source3.cpp 6 | project/source4.cxx 7 | project/source5.c++ 8 | project/subfolder/source1.c 9 | project/subfolder/source2.cc 10 | project/subfolder/source3.cpp 11 | project/subfolder/source4.cxx 12 | project/subfolder/source5.c++ 13 | ) 14 | 15 | set(headers 16 | # headers 17 | project/header1.h 18 | project/subfolder/header4.hxx) 19 | 20 | add_library(my_project_lib 21 | # sources 22 | project/source1.c 23 | project/source2.cc 24 | project/source3.cpp 25 | project/source4.cxx 26 | project/source5.c++ 27 | project/subfolder/source1.c 28 | project/subfolder/source2.cc 29 | project/subfolder/source3.cpp 30 | project/subfolder/source4.cxx 31 | project/subfolder/source5.c++ 32 | ) 33 | 34 | add_library(my_project_lib2 35 | # sources 36 | project/source1.c 37 | project/source2.cc 38 | project/source3.cpp 39 | project/source4.cxx 40 | project/source5.c++ 41 | project/subfolder/source1.c 42 | project/subfolder/source2.cc 43 | project/subfolder/source3.cpp 44 | project/subfolder/source4.cxx 45 | project/subfolder/source5.c++ 46 | ) 47 | 48 | target_sources(my_project_lib 49 | PRIVATE 50 | # headers 51 | project/header2.hh) 52 | 53 | add_library(my_weird_interface_lib INTERFACE) 54 | target_sources(my_weird_interface_lib 55 | INTERFACE 56 | # sources 57 | project/source1.c 58 | project/source2.cc 59 | project/source3.cpp 60 | project/source4.cxx 61 | project/source5.c++ 62 | project/subfolder/source1.c 63 | project/subfolder/source2.cc 64 | project/subfolder/source3.cpp 65 | project/subfolder/source4.cxx 66 | project/subfolder/source5.c++) 67 | 68 | # sources 69 | # headers 70 | -------------------------------------------------------------------------------- /tests/existing/sample/project/src/project/header1.h: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vector-of-bool/pitchfork/1f1d16549408dc29dae03fc1a479c93556239112/tests/existing/sample/project/src/project/header1.h -------------------------------------------------------------------------------- /tests/existing/sample/project/src/project/header2.hh: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vector-of-bool/pitchfork/1f1d16549408dc29dae03fc1a479c93556239112/tests/existing/sample/project/src/project/header2.hh -------------------------------------------------------------------------------- /tests/existing/sample/project/src/project/header3.hpp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vector-of-bool/pitchfork/1f1d16549408dc29dae03fc1a479c93556239112/tests/existing/sample/project/src/project/header3.hpp -------------------------------------------------------------------------------- /tests/existing/sample/project/src/project/header4.hxx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vector-of-bool/pitchfork/1f1d16549408dc29dae03fc1a479c93556239112/tests/existing/sample/project/src/project/header4.hxx -------------------------------------------------------------------------------- /tests/existing/sample/project/src/project/header5.h++: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vector-of-bool/pitchfork/1f1d16549408dc29dae03fc1a479c93556239112/tests/existing/sample/project/src/project/header5.h++ -------------------------------------------------------------------------------- /tests/existing/sample/project/src/project/source1.c: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vector-of-bool/pitchfork/1f1d16549408dc29dae03fc1a479c93556239112/tests/existing/sample/project/src/project/source1.c -------------------------------------------------------------------------------- /tests/existing/sample/project/src/project/source2.cc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vector-of-bool/pitchfork/1f1d16549408dc29dae03fc1a479c93556239112/tests/existing/sample/project/src/project/source2.cc -------------------------------------------------------------------------------- /tests/existing/sample/project/src/project/source3.cpp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vector-of-bool/pitchfork/1f1d16549408dc29dae03fc1a479c93556239112/tests/existing/sample/project/src/project/source3.cpp -------------------------------------------------------------------------------- /tests/existing/sample/project/src/project/source4.cxx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vector-of-bool/pitchfork/1f1d16549408dc29dae03fc1a479c93556239112/tests/existing/sample/project/src/project/source4.cxx -------------------------------------------------------------------------------- /tests/existing/sample/project/src/project/source5.c++: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vector-of-bool/pitchfork/1f1d16549408dc29dae03fc1a479c93556239112/tests/existing/sample/project/src/project/source5.c++ -------------------------------------------------------------------------------- /tests/existing/sample/project/src/project/subfolder/header1.h: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vector-of-bool/pitchfork/1f1d16549408dc29dae03fc1a479c93556239112/tests/existing/sample/project/src/project/subfolder/header1.h -------------------------------------------------------------------------------- /tests/existing/sample/project/src/project/subfolder/header2.hh: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vector-of-bool/pitchfork/1f1d16549408dc29dae03fc1a479c93556239112/tests/existing/sample/project/src/project/subfolder/header2.hh -------------------------------------------------------------------------------- /tests/existing/sample/project/src/project/subfolder/header3.hpp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vector-of-bool/pitchfork/1f1d16549408dc29dae03fc1a479c93556239112/tests/existing/sample/project/src/project/subfolder/header3.hpp -------------------------------------------------------------------------------- /tests/existing/sample/project/src/project/subfolder/header4.hxx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vector-of-bool/pitchfork/1f1d16549408dc29dae03fc1a479c93556239112/tests/existing/sample/project/src/project/subfolder/header4.hxx -------------------------------------------------------------------------------- /tests/existing/sample/project/src/project/subfolder/header5.h++: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vector-of-bool/pitchfork/1f1d16549408dc29dae03fc1a479c93556239112/tests/existing/sample/project/src/project/subfolder/header5.h++ -------------------------------------------------------------------------------- /tests/existing/sample/project/src/project/subfolder/source1.c: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vector-of-bool/pitchfork/1f1d16549408dc29dae03fc1a479c93556239112/tests/existing/sample/project/src/project/subfolder/source1.c -------------------------------------------------------------------------------- /tests/existing/sample/project/src/project/subfolder/source2.cc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vector-of-bool/pitchfork/1f1d16549408dc29dae03fc1a479c93556239112/tests/existing/sample/project/src/project/subfolder/source2.cc -------------------------------------------------------------------------------- /tests/existing/sample/project/src/project/subfolder/source3.cpp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vector-of-bool/pitchfork/1f1d16549408dc29dae03fc1a479c93556239112/tests/existing/sample/project/src/project/subfolder/source3.cpp -------------------------------------------------------------------------------- /tests/existing/sample/project/src/project/subfolder/source4.cxx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vector-of-bool/pitchfork/1f1d16549408dc29dae03fc1a479c93556239112/tests/existing/sample/project/src/project/subfolder/source4.cxx -------------------------------------------------------------------------------- /tests/existing/sample/project/src/project/subfolder/source5.c++: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vector-of-bool/pitchfork/1f1d16549408dc29dae03fc1a479c93556239112/tests/existing/sample/project/src/project/subfolder/source5.c++ -------------------------------------------------------------------------------- /tests/existing/update_source_files.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | #include 10 | 11 | namespace fs = pf::fs; 12 | 13 | TEST_CASE("update source files") { 14 | if (!fs::copy_file(fs::path{PF_TEST_SRCDIR "/existing/sample/project/src/CMakeLists.txt"}, 15 | fs::path{PF_TEST_BINDIR "/existing/sample/project/src/CMakeLists.txt"}, 16 | fs::copy_options::overwrite_existing)) { 17 | throw std::runtime_error{ 18 | "Critical failure: unable to restore test state. Please rerun cmake"}; 19 | } 20 | 21 | INFO("file (compare with '.after_update'): " << PF_TEST_BINDIR 22 | "/existing/sample/project/src/CMakeLists.txt"); 23 | 24 | pf::update_source_files( 25 | fs::path{PF_TEST_BINDIR "/existing/sample/project/src/CMakeLists.txt"}, 26 | { 27 | fs::path{PF_TEST_BINDIR "/existing/sample/project/src/project/source1.c"}, 28 | fs::path{PF_TEST_BINDIR "/existing/sample/project/src/project/source2.cc"}, 29 | fs::path{PF_TEST_BINDIR "/existing/sample/project/src/project/source3.cpp"}, 30 | fs::path{PF_TEST_BINDIR "/existing/sample/project/src/project/source4.cxx"}, 31 | fs::path{PF_TEST_BINDIR "/existing/sample/project/src/project/source5.c++"}, 32 | fs::path{PF_TEST_BINDIR "/existing/sample/project/src/project/subfolder/source1.c"}, 33 | fs::path{PF_TEST_BINDIR "/existing/sample/project/src/project/subfolder/source2.cc"}, 34 | fs::path{PF_TEST_BINDIR "/existing/sample/project/src/project/subfolder/source3.cpp"}, 35 | fs::path{PF_TEST_BINDIR "/existing/sample/project/src/project/subfolder/source4.cxx"}, 36 | fs::path{PF_TEST_BINDIR "/existing/sample/project/src/project/subfolder/source5.c++"}, 37 | }); 38 | 39 | std::ifstream expected_file{ 40 | fs::path{PF_TEST_BINDIR "/existing/sample/project/src/CMakeLists.txt.after_update"}, 41 | }; 42 | 43 | std::ifstream actual_file{ 44 | fs::path{PF_TEST_BINDIR "/existing/sample/project/src/CMakeLists.txt"}, 45 | }; 46 | 47 | std::string expected_line, actual_line; 48 | 49 | int line = 0; 50 | while (true) { 51 | // Not in the condition to avoid short circuiting; we need to actually attempt to read from 52 | // both files so we can detect if we've read everything 53 | std::getline(actual_file, actual_line); 54 | std::getline(expected_file, expected_line); 55 | 56 | if (!actual_file || !expected_file) { 57 | break; 58 | } 59 | 60 | CAPTURE(line); 61 | // Don't use CHECK(...) because then a single line difference could output a lot of lines 62 | REQUIRE(actual_line == expected_line); 63 | ++line; 64 | } 65 | 66 | INFO("Files differ after line: " << line); 67 | CHECK(static_cast(actual_file) == static_cast(expected_file)); 68 | } 69 | -------------------------------------------------------------------------------- /tests/expected/simple-cmake/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.10) 2 | 3 | list(INSERT CMAKE_MODULE_PATH 0 "${CMAKE_CURRENT_SOURCE_DIR}/cmake") 4 | 5 | project(simple-cmake VERSION 0.0.1 DESCRIPTION "A great new project") 6 | 7 | # Include third-party components we need for the build 8 | add_subdirectory(third_party) 9 | 10 | add_subdirectory(src) 11 | 12 | option(BUILD_TESTING "Build tests" ON) 13 | if(BUILD_TESTING AND (PROJECT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR)) 14 | enable_testing() 15 | add_subdirectory(tests) 16 | endif() 17 | 18 | option(BUILD_EXAMPLES "Build examples" ON) 19 | if(BUILD_EXAMPLES AND (PROJECT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR)) 20 | add_subdirectory(examples) 21 | endif() 22 | 23 | 24 | -------------------------------------------------------------------------------- /tests/expected/simple-cmake/cmake/ignore_in_diff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vector-of-bool/pitchfork/1f1d16549408dc29dae03fc1a479c93556239112/tests/expected/simple-cmake/cmake/ignore_in_diff -------------------------------------------------------------------------------- /tests/expected/simple-cmake/docs/ignore_in_diff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vector-of-bool/pitchfork/1f1d16549408dc29dae03fc1a479c93556239112/tests/expected/simple-cmake/docs/ignore_in_diff -------------------------------------------------------------------------------- /tests/expected/simple-cmake/examples/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_executable(example1 example1.cpp) 2 | 3 | target_link_libraries(example1 PRIVATE simple::simple-cmake) 4 | -------------------------------------------------------------------------------- /tests/expected/simple-cmake/examples/example1.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | 5 | int main() { 6 | std::cout << "I am an example executable\n"; 7 | std::cout << "Let's calculate the value...\n"; 8 | const auto value = simple::calculate_value(); 9 | std::cout << "The value we got is " << value << '\n'; 10 | } 11 | -------------------------------------------------------------------------------- /tests/expected/simple-cmake/src/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_library( 2 | simple-cmake 3 | simple/simple-cmake.hpp 4 | simple/simple-cmake.cpp 5 | ) 6 | add_library(simple::simple-cmake ALIAS simple-cmake) 7 | target_include_directories(simple-cmake 8 | PUBLIC $ 9 | ) 10 | -------------------------------------------------------------------------------- /tests/expected/simple-cmake/src/simple/simple-cmake.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | int simple::calculate_value() { 4 | // How many roads must a man walk down? 5 | return 6 * 7; 6 | } 7 | -------------------------------------------------------------------------------- /tests/expected/simple-cmake/src/simple/simple-cmake.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SIMPLE_SIMPLE_CMAKE_HPP_INCLUDED 2 | #define SIMPLE_SIMPLE_CMAKE_HPP_INCLUDED 3 | 4 | namespace simple { 5 | 6 | /** 7 | * Calculate the answer. Not sure what the question is, though... 8 | */ 9 | int calculate_value(); 10 | 11 | } // namespace simple 12 | 13 | #endif // SIMPLE_SIMPLE_CMAKE_HPP_INCLUDED -------------------------------------------------------------------------------- /tests/expected/simple-cmake/tests/my_test.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | 5 | int main() { 6 | const auto value = simple::calculate_value(); 7 | if (value == 42) { 8 | std::cout << "We calculated the value correctly\n"; 9 | return 0; 10 | } else { 11 | std::cout << "The value was incorrect!\n"; 12 | return 1; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tests/expected/simple-cmake/third_party/ignore_in_diff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vector-of-bool/pitchfork/1f1d16549408dc29dae03fc1a479c93556239112/tests/expected/simple-cmake/third_party/ignore_in_diff -------------------------------------------------------------------------------- /tests/expected/simple/docs/ignore_in_diff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vector-of-bool/pitchfork/1f1d16549408dc29dae03fc1a479c93556239112/tests/expected/simple/docs/ignore_in_diff -------------------------------------------------------------------------------- /tests/expected/simple/examples/example1.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | 5 | int main() { 6 | std::cout << "I am an example executable\n"; 7 | std::cout << "Let's calculate the value...\n"; 8 | const auto value = simple::calculate_value(); 9 | std::cout << "The value we got is " << value << '\n'; 10 | } 11 | -------------------------------------------------------------------------------- /tests/expected/simple/src/simple/simple.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | int simple::calculate_value() { 4 | // How many roads must a man walk down? 5 | return 6 * 7; 6 | } 7 | -------------------------------------------------------------------------------- /tests/expected/simple/src/simple/simple.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SIMPLE_SIMPLE_HPP_INCLUDED 2 | #define SIMPLE_SIMPLE_HPP_INCLUDED 3 | 4 | namespace simple { 5 | 6 | /** 7 | * Calculate the answer. Not sure what the question is, though... 8 | */ 9 | int calculate_value(); 10 | 11 | } // namespace simple 12 | 13 | #endif // SIMPLE_SIMPLE_HPP_INCLUDED -------------------------------------------------------------------------------- /tests/expected/simple/tests/my_test.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | 5 | int main() { 6 | const auto value = simple::calculate_value(); 7 | if (value == 42) { 8 | std::cout << "We calculated the value correctly\n"; 9 | return 0; 10 | } else { 11 | std::cout << "The value was incorrect!\n"; 12 | return 1; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tests/expected/simple/third_party/ignore_in_diff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vector-of-bool/pitchfork/1f1d16549408dc29dae03fc1a479c93556239112/tests/expected/simple/third_party/ignore_in_diff -------------------------------------------------------------------------------- /tests/generate.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | 5 | #include "./compare_fs.hpp" 6 | 7 | namespace fs = pf::fs; 8 | 9 | namespace { 10 | 11 | fs::path create_test_project(const pf::new_project_params& params) { 12 | std::error_code ec; 13 | INFO("Project name: " + params.name); 14 | INFO("Project namespace: " + params.root_namespace); 15 | INFO("Project directory: " + params.directory.string()); 16 | fs::remove_all(params.directory, ec); 17 | if (ec && ec != std::errc::no_such_file_or_directory) { 18 | REQUIRE_FALSE(ec); 19 | } 20 | REQUIRE_NOTHROW(pf::create_project(params)); 21 | return params.directory; 22 | } 23 | 24 | pf::new_project_params make_project_params(const std::string& name, const std::string& ns) { 25 | return pf::new_project_params{name, 26 | ns, 27 | name, 28 | fs::path{PF_TEST_BINDIR} / "_gen_projects" / name}; 29 | } 30 | 31 | fs::path expected_for(const std::string& name) { 32 | return fs::path{PF_TEST_SRCDIR} / "expected" / name; 33 | } 34 | 35 | void generate_and_compare(const pf::new_project_params& params, const std::string& name) { 36 | auto dir = create_test_project(params); 37 | auto diff = pf::test::compare_fs_tree(dir, expected_for(name)); 38 | CHECK_FALSE(diff); 39 | } 40 | 41 | } // namespace 42 | 43 | TEST_CASE("simple_project") { 44 | auto params = make_project_params("simple", "simple"); 45 | generate_and_compare(params, "simple"); 46 | } 47 | 48 | TEST_CASE("Simple project with CMake") { 49 | auto params = make_project_params("simple-cmake", "simple"); 50 | params.build_system = pf::build_system::cmake; 51 | generate_and_compare(params, "simple-cmake"); 52 | } 53 | -------------------------------------------------------------------------------- /tests/test-main.cpp: -------------------------------------------------------------------------------- 1 | #define CATCH_CONFIG_MAIN 2 | #include 3 | --------------------------------------------------------------------------------