Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
66fc575
✨ feat: add Go binary-builder rewrite with full cflinuxfs4 parity
ramonskie Feb 25, 2026
2514337
🔥 refactor: remove Ruby binary-builder, update JDK URLs to jammy Bell…
ramonskie Feb 25, 2026
16ef195
fix: address all 8 code review findings
ramonskie Feb 27, 2026
77e28d4
fix: repair previously failing tests
ramonskie Feb 27, 2026
c4366e2
point to log location in overview for our parity test
ramonskie Feb 27, 2026
507b885
[test] remove deps that we do not have and add deps that where missing
ramonskie Feb 27, 2026
4884af1
output: always serialize source sha256 and url fields
ramonskie Feb 27, 2026
7687402
stacks: update JRuby JDK to 8u452+11 and add libunwind_build apt pack…
ramonskie Feb 27, 2026
1bc0264
recipe/libunwind: fix build — autoreconf, apt install, dirName
ramonskie Feb 27, 2026
1e203db
recipe/jruby: fix JAVA_HOME/JAVACMD propagation to Maven subprocesses
ramonskie Feb 27, 2026
d78934c
recipe/r: preserve Rserve key casing in sub-dependencies map
ramonskie Feb 27, 2026
6adebf4
test/parity: fix run-all.sh dep versions/sha256s, add run_dep_r and F…
ramonskie Feb 27, 2026
08a9f73
docs: expand README parity test section, remove stale results from AG…
ramonskie Feb 27, 2026
05fcade
recipe/jruby: fix unit tests — pre-create jdk subdir, test observable…
ramonskie Feb 27, 2026
3d03dea
🏗️ refactor: embed PHP extension YAMLs into binary via go:embed
ramonskie Mar 2, 2026
dbe227a
🔍 fix: clarify patchFileRE intent, drop duplicate test, rename mislea…
ramonskie Mar 2, 2026
a0a5160
remove temp test files
ramonskie Mar 4, 2026
a3c5aec
feat: replace wget with Fetcher.Download and add real SHA256s for all…
ramonskie Mar 4, 2026
72680bc
fix: parity test infrastructure (Ruby builder compat, zstd, libunwind…
ramonskie Mar 4, 2026
f4a481e
♻️ refactor: move hardcoded apt packages into stacks/*.yaml
ramonskie Mar 5, 2026
1b94f1c
refactor: phase 0–3 — extract GoToolRecipe, RepackRecipe, BundleRecip…
ramonskie Mar 5, 2026
53579af
refactor: phase 4–5 — extract autoconf.Recipe, migrate libunwind/libg…
ramonskie Mar 5, 2026
a6da6cb
docs: phase 6 — update AGENTS.md for new recipe abstractions
ramonskie Mar 5, 2026
94a909d
refactor: unify bootstrap config under bootstrap.{go,jruby,ruby} in s…
ramonskie Mar 5, 2026
80b220d
test: pin Ruby builder parity clone to ruby-builder-final tag
ramonskie Mar 5, 2026
c9f2911
docs: update README with dual-mode CLI, JSON summary format, and corr…
ramonskie Mar 12, 2026
41792fe
feat: rewrite CLI with dual-mode input, --output-file JSON summary, a…
ramonskie Mar 12, 2026
c5c8ff8
✨ feat: add flit-core and generalize setuptools into PyPISourceRecipe
ramonskie Mar 23, 2026
3f84107
🐛 fix: switch R recipe from devtools to remotes for install_version
ramonskie Mar 23, 2026
e8c21b0
🐛 fix: correct gfortran version to 13 for cflinuxfs5 (Ubuntu 24.04 no…
ramonskie Mar 23, 2026
a6734fe
✅ test: fix cflinuxfs5 gfortran tests to use version 13 and libexec_path
ramonskie Mar 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
/*.tgz
//*.tgz
/*.tar.gz
.rspec
internal/recipe/artifacts/
.bundle
.ccache
/logs
ports/
cflinuxfs4/
tmp/
mini_portile/
.idea/
oracle_client_libs
*deb*
**/.idea
/binary-builder
4 changes: 0 additions & 4 deletions .rspec

This file was deleted.

18 changes: 0 additions & 18 deletions .rubocop.yml

This file was deleted.

1 change: 0 additions & 1 deletion .ruby-version

This file was deleted.

296 changes: 280 additions & 16 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,23 +1,287 @@
# Agent Guidelines for Binary Builder

## Test Commands
- Run all tests: `bundle exec rspec`
- Run single test: `bundle exec rspec spec/integration/ruby_spec.rb`
- Exclude Oracle PHP tests: `bundle exec rspec --tag ~run_oracle_php_tests`
## Overview

## Lint Commands
- Run RuboCop: `bundle exec rubocop`
Go tool that compiles CF buildpack dependencies (ruby, node, python, httpd, …) for a
specific CF stack (cflinuxfs4, cflinuxfs5). Entry point: `cmd/binary-builder/main.go`.

---

## Build & Test Commands

### Unit tests (Tier 1 — no Docker, no network)
```bash
go test ./... # all packages
go test -race ./... # with race detector (CI requirement)
```

### Run a single test or test file
```bash
# By test name (regex) within a package:
go test ./internal/recipe/ -run TestRubyRecipeBuild
go test ./internal/runner/ -run TestFakeRunner
go test ./internal/stack/ -run TestLoad

# Run an entire package verbosely:
go test -v ./internal/recipe/

# Run with race detector for a single package:
go test -race ./internal/recipe/ -run TestHTTPD
```

### Parity tests (Tier 2 — requires Docker + network)
```bash
make parity-test DEP=httpd [STACK=cflinuxfs4]
make parity-test-all [STACK=cflinuxfs4]
```

### Exerciser test (Tier 3 — requires Docker)
```bash
make exerciser-test ARTIFACT=/tmp/ruby_3.3.6_...tgz STACK=cflinuxfs4
```

### Build the binary
```bash
go build ./cmd/binary-builder
```

---

## Architecture

```
cmd/binary-builder/main.go ← CLI entry point
internal/
recipe/ ← one file per dep (ruby.go, node.go, httpd.go, …)
php/ ← PHP extension recipes (pecl.go, fake_pecl.go)
runner/ ← Runner interface + RealRunner + FakeRunner
stack/ ← Stack struct + YAML loader
fetch/ ← Fetcher interface + HTTPFetcher
archive/ ← StripTopLevelDir, StripFiles, InjectFile
source/ ← source.Input (data.json parser)
output/ ← OutData, BuildOutput, DepMetadataOutput
artifact/ ← Artifact naming, SHA256, S3 URL helpers
apt/ ← apt.New(runner).Install(ctx, pkgs...)
portile/ ← configure/make/install wrapper
stacks/ ← cflinuxfs4.yaml, cflinuxfs5.yaml ← ALL stack-specific data lives here
```

### Key design rules
- **Stack config is data, not code.** Every Ubuntu-version-specific value
(apt packages, compiler paths, bootstrap URLs) lives in `stacks/{stack}.yaml`.
Recipes read from `*stack.Stack`; no stack names are hardcoded in Go source.
- **Runner interface** — all `exec.Cmd` usage goes through `runner.Runner`.
`RealRunner` executes; `FakeRunner` records calls for tests.
- **Fetcher interface** — all HTTP calls go through `fetch.Fetcher`.
`HTTPFetcher` does the real work; `FakeFetcher` (in `recipe_test.go`) is used in tests.
- **`RunInDirWithEnv` appends env vars** — appended vars win over inherited env on Linux.
So `GOTOOLCHAIN=local` appended DOES override any existing `GOTOOLCHAIN`.
- **miniconda3-py39 is URL-passthrough**: `Build()` sets `outData.URL`/`outData.SHA256`
directly instead of writing a file. `main.go` checks `if outData.URL == ""` before
calling `handleArtifact`.

---

## Code Style
- **Encoding**: Add `# encoding: utf-8` at the top of all Ruby files
- **Imports**: Use `require_relative` for local files, `require` for gems
- **Naming**: Use snake_case for methods/variables, CamelCase for classes
- **Classes**: Recipe classes inherit from `BaseRecipe` or `MiniPortile`
- **Error Handling**: Use `or raise 'Error message'` for critical failures, check `$?.success?` for command execution
- **String Interpolation**: Prefer double quotes and `#{}` for interpolation
- **Methods**: Define helper methods as private when appropriate

### Language & toolchain
- **Go only.** The Ruby binary-builder has been fully removed.
- Module: `github.com/cloudfoundry/binary-builder` — use this import path.
- Minimum Go version: see `go.mod`.

### Naming conventions
| Kind | Convention | Example |
|------|-----------|---------|
| Exported type / func | PascalCase | `RubyRecipe`, `NewRegistry` |
| Unexported func / var | camelCase | `buildRegistry`, `mustCwd` |
| Interface | noun (no `I` prefix) | `Runner`, `Fetcher`, `Recipe` |
| Test helper | camelCase | `newFakeFetcher`, `useTempWorkDir` |
| Constants | PascalCase (exported) or camelCase | — |

### Import grouping
Three groups, separated by blank lines:
```go
import (
// 1. stdlib
"context"
"fmt"
"os"

// 2. third-party
"gopkg.in/yaml.v3"
"github.com/stretchr/testify/assert"

// 3. internal
"github.com/cloudfoundry/binary-builder/internal/runner"
"github.com/cloudfoundry/binary-builder/internal/stack"
)
```

### Error handling
- Return errors explicitly; never panic in production code paths.
- Wrap with context using `fmt.Errorf("component: action: %w", err)`.
- Pattern: `return fmt.Errorf("ruby: apt install ruby_build: %w", err)`
- Error strings are lowercase (Go convention).
- On fatal CLI errors: `fmt.Fprintf(os.Stderr, "binary-builder: %v\n", err); os.Exit(1)`.

### Comments
- Package-level doc comment on every package: `// Package foo does X.`
- Exported types/funcs always have doc comments.
- Inline comments explain *why*, not *what*.
- Use `// nolint:errcheck` only when the error is genuinely ignorable (e.g., closing
a writer in a test after all data is flushed).

### Structs and interfaces
- Define interfaces where the consumer lives, not where the implementation lives.
- Struct fields use PascalCase; YAML tags use snake_case: `InstallDir string \`yaml:"install_dir"\``.
- Zero-value structs should be usable where practical.

---

## Recipe Patterns
- Override `computed_options`, `url`, `archive_files`, `prefix_path` in recipe classes
- Use `execute()` for build steps, `run()` for apt/system commands
- Place recipes in `recipe/` directory, tests in `spec/integration/`

Every recipe implements `recipe.Recipe`:
```go
type Recipe interface {
Name() string
Build(ctx context.Context, s *stack.Stack, src *source.Input, r runner.Runner, outData *output.OutData) error
Artifact() ArtifactMeta // OS, Arch, Stack ("" = use build stack)
}
```

### Shared recipe abstractions

Before writing a new recipe from scratch, check whether one of these abstractions fits:

| Abstraction | Location | Use when |
|-------------|----------|----------|
| `autoconf.Recipe` | `internal/autoconf/` | configure / make / make install cycle (libunwind, libgdiplus, openresty, nginx) |
| `RepackRecipe` | `internal/recipe/repack.go` | Download an archive and optionally strip its top-level dir (bower, yarn, setuptools, rubygems) |
| `BundleRecipe` | `internal/recipe/bundle.go` | `pip3 download` multiple packages into a tarball (pip, pipenv) |
| `GoToolRecipe` | `internal/recipe/dep.go` | Download + build a Go tool with `go get`/`go build` (dep, glide, godep) |
| `PassthroughRecipe` | `internal/recipe/passthrough.go` | No build step — just record the upstream URL and SHA256 |

#### Using `autoconf.Recipe`

`autoconf.Recipe` lives in `internal/autoconf/` (separate package to avoid import cycles).
It is **not** a `recipe.Recipe` itself — wrap it in a thin struct in `internal/recipe/`:

```go
type MyRecipe struct{ Fetcher fetch.Fetcher }

func (r *MyRecipe) Name() string { return "mylib" }
func (r *MyRecipe) Artifact() ArtifactMeta { return ArtifactMeta{OS: "linux", Arch: "x64"} }

func (r *MyRecipe) Build(ctx context.Context, s *stack.Stack, src *source.Input,
run runner.Runner, out *output.OutData) error {
return (&autoconf.Recipe{
DepName: "mylib",
Fetcher: r.Fetcher,
Hooks: autoconf.Hooks{
AptPackages: func(s *stack.Stack) []string { return s.AptPackages["mylib_build"] },
ConfigureArgs: func(_, prefix string) []string { return []string{"--prefix=" + prefix, "--enable-shared"} },
PackDirs: func() []string { return []string{"include", "lib"} },
},
}).Build(ctx, s, src, run, out)
}
```

Available hooks (all optional — nil = default behaviour):

| Hook | Default | Typical override |
|------|---------|-----------------|
| `AptPackages` | `s.AptPackages["{name}_build"]` | Custom package list |
| `BeforeDownload` | no-op | GPG verification (nginx) |
| `SourceProvider` | fetch tarball, extract to `/tmp/{name}-{version}` | `git clone` (libgdiplus), read from `source/` (libunwind) |
| `AfterExtract` | no-op | `autoreconf -i`, `autogen.sh` |
| `ConfigureArgs` | `["--prefix={prefix}"]` | Full custom args |
| `ConfigureEnv` | nil | `CFLAGS`, `CXXFLAGS` |
| `MakeArgs` | nil | `["-j2"]` |
| `InstallEnv` | nil (falls back to `ConfigureEnv`) | `DESTDIR` (nginx) |
| `AfterInstall` | no-op | Remove runtime dirs, symlinks |
| `PackDirs` | `["."]` | `["include", "lib"]`, `["lib"]` |
| `AfterPack` | no-op | `archive.StripTopLevelDir` (nginx) |

### Adding a new recipe
1. Create `internal/recipe/{name}.go` with a struct implementing `Recipe`.
2. Register it in `buildRegistry()` in `cmd/binary-builder/main.go`.
3. Add a test in `internal/recipe/recipe_test.go` or a new `{name}_test.go` file.
4. If the dep is architecture-neutral, set `Arch: "noarch"` in `ArtifactMeta`.
5. For URL-passthrough deps (no build step), use `PassthroughRecipe` or
set `outData.URL`/`outData.SHA256` directly.
6. For autoconf-based deps, use `autoconf.Recipe` with hooks (see above).
7. For download-and-strip deps, use `RepackRecipe`.

### Stack-specific behaviour
Recipes **must not** contain `if s.Name == "cflinuxfs4"` guards. Instead:
- Add the relevant value to both `stacks/cflinuxfs4.yaml` and `stacks/cflinuxfs5.yaml`.
- Read it from `s.AptPackages["key"]`, `s.Python.UseForceYes`, etc.

---

## Testing Conventions

- Test package: always use `package recipe_test` (external test package) for `internal/recipe/`.
Other packages follow the same `_test` suffix convention.
- Assertion library: `github.com/stretchr/testify/assert` (non-fatal) and
`github.com/stretchr/testify/require` (fatal / setup).
- Use `require.NoError` for setup steps; `assert.*` for behaviour assertions.
- **`FakeRunner`** in `internal/runner/runner.go` — inject instead of `RealRunner` in tests.
Inspect `fakeRunner.Calls` to verify command sequence, args, env, and dir.
- **`FakeFetcher`** defined in `recipe_test.go` — inject instead of `HTTPFetcher`.
Inspect `f.DownloadedURLs` and set `f.ErrMap` / `f.BodyMap` to control behaviour.
- **`useTempWorkDir(t)`** — helper in `recipe_helpers_test.go`. Call it in tests that
need a clean CWD (recipes write artifacts relative to CWD). NOT safe for `t.Parallel()`.
- **`writeFakeArtifact(t, name)`** — creates a minimal valid `.tgz` in CWD so archive
helpers don't fail when processing a fake build output.
- **Table-driven tests** are preferred for multiple similar cases (see
`TestCompiledRecipeArtifactMetaSanity`).
- Test names follow `Test{Type}{Behaviour}` pattern: `TestRubyRecipeBuild`,
`TestNodeRecipeStripsVPrefix`.

---

## Parity Test Infrastructure

- Script: `test/parity/compare-builds.sh --dep <name> --data-json <path> [--stack <stack>]`
- Logs: `/tmp/parity-logs/<dep>-<version>-<stack>.log`
- The Go builder is compiled **inside the container at runtime** (`go build ./cmd/binary-builder`),
so source changes are always picked up on re-run without a separate image rebuild.

### Sample data.json files (on the build host at `/tmp/`)
| File | Dep | Version |
|------|-----|---------|
| `/tmp/go-data.json` | go | 1.22.0 |
| `/tmp/node-data.json` | node | 20.11.0 |
| `/tmp/php-data.json` | php | 8.1.32 |
| `/tmp/r-data.json` | r | 4.2.3 |
| `/tmp/jruby-data.json` | jruby | 9.4.14.0 |
| `/tmp/appdynamics-data.json` | appdynamics | 23.11.0-839 |
| `/tmp/skywalking-data.json` | skywalking-agent | 9.5.0 |

R sub-dep data.json files live under `/tmp/r-sub-deps/source-{pkg}-latest/data.json`
(forecast, plumber, rserve, shiny).

---

## Key Files Reference

| File | Purpose |
|------|---------|
| `cmd/binary-builder/main.go` | CLI, `findIntermediateArtifact`, `handleArtifact`, `buildRegistry` |
| `internal/runner/runner.go` | `Runner` interface, `RealRunner`, `FakeRunner` + `Call` type |
| `internal/fetch/fetch.go` | `Fetcher` interface, `HTTPFetcher` |
| `internal/stack/stack.go` | `Stack` struct, `Load(stacksDir, name)` |
| `internal/recipe/recipe.go` | `Recipe` interface, `Registry` |
| `internal/archive/archive.go` | `StripTopLevelDir`, `StripFiles`, `InjectFile` |
| `internal/portile/` | configure/make/install abstraction |
| `internal/apt/` | apt-get install wrapper |
| `stacks/cflinuxfs4.yaml` | All cflinuxfs4-specific values |
| `stacks/cflinuxfs5.yaml` | All cflinuxfs5-specific values |
| `test/parity/compare-builds.sh` | Parity test harness |
| `Makefile` | `unit-test`, `unit-test-race`, `parity-test`, `exerciser-test` |

## cflinuxfs4/ports/ note
The `cflinuxfs4/ports/` directory contains root-owned build artifacts from previous Ruby
parity test runs. These cannot be removed without `sudo` and are NOT tracked by git.
They do not affect builds or tests.
21 changes: 0 additions & 21 deletions Gemfile

This file was deleted.

Loading