spl-core is a CMake module framework for Software Product Line (SPL) support. It enables managing multiple product variants from a single repository through CMake-based build integration, KConfig feature models for compile-time configuration, automated unit test execution with mock generation (via Hammocking), and coverage/documentation generation per variant.
Internal architecture docs: see
docs/internals/. Architecture decisions (ADRs, underdocs/internals/decisions/) and requirements (underdocs/internals/requirements/) live there and must be consulted when analysing a task — check for an existing decision before changing architecture, and record significant new decisions as an ADR.
src/spl_core/
├── main.py # CLI entry point ("please" command for project init)
├── __init__.py # Package version (__version__)
├── __run.py # Script runner
├── common/ # Shared Python utilities (path helpers)
├── config/ # KConfig default configuration
├── kconfig/ # KConfig integration (feature model parsing via kconfiglib)
├── kickstart/ # Project template generation (cookiecutter)
│ └── templates/ # project/ and application/ scaffolds
├── gcov_maid/ # Coverage report cleanup (gcda/gcno management)
├── report_generation/ # Sphinx documentation integration
├── steps/ # Pipeline steps (CollectPRChanges)
├── test_utils/ # Test infrastructure (SplBuild, JUnit merger, artifacts)
├── common.cmake # Core CMake macros (spl_add_component, test suites, hammocking)
├── spl.cmake # Main SPL CMake module (variant/build-kit setup)
├── kconfig.cmake # KConfig CMake integration
└── conan.cmake # Conan package manager integration
Key CMake macros (in common.cmake):
spl_add_source()— Register production source files for a componentspl_add_test_source()— Register test source filesspl_create_component()— Create a component with build targets_spl_add_test_suite()— Internal: sets up test executable, hammocking mock generation, coverage collection, and JUnit reporting
Key Python classes (in test_utils/):
SplBuild— Python wrapper around CMake/Ninja build orchestrationBaseVariantTestRunner— Per-variant test execution (legacy)JunitMerger— Merges JUnit XML reports from multiple components
Entry points (defined in pyproject.toml):
please→spl_core.main:main(project initialization CLI)junit_merger→spl_core.test_utils.junit_merger:main
- Language: Python >=3.10, <3.12
- Package Manager: Poetry (virtualenvs in-project:
.venv/) - Build System: CMake + Ninja (for C/C++ variants)
- Key dependencies: kconfiglib (feature models), hammocking (mock generation), gcovr (coverage), cookiecutter (templates), Sphinx (docs), pypeline-runner (build pipeline)
- Testing: pytest; coverage measured by coverage.py, started by
coverage run - Linting: ruff, mypy (strict), pre-commit hooks, codespell
- CI/CD: GitHub Actions → python-semantic-release → PyPI
- System deps: MinGW with LLVM (via Scoop on Windows)
# Full pipeline (install + tests + docs)
.\build.ps1
# Clean build (removes .venv)
.\build.ps1 -clean
# Install dependencies only
.\build.ps1 -install
# Run tests directly via pytest
poetry run pytest
# Run specific test categories
poetry run pytest -m unit
poetry run pytest -m integration
# Run tests with coverage, the way CI does it
poetry run coverage run -m pytest
poetry run coverage combine
poetry run coverage report --show-missingCoverage is started by coverage run, never by pytest --cov. pytest imports the
pytest11 entry point of spl_core while it starts up, which is before a pytest
plugin can begin measuring; with --cov every import-time line of that plugin and
of src/spl_core/__init__.py reads as uncovered. Codecov enforces 100% coverage of
the lines a pull request changes, so such a gap blocks the merge.
The build pipeline is defined in pypeline.yaml and executed by pypeline-runner:
- Create virtual environment (Python 3.11)
- Install Scoop packages (MinGW toolchain)
- Run pytest
- Generate Sphinx documentation
- Style: ruff with line-length 220, target Python 3.8+ syntax
- Type checking: mypy strict in production code (
disallow_untyped_defs,disallow_any_generics); relaxed intests/(allow_untyped_defs) - Formatting: 4-space indentation, UTF-8, LF line endings (
.editorconfig) - Imports: isort via ruff (first-party:
spl_core,tests) - Docstrings: Not enforced (D100-D107 are ignored), but welcome for complex logic
- Paths: Use
pathlib.Pathin Python code - Naming: PascalCase classes, snake_case functions/methods, UPPER_CASE CMake variables
- Commits: Conventional Commits (
feat:,fix:,docs:,chore:, etc.) — enforced by commitlint and commitizen pre-commit hook. Unlimited line lengths allowed. - Comments: Only add comments for complex logic or non-obvious decisions. Code should be self-explanatory through clear naming.
- Documentation: Written in English. Code comments in English.
- Tests live in
tests/organized by category:tests/unit/— Unit tests (@pytest.mark.unit)tests/integration/— Integration tests (@pytest.mark.integration)tests/cmake/— CMake-specific teststests/steps/— Pipeline step tests
- Test fixtures (sample SPL projects) are in
tests/data/application/ - Test output:
out/test-report.xml(JUnit format) - Use
tests/utils.pyfor shared test base classes (SplProjectIntegrationTestBase,SplKickstartProjectIntegrationTestBase) - Integration tests perform full CMake builds and require the MinGW toolchain
- Follow TDD when implementing features: write failing test → implement → verify green
- Lint (ubuntu-latest) — pre-commit hooks + commitlint
- Test (windows-latest) — full
.\build.ps1pipeline, publishes JUnit results - Release (ubuntu-latest) — python-semantic-release to PyPI + GitHub Releases
(only on
developbranch or manualworkflow_dispatch)
developis the default/production branch (notmain)- All PRs target
develop - Merges to
developtrigger semantic releases - Non-develop branches produce prerelease versions (no-op for release)
- Version tracked in:
pyproject.toml,src/spl_core/__init__.py,docs/conf.py - Changelog excludes
chore*andci*commits
spl-core CI runs only unit tests — it cannot build a real SPL end-to-end
(e.g. no build.sh lives here). Consumer-facing changes (anything touching
SplBuild, gcov_maid, the CMake modules, the kickstart template, or the
build-wrapper contract) are validated against a real SPL (SPLED) using a
release candidate before the official release:
- spl-core feature branch, unit tests green → the branch builds an RC
(e.g.
8.6.0-rc.1). - A SPLED integration branch pins that RC and runs SPLED's full variant self-tests + coverage — this is the real integration gate.
- On green, merge the spl-core PR to
develop(official release). - Repin the SPLED PR to the official version and merge to SPLED
develop, so SPLEDdevelopalways tracks the newest official spl-core.
SPLED never pins an RC on its develop. See
docs/internals/release_integration.md for the full process.
- SPL project sets
VARIANTto select a product variant - KConfig generates
autoconf.hwith feature flags - Components are added via
spl_add_source()/spl_create_component() - Test suites use partial linking + Hammocking for automatic mock generation
- Coverage collected per component via gcovr, merged at variant level
Hammocking is invoked as a CMake custom command during the test build:
COMMAND python -m hammocking
--suffix _${COMPONENT_NAME}
--sources ${PROD_SRC}
--plink ${CMAKE_CURRENT_BINARY_DIR}/${PROD_PARTIAL_LINK}
--outdir ${CMAKE_CURRENT_BINARY_DIR}
<include dirs> <compile defs> <compiler includes> -x cOutput: mockup_<component>.cc and mockup_<component>.h for GoogleTest.
spl-core already uses a pattern for passing config files to external tools (e.g., Sphinx):
- Generate/locate a config file (JSON or INI)
- Pass the file path via CMake variable or environment variable
- Use
${CMAKE_COMMAND} -E env VAR=valueinadd_custom_command()
SplBuild.execute() drives a repo-level wrapper, not CMake directly:
build.bat (-flag style) on Windows, bash ./build.sh (--flag style) on
Linux/macOS. The wrapper is owned by the SPL consumer repo — spl-core does
not ship build.sh (not even in the kickstart template); it only defines the
command contract each SPL must honour. additional_args are passed through
verbatim (raw passthrough to the inner build tool). See
docs/internals/decisions/0002-build-wrapper-lives-in-the-spl.md for the flag
mapping and rationale.
Each variant creates separate build directories per variant/build-kit combination.
Binary naming: variant path / is converted to _
(e.g., Variant/SubVariant → Variant_SubVariant).
- The main branch is
develop(notmain) - Tests run on Windows (windows-latest in CI) — the CMake toolchain targets MinGW/GCC
- The
.venv/directory is created in-project (seepoetry.toml) - Kickstart templates under
src/spl_core/kickstart/templates/are cookiecutter templates — they contain{{ }}Jinja2 syntax that is NOT Python code - The
bootstrap.jsonand.bootstrap/directory handle initial environment setup (downloaded at install time, not checked into git) - Scoop (Windows package manager) is used for system-level dependencies (
scoopfile.json) - Documentation is hosted on ReadTheDocs, built with Sphinx + myst-parser (Markdown support)