Releases: typeless/putup
Release list
v2.2.0
Highlights
This is the memory-architecture release. The container-as-allocator campaign (after Racordon, OASIcs.Programming.2025.25) removes libc malloc from production code entirely: every container now owns OS virtual memory directly — reserved once, committed lazily, never moving. Measured against the pre-campaign baseline on the gcc-example workload: cycles −9.2%, cache-misses −39%, and peak build memory down from ~1 GB to 86 MB after the arena fix the benchmarks surfaced. The full architecture is documented in DESIGN.md § Memory Management.
Changed: containers own OS memory; malloc is gone (#101, #102)
A two-file platform/vm primitive (reserve / commit / decommit / release) underpins a Region type: reserve address space up front, commit pages as needed, base address never moves — so both integer handles and raw pointers survive growth. All string bytes live in one Region; the string-pool index, path-children index (a new shared RobinHoodIndex), Arena32, IdArray32/IdBitSet, and a new pointer-stable StableVec (replacing PagedVec) are Region-backed.
Everything else allocates through operator new, now a process-wide bump region with obstack semantics: the top allocation grows in place and pops LIFO, every delete is a no-op, and there is no free list. On Windows this required a total CRT override (every new/delete variant, static CRT, verified no fallback). Freshly committed pages are guaranteed zero — decommit maps fresh anonymous memory, never recycling dirty pages. Allocation failure aborts; no null propagates.
Fixed: quadratic growth in the edge arena (#104)
Arena32::append_extend copied the whole slice to the arena tail on every one-element append — O(k²) bytes for a k-edge node, and ~845 MB of abandoned copies in a single mapping during a full build. An at-tail fast path cannot fix this: add_edge interleaves each node's from- and to-slices, so neither is ever at the tail. Blocks are now sized to the next power of two of the slice length, so a non-power-of-two length proves a spare slot exists and the append is in place; total space is at most 4k slots for k elements. The unused arena API was deleted so every slice is born inside append_extend — the invariant is structural, not conventional.
Measured: dry-run peak RSS 1,011 → 51 MB (−95%), cycles −44%, wall −60%; full-build peak 1,032 → 86 MB; parse RSS 46.5 → 18.3 MB.
Changed: Buf spills to private regions; HeapBuf deleted (#103)
A Buf that outgrows its 4 KiB inline buffer now spills into its own private Region instead of the shared bump region, so spilled bytes are released the moment the buffer dies — a local, unconditional guarantee, where bump reclaim depended on what allocated in between. HeapBuf is gone: its movability was load-bearing at exactly one call site (read_file's return, now an output parameter), and everywhere else it duplicated Buf's job. One builder type remains.
Fixed: build correctness
- Output-less commands are rescheduled when their identity changes (#97) — previously an edited command with no declared outputs never re-ran.
- The xwin (Windows cross) config now generates header dependencies (
/clang:-MD), and the scheduler discovers object-path-named depfiles — header edits propagate on Windows cross-builds. show script/show compdbno longer emit commands from untaken conditional branches (#100), andmake bootstrapregenerates the checked-in bootstrap scripts from the live Tupfiles.
CI
Every PR now gets a sticky metrics comment — instructions, page faults, cachegrind D1/LL miss rates, wall, peak RSS, and line coverage — with deltas against the last successful main run (#105, #106).
Full Changelog: v2.1.0...v2.2.0
v2.1.0
Highlights
This release lands a line of conditional-semantics work validated by differential testing against real tup v0.8 throughout: the error directive, tup-exact static conditionals, per-context include processing, and scoped inactive-branch assignments. Along the way the static classifier, ifdef identity tracking, and ghost-input recording for undefined config vars all got stricter.
Added: error directive (#90)
ifeq (@(TOOLCHAIN),)
error CONFIG_TOOLCHAIN must be set
endif
Aborts parsing with the variable-expanded message, reported as <file>:<line>: <message> with a non-zero exit. Previously putup silently ignored error lines — a project ported from tup could build with a broken configuration straight through its guard. The directive fires only in an active conditional context (nested conditionals included) and independently per variant; message expansion is byte-identical to real tup, and a message-less error fails with Empty error directive, matching tup. A variable named error (error = x, error$(SUF) = y) still parses as an assignment.
Fixed: static conditionals are textual; includes join contexts (#91)
A statically false branch — ifeq (a,b), where the condition reads no config or env value — used to register guarded commands that could never run, yet they claimed their outputs: an identical rule at top level failed with already owned by command where tup builds fine. And because include was deduplicated by path alone, a file first included from such a dead branch was lost for later active includes — its rules silently dropped, its error guards bypassed.
Static conditionals now process textually, exactly like tup: the losing branch does not exist, and no condition node is created. The invariant, documented in DESIGN.md: guards exist iff the condition can flip without a Tupfile edit. Config- and env-driven conditionals keep full phi-model treatment, and include now processes a file once per conditional context. Two divergences from tup are documented in COMPATIBILITY.md: same-context duplicate includes still deduplicate (tup double-parses and fails), and cross-context same-output conflicts are reported eagerly under every config.
Fixed: inactive-branch assignments are scoped, not leaked (#93)
Two paths let assignments escape inactive branches into the active world, where real tup leaves them unset: an active inner conditional nested inside an inactive branch, and files included from inactive branches (processed with no filter at all).
Assignments in inactive branches now run against a snapshot restored when the branch closes — the same mechanism bang-macro scoping already uses. Nothing escapes, and the phi model gets something it never had: inactive-branch rules register with the values their branch would actually assign, instead of whatever happened to be in scope outside. ifdef/ifndef now record their config reads, so ifdef-driven guard flips fold into command identity like ifeq-driven ones.
Full Changelog: v2.0.1...v2.1.0
v2.0.1
Highlights
Both fixes in this patch release were found by differential-building a real-world Kbuild-style tup project (a 53-Tupfile, ~1900-command embedded firmware tree) with putup and real tup side by side. After the fixes, every one of the project's 1830 build outputs is bit-identical with real tup's (#88).
Fixed: conditional bang-macro selection
Bang macro definitions inside ifeq/else branches all landed in one macro table where the last definition wins, so rules after endif could execute the inactive branch's command — silently producing wrong artifacts that no rebuild ever repaired:
ifeq ($(CONFIG_DEVICE_MH1903),y)
!to_bin = |> ...hex2bin pipeline... |>
else
!to_bin = |> ...srec_cat fallback... |> # this one always won
endif
: foreach $(mods-y) |> !to_bin |> %B.bin
Definitions made in an inactive branch are now scoped to that branch (still visible to its own guarded rules, per the phi-node model); the active world after endif gets the active branch's definition.
Fixed: variant tup.config changes now reach consuming rules
A rule that reads the variant's tup.config as a declared input — the classic autoconf.h generator —
: ../tup.config |> awk -f gen-autoconf.awk %f > %o |> autoconf.h <gen-headers>
was never re-triggered by config edits in variant builds (in-tree builds were unaffected). Inputs that exist only in the build tree and are produced by no rule were serialized as pathless placeholders, invisible to change detection. They now record path, size, mtime, and content hash, and participate in the change scan.
Index format v13
Indexes from v2.0.0 lack the ghost content records the second fix relies on — and an otherwise-up-to-date build would never rewrite them — so the format version is bumped. The first build after upgrading is a one-time clean full build, then incremental as usual.
Plugin skills
The plugins/putup skills gained patterns verified against the same real-world project: $(CONFIG_X) config access and kernel-style srcs-$(CONFIG_X) composition, per-config bang-macro selection, %<group> expansion in commands, adopting an existing tup project (tup variant → putup configure --config mapping), TUP_VARIANTDIR semantics, tup.config-as-input config headers, GNU-Make-function residue pitfalls, and a differential-testing recipe against real tup.
v2.0.0
Highlights
This release resolves all ten findings from a design audit of putup's value-semantic graph architecture (#68–#77).
⚠️ Breaking: commands run in a minimal declared environment (#68)
Build commands no longer inherit putup's environment. The child environment is PATH (Windows: plus the system set) plus the variables your Tupfile exports — an un-exported variable is absent from the command's environment. This closes a silent-missed-rebuild class (a change to CPATH, LANG, SOURCE_DATE_EPOCH, … could alter outputs invisibly) and matches tup's documented semantics.
Migration: if a command reads an environment variable, declare it: export PKG_CONFIG_PATH. That declaration is exactly the dependency information putup needs to rebuild correctly when the value changes — exported variables are folded into command identity, even when they never appear in the command text.
Index format v12 (#73)
NodeIds now carry a 2-bit kind tag (~1.07B ids per kind; invalid states unrepresentable). Older indexes are detected and rebuilt automatically — the first build after upgrading is a clean full build, then incremental as usual.
New
--no-stat-cache— hash every file's contents, skipping the size+mtime fast path; immune to timestamp manipulation (touch -r,cp -p, same-sizegit checkout). See reference §9.1 for the trade-off it addresses (#69).CONFIG_TRACKED_TOOLS— opt-in, per-variant: fold tool binaries'(path, size, mtime)into command identity so an in-place toolchain upgrade triggers a rebuild (#72).
Correctness & internals
- Write-time index integrity: node serialization is total over all node types and the writer rejects non-dense id sequences, turning a silent edge-corruption class into a hard error; save→load verified by a randomized roundtrip property test (#71).
- Commands are joined across builds by structural identity only; both rendered-string index subsystems were deleted (−88 lines), removing a staleness class outright (#76, supersedes #70).
- Graph traversals are allocation-free via
edges_for_each: topo sort 19–34% faster, +0.75%.text(measured; the size/speed trade is documented at the decision site) (#75).
What's Changed
- Docs/hygiene bundle from paper audit (#77) by @typeless in #78
- Invalidate command_index on command insertion (#70) by @typeless in #79
- Enforce dense index ids at write time; make node serialization total (#71) by @typeless in #80
- Encode node kind as a 2-bit tag; bump index format to v12 (#73) by @typeless in #81
- Run build commands in a minimal declared environment (#68) by @typeless in #82
- Add --no-stat-cache; document the mtime stat-cache trade-off (#69) by @typeless in #83
- Add CONFIG_TRACKED_TOOLS: fold tool binaries into command identity (#72) by @typeless in #84
- Join commands across builds by identity only; delete string indexes (#76) by @typeless in #85
- Add edges_for_each visitor; drop per-call Vec allocation in hot traversals (#75) by @typeless in #86
- Bump version to 2.0.0 by @typeless in #87
Full Changelog: v1.1.0...v2.0.0
v1.1.0
v1.0.0
What's Changed
- Fix CI clang-tidy failure by installing LLVM 21 by @typeless in #1
- Fix test-windows-mingw CI hang: type CON blocks on headless runners by @typeless in #2
- Use generic_string() for cross-platform path correctness by @typeless in #3
- Fix % expansion to include all members by @typeless in #4
- Fix include_rules to include all Tuprules.tup root-to-leaf by @typeless in #5
- Scoped config merging: parent overrides child by @typeless in #8
- Document scoped tup.config merging by @typeless in #9
- Copy subdir tup.config files during configure for scoped configs by @typeless in #10
- Build GCC 15.2.0 from source as a putup BSP by @typeless in #6
- Remove install_source_configs — Tupfile-driven config by @typeless in #11
- bsp: eliminate Makefile.pup preprocessing by @typeless in #12
- fix: support -a (all-deps) for fresh scoped builds by @typeless in #13
- Eliminate STL associative containers + VarDb string interning by @typeless in #14
- Add pup::String with SSO, migrate production headers from std::string by @typeless in #15
- Clean up std::string in production .cpp files by @typeless in #16
- Complete std::vector to pup::Vec migration by @typeless in #17
- Clean remaining std::string and std::vector from .cpp files by @typeless in #18
- Add --strict convention checker for dual-mode composability by @typeless in #19
- Add failing test: 3-tree %o path missing build root prefix by @typeless in #25
- Rooted path algebra: Ungrounded, SourceRoot, BuildRoot (#24) by @typeless in #26
- Thread PathId through CommandInfo and output pipeline by @typeless in #22
- Migrate include/Tupfile nodes to ensure_file_node by @typeless in #27
- Replace walk_to_file_node with ensure_file_node in resolve_input_node by @typeless in #28
- Migrate get_or_create_directory_node callers to ensure_file_node by @typeless in #29
- Unify node lookup via PathPool::find_path + path_to_node by @typeless in #30
- Delete find_by_path, consolidate prefix stripping by @typeless in #31
- Eliminate string round-trip for group order-only edges by @typeless in #32
- Consolidate macro fallback logic in expand_rule by @typeless in #33
- Reorganize builder.cpp section layout by @typeless in #34
- Delete GraphBuilder wrapper, use free functions directly by @typeless in #35
- Rename deferred edge resolver, cache CI source tarballs by @typeless in #36
- Simplify builder public API to three functions by @typeless in #37
- Rename BuilderState → Builder, BuildState → BuildGraph by @typeless in #38
- Internalize 5 dag.hpp functions with no external callers by @typeless in #39
- Delete dead code and internalize unused dag.hpp functions by @typeless in #40
- Internalize mutable node accessors and condition node API by @typeless in #41
- Add get_node_type accessor, migrate type-only callers by @typeless in #42
- Add ECS property accessors, internalize raw node pointers by @typeless in #43
- Replace 4 named accessors with templated get by @typeless in #44
- Add view for composite accessors, drop 3 named Vec/SortedIdVec getters by @typeless in #45
- Fold 5 StringId accessors into get with role tags by @typeless in #46
- Add multi-scope regression tests for scoped-build implicit-dep detection by @typeless in #47
- Add pup show index forensic command by @typeless in #49
- Add [!shouldfail] reproducer for transitive implicit-dep tracking bug by @typeless in #50
- Re-run dep-scan when its parent compile is affected by @typeless in #51
- Identify commands by a structural hash, not a string or position by @typeless in #52
- Fold env-driven conditionals into command identity by @typeless in #53
- Make
import VAR ?= defaultrevert to default when env-var is unset by @typeless in #54 - Add gcov/gcovr test-coverage integration by @typeless in #55
- Build Windows releases with clang-cl + xwin instead of MinGW by @typeless in #56
- Remove leftover MinGW bootstrap script and config by @typeless in #57
- Remove leftover MSVC (cl.exe) bootstrap script and config by @typeless in #58
- Bump actions/checkout from v4 to v7 by @typeless in #59
- Fix documentation and plugin-skill accuracy/staleness (Fable review) by @typeless in #60
- Bump version to 1.0.0 by @typeless in #61
- Fix Windows Wine test on wine64-only runners by @typeless in #62
- Test Windows binary natively instead of under Wine by @typeless in #63
Full Changelog: https://github.com/typeless/putup/commits/v1.0.0