sub_dependencies and git_commit_sha are omitted when not applicable.
All build subprocess output (compiler, make, etc.) goes to stdout/stderr so it
is visible in logs without corrupting the structured JSON output file.
The CI task that wraps this tool is responsible for moving the artifact,
writing dep-metadata and builds-artifacts JSON, and committing to git.
PHP
PHP is built the same way as any other dependency — no extra flags needed.
Extension and native module definitions are embedded directly in the binary
(see internal/php/assets/):
To add support for a new PHP minor version, create
internal/php/assets/php<major><minor>-extensions-patch.yml with any
additions or exclusions relative to the major-version base file. No code
changes are required — the file is discovered automatically at build time.
Building
go build ./cmd/binary-builder
Testing
# Unit tests (no Docker or network required)
make unit-test
# Unit tests with race detector
make unit-test-race
# Parity test for a single dep from the matrix (requires Docker + network)
# VERSION is not an argument — each dep runs at the version pinned in run-all.sh.
make parity-test DEP=ruby
make parity-test DEP=php STACK=cflinuxfs4
# To test a specific version not in the matrix, call compare-builds.sh directly
# with a custom data.json:
test/parity/compare-builds.sh --dep php --data-json /tmp/php-8.3.0-data.json --stack cflinuxfs4
# Parity test for all deps
make parity-test-all
Architecture
cmd/binary-builder/ — CLI entry point
internal/recipe/ — per-dependency build recipes
internal/php/ — PHP extension build logic and embedded extension data (assets/)
internal/archive/ — tarball / zip manipulation helpers
test/parity/ — Parity test scripts (compare Ruby vs Go builder outputs)
Parity Tests
The parity tests verify that the Go builder produces identical output to the
original Ruby builder for every supported dependency. This is the primary
confidence check that the Go rewrite is correct.
Scripts
Script
Purpose
test/parity/run-all.sh
Runs every dep in the test matrix sequentially; prints a pass/fail summary and tails failure logs
test/parity/compare-builds.sh
Runs both builders for a single dep and diffs their output
How it works
For each dependency, compare-builds.sh does the following:
1. Source pre-download
Some deps (libunwind, dotnet-*, jprofiler-profiler, your-kit-profiler)
are built from a source tarball that must already be present in a source/
directory at build time — neither builder downloads them inline. The script
downloads the tarball on the host first, then mounts it into both containers
as a read-only volume at /tmp/host-source/.
All other deps download their own source inside the container during the build.
2. Run the Ruby builder
Runs buildpacks-ci/tasks/build-binary-new-cflinuxfs4/build.rb inside a
cloudfoundry/<stack> Docker container with this layout:
/task/
source/data.json ← the depwatcher input
source/<tarball> ← pre-downloaded source (if applicable)
source-*-latest/ ← R sub-dep data.json dirs (r dep only)
binary-builder/ ← symlink to this repo
buildpacks-ci/ ← symlink to ../buildpacks-ci
artifacts/ ← artifact output (*.tgz / *.zip)
dep-metadata/ ← dep-metadata JSON output
builds-artifacts/
binary-builds-new/<dep>/ ← builds JSON output
SKIP_COMMIT=true prevents git commits. Ruby 3.4.6 is compiled from source
inside the container if not already present.
3. Run the Go builder
Compiles binary-builder from source inside the same cloudfoundry/<stack>
container (using mise to install the required Go version), then runs:
The JSON summary written to --output-file is then used by the script to move
the artifact, write the dep-metadata JSON, and write the builds-artifacts JSON
into /out/ — mirroring exactly what the CI task (tasks/build-binary/build.sh)
does in production.
The source tarball (if any) and R sub-dep dirs are copied into the working
directory before the build runs.
4. Compare outputs
If the Ruby builder failed, the comparison is skipped entirely — the test exits
0 with RUBY BROKEN. Otherwise all three output types are compared:
Output
How it is compared
Hard failure?
Artifact filename
Both filenames are normalised by replacing the 8-char content SHA (_<sha8>.) with _. then compared
Yes
Artifact contents
Files inside the .tgz or .zip are listed and sorted, then diffed
Yes
Builds JSON
Fields version, source.url, source.sha256, source.sha512, source.md5, url, sha256, and sub_dependencies[*].version are compared individually
Yes
Dep-metadata JSON structural fields
All fields except sha256 and url (the artifact hash) and sub_dependencies[*].source.sha256 are compared with jq -S (sorted keys)
Yes
Dep-metadata JSON artifact hash
Top-level sha256 and url fields are diffed
Warn only — non-reproducible builds (e.g. bundler) legitimately differ
Sub-dep source sha256
sub_dependencies[*].source.sha256
Warn only — Ruby builder has a known bug where it records the sha256 of an HTTP redirect response body rather than the actual tarball
Exit outcomes
Result
Meaning
PASS
Both builders produced identical output on all hard-failure checks
RUBY BROKEN
Ruby builder failed; Go builder output not compared; exits 0
FAIL
One or more hard-failure mismatches; exits 1
Input format
Both builders receive the same depwatcher data.json:
For SHA512-only deps (e.g. dotnet-*, skywalking-agent), sha256 is ""
and sha512 carries the real checksum. Both fields are always present in the
builder output — the sha256 field is never omitted even when empty.
Running
# All deps (requires Docker + network)
test/parity/run-all.sh [<stack>]
# Single dep
test/parity/compare-builds.sh --dep ruby --data-json /tmp/ruby-data.json --stack cflinuxfs4
# R dep (needs sub-dep data.json dirs)
test/parity/compare-builds.sh --dep r --data-json /tmp/r-data.json \
--sub-deps-dir /tmp/r-sub-deps
All output is written to both the terminal and
/tmp/parity-logs/<dep>-<version>-<stack>.log. To watch a running build:
binary-builder
A Go tool for building binaries used by Cloud Foundry buildpacks.
Supported binaries
Usage
The tool supports two input modes.
Mode 1 — Direct flags (manual / local use)
--url,--sha256, and--sha512are optional; include whichever checksums the recipe needs to verify the source download.Mode 2 — Source file (CI / depwatcher use)
data.jsonis the standard depwatcher output format:If
--source-fileis omitted andsource/data.jsonexists in the current working directory, it is used automatically.Common flags
--stackcflinuxfs4orcflinuxfs5--stacks-dirstacks--output-filesummary.jsonOutput
The artifact (
.tgzor.zip) is written to the current working directory using the canonical filename:A JSON summary is written to
--output-file(default:summary.json):sub_dependenciesandgit_commit_shaare omitted when not applicable. All build subprocess output (compiler, make, etc.) goes to stdout/stderr so it is visible in logs without corrupting the structured JSON output file.The CI task that wraps this tool is responsible for moving the artifact, writing dep-metadata and builds-artifacts JSON, and committing to git.
PHP
PHP is built the same way as any other dependency — no extra flags needed. Extension and native module definitions are embedded directly in the binary (see
internal/php/assets/):To add support for a new PHP minor version, create
internal/php/assets/php<major><minor>-extensions-patch.ymlwith any additions or exclusions relative to the major-version base file. No code changes are required — the file is discovered automatically at build time.Building
Testing
Architecture
cmd/binary-builder/— CLI entry pointinternal/recipe/— per-dependency build recipesinternal/php/— PHP extension build logic and embedded extension data (assets/)internal/archive/— tarball / zip manipulation helpersinternal/runner/— subprocess execution helpersstacks/— per-stack YAML configuration (versions, URLs, paths)test/parity/— Parity test scripts (compare Ruby vs Go builder outputs)Parity Tests
The parity tests verify that the Go builder produces identical output to the original Ruby builder for every supported dependency. This is the primary confidence check that the Go rewrite is correct.
Scripts
test/parity/run-all.shtest/parity/compare-builds.shHow it works
For each dependency,
compare-builds.shdoes the following:1. Source pre-download
Some deps (
libunwind,dotnet-*,jprofiler-profiler,your-kit-profiler) are built from a source tarball that must already be present in asource/directory at build time — neither builder downloads them inline. The script downloads the tarball on the host first, then mounts it into both containers as a read-only volume at/tmp/host-source/.All other deps download their own source inside the container during the build.
2. Run the Ruby builder
Runs
buildpacks-ci/tasks/build-binary-new-cflinuxfs4/build.rbinside acloudfoundry/<stack>Docker container with this layout:SKIP_COMMIT=trueprevents git commits. Ruby 3.4.6 is compiled from source inside the container if not already present.3. Run the Go builder
Compiles
binary-builderfrom source inside the samecloudfoundry/<stack>container (usingmiseto install the required Go version), then runs:The JSON summary written to
--output-fileis then used by the script to move the artifact, write the dep-metadata JSON, and write the builds-artifacts JSON into/out/— mirroring exactly what the CI task (tasks/build-binary/build.sh) does in production.The source tarball (if any) and R sub-dep dirs are copied into the working directory before the build runs.
4. Compare outputs
If the Ruby builder failed, the comparison is skipped entirely — the test exits 0 with
RUBY BROKEN. Otherwise all three output types are compared:_<sha8>.) with_.then compared.tgzor.zipare listed and sorted, then diffedversion,source.url,source.sha256,source.sha512,source.md5,url,sha256, andsub_dependencies[*].versionare compared individuallysha256andurl(the artifact hash) andsub_dependencies[*].source.sha256are compared withjq -S(sorted keys)sha256andurlfields are diffedbundler) legitimately differsub_dependencies[*].source.sha256Exit outcomes
PASSRUBY BROKENFAILInput format
Both builders receive the same depwatcher
data.json:For SHA512-only deps (e.g.
dotnet-*,skywalking-agent),sha256is""andsha512carries the real checksum. Both fields are always present in the builder output — thesha256field is never omitted even when empty.Running
All output is written to both the terminal and
/tmp/parity-logs/<dep>-<version>-<stack>.log. To watch a running build:run-all.shprints a summary at the end and tails the last 20 lines of each failure log automatically.Contributing
See CONTRIBUTING.md.