From putup
Writing Tupfiles with putup. Use when authoring Tupfiles, debugging build failures, working with rule syntax, variables, bang macros, groups, conditionals, or generators.
How this skill is triggered — by the user, by Claude, or both
Slash command
/putup:tupfile-authoringThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Guide for writing Tupfiles with putup. For the full manual, see
Guide for writing Tupfiles with putup. For the full manual, see https://github.com/typeless/putup/blob/main/docs/reference.md.
: [foreach] [inputs] [| order-only] |> command |> [outputs] [| extra-outputs] [{group}] [<order-group>]
: main.c |> gcc -c %f -o %o |> main.o # simple
: foreach *.c |> gcc -c %f -o %o |> %B.o # one command per input
: foo.o bar.o |> gcc %f -o %o |> program # multiple inputs
: main.c | config.h |> gcc -c %f -o %o |> main.o # order-only after |
| Flag | Expands to | Example result |
|---|---|---|
%f | All inputs | foo.c bar.c |
%o | All outputs | foo.o |
%b | Input basename (with ext) | foo.c |
%B | Input basename (no ext) | foo |
%d | Input directory | src |
%e | Extension | c |
%g | Glob match portion | foo from *_test.c + foo_test.c |
%% | Literal % | % |
Numbered variants select a specific input or output:
: header.h template.c |> gen %1f %2f -o %o |> output.c
putup expands $(VAR) at parse time. Bare $VAR passes through to the shell.
# $(CC) is expanded by putup; $PWD is expanded by /bin/sh
: |> $(CC) -DPREFIX=$PWD -c %f -o %o |> output.o
Use this to capture CWD before cd:
: |> SRCDIR=$PWD && cd $(TUP_VARIANT_OUTPUTDIR) && ./tool $SRCDIR/input |> output
| Operator | Name | Behavior |
|---|---|---|
= | Set | Replace value |
+= | Append | Append (space-separated) |
:= | Define | Assign literal string, no expansion |
?= | Soft set | Set only if undefined (first wins) |
??= | Weak set | Deferred default (last wins) |
CC ?= gcc # first ?= wins
CC ?= clang # ignored -- CC already defined
CFLAGS ??= -O0 # fallback if nothing else sets CFLAGS
CFLAGS ??= -O2 # this wins (last ??= wins)
CC = clang # unconditional set always wins
?= and ??= check whether a variable is defined, not whether it is empty.
Config values live in tup.config with a CONFIG_ prefix:
CONFIG_CC=clang
CONFIG_DEBUG=y
Access them in Tupfiles with @(NAME) (the CONFIG_ prefix is stripped):
CC = @(CC)
DEBUG = @(DEBUG:-n) # default value if unset
Override from the CLI:
putup -D CC=clang -D DEBUG=y
Define reusable command templates in Tuprules.tup:
!cc = |> ^ CC %b^ $(CC) $(CFLAGS) -c %f -o %o |> %B.o
!link = |> ^ LINK %o^ $(CC) $(LDFLAGS) %f -o %o |>
Invoke in a Tupfile:
include_rules
: foreach *.c |> !cc |> {objs}
: {objs} |> !link |> program
Embed order-only deps in the macro so every consumer waits automatically:
!cc = | <gen-headers> |> ^ CC %b^ $(CC) $(CFLAGS) -c %f -o %o |> %B.o
Output group -- collect outputs for later use as inputs:
: foreach *.c |> !cc |> %B.o {objs}
: {objs} |> !link |> program
Order-only group -- establish ordering without data dependency:
: |> gen-header > %o |> config.h <gen-headers>
: src.c | <gen-headers> |> $(CC) -c %f -o %o |> src.o
Cross-directory groups -- prefix with the path to the producing directory:
# In lib/Tupfile
: |> generate > %o |> header.h <gen-headers>
# In app/Tupfile ($(S) is the source root convention)
: src.c | $(S)/lib/<gen-headers> |> $(CC) -c %f -o %o |> src.o
ifdef DEBUG
CFLAGS += -g -O0
else
CFLAGS += -O2
endif
ifeq ($(TUP_PLATFORM),linux)
LDFLAGS += -lpthread
endif
ifeq (@(ENABLE_TESTS),y)
: foreach test_*.c |> !cc |> {test_objs}
endif
Conditional source pattern -- append sources guarded by config:
srcs += always.c
srcs-@(FEATURE_FOO) += foo.c
srcs-@(FEATURE_BAR) += bar.c
: foreach $(srcs) $(srcs-y) |> !cc |> {objs}
Tup deduplicates inputs, so listing a file under multiple config guards is safe.
Custom display text replaces the raw command in build output. Place
^ TEXT ^ right after the opening |>, on the same line:
# CORRECT
: input |> ^ CC %b^ $(CC) -c %f -o %o |> output.o
# CORRECT (continuation after display text is fine)
: input |> ^ CC %b^ \
$(CC) -c %f -o %o |> output.o
# WRONG -- ^ becomes a shell command
: input |> \
^ CC %b^ $(CC) -c %f -o %o |> output.o
Use \ at end of line to split long rules. Place |> at the start of
continuation lines for readability:
: main.c utils.c \
|> $(CC) $(CFLAGS) -c %f -o %o \
|> main.o
CFLAGS += -Wall \
-Wextra \
-O2
The |> delimiters become visual column anchors separating inputs, command,
and outputs. This is the recommended style for multi-line rules.
import and exportimport reads an environment variable into Tup's namespace at parse time.
export makes a Tup variable available to shell subprocesses.
import CC=gcc # read $CC from env, default to gcc
import CROSS_COMPILE= # read $CROSS_COMPILE, default empty
export PKG_CONFIG_PATH # pass to shell subprocesses
Without export, shell $VAR is empty even if $(VAR) expands correctly.
For projects with many similar targets, factor the build rules into a shared
.tup file. Each Tupfile becomes a pure source list:
# lib.tup — shared template (included by each library Tupfile)
: foreach $(srcs-y) | <gen-headers> \
|> !cc \
|> %B.o {objs}
: {objs} \
|> $(AR) rcs %o %f \
|> built-in.o
# modules/kernel/Tupfile — just lists sources
include_rules
srcs-y += main.c
srcs-$(CONFIG_FS) += fs.c
srcs-$(CONFIG_NET) += network.c
include $(ROOT)/lib.tup
This separates "what to build" from "how to build." Adding a source file means adding one line to a Tupfile — no rule changes needed.
srcs-y += *.c works but has tradeoffs:
Use globs for small stable directories. Use explicit lists when files change frequently or when you need to exclude specific files.
Read the first error. putup prints errors in dependency order. Fix the first one -- later errors often cascade from it.
Parse without building to check syntax:
putup parse -v # verbose: shows each Tupfile as parsed
putup parse --strict # check dual-mode composability conventions
Inspect variables:
putup show var CC # see where CC was assigned and overridden
putup show var --json # machine-readable output
Common error patterns:
| Symptom | Likely cause |
|---|---|
command not found | $(CC) undefined or $CC passed to shell without export |
| Output written to wrong directory | Missing cd $(TUP_VARIANT_OUTPUTDIR) for generators |
No tup.config found | Run putup configure first |
| Circular dependency | <group> references forming a loop |
| File not found during build | Cross-dir group path wrong; check $(S)/dir/<group> |
| Variable has unexpected value | Run putup show var NAME to see assignment history |
Generate a dependency graph to visualize the build:
putup show graph --summary # text overview
putup show graph | dot -Tpng -o deps.png # graphviz visualization
For the full reference, see https://github.com/typeless/putup/blob/main/docs/reference.md.
npx claudepluginhub typeless/putup --plugin putupCreates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.