Skip to content

Latest commit

 

History

676 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Clue - A C++ Build System

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.

Installation

Install from source:

go install github.com/loov/clue@latest

Tagged 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 .

Quick Start

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 project

clue.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"
}

Commands

  • clue validate - Validate configuration and check dependencies
  • clue build - Build all targets (use -variant release for optimized builds)
  • clue clean - Remove build artifacts (use -all to clean all variants)
  • clue run <target> - Build and run an executable target
  • clue test [name|label...] - Build and run configured tests
  • clue install [target...] - Build and install artifacts and public headers
  • clue deps <list|fetch|build|clean|update> - Manage external dependencies
  • clue 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.

Watch mode and build profiles

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.

Common Flags

  • -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 (-v prints the slowest files)
  • -save-profile - Write recorded timings as Chrome Trace JSON
  • -top N - Number of slowest files printed with profiling (default: 10)

Example Configurations

Multi-target project with library

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"]
    }
}

C++ modules and header units

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.

Build variants

variants: {
    debug: {
        optimization: "none"
        debug_info:   true
    }
    release: {
        optimization: "aggressive"
        debug_info:   false
    }
}

Unity builds

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.

Tests

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.

Installation

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 ./pkg

Build with a variant:

clue build -variant release

Generated sources

Use 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 dependency

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"]
    }
}

About

Experimental build system.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages