Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,42 @@ jobs:
env:
ZU_TEST_ROWS: 300

# The reference, built the same way the release builds it, and graded
# rather than glanced at.
#
# No engine on this one, which is why it is thirty seconds rather than
# ten minutes: the reference is about the header, and the header is
# here whether a library was built or not.
#
# ctest rather than the docs target, because they run the same script
# and this is the spelling that fails the job. Doxygen exits 0 on a
# header it read nothing out of, so a step that only ran Doxygen would
# be a green job over an empty reference, which is exactly what
# happened once. docs/reference.py says what that check is.
docs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5

- uses: lukka/get-cmake@latest

- name: Install doxygen
run: sudo apt-get update && sudo apt-get install -y doxygen

- name: Configure
run: cmake -B build -DZU_CPP_DOCS=ON

- name: Every type the header declares has a page and no member is bare
run: ctest --test-dir build -R reference --output-on-failure

# So that a reviewer can read the page a change to a comment
# produced rather than take the diff's word for it.
- uses: actions/upload-artifact@v4
with:
name: reference
path: build/docs/reference/html
if-no-files-found: error

# The claim the packaging makes is that a project that installed this
# can find it. Nothing in the suite can check that, because the suite
# is inside the build tree and finds the header by being next to it.
Expand Down
61 changes: 61 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
name: Release

on:
push:
tags: ["v*"]
workflow_dispatch:

# The reference goes out with the release rather than beside the source.
#
# A generated reference that lives in the repository is a directory of
# HTML in every diff and a thing somebody eventually regenerates by hand
# from a working copy that was not the tag. Built here, from the tag, it
# is the header that shipped and nothing else, and there is one copy of
# it rather than one per branch.
#
# Nothing else is released from this repository yet. zu.hpp is a single
# header and the way to get it is the tag, so there is no archive to
# build; when the packaging the README promises lands, the vcpkg and
# Conan steps belong in this file beside this job.
jobs:
reference:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v5

- uses: lukka/get-cmake@latest

- name: Install doxygen
run: sudo apt-get update && sudo apt-get install -y doxygen

- name: Configure
run: cmake -B build -DZU_CPP_DOCS=ON

# The same check CI runs, run again here, because this is the copy
# that gets published and a release is the worst place to find out
# that the reference came out empty.
- name: Every type the header declares has a page and no member is bare
run: ctest --test-dir build -R reference --output-on-failure

- name: Pack it
run: |
set -eu
tar -czf "zu-cpp-reference-${GITHUB_REF_NAME}.tar.gz" \
-C build/docs/reference html

- uses: actions/upload-artifact@v4
with:
name: reference
path: zu-cpp-reference-*.tar.gz
if-no-files-found: error

# Only on a tag. A manual run builds the reference and leaves the
# artifact, which is what somebody checking this file wants, and
# does not attach anything to a release that is not there.
- name: Attach it to the release
if: startsWith(github.ref, 'refs/tags/')
run: gh release upload "$GITHUB_REF_NAME" "zu-cpp-reference-${GITHUB_REF_NAME}.tar.gz" --clobber
env:
GH_TOKEN: ${{ github.token }}
18 changes: 17 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ endif()
option(ZU_CPP_TESTS "Build the test suite" ${ZU_CPP_TOP_LEVEL})
option(ZU_CPP_EXAMPLES "Build the examples" ${ZU_CPP_TOP_LEVEL})
option(ZU_CPP_BENCH "Build the benchmarks" ${ZU_CPP_TOP_LEVEL})
# Off even at top level, unlike the three above. Doxygen is not needed
# to build this header or to use it, and a contributor who has a
# compiler and nothing else should be able to configure the repository
# and run the suite. CI turns it on, and so does the release.
option(ZU_CPP_DOCS "Build the API reference, which needs Doxygen" OFF)
option(ZU_CPP_WERROR "Treat warnings as errors in this repository's own code" OFF)
set(ZU_CPP_SANITIZE "" CACHE STRING "Sanitizers to build the suite with, for example address,undefined")

Expand Down Expand Up @@ -122,8 +127,13 @@ endif()

# ---- the suite, the examples, the benchmarks ----

if(TARGET zu::zu AND ZU_CPP_TESTS)
# Here rather than beside add_subdirectory(test), because the reference
# is graded by ctest too and it needs no engine to be graded.
if(ZU_CPP_TESTS)
enable_testing()
endif()

if(TARGET zu::zu AND ZU_CPP_TESTS)
add_subdirectory(test)
# The front page is code too, and it was the only code here that
# nothing compiled. It is read out of README.md at configure time, so
Expand All @@ -139,6 +149,12 @@ if(TARGET zu::zu AND ZU_CPP_BENCH)
add_subdirectory(bench)
endif()

# No engine on this one. The reference is about the header, and the
# header is what this repository has whether an engine was found or not.
if(ZU_CPP_DOCS)
add_subdirectory(docs)
endif()

# ---- installing ----

include(GNUInstallDirs)
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ lynn
- `readme/`, which lifts the two programs above off this page, builds them, runs them and diffs what they print against the blocks under them. The page is the code most people read and the code least often run, and it is the only code here that had nothing compiling it.
- `bench/`, the numbers below, with a timing harness that needs no package manager to run.
- `cmake/`, `find_package(Zu)` to find the engine and `find_package(zu-cpp)` to find this. vcpkg, Conan and pkg-config packaging come with the first release.
- `docs/`, the API reference, generated from `include/zu.hpp` and published with the release rather than checked in beside the source. `docs/reference.py` is the part worth reading: Doxygen exits 0 on a header it extracted nothing from, so the check counts the types the header declares and fails when the reference does not have them, which is what an empty reference looks like from the outside.

Four sanitizer jobs run over the suite, because an ABI nine languages depend on should fail loudly rather than corrupt quietly. The whole tree runs under ASan and UBSan; `test/misuse.c` runs again with leak detection on, which it can and the C++ files cannot, because it is the file that gives every handle back by hand; `test/threads.c` runs under TSan; and both C files run under valgrind, which sees what the sanitizers cannot, since libzu is compiled without instrumentation and memcheck does not need any. `test/tsan.supp` records what TSan is unable to be told about a library that takes no pthread lock, and why the reports from inside the engine are dropped rather than read.

Expand All @@ -121,6 +122,15 @@ ctest --test-dir build --output-on-failure

A checkout with no engine beside it still configures and installs the header, and skips the suite, because a header-only library compiles against a header. `cmake --build build --target bench` builds the benchmarks, which ctest does not run: a timing number produced on a machine that is also running a compile is not a number.

The reference is off by default, because Doxygen is not needed to build this header or to use it.

```
cmake -B build -DZU_CPP_DOCS=ON
cmake --build build --target docs
```

No engine is needed for that one either.

The wrapper is header-only, so a project that would rather not use CMake needs the include path and nothing else.

## Numbers
Expand Down
51 changes: 51 additions & 0 deletions docs/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# The reference, and the test that it is one.
#
# Off by default, because Doxygen is not a dependency of building or
# using this header and a contributor with a compiler and nothing else
# should be able to configure this repository. On in CI, where the
# release builds what it publishes.
#
# cmake -B build -DZU_CPP_DOCS=ON
# cmake --build build --target docs
# ctest --test-dir build -R reference
#
# No engine is needed for any of it. The reference is about the header,
# and the header is what this repository owns. zu.h is generated in the
# engine's tree and is not here, so a reference for the C API belongs
# where the C API is written rather than as a second copy that drifts
# the day the generator runs again.

find_package(Doxygen REQUIRED)
find_package(Python3 REQUIRED COMPONENTS Interpreter)

# Absolute, because Doxygen resolves what it is given against whatever
# directory it was started in and reference.py reads these back out of
# the configured file to find out where the reference went.
set(ZU_CPP_DOCS_INPUT "${CMAKE_SOURCE_DIR}/include/zu.hpp")
set(ZU_CPP_DOCS_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/reference")
# Beside the output and not in it, because the output directory is
# emptied on every run and the log would go with it.
set(ZU_CPP_DOCS_WARNINGS "${CMAKE_CURRENT_BINARY_DIR}/warnings.txt")

set(ZU_CPP_DOXYFILE "${CMAKE_CURRENT_BINARY_DIR}/Doxyfile")
configure_file(Doxyfile.in "${ZU_CPP_DOXYFILE}" @ONLY)

# Through reference.py rather than through Doxygen, everywhere. Doxygen
# exits 0 on a header it read nothing out of, so running it on its own
# is running the half of this that cannot fail.
set(ZU_CPP_REFERENCE
"${Python3_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/reference.py" "${ZU_CPP_DOXYFILE}")

add_custom_target(docs
COMMAND ${ZU_CPP_REFERENCE}
DEPENDS "${ZU_CPP_DOCS_INPUT}" "${ZU_CPP_DOXYFILE}"
WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}"
COMMENT "Generating the reference from include/zu.hpp"
VERBATIM)

# The same command as the target, so that what CI publishes and what
# ctest grades are one run of one script and not two things that agree
# until they do not.
if(ZU_CPP_TESTS)
add_test(NAME reference COMMAND ${ZU_CPP_REFERENCE})
endif()
80 changes: 80 additions & 0 deletions docs/Doxyfile.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# The reference, generated from the header that ships.
#
# Configured by CMake into the build tree, because three of the paths in
# it are the build's rather than the repository's. Nothing here is a
# second description of the API: the input is the one header, and every
# name in the reference came out of it.
#
# Read docs/reference.py beside this. Doxygen exiting 0 does not mean it
# documented anything, and that script is what says it did.

PROJECT_NAME = "zu for C++"
PROJECT_NUMBER = "@PROJECT_VERSION@"
PROJECT_BRIEF = "@PROJECT_DESCRIPTION@"

INPUT = @ZU_CPP_DOCS_INPUT@
OUTPUT_DIRECTORY = @ZU_CPP_DOCS_OUTPUT@
WARN_LOGFILE = @ZU_CPP_DOCS_WARNINGS@

# HTML is what a reader opens and XML is what the gate reads. Neither
# needs LaTeX, and building it costs a minute and a toolchain nobody
# here has.
GENERATE_HTML = YES
GENERATE_XML = YES
GENERATE_LATEX = NO

# The whole point of the item. EXTRACT_ALL would publish a page for
# every name whether anybody wrote a word about it or not, which is a
# reference that is complete and says nothing, and it would turn
# WARN_IF_UNDOCUMENTED off in the process.
EXTRACT_ALL = NO
EXTRACT_STATIC = YES
WARN_IF_UNDOCUMENTED = YES

# The header documents an overload set once rather than once per
# overload, and @param on a call whose parameter is named for what it is
# would be noise in the source and in the output both.
WARN_NO_PARAMDOC = NO

# One comment inside ///@{ ... ///@} covers every member of the group.
# That is how the try_ half of each class is documented: it is the same
# call as the one above it answering a failure rather than throwing, and
# saying so sixty times is how prose comes to disagree with itself.
DISTRIBUTE_GROUP_DOC = YES

JAVADOC_AUTOBRIEF = YES
QUIET = YES

# Graphviz is not a dependency of this repository and the reference does
# not need call graphs. The inheritance of nine exception classes off
# one base reads fine as a list.
HAVE_DOT = NO

# zu::detail is where the machinery lives. It has an underscore's worth
# of a name on it and none of it is a thing to call.
EXCLUDE_SYMBOLS = zu::detail zu::detail::*

ENABLE_PREPROCESSING = YES
MACRO_EXPANSION = YES
EXPAND_ONLY_PREDEF = YES

# Never put ZU_HPP in here.
#
# It is the header's own include guard, and defining it makes #ifndef
# ZU_HPP false, so Doxygen's preprocessor throws the entire file away.
# What comes out is exit status 0, no warnings at all, and a reference
# with nothing in it. This cost a day. WARN_IF_UNDOCUMENTED does not
# fire when there is nothing to be undocumented, which is why the gate
# in reference.py counts what the header declares rather than trusting
# the exit status.
#
# The feature test macros rather than ZU_HAS_EXPECTED and ZU_HAS_FORMAT,
# because the header defines those two itself from these and predefining
# them leaves the #else branch as the one Doxygen reads and documents.
#
# ZU_FORMATTER expands to nothing so that its ten invocations do not
# arrive as ten functions nobody declared. The macro itself is
# documented where it is defined and names what it specializes.
PREDEFINED = __cpp_lib_expected=202202L \
__cpp_lib_format=201907L \
ZU_FORMATTER(TYPE)=
Loading
Loading