Install
Three ways to get the pack, depending on how much you want on your machine. The generators are the same in every case.
Globally
One install, every project. jen finds globally installed packs next to itself, without listing them anywhere, so this also works in projects without a package.json, like most C++ repos.
$ npm install -g @codejen/jen @codejen/pack-cpp $ jen cpp:sdl3 --name=MyGame
Locally best for monorepos
Add both as dev dependencies. jen reads packs straight from the project's package.json, so the generator versions are pinned with the repo: every contributor and every CI run gets the same ones, and there's nothing to set up on anyone's machine. In a monorepo, put them in the root.
$ npm install --save-dev @codejen/jen @codejen/pack-cpp $ npx jen cpp:sdl3 --name=MyGame
Not at all
Nothing to install but jen itself, or not even that with npx. When you ask for a prefixed generator like cpp:sdl3 and the pack isn't installed, jen fetches @codejen/pack-cpp via npm into a throwaway directory, runs it once and removes it again: no package.json entry, no node_modules left behind.
$ npx @codejen/jen cpp:sdl3 --name=MyGame
That automatic fetch only ever pulls from the @codejen scope, so a mistyped name can't install someone else's package; set JEN_NO_FETCH=1 to turn it off. For a pack from any other scope, or a Yeoman generator, be explicit with --from.
$ npx @codejen/jen --from @codejen/pack-cpp cpp:sdl3 --name=MyGame
Where files go
Generators write into the folder you run them in, or --dir: from the project root, cpp:class --name=Foo --dir=src writes src/Foo.h. The app starters make a new project folder in the current directory instead; --dir names it. The home page has the general rules.
From src/net/, cpp:class --name=Socket writes src/net/Socket.h and wires net/Socket.cpp into the CMakeLists.txt that carries # jen:sources, which jen finds by walking up from where you are. Wiring is looked up by marker, never guessed:
--dir: write somewhere else, relative to the current directory.--withTest: the test goes next to the header and is wired into theCMakeLists.txtthat carries# jen:tests.- No marker: the plan shows the insert as skipped; add the marker and run the generator again.
The tooling generators (cpp:warnings, cpp:tidy, cpp:doctest, …) work on the project as a whole, so their files go to the project root from wherever you are.
App starters
Every starter creates a new project folder named after the app (MyGame becomes my-game/), so cd into it before running the other generators. All of them split the project into a <name>-core library that holds every source file and a thin <name> executable that holds only main.cpp, which is what makes the code linkable from tests.
cpp:sdl3
An SDL3 callbacks-based app: root CMakeLists.txt, a vendor/ that FetchContents SDL3 (a local find_package is tried first, so nothing touches the network if SDL3 is already installed), and an app class in the core library clearing the window to an animated color.
$ jen cpp:sdl3 --name=MyGame cpp:sdl3 (pack @codejen/pack-cpp) – create an SDL3 callback-based app starter (<name>-core library + thin executable, vendor/ FetchContent SDL3) + my-game/CMakeLists.txt + my-game/vendor/CMakeLists.txt + my-game/src/CMakeLists.txt + my-game/src/main.cpp + my-game/src/app.h + my-game/src/app.cpp ✓ 6 written
Params
name
project and target name, window title and app id
width
window width in pixels
height
window height in pixels
bundleId
app identifier, e.g. com.example.my-game (default: com.example.<name>)
folderCase
project folder name from --name: kebab (my-game) or pascal (MyGame)
dir
project folder instead of one named after --name; . for the current directory
sdlTag
SDL3 git tag to fetch
cpp:sdl3-opengl
The same starter with an OpenGL 4.1 core context instead: vendors a real, checked-in glad loader under vendor/glad/, adds cmake/embed-glsl.cmake, and scaffolds an app class that compiles a shader pair and draws an animated full-screen quad.
$ jen cpp:sdl3-opengl --name=MyGame cpp:sdl3-opengl (pack @codejen/pack-cpp) – create an SDL3 + OpenGL (4.1 core, glad) app starter with a shader-quad demo; vendors glad and wires up cmake/embed-glsl.cmake + my-game/CMakeLists.txt + my-game/vendor/CMakeLists.txt + my-game/cmake/embed-glsl.cmake + my-game/vendor/glad/CMakeLists.txt + my-game/vendor/glad/include/glad/glad.h + my-game/vendor/glad/include/KHR/khrplatform.h + my-game/vendor/glad/src/glad.c + my-game/src/CMakeLists.txt + my-game/src/main.cpp + my-game/src/app.h + my-game/src/app.cpp + my-game/src/shader-utils.h + my-game/src/shader-utils.cpp + my-game/src/quad.vert.glsl + my-game/src/quad.frag.glsl ✓ 15 written
Params
name
project and target name, window title and app id
width
window width in pixels
height
window height in pixels
bundleId
app identifier, e.g. com.example.my-game (default: com.example.<name>)
folderCase
project folder name from --name: kebab (my-game) or pascal (MyGame)
dir
project folder instead of one named after --name; . for the current directory
sdlTag
SDL3 git tag to fetch
cpp:app
A console starter without SDL. main(int argc, char** argv) wraps the arguments in a std::span to avoid pointer arithmetic (bounds profile).
$ jen cpp:app --name=MyTool cpp:app (pack @codejen/pack-cpp) – create a console app starter without SDL (<name>-core library + thin executable, main wraps argv in a span) + my-tool/CMakeLists.txt + my-tool/src/CMakeLists.txt + my-tool/src/main.cpp + my-tool/src/app.h + my-tool/src/app.cpp ✓ 5 written
Params
name
project and target name
folderCase
project folder name from --name: kebab (my-game) or pascal (MyGame)
dir
project folder instead of one named after --name; . for the current directory
cpp:lib
A library starter: public headers in include/<name>/, BUILD_INTERFACE/INSTALL_INTERFACE include dirs, a generated export header, and install rules plus a CMake package config, so consumers can find_package(my-lib) and link my-lib::my-lib. The target is called <name>-core internally like in the other starters (so tests and cpp:embed find it) and is exported as <name>::<name>.
$ jen cpp:lib --name=MyLib cpp:lib (pack @codejen/pack-cpp) – create a library starter: include/<name>/, BUILD_INTERFACE/INSTALL_INTERFACE, export header, install + package config + my-lib/CMakeLists.txt + my-lib/src/CMakeLists.txt + my-lib/include/my-lib/my-lib.h + my-lib/src/my-lib.cpp + my-lib/cmake/my-libConfig.cmake.in ✓ 5 written
Params
name
project and target name; headers go in include/<name>/
folderCase
project folder name from --name: kebab (my-game) or pascal (MyGame)
dir
project folder instead of one named after --name; . for the current directory
cpp:module
Experimental. A C++20 modules starter: a .cppm module interface in a FILE_SET CXX_MODULES of the core library, imported by main.cpp. Needs CMake 3.28 or newer and a module-capable generator (cmake -G Ninja).
$ jen cpp:module --name=MyMod cpp:module (pack @codejen/pack-cpp) – (experimental) create a C++20 modules starter: .cppm in a FILE_SET CXX_MODULES, CMake >= 3.28, needs Ninja + my-mod/CMakeLists.txt + my-mod/src/CMakeLists.txt + my-mod/src/my_mod.cppm + my-mod/src/main.cpp ✓ 4 written
Params
name
project and target name
folderCase
project folder name from --name: kebab (my-game) or pascal (MyGame)
dir
project folder instead of one named after --name; . for the current directory
Core library and markers
The starters leave marker comments in the files they write, like # jen:sources in src/CMakeLists.txt. When you run another generator afterwards, say cpp:class, it inserts its new line at the matching marker instead of guessing where it belongs. jen looks for the nearest file with the marker, walking up from where the files go, so a subfolder can have its own CMakeLists.txt. If a project has no such marker (it wasn't made from a starter), the plan shows that insert as skipped: add the marker and run the generator again. Running it twice is safe: the second time it just reports "already present".
Each card below is one marker: the file it lives in, and the generators that insert there.
Markers
# jen:sources
cpp:class, cpp:interface --impl, cpp:pimpl
# jen:embed
cpp:embed, cpp:shader
# jen:link
cpp:warnings, cpp:compiler, cpp:tracy
# jen:app
cpp:icon (settings for the executable only)
# jen:options
cpp:tidy, cpp:warnings, cpp:compiler, cpp:tracy, cpp:cpack --staticSdl
# jen:subdirs
cpp:doctest
# jen:install, # jen:cpack
cpp:cpack (CPack settings come last, after every install())
# jen:tests
--withTest (set up by cpp:doctest)
// jen:includes
cpp:tracy
// jen:frame-end
cpp:tracy
// jen:gl-init
cpp:tracy --gpu
The tooling generators write a cmake/<name>.cmake and add an include(...) at # jen:options, and refer to the core library as ${PROJECT_NAME}-core.
Classes and types
cpp:class
Plain header/source class, wired into its CMakeLists.txt's sources list at the # jen:sources marker.
$ jen cpp:class --name=rigid_body --dir=src cpp:class (pack @codejen/pack-cpp) – create a C++ class (header + source), wired into the CMakeLists.txt with # jen:sources + src/RigidBody.h + src/RigidBody.cpp ~ src/CMakeLists.txt + RigidBody.cpp ✓ 3 written
cpp:class --moveOnly
A move-only RAII wrapper instead (Rule of Five): deleted copy, noexcept move via release()/reset(), [[nodiscard]] get()/release(), and an explicit operator bool(). Use it for C-APIs with a non-trivial destroy call (raii in my-cpp-snippets).
$ jen cpp:class --name=Texture --moveOnly --resource=GLuint --nullValue=0 --destroy=glDeleteTextures --dir=src cpp:class (pack @codejen/pack-cpp) – create a C++ class (header + source), wired into the CMakeLists.txt with # jen:sources + src/Texture.h + src/Texture.cpp ~ src/CMakeLists.txt + Texture.cpp ✓ 3 written
Params
name
the class, any case: player-controller and PlayerController both make PlayerController.h and PlayerController.cpp
namespace
C++ namespace to wrap it in, nested ones as engine::physics; empty means the global namespace
moveOnly
RAII wrapper: copy deleted, move defaulted (Rule of Five)
resource
with --moveOnly: the handle type it owns, e.g. SDL_Window* or GLuint
nullValue
with --moveOnly: the empty handle value, e.g. 0 for GLuint
destroy
with --moveOnly: the function that frees the handle, e.g. SDL_DestroyWindow
withTest
add a doctest case (needs cpp:doctest)
Every generator shows its params in the terminal too, with --help:
$ jen cpp:class --help
cpp:class – create a C++ class (header + source), wired into the CMakeLists.txt with # jen:sources
--name (required) the class, any case: player-controller and PlayerController both make PlayerController.h and PlayerController.cpp
--namespace="" C++ namespace to wrap it in, nested ones as engine::physics; empty means the global namespace
--moveOnly RAII wrapper: copy deleted, move defaulted (Rule of Five)
--resource=Resource* with --moveOnly: the handle type it owns, e.g. SDL_Window* or GLuint
--nullValue=nullptr with --moveOnly: the empty handle value, e.g. 0 for GLuint
--destroy=destroy with --moveOnly: the function that frees the handle, e.g. SDL_DestroyWindow
--withTest add a doctest case (needs cpp:doctest)
cpp:handle
A generic, header-only Handle<T, Deleter, Null> template for non-pointer handles (GLuint, a file descriptor, …), constrained with std::regular/std::invocable (raiit in my-cpp-snippets).
$ jen cpp:handle --name=Handle --dir=src cpp:handle (pack @codejen/pack-cpp) – create a generic Handle<T, Deleter, Null> RAII template for non-pointer handles (header-only) + src/Handle.h ✓ 1 written
Params
name
the template, any case: gl-handle and GlHandle both make GlHandle in GlHandle.h
namespace
C++ namespace to wrap it in, nested ones as engine::physics; empty means the global namespace
cpp:scopeexit
A header-only ScopeExit scope-guard template: runs a noexcept-checked callable on destruction unless release()d first (scopeexit in my-cpp-snippets).
$ jen cpp:scopeexit --name=ScopeExit --dir=src cpp:scopeexit (pack @codejen/pack-cpp) – create a ScopeExit scope-guard template (nothrow-enforced, header-only) + src/ScopeExit.h ✓ 1 written
Params
name
the template, any case: scope-exit and ScopeExit both make ScopeExit in ScopeExit.h
namespace
C++ namespace to wrap it in, nested ones as engine::physics; empty means the global namespace
cpp:r0
A Rule of Zero class: no special member functions, one constructor generated from --members, private members set from the moved-from constructor params (r0 in my-cpp-snippets).
$ jen cpp:r0 --name=Person --members="std::string name, int age" --dir=src cpp:r0 (pack @codejen/pack-cpp) – create a Rule of Zero class: members generated from constructor params, no special member functions (header-only) + src/Person.h ✓ 1 written
Params
name
the class, any case: movie-info and MovieInfo both make MovieInfo in MovieInfo.h
members
comma-separated "Type name" pairs, e.g. "std::string title, int year"
namespace
C++ namespace to wrap it in, nested ones as engine::physics; empty means the global namespace
compare
add a defaulted <=> (comparison and ==)
withTest
add a doctest case (needs cpp:doctest)
cpp:interface
A polymorphic base class (C.35, C.67, C.128): virtual defaulted destructor, protected copy/move to prevent slicing, pure virtual methods from --methods. With --impl it also scaffolds a final derived class with all the overrides, stub bodies in a .cpp wired in at # jen:sources. Not sure it's the right tool? See virtual, variant or templates?.
$ jen cpp:interface --name=Drawable --methods="void draw(), void resize(int w, int h)" --impl=SpriteDrawable --dir=src cpp:interface (pack @codejen/pack-cpp) – create a polymorphic interface class (virtual dtor, protected copy/move, pure virtual methods) + src/Drawable.h + src/SpriteDrawable.h + src/SpriteDrawable.cpp ~ src/CMakeLists.txt + SpriteDrawable.cpp ✓ 4 written
Params
name
the interface, any case: shape and Shape both make Shape in Shape.h
methods
comma-separated pure virtual methods, e.g. "void draw() const, bool resize(int w, int h)"
impl
also create a final class with this name that implements the interface
namespace
C++ namespace to wrap it in, nested ones as engine::physics; empty means the global namespace
withTest
add a doctest case (needs cpp:doctest)
cpp:struct
An aggregate for plain data bundles without an invariant (C.2, C.131): public members with default member initializers (C.48), so designated initializers work (Config{.width = 800}). Members without an initializer get {}.
$ jen cpp:struct --name=Config --members="int width = 800, int height = 600" --compare --dir=src cpp:struct (pack @codejen/pack-cpp) – create an aggregate struct with default member initializers for plain data bundles (header-only) + src/Config.h ✓ 1 written
Params
name
the struct, any case: window-config and WindowConfig both make WindowConfig in WindowConfig.h
members
comma-separated "Type name [= value]" entries, e.g. "int width = 800, int height = 600"
compare
add a defaulted <=> (comparison and ==)
namespace
C++ namespace to wrap it in, nested ones as engine::physics; empty means the global namespace
withTest
add a doctest case (needs cpp:doctest)
cpp:strong
A strong typedef (I.4): explicit constructor, value(), and optional operators. Keeps pixels, texels and world units apart.
$ jen cpp:strong --name=Pixels --underlying=int --ops=compare,arith --dir=src cpp:strong (pack @codejen/pack-cpp) – create a strong typedef: explicit constructor, optional <=> / arithmetic / std::hash (header-only) + src/Pixels.h ✓ 1 written
Params
name
the type, any case: pixels and Pixels both make Pixels in Pixels.h
underlying
the wrapped type, e.g. float or int
ops
comma-separated: compare (<=>), arith (+ - * /), hash (std::hash)
namespace
C++ namespace to wrap it in, nested ones as engine::physics; empty means the global namespace
withTest
add a doctest case (needs cpp:doctest)
cpp:enum
An enum class with to_string() and a std::formatter specialization (Enum.3), so std::print("{}", GameState::Playing) just works. The switch has no default on purpose: -Wswitch then warns when a value is added but to_string isn't updated. Header-only.
$ jen cpp:enum --name=GameState --values="menu, playing, paused" --dir=src cpp:enum (pack @codejen/pack-cpp) – create an enum class with to_string() and a std::formatter specialization (header-only) + src/GameState.h ✓ 1 written
Params
name
the enum, any case: game-state and GameState both make GameState in GameState.h
values
comma-separated enumerators, e.g. "idle, running, paused"
namespace
C++ namespace to wrap it in, nested ones as engine::physics; empty means the global namespace
std
C++ standard: 23, or 20 (std::abort instead of std::unreachable)
withTest
add a doctest case (needs cpp:doctest)
cpp:pimpl
A pimpl class (I.27): std::unique_ptr<Impl>, with the destructor and move operations defaulted in the .cpp so Impl can stay incomplete in the header. Easy to get wrong by hand.
$ jen cpp:pimpl --name=Renderer --dir=src cpp:pimpl (pack @codejen/pack-cpp) – create a pimpl class: unique_ptr<Impl>, special members defaulted in the .cpp; wired into the CMakeLists.txt with # jen:sources + src/Renderer.h + src/Renderer.cpp ~ src/CMakeLists.txt + Renderer.cpp ✓ 3 written
Params
name
the class, any case: renderer and Renderer both make Renderer.h and Renderer.cpp
namespace
C++ namespace to wrap it in, nested ones as engine::physics; empty means the global namespace
withTest
add a doctest case (needs cpp:doctest)
cpp:variant
A std::variant of small case structs plus the overloaded visitor helper (in its own src/overloaded.h, shared between variants): a modern alternative to a class hierarchy, e.g. for events or render commands. Often the better pick when the set of cases is closed, see virtual, variant or templates?.
$ jen cpp:variant --name=Event --cases="KeyDown, KeyUp, Resize" --dir=src cpp:variant (pack @codejen/pack-cpp) – create a std::variant of case structs plus the overloaded visitor helper (header-only) + src/overloaded.h + src/Event.h ✓ 2 written
Params
name
the variant, any case: input-event and InputEvent both make InputEvent in InputEvent.h, plus a shared overloaded.h
cases
comma-separated case structs, e.g. "click, keyPress, resize"
namespace
C++ namespace to wrap it in, nested ones as engine::physics; empty means the global namespace
withTest
add a doctest case (needs cpp:doctest)
--withTest
cpp:class, cpp:r0, cpp:struct, cpp:strong, cpp:enum, cpp:pimpl, cpp:variant and cpp:interface take --withTest. It scaffolds <Name>_test.cpp next to the header with a doctest case checking what the generator promises (move-only, aggregate, abstract, …), and adds it to the CMakeLists.txt that carries # jen:tests. The checks describe the class as generated, so update them when you change the design: give a class a constructor with arguments and the default-constructible check has to go. Run cpp:doctest first, otherwise only the test file is written and the insert is reported as skipped.
$ jen cpp:class --name=Camera --withTest --dir=src cpp:class (pack @codejen/pack-cpp) – create a C++ class (header + source), wired into the CMakeLists.txt with # jen:sources + src/Camera.h + src/Camera.cpp ~ src/CMakeLists.txt + Camera.cpp + src/Camera_test.cpp ~ tests/CMakeLists.txt + ../src/Camera_test.cpp ✓ 5 written
Virtual, variant or templates?
Polymorphism is still good practice, but it isn't the default. Pick by asking who can add new types:
open: plugins, drivers, code you haven't written yet
cpp:interface
closed: events, render commands, AST nodes
cpp:variant
known at compile time
cpp:handle, cpp:scopeexit show the style
just one operation
—
What does virtual cost in a game loop? A single virtual call is cheap: one indirect jump, well predicted when the same type shows up repeatedly. The expensive parts are around it:
- the call can't be inlined, so the compiler can't optimize across it or vectorize the loop;
- objects live behind pointers, usually scattered on the heap, so iterating them is a cache miss per element;
- with mixed types in one container, the indirect branch is hard to predict.
It rarely matters for a handful of objects per frame, and it matters a lot for the inner loop over thousands of particles or entities. Measure before you rewrite, then:
- keep hot loops free of virtual calls: store plain structs by value in contiguous containers, one container per type, and loop over each;
- call virtual once per batch (
renderer.draw(span_of_sprites)), not once per element; - mark leaf classes
final, which lets the compiler devirtualize calls it can see (cpp:interface --implalready generates afinalclass); - use
std::variantwhere it fits: values in astd::vector, no heap, no pointer chasing, and a missing case is a compile error.
Patterns are a vocabulary, not a rule book. A pattern name describes a solution that worked in a particular context; it doesn't say your code needs it. Before reaching for one, ask:
- What changes here, and who changes it? If nothing varies, nothing needs the indirection.
- Is there a second implementation yet? Turning a plain struct into an interface later is easy. Removing an abstraction nobody uses is not.
- What does it cost, and where? Runtime (the list above), compile time (templates), and reading time (every layer is one more file to open).
The Core Guidelines back the plain option more often than you might expect: use a struct for data without an invariant (C.2, cpp:struct), prefer concrete types over class hierarchies (C.10), and use a hierarchy only for concepts with inherent hierarchical structure (C.120). They are a neutral reference to point at in a design discussion, and they apply to patterns the same way they apply to everything else: "can you justify this?" is a fair question for a Factory and for a free function alike.
This is the core of the "clean code vs. performance" debate: hiding every decision behind small polymorphic objects reads nicely, but costs real throughput once it's in the hot path. Use virtual at the architectural seams (rendering backend, audio device, plugin boundary) and plain data in the loops.
Tooling
These work on an existing project made from a starter.
cpp:doctest
Sets up doctest via FetchContent: enable_testing(), a tests/ directory with a DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN main, a tests target linked against ${PROJECT_NAME}-core, doctest_discover_tests(), and the # jen:tests marker for --withTest. Param: doctestTag (default v2.4.12).
$ jen cpp:doctest cpp:doctest (pack @codejen/pack-cpp) – set up doctest: tests/ with a main, a tests target linked against <name>-core, ctest discovery, and the # jen:tests marker for --withTest + tests/CMakeLists.txt + tests/main.cpp ~ CMakeLists.txt + enable_testing() (+1 more) ✓ 3 written
doctest also supports inline tests in the .cpp files with DOCTEST_CONFIG_DISABLE in release builds. That's charming for small projects but scales worse, so it isn't scaffolded.
Params
doctestTag
doctest git tag to fetch
cpp:tidy
A .clang-tidy enabling cppcoreguidelines-*, modernize-*, bugprone-* and performance-*, with the checks that are noisy in graphics/SDL code (magic numbers, pointer arithmetic, varargs, …) switched off and explained in a comment. It also adds cmake/tidy.cmake, an opt-in -DCLANG_TIDY=ON that runs it during the build; --cmake=false skips that part.
$ jen cpp:tidy cpp:tidy (pack @codejen/pack-cpp) – add a .clang-tidy enforcing the C++ Core Guidelines, plus an opt-in CLANG_TIDY CMake option (cmake/tidy.cmake) + .clang-tidy + cmake/tidy.cmake ~ CMakeLists.txt + include(cmake/tidy.cmake) ✓ 3 written
Params
cmake
also add the CLANG_TIDY CMake option (cmake/tidy.cmake); --cmake=false for just .clang-tidy
cpp:format
A .clang-format matching the style the generators emit. Keep --indent and --columnLimit in line with your .editorconfig.
$ jen cpp:format cpp:format (pack @codejen/pack-cpp) – add a .clang-format matching the generated code style (Google-based), aligned with .editorconfig + .clang-format ✓ 1 written
Params
basedOn
clang-format base style, e.g. LLVM, Chromium, Mozilla, WebKit
indent
indent width in spaces
columnLimit
maximum line length
cpp:presets
A CMakePresets.json with debug, release, asan and ubsan configure, build and test presets (cmake --preset asan). The sanitizer presets are for GCC and Clang.
$ jen cpp:presets cpp:presets (pack @codejen/pack-cpp) – add a CMakePresets.json with debug, release, asan and ubsan presets + CMakePresets.json ✓ 1 written
cpp:warnings
A project_warnings INTERFACE target with -Wall -Wextra -Wpedantic -Wconversion -Wshadow (/W4 /permissive- on MSVC), linked privately into the core library so vendored code isn't affected. --werror adds -Werror / /WX.
$ jen cpp:warnings cpp:warnings (pack @codejen/pack-cpp) – add a project_warnings INTERFACE target (-Wall -Wextra … resp. /W4) and link it into the core library + cmake/warnings.cmake ~ CMakeLists.txt + include(cmake/warnings.cmake) ~ src/CMakeLists.txt + target_link_libraries(${PROJECT_NAME}-core PRIVATE project_warnings) ✓ 3 written
Params
werror
treat warnings as errors (-Werror, /WX)
cpp:compiler
Compiler settings that nudge the build towards the Core Guidelines, complementing cpp:warnings (general warnings) and cpp:tidy (static analysis). cmake/compiler.cmake sets build-wide conventions: CMAKE_CXX_EXTENSIONS OFF (-std=c++23, not gnu++23, P.2), CMAKE_CXX_STANDARD_REQUIRED, and compile_commands.json for clangd and clang-tidy. It also defines a project_options INTERFACE target, linked privately into the core library, with:
- guideline warnings, each commented with its rule:
-Wnon-virtual-dtor(C.35),-Woverloaded-virtual(C.138),-Wsuggest-override(C.128),-Wold-style-cast(ES.49),-Wcast-align(ES.48),-Wdouble-promotion(ES.46),-Wformat=2(ES.34),-Wimplicit-fallthrough(ES.78),-Wnull-dereference(ES.65) and-Werror=return-type;/utf-8 /Zc:__cplusplus /Zc:inline /EHscon MSVC - hardening: bounds-checked
operator[]in Debug via_GLIBCXX_ASSERTIONS(and libc++'s hardening modes),-fstack-protector-strong,_FORTIFY_SOURCE=2outside Debug, stack-clash protection and CET on Linux x86,/GS /guard:cf /sdlon MSVC.--hardening=falseleaves these out.
-Wuseless-cast is deliberately not enabled: it fires inside SDL's own macros. Note that the Debug-only bounds checks are inactive without a build type, so use the debug preset or -DCMAKE_BUILD_TYPE=Debug.
$ jen cpp:compiler cpp:compiler (pack @codejen/pack-cpp) – add guideline-oriented compiler settings (project_options: no extensions, guideline warnings, hardening) and link them into the core library + cmake/compiler.cmake ~ CMakeLists.txt + include(cmake/compiler.cmake) ~ src/CMakeLists.txt + target_link_libraries(${PROJECT_NAME}-core PRIVATE project_options) ✓ 3 written
Params
hardening
bounds-checked standard library and exploit mitigations (stack protector, fortify); --hardening=false to leave them out
Profiling
cpp:tracy
The Tracy profiler via FetchContent, linked into the core library. A PROFILING CMake option (default OFF) drives TRACY_ENABLE; Tracy's macros compile to nothing without it, so no wrapper header is needed. Inserts FrameMark; at // jen:frame-end. --gpu (OpenGL starter) also adds TracyGpuContext; at // jen:gl-init and TracyGpuCollect; before the frame mark.
$ jen cpp:tracy --gpu cpp:tracy (pack @codejen/pack-cpp) – add the Tracy profiler (PROFILING option, FetchContent) and a FrameMark at the app starter's frame end + cmake/tracy.cmake ~ CMakeLists.txt + include(cmake/tracy.cmake) ~ src/CMakeLists.txt + target_link_libraries(${PROJECT_NAME}-core PUBLIC Tracy::TracyClient) ~ src/app.cpp + #include <tracy/Tracy.hpp> ~ src/app.cpp + #include <tracy/TracyOpenGL.hpp> ~ src/app.cpp + TracyGpuContext; ~ src/app.cpp + TracyGpuCollect; ~ src/app.cpp + FrameMark; ✓ 4 written
Params
gpu
also profile OpenGL on the GPU (GPU zones)
file
the app source to hook into (default: the one with the starter's // jen:includes marker)
tracyTag
Tracy git tag to fetch
Then cmake -S . -B build -DPROFILING=ON.
Packaging
cpp:cpack
install() rules and CPack packaging, at # jen:install and # jen:cpack: the executable, an assets directory (skipped if it doesn't exist), CPACK_GENERATOR per platform (Windows ZIP;NSIS, macOS DragNDrop, Linux TGZ;DEB), an $ORIGIN RPATH so the unpacked binary finds its libraries, and a macOS .app bundle with cmake/Info.plist.in.
A shared SDL3 is shipped next to the executable (IMPORTED_RUNTIME_ARTIFACTS for an installed SDL3, install(TARGETS SDL3-shared) for a fetched one). --staticSdl builds SDL3 statically instead, often the simpler choice for games. The NSIS generator needs NSIS installed; cpack -G ZIP works without it.
$ jen cpp:cpack --staticSdl cpp:cpack (pack @codejen/pack-cpp) – add install rules and CPack packaging (ZIP/NSIS, DragNDrop, TGZ/DEB), SDL3 runtime, RPATH and a macOS bundle + cmake/Info.plist.in ~ CMakeLists.txt + set(SDL_STATIC ON) (+1 more) ~ CMakeLists.txt + include(GNUInstallDirs) (+25 more) ~ CMakeLists.txt + set(CPACK_PACKAGE_NAME "${PROJECT_NAME}") (+10 more) ✓ 2 written
Params
assets
folder to install next to the app, if it exists
staticSdl
link SDL3 statically instead of shipping its shared library
bundleId
macOS bundle identifier (default: com.example.<project>)
maintainer
package vendor and Debian maintainer, e.g. "Jane Doe <jane@example.com>"
cpp:icon
Platform app resources, separate from embedding: a Windows resources/app.rc that attaches an icon to the executable, and a macOS .icns copied into the bundle's Resources, both wired in at # jen:app. jen only writes text, so drop your own resources/app.ico and resources/app.icns in (--ico and --icns change the paths). Pairs with cpp:cpack.
$ jen cpp:icon cpp:icon (pack @codejen/pack-cpp) – wire a Windows .rc icon and a macOS .icns bundle icon into the executable (you supply the icon files) + resources/app.rc ~ src/CMakeLists.txt + if(WIN32) (+6 more) ✓ 2 written
Params
ico
Windows icon, relative to the project root
icns
macOS icon, relative to the project root
Resources
cpp:shader
Embeds a GLSL file you already have: write it with glsl:frag from the glsl pack, or by hand. cpp:shader turns it into a <file>.h header holding the source as a C string at CMake-configure time, using cmake/embed-glsl.cmake (added if missing). It writes no GLSL of its own; it only wires embed_glsl(...) in at the # jen:embed marker, with the shader's path relative to that CMakeLists.txt. Edit the .glsl later and just re-run cmake.
$ jen cpp:shader --file=shaders/vignette.frag.glsl cpp:shader (pack @codejen/pack-cpp) – embed an existing GLSL file as a C string via cmake/embed-glsl.cmake, wired in at # jen:embed (write the shader with glsl:frag or by hand) = cmake/embed-glsl.cmake exists, skipped ~ src/CMakeLists.txt + include(${PROJECT_SOURCE_DIR}/cmake/embed-glsl.cmake) ~ src/CMakeLists.txt + embed_glsl("../shaders/vignette.frag.glsl" vignetteFragmentShader) ✓ 2 written
If the file doesn't exist yet, jen says so and points at glsl:frag.
Params
file
the GLSL file to embed, e.g. shaders/tonemap.frag.glsl
name
C++ variable name (default: from the file name, e.g. tonemapFragmentShader)
cpp:embed
Cross-platform binary embedding. #embed isn't portable yet (C only in Clang/GCC 15, C++26 for C++, no MSVC), so the byte array is generated at build time with add_custom_command: assets are re-embedded when they change, without re-running cmake. Then #include "tilesPng.h" and use SDL_IOFromConstMem(tilesPng.data(), tilesPng.size()).
$ jen cpp:embed --file=assets/tiles.png --name=tilesPng cpp:embed (pack @codejen/pack-cpp) – embed a binary file as a constexpr byte array, generated at build time by cmake/embed.cmake and wired in at # jen:embed + cmake/embed-file.cmake + cmake/embed.cmake ~ src/CMakeLists.txt + include(${PROJECT_SOURCE_DIR}/cmake/embed.cmake) ~ src/CMakeLists.txt + embed_file(${PROJECT_NAME}-core assets/tiles.png tilesPng) ✓ 3 written
Params
file
the binary file to embed, relative to the project root
name
C++ variable name for the byte array
target
CMake target that gets the array (default: <project>-core)
method
how to embed: only cmake so far (#embed isn't portable yet)
CMake's regex gets noticeably slow for assets in the tens of MB, and each generated header should be included from exactly one .cpp, since large constexpr arrays cost compile time in every translation unit.
Putting it together
The starters set up the markers the other generators insert into, so a starter plus scaffolding is just a few commands:
$ jen cpp:sdl3-opengl --name=MyGame … $ jen glsl:frag --name=vignette --version=410 … $ jen cpp:shader --file=shaders/vignette.frag.glsl … $ jen cpp:class --name=Camera … $ cmake -S . -B build && cmake --build build
And with the tooling on top:
$ jen cpp:doctest && jen cpp:warnings && jen cpp:compiler && jen cpp:tidy … $ jen cpp:class --name=Camera --withTest … $ jen cpp:enum --name=GameState --values="menu, playing, paused" --withTest … $ jen cpp:tracy --gpu … $ cmake -S . -B build && cmake --build build && ctest --test-dir build
C++ standard
The starters build as C++23 (cxx_std_23), which cpp:enum's std::unreachable() and std::format formatter rely on. cpp:enum --std=20 is there for projects that stay on C++20.