Skip to content

Commit 7d0ae52

Browse files
committed
Initialize C++/Python tool template with CMake, pip, and Docker support. This commit lays the foundation for a reusable template that combines a C++ executable built with CMake and a Python CLI wrapper
1 parent b8046b8 commit 7d0ae52

File tree

12 files changed

+521
-1
lines changed

12 files changed

+521
-1
lines changed

.gitignore

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
# ------------------------
2+
# Mac OS
3+
# ------------------------
4+
.DS_Store
5+
.AppleDouble
6+
.LSOverride
7+
8+
# Icon must end with two \r
9+
Icon
10+
11+
# Thumbnails
12+
._*
13+
14+
# Files that might appear on external disks
15+
.Spotlight-V100
16+
.Trashes
17+
18+
# ------------------------
19+
# Build directories
20+
# ------------------------
21+
# CMake build output
22+
/build/
23+
/_build/
24+
/_build_*/
25+
/CMakeFiles/
26+
/CMakeCache.txt
27+
/*.cmake.user
28+
/*.cmake.user.*
29+
/Makefile
30+
/compile_commands.json
31+
32+
# Scikit-build temporary directories
33+
/_skbuild/
34+
/wheelhouse/
35+
36+
/dist/
37+
/*.egg-info/
38+
/.build/
39+
40+
# CLion specific
41+
cmake-build-debug/
42+
CMakeSettings.json
43+
.idea/ # if using CLion’s shared project files
44+
45+
# ------------------------
46+
# Python virtual environments
47+
# ------------------------
48+
# Common names for virtual environments
49+
/venv/
50+
/env/
51+
/.env/
52+
/.venv/
53+
/test_env/
54+
/__venv__/
55+
/pip-wheel-metadata/
56+
/*.egg
57+
*.egg-info/
58+
/*.egg/
59+
60+
/python_env/
61+
/python-cache/
62+
/__pycache__/
63+
/*.py[cod]
64+
*$py.class
65+
66+
# pip cache
67+
pip-log.txt
68+
69+
# pytest cache
70+
/.pytest_cache/
71+
/.vscode/pytest
72+
73+
# coverage output
74+
htmlcov/
75+
/.coverage
76+
.coverage.*
77+
.cache
78+
nosetests.xml
79+
coverage.xml
80+
*.cover
81+
*.py,cover
82+
83+
# mypy
84+
.mypy_cache/
85+
.dmypy.json
86+
87+
# pytype
88+
.pytype/
89+
90+
# ------------------------
91+
# PyCharm
92+
# ------------------------
93+
.idea/
94+
*.iml
95+
*.ipr
96+
*.iws
97+
98+
# ------------------------
99+
# CLion
100+
# ------------------------
101+
# (Already covered by .idea and cmake-build-debug)
102+
103+
# ------------------------
104+
# Logs and databases
105+
# ------------------------
106+
*.log
107+
*.sqlite3
108+
*.db
109+
110+
# ------------------------
111+
# Docker
112+
# ------------------------
113+
/docker-compose.override.yml
114+
Dockerfile.*
115+
116+
# ------------------------
117+
# Conda build output
118+
# ------------------------
119+
/conda-recipe/build_artifacts/
120+
/conda-bld/
121+
/pkgs/
122+
123+
# ------------------------
124+
# Others
125+
# ------------------------
126+
*.swp
127+
*~
128+
._*
129+
*.tmp

CMakeLists.txt

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
cmake_minimum_required(VERSION 3.15...3.26)
2+
project(cpp_python_tool_template VERSION 0.1 LANGUAGES CXX)
3+
set(CMAKE_CXX_STANDARD 20 CACHE STRING "C++ standard")
4+
5+
# --- Configure doctest as an external project: ---
6+
include(ExternalProject)
7+
find_package(Git REQUIRED)
8+
9+
ExternalProject_Add(
10+
doctest
11+
PREFIX ${CMAKE_BINARY_DIR}/doctest
12+
GIT_REPOSITORY https://github.com/doctest/doctest.git
13+
TIMEOUT 10
14+
UPDATE_COMMAND ${GIT_EXECUTABLE} pull
15+
CONFIGURE_COMMAND ""
16+
BUILD_COMMAND ""
17+
INSTALL_COMMAND ""
18+
LOG_DOWNLOAD ON
19+
)
20+
21+
# Expose required variable (DOCTEST_INCLUDE_DIR) to parent scope
22+
ExternalProject_Get_Property(doctest source_dir)
23+
set(DOCTEST_INCLUDE_DIR ${source_dir}/doctest CACHE INTERNAL "Path to include folder for doctest")
24+
25+
# --- Application executable ---
26+
add_executable(linecount src/main.cpp)
27+
install(TARGETS linecount RUNTIME DESTINATION bin)
28+
29+
# --- Test executable ---
30+
# Make test executable
31+
add_executable(test_linecount tests/cpp/test_main.cpp)
32+
target_include_directories(test_linecount PUBLIC ${DOCTEST_INCLUDE_DIR})
33+
34+
# Make test_linecount wait until doctest has finished downloading
35+
add_dependencies(test_linecount doctest)
36+
37+
enable_testing()
38+
add_test(NAME linecount_test COMMAND test_linecount)

Dockerfile

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# ----------------------------------------------------------------------------
2+
# Stage 1: Build C++ + Python wheel
3+
# ----------------------------------------------------------------------------
4+
FROM ubuntu:22.04 AS builder
5+
6+
# 1) Install build tools, CMake, Git, Python headers, and pip
7+
RUN apt-get update && apt-get install -y \
8+
build-essential \
9+
cmake \
10+
git \
11+
python3-dev \
12+
python3-pip \
13+
&& rm -rf /var/lib/apt/lists/*
14+
15+
WORKDIR /app
16+
17+
# 2) Copy entire repo into /app
18+
COPY . .
19+
20+
# 3) Upgrade pip and install scikit-build so that "pip wheel ." will run CMake
21+
RUN pip3 install --upgrade pip setuptools scikit-build
22+
23+
# 4) Manually configure, compile, and test the C++ code
24+
# This builds 'linecount' and 'test_linecount', then runs CTest.
25+
RUN mkdir build && cd build \
26+
&& cmake .. -DCMAKE_BUILD_TYPE=Release \
27+
&& cmake --build . --parallel $(nproc) \
28+
&& ctest --output-on-failure
29+
30+
# 4) Build a wheel into /wheelhouse
31+
RUN pip3 wheel . -w /wheelhouse
32+
33+
# (At this point, /wheelhouse/ contains something like:
34+
# aligncount_demo-0.1.0-py3-none-any.whl )
35+
36+
37+
# ----------------------------------------------------------------------------
38+
# Stage 2: Final runtime image
39+
# ----------------------------------------------------------------------------
40+
FROM ubuntu:22.04
41+
42+
# 1) Install only runtime dependencies: Python 3 and samtools (if your wrapper needs it)
43+
RUN apt-get update && apt-get install -y \
44+
python3 \
45+
python3-pip \
46+
samtools \
47+
&& rm -rf /var/lib/apt/lists/*
48+
49+
WORKDIR /app
50+
51+
# 2) Copy the wheel (using wildcard) from the builder stage
52+
COPY --from=builder /wheelhouse/*.whl /tmp/
53+
54+
# 3) Upgrade pip, then install whichever .whl we copied
55+
RUN pip3 install --upgrade pip \
56+
&& pip3 install /tmp/*.whl \
57+
&& rm /tmp/*.whl
58+
59+
# Copy only the Python unit test
60+
COPY tests/python/test_cli.py /app/tests/python/test_cli.py
61+
62+
# Install pytest to run our simple unit test
63+
RUN pip3 install pytest
64+
65+
# Run only the format_output() unit test
66+
RUN pytest -q /app/tests/python/test_cli.py
67+
68+
# 4) Verify the binaries are on PATH (just a check; you can remove)
69+
RUN which linecount && which aligncount
70+
71+
# Final entrypoint: run the Python wrapper by default
72+
ENTRYPOINT ["aligncount"]
73+
CMD ["--help"]

README.md

Lines changed: 147 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,147 @@
1-
# cpp-python-tool-template
1+
# C++/Python Tool Template
2+
3+
A reusable starter template for building a CLI tool that combines:
4+
5+
- A **C++ executable** (compiled with CMake)
6+
- A **Python wrapper** (installed via pip)
7+
- Built-in **unit tests** (C++ and Python)
8+
- A **multi-stage Dockerfile** (builds, tests, and packages a wheel)
9+
- [TODO] A **Conda recipe** (creates a cross-platform package for Linux/macOS)
10+
11+
This template lets you jump straight into developing bioinformatics or similar tools without wiring up all the build, test, container, and packaging boilerplate from scratch.
12+
13+
---
14+
15+
## Repository Layout
16+
17+
├── CMakeLists.txt
18+
├── setup.py
19+
├── setup.cfg
20+
├── pyproject.toml
21+
├── src/
22+
│ └── main.cpp ← C++ “linecount” implementation
23+
├── cli/
24+
│ ├── __init__.py
25+
│ └── entrypoint.py ← Python wrapper (“aligncount” console script)
26+
├── tests/
27+
│ ├── cpp/
28+
│ │ └── test_main.cpp ← C++ unit test (Doctest + CTest)
29+
│ └── python/
30+
│ └── test_wrapper.py ← Python unit test for format_output()
31+
├── Dockerfile ← Multi-stage Docker build + test
32+
└── .gitignore
33+
34+
---
35+
36+
## C++ Executable (linecount)
37+
38+
- Source: src/main.cpp
39+
- Build system: CMake (CMakeLists.txt)
40+
- Functionality: Counts non-header lines (simulates an “alignment counter”)
41+
- Unit test: tests/cpp/test_main.cpp using Doctest; run via CTest
42+
43+
To build manually:
44+
45+
mkdir build
46+
cd build
47+
cmake .. -DCMAKE_BUILD_TYPE=Release
48+
cmake --build . --parallel $(nproc)
49+
# Now ./linecount is available in build/
50+
./linecount sample.sam
51+
52+
---
53+
54+
## Python Wrapper (aligncount)
55+
56+
- Source: cli/entrypoint.py
57+
- Entry point: aligncount console script (configured in setup.py)
58+
- Logic:
59+
1. Parses subcommands (count-mapped or count-unmapped)
60+
2. Checks that the input SAM file exists and is non-empty
61+
3. Calls `linecount <path>` and formats output via format_output(cmd, raw_bytes)
62+
63+
To install in a virtualenv:
64+
65+
python3 -m venv venv
66+
source venv/bin/activate
67+
pip install --upgrade pip setuptools scikit-build pytest
68+
pip install .
69+
# Now you have:
70+
# - linecount (C++ binary, in venv/bin)
71+
# - aligncount (Python wrapper, in venv/bin)
72+
73+
Run the wrapper (just counts file lines):
74+
75+
aligncount count-mapped -i sample.sam
76+
aligncount count-unmapped -i sample.sam
77+
78+
---
79+
80+
## Unit Tests
81+
82+
### C++ Tests
83+
84+
- Framework: Doctest (fetched via ExternalProject_Add in CMake)
85+
- Test file: tests/cpp/test_main.cpp
86+
- Run:
87+
88+
cd build
89+
ctest --output-on-failure
90+
91+
### Python Tests
92+
93+
- Framework: pytest
94+
- Test file: tests/python/test_wrapper.py
95+
- Run:
96+
97+
source venv/bin/activate
98+
pytest -q
99+
100+
---
101+
102+
## Dockerfile
103+
104+
A multi-stage Dockerfile builds and tests everything, then produces a minimal runtime image:
105+
106+
1. Builder stage (ubuntu:22.04):
107+
- Installs build-time dependencies (CMake, Git, Python dev headers, pip)
108+
- Compiles C++ (linecount) and runs CTest
109+
- Builds a Python wheel (bundles linecount + aligncount)
110+
111+
2. Final stage (ubuntu:22.04):
112+
- Installs runtime dependencies (Python, pip, samtools)
113+
- Installs the wheel (placing linecount and aligncount into /usr/local/bin)
114+
- Runs the Python unit test (pytest test_wrapper.py)
115+
- Sets ENTRYPOINT ["aligncount"]
116+
117+
Build and test in one command:
118+
119+
docker build -t your-org/aligncount-demo:latest .
120+
121+
Run:
122+
123+
docker run --rm your-org/aligncount-demo:latest --help
124+
125+
---
126+
127+
## [TODO] Conda Recipe
128+
129+
---
130+
131+
## .gitignore
132+
133+
Included patterns to ignore:
134+
- macOS system files (.DS_Store, ._*)
135+
- CMake and CLion build directories (build/, cmake-build-debug/, _skbuild/, wheelhouse/)
136+
- Python venvs and caches (venv/, __pycache__/, .pytest_cache/, *.egg-info/)
137+
- IDE files (PyCharm/CLion .idea/, *.iml)
138+
- Conda build artifacts (conda-bld/, pkgs/)
139+
140+
---
141+
142+
## What’s Next
143+
144+
- GitHub Actions: Add workflows to automatically build/test on every push/PR to main, and to release Docker images and Conda packages when tags are pushed.
145+
- Customize: Adapt this template to any C++/Python CLI project by renaming targets, adjusting dependencies, and updating metadata (license, homepage, etc).
146+
147+
With this template, you have a fully tested building, packaging, and distribution pipeline out of the box. Happy coding!

cli/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)