name: codex-cup-release description: Use when Codex needs to build, test, or modify an external C/C++ project that uses only a Cup release artifact: build/embedded/cup.exe or build/header_only/cup.h. Covers first-time setup, embedded vs header-only detection, cup -hh, cached options, target paths, tests, ENTRY DAG editing, and correct macro recipes including LIB+AR for static libraries and EXE/DLL+LINK for linked binaries.
Cup Release Usage
Use this skill in a consumer C/C++ project that uses Cup as its build system. Do not assume Cup source files exist. The user may only have one downloaded release artifact:
build/embedded/cup.exeon Windows, or an equivalent executablecupon Linux/macOS.build/header_only/cup.h.
Codex Location
Install this folder as codex-cup-release/SKILL.md under Codex's configured skills directory, such as $CODEX_HOME/skills or ~/.codex/skills.
First Steps
From the consumer project's root directory, detect how Cup is available:
- If
cup.hexists in the current directory, use header-only mode. - Otherwise, if
cup.exeexists on Windows, use embedded mode. - Otherwise, on Linux/macOS, if an executable named
cupor starting withcupcan be run as./cup..., use embedded mode. - If neither is present, ask for the release artifact. If the user has
build/embedded/cup.exe, run it from the project root or place it in the project root ascup.exe. If the user hasbuild/header_only/cup.h, place it in the project root ascup.hand compile it.
If both cup.h and a Cup executable are present, treat the project as header-only mode.
Cup build scripts are C files named build.c. In both embedded and header-only release usage, build scripts include:
#include "cup.h"
Do not include Cup source-tree paths such as cup/cup.h unless the user's project explicitly provides that layout.
Run Cup
Embedded mode on Windows:
.\cup.exe -hh
.\cup.exe
Embedded mode on Linux/macOS:
./cup -hh
./cup
Header-only mode needs a one-time compile of cup.h with MAIN_ENTRY.
Windows with MSVC:
cl /Fe:cup.exe /Tc cup.h /std:clatest /DMAIN_ENTRY /nologo
.\cup.exe -hh
Linux:
clang -x c cup.h -DMAIN_ENTRY -o cup -D_GNU_SOURCE -fms-extensions
./cup -hh
macOS:
clang -x c cup.h -DMAIN_ENTRY -o cup -fms-extensions
./cup -hh
Always run cup -hh for the actual artifact in the current project before relying on options. Key options:
-out_dir <dir>: output directory, defaultbuild.-t <toolchain>:llvm,msvc,gcc, orzig.-linker <linker>: LLVM linker, usuallydefaultorlld.-O0,-O3,-Os: debug, fast release, size release.-clean: remove generated artifacts known to Cup.-dry: print/prepare work without executing commands.-test [targets...]: build and run test executables.-root <dir>: resolve relative paths from another root.
Cup records the last selected toolchain, optimization level, and LLVM linker in {out_dir}/.last_status. If later commands omit -t, -O..., or -linker, Cup reuses those saved values. Pass these flags explicitly when changing configuration or when reproducibility matters. Changing -out_dir changes where this status is read and written.
The CUP_CLANG environment variable overrides the LLVM toolchain's C/C++ compiler path:
export CUP_CLANG=/usr/lib/llvm-21/bin/clang-21
./cup -t llvm # uses clang-21 / clang++-21
Minimal build.c
Use this shape when creating or repairing a simple project:
#include "cup.h"
ENTRY(build_app)
{
Node* src = SRC("src/main.c");
Node* obj = OBJ(src);
CC(src, obj);
Node* exe = EXE("{out_dir}/app");
Node* link = LINK(exe);
link_cmd_add_input(link, obj);
}
int main(int argc, char** argv)
{
set_test_enabled(true);
return execute();
}
Run it:
.\cup.exe
.\cup.exe build/app.exe
On Linux/macOS the executable target is usually build/app, because EXE(...) appends an empty executable extension there.
Macro Rules
These macros are the high-signal surface area for everyday Cup build scripts:
SRC(fmt, ...): declare a C/C++ source node.OBJ(src): get the default object node for a source.CC(input, output): create a compile command, usuallyCC(src, OBJ(src)).EXE(fmt, ...): declare an executable output and append{exe_ext}.DLL(fmt, ...): declare a shared library output and append{dll_ext}.LIB(fmt, ...): declare a static library output and append{lib_ext}.FILE(fmt, ...): declare a normal file node, useful for headers, generated files, assets, stamps, and copy inputs/outputs.LINK(output): create a linker command for anEXE(...)orDLL(...)output.AR(output): create an archive/static-library command for aLIB(...)output.CMD(command_line): create a custom external process command.CMD_FROM_EXE(exe, name): create a command that runs an executable node built by Cup.CALLBACK_CMD(fn, ctx): create an in-process C callback command.COPY(src, dst): create a platform-specific copy command.
Mental model:
SRC,OBJ,EXE,DLL,LIB, andFILEcreate or fetch nodes. They do not build anything by themselves.CC,LINK,AR,CMD,CMD_FROM_EXE,CALLBACK_CMD, andCOPYcreate commands that build or update nodes.EXE("{out_dir}/app")only names the executable node;LINK(EXE("{out_dir}/app"))creates the link command.LIB("{out_dir}/core")only names the static library node;AR(LIB("{out_dir}/core"))creates the archive command.
Important rule: do not use LINK() to generate a static library. Static libraries are LIB(...) plus AR(...). LINK() is for linked binaries: executables and DLL/shared libraries.
Correct:
Node* lib = LIB("{out_dir}/core");
Node* ar = AR(lib);
ar_cmd_add_input(ar, obj);
Never write LINK(lib) or LINK(LIB(...)) for a static library target.
Common Recipes
Compile one source to one object:
Node* src = SRC("src/foo.c");
Node* obj = OBJ(src);
CC(src, obj);
Build an executable:
Node* src = SRC("src/main.c");
Node* obj = OBJ(src);
CC(src, obj);
Node* link = LINK(EXE("{out_dir}/app"));
link_cmd_add_input(link, obj);
Build a static library:
Node* lib = LIB("{out_dir}/core");
Node* ar = AR(lib);
Node* src_a = SRC("src/a.c");
Node* obj_a = OBJ(src_a);
CC(src_a, obj_a);
ar_cmd_add_input(ar, obj_a);
Node* src_b = SRC("src/b.c");
Node* obj_b = OBJ(src_b);
CC(src_b, obj_b);
ar_cmd_add_input(ar, obj_b);
Build an executable that links objects and a static library:
Node* lib = LIB("{out_dir}/core");
Node* ar = AR(lib);
Node* core_src = SRC("src/core.c");
Node* core_obj = OBJ(core_src);
CC(core_src, core_obj);
ar_cmd_add_input(ar, core_obj);
Node* app_src = SRC("src/main.c");
Node* app_obj = OBJ(app_src);
CC(app_src, app_obj);
Node* link = LINK(EXE("{out_dir}/app"));
link_cmd_add_input(link, app_obj);
link_cmd_add_input(link, lib);
Build a DLL/shared library:
Node* src = SRC("src/plugin.c");
Node* obj = OBJ(src);
CC(src, obj);
Node* link = LINK(DLL("{out_dir}/plugin"));
link_cmd_add_input(link, obj);
Copy a file:
Node* input = FILE("assets/config.json");
Node* output = FILE("{out_dir}/config.json");
COPY(input, output);
Run a generated tool:
Node* gen = EXE("{out_dir}/gen_assets");
Node* run = CMD_FROM_EXE(gen, "generate assets");
cmd_add_output(run, FILE("{out_dir}/assets.c"));
Build Targets
Cup accepts zero, one, or many target paths after options:
.\cup.exe
.\cup.exe build/core.lib
.\cup.exe build/core.lib build/app.exe
.\cup.exe -t llvm -O3 build/core.lib
Targets are node paths relative to the current root directory. Use Linux-style / separators even on Windows: build/core.lib, not build\core.lib.
The target path must match a node created by build.c. Examples:
EXE("{out_dir}/app")becomesbuild/app.exeon Windows andbuild/appon Linux/macOS.DLL("{out_dir}/plugin")becomesbuild/plugin.dllon Windows andbuild/plugin.soon Linux.LIB("{out_dir}/core")becomesbuild/core.libon Windows andbuild/core.aon Linux/macOS.FILE("{out_dir}/generated/config.h")becomesbuild/generated/config.h.
Use -dry when unsure:
.\cup.exe -dry build/core.lib
Tests
Test sources include cup/test.h and declare test cases with TEST(fn_name, group_name) or TEST(fn_name).
#include "cup/test.h"
TEST(test_parser_accepts_empty_file, parser)
{
ASSERT(true);
}
Compile test sources in ENTRY blocks:
#include "cup.h"
ENTRY(build_test_parser)
{
Node* src = SRC("{dir}/test_parser.c");
Node* obj = OBJ(src);
CC(src, obj);
obj_add_link_obj_from_src(obj, SRC("src/parser.c"));
}
int main(int argc, char** argv)
{
set_test_enabled(true);
add_build_script("tests/build.c");
return execute();
}
Run all tests:
.\cup.exe -test
Run one or more test executables by target path:
.\cup.exe -test build/tests/tests/xxx.c.exe
.\cup.exe -test build/tests/tests/aaa.c.exe build/tests/tests/bbb.c.exe
On Linux/macOS, test executable targets use .test:
./cup -test build/tests/tests/xxx.c.test
How Cup derives the test target path:
-testenables test scanning.- Cup scans compiled sources for
TEST(...). - For each compiled source with tests, Cup calls
add_test_exe_for_obj(obj, entries). - The source path comes from the object's compile command.
- Windows test exe path:
{out_dir}/tests/<source-path>.exe. - Linux/macOS test exe path:
{out_dir}/tests/<source-path>.test.
Example: if tests/build.c compiles SRC("{dir}/xxx.c"), {dir} resolves to tests, so the source path is tests/xxx.c. With default {out_dir} of build, the Windows test target is build/tests/tests/xxx.c.exe. The repeated tests/tests is intentional: the first tests is Cup's test-output directory, and the second is the source path's tests/ directory.
ENTRY And DAG Edits
All build graph changes happen inside ENTRY functions. Add source, object, command, output, and dependency nodes there.
Use {dir} for paths relative to the build.c file that contains the current ENTRY. Use {out_dir} for generated outputs.
Register extra build scripts in main:
int main(int argc, char** argv)
{
set_test_enabled(true);
add_build_script("src/build.c");
add_build_script("tests/build.c");
return execute();
}
Use priorities when an entry must run before or after normal node creation:
ENTRY(set_project_vars, PRIORITY_BEFORE_DEFAULT)
{
set_var("generated_dir", "{out_dir}/generated");
}
ENTRY(adjust_nodes, PRIORITY_AFTER_DEFAULT)
{
/* Inspect or adjust nodes after normal entries created them. */
}
If all nodes need compile/link flag adjustments, prefer an after_prepare callback. Do not create new targets there; dependencies have already been determined.
static void setup_flags(void)
{
Node** nodes = get_all_nodes();
for (size_t i = 0; i != array_size(nodes); i++)
{
if (nodes[i]->type == node_make_cmd_type(CMD_TYPE_EXECUTABLE, C_CMD_COMPILE))
{
c_compile_cmd_add_include_directory(nodes[i], "src");
}
}
}
int main(int argc, char** argv)
{
set_after_prepare_callback(setup_flags);
return execute();
}