From e5d0973fa2f0a5b319b1f9bc724c3e6ef15849c6 Mon Sep 17 00:00:00 2001 From: Thomas Willetal Date: Sun, 4 Oct 2026 09:45:51 +0200 Subject: [PATCH 1/5] Refactor argument forwarding to perfect forwarding --- ctprintf/include/ctprintf/detail/formatter.hpp | 3 ++- ctprintf/include/ctprintf/format.hpp | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/ctprintf/include/ctprintf/detail/formatter.hpp b/ctprintf/include/ctprintf/detail/formatter.hpp index b2dd408..548e03a 100644 --- a/ctprintf/include/ctprintf/detail/formatter.hpp +++ b/ctprintf/include/ctprintf/detail/formatter.hpp @@ -8,6 +8,7 @@ #include #include #include +#include namespace ctprintf::detail { @@ -213,7 +214,7 @@ void write_formatted_arguments(O &output, const char *&cursor, const First &firs const parsed_spec parsed = parse_spec(cursor); write_value(output, parsed.spec, first); - write_formatted_arguments(output, cursor, static_cast(rest)...); + write_formatted_arguments(output, cursor, std::forward(rest)...); } } // namespace ctprintf::detail diff --git a/ctprintf/include/ctprintf/format.hpp b/ctprintf/include/ctprintf/format.hpp index 4136f2e..64cc378 100644 --- a/ctprintf/include/ctprintf/format.hpp +++ b/ctprintf/include/ctprintf/format.hpp @@ -4,6 +4,7 @@ #include "ctprintf/detail/parser.hpp" #include +#include namespace ctprintf { @@ -17,7 +18,7 @@ void format(O &output, format_text format_text, Args &&...args) if constexpr (sizeof...(Args) == 0) detail::write_formatted_arguments(output, cursor); else - detail::write_formatted_arguments(output, cursor, static_cast(args)...); + detail::write_formatted_arguments(output, cursor, std::forward(args)...); } } // namespace ctprintf From 54799b77f7d1cdd8377238a2b0c8eeb7ca26bbe3 Mon Sep 17 00:00:00 2001 From: Thomas Willetal Date: Sun, 4 Oct 2026 10:35:47 +0200 Subject: [PATCH 2/5] Enhance CMake configuration with installation rules and target management --- ctprintf/CMakeLists.txt | 30 ++++++++++++++++++++++------ ctprintf/cmake/ctprintf-config.cmake | 3 +++ 2 files changed, 27 insertions(+), 6 deletions(-) create mode 100644 ctprintf/cmake/ctprintf-config.cmake diff --git a/ctprintf/CMakeLists.txt b/ctprintf/CMakeLists.txt index 7e811a7..2d8e680 100644 --- a/ctprintf/CMakeLists.txt +++ b/ctprintf/CMakeLists.txt @@ -1,6 +1,8 @@ cmake_minimum_required(VERSION 3.25) project(ctprintf LANGUAGES CXX) +include(GNUInstallDirs) + option(ENABLE_TESTING "Build and enable tests" OFF) add_library(ctprintf INTERFACE) @@ -9,16 +11,32 @@ add_library(ctprintf::ctprintf ALIAS ctprintf) target_sources( ctprintf - INTERFACE include/ctprintf/format.hpp - include/ctprintf/output.hpp - include/ctprintf/types.hpp - include/ctprintf/detail/formatter.hpp - include/ctprintf/detail/parser.hpp) + INTERFACE $ + $ + $ + $ + $) -target_include_directories(ctprintf INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/include) +target_include_directories(ctprintf INTERFACE $ + $) target_compile_features(ctprintf INTERFACE cxx_std_20) +install( + TARGETS ctprintf + EXPORT ctprintf-targets + INCLUDES + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) + +install(DIRECTORY include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) + +install( + EXPORT ctprintf-targets + NAMESPACE ctprintf:: + DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/ctprintf) + +install(FILES cmake/ctprintf-config.cmake DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/ctprintf) + if(ENABLE_TESTING) include(CTest) enable_testing() diff --git a/ctprintf/cmake/ctprintf-config.cmake b/ctprintf/cmake/ctprintf-config.cmake new file mode 100644 index 0000000..e2a9163 --- /dev/null +++ b/ctprintf/cmake/ctprintf-config.cmake @@ -0,0 +1,3 @@ +if(NOT TARGET ctprintf::ctprintf) + include("${CMAKE_CURRENT_LIST_DIR}/ctprintf-targets.cmake") +endif() From d35671edfa3092c4b1109f2d19224c74a13cc322 Mon Sep 17 00:00:00 2001 From: Thomas Willetal Date: Sun, 4 Oct 2026 11:38:46 +0200 Subject: [PATCH 3/5] Add Debian packaging support --- .github/workflows/ci-build.yml | 21 +++++++++++---------- .gitignore | 17 +++++++++++++++++ debian/changelog | 5 +++++ debian/control | 19 +++++++++++++++++++ debian/copyright | 25 +++++++++++++++++++++++++ debian/rules | 7 +++++++ debian/source/format | 1 + scripts/build-dependencies.sh | 22 ++++++++++++++++++++++ scripts/build-package.sh | 24 ++++++++++++++++++++++++ 9 files changed, 131 insertions(+), 10 deletions(-) create mode 100644 debian/changelog create mode 100644 debian/control create mode 100644 debian/copyright create mode 100755 debian/rules create mode 100644 debian/source/format create mode 100755 scripts/build-dependencies.sh create mode 100755 scripts/build-package.sh diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index f1678a2..86f7347 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -16,16 +16,7 @@ jobs: - uses: actions/checkout@v7 - name: Install build dependencies - run: | - sudo apt-get update - sudo apt-get install --yes \ - clang-format \ - libgtest-dev \ - ninja-build \ - pipx - pipx install cmakelang - pipx install cmakelint - echo "$HOME/.local/bin" >> "$GITHUB_PATH" + run: ./scripts/build-dependencies.sh - name: Check C++ formatting run: ./scripts/lint.sh cpp-format @@ -39,3 +30,13 @@ jobs: - name: Configure, build, and test working-directory: ctprintf run: cmake --workflow --preset debug-workflow + + - name: Build Debian package + run: ./scripts/build-package.sh + + - name: Upload Debian package + uses: actions/upload-artifact@v4 + with: + name: ctprintf-debian-package + path: build/debian/ + if-no-files-found: error diff --git a/.gitignore b/.gitignore index 0ba1c63..ee9519d 100644 --- a/.gitignore +++ b/.gitignore @@ -67,3 +67,20 @@ vcpkg_installed/ # test output & cache Testing/ .cache/ + +# Debian package build artifacts +*.build +*.buildinfo +*.changes +*.deb +*.debian.tar.* +*.dsc +*.orig.tar.* +/debian/.debhelper/ +/debian/debhelper-build-stamp +/debian/files +/debian/libctprintf-dev/ +/debian/tmp/ +/debian/*.log +/debian/*.substvars +/obj-*/ diff --git a/debian/changelog b/debian/changelog new file mode 100644 index 0000000..c57c887 --- /dev/null +++ b/debian/changelog @@ -0,0 +1,5 @@ +ctprintf (0.1.0-1) unstable; urgency=medium + + * Initial release. + + -- Thomas Willetal Sun, 04 Oct 2026 00:00:00 +0000 diff --git a/debian/control b/debian/control new file mode 100644 index 0000000..7d46721 --- /dev/null +++ b/debian/control @@ -0,0 +1,19 @@ +Source: ctprintf +Section: libdevel +Priority: optional +Maintainer: Thomas Willetal +Build-Depends: + debhelper-compat (= 13), + cmake (>= 3.25), + g++, + libgtest-dev, + ninja-build +Standards-Version: 4.7.0 +Rules-Requires-Root: no + +Package: libctprintf-dev +Architecture: all +Depends: ${misc:Depends} +Description: Compile-time checked printf-style formatting library + Header-only C++20 library for formatting text through an output sink with + compile-time validation of printf-style format strings. diff --git a/debian/copyright b/debian/copyright new file mode 100644 index 0000000..39c57d4 --- /dev/null +++ b/debian/copyright @@ -0,0 +1,25 @@ +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: ctprintf + +Files: * +Copyright: 2026 Thomas Willetal +License: MIT + +License: MIT + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + . + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + . + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. diff --git a/debian/rules b/debian/rules new file mode 100755 index 0000000..9df3c57 --- /dev/null +++ b/debian/rules @@ -0,0 +1,7 @@ +#!/usr/bin/make -f + +%: + dh $@ --buildsystem=cmake+ninja --sourcedirectory=ctprintf + +override_dh_auto_configure: + dh_auto_configure --sourcedirectory=ctprintf -- -DENABLE_TESTING=ON diff --git a/debian/source/format b/debian/source/format new file mode 100644 index 0000000..163aaf8 --- /dev/null +++ b/debian/source/format @@ -0,0 +1 @@ +3.0 (quilt) diff --git a/scripts/build-dependencies.sh b/scripts/build-dependencies.sh new file mode 100755 index 0000000..e6cec82 --- /dev/null +++ b/scripts/build-dependencies.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash + +set -euo pipefail + +sudo DEBIAN_FRONTEND=noninteractive apt-get update +sudo DEBIAN_FRONTEND=noninteractive apt-get install --yes --no-install-recommends \ + build-essential \ + clang-format \ + cmake \ + debhelper \ + devscripts \ + libgtest-dev \ + ninja-build \ + pipx + +pipx install --force cmakelang +pipx install --force cmakelint + +export PATH="$HOME/.local/bin:$PATH" +if [[ -n "${GITHUB_PATH:-}" ]]; then + echo "$HOME/.local/bin" >>"$GITHUB_PATH" +fi diff --git a/scripts/build-package.sh b/scripts/build-package.sh new file mode 100755 index 0000000..ee75c96 --- /dev/null +++ b/scripts/build-package.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash + +set -euo pipefail + +package_directory="${PKGDIR:-$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)}" +output_directory="$package_directory/build/debian" + +cd "$package_directory" +source_name="$(dpkg-parsechangelog --show-field Source)" +binary_package="$(awk '$1 == "Package:" { print $2; exit }' debian/control)" +mkdir --parents "$output_directory" + +dpkg-buildpackage \ + --unsigned-source \ + --unsigned-changes \ + --post-clean \ + --build=binary + +shopt -s nullglob +mv -- \ + "$package_directory"/../"$binary_package"_*.deb \ + "$package_directory"/../"$source_name"_*.build{,info} \ + "$package_directory"/../"$source_name"_*.changes \ + "$output_directory" From 930a88691edbd470d74ae9ab57eeffaa2d3973cd Mon Sep 17 00:00:00 2001 From: Thomas Willetal Date: Sun, 4 Oct 2026 13:41:01 +0200 Subject: [PATCH 4/5] Add README.md --- README.md | 165 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 165 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..718a3ac --- /dev/null +++ b/README.md @@ -0,0 +1,165 @@ +# ctprintf + +`ctprintf` is a small C++20, header-only formatter for embedded and +freestanding-oriented applications. It provides familiar `printf`-style +formatting, validates literal format strings and argument types at compile +time, and writes one character at a time to an application-provided output. + +The library is intended for diagnostic output such as UART, ITM/SWO, log +buffers, or host-side test buffers. It does not use `printf`, iostreams, +`std::string`, or dynamic allocation itself. + +## Features + +- C++20 header-only library +- Compile-time validation of literal format strings and argument types +- No dynamic allocation in the formatter +- No dependency on libc formatting functions +- Pluggable output through a single `put(char)` operation +- Integer, character, string, pointer, width, flag, and escaped-percent + formatting + +## Quick start + +```cpp +#include + +struct Uart { + void put(char character) + { + // Transmit character. + } +}; + +int main() +{ + Uart uart; + + ctprintf::format( + uart, + "PC=%08x LR=%08x\n", + 0x08001234U, + 0x08005678U); +} +``` + +The output is: + +```text +PC=08001234 LR=08005678 +``` + +## Output interface + +The first argument to `ctprintf::format` can be any type that provides: + +```cpp +void put(char character); +``` + +For example, a fixed-size buffer can be used in a test or a logging adapter: + +```cpp +struct BufferOutput { + char *buffer; + std::size_t position = 0; + + void put(char character) + { + buffer[position++] = character; + } +}; +``` + +`ctprintf` does not own the output or perform bounds checking; the output type +is responsible for transport, storage, synchronization, and capacity handling. + +## Format strings + +Format strings must be string literals. They are validated during compilation: +the number of conversions must match the number of arguments, and each +argument must have a supported type for its conversion. + +```cpp +ctprintf::format(output, "value=%08x\n", 42U); // Valid. +ctprintf::format(output, "value=%08x\n", "42"); // Compile-time error. +``` + +The following conversions are supported: + +| Conversion | Accepted argument | Description | +| --- | --- | --- | +| `%d`, `%i` | Signed integral type | Signed decimal | +| `%u` | Unsigned integral type, excluding `bool` | Unsigned decimal | +| `%o` | Unsigned integral type, excluding `bool` | Octal | +| `%x` | Unsigned integral type, excluding `bool` | Lowercase hexadecimal | +| `%X` | Unsigned integral type, excluding `bool` | Uppercase hexadecimal | +| `%c` | Integral type | Character | +| `%s` | Type convertible to `const char *` | Null-terminated string | +| `%p` | Object pointer, `void` pointer, or `nullptr` | Pointer in hexadecimal | +| `%%` | No argument | Literal percent sign | + +`%s` formats a null pointer as `(null)`. `%p` always includes a `0x` prefix; +for example, `nullptr` is formatted as `0x0`. + +### Flags and width + +The formatter supports the following flags and a decimal minimum field width: + +| Option | Meaning | +| --- | --- | +| `-` | Left-align within the field width | +| `+` | Prefix non-negative signed decimal values with `+` | +| space | Prefix non-negative signed decimal values with a space | +| `#` | Add an octal or hexadecimal prefix where applicable | +| `0` | Pad numeric values with zeroes when not left-aligned | +| width | Minimum field width, for example `%08x` or `%-6s` | + +Precision, length modifiers, floating-point conversions, positional arguments, +and runtime-provided format strings are not supported. + +## Build and test + +Requirements: + +- A C++20-capable compiler +- CMake 3.25 or newer +- GoogleTest, when building the test suite + +Configure and build the library: + +```bash +cmake -S ctprintf -B build +cmake --build build +``` + +To build and run the tests, enable them explicitly: + +```bash +cmake -S ctprintf -B build -DENABLE_TESTING=ON +cmake --build build +ctest --test-dir build --output-on-failure +``` + +## Installation and CMake integration + +Install the header and CMake package files with: + +```bash +cmake --install build --prefix /desired/prefix +``` + +An application can then consume the installed package: + +```cmake +find_package(ctprintf CONFIG REQUIRED) + +target_link_libraries(my_application PRIVATE ctprintf::ctprintf) +``` + +The exported target supplies the include directory and requires C++20. + + +## License + +MIT License. See [LICENSE](LICENSE). From 59c8d11d96fc3364e3e5dc2a9fc9566841878504 Mon Sep 17 00:00:00 2001 From: Thomas Willetal Date: Sun, 4 Oct 2026 13:46:45 +0200 Subject: [PATCH 5/5] Enhance CMake and CI configuration for packaging --- .github/workflows/ci-build.yml | 12 +++++++----- README.md | 13 +++++++++++++ ctprintf/CMakeLists.txt | 13 ++++++++++++- ctprintf/CMakePresets.json | 22 ++++++++++++++++++++-- 4 files changed, 52 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index 86f7347..d20e6f4 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -29,14 +29,16 @@ jobs: - name: Configure, build, and test working-directory: ctprintf - run: cmake --workflow --preset debug-workflow + run: cmake --workflow --preset release-workflow - name: Build Debian package run: ./scripts/build-package.sh - - name: Upload Debian package - uses: actions/upload-artifact@v4 + - name: Upload packages + uses: actions/upload-artifact@v7 with: - name: ctprintf-debian-package - path: build/debian/ + name: ctprintf-packages + path: | + ctprintf/build/release/*.tar.zst + build/debian/ if-no-files-found: error diff --git a/README.md b/README.md index 718a3ac..1095d44 100644 --- a/README.md +++ b/README.md @@ -141,6 +141,19 @@ cmake --build build ctest --test-dir build --output-on-failure ``` +## Creating a package + +CPack creates a Zstandard-compressed tarball containing the installable headers, +CMake package files, README, and license: + +```bash +cpack --config build/CPackConfig.cmake +``` + +The archive is written to the build directory and is named +`ctprintf--.tar.zst`, for example +`ctprintf-0.1.0-Linux.tar.zst`. + ## Installation and CMake integration Install the header and CMake package files with: diff --git a/ctprintf/CMakeLists.txt b/ctprintf/CMakeLists.txt index 2d8e680..5ece6f4 100644 --- a/ctprintf/CMakeLists.txt +++ b/ctprintf/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.25) -project(ctprintf LANGUAGES CXX) +project(ctprintf VERSION 0.1.0 LANGUAGES CXX) include(GNUInstallDirs) @@ -37,8 +37,19 @@ install( install(FILES cmake/ctprintf-config.cmake DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/ctprintf) +install( + FILES ${CMAKE_CURRENT_SOURCE_DIR}/../README.md ${CMAKE_CURRENT_SOURCE_DIR}/../LICENSE + DESTINATION ${CMAKE_INSTALL_DOCDIR}) + if(ENABLE_TESTING) include(CTest) enable_testing() add_subdirectory(tests) endif() # ENABLE_TESTING + +set(CPACK_GENERATOR "TZST") +set(CPACK_PACKAGE_CONTACT "Thomas Willetal ") +set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "Compile-time checked printf-style formatting library") +set(CPACK_PACKAGE_DIRECTORY "${CMAKE_BINARY_DIR}") +set(CPACK_PACKAGE_VENDOR "embtom") +include(CPack) diff --git a/ctprintf/CMakePresets.json b/ctprintf/CMakePresets.json index 143991a..83ed12d 100644 --- a/ctprintf/CMakePresets.json +++ b/ctprintf/CMakePresets.json @@ -55,10 +55,20 @@ } } ], + "packagePresets": [ + { + "name": "debug", + "configurePreset": "debug" + }, + { + "name": "release", + "configurePreset": "release" + } + ], "workflowPresets": [ { "name": "debug-workflow", - "displayName": "Debug: configure, build, and test", + "displayName": "Debug: configure, build, test, and package", "steps": [ { "type": "configure", @@ -71,12 +81,16 @@ { "type": "test", "name": "debug" + }, + { + "type": "package", + "name": "debug" } ] }, { "name": "release-workflow", - "displayName": "Release: configure, build, and test", + "displayName": "Release: configure, build, test, and package", "steps": [ { "type": "configure", @@ -89,6 +103,10 @@ { "type": "test", "name": "release" + }, + { + "type": "package", + "name": "release" } ] }