Compare commits

...

67 Commits

Author SHA1 Message Date
Duncan Ogilvie 58c7de1d60
Merge pull request #129 from build-cpp/gitignore-improvement
1 year ago
Duncan Ogilvie a61e4bb999
Merge pull request #128 from build-cpp/language-documentation
1 year ago
Duncan Ogilvie 47aaeaf8ce Document the supported languages
1 year ago
Duncan Ogilvie 62609a0088 Improve the default .gitignore that's generated
1 year ago
Duncan Ogilvie e0d8a085db
Merge pull request #121 from anthonyprintup/relative-paths
1 year ago
Duncan Ogilvie 56da4144ae
Merge pull request #125 from anthonyprintup/fix-124
1 year ago
Duncan Ogilvie 9388b3f29d
Merge pull request #123 from anthonyprintup/quotes-fix
1 year ago
Anthony Printup 079644cfeb
fix(generator): Prevent calling `std::string::back` on an empty string
1 year ago
Anthony Printup a7ca9f04c0
fix(relative-paths): Replaced `has_parent_path` with a simpler path check
1 year ago
Anthony Printup 339c2aac94
fix(generator): Prevent command arguments from being quoted when generating tests
1 year ago
Anthony Printup c573c9a76d
test(relative-paths): Add the test-library file to libs
1 year ago
Anthony Printup b24a0a2fdc
test(relative-paths): Make `link-libraries` conditional to the Windows platform
1 year ago
Anthony Printup 854e8817c4
test(relative-paths): Remove inlined CMake from cmake.toml
1 year ago
Anthony Printup 1c2947b2fc
fix(relative-paths): Prevent processing paths which may contain CMake macros, added a check to see if the library that's being linked exists on disk
1 year ago
Anthony Printup 19e61aa1aa
test(relative-paths): Attempt to fix the tests workflow
1 year ago
Anthony Printup bfa1e1fe0d
test(relative-paths): Added the latest `relative-paths` test to the tests workflow
1 year ago
Anthony Printup 56bd78f7ad
feat(project-parser): Added support for relative paths in `link-libraries`
1 year ago
Anthony Printup 4362b3547f
test(relative-paths): Added tests for #116
1 year ago
Duncan Ogilvie 08204d9847 Bump to 0.2.25
1 year ago
Duncan Ogilvie 2202aa8173
Merge pull request #120 from anthonyprintup/new-lines-fix
1 year ago
Anthony Printup d3829fb6cd Trim additional new lines at the end of the generated CMakeLists.txt
1 year ago
Duncan Ogilvie 3fb095210a
Merge pull request #118 from ZehMatt/vcpkg-errors
1 year ago
ζeh Matt 9f99365a25
Report better error messages for vcpkg projects and features
1 year ago
Duncan Ogilvie be387141b5 Add a 'Compiler flags' example to the documentation
1 year ago
Duncan Ogilvie 09ded38f70
Merge pull request #115 from build-cpp/clang-cl-condition
1 year ago
Duncan Ogilvie ad90b1ad3e Add 'clang-cl' and 'clang-any' conditions
1 year ago
Duncan Ogilvie 5bfa03d311 Bump to 0.2.24
2 years ago
Duncan Ogilvie bdd9d393d6
Merge pull request #113 from build-cpp/editorconfig
2 years ago
Duncan Ogilvie 1deaec2b41 Add .editorconfig and linting
2 years ago
Duncan Ogilvie cfee3bbc14
Merge pull request #111 from anthonyprintup/fetch-content-system
2 years ago
Anthony Printup bcaa60cabe
feat: Added SYSTEM flag support to fetch-content
2 years ago
Duncan Ogilvie 7b1d33395e Bump to 0.2.23
2 years ago
Duncan Ogilvie 240fafc102
Merge pull request #101 from build-cpp/minor-cleanup
2 years ago
Duncan Ogilvie 7a7ddf2699 Speed up the pipeline by using Ninja
2 years ago
Duncan Ogilvie 605a04e72d Fix formatting of files
2 years ago
Duncan Ogilvie fd7f078127 Add workflow for checking clang-format
2 years ago
Duncan Ogilvie ee0e0d71d9 Remove a bunch of dead code
2 years ago
Duncan Ogilvie 9d6897b572 Suppress warnings related to DOWNLOAD_EXTRACT_TIMESTAMP
2 years ago
Duncan Ogilvie f8c34e5545 Error when [vcpkg] is used from a non-root project
2 years ago
Duncan Ogilvie e6d783ea20 Fix a few clang-tidy warnings
2 years ago
Duncan Ogilvie ee7fe38a7b
Merge pull request #100 from build-cpp/include-subdir-bugfix
2 years ago
Duncan Ogilvie 66b2aad843 Fix a bug where includes in a subdir would not work
2 years ago
Duncan Ogilvie d2e376080a Bump to 0.2.22
2 years ago
Duncan Ogilvie 634908c651
Merge pull request #97 from build-cpp/cmkr-init-regression
2 years ago
Duncan Ogilvie 33911f3f1b Fix a regression where cmkr init wouldn't create any source files
2 years ago
Duncan Ogilvie e650073bef Bump to 0.2.21
2 years ago
Duncan Ogilvie 96e44f7771
Merge pull request #96 from build-cpp/check-sources
2 years ago
Duncan Ogilvie cb9ad5cdb3 Improve error messages and check if specified sources exist
2 years ago
Duncan Ogilvie c785b540d3 Document target templates
2 years ago
Duncan Ogilvie 275b73eefa Remove debug print
2 years ago
Duncan Ogilvie ea49a3acb7 Bump to 0.2.20
2 years ago
Duncan Ogilvie 257e14c869
Merge pull request #93 from build-cpp/gitattributes
2 years ago
Duncan Ogilvie 7508816fe7
Merge pull request #94 from build-cpp/user-experience
2 years ago
Duncan Ogilvie 45678bc5bc Automatically generate .gitattributes and .gitignore with cmkr init
2 years ago
Duncan Ogilvie b869e5646f Add a special "root" value for [options]
2 years ago
Duncan Ogilvie a499df84b1 Bump to 0.2.19
2 years ago
Duncan Ogilvie 49440f99e0
Merge pull request #92 from build-cpp/improved-options
2 years ago
Duncan Ogilvie 0c19c3da0c Improve the code related to [options]
2 years ago
Duncan Ogilvie 233cadadd0
Merge pull request #90 from mike1k/sanity
2 years ago
Duncan Ogilvie 58a5a935e5 Add a bunch of missing languages and extensions and refactor
2 years ago
Duncan Ogilvie e69427bf94
Merge pull request #91 from build-cpp/clang-condition
2 years ago
Duncan Ogilvie bc6359805d Change the `clang` condition to not detect clang-cl
2 years ago
Duncan Ogilvie 8dcc11e349 Add helper function Project::cmake_minimum_version for version-dependent features
2 years ago
Duncan Ogilvie 51dc49e6f1 Improve CI performance for non-tagged builds
2 years ago
mike 7d27c28b59 Fixed failing tests due to implicit project languages (internal list empty)
2 years ago
Duncan Ogilvie ae6bea3b58 Improve reference documentation
2 years ago
Duncan Ogilvie 5a0eda7abc Automatically generate release notes
2 years ago

@ -72,7 +72,7 @@ PenaltyExcessCharacter: 1000000
PenaltyReturnTypeOnItsOwnLine: 60
PointerAlignment: Right
ReflowComments: true
SortIncludes: true
SortIncludes: false
SortUsingDeclarations: true
SpaceAfterCStyleCast: false
SpaceAfterTemplateKeyword: true

@ -0,0 +1,24 @@
root = true
[*]
end_of_line = lf
trim_trailing_whitespace = true
charset = utf-8
[*.{c,h,cpp,hpp,toml}]
indent_style = space
insert_final_newline = true
[{CMakeLists.txt, *.cmake}]
indent_style = tab
tab_width = 8
insert_final_newline = true
# Exclude the third_party folder
[/third_party/**]
charset = unset
end_of_line = unset
insert_final_newline = unset
trim_trailing_whitespace = unset
indent_style = unset
indent_size = unset

@ -1,21 +1,28 @@
name: CMake
name: build
on: [push, pull_request]
jobs:
build:
cmake:
# Skip building pull requests from the same repository
if: ${{ github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository) }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [windows-2019, macos-10.15, ubuntu-20.04]
os: [windows-2022, macos-11, ubuntu-20.04]
env:
BUILD_TYPE: Release
BUILD_TYPE: 'Release'
CMAKE_GENERATOR: 'Ninja'
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Install Ninja
uses: seanmiddleditch/gha-setup-ninja@6263846cf3c17009dfc81604efabae16044fc074 # master
- name: Visual Studio Development Environment
uses: ilammy/msvc-dev-cmd@cec98b9d092141f74527d0afa6feb2af698cfe89 # v1.12.1
- name: Tag cmkr.cmake
if: ${{ startsWith(github.ref, 'refs/tags/') }}
@ -38,28 +45,31 @@ jobs:
ctest -C ${{ env.BUILD_TYPE }} --verbose
- name: Upload artifacts
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
with:
name: ${{ github.event.repository.name }}-${{ matrix.os }}
path: install/bin/*
- name: Get lowercase OS name
id: osname
uses: ASzc/change-string-case-action@v1
uses: ASzc/change-string-case-action@07c1e24a97f0951e13f88870b99c058fcf0b14cf # v5
if: ${{ startsWith(github.ref, 'refs/tags/') }}
with:
string: ${{ runner.os }}
- name: Compress artifacts
uses: vimtor/action-zip@v1
uses: vimtor/action-zip@26a249fb00d43ca98dad77a4b3838025fc226aa1 # v1.1
if: ${{ startsWith(github.ref, 'refs/tags/') }}
with:
files: install/bin/
dest: ${{ github.event.repository.name }}-${{ steps.osname.outputs.lowercase }}.zip
- name: Release
uses: softprops/action-gh-release@v1
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v0.1.15
if: ${{ startsWith(github.ref, 'refs/tags/') }}
with:
prerelease: ${{ !startsWith(github.ref, 'refs/tags/v') || contains(github.ref, '-pre') }}
files: ${{ github.event.repository.name }}-${{ steps.osname.outputs.lowercase }}.zip
generate_release_notes: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

@ -0,0 +1,37 @@
name: lint
on: [push]
jobs:
clang-format:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: clang-format
id: clang-format
uses: DoozyX/clang-format-lint-action@c3b2c943e924028b93a707a5b1b017976ab8d50c # v0.15
with:
exclude: './third_party'
extensions: 'c,h,cpp,hpp'
clangFormatVersion: 15
style: file
- name: clang-format instructions
if: ${{ failure() && steps.clang-format.outcome == 'failure' }}
run: |
# Instructions for fixing the formatting errors
echo -e "\n\033[0;31mTo fix the formatting, run:\nclang-format -style=file -i \$(git ls-files \"*.c\" \"*.h\" \"*.cpp\" \"*.hpp\")\033[0m\n"
exit 1
editorconfig:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Run editorconfig-checker
uses: editorconfig-checker/action-editorconfig-checker@d4fca16fc71adef10fbe101903b654449fa9570c # master 2022-03-15

1
.gitignore vendored

@ -10,3 +10,4 @@ build*/
cmake-build*/
CMakeLists.txt.user
.vscode/
.DS_Store

4
CMakeLists.txt generated

@ -22,7 +22,7 @@ project(cmkr
LANGUAGES
CXX
VERSION
0.2.18
0.2.25
DESCRIPTION
"CMakeLists generator from TOML"
)
@ -61,14 +61,12 @@ set(cmkr_SOURCES
"src/arguments.cpp"
"src/build.cpp"
"src/cmake_generator.cpp"
"src/error.cpp"
"src/help.cpp"
"src/main.cpp"
"src/project_parser.cpp"
"include/arguments.hpp"
"include/build.hpp"
"include/cmake_generator.hpp"
"include/error.hpp"
"include/fs.hpp"
"include/help.hpp"
"include/literals.hpp"

@ -4,7 +4,7 @@ cmkr-include = false
[project]
name = "cmkr"
version = "0.2.18"
version = "0.2.25"
description = "CMakeLists generator from TOML"
languages = ["CXX"]
include-after = [

@ -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.18" CACHE STRING "cmkr git tag (this needs to be available forever)" FORCE)
set(CMKR_TAG "v0.2.25" 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

@ -39,6 +39,32 @@ include-before = ["cmake/before-project.cmake"]
include-after = ["cmake/after-project.cmake"]
```
### Languages
Supported languages are (see [`enable_language`](https://cmake.org/cmake/help/latest/command/enable_language.html) for more information):
- `C`
- `CXX` → C++
- `CSharp` → C#
- `CUDA`
- `OBJC` → Objective-C
- `OBJCXX` → Objective-C++
- `Fortran`
- `HIP`
- `ISPC`
- `Swift`
- `ASM`
- `ASM_MASM` → [Microsoft Macro Assembler (MASM)](https://learn.microsoft.com/en-US/cpp/assembler/masm/masm-for-x64-ml64-exe)
- `ASM_NASM` → [Netwide Assembler (NASM)](https://www.nasm.us)
- `ASM_MARMASM` [Microsoft ARM Assembler](https://learn.microsoft.com/en-us/cpp/assembler/arm/arm-assembler-command-line-reference)
- `ASM-ATT`
- `Java` (undocumented)
- `RC` (undocumented)
After a language is enabled, adding sources files with the corresponding extension to your target will automatically use the appropriate compiler/assembler for it.
_Note_: It is generally discouraged to disable the `C` language, unless you are absolutely sure it is not used. Sometimes projects added with `fetch-content` implicitly require it and the error messages can be extremely confusing.
## Conditions
You can specify your own conditions and use them in any `condition` field:
@ -72,8 +98,10 @@ unix = "UNIX"
bsd = "CMAKE_SYSTEM_NAME MATCHES \"BSD\""
linux = "CMAKE_SYSTEM_NAME MATCHES \"Linux\""
gcc = "CMAKE_CXX_COMPILER_ID STREQUAL \"GNU\" OR CMAKE_C_COMPILER_ID STREQUAL \"GNU\""
clang = "CMAKE_CXX_COMPILER_ID MATCHES \"Clang\" OR CMAKE_C_COMPILER_ID MATCHES \"Clang\""
msvc = "MSVC"
clang = "(CMAKE_CXX_COMPILER_ID MATCHES \"Clang\" AND NOT CMAKE_CXX_COMPILER_FRONTEND_VARIANT MATCHES \"^MSVC$\") OR (CMAKE_C_COMPILER_ID MATCHES \"Clang\" AND NOT CMAKE_C_COMPILER_FRONTEND_VARIANT MATCHES \"^MSVC$\")"
clang-cl = "(CMAKE_CXX_COMPILER_ID MATCHES \"Clang\" AND CMAKE_CXX_COMPILER_FRONTEND_VARIANT MATCHES \"^MSVC$\") OR (CMAKE_C_COMPILER_ID MATCHES \"Clang\" AND CMAKE_C_COMPILER_FRONTEND_VARIANT MATCHES \"^MSVC$\")"
clang-any = "CMAKE_CXX_COMPILER_ID MATCHES \"Clang\" OR CMAKE_C_COMPILER_ID MATCHES \"Clang\""
root = "CMKR_ROOT_PROJECT"
x64 = "CMAKE_SIZEOF_VOID_P EQUAL 8"
x32 = "CMAKE_SIZEOF_VOID_P EQUAL 4"
@ -94,6 +122,19 @@ include-before = ["cmake/before-subdir.cmake"]
include-after = ["cmake/after-subdir.cmake"]
```
## Options
```toml
[options]
MYPROJECT_BUILD_TESTS = false
MYPROJECT_SPECIAL_OPTION = { value = true, help = "Docstring for this option." }
MYPROJECT_BUILD_EXAMPLES = "root"
```
Options correspond to [CMake cache variables](https://cmake.org/cmake/help/book/mastering-cmake/chapter/CMake%20Cache.html) that can be used to customize your project at configure-time. You can configure with `cmake -DMYPROJECT_BUILD_TESTS=ON` to enable the option. Every option automatically gets a corresponding [condition](#conditions).
The special value `root` can be used to set the option to `true` if the project is compiled as the root project (it will be `false` if someone is including your project via `[fetch-content]` or `[subdir]`).
## Variables
```toml
@ -137,6 +178,8 @@ components = ["mycomponent"]
condition = "mycondition"
git = "https://github.com/myuser/gitcontent"
tag = "v0.1"
shallow = false
system = false
[fetch-content.svncontent]
condition = "mycondition"
@ -146,7 +189,10 @@ rev = "svn_rev"
[fetch-content.urlcontent]
condition = "mycondition"
url = "https://content-host.com/urlcontent.zip"
hash = "123123123123"
# These are equivalent, supported algorithms:
# md5, sha1, sha224, sha256, sha384, sha512, sha3_224, sha3_256, sha3_384, sha3_512
hash = "SHA1 502a4e25b8b209889c99c7fa0732102682c2e4ff"
sha1 = "502a4e25b8b209889c99c7fa0732102682c2e4ff"
```
## Targets
@ -195,6 +241,29 @@ CXX_STANDARD_REQUIRED = true
FOLDER = "MyFolder"
```
## Templates
To avoid repeating yourself you can create your own target type and use it in your targets:
```toml
[template.example]
condition = "MYPROJECT_BUILD_EXAMPLES"
type = "executable"
link-libraries = ["myproject::mylib"]
add-function = ""
pass-sources = false
# Properties from the template are merged with the ones here
[target.myexample]
type = "example"
sources = ["src/myexample.cpp"]
```
The properties declared on a `template` are the same as the ones you use for targets. The only exceptions are:
- `add-function`: Specifies a custom add function. Projects like [pybind11](https://pybind11.readthedocs.io/en/stable/cmake/index.html#new-findpython-mode) have their own `add_xxx` function, which you can specify here.
- `pass-sources`: Pass sources directly to the add function instead of using `target_sources`.
## Tests and installation (unfinished)
**Note**: The `[[test]]` and `[[install]]` are unfinished features and will likely change in a future release.

@ -0,0 +1,31 @@
---
# Automatically generated from tests/compile-options/cmake.toml - DO NOT EDIT
layout: default
title: Compiler flags
permalink: /examples/compile-options
parent: Examples
nav_order: 9
---
# Compiler flags
Example project that sets compiler/linker flags for various platforms.
```toml
[project]
name = "compile-options"
description = "Compiler flags"
[target.hello]
type = "executable"
sources = ["src/main.cpp"]
msvc.compile-options = ["/W2"]
gcc.compile-options = ["-Wall"]
clang.compile-options = ["-Wall"]
```
The `hello` target uses [conditions](/cmake-toml#conditions) to set different compiler flags depending on the platform. See the [targets](/cmake-toml/#targets) documentation for other things you can set.
_Note_: In general you only want to specify flags _required_ to compile your code without errors.
<sup><sub>This page was automatically generated from [tests/compile-options/cmake.toml](https://github.com/build-cpp/cmkr/tree/main/tests/compile-options/cmake.toml).</sub></sup>

@ -11,9 +11,3 @@ int install();
} // namespace build
} // namespace cmkr
int cmkr_build_run(int argc, char **argv);
int cmkr_build_clean();
int cmkr_build_install();

@ -1,27 +0,0 @@
#pragma once
namespace cmkr {
namespace error {
struct Status {
enum class Code {
Success = 0,
RuntimeError,
InitError,
GenerationError,
BuildError,
CleanError,
InstallError,
};
Status(Code ec) noexcept;
operator int() const noexcept;
Code code() const noexcept;
private:
Code ec_ = Code::Success;
};
} // namespace error
} // namespace cmkr
const char *cmkr_error_status_string(int);

@ -1,8 +1,6 @@
#pragma once
#if ((defined(_MSVC_LANG) && _MSVC_LANG >= 201703L) || \
(defined(__cplusplus) && __cplusplus >= 201703L)) && \
defined(__has_include)
#if ((defined(_MSVC_LANG) && _MSVC_LANG >= 201703L) || (defined(__cplusplus) && __cplusplus >= 201703L)) && defined(__has_include)
#if __has_include(<filesystem>) && (!defined(__MAC_OS_X_VERSION_MIN_REQUIRED) || __MAC_OS_X_VERSION_MIN_REQUIRED >= 101500)
#define GHC_USE_STD_FS
#include <filesystem>

@ -9,7 +9,3 @@ const char *message() noexcept;
} // namespace help
} // namespace cmkr
const char *cmkr_help_version(void);
const char *cmkr_help_message(void);

@ -1,10 +1,11 @@
#pragma once
#include <mpark/variant.hpp>
#include <vector>
#include <string>
#include <mpark/variant.hpp>
#include <tsl/ordered_map.h>
#include <tsl/ordered_set.h>
#include <vector>
namespace cmkr {
namespace parser {
@ -16,16 +17,16 @@ using ConditionVector = Condition<std::vector<std::string>>;
struct Variable {
std::string name;
std::string comment;
mpark::variant<bool, std::string> val;
std::string help;
mpark::variant<bool, std::string> value;
bool cache = false;
bool force = false;
};
struct Option {
std::string name;
std::string comment;
bool val = false;
std::string help;
mpark::variant<bool, std::string> value;
};
struct Package {
@ -47,6 +48,10 @@ struct Vcpkg {
};
std::vector<Package> packages;
bool enabled() const {
return !packages.empty();
}
};
enum TargetType {
@ -150,6 +155,7 @@ struct Content {
Condition<std::string> cmake_after;
ConditionVector include_before;
ConditionVector include_after;
bool system;
};
enum MsvcRuntimeType {
@ -180,6 +186,7 @@ struct Project {
std::string project_version;
std::string project_description;
std::vector<std::string> project_languages;
bool project_allow_unknown_languages = false;
MsvcRuntimeType project_msvc_runtime = msvc_last;
Condition<std::string> cmake_before;
Condition<std::string> cmake_after;
@ -199,6 +206,7 @@ struct Project {
Project(const Project *parent, const std::string &path, bool build);
const Project *root() const;
bool cmake_minimum_version(int major, int minor) const;
};
bool is_root_path(const std::string &path);

@ -2,10 +2,8 @@
#include "build.hpp"
#include "cmake_generator.hpp"
#include "help.hpp"
#include "fs.hpp"
#include <exception>
#include <iostream>
#include <stdexcept>
#include <string>
#include <vector>

@ -1,14 +1,10 @@
#include "build.hpp"
#include "cmake_generator.hpp"
#include "error.hpp"
#include "project_parser.hpp"
#include "fs.hpp"
#include <cstddef>
#include <cstdlib>
#include <sstream>
#include <stdexcept>
#include <system_error>
namespace cmkr {
namespace build {
@ -17,7 +13,7 @@ int run(int argc, char **argv) {
parser::Project project(nullptr, ".", true);
if (argc > 2) {
for (int i = 2; i < argc; ++i) {
project.build_args.push_back(argv[i]);
project.build_args.emplace_back(argv[i]);
}
}
std::stringstream ss;
@ -48,13 +44,13 @@ int run(int argc, char **argv) {
}
int clean() {
bool ret = false;
bool success = false;
parser::Project project(nullptr, ".", true);
if (fs::exists(project.build_dir)) {
ret = fs::remove_all(project.build_dir);
success = fs::remove_all(project.build_dir);
fs::create_directory(project.build_dir);
}
return !ret;
return success ? EXIT_SUCCESS : EXIT_FAILURE;
}
int install() {
@ -64,33 +60,3 @@ int install() {
}
} // namespace build
} // namespace cmkr
int cmkr_build_run(int argc, char **argv) {
try {
return cmkr::build::run(argc, argv);
} catch (const std::system_error &e) {
return e.code().value();
} catch (...) {
return cmkr::error::Status(cmkr::error::Status::Code::BuildError);
}
}
int cmkr_build_clean(void) {
try {
return cmkr::build::clean();
} catch (const std::system_error &e) {
return e.code().value();
} catch (...) {
return cmkr::error::Status(cmkr::error::Status::Code::CleanError);
}
}
int cmkr_build_install(void) {
try {
return cmkr::build::install();
} catch (const std::system_error &e) {
return e.code().value();
} catch (...) {
return cmkr::error::Status(cmkr::error::Status::Code::InstallError);
}
}

@ -1,20 +1,48 @@
#include "cmake_generator.hpp"
#include "error.hpp"
#include "literals.hpp"
#include <resources/cmkr.hpp>
#include "fs.hpp"
#include "project_parser.hpp"
#include <cstdio>
#include <fstream>
#include <memory>
#include <sstream>
#include <stdexcept>
#include <fstream>
namespace cmkr {
namespace gen {
static std::string format(const char *format, tsl::ordered_map<std::string, std::string> variables) {
/*
Location: CMake/share/cmake-3.26/Modules
rg "set\(CMAKE_(.+)_SOURCE_FILE_EXTENSIONS"
Links:
- https://gitlab.kitware.com/cmake/cmake/-/issues/24340
- https://cmake.org/cmake/help/latest/command/enable_language.html
*/
static tsl::ordered_map<std::string, std::vector<std::string>> known_languages = {
{"ASM", {".s", ".S", ".asm", ".abs", ".msa", ".s90", ".s43", ".s85", ".s51"}},
{"ASM-ATT", {".s", ".asm"}},
{"ASM_MARMASM", {".asm"}},
{"ASM_MASM", {".asm"}},
{"ASM_NASM", {".nasm", ".asm"}},
{"C", {".c", ".m"}},
{"CSharp", {".cs"}},
{"CUDA", {".cu"}},
{"CXX", {".C", ".M", ".c++", ".cc", ".cpp", ".cxx", ".m", ".mm", ".mpp", ".CPP", ".ixx", ".cppm"}},
{"Fortran", {".f", ".F", ".fpp", ".FPP", ".f77", ".F77", ".f90", ".F90", ".for", ".For", ".FOR", ".f95", ".F95", ".cuf", ".CUF"}},
{"HIP", {".hip"}},
{"ISPC", {".ispc"}},
{"Java", {".java"}},
{"OBJC", {".m"}},
{"OBJCXX", {".M", ".m", ".mm"}},
{"RC", {".rc", ".RC"}},
{"Swift", {".swift"}},
};
static std::string format(const char *format, const tsl::ordered_map<std::string, std::string> &variables) {
std::string s = format;
for (const auto &itr : variables) {
size_t start_pos = 0;
@ -90,6 +118,19 @@ static void create_file(const fs::path &path, const std::string &contents) {
ofs << contents;
}
static std::string read_file(const fs::path &path) {
std::ifstream ifs(path, std::ios::binary);
if (!ifs) {
throw std::runtime_error("Failed to read " + path.string());
}
std::string contents;
ifs.seekg(0, std::ios::end);
contents.resize(ifs.tellg());
ifs.seekg(0, std::ios::beg);
ifs.read(&contents[0], contents.size());
return contents;
}
// CMake target name rules: https://cmake.org/cmake/help/latest/policy/CMP0037.html [A-Za-z0-9_.+\-]
// TOML bare keys: non-empty strings composed only of [A-Za-z0-9_-]
// We replace all non-TOML bare key characters with _
@ -106,18 +147,82 @@ static std::string escape_project_name(const std::string &name) {
return escaped;
}
static void generate_gitfile(const char *gitfile, const std::vector<std::string> &desired_lines) {
// Reference: https://github.com/github/linguist/blob/master/docs/overrides.md#summary
auto lines = desired_lines;
auto generate = [&lines](const char *newline) {
std::string generated;
generated += "# cmkr";
generated += newline;
for (const auto &line : lines) {
generated += line;
generated += newline;
}
return generated;
};
if (!fs::exists(gitfile)) {
create_file(gitfile, generate("\n"));
} else {
auto contents = read_file(gitfile);
std::string line;
auto cr = 0, lf = 0;
auto flush_line = [&line, &lines]() {
auto itr = std::find(lines.begin(), lines.end(), line);
if (itr != lines.end()) {
lines.erase(itr);
}
line.clear();
};
for (size_t i = 0; i < contents.length(); i++) {
if (contents[i] == '\r') {
cr++;
continue;
}
if (contents[i] == '\n') {
lf++;
flush_line();
} else {
line += contents[i];
}
}
if (!line.empty()) {
flush_line();
}
if (!lines.empty()) {
// Append the cmkr .gitattributes using the detected newline
auto newline = cr == lf ? "\r\n" : "\n";
if (!contents.empty() && contents.back() != '\n') {
contents += newline;
}
contents += newline;
contents += generate(newline);
create_file(gitfile, contents);
}
}
}
void generate_project(const std::string &type) {
const auto name = escape_project_name(fs::current_path().stem().string());
if (fs::exists(fs::current_path() / "cmake.toml")) {
throw std::runtime_error("Cannot initialize a project when cmake.toml already exists!");
}
// Check if the folder is empty before creating any files
auto is_empty = fs::is_empty(fs::current_path());
// Automatically generate .gitattributes to not skew the statistics of the repo
generate_gitfile(".gitattributes", {"/**/CMakeLists.txt linguist-generated", "/**/cmkr.cmake linguist-vendored"});
// Generate .gitignore with reasonable defaults for CMake
generate_gitfile(".gitignore", {"build*/", "cmake-build*/", "CMakerLists.txt", "CMakeLists.txt.user"});
tsl::ordered_map<std::string, std::string> variables = {
{"@name", name},
{"@type", type},
};
if (!fs::is_empty(fs::current_path())) {
if (!is_empty) {
// Make a backup of an existing CMakeLists.txt if it exists
std::error_code ec;
fs::rename("CMakeLists.txt", "CMakeLists.txt.bak", ec);
@ -143,7 +248,7 @@ void generate_project(const std::string &type) {
struct CommandEndl {
std::stringstream &ss;
CommandEndl(std::stringstream &ss) : ss(ss) {
explicit CommandEndl(std::stringstream &ss) : ss(ss) {
}
void endl() {
ss << '\n';
@ -152,7 +257,7 @@ struct CommandEndl {
struct RawArg {
RawArg() = default;
RawArg(std::string arg) : arg(std::move(arg)) {
explicit RawArg(std::string arg) : arg(std::move(arg)) {
}
std::string arg;
@ -187,7 +292,7 @@ struct Command {
// https://cmake.org/cmake/help/latest/manual/cmake-language.7.html#unquoted-argument
// NOTE: Normally '/' does not require quoting according to the documentation but this has been the case here
// previously, so for backwards compatibility its still here.
if (str.find_first_of("()#\"\\'> |/;") == str.npos)
if (str.find_first_of("()#\"\\'> |/;") == std::string::npos)
return str;
std::string result;
result += "\"";
@ -336,14 +441,15 @@ static std::string tolf(const std::string &str) {
}
}
return result;
};
}
struct Generator {
Generator(const parser::Project &project) : project(project) {
Generator(const parser::Project &project, fs::path path) : project(project), path(std::move(path)) {
}
Generator(const Generator &) = delete;
const parser::Project &project;
fs::path path;
std::stringstream ss;
int indent = 0;
@ -373,8 +479,8 @@ struct Generator {
void inject_includes(const std::vector<std::string> &includes) {
if (!includes.empty()) {
for (const auto &file : includes) {
if (!fs::is_regular_file(file)) {
throw std::runtime_error("Include '" + file + "' does not exist");
if (!fs::exists(path / file)) {
throw std::runtime_error("Include not found: " + file);
}
cmd("include")(file);
}
@ -387,7 +493,7 @@ struct Generator {
throw std::runtime_error("Detected additional \" at the end of cmake block");
}
auto cmake_lf = tolf(cmake);
while (cmake_lf.back() == '\n')
while (!cmake_lf.empty() && cmake_lf.back() == '\n')
cmake_lf.pop_back();
bool did_indent = false;
for (char ch : cmake_lf) {
@ -456,28 +562,26 @@ struct ConditionScope {
};
static bool vcpkg_valid_identifier(const std::string &name) {
// prn|aux|nul|con|lpt[1-9]|com[1-9]|core|default
auto is_reserved = [](const std::string &s) {
if (s == "prn" || s == "aux" || s == "nul" || s == "con" || s == "core" || s == "default") {
return true;
}
if (s.length() == 4 && (s.compare(0, 3, "lpt") == 0 || s.compare(0, 3, "com") == 0) && (s[3] >= '1' && s[3] <= '9')) {
return true;
}
return false;
};
// [a-z0-9]+(-[a-z0-9]+)*
auto is_identifier = [](const std::string &s) {
for (size_t i = 0; i < s.length(); i++) {
auto c = s[i];
for (size_t i = 0; i < name.length(); i++) {
auto c = name[i];
if ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || (i > 0 && c == '-')) {
continue;
}
return false;
}
return true;
};
return is_identifier(name) && !is_reserved(name);
}
static bool vcpkg_identifier_reserved(const std::string &name) {
// prn|aux|nul|con|lpt[1-9]|com[1-9]|core|default
if (name == "prn" || name == "aux" || name == "nul" || name == "con" || name == "core" || name == "default") {
return true;
}
if (name.length() == 4 && (name.compare(0, 3, "lpt") == 0 || name.compare(0, 3, "com") == 0) && (name[3] >= '1' && name[3] <= '9')) {
return true;
}
return false;
}
static std::string vcpkg_escape_identifier(const std::string &name) {
@ -494,10 +598,12 @@ static std::string vcpkg_escape_identifier(const std::string &name) {
escaped += std::tolower(ch);
}
if (!vcpkg_valid_identifier(escaped)) {
throw std::runtime_error("The escaped project name '" + escaped + "' is not usable with [vcpkg]");
}
if (vcpkg_identifier_reserved(escaped)) {
throw std::runtime_error("The escaped project name '" + escaped + "' is a reserved name [vcpkg]");
}
return escaped;
}
@ -510,7 +616,18 @@ void generate_cmake(const char *path, const parser::Project *parent_project) {
auto is_root_project = parent_project == nullptr;
parser::Project project(parent_project, path, false);
Generator gen(project);
for (auto const &lang : project.project_languages) {
if (known_languages.find(lang) == known_languages.end()) {
if (project.project_allow_unknown_languages) {
printf("[warning] Unknown language '%s' specified\n", lang.c_str());
} else {
throw std::runtime_error("Unknown language '" + lang + "' specified");
}
}
}
Generator gen(project, path);
// Helper lambdas for more convenient CMake generation
auto &ss = gen.ss;
@ -616,7 +733,13 @@ void generate_cmake(const char *path, const parser::Project *parent_project) {
if (!project.options.empty()) {
comment("Options");
for (const auto &opt : project.options) {
cmd("option")(opt.name, RawArg(Command::quote(opt.comment)), opt.val ? "ON" : "OFF");
std::string default_val;
if (opt.value.index() == 0) {
default_val = mpark::get<0>(opt.value) ? "ON" : "OFF";
} else {
default_val = mpark::get<1>(opt.value);
}
cmd("option")(opt.name, RawArg(Command::quote(opt.help)), default_val);
}
endl();
}
@ -625,16 +748,16 @@ void generate_cmake(const char *path, const parser::Project *parent_project) {
comment("Variables");
for (const auto &set : project.variables) {
std::string set_val;
if (set.val.index() == 1) {
set_val = mpark::get<1>(set.val);
if (set.value.index() == 1) {
set_val = mpark::get<1>(set.value);
} else {
set_val = mpark::get<0>(set.val) ? "ON" : "OFF";
set_val = mpark::get<0>(set.value) ? "ON" : "OFF";
}
if (set.cache) {
auto typ = set.val.index() == 1 ? "STRING" : "BOOL";
auto typ = set.value.index() == 1 ? "STRING" : "BOOL";
auto force = set.force ? "FORCE" : "";
cmd("set")(set.name, set_val, typ, set.comment, force);
cmd("set")(set.name, set_val, typ, set.help, force);
} else {
cmd("set")(set.name, set_val);
}
@ -662,7 +785,11 @@ void generate_cmake(const char *path, const parser::Project *parent_project) {
gen.conditional_includes(project.include_after);
gen.conditional_cmake(project.cmake_after);
if (!project.vcpkg.packages.empty()) {
if (project.vcpkg.enabled()) {
if (!is_root_project) {
throw std::runtime_error("[vcpkg] is only supported in the root project");
}
// Allow the user to specify a url or derive it from the version
auto url = project.vcpkg.url;
auto version_name = url;
@ -675,7 +802,8 @@ void generate_cmake(const char *path, const parser::Project *parent_project) {
}
// Show a nicer error than vcpkg when specifying an invalid package name
for (const auto &package : project.vcpkg.packages) {
const auto &packages = project.vcpkg.packages;
for (const auto &package : packages) {
if (!vcpkg_valid_identifier(package.name)) {
throw std::runtime_error("Invalid [vcpkg].packages name '" + package.name + "' (needs to be lowercase alphanumeric)");
}
@ -685,6 +813,12 @@ void generate_cmake(const char *path, const parser::Project *parent_project) {
// clang-format off
cmd("if")("CMKR_ROOT_PROJECT", "AND", "NOT", "CMKR_DISABLE_VCPKG");
cmd("include")("FetchContent");
comment("Fix warnings about DOWNLOAD_EXTRACT_TIMESTAMP");
// clang-format off
cmd("if")("POLICY", "CMP0135");
cmd("cmake_policy")("SET", "CMP0135", "NEW");
cmd("endif")();
// clang-format on
cmd("message")("STATUS", "Fetching vcpkg (" + version_name + ")...");
cmd("FetchContent_Declare")("vcpkg", "URL", url);
// Not using FetchContent_MakeAvailable here in case vcpkg adds CMakeLists.txt
@ -709,16 +843,21 @@ void generate_cmake(const char *path, const parser::Project *parent_project) {
"dependencies": [
)";
const auto &packages = project.vcpkg.packages;
for (size_t i = 0; i < packages.size(); i++) {
const auto &package = packages[i];
const auto &features = package.features;
if (!vcpkg_valid_identifier(package.name)) {
throw std::runtime_error("Invalid vcpkg package name '" + package.name + "'");
throw std::runtime_error("Invalid vcpkg package name '" + package.name + "', name is not valid");
}
if (vcpkg_identifier_reserved(package.name)) {
throw std::runtime_error("Invalid vcpkg package name '" + package.name + "', name is reserved");
}
for (const auto &feature : features) {
if (!vcpkg_valid_identifier(feature)) {
throw std::runtime_error("Invalid vcpkg package feature '" + feature + "'");
throw std::runtime_error("Invalid vcpkg package feature '" + feature + "', name is not valid");
}
if (vcpkg_identifier_reserved(feature)) {
throw std::runtime_error("Invalid vcpkg package feature '" + feature + "', name is reserved");
}
}
if (features.empty()) {
@ -763,20 +902,32 @@ void generate_cmake(const char *path, const parser::Project *parent_project) {
if (!project.contents.empty()) {
cmd("include")("FetchContent").endl();
if (!project.root()->vcpkg.enabled()) {
comment("Fix warnings about DOWNLOAD_EXTRACT_TIMESTAMP");
// clang-format off
cmd("if")("POLICY", "CMP0135");
cmd("cmake_policy")("SET", "CMP0135", "NEW");
cmd("endif")();
// clang-format on
}
for (const auto &content : project.contents) {
ConditionScope cs(gen, content.condition);
gen.conditional_includes(content.include_before);
gen.conditional_cmake(content.cmake_before);
std::string version_info = "";
std::string version_info;
if (content.arguments.contains("GIT_TAG")) {
version_info = " (" + content.arguments.at("GIT_TAG") + ")";
} else if (content.arguments.contains("SVN_REVISION")) {
version_info = " (" + content.arguments.at("SVN_REVISION") + ")";
}
cmd("message")("STATUS", "Fetching " + content.name + version_info + "...");
if (content.system) {
cmd("FetchContent_Declare")(content.name, "SYSTEM", content.arguments);
} else {
cmd("FetchContent_Declare")(content.name, content.arguments);
}
cmd("FetchContent_MakeAvailable")(content.name).endl();
gen.conditional_includes(content.include_after);
@ -837,10 +988,38 @@ void generate_cmake(const char *path, const parser::Project *parent_project) {
gen.conditional_cmake(subdir.cmake_after);
}
// The implicit default is ["C", "CXX"], so make sure this list isn't
// empty or projects without languages explicitly defined will error.
auto project_languages = project.project_languages;
if (project_languages.empty())
project_languages = {"C", "CXX"};
// All acceptable extensions based off our given languages.
tsl::ordered_set<std::string> project_extensions;
for (const auto &language : project_languages) {
auto itr = known_languages.find(language);
if (itr != known_languages.end()) {
project_extensions.insert(itr->second.begin(), itr->second.end());
}
}
auto contains_language_source = [&project_extensions](const std::vector<std::string> &sources) {
for (const auto &source : sources) {
auto extension = fs::path(source).extension().string();
if (project_extensions.count(extension) > 0) {
return true;
}
}
return false;
};
if (!project.targets.empty()) {
auto project_root = project.root();
for (size_t i = 0; i < project.targets.size(); i++) {
const auto &target = project.targets[i];
auto throw_target_error = [&target](const std::string &message) { throw std::runtime_error("[target." + target.name + "] " + message); };
const parser::Template *tmplate = nullptr;
std::unique_ptr<ConditionScope> tmplate_cs{};
@ -944,12 +1123,46 @@ void generate_cmake(const char *path, const parser::Project *parent_project) {
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");
throw_target_error(source_key + " wildcard found 0 files");
}
// Make sure there are source files for the languages used by the project
switch (target.type) {
case parser::target_executable:
case parser::target_library:
case parser::target_shared:
case parser::target_static:
case parser::target_object:
if (!contains_language_source(sources)) {
std::string extensions;
for (const auto &language : project_extensions) {
if (!extensions.empty()) {
extensions += " ";
}
extensions += language;
}
throw_target_error("No sources found with valid extensions (" + extensions + ")");
}
// Make sure relative source files exist
for (const auto &source : sources) {
auto var_index = source.find("${");
if (var_index != std::string::npos)
continue;
const auto &source_path = fs::path(path) / source;
if (!fs::exists(source_path)) {
throw_target_error("Source file not found: " + source);
}
}
break;
default:
break;
}
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");
throw_target_error("Unreachable code, make sure unconditional sources are first");
}
cmd("set")(sources_var, sources);
sources_with_set = false;
@ -962,7 +1175,7 @@ void generate_cmake(const char *path, const parser::Project *parent_project) {
if (tmplate != nullptr) {
if (target_type != parser::target_template) {
throw std::runtime_error("Unreachable code, unexpected target type for template");
throw_target_error("Unreachable code, unexpected target type for template");
}
target_type = tmplate->outline.type;
}
@ -1010,7 +1223,7 @@ void generate_cmake(const char *path, const parser::Project *parent_project) {
target_scope = "PUBLIC";
break;
default:
throw std::runtime_error("Unimplemented enum value");
throw_target_error("Unimplemented enum value");
}
// Handle custom add commands from templates.
@ -1091,7 +1304,7 @@ void generate_cmake(const char *path, const parser::Project *parent_project) {
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");
throw_target_error("You cannot set msvc-runtime without setting the root [project].msvc-runtime");
}
}
}
@ -1134,7 +1347,12 @@ void generate_cmake(const char *path, const parser::Project *parent_project) {
}
auto working_directory = std::make_pair("WORKING_DIRECTORY", dir);
auto command = std::make_pair("COMMAND", test.command);
auto arguments = std::make_pair("", test.arguments);
// Transform the provided arguments into raw arguments to prevent them from being quoted when the generator runs
std::vector<RawArg> raw_arguments{};
for (const auto &argument : test.arguments)
raw_arguments.emplace_back(argument);
auto arguments = std::make_pair("", raw_arguments);
ConditionScope cs(gen, test.condition);
cmd("add_test")(name, configurations, working_directory, command, arguments).endl();
}
@ -1165,10 +1383,19 @@ void generate_cmake(const char *path, const parser::Project *parent_project) {
}
}
// Fetch the generated CMakeLists.txt output from the stringstream buffer
auto generated_cmake = ss.str();
// Make sure the file ends in a single newline
while (!generated_cmake.empty() && std::isspace(generated_cmake.back())) {
generated_cmake.pop_back();
}
generated_cmake += '\n';
// Generate CMakeLists.txt
auto list_path = fs::path(path) / "CMakeLists.txt";
auto should_regenerate = [&list_path, &ss]() {
auto should_regenerate = [&list_path, &generated_cmake]() {
if (!fs::exists(list_path))
return true;
@ -1178,11 +1405,11 @@ void generate_cmake(const char *path, const parser::Project *parent_project) {
}
std::string data((std::istreambuf_iterator<char>(ifs)), std::istreambuf_iterator<char>());
return data != ss.str();
return data != generated_cmake;
}();
if (should_regenerate) {
create_file(list_path, ss.str());
create_file(list_path, generated_cmake);
}
auto generate_subdir = [path, &project](const fs::path &sub) {

@ -1,31 +0,0 @@
#include "error.hpp"
#include <cassert>
#include <cstddef>
namespace cmkr {
namespace error {
Status::Status(Code ec) noexcept : ec_(ec) {
}
Status::operator int() const noexcept {
return static_cast<int>(ec_);
}
Status::Code Status::code() const noexcept {
return ec_;
}
} // namespace error
} // namespace cmkr
// strings for cmkr::error::Status::Code
static const char *err_string[] = {
"Success", "Runtime error", "Initialization error", "CMake generation error", "Build error", "Clean error", "Install error",
};
const char *cmkr_error_status(int i) {
assert(i >= 0 && static_cast<size_t>(i) < (sizeof(err_string) / sizeof(*(err_string))));
return err_string[i];
}

@ -23,11 +23,3 @@ arguments:
}
} // namespace help
} // namespace cmkr
const char *cmkr_help_version(void) {
return cmkr::help::version();
}
const char *cmkr_help_message(void) {
return cmkr::help::message();
}

@ -36,7 +36,7 @@ static std::string format_key_message(const std::string &message, const toml::ke
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();
const auto &line_str = loc.line_str();
std::ostringstream oss;
oss << message << "\n";
@ -73,7 +73,7 @@ class TomlChecker {
public:
TomlChecker(const TomlBasicValue &v, const toml::key &ky) : m_v(toml::find(v, ky)) {
}
TomlChecker(const TomlBasicValue &v) : m_v(v) {
explicit TomlChecker(const TomlBasicValue &v) : m_v(v) {
}
TomlChecker(const TomlChecker &) = delete;
TomlChecker(TomlChecker &&) = delete;
@ -175,7 +175,7 @@ class TomlCheckerRoot {
bool m_checked = false;
public:
TomlCheckerRoot(const TomlBasicValue &root) : m_root(root) {
explicit TomlCheckerRoot(const TomlBasicValue &root) : m_root(root) {
}
TomlCheckerRoot(const TomlCheckerRoot &) = delete;
TomlCheckerRoot(TomlCheckerRoot &&) = delete;
@ -265,8 +265,12 @@ Project::Project(const Project *parent, const std::string &path, bool build) : p
conditions["bsd"] = R"cmake(CMAKE_SYSTEM_NAME MATCHES "BSD")cmake";
conditions["linux"] = conditions["lunix"] = R"cmake(CMAKE_SYSTEM_NAME MATCHES "Linux")cmake";
conditions["gcc"] = R"cmake(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_C_COMPILER_ID STREQUAL "GNU")cmake";
conditions["clang"] = R"cmake(CMAKE_CXX_COMPILER_ID MATCHES "Clang" OR CMAKE_C_COMPILER_ID MATCHES "Clang")cmake";
conditions["msvc"] = R"cmake(MSVC)cmake";
conditions["clang"] =
R"cmake((CMAKE_CXX_COMPILER_ID MATCHES "Clang" AND NOT CMAKE_CXX_COMPILER_FRONTEND_VARIANT MATCHES "^MSVC$") OR (CMAKE_C_COMPILER_ID MATCHES "Clang" AND NOT CMAKE_C_COMPILER_FRONTEND_VARIANT MATCHES "^MSVC$"))cmake";
conditions["clang-cl"] =
R"cmake((CMAKE_CXX_COMPILER_ID MATCHES "Clang" AND CMAKE_CXX_COMPILER_FRONTEND_VARIANT MATCHES "^MSVC$") OR (CMAKE_C_COMPILER_ID MATCHES "Clang" AND CMAKE_C_COMPILER_FRONTEND_VARIANT MATCHES "^MSVC$"))cmake";
conditions["clang-any"] = R"cmake(CMAKE_CXX_COMPILER_ID MATCHES "Clang" OR CMAKE_C_COMPILER_ID MATCHES "Clang")cmake";
conditions["root"] = R"cmake(CMKR_ROOT_PROJECT)cmake";
conditions["x64"] = R"cmake(CMAKE_SIZEOF_VOID_P EQUAL 8)cmake";
conditions["x32"] = R"cmake(CMAKE_SIZEOF_VOID_P EQUAL 4)cmake";
@ -288,6 +292,7 @@ Project::Project(const Project *parent, const std::string &path, bool build) : p
project.optional("version", project_version);
project.optional("description", project_description);
project.optional("languages", project_languages);
project.optional("allow-unknown-languages", project_allow_unknown_languages);
project.optional("cmake-before", cmake_before);
project.optional("cmake-after", cmake_after);
project.optional("include-before", include_before);
@ -345,18 +350,18 @@ Project::Project(const Project *parent, const std::string &path, bool build) : p
s.name = itr.first;
const auto &value = itr.second;
if (value.is_boolean()) {
s.val = value.as_boolean();
s.value = value.as_boolean();
} else if (value.is_string()) {
s.val = value.as_string();
s.value = value.as_string();
} else {
auto &setting = checker.create(value);
setting.optional("comment", s.comment);
setting.optional("help", s.help);
if (setting.contains("value")) {
const auto &v = setting.find("value");
if (v.is_boolean()) {
s.val = v.as_boolean();
s.value = v.as_boolean();
} else {
s.val = v.as_string();
s.value = v.as_string();
}
}
setting.optional("cache", s.cache);
@ -374,13 +379,37 @@ Project::Project(const Project *parent, const std::string &path, bool build) : p
o.name = itr.first;
const auto &value = itr.second;
if (value.is_boolean()) {
o.val = value.as_boolean();
o.value = value.as_boolean();
} else if (value.is_string()) {
auto str = std::string(value.as_string());
if (str == "root") {
o.value = std::string("${CMKR_ROOT_PROJECT}");
} else {
throw_key_error("Unsupported option value '" + str + "'", str, value);
}
} else if (value.is_table()) {
auto &option = checker.create(value);
option.optional("comment", o.comment);
option.optional("value", o.val);
option.optional("help", o.help);
if (option.contains("value")) {
const auto &ovalue = option.find("value");
if (ovalue.is_boolean()) {
o.value = ovalue.as_boolean();
} else if (ovalue.is_string()) {
auto str = std::string(ovalue.as_string());
if (str == "root") {
o.value = std::string("${CMKR_ROOT_PROJECT}");
} else {
throw_key_error("Unsupported option value '" + str + "'", str, value);
}
} else {
throw_key_error(toml::concat_to_string("Unsupported value type: ", ovalue.type()), "value", value);
}
}
} else {
throw_key_error(toml::concat_to_string("Unsupported value type: ", itr.second.type()), itr.first, itr.second);
}
options.push_back(o);
conditions.emplace(o.name, o.name);
}
}
@ -417,6 +446,15 @@ Project::Project(const Project *parent, const std::string &path, bool build) : p
c.optional("cmake-after", content.cmake_after);
c.optional("include-before", content.include_before);
c.optional("include-after", content.include_after);
c.optional("system", content.system);
// Check if the minimum version requirement is satisfied (CMake 3.25)
if (c.contains("system") && !this->cmake_minimum_version(3, 25)) {
throw_key_error("The system argument is only supported on CMake version 3.25 and above.\nSet the CMake version in cmake.toml:\n"
"[cmake]\n"
"version = \"3.25\"\n",
"system", "");
}
for (const auto &argItr : itr.second.as_table()) {
std::string value;
@ -559,6 +597,38 @@ Project::Project(const Project *parent, const std::string &path, bool build) : p
t.optional("link-libraries", target.link_libraries);
t.optional("private-link-libraries", target.private_link_libraries);
// Add support for relative paths for (private-)link-libraries
const auto fix_relative_paths = [&name, &path](ConditionVector &libraries, const char *key) {
for (const auto &library_entries : libraries) {
for (auto &library_path : libraries[library_entries.first]) {
// Skip processing paths with potential CMake macros in them (this check isn't perfect)
// https://cmake.org/cmake/help/latest/manual/cmake-language.7.html#variable-references
if ((library_path.find("${") != std::string::npos || library_path.find("$ENV{") != std::string::npos ||
library_path.find("$CACHE{") != std::string::npos) &&
library_path.find('}') != std::string::npos) {
continue;
}
// Skip paths that don't contain backwards or forwards slashes
if (library_path.find_first_of(R"(\/)") == std::string::npos) {
continue;
}
// Check if the new file path exists, otherwise emit an error
const auto expected_library_file_path = fs::path{path} / library_path;
if (!fs::exists(expected_library_file_path)) {
throw std::runtime_error("Attempted to link against a library file that doesn't exist for target \"" + name + "\" in \"" +
key + "\": " + library_path);
}
// Prepend ${CMAKE_CURRENT_SOURCE_DIR} to the path
library_path.insert(0, "${CMAKE_CURRENT_SOURCE_DIR}/");
}
}
};
fix_relative_paths(target.link_libraries, "link-libraries");
fix_relative_paths(target.private_link_libraries, "private-link-libraries");
t.optional("link-options", target.link_options);
t.optional("private-link-options", target.private_link_options);
@ -659,6 +729,7 @@ Project::Project(const Project *parent, const std::string &path, bool build) : p
t.optional("add-function", tmplate.add_function);
t.optional("pass-sources-to-add-function", tmplate.pass_sources_to_add_function);
t.optional("pass-sources", tmplate.pass_sources_to_add_function);
templates.push_back(tmplate);
}
@ -741,6 +812,25 @@ const Project *Project::root() const {
return root;
}
bool Project::cmake_minimum_version(int major, int minor) const {
// NOTE: this code is like pulling teeth, sorry
auto root_version = root()->cmake_version;
auto range_index = root_version.find("...");
if (range_index != std::string::npos) {
root_version.resize(range_index);
}
auto period_index = root_version.find('.');
auto root_major = atoi(root_version.substr(0, period_index).c_str());
int root_minor = 0;
if (period_index != std::string::npos) {
auto end_index = root_version.find('.', period_index + 1);
root_minor = atoi(root_version.substr(period_index + 1, end_index).c_str());
}
return std::tie(root_major, root_minor) >= std::tie(major, minor);
}
bool is_root_path(const std::string &path) {
const auto toml_path = fs::path(path) / "cmake.toml";
if (!fs::exists(toml_path)) {

22
tests/CMakeLists.txt generated

@ -100,3 +100,25 @@ if(MSVC) # msvc
)
endif()
add_test(
NAME
compile-options
WORKING_DIRECTORY
"${CMAKE_CURRENT_LIST_DIR}/compile-options"
COMMAND
"$<TARGET_FILE:cmkr>"
build
)
if(WIN32) # windows
add_test(
NAME
relative-paths
WORKING_DIRECTORY
"${CMAKE_CURRENT_LIST_DIR}/relative-paths"
COMMAND
"$<TARGET_FILE:cmkr>"
build
)
endif()

@ -52,3 +52,16 @@ name = "msvc-runtime"
working-directory = "msvc-runtime"
command = "$<TARGET_FILE:cmkr>"
arguments = ["build"]
[[test]]
name = "compile-options"
working-directory = "compile-options"
command = "$<TARGET_FILE:cmkr>"
arguments = ["build"]
[[test]]
condition = "windows"
name = "relative-paths"
working-directory = "relative-paths"
command = "$<TARGET_FILE:cmkr>"
arguments = ["build"]

@ -0,0 +1,15 @@
# Example project that sets compiler/linker flags for various platforms.
[project]
name = "compile-options"
description = "Compiler flags"
[target.hello]
type = "executable"
sources = ["src/main.cpp"]
msvc.compile-options = ["/W2"]
gcc.compile-options = ["-Wall"]
clang.compile-options = ["-Wall"]
# The `hello` target uses [conditions](/cmake-toml#conditions) to set different compiler flags depending on the platform. See the [targets](/cmake-toml/#targets) documentation for other things you can set.
# _Note_: In general you only want to specify flags _required_ to compile your code without errors.

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

@ -1 +1,2 @@
int main() { }
int main() {
}

@ -1,3 +1,4 @@
#include <Windows.h>
void foo() { }
void foo() {
}

@ -1,8 +1,7 @@
#include <cstdio>
#include <tuple>
int main()
{
int main() {
auto tpl = std::make_tuple(1, 2);
printf("Hello from C++11 %d\n", std::get<0>(tpl));
}

@ -1,6 +1,5 @@
#include <fmt/core.h>
int main()
{
int main() {
fmt::print("Hello, world!\n");
}

@ -2,7 +2,6 @@
#include <string>
namespace mylib
{
namespace mylib {
std::string message();
}

@ -1,6 +1,5 @@
#include <mylib/mylib.hpp>
std::string mylib::message()
{
std::string mylib::message() {
return "cmkr is awesome!";
}

@ -1,4 +1,5 @@
namespace mylib
{
static const char* version() { return "v1.0"; }
namespace mylib {
static const char *version() {
return "v1.0";
}
} // namespace mylib

@ -2,7 +2,6 @@
#include "mylib/mylib.hpp"
int main()
{
int main() {
printf("mylib version: %s\n", mylib::version());
}

@ -0,0 +1,11 @@
[project]
name = "relative-paths"
[target.test-library]
type = "static"
sources = ["src/library-code.cpp"]
[target.example]
type = "executable"
sources = ["src/main.cpp"]
windows.link-libraries = ["libs/test-library-x64-Release.lib"]

@ -0,0 +1,6 @@
// Created by Anthony Printup on 9/18/2023.
#include <cstdio>
extern "C" void library_function() {
std::puts("Hello from library_function!");
}

@ -0,0 +1,12 @@
// Created by Anthony Printup on 9/18/2023.
#include <cstdio>
#ifdef WIN32
extern "C" void library_function();
#endif
int main() {
puts("Hello from cmkr(relative-paths)!");
#ifdef WIN32
library_function();
#endif
}

@ -1,6 +1,5 @@
#include <fmt/core.h>
int main()
{
int main() {
fmt::print("Hello, world!\n");
}

@ -0,0 +1,3 @@
# Disable clang-format in this folder
DisableFormat: true
SortIncludes: Never
Loading…
Cancel
Save