Writing dual-mode Tupfiles for standalone and component builds. Use when writing reusable libraries, using the self-contained library convention, setting up multi-library projects, or running putup parse --strict.
Write Tupfiles that build both standalone (cd libfoo && putup -B build) and as part of a larger project (cd big-project && putup -B build) with zero changes.
Full reference: https://github.com/typeless/putup/blob/main/docs/reference.md
Every Tuprules.tup sets S = $(TUP_CWD) to anchor source paths. When a component uses =, it overwrites the parent's value during include_rules (root-to-leaf merge), breaking all $(S)/... references in composed mode.
# Root Tuprules.tup
S = $(TUP_CWD) # S = ../.. (from mpfr/src/)
# mpfr/Tuprules.tup
S = $(TUP_CWD) # OVERWRITES S = .. -- root's value lost!
(it IS the authority). (set only if undefined):
=?=# Root Tuprules.tup — authoritative assignments
S = $(TUP_CWD)
B = $(TUP_VARIANT_OUTPUTDIR)/$(S)
CC = @(CC)
AR = @(AR)
GMP_DIR = gmp
MPFR_DIR = mpfr
MPC_DIR = mpc
# mpfr/Tuprules.tup — soft defaults for standalone builds
S ?= $(TUP_CWD)
B ?= $(TUP_VARIANT_OUTPUTDIR)/$(S)
CC ?= gcc
AR ?= ar
GMP_DIR ?= ../gmp
MPFR_DIR ?= .
CFLAGS = -O2 -DHAVE_CONFIG_H
CFLAGS += -I$(S)/$(MPFR_DIR)/src
CFLAGS += -I$(S)/$(GMP_DIR)
!cc = | $(S)/$(GMP_DIR)/<gen-headers> |> ^ CC %b^ $(CC) $(CFLAGS) -c %f -o %o |> %B.o
include_rules includes every Tuprules.tup from project root down to the current directory. For mpfr/src/Tupfile:
Tuprules.tup runs first: sets S, B, CC, GMP_DIR = gmp, MPFR_DIR = mpfrmpfr/Tuprules.tup runs second: ?= defaults are no-ops (already set), but CFLAGS and !cc are defined freshThis layering means root controls the layout while each component controls its own build flags.
A library that depends on another needs to reference both directories in the same file:
# mpc/Tuprules.tup
CFLAGS += -I$(S)/$(GMP_DIR)
CFLAGS += -I$(S)/$(MPFR_DIR)/src
CFLAGS += -I$(S)/$(MPC_DIR)/src
Three different paths need three different names. The root sets all of them in one shared scope. A single DIR would collide.
Default to . for standalone use:
# In component Tuprules.tup
GMP_DIR ?= ../gmp
MPFR_DIR ?= .
CFLAGS and !cc do NOT need prefixes. Each component's Tuprules.tup is only included by Tupfiles in its own subtree via include_rules. There is no shared scope where they could collide.
# gmp/Tuprules.tup
CFLAGS = -O2 -I$(S)/$(GMP_DIR)
!cc = |> ^ CC %b^ $(CC) $(CFLAGS) -c %f -o %o |> %B.o
# mpfr/Tuprules.tup (different CFLAGS, no collision)
CFLAGS = -O2 -I$(S)/$(MPFR_DIR)/src -I$(S)/$(GMP_DIR)
!cc = | $(S)/$(GMP_DIR)/<gen-headers> |> ^ CC %b^ $(CC) $(CFLAGS) -c %f -o %o |> %B.o
putup parse --strictRun the convention checker to catch violations before they cause composed-mode failures:
putup parse --strict
| Code | Rule | Example violation |
|---|---|---|
| E1 | S must use ?= in component Tuprules.tup | S = $(TUP_CWD) in mpfr/Tuprules.tup |
| E2 | B must use ?= in component Tuprules.tup | B = $(TUP_VARIANT_OUTPUTDIR)/$(S) in mpfr/Tuprules.tup |
| Code | Rule | Example violation |
|---|---|---|
| W1 | Toolchain vars (CC, CXX, AR, etc.) should use ?= in components | CC = gcc in mpfr/Tuprules.tup |
| W2 | Component directories should contain Tupfile.ini for standalone builds | mpfr/ missing Tupfile.ini |
The root directory is exempt from all checks -- it IS the authority.
project/
Tupfile.ini
Tuprules.tup # Root: S=, B=, GMP_DIR=gmp, MPFR_DIR=mpfr
tup.config
gmp/
Tupfile.ini # Standalone marker
Tuprules.tup # S?=, B?=, GMP_DIR?=., CFLAGS, !cc
Tupfile
defaults.config
mpfr/
Tupfile.ini
Tuprules.tup # S?=, B?=, GMP_DIR?=../gmp, MPFR_DIR?=.
src/
Tupfile
defaults.config
From mpfr/src/Tupfile with include_rules:
Composed mode (root is ../..):
Root Tuprules.tup: S = ../.. GMP_DIR = gmp MPFR_DIR = mpfr
mpfr/Tuprules.tup: S ?= (no-op) GMP_DIR ?= (no-op)
$(S)/$(GMP_DIR) = ../../gmp (correct)
$(S)/$(MPFR_DIR)/src = ../../mpfr/src (correct)
Standalone mode (mpfr/ is root, root is ..):
mpfr/Tuprules.tup: S = .. GMP_DIR = ../gmp MPFR_DIR = .
$(S)/$(GMP_DIR) = ../../gmp (correct)
$(S)/$(MPFR_DIR)/src = ../src (correct)
Components ship defaults in a defaults.config file. During configure, the parent config overrides child values on collision:
project/tup.config # CC=gcc, AR=ar
gmp/defaults.config # HAVE_ALLOCA=1
mpfr/defaults.config # HAVE_LOCALE=1
= for S, B, toolchain, and *_DIR variables?= for S, B, toolchain, and *_DIR variables*_DIR defaults resolve correctly for standalone builds (e.g., GMP_DIR ?= ../gmp)Tupfile.ini for standalone modeCFLAGS and !cc are unprefixed (subtree-scoped, no collision risk)putup parse --strict reports no errorscd gmp && putup -B build && ls build/cd project && putup -B build && ls build/gmp/