Skip to content

Make header generation independent of filesystem case and git history - #11

Merged
CraigHutchinson merged 3 commits into
masterfrom
fix/template-selection-and-ci
Sep 21, 2026
Merged

CraigHutchinson merged 3 commits into
masterfrom
fix/template-selection-and-ci

Conversation

@CraigHutchinson

Copy link
Copy Markdown
Contributor

The same project produced different headers on Windows and on Linux, and nothing in either log said so.

Template lookup was a bare EXISTS on the requested filename. That check is case-insensitive on Windows and macOS and case-sensitive on Linux. The shipped template is Version.hpp.in, while the README, the template's own header comment and the test suite all asked for version.hpp -- so a consumer asking for the C++ constexpr output got it on a developer box and silently fell through to the C-macro template on a Linux build machine. Resolution now matches against the package directory listing, reports the spelling actually on disk, and accepts either casing everywhere. Two templates differing only in case stop the configure rather than one being picked at random.

VERSION_H_TEMPLATE was documented as the way to supply your own template, but the module overwrote it before use. The only route left was writing into the package's own directory, which an installed or CPM-fetched copy is not the consumer's to write to -- the test suite did exactly that, and the artifact it wrote had been committed to this repository. It is honoured as given now:

set(VERSION_H_FILENAME "MyVersion.h")
set(VERSION_H_TEMPLATE "${CMAKE_CURRENT_SOURCE_DIR}/cmake/MyVersion.h.in")
CPMAddPackage("gh:BareCpper/Version.cmake@0.4")

A path that does not exist stops the configure instead of falling through to a different template.

A tree with no git history configured cleanly and then could not be built at all: the generation target named .git/index and .git/HEAD as inputs unconditionally, and Ninja and Make refuse to build a target whose input no rule can produce. Only the bookkeeping files that exist are declared now, which also covers a repository whose first commit is not yet made.

Diagnostics on that same path said the opposite of what happened -- an untagged build, and a build outside a checkout, each printed a failure line and then generated a complete version anyway. Both are ordinary states the maintainer cannot act on, so they close as passes naming the fallback. CHECK_FAIL is kept for the tag that matches VERSION_TAG_PATTERN but does not parse, which is a real misconfiguration. Shipping Version.h.in as a file rather than writing it at configure time removes the most visible of those false failure lines: Find 'Version.h.in' - Not Found printed on every build of every project that took the defaults.

Functional testing: ctest suite grown from 8 cases to 11, 33/33 pass. Verified under both Ninja and Visual Studio on Windows. New cases pin the differently-cased request, a template supplied from outside the package, an archive with no .git, and a real executable that compiles and links against the generated header -- every prior case read the header as text, which cannot see a template emitting something the compiler rejects.

CI: none existed, which is why a case-sensitivity defect survived simultaneously in the module, the README and the suite's own fixtures. ubuntu-latest is the job that catches it; macos-latest is there because a consumer's build machine is as likely to be one.

Reviewer notes:

  • version_resolveTemplate in CMake/Version.cmake is the substance; the resolution order is documented above it.
  • _VERSION_PACKAGE_DIR is captured at file scope because CMAKE_CURRENT_LIST_DIR read inside a macro resolves against the list file that called it, not the one that defined it.
  • tests/test_no_git_repo sets GIT_CEILING_DIRECTORIES; without it git ascends out of the build tree and describes this checkout.

Risk: Low. Behaviour changes only where the previous behaviour was platform-dependent or produced an unbuildable target; the default path generates a byte-identical header.

🤖 Generated with Claude Code

CraigHutchinson and others added 3 commits September 21, 2026 16:59
Two consumer-visible defects, both invisible on a Windows or macOS
developer box.

Template lookup was a bare EXISTS on the requested filename. That check
is case-insensitive on Windows and macOS and case-sensitive on Linux,
and the shipped template is Version.hpp.in while the README, the
template's own header comment and the test suite all asked for
version.hpp. The same project therefore produced constexpr constants on
one platform and silently fell through to the C-macro template on
another. Resolution now matches against the package directory listing,
so it reports the spelling actually on disk and accepts either casing on
every filesystem; two templates differing only in case stop the
configure rather than picking one at random.

VERSION_H_TEMPLATE was documented as the way to supply a template but
the module overwrote it before use, so the only route left was writing
into the package's own directory. The test suite did exactly that, and
the artifact it wrote had been committed to the repository. It is now
honoured as given, and a path that does not exist stops the configure
instead of falling through to a different template.

The no-history path configured cleanly and then could not be built: the
generation target named .git/index and .git/HEAD as inputs
unconditionally, and Ninja and Make refuse to build a target whose input
no rule can produce. Only the bookkeeping files that exist are declared
now. This also covers a repository whose first commit is not yet made,
which has HEAD but no index.

Diagnostics for the same path said the opposite of what happened. An
untagged build, and a build outside a checkout, both printed a failure
line and then generated a complete version anyway. Those two states are
ordinary and the maintainer cannot act on either, so they close as
passes that name the fallback; CHECK_FAIL is kept for the tag that
matches VERSION_TAG_PATTERN but does not parse, which is a real
misconfiguration.

Shipping Version.h.in as a file rather than writing it at configure time
removes the last caller of the "not found, generating" path for the
default filename, which was the most visible of the false failure lines:
it printed on every build of every project that took the defaults.

Version.hpp.in gains VERSION_SEMANTIC alongside VERSION_STRING so the
two templates agree on the macro name; VERSION_STRING stays for existing
call sites.

Tests: test_template_case pins the differently-cased request,
test_custom_template now supplies a template from outside the package,
test_no_git_repo pins the archive-with-no-.git build, and
test_generated_compiles compiles and links a real target against the
generated header. The suite passes the generator down to its
sub-projects, which both halves its runtime and stops it silently
testing a generator nobody uses.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The suite had no CI at all, so every run happened on a case-insensitive
filesystem. A template referenced by a spelling that differs from the
name on disk resolves silently there, which is why that defect survived
in the module, in the README and in the suite's own fixtures at the same
time. ubuntu-latest is the job that catches it; macos-latest is there
because a consumer's build machine is as likely to be one.

CMakePresets.json makes the "cmake --preset default && ctest --preset
default" line the root CMakeLists already printed actually work.

.gitattributes pins line endings in the repository so a contributor
whose core.autocrlf is off cannot commit a whole-file ending change on
top of a one-line edit.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
add_dependencies takes no keyword arguments, so "INTERFACE" was parsed
as a dependency name of its own. No target has that name; CMake
tolerated it silently rather than reporting the unknown dependency.

The ordering this line exists for is unaffected. Checked against the
generated Ninja graph before and after: the consumer's object still
carries "cmake_object_order_depends_target_consumer: phony ||
genCmakeVersion", so a target linking version::version still compiles
after the header is generated.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.

1 participant