A C++ build system written in Go using CUE for configuration. Clue provides minimal configuration for common cases, with CUE's type system catching config errors before build time.
Install from source:
go install github.com/loov/clue@latestTagged versions are also available as Linux, macOS, and Windows archives with SHA-256 checksums on the GitHub releases page.
Or build locally for development:
go build -o clue .For a conventional project, run clue build without a configuration file. Clue treats each directory containing C, C++, or .s/.S assembly sources as a target; a main.c or main.cpp makes that target an executable, while other source directories become static libraries. Project headers provide include roots, and internal includes infer dependencies between those targets. Clue selects the first complete Clang, GCC, or MSVC toolchain available on the host.
Generated, dependency, and hidden directories are skipped. Add a clue.cue file when target boundaries or dependencies cannot be inferred from those conventions; an explicit file always takes precedence:
name: "hello"
version: "1.0.0"
toolchain: {
compiler: "clang"
cxxStd: "c++17"
}
targets: {
hello: {
name: "hello"
type: "executable"
sources: ["main.cpp"]
}
}Validate and build:
clue validate # Check configuration
clue build # Build projectclue.cue may use a CUE package and split configuration across other .cue files in the same directory.
Target sources and headers accept standard file globs such as src/*.cpp.
Configuration can branch on the selected platform through _target.os and _target.arch:
if _target.os == "windows" {
targets.app.defines: ["WINDOWS_BUILD"]
}To run the compiler, linker, archiver, and build commands in a container, add a pre-pulled image containing the selected toolchain:
toolchain: {
compiler: "clang"
cxxStd: "c++23"
container: {
runtime: "podman" // optional
image: "project-toolchain:20"
workdir: "/workspace"
}
}Clue starts a disposable container for each command and mounts the project directory at workdir. Compiler tools and pkg-config execute in that container, so their headers and libraries must be present in the image. When runtime is omitted, Clue uses the first available command from Docker, Podman, Apple container, and nerdctl. Set it to another Docker-compatible executable when needed. When image is used, it must already exist in that runtime. Files outside the project directory are not mounted.
Instead of image, specify a Containerfile to let Clue build and cache the
toolchain image. The project directory is its build context:
toolchain: container: {
containerfile: "toolchain/Containerfile"
platform: "linux/amd64" // optional image platform
workdir: "/workspace"
}Exactly one of image and containerfile is required.
Cross-compilers can be selected explicitly. Clue rejects cross targets that would otherwise fall back to the host compiler:
toolchain: {
compiler: "clang"
cc: "clang"
cxx: "clang++"
ar: "llvm-ar"
targetTriple: "aarch64-linux-gnu"
sysroot: "/opt/aarch64-sysroot"
}clue validate- Validate configuration and check dependenciesclue build- Build all targets (use-variant releasefor optimized builds)clue clean- Remove build artifacts (use-allto clean all variants)clue run <target>- Build and run an executable targetclue test [name|label...]- Build and run configured testsclue install [target...]- Build and install artifacts and public headersclue deps <list|fetch|build|clean|update>- Manage external dependenciesclue generate <ninja|compile-commands|all>- Generate build files for editors/tools
Long GCC, Clang, and MSVC compile/link invocations automatically use response files, including commands emitted by the Ninja generator.
Fetched Git commits and tarball checksums are recorded in clue.lock. Commit
that file so builds use the same dependency revisions; run clue deps update
to resolve configured Git refs again.
clue watch performs an initial build, then recursively watches the project
for source, header, module, assembly, and CUE changes. Changes are debounced for
300 ms; a new change cancels an in-progress build, and CUE changes reload the
configuration. .git, .deps, and .build directories are ignored.
Watch mode relies on native filesystem notifications. Use a local checkout: changes on NFS, SMB, or other network filesystems may not be reported, and very large directory trees may exceed the operating system's watcher limit.
To find expensive translation units, use clue build -profile -v. Add
-top N to choose how many slow files are displayed. Running with
-profile -save-profile also writes a Chrome Trace file to
.build/<variant>/profile.json, which can be opened in Perfetto or a compatible
trace viewer.
-variant debug|release- Select build variant (default: debug)-j N- Number of parallel jobs (0 = half CPU cores, -1 = all cores)-v- Verbose output showing detailed build steps-quiet- Suppress all non-error output-rebuild-all- Force rebuild of all files-keep-going- Continue building despite errors-target <platform>- Cross-compile for target platform (e.g., linux-arm64, darwin-amd64, windows-amd64)-prefix <path>- Set the installation prefix-destdir <path>- Stage an installation for packaging-profile- Record compilation timings (-vprints the slowest files)-save-profile- Write recorded timings as Chrome Trace JSON-top N- Number of slowest files printed with profiling (default: 10)
name: "calculator"
version: "1.0.0"
toolchain: {
compiler: "clang"
cxxStd: "c++17"
}
targets: {
mathlib: {
name: "mathlib"
type: "static_library"
sources: ["lib/math.cpp"]
headers: ["lib/arithmetic.h"]
public: {
includes: ["lib"]
}
}
app: {
name: "app"
type: "executable"
sources: ["src/main.cpp"]
depends: ["mathlib"]
}
}Clang, GCC, and MSVC builds support named modules, interface and internal
partitions, and modules imported across target boundaries. Cross-target imports
must name the provider in depends. Header units are explicit so Clue knows
which headers require a BMI:
toolchain: {compiler: "clang", cxxStd: "c++20"}
targets: {
math: {
name: "math"
type: "static_library"
sources: ["math.cppm", "math-detail.cpp"]
}
app: {
name: "app"
type: "executable"
sources: ["main.cpp"]
depends: ["math"]
headerUnits: [
{name: "vector", system: true},
{name: "project/config.hpp", path: "include/project/config.hpp"},
]
}
}Source code imports those headers with import <vector>; and
import "project/config.hpp";. GCC module builds use a generated module mapper;
MSVC builds use IFC references; Clang builds use PCM references. The selected
compiler and standard library still determine which system headers can be built
as header units.
variants: {
debug: {
optimization: "none"
debug_info: true
}
release: {
optimization: "aggressive"
debug_info: false
}
}Unity builds reduce compiler startup and repeated header-parsing work by combining sources in configurable batches:
targets.app: {
name: "app"
type: "executable"
sources: ["src/*.cpp"]
unity: {
batchSize: 8
exclude: ["src/legacy.cpp", "src/generated.cpp"]
}
}batchSize defaults to 8. C and C++ sources are kept in separate batches;
assembly and C++ module sources remain separate automatically. Use exclude
for files whose macros, anonymous namespaces, or other translation-unit-local
state conflict when combined. Exclusions accept the same file globs as
sources and must select files in that target.
Mark executable targets as tests and optionally configure their invocation:
targets: unit_tests: {
name: "unit_tests"
type: "executable"
sources: ["tests/unit.cpp"]
test: {
args: ["--reporter", "console"]
env: TEST_DATA: "tests/data"
labels: ["unit", "fast"]
}
}clue test runs every configured test. Positional selectors match either a
target name or label, and -j controls execution parallelism.
clue install copies executables to bin, libraries to lib, and declared
headers to include (preserving paths beneath public.includes). The default
prefix is /usr/local; use -prefix to change it and -destdir to stage a package:
clue install -variant release -prefix /usr -destdir ./pkgBuild with a variant:
clue build -variant releaseUse a custom target when a tool must produce sources or headers before compilation:
targets: {
generate: {
name: "generate"
type: "custom"
command: ["protoc", "--cpp_out=generated", "schema.proto"]
inputs: ["schema.proto"]
outputs: ["generated/schema.pb.cc", "generated/schema.pb.h"]
}
app: {
name: "app"
type: "executable"
sources: ["main.cpp", "generated/schema.pb.cc"]
includes: ["generated"]
depends: ["generate"]
}
}Header-only Git, tarball, and vendored dependencies need only their include directory:
dependencies: json: {
type: "vendored"
path: "vendor/json"
build: {
targetType: "header_only"
includes: ["include"]
}
}Project-local header-only libraries use an interface_library target. Its
public requirements are inherited transitively by consumers:
targets: headers: {
name: "headers"
type: "interface_library"
public: {
includes: ["include"]
systemIncludes: ["vendor/include"]
cxxStd: "c++20"
}
}For an already-built library, point at the exact artifact instead:
dependencies: sdk: {
type: "vendored"
path: "vendor/sdk"
build: {
targetType: "prebuilt_static" // or "prebuilt_shared"
library: "lib/libsdk.a"
includes: ["include"]
}
}System packages can export their compiler and linker flags through pkg-config:
dependencies: ssl: {
type: "pkg_config"
package: "openssl" // defaults to the dependency name
static: false // use pkg-config --static when true
}Dependencies driven by CMake, Meson, or another build tool can run an argument-vector command sequence and expose its output:
dependencies: foo: {
type: "vendored"
path: "vendor/foo"
build: {
targetType: "external_static" // or "external_shared"
commands: [
["cmake", "-S", ".", "-B", "build"],
["cmake", "--build", "build", "--target", "foo"],
]
library: "build/libfoo.a"
includes: ["include"]
}
}