Build

This document defines the target-based build interface and the configuration- specific output layout used by cup.

Public build interface

The platform remains the only public selector:

PLATFORM  linux-x64, linux-arm64, macos-x64, macos-arm64, windows-x64

Build configurations are selected by targets, not combinable variables:

make PLATFORM=linux-x64
make PLATFORM=linux-x64 debug
make PLATFORM=linux-x64 coverage
make PLATFORM=linux-x64 sanitizers
make PLATFORM=linux-x64 release

A normal local make release produces release-mode code with a development version identity. Official identity remains restricted to the controlled candidate workflow.

Quality-tool prerequisites

The native quality runners validate optional host tools before starting and print platform-specific installation guidance instead of failing with an opaque command not found. The canonical entry points are:

make PLATFORM=<platform> test-coverage
make PLATFORM=<platform> test-sanitizers

Linux and Windows coverage use GCC/gcov. macOS coverage uses Clang source-based instrumentation and therefore requires gcovr 8.5 or newer together with matching llvm-profdata and llvm-cov tools. ASan/UBSan are exercised natively on every supported platform; leak detection is enabled on Linux and macOS and disabled on Windows. These tools are host-side diagnostics and are not added to the pinned application/test-library prefix or release notices.

Output layout

build/<platform>/development/
build/<platform>/debug/
build/<platform>/coverage/
build/<platform>/sanitizers/
build/<platform>/release/

Each directory contains its own objects, generated version data, build identity and executable. Changing configuration therefore cannot reuse objects from another configuration.

Every configuration contains build-config.txt. It records the platform, configuration, host architecture, compiler command/path/target/version, resource compiler identity, effective preprocessor/compiler/linker flags, dependency compatibility metadata and official-build status. The file is replaced atomically only when this content changes. Objects depend on it, so changing the compiler, EXTRA_* flags, dependency metadata or official status invalidates prior objects without forcing an unrelated dependency rebuild.

Compiler configuration

Compiler choice is role-based rather than globally uniform. Each published platform has one canonical compiler, while a different compiler is used only when it provides independent diagnostic value:

RoleLinuxmacOSWindows
Primary development, integration and release compilerGCCApple ClangMSYS2 UCRT64 GCC
Secondary compilerClang app/unit x64Native compilerCLANG64 sanitizer
CoverageGCC/gcovClang source-based coverageUCRT64 GCC/gcov
ASan/UBSanClang/Compiler-RTApple ClangMSYS2 CLANG64 Clang/Compiler-RT
Canonical dependency compilerGCCApple ClangUCRT64 GCC; CLANG64 for sanitizers

This is intentional diversity, not interchangeable compiler selection. Release artifacts and native integration behavior always use one primary compiler. The secondary Linux Clang pass compiles the complete application and runs all C unit tests against the same canonical dependency prefix; it does not create a second release graph. Sanitizers use Clang consistently so diagnostics and runtime behavior do not vary between GCC's libsanitizer and LLVM Compiler-RT.

All C builds use C11 with warnings treated as errors. Development and debug use -O0 -g3; release uses -O2 -g1 -DNDEBUG so separate native symbol artifacts remain useful after the public executable is stripped. Coverage and sanitizer instrumentation are isolated in their own directories. The debug target adds the richest portable symbol information selected for the platform.

Mandatory flags are defined by the Makefile and cannot be replaced with direct CPPFLAGS=, CFLAGS=, LDFLAGS= or LDLIBS= command-line assignments. Local additions use:

make EXTRA_CPPFLAGS=-DLOCAL_FEATURE
make EXTRA_CFLAGS=-Wconversion
make EXTRA_LDFLAGS=-Wl,--build-id=none
make EXTRA_LDLIBS=-lm

Ambient direct flag variables are ignored. Official candidate builds reject all EXTRA_* additions so their identity cannot depend on an uncontrolled local environment.

Before compiling, scripts/build/validate-toolchain.sh verifies the native host, the compiler target triple and the Windows resource compiler where applicable. Linux and macOS builds are native. Every Windows x64 path—development, tests, debug dependencies and release candidates—runs natively in an MSYS2 UCRT64 shell and rejects the MINGW64/MSVCRT toolchain.

macOS x64 and ARM64 currently use deployment target 13.0 for cup and every pinned dependency. Windows currently defines _WIN32_WINNT and WINVER as 0x0A00. These are explicit CI build baselines, not yet final minimum-support promises; the final floors require evidence from native runners. The Makefile rejects conflicting ambient values and records the effective compiler/linker flags in build-config.txt.

Script execution boundaries

Shell portability is evaluated according to where a script executes:

  • release install.sh and the copied uninstall.sh execute on machines that CUP does not control. They use /bin/sh, avoid optional text processors and non-portable GNU/BSD command options, preflight their required core commands, and provide explicit downloader and SHA-256 alternatives;
  • build, dependency, test and release scripts execute in repository-controlled CI profiles. They may use a broader tool set, but the matching workflow must install it or scripts/ci/prepare-posix.sh must verify it before work starts. Source, coverage, sanitizer, debug and release profiles are checked against their actual compiler, timeout, inspection and symbol-tool requirements. The package versions selected by APT or Homebrew are printed in the job log. A command being present incidentally in a hosted-runner image is not treated as a stable dependency contract.

This distinction avoids weakening the build scripts merely to support arbitrary hosts while keeping the public bootstrap usable without a compiler or a package preparation step.

Dependencies

End-user runtime

Official releases are static application artifacts. End users do not need a C compiler, package sources, Unity, MSYS2, Homebrew or dependency preparation to run cup.

Direct application dependencies

  • Argtable3 implements command-specific argument parsing in main.c. The initial subcommand dispatch is only command selection, not a second parser.
  • uthash is used by archive extraction to reject duplicate normalized paths.
  • libcurl provides bounded HTTPS transfers, redirect handling and timeouts.
  • libarchive performs archive preflight and extraction.
  • SHA-256 is implemented in-tree by sha256.c; OpenSSL is not a direct checksum dependency. POSIX builds use OpenSSL as libcurl's TLS backend.

c-ares is a pinned transitive runtime dependency of libcurl. It provides the asynchronous hostname resolver on every supported platform.

Test dependencies

Unity is linked only into C unit-test binaries. Libevent is linked only into the network-helper fixture used by integration, portability and release tests; it does not enter the cup application link or any published cup executable. gcovr and sanitizer runtime support are host tools and are not part of the application prefix contract.

The canonical versions and source SHA-256 values are stored in config/dependencies.lock. Download locations and transport checks remain in scripts/dependencies/sources.sh. The adjacent scripts/dependencies/THIRD_PARTY_NOTICES.txt preserves the corresponding notices and license texts and is published as a release disclosure; it is not an alternate dependency resolver.

Dependency prefixes and compatibility

The canonical production/native prefix is:

~/deps/<platform>/install

Windows sanitizers use a separate CLANG64 prefix because CLANG64 archives must not be mixed with the UCRT64/GCC production graph:

~/deps/windows-x64-clang64/install

DEPS_PREFIX can override these paths. Explicit build and dependency paths must be absolute and whitespace-free so GNU Make, generated metadata and MSYS2 path conversion share one unambiguous representation.

Dependency preparation is driven by two small repository files:

config/dependencies.lock
config/dependencies.recipe

dependencies.lock contains the pinned package versions and source SHA-256 values. dependencies.recipe contains one positive integer. The recipe is incremented only when a change alters the produced prefix, such as a build flag, TLS backend, runtime profile, installed layout or library set. Comments and formatting do not change compatibility.

A committed prefix contains .cup-dependencies:

prefix_format=4
platform=<platform>
profile=<gcc|apple-clang|ucrt64-gcc|clang64>
recipe=<positive integer>
lock_sha256=<semantic lock digest>

The lock digest is calculated from the canonical package names, versions and source checksums. It does not hash the text of the build scripts. Reuse still requires every expected header, archive and generated metadata file to pass the normal prefix validation; the manifest alone is never sufficient.

The local dependency commands are:

JOBS=4 make PLATFORM=<platform> deps
make PLATFORM=<platform> deps-check
make PLATFORM=<platform> deps-force
make PLATFORM=<platform> deps-clean
  • deps reuses a compatible prefix or builds it transactionally;
  • deps-check validates without modifying anything;
  • deps-force rebuilds even when the current prefix is compatible;
  • deps-clean removes the selected prefix and its build state.

Public build and test targets depend on the idempotent deps target. A fresh checkout can therefore run make test directly. Existing compatible prefixes are checked and reused without network access or recompilation.

For an offline Linux x64 build, place the verified source archives in the canonical source directory before invoking the same target:

mkdir -p "$HOME/deps/linux-x64"
tar -xJf cup-linux-x64-dependency-sources.tar.xz \
    -C "$HOME/deps/linux-x64"
JOBS=4 make PLATFORM=linux-x64 deps

Every cached source archive is checked for minimum size and exact SHA-256. An invalid archive is removed and downloaded again; a valid one is reused.

Dependency preparation is implemented by:

scripts/dependencies/sources.sh
scripts/dependencies/common.sh
scripts/dependencies/build-posix.sh
scripts/dependencies/build-windows.sh
scripts/dependencies/verify.sh

The builders normalize the environment, construct a sibling staging tree, normalize generated text metadata, reject forbidden build or host paths, verify the complete result and replace the final prefix transactionally. OpenSSL uses the neutral /__cup_runtime__/openssl namespace (without the leading space in the actual configured path) and automatic configuration loading remains disabled. Windows uses Schannel and does not include OpenSSL.

GitHub Actions uses the same manifest through .github/workflows/dependencies.yml. Its cache keys are readable combinations of platform, profile, recipe and lock digest. Tests and releases call the workflow automatically, and manual dispatch is available for all profiles or one selected profile. Preparation is serialized per platform/profile so overlapping runs do not rebuild the same missing cache. A cache miss runs the same make deps path used by local verification.

One pinned application dependency graph

Every build configuration consumes the same headers and static third-party libraries from DEPS_PREFIX:

  • the prefix include directory is used for Argtable3, uthash, c-ares, libcurl, libarchive, zlib, xz and the platform TLS backend;
  • Argtable3 is linked by exact archive path;
  • curl-config --static-libs supplies the pinned curl graph;
  • prefix-scoped pkg-config --static --libs libarchive supplies the pinned archive graph;
  • empty metadata or an incomplete prefix is a hard error before compilation.

Development, debug, coverage and sanitizer builds do not add a global -static option. Their third-party libraries are still the pinned static archives, while normal operating-system libraries retain the platform's native linkage. This keeps diagnostics and instrumented builds comparable to release without forcing a fully static process during local development.

The release configuration uses the same third-party graph and adds only the standalone policy for its platform. Linux currently adds global static linking; macOS keeps Apple system libraries and frameworks dynamic; Windows uses static third-party/runtime libraries with the approved system import libraries.

Unity is linked into C test executables by exact archive path. The network fixture obtains libevent_extra and libevent_core from prefix-scoped static pkg-config metadata. Unit suites that exercise archive code obtain libarchive and its transitive flags from the same prefix-scoped metadata instead of using a host -larchive fallback. Production build configuration is checked not to contain libevent link inputs.

Only release-configuration executables are eligible for publication. Debug and instrumented artifacts remain diagnostic even though their third-party graph is identical. Candidate packaging separates native symbols (cup.debug or cup.dSYM), strips the public executable and rejects checkout, dependency-root and transactional staging paths in both dependency archives and the final binary. OpenSSL is built with the deterministic, non-existent /__cup_runtime__/openssl default namespace while automatic configuration and DSO loading remain disabled.

Generated version files

scripts/version.sh reads VERSION and Git state, then generates under the selected build directory:

version.h
release.txt
version.rc       Windows

The Makefile declares all three outputs. If one is missing while the stamp exists, the stamp is invalidated and generation is repeated. Source files include the generated header rather than rewriting tracked files.

Embedded CA bundle

certs/cacert.pem is converted deterministically by scripts/certs/generate-ca-bundle.sh into generated ca_bundle.h and ca_bundle.c. certs/cacert.meta records the authenticated source, Mozilla source date, SHA-256, certificate count and maximum accepted age. make check-ca-bundle verifies that contract without network access. make update-ca-bundle validates and compiles a newly downloaded candidate, rejects rollback/future/suspiciously small bundles and replaces PEM plus metadata with rollback protection. See SECURITY.

Make targets

make help is the authoritative index of public targets.

Build and dependency targets:

make
make debug
make coverage
make sanitizers
make release
make clean
JOBS=4 make PLATFORM=<platform> deps
make PLATFORM=<platform> deps-check
make PLATFORM=<platform> deps-force
make PLATFORM=<platform> deps-clean
make check-toolchain
make check-binary

Behavioral tests and repository quality are intentionally separate:

make test
make test-unit
make test-integration
make quality
make check
make test-coverage
make test-sanitizers
make test-portability-linux
make test-windows
make test-release RELEASE_DIR=<candidate-directory>

make test runs unit and native integration behavior. make quality checks the repository, scripts, workflows and release contracts. make check runs dependency preparation, both groups and their required build steps.

Focused test build targets are:

make test-unit-build
make test-helpers
make test-build

Version, release, documentation and CA maintenance:

make version
make validate-release
make release-metadata
make finalize-release
make docs-assets
make docs
make serve
make check-ca-bundle
make update-ca-bundle

Destructive local cleanup remains guarded:

CUP_ALLOW_DEV_CLEAN=1 make reset-dev-home

The target rejects a missing, relative or root HOME. It deletes only the candidate root whose strict root.txt identifies coffee-clang/cup; an unrelated .cup, an unmarked directory, or two marked candidates makes the target stop without deleting either root.

Documentation build

Documentation uses mdBook with book.toml and Markdown sources under docs/. Remote theme assets can be fetched through scripts/fetch-docs-assets.sh. The static documentation workflow remains independent of application testing and release publication.

Linux static runtime portability test

make PLATFORM=linux-x64 test-portability-linux builds one isolated static release with a temporary test CA and exercises the Linux runtime against local fixtures. It verifies rejection of an unknown CA, acceptance of the embedded test CA, direct HTTPS downloads and HTTP CONNECT proxy tunnelling.

The target does not contact the public Internet and is intentionally separate from make test because it validates Linux-specific static-runtime properties, creates certificates, starts local servers and builds an additional release executable. CI runs it on Linux x64 as the dedicated static-runtime portability gate.

Native binary inspection

make PLATFORM=<platform> check-binary verifies the executable produced by the current configuration and writes build/<platform>/<configuration>/binary-inspection.txt. The report records the object format, exact architecture, SHA-256 and the native dynamic-link policy.

On Linux, development and diagnostic configurations may depend only on the explicit system/compiler-runtime allowlist. A Linux release must have no ELF interpreter, DT_NEEDED, RPATH or RUNPATH entries. macOS does not provide the same fully static system-linking model: cup links all pinned third-party dependencies statically, while the Mach-O executable may dynamically reference only /usr/lib and public /System/Library/Frameworks. Homebrew, @rpath, @loader_path, @executable_path and LC_RPATH are rejected. The report requires and records the repository deployment target 13.0 encoded in the Mach-O load commands. On Windows, the executable must be PE32+ x86-64, use the console subsystem, import only allowlisted Windows system DLLs, contain resources and advertise DYNAMIC_BASE and NX_COMPAT; MinGW and third-party runtime DLLs are rejected.

Source CI, debug-artifact construction and release-candidate construction run the inspector on all five supported platform identifiers. Platform-native tools are used where available: readelf for ELF, lipo/otool for Mach-O and a PE-capable objdump for Windows.

  • ARCHITECTURE — runtime and script boundaries;
  • TESTING — test environments and gates;
  • RELEASES — official identity and candidate publication;
  • PLATFORMS — platform-specific behavior.

Need help?
Please wait: the project is still pre-alpha!

Cup of Coffee is an open source project inspired by Cargo, Rustup Crates, and Conda-forge.