Declare it.
Forge builds it.
One forge.toml describes your entire build — packages, source fetch, compile, binaries, verification. Topological dependency ordering, parallel execution, automatic caching. Replace setup.sh with a declarative manifest.
Container projects conventionally ship a setup.sh.
Every project reinvents the same fragile shell scaffolding.
Numbered shell scripts with implicit ordering and no caching.
Setup scripts are brittle, unreadable, and unportable. Every project reinvents the same fragile shell scaffolding.
apt-get install scattered across 15 files with no cleanup.
Setup scripts are brittle, unreadable, and unportable. Every project reinvents the same fragile shell scaffolding.
Source builds mixed with binary downloads, no dependency graph.
Setup scripts are brittle, unreadable, and unportable. Every project reinvents the same fragile shell scaffolding.
Rebuilds re-execute everything because there is no cache key.
Setup scripts are brittle, unreadable, and unportable. Every project reinvents the same fragile shell scaffolding.
Package names differ across distros — nobody maps them.
Setup scripts are brittle, unreadable, and unportable. Every project reinvents the same fragile shell scaffolding.
What forge does for you.
Topological ordering, parallel execution, automatic caching, OS detection, and 12 operation types — all declarative.
Topological ordering
Components declare dependencies; forge resolves the execution order automatically. Diamond dependencies, fan-in, shared deps — all handled.
DAGParallel execution
Independent components run concurrently. Control concurrency with --parallel N or let forge auto-detect.
PARALLELBuild cache
Fully automatic. Cache key hashes ops + vars + dependency keys. On a hit, forge verifies outputs still exist before trusting the cache.
CACHEOS package manager detection
Detects apt, dnf, yum, or apk automatically. One manifest works on Debian, Alpine, Fedora. Build packages are cleaned up after all components finish.
PACKAGESPackage name mapping
Automatically maps common names across distros: build-essential, pkg-config, libfoo-dev/-devel. Unknown names pass through unchanged.
MAP12 operation types
packages, source_install, binary_install, user, mkdir, chown, chmod, copy, touch, generate, verify, raw — everything a build needs.
OPSVariable interpolation
${VAR} and ${VAR:-default} resolve from [vars], environment, and --var overrides. Precedence: vars < env < overrides.
VARSBuild strategies
source_install supports configure, autogen, cmake, meson, make, and detect. Auto-selects from the source tree when strategy = "detect".
BUILDConditional packages
Gate packages behind structured version conditions. Install bison + flex only when POSTGRESQL_VERSION >= 17.
CONDArtifact restoration
On cache hit with missing files, forge restores from an artifact archive. Works with BuildKit cache mounts and CI cache export.
ARTIFACTOne forge.toml replaces your entire setup.sh — declarative, ordered, cacheable.
One forge.toml replaces your entire setup.sh — declarative, ordered, cacheable.
[project]
name = "demo"
description = "A complete build manifest"
[vars]
VERSION = "1.0"
POSTGRESQL_VERSION = "17"
[[components]]
name = "base"
ops = [
{ packages = { build = ["build-essential", "pkg-config"],
runtime = ["curl", "ca-certificates"],
conditional = [{ category = "build",
when = { var = "POSTGRESQL_VERSION", gte = "17" },
packages = ["bison", "flex"] }] } },
{ user = { name = "app", create_home = true, system = true,
shell = "/bin/bash" } },
{ mkdir = [{ path = "/opt/app", mode = "0755", owner = "app" }] },
]
[[components]]
name = "app"
ops = [
{ source_install = { type = "archive",
url = "https://example.com/app-${VERSION}.tar.gz",
strategy = "configure",
prefix = "/usr/local/app",
flags = ["--with-ssl"],
jobs = 4 } },
{ verify = [{ file = "/usr/local/app/bin/app" }] },
]
[[components]]
name = "metrics"
ops = [
{ binary_install = { url = "https://example.com/metrics.tar.gz",
copy = [{ from = "metrics", to = "/usr/local/bin/metrics",
mode = "0755" }] } },
{ raw = "systemctl daemon-reload" },
]
# Build packages are removed after all components finish.
# Runtime packages are kept. Remove packages are explicitly stripped.
# Conditional packages gate behind version checks.
[[components]]
name = "deps"
ops = [
{ packages = { build = ["build-essential", "pkg-config"],
runtime = ["curl", "ca-certificates"],
remove = ["vim-tiny", "nano"],
conditional = [{ category = "build",
when = { var = "POSTGRESQL_VERSION", gte = "17" },
packages = ["bison", "flex"] }] } },
]
# Fetch source (archive or git), build with the chosen strategy,
# run before/after hooks, and verify outputs.
[[components]]
name = "app"
ops = [
{ source_install = { type = "archive",
url = "https://example.com/app-${VERSION}.tar.gz",
ref = "",
strategy = "configure",
prefix = "/usr/local/app",
flags = ["--with-ssl"],
jobs = 4,
before = [{ raw = "echo preparing" }],
after = [{ verify = [{ file = "/usr/local/app/bin/app" }] }] } },
]
# Download an archive and copy prebuilt binaries into place.
[[components]]
name = "tools"
ops = [
{ binary_install = { url = "https://example.com/tools.tar.gz",
copy = [{ from = "tool-a", to = "/usr/local/bin/tool-a",
mode = "0755" },
{ from = "tool-b", to = "/usr/local/bin/tool-b",
mode = "0755" }] } },
]
# Users, directories, permissions, ownership, file copy, code generation.
[[components]]
name = "setup"
ops = [
{ user = { name = "app", create_home = true, system = true, shell = "/bin/bash" } },
{ mkdir = [{ path = "/opt/app", mode = "0755", owner = "app" }] },
{ copy = [{ from = "conf/app.toml", to = "/etc/app/app.toml", mode = "0644" }] },
{ chown = [{ path = "/var/log/app.log", owner = "app", group = "app", recursive = true }] },
{ chmod = [{ path = "/opt/app/run.sh", mode = "0700" }] },
{ touch = ["/var/log/app.log"] },
{ generate = { tool = "protoc-c", input = "proto/msg.proto",
out = "gen", flags = ["--proto_path=proto"] } },
{ verify = [{ file = "/usr/local/app/bin/app" }] },
]
From TOML to execution.
forge reads your forge.toml, resolves the dependency graph, and executes components in topological order with maximum parallelism.
Each [[components]] entry is a node in the DAG. The ops list within each component is the build lifecycle — executed in order. Independent components run in parallel; dependent components wait. The build cache hashes each component's ops, vars, and dependency keys — a hit is trusted only if the output files still exist.
12 operations. Everything a build needs.
Each operation is a single-key table in the ops list.
Install packages via the detected OS package manager (apt, dnf, yum, apk).
Fetch source (archive/git) and build/install with configurable strategy.
Download an archive and copy a prebuilt binary into place.
Create a system or user account.
Create directories with mode and owner.
Change ownership of a path.
Change permissions of a path.
Copy a file with optional mode.
Create an empty file.
Generate code (e.g. protoc-c).
Check files/directories exist.
Run an arbitrary shell command (last resort).
Install forge v0.1.0
Prebuilt binaries are on the GitHub releases page, or build from source.
go build -o forge ./app
Questions, answered.
What exactly is forge?
forge is a declarative, TOML-driven build orchestrator. One forge.toml describes packages, source builds, binary downloads, filesystem ops, and verification as an ordered ops list per component. forge resolves dependencies topologically and executes in parallel.
How does it differ from Make or shell scripts?
Make is file-oriented; forge is component-oriented with explicit dependency ordering, parallel execution, automatic caching, and OS package manager detection. No more setup.sh with numbered scripts and implicit ordering.
What package managers does forge support?
apt (Debian/Ubuntu), dnf (Fedora/RHEL 8+), yum (RHEL/CentOS 7), and apk (Alpine). forge detects the OS automatically and maps common package names across distros.
How does the build cache work?
Each component's cache key hashes its ops, the vars it references, and its dependencies' keys. A hit is trusted only if the output files still exist. On a hit with missing files, forge restores from an artifact archive.
What licence is forge under?
Apache License 2.0. The current release is v0.1.0; prebuilt binaries are available on the GitHub releases page, or you can build from source.
Stop writing setup.sh.
Declare your build in TOML. Let forge handle the ordering, parallelism, and caching.
Get started →