Version 2.0.1
[Summary]
BuildMaster is a small CMake DSL for a graph of other people’s builds.
You declare each dependency once — how it is produced, what it waits on — and the parent gets one shared prefix instead of a pile of ExternalProject / FetchContent glue.
CMake and Meson trees are first-class.
Typical use: add the submodule, add_subdirectory, declare components, then depend / link.
Declaration order does not matter.
The public surface is ten commands on purpose; everything else is internal.
If you landed here from a release link and have not read the tree:
- How to write a component, every optstr key, and the contract: README.md
- The ten names, and nothing else: public_functions.md
- Porting an older caller: MIGRATE.md
Fixed
- Nested
project()no longer stacksbm/<id>under the child's-B. Component build dirs stay${BUILDMASTER_BINDIR}/bm/<id>of the outermost consumer, so WindowsCMAKE_OBJECT_PATH_MAX(250) is not blown by Suite BUNDLED graphs (Crypto → Buffer → Logger → String → Base). - Windows env runner
Add-TypedumpedSOURCE_CODE_ERRORunder MSVCLIB.Enable-BmAnsiConsolecompiled the VT P/Invoke while the process still had the parent jobLIB/INCLUDE/LIBPATH(invalid SDK fragments such as10.0.26100.0//x64). PowerShell 5.1 treats those as compiler errors. The variables are cleared only aroundAdd-Typeand restored after, in bothrunner_windows.ps1.inandrunner_windows_silent.ps1.in. NestedLIBprepend for the real compile is unchanged. - Nested
-G Ninjaused a ghostCMAKE_MAKE_PROGRAM. Theninjatool already exportedNINJA_EXECUTABLEon the component toolchain. It did not exportCMAKE_MAKE_PROGRAM, so a child CMake (including aclang-clleaf under anmsvcparent) ranC:/ProgramData/chocolatey/bin/ninja.exe --versionand died when that path was missing. When theninjatool is loaded,update_toolchain.cmakenow also exportsCMAKE_MAKE_PROGRAMto the same binary. If the tool was not requested, nothing is written. - A later build re-entered the leaf on the unpatched tree. Configure, build and install now hash the component srcdir with CMake only (
file(SHA256)per file,string(SHA256)of the sorted paths). Nosha256sumand nocat. The other half of the key is options, toolchain,CMAKE_BUILD_TYPE, IPO,mode,producedand host OS/arch, stored inbm-stamp.extra.bm-stamp.keyis created when install finishes and refreshed when a later build actually recompiles, and is the file a later install cache will read. A match skips nested cmake/meson and does not touch the prefix. A miss runs that stage.GIT={PATCH}is applied before the hash and the git root is reset when the last live holder finishes, including on a hit, so a latermakemay re-apply and re-hash. The hit still does not parse upstreamCMakeLists.txt. Fixturestamp-patch: upstreamcmake_minimum_required(VERSION 99.0), the patch lowers it to 3.20, the secondstamp-leaf_installis a stamp hit and the worktree is back on 99.0. buildmaster_group_addrequired the group to already exist. Membership is stored even whenbuildmaster_grouphas not run yet. Finalize (_bm_group_plan) is the only existence check: an id named bybuildmaster_group_addthat was never created is FATAL, including when that was the only declaration. Cycles and id clashes stay FATAL where they already were. A group is still not a component, a meta, or a link edge. Fixturegroups-late:buildmaster_group_addnameslate-outerandlate-innerbefore eitherbuildmaster_group. The outline is stilllate-outer→late-inner→late-leaf(indent 2). Negativegroup-undefineddies at configure withgroup was never created.- Shared-dep skip dropped the consumer's need. First-wins still skips configure and build when
links/<id>.cmakealready exists (already built by). That skip is not "this consumer has no dependency". The nested process appends the skipped id tobm-reuse-needs.txt. After that configure returns, the parent walks the id and the dests already stored in its links file and hangs the closure on the consumer (LINKS_ATTACHED, so a later rewrite keeps it).<consumer>_configure/<consumer>_buildwait on<id>_installwhen that stage is in this process, otherwise on the parent component that created the links file (<owner>_install). A skipped configure is still not a second build. Fixturelinks-race: RaceA and RaceB both declare RaceMid, RaceMid declares RaceBase, no parentbuildmaster_dependand no hoist.RaceB_buildwaits onRaceA_install. Both shared libs call a Base-only symbol and link only RaceMid. - Transitive
buildmaster_linkstopped at one recorded hop.buildmaster_link(A B)still names only B._bm_links_write_onenow unions this process's link edges withLINKS_ATTACHEDand the dests already stored inlinks/<id>.cmake, then walks those ids to a fixpoint (seen-set, cycle cut). A parent rewrite no longer replaceslinks/StormByte-String.cmakewith the edges it knows and drops String → Base. A later configure that skips the winner still flattens-l/Stem.libfor the whole chain (--no-allow-shlib-undefined, Apple-undefined,error; Windows already fails unresolved). The caller does not name grandparents. Fixturelinks-transitive: shared TransBase ← TransMid ← TransUpper, and deferred TransLeaf links only TransUpper after the skip, while calling a Base-only symbol. WHOLEwrap was empty on ELF._bm_opt_whole_itemsbuilt-Wl,--whole-archive+ produced.a+-Wl,--no-whole-archive, thenfragment.cmakeflattened the CMake list to spaces andtarget_link_libraries(<id> INTERFACE …)let CMake classify the-Wltokens as flags and the archives as libraries. The DSO line becamelibavutil.a … libavfilter.a -Wl,--whole-archive -Wl,--no-whole-archive. GNU ld.bfd (single pass) then dropped unreferenced avutil objects (av_md5_sum, AES/HMAC, …) while lld still linked. ELF now emits one$<LINK_GROUP:BM_WHOLE,…>and registersCMAKE_{,C_,CXX_}LINK_GROUP_USING_BM_WHOLE(prefix / suffix--whole-archive/--no-whole-archive) so every produced static of that id stays inside the wrap. Apple (-force_load) and MSVC (-WHOLEARCHIVE:) are unchanged. The fragment no longer replaces;with spaces.WHOLEstill means one region around all produced archives of the id, not one wrap per file.