A declarative CMake DSL for turning other people’s CMake and Meson projects into first-class nodes in your graph.
You register components and edges in any order. BuildMaster materializes
stage targets, IMPORTED libraries, and link lines once — at the end of
the parent CMAKE_SOURCE_DIR — into a single install prefix, with a
toolchain and environment that actually reach nested Meson.
This is not a wrapper around ExternalProject_Add. It is not FetchContent
with extra macros. It is a small language for graphs of third-party builds.
One well-behaved CMake library? FetchContent is enough.
Twelve upstreams — some CMake, some Meson, some that install zsd.lib when
you asked for z.lib, some that must configure after another prefix
exists, some that only work under clang-cl, and a static plugin pack the
linker will drop unless you wrap it in --whole-archive — and you already
have a private orchestration layer. Usually it is add_custom_command,
hardcoded paths, and “remember to declare ogg before vorbis”.
BuildMaster is that layer, written once:
| You stop writing… | You get… |
|---|---|
| “Declare A before B or configure explodes” | Order-independent registration |
ExternalProject that configures at build time |
Eager configure when the graph allows it |
Hand-rolled Meson setup that misses .pc files |
Same prefix, PKG_CONFIG_PATH, compilers, cache launchers |
POST_BUILD rename scripts per MSVC flavor |
Optional RENAME on the component |
--whole-archive soup in the parent |
WHOLE on a component or a meta collection |
LNK2005 / duplicate .res after /WHOLEARCHIVE |
STRIPRES on static MSVC/clang-cl archives (default on) |
Hand-written .pc so the next Meson node finds this prefix |
Optional helper PC={…} on the component |
| “Did anyone actually link this plugin?” | Orphan warnings at configure |
Waiting on a slow tarball every rm -rf build |
Point BUILDMASTER_DOWNLOADSDIR at a folder you keep |
The cost is a short public API. The payoff is a parent tree that looks like a product, not a build blog.
- What it is
- Comparison
- Quick start
- Declarative model
- How a component works
- Dependencies and links
- Meta components
- Orphan warnings
- Prerequisites
- Component options
- Whole-archive linking (
WHOLE) - Stripping
.resmembers (STRIPRES) - Helper pkg-config files (
PC) - Build-only components and repack
- Subcomponent specs
- Header-only components
- Per-component toolchains
- Recursive usage
- Logging
- Verbosity of tool output
- Fail-fast
- Compiler cache
- Platform notes
- Git helpers
- File download and decompress
- API map
- Self-tests
- License
- Supporting the project
While the parent is still configuring, you declare:
- components (
create_cmake_component/create_meson_component/ headers variants / low-levelcreate_component) - collections (
create_meta_component+meta_component_add) - edges (
component_dependency,component_link) - optional work that must finish first (
component_prerequisite, file and git helpers)
You do not include() generated fragments. You do not call a public
finalize. Materialization runs once via an internal cmake_language(DEFER)
at the end of CMAKE_SOURCE_DIR.
After that, each real component is a small machine:
<id>_configure → <id>_build → <id>_install
↑
<id> (INTERFACE — this is what you link)
A meta uses the same anchor names (<id>_install waits on members) but
has no sources and installs nothing of its own.
Sources can be a git checkout, a cached tarball, a submodule, or any tree you already have on disk.
Typical shapes: a bundled third-party stack, several bit-depth builds of
the same encoder that you later repack, a header-only SDK, a mixed
CMake + Meson graph on Linux / Windows / macOS, a plugin pack that a
larger library links as one WHOLE node.
| Capability | FetchContent | ExternalProject_Add | BuildMaster |
|---|---|---|---|
| Fetch / manage sources | Yes | Yes | Yes (Git helpers) |
| Cacheable downloads | Partial | Manual | Built-in (BUILDMASTER_DOWNLOADSDIR) |
| Hash-verified downloads | Yes | Manual | Yes, plus reuse across builds |
| Download / unpack during parent configure | Manual | No | Yes (file_* helpers) |
| Declarative graph (order-independent) | No | No | Yes |
| Configure external project | N/A | Build time | Eager or deferred |
| Inspect artifacts before main build | No | No | When eager |
Explicit _configure / _build / _install |
No | No | Yes |
| Attach post-steps to those targets | No | Limited | Yes |
| Native Meson stages | No | Manual | Yes |
| Shared install + env propagation | No | Manual | Yes |
| Compiler cache into child builds | Manual | Manual | Yes |
| Per-component toolchain | No | Manual | Optional |
| Header-only INTERFACE components | Manual | Manual | Yes |
| Meta collections (no sources) | No | Manual INTERFACE | Yes |
Path-qualified subcomponents (subdir/name) |
No | Manual | Yes |
| Whole-archive static link on INTERFACE | Manual | Manual | Optional (WHOLE) |
Strip .res from static MSVC archives |
Manual /REMOVE |
Manual | Default on static (STRIPRES) |
Helper .pc for the shared prefix |
Manual | Manual | Optional (PC={…}) |
| Build-only + static repack | No | Manual | Yes |
Unified log API (buildmaster_message) |
No | No | Yes |
| Safe recursive nesting | Fragile | Fragile | Designed for it |
| Fail-fast after a stage failure | No | Manual | Optional |
INTERFACE depends on _install |
No | Manual | Yes |
| Orphan component / meta warning | No | No | Yes |
Git reset + reconfigure (buildmaster_clean) |
No | Manual | Optional |
| Per-repo post-install git reset | No | Manual | Yes |
set(BUILDMASTER_INITIALIZE_EXTRA_TOOLS "pkgconf") # optional
add_subdirectory(path/to/buildmaster)
include(path/to/buildmaster/helpers.cmake)
buildmaster_message(USER STATUS "Setting up My Library" 1)
set(_opts "-DENABLE_FOO=ON")
create_cmake_component(
mylib
"My Library"
${CMAKE_SOURCE_DIR}/thirdparty/mylib
${CMAKE_BINARY_DIR}/thirdparty/mylib_build
"${_opts}"
shared
"mylib"
)
target_link_libraries(MyApp PRIVATE mylib)No out-variable. No generated fragment to include(). Stage targets and
IMPORTED libraries appear when the parent CMakeLists.txt finishes.
Optional policy string (one trailing argument, never a pile of positionals):
create_cmake_component(
mylib
"My Library"
${CMAKE_SOURCE_DIR}/thirdparty/mylib
${CMAKE_BINARY_DIR}/thirdparty/mylib_build
"${_opts}"
static
"mylib"
"INDENT=2;TOOLCHAIN=clang-cl;RENAME;WHOLE;PC={VERSION=1.2.3;NAME=mylib}"
)On a static MSVC / clang-cl archive, STRIPRES is already on. You only
write it when you want it off (STRIPRES=OFF). Meson is the same shape
with create_meson_component.
- Register components (order does not matter).
- Optional: group them with
create_meta_component/meta_component_add(addmay run beforecreate). - Connect with
component_dependencyand/orcomponent_link(again, any order). - Optional:
component_prerequisite,file_download/file_download_cached/file_decompress(these run during the call, so sources exist beforecreate_*), or configure-timecreate_git_*. - End of
CMAKE_SOURCE_DIR: BuildMaster materializes. Unused ids produce one WARNING.
| Nested configure | When |
|---|---|
| Eager | The component is not the source of any component_dependency — it can configure while the parent configures. |
| Deferred | It depends on another node — configure runs at build time under <id>_configure. |
That is the same behaviour you want by hand (consumer after producer) without writing two APIs.
A tarball you unpack in the same CMakeLists.txt as create_* does
not need a dependency edge for the first configure: the files are
already on disk. Add component_dependency / component_prerequisite
only when a later rebuild of that file target must precede configure.
| Target | Role |
|---|---|
<id> |
INTERFACE. Depends on <id>_install. This is what you link. |
<id>_configure |
Nested CMake or Meson setup |
<id>_build |
Compile |
<id>_install |
Install into BUILDMASTER_INSTALL_DIR (skipped for BUILDONLY) |
| produced libs | STATIC / SHARED IMPORTED files under the prefix (or the build dir) |
Library-mode installs list archive paths as OUTPUT so Ninja can depend on
real files, not empty stamps.
Ids become target and script names — keep them filesystem-friendly. Titles may contain spaces; they only appear in status lines.
The INTERFACE stub exists as soon as you call create_*. You may
add_library(Vendor::Foo ALIAS foo) in the same file. Produced paths
are filled in at materialize.
Order-only edge. At materialize time dest resolves as the first match:
- Registered component id →
<id>_install - Registered meta id →
<id>_install - Name matching
*_install/*_configure/*_build - Existing CMake target (prerequisite,
file_*, your own custom target)
Otherwise: FATAL_ERROR.
Use this when you need ordering without a link line (headers-only producer, a download that is not a library, a host target that writes files).
Records a link on the component INTERFACE.
If dest is a graph node (component, meta, stage, or existing target),
BuildMaster also records component_dependency. A raw library spec
(foo, vendor/foo) does not get an automatic wait edge.
Host binaries are not graph nodes. Link them the ordinary way:
target_link_libraries(MyApp PRIVATE mylib)
target_link_libraries(MyApp PRIVATE plugins) # a metaA meta is an INTERFACE plus a graph anchor. No sources, no compile,
no artifacts of its own. It collects members (components, other metas,
static or shared) and forwards wait + link. It may set WHOLE on the
collection even if members did not.
TOOLCHAIN on a meta does not compile the meta. At materialize time
that profile is copied onto members (and onto component_dependency /
component_link destinations whose source is the meta) that do not
already have TOOLCHAIN set. An explicit TOOLCHAIN on the child is
kept. Two metas inheriting different profiles onto the same empty
destination is FATAL.
RENAME, BUILDONLY and STRIPRES on a meta are ignored with a warning
when the key is actually written (there is nothing to install or strip).
The default-on STRIPRES does not warn on a meta that never mentioned it.
PC={…} on a meta is FATAL. A collection has no single library
contract. Generating one .pc from an unbounded member set would invent
Requires you did not choose and collide with upstream files. Put
PC={…} on the leaf that owns the archive.
| Call | Meaning |
|---|---|
meta_component_add(meta, member…) |
Membership. member belongs to meta. |
component_link / component_dependency / host target_link_libraries to the meta |
Consumption. Something actually needs the collection. |
If nothing consumes the meta, members are not built just because they were added. That is deliberate: a plugin pack you forgot to link should not silently compile half the tree.
meta_component_add may run before create_meta_component. Cycles
(plugins → codecs → plugins) are FATAL.
meta_component_add(plugins zlib png)
create_meta_component(plugins "Plugin pack" "INDENT=1;WHOLE;TOOLCHAIN=clang")
component_link(engine plugins)
target_link_libraries(MyApp PRIVATE engine)zlib and png compile as clang unless they already declared their
own TOOLCHAIN.
After materialize, components and metas that were never consumed — no link,
no dependency, no host target_link_libraries, no used component_repack
— are listed in a single WARNING.
Membership in an unused meta does not count. A BUILDONLY phase that only
feeds an unused repack is still an orphan (and so is that repack).
component_prerequisite(mylib my-unpack)<id>_configure waits on an existing target: a download, an unpack, a
custom codegen step, anything CMake already knows.
File helpers already run during the file_* call (see below). Use a
prerequisite or component_dependency when a rebuild of that target
must happen before a deferred configure — not to get the files onto
disk the first time.
Every create_*_component accepts at most one optional trailing
argument:
KEY=value;KEY2=value with spaces;PC={VERSION=1.0.0;NAME=foo}
| Rule | Detail |
|---|---|
| Pair separator | ; outside {…} |
| Brace group | PC={…} — ; inside the braces is part of the group |
| Key / value | Only the first = in a pair |
| Keys | Case-insensitive, stored UPPERCASE |
| Values | May contain spaces and extra = (test==value is fine) |
; outside braces |
Pair break. ; inside {…} is allowed |
| Bare flag | RENAME / WHOLE / BUILDONLY / STRIPRES / PC ≡ KEY=ON |
Bare PC / PC=ON without {…} |
FATAL — use PC={VERSION=…} or PC={ENABLED=FALSE} |
| Unknown key | WARNING, ignored |
| Extra positional arguments | FATAL_ERROR |
| Key | Meaning |
|---|---|
INDENT / INDENT_LEVEL |
Tabs after the log header (non-negative integer) |
TOOLCHAIN |
Profile (gcc, clang, clang-cl, msvc). Empty = inherit |
RENAME |
Normalize archives to the declared name (install prefix, or build dir if BUILDONLY) |
WHOLE |
Whole-archive link of produced static archives |
BUILDONLY |
Do not install into the shared prefix |
STRIPRES |
After RENAME, strip .res members from static MSVC / clang-cl archives (default ON) |
PC={…} |
After install, write a helper .pc under the shared prefix (see below) |
Static plugin-style archives often contain objects the linker will drop
unless the whole archive is forced in. Set WHOLE on the component or on
the meta that collects them.
One linear group per consumer (never nested --whole-archive sandwiches):
-Wl,--whole-archive A B -Wl,--no-whole-archive # ELF
-Wl,-force_load,A -Wl,-force_load,B # Mach-O
-WHOLEARCHIVE:A.lib -WHOLEARCHIVE:B.lib # MSVC (Ninja-safe spelling)
On shared, headers, or BUILDONLY, WHOLE is ignored with a warning.
A non-WHOLE library linked next to a WHOLE meta stays outside the group.
WHOLE is why STRIPRES exists. Forcing every object out of two static
.lib files also forces every .res those archives still carry. Two
upstreams that both compiled a resource script suddenly share a symbol
name the linker will not forgive. See the next section.
You already know this one if you have ever linked a static plugin pack
on MSVC with /WHOLEARCHIVE.
A .res is a Windows resource object. In a DLL it is useful. In a
static .lib it is ballast: version info, manifests, icons nobody
will load from an archive member. MSVC and clang-cl still stuff one into
the library because that is what the toolchain does when a .rc is in
the sources.
Then you ask the linker for the whole archive — because otherwise the
plugin objects vanish — and two otherwise unrelated .lib files both
contribute something.res. The link dies with a duplicate resource
symbol. The “fix” people reach for is a one-off lib /REMOVE:….res
after staring at lib /LIST until they guess the member name. Next
upstream, next filename, next POST_BUILD.
BuildMaster does not ask you for the member name. After RENAME (so the
canonical .lib already exists), install lists every member with
lib.exe / llvm-lib /LIST, keeps only basenames that end in .res
(case-insensitive), and /REMOVEs those. Anything else in the archive
is left alone.
| Case | Behaviour |
|---|---|
Static + MSVC / clang-cl + STRIPRES on (default) |
Strip after rename / contract |
Static + BUILDONLY |
Same, against the component build dir |
| Static + other toolchain | Silent no-op (no warning) |
| Shared / headers | WARNING, ignored |
| Meta | WARNING only if you wrote the STRIPRES key |
STRIPRES=OFF |
Skip. Use this if you actually need the resources |
You do not list members. You do not write a per-library script. If a repack consumes those statics, the inputs are already clean because strip ran on each component’s install (or BUILDONLY “install”) first.
create_cmake_component(
plugin-a
"Plugin A"
${A_SRC} ${A_BUILD}
"${A_OPTS}"
static
"plugina"
"WHOLE"
)
# Opt out when the .res is load-bearing:
create_cmake_component(
branded
"Branded static"
${B_SRC} ${B_BUILD}
"${B_OPTS}"
static
"branded"
"STRIPRES=OFF"
)This is not a replacement for a real upstream .pc. It exists so
later components in the same BuildMaster prefix can find this library
without you writing a file(WRITE …) after install.
Typical pain: the archive lands under BUILDMASTER_INSTALL_DIR, Meson
or another CMake node looks at PKG_CONFIG_PATH, and the project never
shipped a .pc (or shipped one only for the system layout). You already
know the name, the version you care about, and the component_link graph.
BuildMaster can emit a small helper file from that.
create_cmake_component(
ogg
"Ogg"
${OGG_SRC} ${OGG_BUILD}
"${OGG_OPTS}"
static
"ogg"
"PC={VERSION=1.3.5;NAME=ogg;DESCRIPTION=Ogg bitstream}"
)
create_cmake_component(
vorbis
"Vorbis"
${VORBIS_SRC} ${VORBIS_BUILD}
"${VORBIS_OPTS}"
static
"vorbis"
"PC={VERSION=1.3.7;NAME=vorbis}"
)
component_link(vorbis ogg)After ogg_install / vorbis_install:
${BUILDMASTER_INSTALL_LIBDIR}/pkgconfig/ogg.pc
${BUILDMASTER_INSTALL_LIBDIR}/pkgconfig/vorbis.pc # Requires: ogg
prefix / libdir / includedir are the BuildMaster install tree.
That is enough for this project. A portable distro .pc is out of scope.
| Inner key | Required | Default |
|---|---|---|
VERSION |
when enabled | — FATAL if missing |
NAME |
no | First produced spec basename, else the component id |
DESCRIPTION |
no | Component title |
ENABLED |
no | TRUE. ENABLED=FALSE skips the file and does not require VERSION |
| Field written | Source |
|---|---|
Name / Version / Description |
Inner keys (or defaults above) |
Libs |
-L${libdir} plus -l<produced> for each produced spec |
Requires |
Direct component_link destinations that are registered components with PC enabled (not metas). One hop, not a full flatten |
Cflags |
Extra flags from the component’s own configure options minus the parent CMAKE_C{,XX}_FLAGS. Include tokens (-I, /I, -isystem) are dropped — the prefix include dir is already in the BM environment |
| Case | Behaviour |
|---|---|
PC={VERSION=…} |
Write after RENAME + STRIPRES |
PC={ENABLED=FALSE} |
No file. Keep the group in the options string |
Bare PC / PC=ON |
FATAL — braces are the contract |
| File already exists at the canonical path | FATAL — do not clobber an upstream .pc |
BUILDONLY + enabled PC |
FATAL — there is no shared prefix to publish into |
Meta + PC={…} |
FATAL — unbounded membership, no single library |
| Unknown inner key | WARNING, ignored |
Keep ENABLED=FALSE when you are mid-port and do not want to delete the
group. Turn it back on without rewriting the rest of the options string.
Some upstreams are not “the library you ship”. They are intermediate static archives you later merge (several bit-depth builds, a main lib plus an extra helper built from the same tree).
BUILDONLY:
- still has
_configure/_build/_installanchors (_installis a coherence target — it does not publish to the shared prefix) - artifacts live in that component’s build directory
RENAMEis allowed and runs against the build dirSTRIPRESis allowed and runs against the same build-dir archivesPC={…}withENABLED=TRUEis FATAL (helper.pcfiles belong on the shared prefix)component_linkfrom a normal component to a BUILDONLY is FATAL (you cannot link a tree that was never installed)
component_repack(id title output inputs…) merges listed archives with the
host archiver (ar / llvm-ar / lib.exe / libtool) into one file under
the shared prefix and exposes it as an IMPORTED target. Inputs may be
BUILDONLY components. The repack waits on each input’s _build, not
_install, so BUILDONLY works. A custom host target that only has artifacts
(no _build) is accepted as a corner case.
A repack that nothing consumes does not mark its inputs as used.
One component can produce several archives. List them on create_*:
| Spec | File | IMPORTED target |
|---|---|---|
mylib |
${BUILDMASTER_INSTALL_LIBDIR}/libmylib.a |
mylib |
vendor/foo/foolib |
${BUILDMASTER_INSTALL_LIBDIR}/vendor/foo/libfoolib.a |
vendor_foo_foolib |
library_import_static_hint(out name prefix [subdir])
library_import_hint(out name prefix [subdir])A static .a does not pull sibling static archives. List every required
spec on the outermost component, or component_link them.
create_cmake_headers_component / create_meson_headers_component:
- no IMPORTED archive
- install stamp under the include tree
INTERFACE+SYSTEMinclude ofBUILDMASTER_INSTALL_INCLUDEDIR
Useful for SDKs and for graphs that only need headers before a later compile.
TOOLCHAIN= pins that component (and nested BuildMaster under it).
The parent job’s compiler does not change.
| Name | Drivers | Linker |
|---|---|---|
gcc |
gcc / g++ |
System default |
clang |
clang / clang++ |
LLD required on Linux; not forced on macOS |
clang-cl |
clang-cl |
lld-link + llvm-lib (Windows) |
msvc |
cl |
link.exe + lib.exe (Windows) |
Unknown names fail at configure and list known profiles. Nested Meson
always receives the matching native file
(BUILDMASTER_MESON_NATIVE_FILE), including when the profile is inherited.
That keeps ccache/sccache keys coherent.
An external CMake project may add_subdirectory(buildmaster) again.
BuildMaster initializes once (BUILDMASTER_CONFIGURED) and reuses the
install root, markers, scripts, and log level.
Pass the repo root if the nested project must find BuildMaster:
create_cmake_component(
nest
"Nested graph"
${NEST_SRC}
${NEST_BUILD}
"-DBUILDMASTER_ROOT=${BUILDMASTER_ROOT}"
static
"vendor/nest/nestlib;vendor/nest/midlib"
)Third-party trees that themselves vendor BuildMaster must sit as sibling directories of the BuildMaster checkout the parent added. Nested projects expect that layout.
All BuildMaster diagnostics go through one API. Do not use CMake
message() in a project that uses BuildMaster (and never inside
BuildMaster itself, except log.cmake). Raw message() ignores
BUILDMASTER_LOGLEVEL and breaks the aligned headers.
buildmaster_message(<module> <level> "<text>" [<indent>])| Argument | Meaning |
|---|---|
module |
Who is speaking. Internal keys below, or USER from a parent project. |
level |
LOWLEVEL, DEBUG, INFO, WARNING, STATUS, FATAL (always uppercase). |
text |
Body. The header is never indented. |
indent |
Optional tab count after the header (default 0). |
USER is reserved for your project (header label User). Use it for
lines such as “Setting up the library”, not an internal name like CMake.
buildmaster_message(USER STATUS "Setting up the library" 1)
buildmaster_message(USER INFO "extra data already cached" 2)
buildmaster_message(USER FATAL "extra data hash missing")-- [BuildMaster/User ]: Setting up the library
-- [INFO ][BuildMaster/User ]: extra data already cached
Higher number = quieter filter threshold:
| Level | Role |
|---|---|
LOWLEVEL |
Function enter/exit and path plumbing |
DEBUG |
Useful when debugging BuildMaster or a consumer graph |
INFO |
Optional progress (rename skip, unpack OK, .res strip skip) |
WARNING |
Shown at INFO or more verbose; hidden at STATUS and FATAL |
STATUS |
Default. Stage titles (Configuring / Compiling / Installing) |
FATAL |
Always printed. Stops configure/script. Never filtered |
BUILDMASTER_LOGLEVEL=FATAL is the quietest user setting. Allowed;
discouraged.
An unknown level (typo DEHBUG) is FATAL and lists accepted names.
A line is printed when its level number is ≥ the current
BUILDMASTER_LOGLEVEL, except:
FATALis never droppedWARNINGis dropped when the current level is stricter thanINFO
STATUS lines use only the module header. Other levels prefix a padded
level tag with no space between the two brackets:
-- [BuildMaster/CMake ]: Configuring My Library
-- [DEBUG ][BuildMaster/File ]: cache hit
BUILDMASTER_DEBUG is ignored. Use BUILDMASTER_LOGLEVEL.
BUILDMASTER_VERBOSE is independent of the log level. It controls whether
nested CMake / Meson / Ninja stdout is shown on success. Failures always
print captured output.
BUILDMASTER_FAIL_FAST=ON writes a marker after a stage failure so later
stages skip instead of cascading.
ccache / sccache launchers and cache directories propagate into nested
CMake and Meson (including the Meson native file). Keep
BUILDMASTER_MESON_NATIVE_FILE aligned with TOOLCHAIN= so cache keys
do not mix compilers.
- Windows shared: DLLs under
CMAKE_INSTALL_BINDIR, import libs underCMAKE_INSTALL_LIBDIR.RENAMEtreats both and keeps the produced case. - Windows static: archives under
LIBDIRonly. - Unix:
lib/lib64fromGNUInstallDirs(BuildMaster loads it). - Nested configure injects
-I/-L(and WindowsINCLUDE/LIB) for the shared prefix so a child project finds siblings without extra flags.
Bound to a component id. Configure-time ops run when you call them; a post-install reset can restore the tree after patching.
create_git_reset_file(mylib "MyLib reset" ${MYLIB_SRC_DIR})
create_git_patch_file(mylib "MyLib patch" ${MYLIB_SRC_DIR} ${PATCH_FILE})
create_git_switch_branch(mylib "MyLib branch" ${MYLIB_SRC_DIR} my-topic)
create_git_fetch(mylib "MyLib fetch" ${MYLIB_SRC_DIR})buildmaster_clean resets registered git roots and is meant to be followed
by a reconfigure. buildmaster_git_post_install_marker_for_srcdir resolves
the reset script path for a source tree.
CMake can already hash a download. What it does not give you for free is a
stable cache that survives rm -rf build, plus a call that leaves the
file on disk before create_*_component runs.
Default destination is ${BUILDMASTER_BINDIR}/downloads. Point
BUILDMASTER_DOWNLOADSDIR at a folder outside the build tree and the
same URL + hash is reused on the next configure. No extra if(EXISTS), no
hand-rolled stamp files.
# Keep tarballs across wipe-and-rebuild
export BUILDMASTER_DOWNLOADSDIR="$HOME/.cache/buildmaster/downloads"
# or
cmake -DBUILDMASTER_DOWNLOADSDIR=/var/cache/buildmaster/downloads …A slow extra-data tarball from a far-away host should not be the reason you wait before you can compile your code again. The first configure pays the network; every configure after that is a hash check against a file you already have.
| Function | Role |
|---|---|
file_download_cached |
Reuse the file when the hash matches; download only on miss or mismatch |
file_download |
Always fetch (progressive backoff) |
file_decompress |
Unpack into a directory |
Contract
- No out-variable. No
include()of a generated script. - Each call creates a CMake target of the same
name. - The generated
-Pscript also runs during that call (parent configure). Scripts are idempotent: a cache hit or an already-extracted tree is a no-op. - After
file_download_cached/file_decompressreturn, the artifact is on disk. You maycreate_*_componentagainst that tree in the same file; that component can still eager-configure. DEPENDSon the file helpers is a build-graph edge only. It does not delay the configure-time run. Call download before decompress in the sameCMakeLists.txt.- Paths are rejected if they contain
..traversal. - Optional graph wiring:
component_prerequisite/component_dependencyso a rebuild of the file target precedes a deferred<id>_configure. You do not need that edge just to unpack once at configure.
file_download_cached(extra-download
"https://example.invalid/extra.tar.gz"
EXPECTED_HASH SHA256=${EXTRA_HASH}
TITLE "Example extra data"
INDENT 2
)
file_decompress(extra-unpack
"${BUILDMASTER_DOWNLOADSDIR}/extra.tar.gz"
"${CMAKE_BINARY_DIR}/extra"
TITLE "Example extra data"
INDENT 2
)
create_cmake_component(
mylib
"My Library"
"${CMAKE_BINARY_DIR}/extra/upstream"
"${CMAKE_BINARY_DIR}/extra_build"
"${_opts}"
static
"mylib"
)BUILDMASTER_DOWNLOADSDIR is the cache. The unpack destination is yours
(usually under the build tree). Keep the cache folder if you wipe build/.
| Area | Commands |
|---|---|
| Components | create_cmake_component, create_meson_component, create_cmake_headers_component, create_meson_headers_component, create_component |
| Graph | component_dependency, component_link, component_prerequisite |
| Meta | create_meta_component, meta_component_add |
| Repack | component_repack |
| Files | file_download, file_download_cached, file_decompress, file_checksum_correct |
| Git | create_git_reset_file, create_git_patch_file, create_git_switch_branch, create_git_fetch, buildmaster_git_post_install_marker_for_srcdir |
| Log | buildmaster_message |
| Paths / import | ensure_build_dir, library_import_hint, library_import_static_hint, sanitize_for_filename, buildmaster_parse_subcomponent |
| Toolchain | buildmaster_validate_toolchain, buildmaster_load_toolchain_profile, buildmaster_find_archiver |
| Options | buildmaster_parse_component_options, buildmaster_parse_component_pc |
Stage generators (create_cmake_stages / create_meson_stages) are
internal. The supported surface is create_*_component.
A synthetic harness lives under .github/tests/ (not part of the DSL
runtime). It has no real third-party projects.
| You added… | Update |
|---|---|
| Public function or macro | .github/tests/expected/public_functions.txt |
| Propagated / toolchain-exported variable | .github/tests/expected/propagated_vars.txt |
| Dependant graph edge | .github/tests/expected/dependant_edges.txt |
| Smoke install artifact | .github/tests/expected/smoke_artifacts.txt |
Do not hardcode new assertions in .github/workflows/ci.yml.
cmake -S .github/tests/harness -B build/harness -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake --build build/harness --target run_buildmaster_checks
cmake --build build/harness --target run_buildmaster_smokeMIT. See LICENSE.
BuildMaster is free software. If it saves you a weekend of glue code — or if you use it together with the rest of the StormByte stack — voluntary support helps keep maintenance and CI going.
PayPal: StormByte@gmail.com
If you prefer another channel (bank transfer, sponsorship of a specific issue, or something that fits your organisation), write to the same address and we will find a workable option. There is no obligation; the license does not change either way.