Skip to content

Add CMake 4 support and a latest-CMake-4 CI job - #59

Open
Orphis wants to merge 6 commits into
mainfrom
feature/cmake4
Open

Add CMake 4 support and a latest-CMake-4 CI job#59
Orphis wants to merge 6 commits into
mainfrom
feature/cmake4

Conversation

@Orphis

@Orphis Orphis commented Jul 14, 2026

Copy link
Copy Markdown
Contributor
  • Pin CMP0168 to OLD around every FetchContent_Populate call via the new hfc_fetchcontent_populate() wrapper (hfc_policy_helpers.cmake). Direct population (CMP0168=NEW, the default for consumers with policy version

    = 3.30, typical on CMake 4) ignores SUBBUILD_DIR, keeps its stamps in
    the consumer's build dir and rm -rf's + re-clones the shared source cache whenever those stamps are missing (fresh build dir, cmake --fresh), defeating the central cache and HFC's invalidation logic.

  • Add HERMETIC_POLICY_VERSION_MINIMUM (per-content, on FetchContent_MakeHermetic) and HERMETIC_FETCHCONTENT_POLICY_VERSION_MINIMUM (global) to set CMAKE_POLICY_VERSION_MINIMUM for legacy dependencies that declare cmake_minimum_required < 3.5, which CMake 4 rejects. Injected as -D on the isolated configure command (the toolchain is read too late for the dependency's top-level cmake_minimum_required), as a set() in the proxy toolchain (covers nested code and keys the cmake-re ABI-hash cache), in the no-target add_subdirectory wrapper, and around the interlocked provider's consumer-scope add_subdirectory.

  • Replace deprecated make_directory() with file(MAKE_DIRECTORY).

  • CI: new build-cmake4 job runs the entire suite against the latest stable CMake 4.x, provisioned through the tipi distro system: TIPI_DISTRO_JSON points at a manifest generated by test/generate_cmake4_distro.sh (re-zips the Kitware release; the distro ship step only extracts zip archives). To be simplified once CMake 4 zips are hosted on the distro CDN.

Change-Id: Ib81b43519abe605e03f9267648dd359a858b22ca

Orphis and others added 5 commits August 11, 2026 11:29
- Pin CMP0168 to OLD around every FetchContent_Populate call via the new
  hfc_fetchcontent_populate() wrapper (hfc_policy_helpers.cmake). Direct
  population (CMP0168=NEW, the default for consumers with policy version
  >= 3.30, typical on CMake 4) ignores SUBBUILD_DIR, keeps its stamps in
  the consumer's build dir and rm -rf's + re-clones the shared source
  cache whenever those stamps are missing (fresh build dir, cmake --fresh),
  defeating the central cache and HFC's invalidation logic.

- Add HERMETIC_POLICY_VERSION_MINIMUM (per-content, on
  FetchContent_MakeHermetic) and HERMETIC_FETCHCONTENT_POLICY_VERSION_MINIMUM
  (global) to set CMAKE_POLICY_VERSION_MINIMUM for legacy dependencies that
  declare cmake_minimum_required < 3.5, which CMake 4 rejects. Injected as
  -D on the isolated configure command (the toolchain is read too late for
  the dependency's top-level cmake_minimum_required), as a set() in the
  proxy toolchain (covers nested code and keys the cmake-re ABI-hash cache),
  in the no-target add_subdirectory wrapper, and around the interlocked
  provider's consumer-scope add_subdirectory.

- Replace deprecated make_directory() with file(MAKE_DIRECTORY).

- CI: new build-cmake4 job runs the entire suite against the latest stable
  CMake 4.x, provisioned through the tipi distro system: TIPI_DISTRO_JSON
  points at a manifest generated by test/generate_cmake4_distro.sh (re-zips
  the Kitware release; the distro ship step only extracts zip archives).
  To be simplified once CMake 4 zips are hosted on the distro CDN.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Change-Id: Ib81b43519abe605e03f9267648dd359a858b22ca
check_source_cache_reuse locks the property that a dependency is downloaded
exactly once into the central source cache and that new build trees reuse it:
after the first build tree populates the cache, the dependency origin is
renamed away, so any re-download attempt fails loudly. The consumer opts into
cmake_policy(SET CMP0168 NEW) (when available) to model the modern
cmake_minimum_required(VERSION 3.30+) consumers that broke before the
CMP0168 pin in hfc_policy_helpers.cmake.

Scenarios (x native cmake / cmake-re):
- warm clean git clone: a fresh build tree configures and builds without the
  origin, the cached clone's HEAD is untouched, and no clone runs
- URL/archive content: same, resting purely on the populate stamps next to
  the cache (there is no HFC-side git check on this path)
- local uncommitted edits in the cached clone survive a new build tree and
  are what gets compiled (the debugging workflow of a56de21; native cmake
  only, as cmake-re's install cache is keyed on ORIGIN+REVISION)

Validated green with CMake 4.4.0 and 3.31.9 (6 cases each, tipi distro) and
bare CMake 3.28.3 (3 native cases) - and red against the pre-pin tree under
CMake 4.4, failing in exactly the modes reported in the issue: the URL case
re-downloads and the local-modifications case is destroyed by the
direct-population re-clone. The clean-git case passes even pre-pin because
HFC's git-warm early-return short-circuits before FetchContent_Populate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Change-Id: Ica3a245361c7f808a11ccc2dad5fc42d1336b09f
Run natively on CMP0168 NEW (CMake >= 3.30): no populate sub-build, one
nested configure less per dependency. The invariant that makes direct
population safe for the shared source cache is that FetchContent_Populate()
now only ever runs against a COLD (missing/empty) source dir - warmness is
decided by HFC from the cache contents themselves, in the new
hfc_populate_cache_state module:

- git content: a valid clone with HEAD at the declared GIT_TAG (or at a
  local git tag of that name) is WARM; local uncommitted modifications are
  allowed and reused (the a56de21 debugging workflow). A valid clone at
  another revision is UPDATEd in place (best-effort fetch + force-clean
  checkout, preferring origin/<tag> so branch GIT_TAGs follow the branch) -
  never a re-clone. Offline reconfigures keep working from the local clone.
- URL content: a completed populate is recorded by a marker file next to
  the source dir (the legacy sub-build populate-complete stamp is also
  honored so pre-existing caches stay warm).
- anything non-empty that is neither -> invalidated (marker, source dir,
  and the populate state of BOTH implementations) and re-populated.

Applied at all three populate sites: the populate closure, the in-source
secondary populate (URL trees always re-extract from the locally cached
archive: in cmake-re mode the mirror machinery rearranges the tree behind
HFC's back, so no marker can be trusted there; git trees are
self-validating), and the interlocked provider (which had no warm check).
A COLD populate also clears direct population's per-build-tree step stamps
first, so a stale download.stamp cannot skip the download after
HFC_V1_REMOVE_SOURCE_DIR_AFTER_INSTALL deleted the cache sources.

HERMETIC_FETCHCONTENT_FORCE_SUBBUILD=ON restores the previous CMP0168 OLD
sub-build behavior as an escape hatch; on CMake < 3.30 the sub-build
remains the only implementation.

check_source_cache_reuse gains a branch-tag scenario: a moved branch is
followed via in-place fetch+checkout (never a re-clone) and an unreachable
origin falls back to the local clone state.

Validated on CMake 4.4.0: full suite 31/33 (the two failures are the
pre-existing configure_stability autotools cmake-re cases, unchanged from
the pristine-main baseline), including the source-migration matrix and all
cmake-re variants; check_source_cache_reuse also green on CMake 3.31.9
(8/8) and 3.28.3 (4/4 native).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Change-Id: Id27ae9de9d5a08b6968abc78b72f1ee4e882e434
…dDetails

HFC no longer reads FetchContent's private saved-details storage. A guarded
FetchContent_Declare() override (installed by HermeticFetchContent, forwarding
to the original implementation) records the raw declaration arguments into
HFC's own _HermeticFetchContent_<name>_declaredDetails global property,
mirroring the documented FETCHCONTENT_BASE_DIR/<name>-(src|build) SOURCE_DIR
and BINARY_DIR defaults. FetchContent_MakeHermetic(), the interlocked
MakeAvailable path and hfc_determine_cache_id() (which also loses its reliance
on a leaked caller variable) now read that copy.

__FetchContent_getSavedDetails() is only used as a fallback for contents
declared before including HermeticFetchContent, with a clear error when even
that is unavailable.

Behavioral notes:
- relative GIT_REPOSITORY URLs (CMP0150 resolution happens inside
  FetchContent_Declare) are only supported through the fallback path;
  hermetic declarations should use absolute origins
- the recorded details no longer contain FetchContent's internal tokens
  (argument separator, injected DOWNLOAD_EXTRACT_TIMESTAMP), so
  <name>_DETAILS_HASH changes once: existing setups reconfigure their
  dependencies one time

Validated on CMake 4.4.0: full suite 32/33 (only the pre-existing
configure_stability autotools cmake-re cases fail, unchanged), migration
matrix included; the fallback path was never taken in the validation runs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Change-Id: I9ade04f897eebf1a7b992552daa567d09d971487
The tipi-build/distro branch feature/cmake-4.4.0 now serves distro-ready
CMake 4.4.0 zips from the distro CDN, so the build-cmake4 job no longer
needs to download the Kitware tarball, re-zip it and synthesize a manifest:
it fetches the branch's distro.json (pinned to a commit for immutability),
stages the linux CMake zip into the workspace and rewrites that one URL to
file:// so the network-isolated offline_source_override_test container -
which only sees the workspace mount - provisions the same CMake without
network access. test/generate_cmake4_distro.sh is retired.

Testing CMake 4 locally is now just:
  export TIPI_DISTRO_JSON=https://raw.githubusercontent.com/tipi-build/distro/c5d489ccaf51f28fcb35def9e58ec78ab3491169/distro.json
  export TIPI_DISTRO_JSON_SHA1=eee2d2d8b9c0f40f91cf9f416a33c52ee9257fa3
  tipi run <cmake|ctest> ...

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Change-Id: I09a7737271c7265c3a88e047c26f2d18da625b58
The matrix has started failing intermittently on CI only (a couple of
native-cmake scenarios per run report STALE), and the log so far carries just
the verdict, which is not enough to tell the candidate causes apart. Capture,
at the moment of failure:

- the migration cycle's configure output (HFC's populate decision: cold
  populate / warm reuse / update / invalidate, and whether the install and
  configure markers short-circuited the dependency rebuild) and build output
- the source cache on disk, with the variant id each clone actually holds, so
  "populated the wrong sources" is distinguishable from "right sources, stale
  binary", plus whether a sub-build dir was configured (i.e. which populate
  implementation ran)
- the direct-population step stamps, which live per consumer build tree and
  are keyed on the content name only, so they are shared across source-cache
  keys - a stale download.stamp there would silently skip a re-download
- mtimes of the installed library, header and the consumer executable, to
  separate "dependency never rebuilt/reinstalled" from "consumer never
  relinked"

Fires from both assertion helpers (the CLEAN-expecting migrations and the
exact-outcome cases) only when the outcome deviates, so passing runs stay
quiet; outputs are tail-bounded to stay readable in CI logs.

Verified under CMake 4.4.0: a deliberately flipped expectation produces the
full dump, and the unmodified scenarios pass with no output.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Change-Id: If96dc2d63d2355849556d59d2940401dfa221610

@Lambourl Lambourl 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 for this great post. But I think the examples should also be built with CMake 4 (as they already are with CMake 3); that would give us an idea of how they behave with CMake 3 or CMake 4.

Otherwise, the PR looks really solid.

Comment thread .github/workflows/ci.yml
# only thing this job adds over `build`.
build-cmake4:
name: Run HermeticFetchContent tests (latest CMake 4)
if: ${{ !inputs.docker_image }}

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.

This should not be overlooked; this change should be tested with every new commit to cmake-re. This would give us information on how cmake-re interacts with CMake 4.

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