Skip to content

Instantly share code, notes, and snippets.

@kristopherjohnson
Last active March 11, 2025 02:31
Show Gist options
  • Save kristopherjohnson/17fc6b6d0f02ac4e2fd41bd2e2fac289 to your computer and use it in GitHub Desktop.
Save kristopherjohnson/17fc6b6d0f02ac4e2fd41bd2e2fac289 to your computer and use it in GitHub Desktop.
Script for generating a new CMake-based C++ project
#!/usr/bin/env python3
"""
generate_cpp_project.py
This script generates a standard C++ project structure with CMake build system.
The generated project includes:
- A main executable
- A library with header files
- Unit tests using Doctest
- CMake configuration with CPack support
- Makefile with common targets
- README, LICENSE, and .gitignore files
Usage:
./generate_cpp_project.py [options]
Options:
--name NAME Set the project name (default: example)
--std STD Set the C++ standard (default: 17)
-h, --help Show this help message
"""
import os
import sys
import argparse
import shutil
from pathlib import Path
def parse_args():
"""Parse command line arguments."""
parser = argparse.ArgumentParser(
description="Generate a standard C++ project structure with CMake build system."
)
parser.add_argument(
"--name", default="example", help="Name of the project (default: example)"
)
parser.add_argument(
"--std", default="17", help="C++ standard to use (default: 17)"
)
return parser.parse_args()
def create_directory_structure(project_name):
"""Create the project directory structure."""
directories = [
"",
"lib",
"lib/src",
"lib/include",
f"lib/include/{project_name}",
"src",
"test",
]
for directory in directories:
Path(f"{project_name}/{directory}").mkdir(parents=True, exist_ok=True)
print(f"Created directory structure for project '{project_name}'")
def create_cmakelists(project_name, cpp_standard):
"""Create the main CMakeLists.txt file."""
cmakelists_content = f"""cmake_minimum_required(VERSION 3.14)
project({project_name} VERSION 0.1.0 LANGUAGES CXX)
# Set C++ standard
set(CMAKE_CXX_STANDARD {cpp_standard})
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
# Include directories
include_directories(lib/include)
# Set output directories
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${{CMAKE_BINARY_DIR}}/lib)
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${{CMAKE_BINARY_DIR}}/lib)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${{CMAKE_BINARY_DIR}}/bin)
# Build library
file(GLOB LIB_SOURCES "lib/src/*.cpp")
add_library({project_name}_lib ${{LIB_SOURCES}})
target_include_directories({project_name}_lib PUBLIC lib/include)
# Build executable
file(GLOB APP_SOURCES "src/*.cpp")
add_executable({project_name} ${{APP_SOURCES}})
target_link_libraries({project_name} PRIVATE {project_name}_lib)
# Testing with Doctest
include(FetchContent)
FetchContent_Declare(
doctest
URL https://github.com/doctest/doctest/archive/v2.4.11.tar.gz
)
FetchContent_MakeAvailable(doctest)
# Build tests
file(GLOB TEST_SOURCES "test/*.cpp")
add_executable(run_tests ${{TEST_SOURCES}})
target_link_libraries(run_tests PRIVATE {project_name}_lib doctest::doctest)
target_compile_definitions(run_tests PRIVATE DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN)
enable_testing()
add_test(NAME unit_tests COMMAND run_tests)
# Installation
install(TARGETS {project_name}
RUNTIME DESTINATION bin
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib)
install(TARGETS {project_name}_lib
RUNTIME DESTINATION bin
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib)
install(DIRECTORY lib/include/ DESTINATION include)
# CPack configuration
include(InstallRequiredSystemLibraries)
set(CPACK_RESOURCE_FILE_LICENSE "${{CMAKE_CURRENT_SOURCE_DIR}}/LICENSE")
set(CPACK_PACKAGE_VERSION_MAJOR "${{PROJECT_VERSION_MAJOR}}")
set(CPACK_PACKAGE_VERSION_MINOR "${{PROJECT_VERSION_MINOR}}")
set(CPACK_PACKAGE_VERSION_PATCH "${{PROJECT_VERSION_PATCH}}")
include(CPack)
"""
with open(f"{project_name}/CMakeLists.txt", "w") as f:
f.write(cmakelists_content)
print(f"Created CMakeLists.txt")
def create_library_files(project_name):
"""Create the library source and header files."""
# Header file
header_content = f"""#ifndef {project_name.upper()}_LIBRARY_H
#define {project_name.upper()}_LIBRARY_H
namespace {project_name} {{
/**
* @brief A simple function that returns an integer
* @param value The input value
* @return The input value multiplied by 2
*/
int multiply_by_two(int value);
}} // namespace {project_name}
#endif // {project_name.upper()}_LIBRARY_H
"""
with open(f"{project_name}/lib/include/{project_name}/library.h", "w") as f:
f.write(header_content)
# Source file
source_content = f"""#include "{project_name}/library.h"
namespace {project_name} {{
int multiply_by_two(int value) {{
return value * 2;
}}
}} // namespace {project_name}
"""
with open(f"{project_name}/lib/src/library.cpp", "w") as f:
f.write(source_content)
print(f"Created library files")
def create_main_file(project_name):
"""Create the main source file."""
main_content = f"""#include <iostream>
#include "{project_name}/library.h"
int main(int argc, char* argv[]) {{
try {{
std::cout << "Welcome to {project_name}!" << std::endl;
int input = 21;
int result = {project_name}::multiply_by_two(input);
std::cout << input << " multiplied by 2 is " << result << std::endl;
return 0;
}} catch (const std::exception& e) {{
std::cerr << "error: " << e.what() << std::endl;
return 1;
}}
}}
"""
with open(f"{project_name}/src/main.cpp", "w") as f:
f.write(main_content)
print(f"Created main.cpp")
def create_test_file(project_name):
"""Create the test file."""
test_content = f"""#include <doctest/doctest.h>
#include "{project_name}/library.h"
TEST_CASE("Testing the multiply_by_two function") {{
SUBCASE("Testing with positive values") {{
CHECK({project_name}::multiply_by_two(1) == 2);
CHECK({project_name}::multiply_by_two(2) == 4);
CHECK({project_name}::multiply_by_two(10) == 20);
}}
SUBCASE("Testing with zero") {{
CHECK({project_name}::multiply_by_two(0) == 0);
}}
SUBCASE("Testing with negative values") {{
CHECK({project_name}::multiply_by_two(-1) == -2);
CHECK({project_name}::multiply_by_two(-5) == -10);
}}
}}
"""
with open(f"{project_name}/test/test_main.cpp", "w") as f:
f.write(test_content)
print(f"Created test_main.cpp")
def create_makefile(project_name):
"""Create the Makefile."""
makefile_content = """# Makefile for CMake-based C++ project
.PHONY: all build test run clean install package debug release
# Default build type
BUILD_TYPE ?= Debug
all: build
build:
cmake -S . -B build -DCMAKE_BUILD_TYPE=$(BUILD_TYPE) -Wno-dev
cmake --build build --config $(BUILD_TYPE)
test: build
cd build && ctest -C $(BUILD_TYPE) --output-on-failure
run: build
./build/bin/$(notdir $(CURDIR))
clean:
rm -rf build
install: build
cmake --install build --config $(BUILD_TYPE)
package: build
cd build && cpack -C $(BUILD_TYPE)
debug:
$(MAKE) BUILD_TYPE=Debug build
release:
$(MAKE) BUILD_TYPE=Release build
"""
with open(f"{project_name}/Makefile", "w") as f:
f.write(makefile_content)
print(f"Created Makefile")
def create_readme(project_name, cpp_standard):
"""Create the README.md file."""
readme_content = f"""# {project_name}
A C++ project template with CMake build system.
## Requirements
- CMake 3.14 or higher
- C++{cpp_standard} compatible compiler
## Building the Project
### Using CMake Directly
```bash
# Configure
cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug
# Build
cmake --build build --config Debug
```
For a Release build:
```bash
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build --config Release
```
### Using the Makefile
```bash
# Debug build (default)
make build
# Release build
make BUILD_TYPE=Release build
# or
make release
```
## Running
```bash
# Using CMake build output directly
./build/bin/{project_name}
# Using Makefile
make run
```
## Testing
```bash
# Using CTest directly
cd build && ctest -C Debug --output-on-failure
# Using Makefile
make test
```
## Installing
```bash
# Using CMake directly
cmake --install build
# Using Makefile
make install
```
## Packaging
```bash
# Using CPack directly
cd build && cpack -C Debug
# Using Makefile
make package
```
## Project Structure
- `lib/`: Library code
- `include/`: Library header files
- `src/`: Library source files
- `src/`: Main application source files
- `test/`: Test source files
## License
This project is licensed under the MIT License - see the LICENSE file for details.
"""
with open(f"{project_name}/README.md", "w") as f:
f.write(readme_content)
print(f"Created README.md")
def create_gitignore():
"""Create a .gitignore file."""
gitignore_content = """# Prerequisites
*.d
# Compiled Object files
*.slo
*.lo
*.o
*.obj
# Precompiled Headers
*.gch
*.pch
# Compiled Dynamic libraries
*.so
*.dylib
*.dll
# Fortran module files
*.mod
*.smod
# Compiled Static libraries
*.lai
*.la
*.a
*.lib
# Executables
*.exe
*.out
*.app
# Build directories
build/
out/
cmake-build-*/
# IDE files
.idea/
.vscode/
.vs/
*.swp
*.swo
*~
# CMake
CMakeLists.txt.user
CMakeCache.txt
CMakeFiles/
CMakeScripts/
Testing/
Makefile.in
cmake_install.cmake
install_manifest.txt
compile_commands.json
CTestTestfile.cmake
_deps/
# macOS
.DS_Store
.AppleDouble
.LSOverride
# Windows
Thumbs.db
ehthumbs.db
Desktop.ini
# Package files
*.tar.gz
*.tar.bz2
*.zip
*.deb
*.rpm
*.dmg
*.pkg
"""
with open(f"{project_name}/.gitignore", "w") as f:
f.write(gitignore_content)
print(f"Created .gitignore")
def create_license(project_name):
"""Create a LICENSE file with MIT license."""
current_year = 2025 # Using current year from the description
license_content = f"""MIT License
Copyright (c) {current_year} {project_name}
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.
"""
with open(f"{project_name}/LICENSE", "w") as f:
f.write(license_content)
print(f"Created LICENSE file")
def main():
"""Main function."""
args = parse_args()
global project_name
project_name = args.name
global cpp_standard
cpp_standard = args.std
print(f"Generating C++ project '{project_name}' with C++{cpp_standard} standard...")
# Create project structure
create_directory_structure(project_name)
create_cmakelists(project_name, cpp_standard)
create_library_files(project_name)
create_main_file(project_name)
create_test_file(project_name)
create_makefile(project_name)
create_readme(project_name, cpp_standard)
create_gitignore()
create_license(project_name)
print(f"\nProject '{project_name}' has been generated successfully!")
print(f"To build the project:")
print(f" cd {project_name}")
print(f" make build")
if __name__ == "__main__":
main()
@kristopherjohnson
Copy link
Author

kristopherjohnson commented Mar 2, 2025

Here is the prompt I gave to Claude.ai to produce the first version of this script:

You are going to create a Python script that can be used by a software developer to generate a C++ project. The generated project will contain simple example code that can be built and tested, but it is expected that the user will replace that code with their own code after generation.

The Python script, named `generate_cpp_project.py`, must have these properties:

- Python 3
- uses only standard Python library modules
- has a doc string at the top explaining what the script does and how to use it
- a command option parser using the argparse module
- starting with a shebang line so it can be run directly from the command line on unix systems
- easy to read and modify
- long multiline strings should be formatted as multiline strings
- take care to properly escape characters in string literals and to handle special characters when substituting text into a string

The default name for the project generated by the script is "example". A command line option `--name` can be used to change the name.

The script will create these things:

- a `CMakeLists.txt` file to be used with CMake to build the project.
- a `lib` directory with subdirectories `src` and `include`. These are used to build a library. There will be a simple .cpp file in `src` and a simple header file in `include`. The header file should include appropriate guard macros to prevent re-inclusion.
- a `src` directory with a `main.cpp` file in it. The main.cpp file will contain a `main()` function that calls a function in the library and prints the result.
- a `test` directory with a `test_main.cpp` file in it. The `test_main.cpp` file will be a unit test runner using the Doctest testing framework
- a `Makefile` with targets `build`, `test`, `run`, `install`, `package` and `clean` that run the appropriate CMake, CTest, and CPack commands
- a `README.md` file that explains how to build and run the program. It should explain how to choose configuration for a (default) Debug build or a Release build
- a `.gitignore` file with appropriate rules for a C++ and CMake-based project on Windows, Linux, and macOS
- A `LICENSE` file (which will be needed by CPack) with complete text of an MIT-style open-source license

The CMakeLists.txt file will be set up as follows:
- CMake 3.14 or higher is required
- CMakeLists.txt will treat all .cpp files in the `lib/src` directory as input files for the library, using a wildcard. If the user adds more .cpp files to that directory, they will automatically be compiled and included in the library. The `lib/include` directory will be in the include path for all targets.
- CMakeLists.txt will treat all .cpp files in the `src` directory as input files for the executable, using a wildcard. If the user adds more .cpp files to that directory, they will automatically be compiled and linked into the executable.
- CMakeLists.txt will treat all .cpp files in the `test` directory as input files for the test runner, using a wildcard. The test runner will be linked to the library
- CPack will be supported
- CMakeLists.txt will automatically download Doctest 2.4.11 using FetchContent
- The default C++ standard will be C++17. A command line option `--std` can be used to change this

The Makefile will be set up as follows:
- It will include .PHONY directives for targets that are not associated with output files
- For CMake, CTest, and CPack, it will use the `-S` option rather than using `cd` commands to change directories, and use `-Wno-dev` to suppress CMake dev warnings

Claude neglected to include the necessary global declarations in main(), but otherwise did well.

I tried this prompt with a few ChatGPT models as well, but the scripts it generated were terrible.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment