v0.1.0 — Apache 2.0

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.

$ forge build forge.toml

Container projects conventionally ship a setup.sh.

Every project reinvents the same fragile shell scaffolding.

1

Numbered shell scripts with implicit ordering and no caching.

Setup scripts are brittle, unreadable, and unportable. Every project reinvents the same fragile shell scaffolding.

2

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.

3

Source builds mixed with binary downloads, no dependency graph.

Setup scripts are brittle, unreadable, and unportable. Every project reinvents the same fragile shell scaffolding.

4

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.

5

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.

DAG
⚙

Parallel execution

Independent components run concurrently. Control concurrency with --parallel N or let forge auto-detect.

PARALLEL
⚙

Build cache

Fully automatic. Cache key hashes ops + vars + dependency keys. On a hit, forge verifies outputs still exist before trusting the cache.

CACHE
⚙

OS 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.

PACKAGES
⚙

Package name mapping

Automatically maps common names across distros: build-essential, pkg-config, libfoo-dev/-devel. Unknown names pass through unchanged.

MAP
⚙

12 operation types

packages, source_install, binary_install, user, mkdir, chown, chmod, copy, touch, generate, verify, raw — everything a build needs.

OPS
⚙

Variable interpolation

${VAR} and ${VAR:-default} resolve from [vars], environment, and --var overrides. Precedence: vars < env < overrides.

VARS
⚙

Build strategies

source_install supports configure, autogen, cmake, meson, make, and detect. Auto-selects from the source tree when strategy = "detect".

BUILD
⚙

Conditional packages

Gate packages behind structured version conditions. Install bison + flex only when POSTGRESQL_VERSION >= 17.

COND
⚙

Artifact restoration

On cache hit with missing files, forge restores from an artifact archive. Works with BuildKit cache mounts and CI cache export.

ARTIFACT

One forge.toml replaces your entire setup.sh — declarative, ordered, cacheable.

One forge.toml replaces your entire setup.sh — declarative, ordered, cacheable.

Examples
  • 📄 Full manifest
  • 📄 Packages
  • 📄 Source install
  • 📄 Binary install
  • 📄 Filesystem ops
[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.

Parse
Read TOML
Validate
Resolve vars
→
Plan
Build DAG
Topo sort
Cache check
→
Execute
Parallel run
Cache store
Verify

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.

packages

Install packages via the detected OS package manager (apt, dnf, yum, apk).

source_install

Fetch source (archive/git) and build/install with configurable strategy.

binary_install

Download an archive and copy a prebuilt binary into place.

user

Create a system or user account.

mkdir

Create directories with mode and owner.

chown

Change ownership of a path.

chmod

Change permissions of a path.

copy

Copy a file with optional mode.

touch

Create an empty file.

generate

Generate code (e.g. protoc-c).

verify

Check files/directories exist.

raw

Run an arbitrary shell command (last resort).

Install forge v0.1.0

Prebuilt binaries are on the GitHub releases page, or build from source.

🔨
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 →