Compare commits

...

32 Commits

Author SHA1 Message Date
Duncan Ogilvie f17923c18b Bump to 0.2.18
1 year ago
Duncan Ogilvie ad17890e23
Merge pull request #85 from build-cpp/settings-rename
1 year ago
Duncan Ogilvie c0b98b3ef4 Add documentation for the [variables] section
1 year ago
Duncan Ogilvie 2c6fc569af Preserve backwards compatibility
1 year ago
Duncan Ogilvie 12ee23a44c Improve error messages
1 year ago
Duncan Ogilvie 82342a6b6f Rename [settings] to [variables]
1 year ago
Duncan Ogilvie 1d95fab0ce Switch to giscus for documentation comments
1 year ago
Duncan Ogilvie 44a77329bf
Merge pull request #83 from build-cpp/document-basics
1 year ago
Duncan Ogilvie e68c5fccdc Add utteranc.es comments
1 year ago
Duncan Ogilvie e2722d2c09 Initial draft of new documentation
1 year ago
Duncan Ogilvie 2627864383
Merge pull request #78 from build-cpp/fix-tests
1 year ago
Duncan Ogilvie 05e21f734a Fix the tests
1 year ago
Duncan Ogilvie 1fd18503cd Successfully fail when running cmkr subcommands
1 year ago
Duncan Ogilvie 8b19441d34 Bump to 0.2.17
1 year ago
Duncan Ogilvie 2437eb90fe Minor improvements
1 year ago
Duncan Ogilvie 6e4006ec07 Fix typo in the docs
2 years ago
Duncan Ogilvie 697b638723 Fix warning on macos
2 years ago
Duncan Ogilvie 109c4bc99f Bump to 0.2.16
2 years ago
Duncan Ogilvie cad85f0bb1
Merge pull request #70 from build-cpp/fix-msvc-runtime
2 years ago
Duncan Ogilvie 53c88ceafa Actually generate the set(CMAKE_MSVC_RUNTIME_LIBRARY)
2 years ago
Duncan Ogilvie d2aa15b1c7 Bump to 0.2.15
2 years ago
Duncan Ogilvie e6ded077cf
Merge pull request #65 from gmh5225/feature-msvc-static
2 years ago
Duncan Ogilvie 3615ccab94 Document msvc-runtime feature
2 years ago
Duncan Ogilvie 73622aa5ba Remove the add_test hook to generate tests the same on all platforms
2 years ago
Duncan Ogilvie 69d844a152 Refactor msvc-static to msvc-runtime
2 years ago
gmh5225 af9a117f50
Fix merge
2 years ago
gmh5225 5fe8220728
Merge branch 'feature-msvc-static' of https://github.com/gmh5225/cmkr into feature-msvc-static
2 years ago
gmh5225 28f541cd3c
[feature] add CMP0091
2 years ago
gmh5225 2f3fd7b95e
Merge branch 'main' into feature-msvc-static
2 years ago
Duncan Ogilvie 07d99c582d
Merge pull request #68 from build-cpp/headers-crash
2 years ago
Duncan Ogilvie 5af2385cc3 Fix the way target.headers is merged into target.sources
2 years ago
gmh5225 981c48dbc5
[feature] msvc-static
2 years ago

1
.gitignore vendored

@ -9,3 +9,4 @@ build*/
.idea/
cmake-build*/
CMakeLists.txt.user
.vscode/

57
CMakeLists.txt generated

@ -7,17 +7,14 @@ if(CMAKE_SOURCE_DIR STREQUAL CMAKE_BINARY_DIR)
message(FATAL_ERROR "In-tree builds are not supported. Run CMake from a separate directory: cmake -B build")
endif()
# Regenerate CMakeLists.txt automatically in the root project
set(CMKR_ROOT_PROJECT OFF)
if(CMAKE_CURRENT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR)
set(CMKR_ROOT_PROJECT ON)
# Enable folder support
set_property(GLOBAL PROPERTY USE_FOLDERS ON)
endif()
# Create a configure-time dependency on cmake.toml to improve IDE support
if(CMKR_ROOT_PROJECT)
# Create a configure-time dependency on cmake.toml to improve IDE support
configure_file(cmake.toml cmake.toml COPYONLY)
endif()
@ -25,7 +22,7 @@ project(cmkr
LANGUAGES
CXX
VERSION
0.2.14
0.2.18
DESCRIPTION
"CMakeLists generator from TOML"
)
@ -33,7 +30,7 @@ project(cmkr
include("cmake/generate_documentation.cmake")
include("cmake/generate_resources.cmake")
# third_party
# Subdirectory: third_party
set(CMKR_CMAKE_FOLDER ${CMAKE_FOLDER})
if(CMAKE_FOLDER)
set(CMAKE_FOLDER "${CMAKE_FOLDER}/third_party")
@ -43,7 +40,7 @@ endif()
add_subdirectory(third_party)
set(CMAKE_FOLDER ${CMKR_CMAKE_FOLDER})
# tests
# Subdirectory: tests
set(CMKR_CMAKE_FOLDER ${CMAKE_FOLDER})
if(CMAKE_FOLDER)
set(CMAKE_FOLDER "${CMAKE_FOLDER}/tests")
@ -53,29 +50,14 @@ endif()
add_subdirectory(tests)
set(CMAKE_FOLDER ${CMKR_CMAKE_FOLDER})
# Target cmkr_generate_documentation
set(CMKR_TARGET cmkr_generate_documentation)
set(cmkr_generate_documentation_SOURCES "")
set(CMKR_SOURCES ${cmkr_generate_documentation_SOURCES})
# Target: cmkr_generate_documentation
add_library(cmkr_generate_documentation INTERFACE)
if(cmkr_generate_documentation_SOURCES)
target_sources(cmkr_generate_documentation INTERFACE ${cmkr_generate_documentation_SOURCES})
endif()
set(CMKR_TARGET cmkr_generate_documentation)
generate_documentation()
unset(CMKR_TARGET)
unset(CMKR_SOURCES)
# Target cmkr
set(CMKR_TARGET cmkr)
set(cmkr_SOURCES "")
list(APPEND cmkr_SOURCES
# Target: cmkr
set(cmkr_SOURCES
"src/arguments.cpp"
"src/build.cpp"
"src/cmake_generator.cpp"
@ -93,24 +75,12 @@ list(APPEND cmkr_SOURCES
"include/project_parser.hpp"
"cmake/cmkr.cmake"
"cmake/version.hpp.in"
)
list(APPEND cmkr_SOURCES
cmake.toml
)
set(CMKR_SOURCES ${cmkr_SOURCES})
add_executable(cmkr)
if(cmkr_SOURCES)
target_sources(cmkr PRIVATE ${cmkr_SOURCES})
endif()
get_directory_property(CMKR_VS_STARTUP_PROJECT DIRECTORY ${PROJECT_SOURCE_DIR} DEFINITION VS_STARTUP_PROJECT)
if(NOT CMKR_VS_STARTUP_PROJECT)
set_property(DIRECTORY ${PROJECT_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT cmkr)
endif()
target_sources(cmkr PRIVATE ${cmkr_SOURCES})
source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${cmkr_SOURCES})
target_compile_features(cmkr PRIVATE
@ -129,10 +99,13 @@ target_link_libraries(cmkr PRIVATE
nlohmann_json
)
generate_resources(${CMKR_TARGET})
get_directory_property(CMKR_VS_STARTUP_PROJECT DIRECTORY ${PROJECT_SOURCE_DIR} DEFINITION VS_STARTUP_PROJECT)
if(NOT CMKR_VS_STARTUP_PROJECT)
set_property(DIRECTORY ${PROJECT_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT cmkr)
endif()
unset(CMKR_TARGET)
unset(CMKR_SOURCES)
set(CMKR_TARGET cmkr)
generate_resources(${CMKR_TARGET})
install(
TARGETS

@ -13,27 +13,29 @@ type = "executable"
sources = ["src/main.cpp"]
```
`cmkr` can bootstrap itself from CMake and you only need CMake to use it.
`cmkr` can bootstrap itself and you only need CMake and a C++ compiler to use it.
## Getting started
To get started run the following commands from your project directory:
To get started, run the following commands from your project directory:
```sh
curl https://raw.githubusercontent.com/build-cpp/cmkr/main/cmake/cmkr.cmake -o cmkr.cmake
cmake -P cmkr.cmake
```
After the bootstrapping process finishes, modify [`cmake.toml`](https://build-cpp.github.io/cmkr/cmake-toml) and open the project in your favorite IDE or build with CMake:
After the bootstrapping process finishes, customize [`cmake.toml`](https://build-cpp.github.io/cmkr/cmake-toml) for your project and run CMake:
```sh
cmake -B build
cmake --build build
```
Once bootstrapped, `cmkr` does not introduce extra steps to your workflow. After modifying `cmake.toml` you simply build/configure your CMake project and `cmkr` will automatically regenerate `CMakeLists.txt`.
Once bootstrapped, `cmkr` does not introduce extra steps to your workflow. After modifying `cmake.toml` you simply build/configure your CMake project and `cmkr` will automatically regenerate `CMakeLists.txt` when necessary.
In CI settings the `cmkr` bootstrapping process is skipped so there is no extra configure-time overhead in your pipelines.
<sub>**Note**: The `cmake.toml` project file, generated `CMakeLists.txt` and `cmkr.cmake` bootstrapping script are all intended to be added to source control.</sub>
In CI environments the `cmkr` bootstrapping process is skipped, so there is no additional overhead in your pipelines.
## Template repositories
@ -53,10 +55,10 @@ Optionally you can put a [`cmkr` release](https://github.com/build-cpp/cmkr/rele
```
Usage: cmkr [arguments]
arguments:
init [executable|library|shared|static|interface] Starts a new project in the same directory.
init [executable|library|shared|static|interface] Create a project.
gen Generates CMakeLists.txt file.
build <extra cmake args> Run cmake and build.
install Run cmake --install. Needs admin privileges.
install Run cmake --install.
clean Clean the build directory.
help Show help.
version Current cmkr version.
@ -64,10 +66,10 @@ arguments:
## Credits
- https://github.com/gulrak/filesystem
- https://github.com/Tessil/ordered-map
- https://github.com/ToruNiina/toml11
- https://github.com/mpark/variant
- https://www.svgrepo.com/svg/192268/hammer
- https://github.com/can1357 for buying `cmkr.build` ❤️
- https://github.com/JustasMasiulis for fixing the dark theme ❤️
- [gulrak/filesystem](https://github.com/gulrak/filesystem)
- [Tessil/ordered-map](https://github.com/Tessil/ordered-map)
- [ToruNiina/toml11](https://github.com/ToruNiina/toml11)
- [mpark/variant](https://github.com/mpark/variant)
- [SVG Repo Hammer](https://www.svgrepo.com/svg/192268/hammer)
- [can1357](https://github.com/can1357) for buying `cmkr.build` ❤️
- [JustasMasiulis](https://github.com/JustasMasiulis) for fixing the dark theme ❤️

@ -4,14 +4,14 @@ cmkr-include = false
[project]
name = "cmkr"
version = "0.2.14"
version = "0.2.18"
description = "CMakeLists generator from TOML"
languages = ["CXX"]
subdirs = ["third_party", "tests"]
include-after = [
"cmake/generate_documentation.cmake",
"cmake/generate_resources.cmake"
]
subdirs = ["third_party", "tests"]
[target.cmkr_generate_documentation]
type = "interface"

@ -2,7 +2,7 @@ include_guard()
# Change these defaults to point to your infrastructure if desired
set(CMKR_REPO "https://github.com/build-cpp/cmkr" CACHE STRING "cmkr git repository" FORCE)
set(CMKR_TAG "v0.2.14" CACHE STRING "cmkr git tag (this needs to be available forever)" FORCE)
set(CMKR_TAG "v0.2.18" CACHE STRING "cmkr git tag (this needs to be available forever)" FORCE)
set(CMKR_COMMIT_HASH "" CACHE STRING "cmkr git commit hash (optional)" FORCE)
# To bootstrap/generate a cmkr project: cmake -P cmkr.cmake

@ -1,20 +1,20 @@
option(CMKR_GENERATE_DOCUMENTATION "Generate cmkr documentation" ${CMKR_ROOT_PROJECT})
set(CMKR_TESTS "" CACHE INTERNAL "List of test directories in the order declared in tests/cmake.toml")
if(CMKR_GENERATE_DOCUMENTATION)
# Hook the add_test function to capture the tests in the order declared in tests/cmake.toml
function(add_test)
cmake_parse_arguments(TEST "" "WORKING_DIRECTORY" "" ${ARGN})
get_filename_component(TEST_WORKING_DIRECTORY "${TEST_WORKING_DIRECTORY}" NAME)
list(APPEND CMKR_TESTS "${TEST_WORKING_DIRECTORY}")
set(CMKR_TESTS "${CMKR_TESTS}" CACHE INTERNAL "")
_add_test(${test} ${ARGN})
endfunction()
endif()
function(generate_documentation)
if(CMKR_GENERATE_DOCUMENTATION)
message(STATUS "[cmkr] Generating documentation...")
# Extract the order of the tests
set(CMKR_TESTS "")
file(READ "${PROJECT_SOURCE_DIR}/tests/cmake.toml" tests_toml NO_HEX_CONVERSION)
string(REGEX MATCHALL "working-directory = \"([^\"]+)\"" tests_match "${tests_toml}")
foreach(match ${tests_match})
if(match MATCHES "working-directory = \"([^\"]+)\"")
list(APPEND CMKR_TESTS "${CMAKE_MATCH_1}")
else()
message(FATAL_ERROR "This should not happen (wrong regex?)")
endif()
endforeach()
# Delete previously generated examples
set(example_folder "${PROJECT_SOURCE_DIR}/docs/examples")

@ -21,4 +21,22 @@ gh_edit_branch: "main"
gh_edit_view_mode: "edit"
gh_edit_source: docs
production_url : https://cmkr.build
ga_tracking: G-DY172KY7Z9
production_url : https://cmkr.build
footer_content: |
<script src="https://giscus.app/client.js"
data-repo="build-cpp/cmkr"
data-repo-id="MDEwOlJlcG9zaXRvcnkyOTQwMDc3MzY="
data-category="Documentation"
data-category-id="DIC_kwDOEYYzuM4CTXeA"
data-mapping="pathname"
data-strict="1"
data-reactions-enabled="1"
data-emit-metadata="0"
data-input-position="top"
data-theme="preferred_color_scheme"
data-lang="en"
crossorigin="anonymous"
async>
</script>

@ -0,0 +1,76 @@
---
layout: page
title: Basics
nav_order: 1
---
# Basics
To effectively use cmkr it helps to understand the basic concepts of CMake.
## Projects
A CMake **project** is a collection of targets. In the context of libraries the project usually corresponds to the **package** that other projects can depend on.
<sub>Visual Studio: a CMake **project** corresponds to a _solution_.</sub>
## Targets
The basic unit of CMake is called a **target**. A target (also referred to as [binary target](https://cmake.org/cmake/help/latest/manual/cmake-buildsystem.7.html#binary-targets) in the CMake documentation) corresponds to an executable or library you can build. There are also [pseudo targets](https://cmake.org/cmake/help/latest/manual/cmake-buildsystem.7.html#pseudo-targets), but we ignore them for now.
<sub>Visual Studio: a **target** corresponds to a _project_.</sub>
## Target Properties
Targets have a collection of **properties** that describe how to build them.
Examples of properties:
- _Sources_: the `*.cpp` files used to build the target.
- _Compile options_: Command line flags for the compiler.
- _Link libraries_: The **dependencies** required to build this target.
<sub>See the [CMake documentation](https://cmake.org/cmake/help/latest/manual/cmake-properties.7.html#properties-on-targets) for an exhaustive list of target properties.</sub>
**Important**: The term **link** has a slightly different meaning in CMake than you might expect. In addition to adding a library to the command line of the linker, CMake also propagates properties of the target you link to.
<sub>You can think of **linking** as _depending on_.</sub>
The propagation of properties depends on their **visibility**:
- **Private**: properties that are only used when building the target itself.
- **Interface**: properties that are used when depending on this target.
- **Public**: combination of private and interface.
In practice you default to **private**, unless consumers of your library _require_ the property to build their target. In that case you use **public**.
### Example
The most intuitive example is with _include directories_. Imagine there are two targets:
1. `StringUtils`: A library with string utilities.
- _Sources_: `StringUtils/src/stringutils.cpp`
- _Include directories_: `StringUtils/include`
2. `DataProcessor`: An executable that uses functionality from `StringUtils` to process some data.
- _Sources_: `DataProcessor/src/main.cpp`
- _Link libraries_: `StringUtils`
The _include directories_ property of `StringUtils` has to be **public**. If it was **private**, the `DataProcessor` target would fail to `#include <stringutils.hpp>` since the _include directories_ property is not propagated.
The `cmake.toml` for this example would look something like this:
```toml
[project]
name = "DataProcessor"
[target.StringUtils]
type = "static"
sources = ["StringUtils/src/stringutils.cpp"]
headers = ["StringUtils/include/stringutils.hpp"]
include-directories = ["StringUtils/include"]
[target.DataProcessor]
type = "executable"
sources = ["DataProcessor/src/main.cpp"]
link-libraries = ["StringUtils"]
```

@ -2,14 +2,15 @@
layout: page
title: Reference
permalink: /cmake-toml/
nav_order: 1
nav_order: 3
---
# Reference
This page is a reference for the options in the `cmake.toml` file. If you think anything is missing or unclear, please [edit this page](https://github.com/build-cpp/cmkr/edit/main/docs/cmake-toml.md) or open an [issue](https://github.com/build-cpp/cmkr/issues).
See the [examples](/examples) section for concrete examples.
This is a reference page. Check out the [examples](/examples) and the [cmkr topic](https://github.com/topics/cmkr) as well.
{:.info}
## CMake configuration
@ -27,6 +28,7 @@ name = "myproject"
version = "1.0.0"
description = "Description of the project"
languages = ["C", "CXX"]
msvc-runtime = "" # dynamic (implicit default), static
cmake-before = """
message(STATUS "CMake injected before the project() call")
"""
@ -43,8 +45,8 @@ You can specify your own conditions and use them in any `condition` field:
```toml
[conditions]
arch64 = "CMAKE_SIZEOF_VOID_P EQUALS 8"
arch32 = "CMAKE_SIZEOF_VOID_P EQUALS 4"
arch64 = "CMAKE_SIZEOF_VOID_P EQUAL 8"
arch32 = "CMAKE_SIZEOF_VOID_P EQUAL 4"
```
This will make the `arch64` and `arch32` conditions available with their respective CMake expressions.
@ -58,6 +60,8 @@ sources = ["src/main.cpp"]
windows.sources = ["src/windows_specific.cpp"]
```
### Predefined conditions
The following conditions are predefined (you can override them if you desire):
```toml
@ -90,6 +94,16 @@ include-before = ["cmake/before-subdir.cmake"]
include-after = ["cmake/after-subdir.cmake"]
```
## Variables
```toml
[variables]
MYBOOL = true
MYSTRING = "hello"
```
Variables emit a [`set`](https://cmake.org/cmake/help/latest/command/set.html) and can be used to configure subprojects and packages.
## Vcpkg
```toml
@ -144,6 +158,7 @@ alias = "mytarget::mytarget"
type = "static" # executable, shared (DLL), static, interface, object, library, custom
headers = ["src/mytarget.h"]
sources = ["src/mytarget.cpp"]
msvc-runtime = "" # dynamic (implicit default), static
# The keys below match the target_xxx CMake commands
# Keys prefixed with private- will get PRIVATE visibility

@ -0,0 +1,34 @@
---
# Automatically generated from tests/msvc-runtime/cmake.toml - DO NOT EDIT
layout: default
title: Static MSVC runtime
permalink: /examples/msvc-runtime
parent: Examples
nav_order: 8
---
# Static MSVC runtime
```toml
[project]
name = "msvc-runtime"
description = "Static MSVC runtime"
msvc-runtime = "static"
# This target will compile with a static runtime
[target.static-runtime]
type = "executable"
sources = ["src/main.cpp"]
# This target overrides the [project].msvc-runtime
[target.dynamic-runtime]
type = "executable"
sources = ["src/main.cpp"]
msvc-runtime = "dynamic"
```
<sup><sub>This page was automatically generated from [tests/msvc-runtime/cmake.toml](https://github.com/build-cpp/cmkr/tree/main/tests/msvc-runtime/cmake.toml).</sub></sup>

@ -19,7 +19,7 @@ description = "Dependencies from vcpkg"
# See https://github.com/microsoft/vcpkg/releases for vcpkg versions
# See https://vcpkg.io/en/packages.html for available packages
[vcpkg]
version = "2021.05.12"
version = "2022.11.14"
packages = ["fmt"]
[find-package]

@ -1,12 +1,12 @@
---
layout: home
title: Index
title: Documentation
nav_order: 0
---
# Index
# Documentation
`cmkr`, pronounced "cmaker", is a modern build system based on [CMake](https://cmake.org/) and [TOML](https://toml.io).
[`cmkr`](https://github.com/build-cpp/cmkr), pronounced "cmaker", is a modern build system based on [CMake](https://cmake.org/) and [TOML](https://toml.io).
`cmkr` parses `cmake.toml` files and generates a modern, idiomatic `CMakeLists.txt` for you. A minimal example:
@ -19,27 +19,29 @@ type = "executable"
sources = ["src/main.cpp"]
```
`cmkr` can bootstrap itself from CMake and you only need CMake to use it.
`cmkr` can bootstrap itself, and you only need CMake and a C++ compiler to use it.
## Getting started
To get started run the following commands from your project directory:
To get started, run the following commands from your project directory:
```sh
curl https://raw.githubusercontent.com/build-cpp/cmkr/main/cmake/cmkr.cmake -o cmkr.cmake
cmake -P cmkr.cmake
```
After the bootstrapping process finishes, modify [`cmake.toml`](https://build-cpp.github.io/cmkr/cmake-toml) and open the project in your favorite IDE or build with CMake:
After the bootstrapping process finishes, customize [`cmake.toml`](./cmake-toml) for your project and run CMake:
```sh
cmake -B build
cmake --build build
```
Once bootstrapped, `cmkr` does not introduce extra steps to your workflow. After modifying `cmake.toml` you simply build/configure your CMake project and `cmkr` will automatically regenerate `CMakeLists.txt`.
Once bootstrapped, `cmkr` does not introduce extra steps to your workflow. After modifying `cmake.toml` you simply build/configure your CMake project and `cmkr` will automatically regenerate `CMakeLists.txt` when necessary.
In CI settings the `cmkr` bootstrapping process is skipped so there is no extra configure-time overhead in your pipelines.
<sub>**Note**: The `cmake.toml` project file, generated `CMakeLists.txt` and `cmkr.cmake` bootstrapping script are all intended to be added to source control.</sub>
In CI environments the `cmkr` bootstrapping process is skipped, so there is no additional overhead in your pipelines.
## Template repositories
@ -59,10 +61,10 @@ Optionally you can put a [`cmkr` release](https://github.com/build-cpp/cmkr/rele
```
Usage: cmkr [arguments]
arguments:
init [executable|library|shared|static|interface] Starts a new project in the same directory.
init [executable|library|shared|static|interface] Create a project.
gen Generates CMakeLists.txt file.
build <extra cmake args> Run cmake and build.
install Run cmake --install. Needs admin privileges.
install Run cmake --install.
clean Clean the build directory.
help Show help.
version Current cmkr version.
@ -70,8 +72,10 @@ arguments:
## Credits
- https://github.com/gulrak/filesystem
- https://github.com/Tessil/ordered-map
- https://github.com/ToruNiina/toml11
- https://github.com/mpark/variant
- https://www.svgrepo.com/svg/192268/hammer
- [gulrak/filesystem](https://github.com/gulrak/filesystem)
- [Tessil/ordered-map](https://github.com/Tessil/ordered-map)
- [ToruNiina/toml11](https://github.com/ToruNiina/toml11)
- [mpark/variant](https://github.com/mpark/variant)
- [SVG Repo Hammer](https://www.svgrepo.com/svg/192268/hammer)
- [can1357](https://github.com/can1357) for buying `cmkr.build` ❤️
- [JustasMasiulis](https://github.com/JustasMasiulis) for fixing the dark theme ❤️

@ -0,0 +1,83 @@
---
layout: page
title: Philosophy
nav_order: 2
published: false
---
# Philosophy
## Problem statement
Similar to writing good C++, writing good CMake is difficult. The main difference is that nobody actually wants to learn CMake. The build system is something that should "just work".
There have been many attempts at creating new build systems (with varying levels of success). Naturally this causes [competing standards](https://xkcd.com/927/), which is undesirable. CMake is pretty much the de facto standard, and it has seen extensive use in complex software projects.
One of the main issues of CMake is the turing-complete scripting language you use to describe your projects. As your project gets more complex this can be very helpful, but it also unnecessarily complicates simple projects. There have been discussions about a declarative language on the [CMake issue tracker](https://gitlab.kitware.com/cmake/cmake/-/issues/19891) to solve this problem, but it looks like a "LISP-like" language is seriously being considered...
## The solution
The way cmkr (pronounced "cmaker") solves this problem is by using [TOML](https://toml.io/). Below is a minimal `cmake.toml` project:
```toml
[project]
name = "cmkr_for_beginners"
[target.hello_world]
type = "executable"
sources = ["src/main.cpp"]
```
The key difference between `cmkr` and other build systems is that it _generates_ CMake. This means that your projects are fully compatible with the CMake ecosystem, and you can try it out without having to rewrite your whole build system.
TODO: link/include more examples? Talk about conditions, packaging (missing)
### Another layer?!
A common issue people have with cmkr is that it introduces an additional layer of indirection to your build system. CMake is already a meta-buildsystem, which means you could call cmkr a "meta-meta-buildsystem".
Presumably the reason for this friction is that additional layers of abstraction introduce additional complexity. Because of this cmkr has been designed to be completely seamless:
- The user doesn't have to install any additional software to use cmkr. All you need is a semi-recent version of CMake and a C++ compiler.
- Modifying `cmake.toml` automatically triggers a regeneration of `CMakeLists.txt`. There is no change to your build process.
- The `CMakeLists.txt` is generated to be human-readable. This means you can easily "eject" from cmkr and go back to CMake.
An additional argument for cmkr is that anecdotally people say it "just works". Because of its simplicity it's also easy to teach, even to people without programming experience.
There is also precedent in the JavaScript community. Bundlers and translators are the norm there and their developer experience is miles ahead of C++.
<sub>Not to say the JavaScript ecosystem is without its flaws, but generators does not appear to be one of them</sub>
### Unsupported features
Because cmkr is still in early in development there are many missing/unfinished features. It was decided that users can bridge the gap by including CMake at arbitrary locations.
This has the advantage that it forces complexity in the build system to be self-contained and modular, a problem all too common in projects as they age.
## Enterprise
Words like "bootstrapping" and "generating code" might worry engineers working in an enterprise environment, and rightly so. From the beginning it has been a priority to make cmkr suitable for use in big corporations with strict protocols for security and reproducibility.
### No additional dependencies
As mentioned above, the only thing you need is a working C++ compiler and a semi-recent version of CMake. It is assumed that you are already building (and executing) C++ projects on your servers, so cmkr does not introduce additional requirements.
All the logic for downloading and compiling the `cmkr` executable is self-contained in a ~250 line `cmkr.cmake` script. You can easily audit it and see if it's up to your standards.
### Reproducibility
Per default the `cmkr.cmake` bootstrapping script contains the exact version of cmkr used to generate your project. As long as the cmkr repository is available you will build the exact same version of cmkr, which will generate the exact same `CMakeLists.txt` file.
This also means that cmkr can decide to break backwards compatibility without affecting legacy projects. An effort will always be made to maintain backwards compatibility though.
### Integrity
As an additional safeguard you can modify `cmkr.cmake` to pin the version tag to a commit hash. This hash is checked to ensure the integrity of the upstream repository.
### Availability
You can easily point `cmkr.cmake` to a mirror of the cmkr repository to ensure availability should something catastrophic happen.
### Not executed in CI
The final (and key) feature is that the bootstrapping process is never executed in CI environments. This means `cmkr` is only ever executed on your developer's machines and not on your infrastructure.

@ -1,2 +0,0 @@
bundle exec just-the-docs rake search:init
bundle exec jekyll serve --livereload

@ -1,2 +1,2 @@
bundle install
bundle exec jekyll serve --force_polling --livereload --incremental
bundle exec jekyll serve --force_polling --livereload --incremental

@ -7,5 +7,3 @@ const char *handle_args(int argc, char **argv);
} // namespace args
} // namespace cmkr
const char *cmkr_args_handle_args(int, char **);

@ -3,6 +3,7 @@
#include <mpark/variant.hpp>
#include <string>
#include <tsl/ordered_map.h>
#include <tsl/ordered_set.h>
#include <vector>
namespace cmkr {
@ -13,7 +14,7 @@ using Condition = tsl::ordered_map<std::string, T>;
using ConditionVector = Condition<std::vector<std::string>>;
struct Setting {
struct Variable {
std::string name;
std::string comment;
mpark::variant<bool, std::string> val;
@ -67,7 +68,6 @@ struct Target {
TargetType type = target_last;
std::string type_name;
ConditionVector headers;
ConditionVector sources;
// https://cmake.org/cmake/help/latest/manual/cmake-commands.7.html#project-commands
@ -152,7 +152,17 @@ struct Content {
ConditionVector include_after;
};
enum MsvcRuntimeType {
msvc_dynamic,
msvc_static,
msvc_last,
};
extern const char *msvcRuntimeTypeNames[msvc_last];
struct Project {
const Project *parent;
// This is the CMake version required to use all cmkr versions.
std::string cmake_version = "3.15";
std::string cmkr_include = "cmkr.cmake";
@ -170,11 +180,12 @@ struct Project {
std::string project_version;
std::string project_description;
std::vector<std::string> project_languages;
MsvcRuntimeType project_msvc_runtime = msvc_last;
Condition<std::string> cmake_before;
Condition<std::string> cmake_after;
ConditionVector include_before;
ConditionVector include_after;
std::vector<Setting> settings;
std::vector<Variable> variables;
std::vector<Option> options;
std::vector<Package> packages;
Vcpkg vcpkg;
@ -187,6 +198,7 @@ struct Project {
std::vector<Subdir> subdirs;
Project(const Project *parent, const std::string &path, bool build);
const Project *root() const;
};
bool is_root_path(const std::string &path);

@ -37,17 +37,17 @@ const char *handle_args(int argc, char **argv) {
} else if (main_arg == "build") {
auto ret = build::run(argc, argv);
if (ret)
return "CMake build error!";
throw std::runtime_error("CMake build failed!");
return "CMake build completed!";
} else if (main_arg == "install") {
auto ret = build::install();
if (ret)
return "CMake install error!";
throw std::runtime_error("CMake install failed!");
return "CMake install completed!";
} else if (main_arg == "clean") {
auto ret = build::clean();
if (ret)
return "CMake clean error!";
throw std::runtime_error("CMake clean failed!");
return "Cleaned build directory!";
} else {
throw std::runtime_error(cmkr::help::message());
@ -55,13 +55,3 @@ const char *handle_args(int argc, char **argv) {
}
} // namespace args
} // namespace cmkr
const char *cmkr_args_handle_args(int argc, char **argv) {
try {
return cmkr::args::handle_args(argc, argv);
} catch (const std::exception &e) {
return e.what();
} catch (...) {
return "Unknown error!";
}
}

@ -4,6 +4,7 @@
#include <resources/cmkr.hpp>
#include "fs.hpp"
#include "project_parser.hpp"
#include <cstdio>
#include <fstream>
#include <memory>
@ -25,7 +26,7 @@ static std::string format(const char *format, tsl::ordered_map<std::string, std:
return s;
}
static std::vector<std::string> expand_cmake_path(const fs::path &name, const fs::path &toml_dir, bool root_project) {
static std::vector<std::string> expand_cmake_path(const fs::path &name, const fs::path &toml_dir, bool is_root_project) {
std::vector<std::string> temp;
auto extract_suffix = [](const fs::path &base, const fs::path &full) {
@ -38,7 +39,7 @@ static std::vector<std::string> expand_cmake_path(const fs::path &name, const fs
auto stem = name.filename().stem().string();
auto ext = name.extension();
if (root_project && stem == "**" && name == name.filename()) {
if (is_root_project && stem == "**" && name == name.filename()) {
throw std::runtime_error("Recursive globbing not allowed in project root: " + name.string());
}
@ -66,11 +67,11 @@ static std::vector<std::string> expand_cmake_path(const fs::path &name, const fs
return temp;
}
static std::vector<std::string> expand_cmake_paths(const std::vector<std::string> &sources, const fs::path &toml_dir, bool root_project) {
static std::vector<std::string> expand_cmake_paths(const std::vector<std::string> &sources, const fs::path &toml_dir, bool is_root_project) {
// TODO: add duplicate checking
std::vector<std::string> result;
for (const auto &src : sources) {
auto expanded = expand_cmake_path(src, toml_dir, root_project);
auto expanded = expand_cmake_path(src, toml_dir, is_root_project);
for (const auto &f : expanded) {
result.push_back(f);
}
@ -416,9 +417,10 @@ struct Generator {
}
if (!condition.empty()) {
cmd("endif")();
cmd("endif")().endl();
} else if (!itr.second.empty()) {
endl();
}
endl();
}
}
}
@ -505,7 +507,7 @@ void generate_cmake(const char *path, const parser::Project *parent_project) {
}
// Root project doesn't have a parent
auto root_project = parent_project == nullptr;
auto is_root_project = parent_project == nullptr;
parser::Project project(parent_project, path, false);
Generator gen(project);
@ -521,9 +523,27 @@ void generate_cmake(const char *path, const parser::Project *parent_project) {
comment("See " + cmkr_url + " for more information");
endl();
if (root_project) {
if (is_root_project) {
cmd("cmake_minimum_required")("VERSION", project.cmake_version).endl();
if (project.project_msvc_runtime != parser::msvc_last) {
comment("Enable support for MSVC_RUNTIME_LIBRARY");
cmd("cmake_policy")("SET", "CMP0091", "NEW");
switch (project.project_msvc_runtime) {
case parser::msvc_dynamic:
cmd("set")("CMAKE_MSVC_RUNTIME_LIBRARY", "MultiThreaded$<$<CONFIG:Debug>:Debug>DLL");
break;
case parser::msvc_static:
cmd("set")("CMAKE_MSVC_RUNTIME_LIBRARY", "MultiThreaded$<$<CONFIG:Debug>:Debug>");
break;
default:
break;
}
endl();
}
// clang-format on
if (!project.allow_in_tree) {
// clang-format off
cmd("if")("CMAKE_SOURCE_DIR", "STREQUAL", "CMAKE_BINARY_DIR");
@ -532,14 +552,13 @@ void generate_cmake(const char *path, const parser::Project *parent_project) {
// clang-format on
}
comment("Regenerate CMakeLists.txt automatically in the root project");
cmd("set")("CMKR_ROOT_PROJECT", "OFF");
// clang-format off
cmd("if")("CMAKE_CURRENT_SOURCE_DIR", "STREQUAL", "CMAKE_SOURCE_DIR");
cmd("set")("CMKR_ROOT_PROJECT", "ON").endl();
if (!project.cmkr_include.empty()) {
comment("Bootstrap cmkr");
comment("Bootstrap cmkr and automatically regenerate CMakeLists.txt");
cmd("include")(project.cmkr_include, "OPTIONAL", "RESULT_VARIABLE", "CMKR_INCLUDE_RESULT");
cmd("if")("CMKR_INCLUDE_RESULT");
cmd("cmkr")();
@ -547,7 +566,10 @@ void generate_cmake(const char *path, const parser::Project *parent_project) {
}
comment("Enable folder support");
cmd("set_property")("GLOBAL", "PROPERTY", "USE_FOLDERS", "ON");
cmd("set_property")("GLOBAL", "PROPERTY", "USE_FOLDERS", "ON").endl();
comment("Create a configure-time dependency on cmake.toml to improve IDE support");
cmd("configure_file")("cmake.toml", "cmake.toml", "COPYONLY");
cmd("endif")().endl();
// clang-format on
@ -555,15 +577,15 @@ void generate_cmake(const char *path, const parser::Project *parent_project) {
if (!project.cmkr_include.empty() && !fs::exists(cmkr_include) && cmkr_include.is_relative()) {
create_file(cmkr_include, resources::cmkr);
}
} else {
// clang-format off
comment("Create a configure-time dependency on cmake.toml to improve IDE support");
cmd("if")("CMKR_ROOT_PROJECT");
cmd("configure_file")("cmake.toml", "cmake.toml", "COPYONLY");
cmd("endif")().endl();
// clang-format on
}
// clang-format off
comment("Create a configure-time dependency on cmake.toml to improve IDE support");
cmd("if")("CMKR_ROOT_PROJECT");
cmd("configure_file")("cmake.toml", "cmake.toml", "COPYONLY");
cmd("endif")().endl();
// clang-format on
// TODO: remove support and replace with global compile-features
if (!project.cppflags.empty()) {
ss << "set(CMAKE_CXX_FLAGS ${CMAKE_CXX_FLAGS} \"";
@ -599,9 +621,9 @@ void generate_cmake(const char *path, const parser::Project *parent_project) {
endl();
}
if (!project.settings.empty()) {
comment("Settings");
for (const auto &set : project.settings) {
if (!project.variables.empty()) {
comment("Variables");
for (const auto &set : project.variables) {
std::string set_val;
if (set.val.index() == 1) {
set_val = mpark::get<1>(set.val);
@ -628,6 +650,13 @@ void generate_cmake(const char *path, const parser::Project *parent_project) {
auto version = std::make_pair("VERSION", project.project_version);
auto description = std::make_pair("DESCRIPTION", project.project_description);
cmd("project")(project.project_name, languages, version, description).endl();
for (const auto &language : project.project_languages) {
if (language == "CSharp") {
cmd("include")("CSharpUtilities").endl();
break;
}
}
}
gen.conditional_includes(project.include_after);
@ -771,7 +800,7 @@ void generate_cmake(const char *path, const parser::Project *parent_project) {
auto add_subdir = [&](const std::string &dir) {
// clang-format off
comment(dir);
comment("Subdirectory: " + dir);
cmd("set")("CMKR_CMAKE_FOLDER", "${CMAKE_FOLDER}");
cmd("if")("CMAKE_FOLDER");
cmd("set")("CMAKE_FOLDER", "${CMAKE_FOLDER}/" + dir);
@ -781,17 +810,19 @@ void generate_cmake(const char *path, const parser::Project *parent_project) {
// clang-format on
cmd("add_subdirectory")(dir);
cmd("set")("CMAKE_FOLDER", "${CMKR_CMAKE_FOLDER}").endl();
cmd("set")("CMAKE_FOLDER", "${CMKR_CMAKE_FOLDER}");
};
// generate_cmake is called on the subdirectories recursively later
if (!project.project_subdirs.empty()) {
gen.handle_condition(project.project_subdirs, [&](const std::string &, const std::vector<std::string> &subdirs) {
for (const auto &dir : subdirs) {
add_subdir(dir);
for (size_t i = 0; i < subdirs.size(); i++) {
add_subdir(subdirs[i]);
if (i + 1 < subdirs.size()) {
endl();
}
}
});
endl();
}
for (const auto &subdir : project.subdirs) {
ConditionScope cs(gen, subdir.condition);
@ -800,22 +831,20 @@ void generate_cmake(const char *path, const parser::Project *parent_project) {
gen.conditional_cmake(subdir.cmake_before);
add_subdir(subdir.name);
endl();
gen.conditional_includes(subdir.include_after);
gen.conditional_cmake(subdir.cmake_after);
}
if (!project.targets.empty()) {
auto project_root = project.root();
for (size_t i = 0; i < project.targets.size(); i++) {
if (i > 0) {
endl();
}
const auto &target = project.targets[i];
const parser::Template *tmplate = nullptr;
std::unique_ptr<ConditionScope> tmplate_cs{};
comment("Target " + target.name);
comment("Target: " + target.name);
// Check if this target is using a template.
if (target.type == parser::target_template) {
@ -829,56 +858,115 @@ void generate_cmake(const char *path, const parser::Project *parent_project) {
ConditionScope cs(gen, target.condition);
cmd("set")("CMKR_TARGET", target.name);
// Detect if there is cmake included before/after the target
auto has_include_before = false;
auto has_include_after = false;
{
auto has_include = [](const parser::ConditionVector &includes) {
for (const auto &itr : includes) {
for (const auto &jtr : itr.second) {
return true;
}
}
return false;
};
auto has_include_helper = [&](const parser::Target &target) {
if (!target.cmake_before.empty() || has_include(target.include_before)) {
has_include_before = true;
}
if (!target.cmake_after.empty() || has_include(target.include_after)) {
has_include_after = true;
}
};
if (tmplate != nullptr) {
has_include_helper(tmplate->outline);
}
has_include_helper(target);
}
// Generate the include before
if (has_include_before) {
cmd("set")("CMKR_TARGET", target.name);
}
if (tmplate != nullptr) {
gen.conditional_includes(tmplate->outline.include_before);
gen.conditional_cmake(tmplate->outline.cmake_before);
}
gen.conditional_includes(target.include_before);
gen.conditional_cmake(target.cmake_before);
auto sources_var = target.name + "_SOURCES";
bool added_toml = false;
cmd("set")(sources_var, RawArg("\"\"")).endl();
// Merge the sources from the template and the target. The sources
// without condition need to be processed first
parser::Condition<tsl::ordered_set<std::string>> msources;
msources[""].clear();
if (tmplate != nullptr) {
gen.handle_condition(tmplate->outline.sources, [&](const std::string &condition, const std::vector<std::string> &condition_sources) {
auto sources = expand_cmake_paths(condition_sources, path, root_project);
if (sources.empty()) {
auto source_key = condition.empty() ? "sources" : (condition + ".sources");
throw std::runtime_error(target.name + " " + source_key + " wildcard found 0 files");
auto merge_sources = [&msources](const parser::ConditionVector &sources) {
for (const auto &itr : sources) {
auto &source_list = msources[itr.first];
for (const auto &source : itr.second) {
source_list.insert(source);
}
cmd("list")("APPEND", sources_var, sources);
});
}
};
if (tmplate != nullptr) {
merge_sources(tmplate->outline.sources);
}
merge_sources(target.sources);
// Improve IDE support
if (target.type != parser::target_interface) {
msources[""].insert("cmake.toml");
}
// If there are only conditional sources we generate a 'set' to
// create an empty source list. The rest is then appended using
// 'list(APPEND ...)'
auto has_sources = false;
for (const auto &itr : msources) {
if (!itr.second.empty()) {
has_sources = true;
break;
}
}
auto sources_var = target.name + "_SOURCES";
auto sources_with_set = true;
if (has_sources && msources[""].empty()) {
sources_with_set = false;
cmd("set")(sources_var, RawArg("\"\"")).endl();
}
gen.handle_condition(target.sources, [&](const std::string &condition, const std::vector<std::string> &condition_sources) {
auto sources = expand_cmake_paths(condition_sources, path, root_project);
gen.handle_condition(msources, [&](const std::string &condition, const tsl::ordered_set<std::string> &source_set) {
std::vector<std::string> condition_sources;
condition_sources.reserve(source_set.size());
for (const auto &source : source_set) {
condition_sources.push_back(source);
}
auto sources = expand_cmake_paths(condition_sources, path, is_root_project);
if (sources.empty()) {
auto source_key = condition.empty() ? "sources" : (condition + ".sources");
throw std::runtime_error(target.name + " " + source_key + " wildcard found 0 files");
}
// Do not add cmake.toml twice
if (!added_toml && std::find(sources.begin(), sources.end(), "cmake.toml") != sources.end()) {
added_toml = true;
if (sources_with_set) {
// This is a sanity check to make sure the unconditional sources are first
if (!condition.empty()) {
throw std::runtime_error("Unreachable code, make sure unconditional sources are first");
}
cmd("set")(sources_var, sources);
sources_with_set = false;
} else {
cmd("list")("APPEND", sources_var, sources);
}
cmd("list")("APPEND", sources_var, sources);
});
auto target_type = target.type;
if (tmplate != nullptr) {
if (target_type != parser::target_template) {
throw std::runtime_error("Unreachable code, unexpected target type for template");
}
target_type = tmplate->outline.type;
}
if (!added_toml && target_type != parser::target_interface) {
cmd("list")("APPEND", sources_var, std::vector<std::string>{"cmake.toml"}).endl();
}
cmd("set")("CMKR_SOURCES", "${" + sources_var + "}");
std::string add_command;
std::string target_type_string;
std::string target_scope;
@ -934,34 +1022,20 @@ void generate_cmake(const char *path, const parser::Project *parent_project) {
cmd(add_command)(target.name, target_type_string, "${" + sources_var + "}");
} else {
cmd(add_command)(target.name, target_type_string).endl();
// clang-format off
cmd("if")(sources_var);
cmd("target_sources")(target.name, target_type == parser::target_interface ? "INTERFACE" : "PRIVATE", "${" + sources_var + "}");
cmd("endif")().endl();
// clang-format on
if (has_sources) {
cmd("target_sources")(target.name, target_type == parser::target_interface ? "INTERFACE" : "PRIVATE",
"${" + sources_var + "}");
}
}
} else {
cmd(add_command)(target.name, target_type_string).endl();
// clang-format off
cmd("if")(sources_var);
if (has_sources) {
cmd("target_sources")(target.name, target_type == parser::target_interface ? "INTERFACE" : "PRIVATE", "${" + sources_var + "}");
cmd("endif")().endl();
// clang-format on
}
// The first executable target will become the Visual Studio startup project
if (target_type == parser::target_executable) {
cmd("get_directory_property")("CMKR_VS_STARTUP_PROJECT", "DIRECTORY", "${PROJECT_SOURCE_DIR}", "DEFINITION", "VS_STARTUP_PROJECT");
// clang-format off
cmd("if")("NOT", "CMKR_VS_STARTUP_PROJECT");
cmd("set_property")("DIRECTORY", "${PROJECT_SOURCE_DIR}", "PROPERTY", "VS_STARTUP_PROJECT", target.name);
cmd("endif")().endl();
// clang-format on
}
}
if (!target.sources.empty()) {
// TODO: support sources from other directories
if (has_sources) {
cmd("source_group")("TREE", "${CMAKE_CURRENT_SOURCE_DIR}", "FILES", "${" + sources_var + "}").endl();
}
@ -1014,23 +1088,39 @@ void generate_cmake(const char *path, const parser::Project *parent_project) {
}
gen.handle_condition(props, [&](const std::string &, const tsl::ordered_map<std::string, std::string> &properties) {
for (const auto &propItr : properties) {
if (propItr.first == "MSVC_RUNTIME_LIBRARY") {
if (project_root->project_msvc_runtime == parser::msvc_last) {
throw std::runtime_error("You cannot set [target].msvc-runtime without setting the root [project].msvc-runtime");
}
}
}
cmd("set_target_properties")(target.name, "PROPERTIES", properties);
});
}
// The first executable target will become the Visual Studio startup project
// TODO: this is not working properly
if (target_type == parser::target_executable) {
cmd("get_directory_property")("CMKR_VS_STARTUP_PROJECT", "DIRECTORY", "${PROJECT_SOURCE_DIR}", "DEFINITION", "VS_STARTUP_PROJECT");
// clang-format off
cmd("if")("NOT", "CMKR_VS_STARTUP_PROJECT");
cmd("set_property")("DIRECTORY", "${PROJECT_SOURCE_DIR}", "PROPERTY", "VS_STARTUP_PROJECT", target.name);
cmd("endif")().endl();
// clang-format on
}
// Generate the include after
if (!has_include_before && has_include_after) {
cmd("set")("CMKR_TARGET", target.name);
}
gen.conditional_includes(target.include_after);
gen.conditional_cmake(target.cmake_after);
if (tmplate != nullptr) {
gen.conditional_includes(tmplate->outline.include_after);
gen.conditional_cmake(tmplate->outline.cmake_after);
}
cmd("unset")("CMKR_TARGET");
cmd("unset")("CMKR_SOURCES");
}
endl();
}
if (!project.tests.empty()) {
@ -1056,7 +1146,7 @@ void generate_cmake(const char *path, const parser::Project *parent_project) {
auto dirs = std::make_pair("DIRS", inst.dirs);
std::vector<std::string> files_data;
if (!inst.files.empty()) {
files_data = expand_cmake_paths(inst.files, path, root_project);
files_data = expand_cmake_paths(inst.files, path, is_root_project);
if (files_data.empty()) {
throw std::runtime_error("[[install]] files wildcard did not resolve to any files");
}

@ -4,8 +4,6 @@
#include <deque>
#include <stdexcept>
#include <toml.hpp>
#include <tsl/ordered_map.h>
#include <tsl/ordered_set.h>
namespace cmkr {
namespace parser {
@ -21,16 +19,27 @@ static TargetType parse_targetType(const std::string &name) {
return target_last;
}
const char *msvcRuntimeTypeNames[msvc_last] = {"dynamic", "static"};
static MsvcRuntimeType parse_msvcRuntimeType(const std::string &name) {
for (int i = 0; i < msvc_last; i++) {
if (name == msvcRuntimeTypeNames[i]) {
return static_cast<MsvcRuntimeType>(i);
}
}
return msvc_last;
}
using TomlBasicValue = toml::basic_value<toml::discard_comments, tsl::ordered_map, std::vector>;
static std::string format_key_error(const std::string &error, const toml::key &ky, const TomlBasicValue &value) {
static std::string format_key_message(const std::string &message, const toml::key &ky, const TomlBasicValue &value) {
auto loc = value.location();
auto line_number_str = std::to_string(loc.line());
auto line_width = line_number_str.length();
auto line_str = loc.line_str();
std::ostringstream oss;
oss << "[error] " << error << '\n';
oss << message << "\n";
oss << " --> " << loc.file_name() << ':' << loc.line() << '\n';
oss << std::string(line_width + 2, ' ') << "|\n";
@ -48,6 +57,14 @@ static std::string format_key_error(const std::string &error, const toml::key &k
return oss.str();
}
static void throw_key_error(const std::string &error, const toml::key &ky, const TomlBasicValue &value) {
throw std::runtime_error(format_key_message("[error] " + error, ky, value));
}
static void print_key_warning(const std::string &message, const toml::key &ky, const TomlBasicValue &value) {
puts(format_key_message("[warning] " + message, ky, value).c_str());
}
class TomlChecker {
const TomlBasicValue &m_v;
tsl::ordered_set<toml::key> m_visited;
@ -124,27 +141,27 @@ class TomlChecker {
const auto &ky = itr.first;
if (m_conditionVisited.contains(ky)) {
if (!conditions.contains(ky)) {
throw std::runtime_error(format_key_error("Unknown condition '" + ky + "'", ky, itr.second));
throw_key_error("Unknown condition '" + ky + "'", ky, itr.second);
}
for (const auto &jtr : itr.second.as_table()) {
if (!m_visited.contains(jtr.first)) {
throw std::runtime_error(format_key_error("Unknown key '" + jtr.first + "'", jtr.first, jtr.second));
throw_key_error("Unknown key '" + jtr.first + "'", jtr.first, jtr.second);
}
}
} else if (!m_visited.contains(ky)) {
if (itr.second.is_table()) {
for (const auto &jtr : itr.second.as_table()) {
if (!m_visited.contains(jtr.first)) {
throw std::runtime_error(format_key_error("Unknown key '" + jtr.first + "'", jtr.first, jtr.second));
throw_key_error("Unknown key '" + jtr.first + "'", jtr.first, jtr.second);
}
}
}
throw std::runtime_error(format_key_error("Unknown key '" + ky + "'", ky, itr.second));
throw_key_error("Unknown key '" + ky + "'", ky, itr.second);
} else if (ky == "condition") {
std::string condition = itr.second.as_string();
if (!conditions.contains(condition)) {
throw std::runtime_error(format_key_error("Unknown condition '" + condition + "'", condition, itr.second));
throw_key_error("Unknown condition '" + condition + "'", condition, itr.second);
}
}
}
@ -182,7 +199,7 @@ class TomlCheckerRoot {
if (check_root) {
for (const auto &itr : m_root.as_table()) {
if (!m_visisted.contains(itr.first)) {
throw std::runtime_error(format_key_error("Unknown key '" + itr.first + "'", itr.first, itr.second));
throw_key_error("Unknown key '" + itr.first + "'", itr.first, itr.second);
}
}
}
@ -192,7 +209,7 @@ class TomlCheckerRoot {
}
};
Project::Project(const Project *parent, const std::string &path, bool build) {
Project::Project(const Project *parent, const std::string &path, bool build) : parent(parent) {
const auto toml_path = fs::path(path) / "cmake.toml";
if (!fs::exists(toml_path)) {
throw std::runtime_error("File not found '" + toml_path.string() + "'");
@ -210,7 +227,7 @@ Project::Project(const Project *parent, const std::string &path, bool build) {
cmake.required("version", cmake_version);
if (cmake.contains("bin-dir")) {
throw std::runtime_error("bin-dir has been renamed to build-dir");
throw_key_error("bin-dir has been renamed to build-dir", "bin-dir", cmake.find("bin-dir"));
}
cmake.optional("build-dir", build_dir);
@ -276,6 +293,21 @@ Project::Project(const Project *parent, const std::string &path, bool build) {
project.optional("include-before", include_before);
project.optional("include-after", include_after);
project.optional("subdirs", project_subdirs);
std::string msvc_runtime;
project.optional("msvc-runtime", msvc_runtime);
if (!msvc_runtime.empty()) {
project_msvc_runtime = parse_msvcRuntimeType(msvc_runtime);
if (project_msvc_runtime == msvc_last) {
std::string error = "Unknown runtime '" + msvc_runtime + "'\n";
error += "Available types:\n";
for (std::string type_name : msvcRuntimeTypeNames) {
error += " - " + type_name + "\n";
}
error.pop_back(); // Remove last newline
throw_key_error(error, msvc_runtime, project.find("msvc-runtime"));
}
}
}
if (checker.contains("subdir")) {
@ -295,11 +327,21 @@ Project::Project(const Project *parent, const std::string &path, bool build) {
}
}
if (checker.contains("settings")) {
if (checker.contains("variables")) {
using set_map = tsl::ordered_map<std::string, TomlBasicValue>;
const auto &sets = toml::find<set_map>(toml, "settings");
for (const auto &itr : sets) {
Setting s;
auto vars = toml::find<set_map>(toml, "variables");
if (checker.contains("settings")) {
print_key_warning("[settings] has been renamed to [variables]", "settings", toml.at("settings"));
const auto &sets = toml::find<set_map>(toml, "settings");
for (const auto &itr : sets) {
if (!vars.insert(itr).second) {
throw_key_error("Key '" + itr.first + "' shadows existing variable", itr.first, itr.second);
}
}
}
for (const auto &itr : vars) {
Variable s;
s.name = itr.first;
const auto &value = itr.second;
if (value.is_boolean()) {
@ -320,7 +362,7 @@ Project::Project(const Project *parent, const std::string &path, bool build) {
setting.optional("cache", s.cache);
setting.optional("force", s.force);
}
settings.push_back(s);
variables.push_back(s);
}
}
@ -430,7 +472,7 @@ Project::Project(const Project *parent, const std::string &path, bool build) {
} else if (is_cmake_arg(key)) {
// allow passthrough of ExternalProject options
} else if (!c.visisted(key)) {
throw std::runtime_error(format_key_error("Unknown key '" + argItr.first + "'", argItr.first, argItr.second));
throw_key_error("Unknown key '" + argItr.first + "'", argItr.first, argItr.second);
}
// Make sure not to emit keys like "condition" in the FetchContent call
@ -445,7 +487,7 @@ Project::Project(const Project *parent, const std::string &path, bool build) {
}
if (checker.contains("bin")) {
throw std::runtime_error("[[bin]] has been renamed to [[target]]");
throw_key_error("[[bin]] has been renamed to [target.<name>]", "", toml.at("bin"));
}
auto parse_target = [&](const std::string &name, TomlChecker &t, bool isTemplate) {
@ -484,12 +526,21 @@ Project::Project(const Project *parent, const std::string &path, bool build) {
}
}
error.pop_back(); // Remove last newline
throw std::runtime_error(format_key_error(error, target.type_name, t.find("type")));
throw_key_error(error, target.type_name, t.find("type"));
}
t.optional("headers", target.headers);
t.optional("sources", target.sources);
// Merge the headers into the sources
ConditionVector headers;
t.optional("headers", headers);
for (const auto &itr : headers) {
auto &dest = target.sources[itr.first];
for (const auto &jtr : itr.second) {
dest.push_back(jtr);
}
}
t.optional("compile-definitions", target.compile_definitions);
t.optional("private-compile-definitions", target.private_compile_definitions);
@ -514,10 +565,32 @@ Project::Project(const Project *parent, const std::string &path, bool build) {
t.optional("precompile-headers", target.precompile_headers);
t.optional("private-precompile-headers", target.private_precompile_headers);
if (!target.headers.empty()) {
auto &sources = target.sources.nth(0).value();
const auto &headers = target.headers.nth(0)->second;
sources.insert(sources.end(), headers.begin(), headers.end());
Condition<std::string> msvc_runtime;
t.optional("msvc-runtime", msvc_runtime);
for (const auto &condItr : msvc_runtime) {
switch (parse_msvcRuntimeType(condItr.second)) {
case msvc_dynamic:
target.properties[condItr.first]["MSVC_RUNTIME_LIBRARY"] = "MultiThreaded$<$<CONFIG:Debug>:Debug>DLL";
break;
case msvc_static:
target.properties[condItr.first]["MSVC_RUNTIME_LIBRARY"] = "MultiThreaded$<$<CONFIG:Debug>:Debug>";
break;
default: {
std::string error = "Unknown runtime '" + condItr.second + "'\n";
error += "Available types:\n";
for (std::string type_name : msvcRuntimeTypeNames) {
error += " - " + type_name + "\n";
}
error.pop_back(); // Remove last newline
const TomlBasicValue *report;
if (condItr.first.empty()) {
report = &t.find("msvc-runtime");
} else {
report = &t.find(condItr.first).as_table().find("msvc-runtime").value();
}
throw_key_error(error, condItr.second, *report);
}
}
}
t.optional("condition", target.condition);
@ -571,13 +644,13 @@ Project::Project(const Project *parent, const std::string &path, bool build) {
for (const auto &type_name : targetTypeNames) {
if (name == type_name) {
throw std::runtime_error(format_key_error("Reserved template name '" + name + "'", name, itr.second));
throw_key_error("Reserved template name '" + name + "'", name, itr.second);
}
}
for (const auto &tmplate : templates) {
if (name == tmplate.outline.name) {
throw std::runtime_error(format_key_error("Template '" + name + "' already defined", name, itr.second));
throw_key_error("Template '" + name + "' already defined", name, itr.second);
}
}
@ -652,7 +725,7 @@ Project::Project(const Project *parent, const std::string &path, bool build) {
package.features.emplace_back(feature);
}
} else {
throw std::runtime_error("Invalid vcpkg package '" + package_str + "'");
throw_key_error("Invalid package name '" + package_str + "'", "packages", p);
}
vcpkg.packages.emplace_back(std::move(package));
}
@ -661,6 +734,13 @@ Project::Project(const Project *parent, const std::string &path, bool build) {
checker.check(conditions, true);
}
const Project *Project::root() const {
auto root = this;
while (root->parent != nullptr)
root = root->parent;
return root;
}
bool is_root_path(const std::string &path) {
const auto toml_path = fs::path(path) / "cmake.toml";
if (!fs::exists(toml_path)) {

12
tests/CMakeLists.txt generated

@ -88,3 +88,15 @@ add_test(
build
)
if(MSVC) # msvc
add_test(
NAME
msvc-runtime
WORKING_DIRECTORY
"${CMAKE_CURRENT_LIST_DIR}/msvc-runtime"
COMMAND
"$<TARGET_FILE:cmkr>"
build
)
endif()

@ -45,3 +45,10 @@ name = "templates"
working-directory = "templates"
command = "$<TARGET_FILE:cmkr>"
arguments = ["build"]
[[test]]
condition = "msvc"
name = "msvc-runtime"
working-directory = "msvc-runtime"
command = "$<TARGET_FILE:cmkr>"
arguments = ["build"]

@ -4,5 +4,5 @@
int main()
{
printf("mylib version: %s\n", mylib::version())
}
printf("mylib version: %s\n", mylib::version());
}

@ -0,0 +1,15 @@
[project]
name = "msvc-runtime"
description = "Static MSVC runtime"
msvc-runtime = "static"
# This target will compile with a static runtime
[target.static-runtime]
type = "executable"
sources = ["src/main.cpp"]
# This target overrides the [project].msvc-runtime
[target.dynamic-runtime]
type = "executable"
sources = ["src/main.cpp"]
msvc-runtime = "dynamic"

@ -0,0 +1,5 @@
#include <cstdio>
int main() {
puts("Hello from cmkr(msvc-static)!");
}

@ -7,7 +7,7 @@ description = "Dependencies from vcpkg"
# See https://github.com/microsoft/vcpkg/releases for vcpkg versions
# See https://vcpkg.io/en/packages.html for available packages
[vcpkg]
version = "2021.05.12"
version = "2022.11.14"
packages = ["fmt"]
[find-package]

Loading…
Cancel
Save