Vendoring¶
A project that must build without fetching anything, as a distribution packager requires, can carry a verbatim copy of core-cpp in its own tree. contour consumes core-cpp this way, so that packaging contour adds no new dependency. This page is the contract between core-cpp and such a consumer; it is the design spec's Part I §5.
The tool is cmake/CoreCppVendor.cmake,
a CMake script run with cmake -P. It needs nothing but CMake and, for sync, git.
The copy is verbatim¶
- Byte-identical to a tag, or to a full commit SHA. Nothing else is a valid source: a branch changes under your CI without a commit in your repository.
- No local changes, ever. A fix goes to core-cpp, is released as a patch version, and the consumer re-vendors that tag. The check below refuses a copy that differs from its manifest, so a local edit fails the consumer's own test suite.
The commands¶
# Copy a ref's files into <dir> and write <dir>/MANIFEST. The script is a core-cpp checkout's:
# sync reads a git repository, and REPO defaults to the one the script itself is in.
cmake -DMODE=sync -DREF=<tag or full SHA> -DDEST=<dir> \
-P <core-cpp checkout>/cmake/CoreCppVendor.cmake
# The same, choosing the modules and the repository explicitly. MODULES is a CMake list, so the
# shell must not see its semicolons: quote the whole -D argument.
cmake -DMODE=sync -DREF=<tag or full SHA> -DDEST=<dir> -DREPO=<url or path> \
"-DMODULES=base;log;cli;platform;async;net;testing" \
-P <core-cpp checkout>/cmake/CoreCppVendor.cmake
# Verify <dir> against its MANIFEST. Needs no git, and this one IS the copy's own script.
cmake -DMODE=check -DDEST=<dir> -P <dir>/cmake/CoreCppVendor.cmake
REPOis a local checkout, a bare repository, or a URL, which is cloned once, bare, into the directorysyncassembles the new copy in —<dir>.core-cpp-vendor-new, beside<dir>— and removed again before that copy is put in place. It defaults to the repository the script itself is part of, which is whysyncis run with a checkout's script and never with the copy's: from inside a copy that default is the repository the copy lives in — yours.syncrefuses aREPOthat is not the root of its own repository, and a ref whose tree is not core-cpp's, so that mis-invocation stops with a message instead of replacing the copy with your own files.MODULESselects which module directories are copied; it defaults to every module the ref has. A module that no option can switch off must be in the list, andsyncreads the ref's owncmake/CoreCppModules.cmaketo refuse a list that leaves one out;CORE_CPP_WITH_TUI=OFFis what lets contour leavetuiout.REFis a tag or a full 40-character commit SHA, andsyncrefuses anything else. A branch,HEADor a short SHA names a different tree from one day to the next, and the manifest would then record a ref that cannot restore the copy it describes.syncreads git blobs withgit -c core.autocrlf=false -c core.eol=lf cat-file blob, so no working tree's line-ending settings reach the copy, and it refuses a file containing a CR byte, a symbolic link and a submodule.- A refusal leaves the previous copy exactly as it was.
syncassembles the whole new copy in<dir>.core-cpp-vendor-new— a sibling of<dir>, not something inside it — and touches<dir>itself only once that copy is complete and legal. Every refusal deletes the sibling on its way out, so aDESTthat a refusedsyncfound still passes its owncheckafterwards, with nothing new beside it. <dir>is the old copy or the new one, never half of each, and never an unfinished one. TheMANIFESTis written into<dir>.core-cpp-vendor-newbefore the swap, so that tree is already a copy you could verify. Putting it in place is then two directory renames —<dir>to<dir>.core-cpp-vendor-old, then<dir>.core-cpp-vendor-newto<dir>— and the old copy is deleted only after both have succeeded. Whichever of the two directories exists when asyncstops, for any reason including being killed, is a complete copy that passescheck. If the second rename fails, and on Windows a rename can fail on an open handle, a lock or a scanner, the previous copy is put back andsyncrefuses. If that restore fails too,syncrefuses and names both directories, deleting neither, so you can adopt either one by renaming it to<dir>. A<dir>.core-cpp-vendor-oldyou find on disk is a previous run that got that far: it holds the only copy of what was there, sosyncrefuses to run again — before it reads anything — until you have moved it back or deleted it.syncreplaces the copy it finds, whole.<dir>is swapped for the new tree rather than edited in place, so a file the new ref no longer has is gone rather than left behind. What it refuses to replace: a<dir>holding files and no manifest, because that is not a copy of ours; and a<dir>that is a regular file rather than a directory, because that is someone's file. Both are refused before anything is read or written.MANIFESTstarts with header lines naming the repository, the ref, the commit, the modules and the file count (# repository ...,# ref ...,# commit ...,# modules ...,# files ...), followed by one<sha256> <path>line per file, sorted by path. It is written with LF endings whatever the host ransync, because you commit this file: two correct syncs of the same tag from a Windows and a Linux machine must not differ in every line. The copied files themselves are the commit's bytes whatever the host.checkrefuses a hash mismatch, a file the manifest lists that is missing, and a file the manifest does not list. It also refuses a manifest that is not one, because an emptied copy beside an emptied manifest would otherwise have nothing left to disagree about and would pass: a line that is neither a#header nor<sha256> <path>; a missing# repositoryor# refline; a# committhat is not 40 lowercase hex digits; and a# filescount that is absent, is not a number, is zero, or disagrees with the lines below it. It reports every refusal that applies, not only the first.- File modes are outside the contract. The copy is bytes and paths;
syncwrites every blob as an ordinary file andcheckcompares no mode, so a100755blob arrives without its execute bit and achmodinside a copy is invisible. Nothing in the file set is executable today, and nothing in it may become executable without this line changing first.
What is copied¶
CMakeLists.txtandcmake/**;- everything directly in
src/core/(thebasemodule: its headers, its sources, its ownCMakeLists.txtandConfig.hpp.in, without which the copy does not configure); src/core/<module>/**for each module inMODULES, the modules' own*_test.cppfiles included;LICENSE,NOTICE,README.md,CHANGELOG.md,.clang-formatand.clang-tidy.
The top-level tests/ directory is not part of the set, so a vendored copy cannot build
core-cpp's own test suite: leave CORE_CPP_TESTING off, which is its default for a subproject.
What the consumer does¶
- Mark the copy as binary for git and skip it in formatting:
.gitattributesgetsvendor/core-cpp/** -text, and.clang-format-ignoregetsvendor/**. Otherwise a checkout's line-ending conversion or a formatting run changes the bytes the manifest pins. - Add it without fetching and without building what you do not link:
With CORE_CPP_FETCH_DEPS OFF, a dependency that neither your project nor find_package
provides stops the configure with the option that needed it, instead of reaching the network.
3. Register the verbatim check as a test, so a local edit fails your own suite:
add_test(NAME core-cpp-vendored-copy
COMMAND ${CMAKE_COMMAND} -DMODE=check
-DDEST=${CMAKE_CURRENT_SOURCE_DIR}/vendor/core-cpp
-P ${CMAKE_CURRENT_SOURCE_DIR}/vendor/core-cpp/cmake/CoreCppVendor.cmake)
To move to a new release, run sync with the new tag and commit the result as one change.
tests/consumer-vendored/ in core-cpp is exactly this, as a project of its own: core-cpp's own CI
exports a copy of each commit, then configures, builds and verifies it inside a container with no
network and no git, which is what a packager's build has.
Release archives¶
Each release also carries core-cpp-vX.Y.Z-vendor.tar.gz, the exported file set of that tag,
and a SHA256SUMS file, for a packager who vendors without git.