Development resources¶
This page covers building libasdf from a git checkout, the conventions the project follows, and how releases are made. If you only want to use the library, the build instructions in the README should be sufficient.
The two build systems¶
libasdf ships two parallel build systems, and both are kept working at all times:
Autotools (configure.ac / Makefile.am) is the primary build system
for development, and the one used to produce release tarballs. make dist
and make distcheck give a canonical, self-contained source archive that
builds without any of the tooling used to generate it–no autotools, no
Sphinx, no git checkout. make distcheck also verifies that the tarball
builds out of tree, that everything needed was actually distributed, and that
it uninstalls cleanly, which is what makes it trustworthy as a release
artifact.
CMake (CMakeLists.txt) exists for consumers, not for producing
releases. A great deal of C and C++ tooling assumes CMake: it drops into
projects that use FetchContent or find_package, into meta-build systems
and package managers, and into IDEs that understand CMake projects natively.
Requiring downstream users to deal with autotools in those settings would be a
real obstacle.
The two are not independent: make distcheck runs the CMake build from the
distribution tarball (the distcheck-cmake target in the top-level
Makefile.am), so a change that breaks CMake, or that forgets to distribute
a file CMake needs, fails the autotools release check. Both are also built and
tested in CI, by the Build and CMake Build workflows respectively.
The practical consequence: when you add a source file, add it to both build systems.
CMake is generally more permissive and picks up most new source files automatically, whereas automake tends to require anything you want built to be listed explicitly.
Building with autotools¶
A git checkout has no configure script; generate it first with
autogen.sh (a one-line wrapper around autoreconf --install). This is
normally only needed once, or again after editing configure.ac or any
Makefile.am, though the generated makefiles normally re-run the necessary
steps by themselves:
$ git clone --recurse-submodules https://github.com/asdf-format/libasdf.git
$ cd libasdf
$ ./autogen.sh
$ ./configure
$ make
$ make check
The asdf-standard submodule provides reference files used by the test
suite; if you cloned without --recurse-submodules, run git submodule
update --init before building (this is also run automatically by the
build system, but sometimes it can be flaky).
Useful configure options:
--enable-debugDebug build:
CFLAGS=-g -O0, andDEBUGis defined.--with-asanBuild with AddressSanitizer.
--with-ubsanBuild with UndefinedBehaviorSanitizer.
--disable-toolSkip building the
asdfcommand-line tool. Useful ifargpis unavailable.--enable-docsBuild the Sphinx documentation. The default is
auto: docs are built if Sphinx and its extensions are found, and silently skipped otherwise.--enable-valgrindEnable the
check-valgrindtargets.--enable-logging,--enable-log-color,--with-log-default=LEVEL,--with-log-min=LEVELCompile-time logging configuration; see Logging.
Autotools fully supports out-of-tree builds, and keeping several configurations side by side is the recommended way to work, since a change should pass under more than one of them:
$ mkdir build-asan && cd build-asan
$ ../configure --with-asan
$ make && make check
Anything that touches library code should pass make check in both an
AddressSanitizer build and a debug build before being committed.
Building with CMake¶
The CMake build is documented in the README and is reproduced here for convenience:
$ mkdir build && cd build
$ cmake .. \
-D CMAKE_BUILD_TYPE=RelWithDebInfo \
-D ENABLE_TESTING=YES \
-D ENABLE_ASAN=[YES/NO] \
-D ENABLE_TOOL=[YES/NO]
$ make
$ ctest --output-on-failure
Other options of note:
-D ENABLE_TESTING_ALL=YESEnable every test target, including the shell-based integration tests. This is what CI uses.
-D ENABLE_DOCS=YESBuild the Sphinx documentation (
make docs).-D FYAML_NO_PKGCONFIG=YES,-D ARGP_NO_PKGCONFIG=YESLocate libfyaml or argp by explicit path rather than
pkg-config; pair with-D FYAML_LIBDIR=/-D FYAML_INCLUDEDIR=and theARGP_equivalents.
make package_source and make package produce CPack archives. Note that
these are not what is published for a release–the release tarball comes
from make dist under autotools.
Note
On systems with a libasdf already installed under, say, ~/.local/lib,
be aware that CMake links test binaries with DT_RUNPATH rather than
DT_RPATH, and the dynamic loader searches LD_LIBRARY_PATH before
DT_RUNPATH. An installed copy can therefore shadow the freshly built
one and cause confusing test failures. Unset LD_LIBRARY_PATH before
running ctest if you hit this.
Running the tests¶
The test suite uses the µnit framework,
with some custom wrappers around it (helper macros) defined in
tests/munit.h.
Test binaries are named test-<name>.unit and live in the tests/
directory of the build tree. Some of them compile a subset of the sources
directly, rather than linking against the library, so that internal components
can be tested in isolation.
From a build directory:
$ make check # everything
$ tests/test-block.unit # one binary directly
$ tests/test-ndarray.unit --help # munit options
munit accepts a test path to run a single case, and other useful flags:
$ tests/test-ndarray.unit /test_ndarray/test_read_all
$ tests/test-ndarray.unit --seed 0x1234 # reproduce a specific ordering
$ tests/test-ndarray.unit --no-fork # keep the debugger attached
In particular, the --no-fork option is always enabled by default for debug
builds, as it’s extremely helpful if you want to run the tests under gdb (or
your debugger of choice).
Shell-based integration tests (tests/test-events.sh, tests/test-info.sh
and friends) exercise the asdf command-line tool against golden output
files under tests/fixtures/. When a deliberate change alters that output,
regenerate the golden files with make update-fixtures from the build
directory, and check the diff carefully before committing it.
Some test data comes from the asdf-standard submodule’s
reference_files/ directory.
Two more targets, both requiring the corresponding configure option:
make check-valgrind(--enable-valgrind)make check-code-coverage(--enable-code-coverage)
Code style¶
Formatting is enforced by clang-format; the rules live in
.clang-format. Run:
$ make format
from a build directory before committing. It rewrites all library sources and headers in place.
There is also a make tidy target that runs clang-tidy, though its
output is not currently clean and it is not enforced.
To have formatting applied automatically, it is recommended to install the
pre-commit hook shipped in
.pre-commit-config.yaml:
$ pip install pre-commit # or: pipx install pre-commit
$ pre-commit install
That registers a git hook which runs clang-format -i over any staged files
under src/ or include/.
Note
The hook uses language: system, so it runs your clang-format;
a different major version may format differently from the rest of the tree.
To check the whole tree at any time:
pre-commit run --all-files
Documentation¶
The documentation is built with Sphinx. API reference pages are generated from
the doc comments in the public headers under include/asdf/ using Hawkmoth, which extracts /** ... */ comments
and feeds them to Sphinx as reStructuredText. Because of that, doc comments in
public headers are written in reST, using field lists (:param foo:,
:return:) rather than a Doxygen-style syntax.
Build them with:
$ ./configure --enable-docs
$ make docs
The rendered output lands in docs/_build/html under the build directory.
CI builds the docs with -W, so warnings are errors; if you add a page,
make sure it is referenced from a toctree and that every cross-reference
resolves.
The asdf(1) man page is generated from docs/usage/cli.rst but is
committed to the repository (as docs/man/asdf.1), so that building or
installing from a release tarball does not require Sphinx. After changing
usage/cli.rst, regenerate and commit it:
$ make man-page
Adding a new documentation page also means adding it to EXTRA_DIST in
docs/Makefile.am, or it will be missing from the release tarball and the
documentation build inside make distcheck will fail.
Changelog entries¶
The changelog is assembled by towncrier from individual news fragments in the
changes/ directory. Each fragment is a small reStructuredText file named
for the issue or pull request it relates to, with the category as its
extension:
changes/123.feature
changes/456.bugfix
The available categories are general, feature, bugfix, doc,
removal and misc. For a change with no associated issue number, use a
descriptive name prefixed with +, for example
changes/+cmake-build.misc.
Write the entry for the reader of the release notes, not for the reviewer of the diff.
Making a release¶
Despite not being a Python package, version numbers follow PEP 440
(because the author likes it) and are managed by
bumpver, configured in
.bumpver.toml.
A single bumpver invocation rewrites the version everywhere it appears
(configure.ac, CMakeLists.txt, docs/conf.py, towncrier.toml,
and .bumpver.toml itself), builds the changelog, commits, tags and pushes.
Tags are the bare version string, with no v prefix–0.1.0a2, not
v0.1.0a2.
Note
CMake does not support PEP 440-style version tags (“a1”, “rc0”, etc.),
so the full version is written in CMakeLists.txt as a
PACKAGE_VERSION variable (mirroring autoconf); the CMake standard
variable PROJECT_VERSION is also managed by bumpver, but only contains
the MAJOR.MINOR.PATCH portion of the version.
Signing the tag¶
Release tags should be signed with your GPG key (bumpver does this via
git tag --annotate). That command does honour git’s tag.gpgSign
setting, so configure it once and release tags are signed automatically:
$ git config --local tag.gpgSign true
This should be configured before making a release; you can find more information about generating a GPG key and registering it with GitHub at Telling Git about your signing key.
Cutting the release¶
Make sure
mainis up to date, the working tree is clean, and CI is passing.Check that every merged change has a news fragment in
changes/, and preview the assembled changelog:$ towncrier build --draft --version <new version>
Run bumpver with the appropriate increment. Use
--dryfirst to see exactly what will be rewritten:$ bumpver update --tag-num --dry # 0.1.0a2 -> 0.1.0a3 $ bumpver update --tag-num
Other useful increments are
--patch,--minor,--major, and--tag beta/--tag finalto move along the pre-release sequence.This runs
scripts/changelog.shas a pre-commit hook, which consumes every file inchanges/and rewritesCHANGES.rst, then commits, tags and pushes.The rest is automated. Pushing the tag triggers the
Buildworkflow; once it succeeds on that tag theReleaseworkflow creates a draft GitHub release, with the release notes converted from the newCHANGES.rstsection and themake disttarball attached.Review the draft release on GitHub and publish it.
The release workflow refuses to run unless the tag matches the version recorded
in .bumpver.toml and configure.ac, and unless the top section of
CHANGES.rst is the one for that version, so a mistagged or half-finished
release fails loudly rather than shipping.
If the Release workflow needs to be re-run against a tag that has already
built successfully, for instance after fixing something in the workflow
itself, it can be triggered by hand, e.g. with the GitHub CLI:
$ gh workflow run release.yml -f tag=<version>
Re-running updates the existing draft rather than failing.