Skip to content

Write the version number in one place and print the commit with --version - #122

Open
aoymt wants to merge 3 commits into
developfrom
version-commit-hash
Open

aoymt wants to merge 3 commits into
developfrom
version-commit-hash

Conversation

@aoymt

@aoymt aoymt commented Oct 3, 2026

Copy link
Copy Markdown
Contributor

Summary

The version number was written by hand in six files, and tenes --version printed the same 2.2-dev for every build of develop and of any feature branch.

This PR makes set(TENES_VERSION ...) in the top-level CMakeLists.txt the only place where the number is written, and makes --version print the commit the program was built from.

$ tenes --version
TeNeS v2.2-dev (3386c1cd)
$ tenes_std --version
2.2-dev (3386c1cd)
$ tenes_simple --version
2.2-dev (3386c1cd)

Changes

  • --version of tenes, tenes_simple and tenes_std prints the version number followed by the first 8 digits of the commit hash, and -dirty when the source tree had uncommitted changes to tracked files.
  • output/timers.json records the commit in meta as git_commit (the full hash) and git_dirty. Both are null when the commit is not known.
  • The manual shows the version number alone. conf.py reads it from CMakeLists.txt.
  • One place for the number. src/version.hpp, the two Python tools and the two conf.py no longer carry a copy.

Where the commit comes from

Source tree Commit
git checkout asked from git at every build
tarball made by git archive (the source archives of GitHub) read from config/git_archive.txt, filled in through export-subst
any other copy of the sources not known; the version number is printed alone

Notes on the choices:

  • The commit is looked up at build time, not at configuration, because a new commit does not trigger a new configuration. version.cpp and the built tools are rewritten only when their content changes, so a build with nothing new compiles and links nothing.
  • The embedded value is %H, not %(describe). The tags of this repository are lightweight, and the hash does not depend on the version of git.
  • When git cannot read the checkout (for example under sudo make install, where git refuses the repository of another user), the values of the last build are kept.
  • A Python tool run from the source tree, without CMake, looks up the number and the commit when --version is asked for. A tool copied out of the tree prints unknown rather than the commit of whatever repository it landed in.

Compatibility

  • TENES_VERSION in version.hpp is still a macro, but it now expands to a function call, tenes::version(), and is no longer a string literal. Code that concatenates it as a literal ("v" TENES_VERSION) has to be changed. version.hpp is an installed header.
  • timers_to_json() takes two more arguments (git_commit, git_dirty).
  • The values live in a generated version.cpp; version.hpp only declares them. A generated header would lose against a stale src/version.hpp in #include "version.hpp", and would drop out of the installed headers.

Also fixed

The shebang of the built tools is now written by the same CMake script. The default interpreter, /usr/bin/env python3, is a list of two elements for CMake and is joined before it is passed on.

Release procedure

Tags are created at release time and are kept equal to TENES_VERSION by hand. No check in CI is added.

Tests

  • ctest: 77/77 (Apple clang, no MPI, Release).
  • New: test/python/test_version.py (18 cases), and two cases in test/timer_registry.cpp.
  • tenes, tenes_std and tenes_simple print the same commit when built from a git checkout and from a git archive tarball. This was checked before the rebase onto the current develop; the build from a checkout was checked again after it.
  • The English manual builds with Sphinx and shows TeNeS 2.2-dev documentation (checked before the rebase).

Not tested:

  • an MPI build,
  • the Ninja generator,
  • a source archive downloaded from GitHub itself (checked with a local git archive).

Notes for reviewers

  • The display is fixed at 8 digits, which is what git rev-parse --short prints in this repository.
  • The source archives of GitHub do not contain the submodules (mptensor, toml11), so a tarball alone still cannot be built. This PR does not change that.

🤖 Generated with Claude Code

aoymt and others added 2 commits October 3, 2026 12:57
The version number was written by hand in six files: CMakeLists.txt,
src/version.hpp, the two Python tools and the two conf.py of the manual.
tenes --version printed the one of version.hpp and did not look at
CMakeLists.txt, and every build of develop and of a feature branch printed
the same "2.2-dev", whatever it was built from.

set(TENES_VERSION ...) in the top-level CMakeLists.txt is now the only
place. tenes, tenes_simple and tenes_std print it followed by the commit,
"TeNeS v2.2-dev (06780ea)": the first 8 digits of the hash, which is what
git rev-parse --short prints in this repository, and "-dirty" for a tree
with uncommitted changes to tracked files. The manual shows the number
alone; conf.py reads it from CMakeLists.txt.

Where the commit comes from:

- a git checkout asks git, at every build and not at configuration, since
  a commit changes the hash without a new configuration. version.cpp and
  the tools are rewritten only when their content changes, so a build with
  nothing new compiles and links nothing;
- a tarball of git archive, which is what GitHub serves as the source
  archive, carries it in config/git_archive.txt (export-subst). It is %H
  and not %(describe): the tags of this repository are lightweight, and
  the hash does not depend on the version of git;
- any other copy of the sources prints the number alone.

When git cannot read the checkout, as under "sudo make install" where it
refuses the repository of another user, the values of the last build are
kept. A Python tool run from the source tree, without CMake, looks the
number and the commit up when --version is asked for; one copied out of
the tree prints "unknown" rather than the commit of whatever repository
it landed in.

The values live in a generated version.cpp and version.hpp only declares
them: a generated header would lose against the stale src/version.hpp of
an earlier checkout in #include "version.hpp", and would drop out of the
installed headers. TENES_VERSION stays as a macro but is a function call
now, not a string literal.

output/timers.json records the commit as git_commit (the full hash) and
git_dirty, null when the commit is not known.

The shebang of the built tools is written by the same CMake script; the
default interpreter "/usr/bin/env python3" is a list of two elements for
CMake and is joined before it is passed on.

ctest 47/47 (Apple clang, no MPI, Release) with a display of 7 digits;
after the change to 8 the tests that see it were run again
(test_timer_registry, python_unittest and two integration tests). tenes,
tenes_std and tenes_simple print the same commit when built from a
checkout and from a git archive tarball. Not tried: an MPI build, Ninja,
and an archive downloaded from GitHub itself.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
With CMake 3 the build stopped at version.cpp:

    bool git_dirty() { return @TENES_GIT_DIRTY@; }

config/write_version.cmake is run with cmake -P, and a script starts with
no policy set. CMake 3 then keeps the old behaviour of CMP0053, which
expands @var@ inside a quoted argument. The pattern "@TENES_GIT_DIRTY@" of
string(REPLACE) became the value of the variable, "false", and nothing was
replaced; the same for the version number and the commit, which sit in
string literals and compiled, so tenes would have printed "@TENES_VERSION@"
had the third not been a bool. CMake 4 has no old behaviour left, which is
why it did not show where this was written.

The script now sets the policies, and the patterns are bracket arguments,
which no version of CMake evaluates. It also stops when a placeholder
@TENES_...@ is left in what it writes.

A placeholder left in a Python tool does not stop the build: the tool
falls back to looking the version up at run time and, installed, prints
"unknown". version_tenes, version_tenes_simple and version_tenes_std run
the built programs with --version and compare with the version number of
CMakeLists.txt.

Checked with CMake 4.4.1 only; CMake 3 is what the CI has.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

@yomichi yomichi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. I checked that the hash comes out right from a checkout, a worktree and a git archive tarball, that -dirty follows tracked files only, and that a second build compiles and links nothing (Makefiles and Ninja). A few points:

1. No test fails when the commit is no longer embedded.
In a git checkout, the hash is optional everywhere it is checked:

  • test/CMakeLists.txt: ( \([0-9a-f]+(-dirty)?\))? for version_*,
  • test/timer_registry.cpp: the hash.empty() branch of "version_string is the version number followed by the commit",
  • test/python/test_version.py: test_format and test_version_option.

With config/git_hash.cmake changed to always return an empty hash, tenes --version prints TeNeS v2.2-dev, and version_tenes, version_tenes_simple, version_tenes_std and test_timer_registry still pass. With the runtime lookup of the Python tools changed to return "", all 18 cases of test_version.py still pass. Could the tests require the hash when the source tree is a git checkout (for example, by choosing the regex at configure time from whether ${TeNeS_SOURCE_DIR}/.git exists)?

2. (Minor) When git cannot read the checkout, the whole output is kept.
config/write_version.cmake returns before it reads INPUT when TENES_GIT_FAILED is set and OUTPUT exists (lines 15-18). Since this script is also the only path from tool/tenes_std.py to build/tool/tenes_std, an edit to a tool or a new TENES_VERSION then silently does not reach the build. This needs a checkout that git cannot read at build time (e.g. built in a container as another user), so it is rare, but keeping only the hash and the dirty flag of the last build, and always filling in INPUT, would avoid it.

3. (Minor) sudo make install and git status.

  • The comment in write_version.cmake and the description say that git refuses the repository under sudo make install. Current git allows it: under sudo, safe.directory accepts a repository owned by SUDO_UID (see git help config, written for "make && sudo make install"). So that case does not take the TENES_GIT_FAILED branch, and the comment could name another case.
  • git status refreshes .git/index when the stat data is stale, so a build can take index.lock while the user runs git. git --no-optional-locks status (in config/git_hash.cmake and in the two tools) avoids this, and is what git recommends for tools that run in the background.

Not tested: an MPI build, CMake 3.8 itself.

Review of #122.

1. No test failed when the commit was no longer embedded. The commit is
   legitimately absent in a copy of the sources that is neither a checkout
   nor a tarball of git archive, so every check made it optional: the
   regex of version_*, the hash.empty() branch in timer_registry.cpp,
   test_format and test_version_option. With config/git_hash.cmake
   returning no commit, or with the lookup of the tools doing so, all of
   them passed.

   What to expect is now decided apart from the code under test, by asking
   git: test/CMakeLists.txt runs git rev-parse at configuration (or reads
   config/git_archive.txt in a tarball) and requires the 8 digits, or
   their absence; it passes the same to test_timer_registry. The Python
   tests ask git when they run and compare the digits themselves. Asking
   config/git_hash.cmake would not do: a script that finds no commit
   would be expected to find none.

2. When git could not read the checkout, config/write_version.cmake
   returned before reading its input and kept the whole output of the
   last build. It is also the only way from tool/tenes_std.py to the
   built tool, so an edit to a tool, or a new version number, did not
   reach the build. It now keeps the commit only: the last commit git gave
   is written beside the output and filled in when git fails; the input
   and the version number are always those of the build.

3. "sudo make install" was given as the case in which git refuses the
   checkout. It is not one: under sudo git accepts the repository of the
   user who ran it (safe.directory in git-config). The comment names a
   checkout owned by another user, as in a container.

   git status may refresh the index and take its lock, in the way of a git
   command the user runs during a build. It is run with
   --no-optional-locks, by config/git_hash.cmake and by the two tools.

Also, a tool run from the source tree dropped the commit when git status
failed after git rev-parse had succeeded; config/git_hash.cmake keeps it
and reports the tree as clean, and the tools do the same now.

Both mutations of the review turn the tests red: no commit from
config/git_hash.cmake fails version_tenes, version_tenes_simple,
version_tenes_std, test_timer_registry and python_unittest; no commit from
the lookup of the tools fails eight cases of test_version.py.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants