Skip to content

wiki: enrich Troubleshooting / cmake / Specifics / Packaging-Guide (drafts inline) #13098

@tannevaled

Description

@tannevaled

Wiki enrichment proposal

After contributing ~30 recipes over the last few weeks I've accumulated a fair amount of pantry-authoring tribal knowledge (cgo build chains, cc-wrapper-style libstdc++ resolution, CMake 4.x policy compat, fix-patchelf quirks, brewkit test-sandbox PATH gotchas, etc.). The current wiki is mostly stubs (Troubleshooting.md = 3 lines, cmake.md = 1 line, Specifics.md = 1 line, Packaging-Guide.md = 8 lines), so I drafted enriched versions of those four pages.

I can't PR a wiki repo directly through GitHub UI, so I'm filing this for review. Three options on how to land:

  1. A maintainer copies the content below into the wiki pages (no permission grant needed).
  2. A maintainer grants me wiki-write access and I push the changes directly (typical pattern for gollum-style wikis).
  3. We discuss specific edits inline first and iterate.

Happy with whichever works. All four pages are below in collapsible blocks for readability.


Troubleshooting.md — failure patterns by language + toolchain (~180 lines)
# Troubleshooting

A growing list of failure patterns seen building packages for pantry, with the diagnostic that pins them down and the fix that's worked. Build with `bk -L build` (Linux docker) to reproduce CI on a Mac.

## Quickest-win checklist

Before deep-diving, try in order — these explain ~60% of new-recipe failures:

1. **Read the upstream `INSTALL` / `BUILDING.md`** for required build tools you didn't declare.
2. **Read another packager's recipe** for the same project — `nixpkgs/pkgs/.../<name>/default.nix`, `Homebrew/homebrew-core/Formula/<name>.rb`, or `gitlab.archlinux.org/.../<name>/PKGBUILD`. Their patches and configure flags are the canonical answer to "what does the upstream really need".
3. **Run `bk build` then inspect the build dir**`cd ./builds/<pkg>/<pkg>+<platform>/`, edit `dev.pkgx.build.sh`, and reproduce manually. The script is plain bash; you can `set -x` it and rerun pieces.
4. **Drop the build into the env** without running it: edit `dev.pkgx.build.sh` to keep only the `export` block, then `source dev.pkgx.build.sh`. You now have the CI env in your shell to poke around.

## c / c++

C and C++ are complex. Try ensuring the `cc` (c-compiler) used is `clang`. How varies by build system. Usually `CC=clang` / `CXX=clang++` is enough. CMake is weird — see [`cmake`](./cmake) for that.

### `error: 'std::set::contains' was not declared` (or similar C++17/20 features)

Project compiled by clang but is using a libstdc++ that's too old to expose the C++17/20 API. The cleanest fix is to make clang resolve libstdc++ from a known-good GCC bottle (this is the pattern nixpkgs uses via its `cc-wrapper`):

```yaml
env:
  linux:
    CXXFLAGS: $CXXFLAGS -isystem {{deps.gnu.org/gcc.prefix}}/include/c++/{{deps.gnu.org/gcc.version.major}} \
                        -isystem {{deps.gnu.org/gcc.prefix}}/include/c++/{{deps.gnu.org/gcc.version.major}}/x86_64-pkgx-linux-gnu
```

Forcing `-DCMAKE_CXX_STANDARD=20` rarely sticks because most projects pin via `target_compile_features(target cxx_std_17)`, which overrides the global.

### `GLIBCXX_3.4.31 not found` at run time

Build links libstdc++ from a newer gcc bottle than what's available at runtime. Options:

- **Statically link the C++ stdlib** in the final binary: `-static-libstdc++ -static-libgcc` on the link line. Trades binary size for ABI hermeticity. Best for self-contained tools (wasm-opt, ripgrep-style).
- **Declare `gnu.org/gcc` as a runtime dep** so the bottle's libstdc++ travels with the binary. Heavier on the user's pkgx prefix but correct for libraries.

### Linker can't find `-lrocksdb` / `-lbz2` / `-lsnappy` despite vendored builds

CGO-style projects (e.g. cubefs, tikv) compile vendored static libs under `build/lib`, then expect the Go build to find them via `CGO_LDFLAGS=-L${BuildDependsLibPath}`. The build script sets that env up at the start — make sure you're invoking it via `make <target>` rather than calling `go build` directly, otherwise `CGO_LDFLAGS` is empty and the link fails.

## Go binaries

### `error: cannot find section '.dynamic'` from `fix-patchelf`

Static Go binaries have no `.dynamic` section, so brewkit's default ELF-relocation pass fails. Disable it for the recipe:

```yaml
build:
  skip: fix-patchelf
```

This is safe specifically because pure-Go binaries don't need RPATH rewriting — they have no dynamic dependencies to relocate. Recipes that link cgo libraries should *not* use this.

### `module not found` in test step but build worked

The test sandbox starts in a clean `$HOME` without your build artifacts. Use absolute paths or place files explicitly into `{{prefix}}` during the build step; the test step only sees `{{prefix}}`.

## Rust / cargo

### `cargo:warning=cannot find -lssl` on linux

openssl-sys looks at `OPENSSL_DIR` / `OPENSSL_LIB_DIR` / `OPENSSL_INCLUDE_DIR`. But projects with bundled grpcio-sys / librocksdb-sys / libtitan-sys also run their *own* CMake configure, which reads `OPENSSL_ROOT_DIR` instead. Set all four:

```yaml
env:
  OPENSSL_DIR: '{{deps.openssl.org.prefix}}'
  OPENSSL_LIB_DIR: '{{deps.openssl.org.prefix}}/lib'
  OPENSSL_INCLUDE_DIR: '{{deps.openssl.org.prefix}}/include'
  OPENSSL_ROOT_DIR: '{{deps.openssl.org.prefix}}'
  PKG_CONFIG_PATH: '{{deps.openssl.org.prefix}}/lib/pkgconfig:$PKG_CONFIG_PATH'
```

### `error: SSL_get_peer_certificate undefined`

The project uses OpenSSL 1.1 APIs that were *renamed* in OpenSSL 3.0. pkgx's `openssl.org` recipe ships 3.x but configured `enable-deprecated`, so the old names still resolve. If you see this error, you're depending on a *different* openssl (system libs, a bundled one) — point your build at pkgx's.

### `error: linking with cc failed: rust-toolchain.toml pinned a channel I don't have`

The recipe needs `rust-lang.org/rustup` (not `rust-lang.org/rust`), and the build script should resolve the channel from `rust-toolchain.toml` and add the resolved toolchain's `bin/` to `$PATH`:

```yaml
script:
  - run:
      - ln -sf {{deps.rust-lang.org/rustup.prefix}}/bin/rustup rustup
      - rustup default "$(grep '^channel' $SRCROOT/rust-toolchain.toml | sed 's/channel = "//;s/"//')"
      - ln -sf $HOME/.rustup/toolchains/*/bin/* .
    working-directory: $HOME/.cargo/bin
```

## CMake projects with very old `cmake_minimum_required`

CMake 4.x dropped policy compatibility below 3.5. Vendored projects pinned to 2.x emit:

```
CMake Error: Compatibility with CMake < 3.5 has been removed from CMake.
```

Fix without patching the upstream `CMakeLists.txt`:

```yaml
env:
  CMAKE_POLICY_VERSION_MINIMUM: '3.5'
```

This is a CMake 4.x escape hatch — see also [`cmake`](./cmake).

## ELF / RPATH on Linux bottles

### `cannot create directory '/...': No such file or directory` during install

brewkit doesn't pre-create `{{prefix}}` before `build.script`. Your script must `mkdir -p "{{prefix}}"` itself before copying anything in (`install -D` handles this automatically).

### Bottle works in `~/.pkgx/...` but segfaults in `~/scratch/pkgx/...`

Loader is parsing an obsolete absolute path baked into the binary. Two common sources:

1. **`PT_INTERP` (the dynamic loader path) is absolute** and `$ORIGIN` substitution does not apply to it. brewkit's `fix-elf` currently rewrites RUNPATH but not PT_INTERP — if you bottle a libc, wrap the resulting binaries in shell scripts that re-exec with the bottle's interpreter.
2. **Linker scripts with baked-in absolute paths.** Glibc installs `libc.so` / `libm.so` etc as TEXT linker scripts (`GROUP ( /opt/.../lib/libc.so.6 ... )`) — those absolute paths break under relocation. Either strip the `+brewing` suffix post-install, or sed the prefix down to basenames and rely on `-L $LIBDIR -lc`.

### `fix-patchelf` writes an RPATH so long it crashes the loader

Seen during glibc bottling: brewkit chains 46 transitive dep prefixes into RPATH, including on `ld-linux-*.so.2` itself, which SIGSEGVs at startup. Use `build.skip: fix-patchelf` and handle relocation yourself (wrapper scripts pattern).

## Test step

### `... is not an explicit dependency!` warning followed by wrong-binary execution

brewkit's test sandbox auto-fetches commands invoked by bare name but not in `dependencies:`. The routing goes through `/opt/bin/pkgx`, which can resolve to a *different* binary than the one your build just installed (very common for cross-compilers and tools whose name collides with a different pantry package).

Fix: invoke own-bottle binaries by full path:

```yaml
env:
  MY_CLANG: "{{prefix}}/bin/x86_64-w64-mingw32-clang"
test:
  script:
    - '"$MY_CLANG" --version'
```

### `file: command not found` (or `man`, `which`, ...) in the test sandbox

Build sandbox has more PATH than test sandbox. If your test needs a basic utility, declare it as a runtime or test dependency, or in-line the check.

## `versions:` block

### `versions: github:` mode silently ignores `match:`

Only `ignore:` and `strip:` are honored in `github:` mode. To filter releases with a regex, use the `url:` variant pointing to the project's tag/release directory listing, with `match:` as the filter:

```yaml
versions:
  url: https://github.com/foo/bar/releases
  match: /v[0-9]+\.[0-9]+\.[0-9]+$/
  strip: /^v/
```

### `distributable: ~`

Yes, that literal null is valid and means "no source archive — fetch the payload in `build.script` yourself." Useful for binary-only packages (e.g. ziglang.org's prebuilt tarballs) where the canonical distribution is downloaded inside the script.

## Sub-packages and modularity

If the upstream project produces a heavy / optional component (Java SDK, gRPC bindings, GPU stack), do **not** silently drop it to make the bottle smaller. Either ship the full surface, or split into an explicit sub-package (e.g. `cubefs.io` + `cubefs.io/blobstore`, `openjdk.org` + `openjdk.org/v8`). Hidden trimming breaks user expectations and forces them back to system packages.

## Process: look at how the others did it

Before iterating ad nauseam on a build failure, check how nixpkgs / guix / homebrew / aur / debian / vcpkg handle the same package. Priority order, fastest signal first:

1. **nixpkgs** — `https://github.com/NixOS/nixpkgs/blob/master/pkgs/<category>/<pkg>/default.nix`. Most thorough wire-up; their stdenv handles the cc-wrapper/libstdc++ question we hit on many C++ projects.
2. **homebrew core** — `https://github.com/Homebrew/homebrew-core/blob/master/Formula/<pkg>.rb`. Brews are pinned to one specific macOS toolchain so they're less useful on Linux, but they document upstream's quirks (configure flags, env vars, patches).
3. **arch (AUR or official)** — `https://gitlab.archlinux.org/archlinux/packaging/packages/<pkg>` or `https://aur.archlinux.org/cgit/aur.git/tree/PKGBUILD?h=<pkg>`. PKGBUILDs are very readable shell.
4. **guix** — `https://github.com/guix-mirror/guix/tree/master/gnu/packages/`. Like nix but smaller surface; hermetic build matches pkgx's problem space.
5. **vcpkg / conan** — `https://github.com/microsoft/vcpkg/blob/master/ports/<pkg>/` (CMake-driven; helpful for C++ toolchain).
6. **debian** — `https://salsa.debian.org/.../<pkg>/-/tree/debian/`; the `debian/rules` + `debian/patches/` directory often holds the canonical fix for upstream bugs.

Even when their answer doesn't apply directly (different stdenv assumptions), reading their recipe surfaces: the upstream bug thread, which configure flags / patches to apply, and which deps actually link in.
cmake.md — CMake-specific gotchas (~90 lines)
# cmake

CMake projects are one of the largest single sources of new-recipe friction in the pantry — they bring their own toolchain logic that frequently fights pkgx's env. A short tour of what to know.

## Useful flags during debugging

* `-DCMAKE_VERBOSE_MAKEFILE=ON` — print every compile/link command. Almost always the first thing to add when a CMake build fails.
* `-DCMAKE_FIND_DEBUG_MODE=ON` — print every `find_package` / `find_library` lookup. Useful when a CMake project picks the wrong library version.
* `--trace-expand` (pass to `cmake` itself, not via `-D`) — every CMake-language line. Heavy output; pipe to a file.

## Old-CMake compatibility (`Compatibility with CMake < 3.5 has been removed`)

CMake 4.x dropped policy compatibility below 3.5. Vendored sub-projects with `cmake_minimum_required(VERSION 2.x)` will fail. Don't patch — set the policy-min override:

```yaml
env:
  CMAKE_POLICY_VERSION_MINIMUM: '3.5'
```

This is the upstream-supported escape hatch.

## `-DCMAKE_CXX_STANDARD=20` doesn't stick

Many projects declare their language requirement per-target, not globally:

```cmake
target_compile_features(mylib PUBLIC cxx_std_17)
```

Once `target_compile_features` is set, it **overrides** the global `CMAKE_CXX_STANDARD` for that target. Adding `-DCMAKE_CXX_STANDARD=20` to the configure line will appear to be ignored.

Fix paths, in order of preference:

1. **Make sure the libstdc++ headers exposed to the compiler are the version the project *expected*** — usually the right answer when the upstream itself works. See the C/C++ section of [Troubleshooting](./Troubleshooting) on `-isystem $gcc/include/c++/<v>`.
2. **Patch the `CMakeLists.txt`** to bump the `cxx_std_NN` (sparingly — see [Packaging Guide](./Packaging-Guide), "don't mess with the innards").
3. As a blunt fallback, `CXXFLAGS=-std=c++20`. This wins because `target_compile_features` doesn't override flags inherited from the environment — but it may cause other warnings/errors elsewhere in the project.

## Library lookup: `find_package` vs pkg-config

CMake's `find_package(OpenSSL)` consults `OPENSSL_ROOT_DIR` first, *not* `PKG_CONFIG_PATH`. If your recipe only sets `PKG_CONFIG_PATH`, vendored CMake builds will miss the pkgx version of the lib and may silently fall back to system libs (or fail).

Belt-and-braces:

```yaml
env:
  OPENSSL_ROOT_DIR: '{{deps.openssl.org.prefix}}'
  OPENSSL_INCLUDE_DIR: '{{deps.openssl.org.prefix}}/include'
  OPENSSL_LIB_DIR: '{{deps.openssl.org.prefix}}/lib'
  PKG_CONFIG_PATH: '{{deps.openssl.org.prefix}}/lib/pkgconfig:$PKG_CONFIG_PATH'
```

Same pattern with `<DEP>_ROOT_DIR` for any package that ships a `find_package` module.

## Out-of-source builds

CMake recommends out-of-source builds (`cmake -S . -B build`). The brewkit `build.script` already runs inside a sandboxed source tree, so the safest pattern is to use a subdirectory under it:

```yaml
- run: |
    cmake -S . -B build \
      -DCMAKE_INSTALL_PREFIX="{{prefix}}" \
      -DCMAKE_BUILD_TYPE=Release \
      -DBUILD_SHARED_LIBS=ON
    cmake --build build --parallel {{hw.concurrency}}
    cmake --install build
```

`{{hw.concurrency}}` is the pkgx moustache for `$(nproc)` — see [Structure of a `package.yml`](./Structure-of-a-%60package.yml%60).

## Cross-platform install prefix

Always use `-DCMAKE_INSTALL_PREFIX="{{prefix}}"` rather than relying on whatever CMake auto-detects — CMake will install to `/usr/local` by default, which is *not* `{{prefix}}` and bottling will fail with "no install artifacts under prefix".

## RPATH handling on Linux

CMake bakes `CMAKE_INSTALL_PREFIX/lib` into RPATH at install time by default — but if your project sets `CMAKE_SKIP_BUILD_RPATH` or `CMAKE_INSTALL_RPATH_USE_LINK_PATH OFF`, RPATH may be empty and brewkit's `fix-patchelf` pass has to do all the work. Usually fine; if a binary fails with "library not found" at install-test time:

```yaml
env:
  CMAKE_INSTALL_RPATH: '$ORIGIN/../lib'
  # or pass via -DCMAKE_INSTALL_RPATH='$ORIGIN/../lib'
```

makes the bottle relocatable on Linux.

## See also

* [Troubleshooting](./Troubleshooting) — generic failure patterns
* [Structure of a `package.yml`](./Structure-of-a-%60package.yml%60) — moustache and env reference
* [Examples](./Examples) — `facebook.com/zstd` is the canonical CMake recipe
Specifics.md — per-language notes: Go / Rust / Bazel / Maven / Node / Python, self-bootstrapping toolchains, sub-package pattern (~70 lines)
# Specifics

Per-language and per-toolchain notes. For generic failure patterns see [Troubleshooting](./Troubleshooting).

## Build systems

* [`cmake`](./cmake) — CMake 4.x policy compat, `target_compile_features` vs `CMAKE_CXX_STANDARD`, `find_package` lookup paths.

## Languages / runtimes

### Go

- Static Go binaries have **no `.dynamic` section** — brewkit's `fix-patchelf` pass will fail. Disable it for pure-Go recipes:
  ```yaml
  build:
    skip: fix-patchelf
  ```
  Don't add this for cgo recipes; their binaries are dynamic and need RPATH rewriting.
- Vendored modules: pass `-mod=vendor` or rely on `GOFLAGS=-mod=vendor` if the upstream ships a `vendor/` tree.
- Multi-binary projects almost always have a `cmd/` subdirectory with one folder per binary; `go build ./cmd/...` builds all of them. Match that surface — don't silently trim.
- For ldflags-injected version strings: `-ldflags="-X main.version={{version.raw}}"` is the standard idiom.

### Rust

- Use `rust-lang.org/rustup` (not `rust-lang.org/rust`) when the project pins a specific toolchain via `rust-toolchain.toml`. The recipe should resolve the channel and prepend `$HOME/.rustup/toolchains/*/bin/` to `$PATH`. See [Troubleshooting](./Troubleshooting) for the snippet.
- `openssl-sys` reads `OPENSSL_DIR` / `OPENSSL_LIB_DIR` / `OPENSSL_INCLUDE_DIR`. Bundled CMake sub-builds (gRPC, RocksDB, Titan) read `OPENSSL_ROOT_DIR` instead. Set all four.
- For projects with bundled C++ deps via cargo build scripts (TiKV, RisingWave), `cmake.org` and `llvm.org` are usually required build deps even though Cargo handles the orchestration.

### Bazel

- Bazel builds are large and slow to download dependencies. The brewkit build sandbox needs `--repository_cache=/tmp/bazel-cache` (or similar) to avoid re-fetching on each iteration during development.
- For projects that build LLVM-derived binaries (Envoy, gRPC C++), the `--config=libc++` flag plus the `-isystem` pattern from [Troubleshooting](./Troubleshooting) C++ section is usually the right combination.

### Maven / Java

- pkgx ships `openjdk.org` (current) and `openjdk.org/v8` (legacy). Use the lowest version that actually works.
- `apache.org/maven` is the standard build tool.
- The `JAVA_HOME` env var must be set in `runtime.env` so that user-launched commands find the JDK:
  ```yaml
  runtime:
    env:
      JAVA_HOME: '{{prefix}}'
  ```

### Node.js / npm

- Use `nodejs.org` for the runtime, not `npmjs.com/cli`. The latter is bundled with node.
- For `corepack`-enabled projects (yarn 2+, pnpm), the recipe can call `corepack enable` then `corepack prepare yarn@<version> --activate`. Avoid global installs.

### Python

- Use `python.org` for the interpreter. `python.org/pip` and `pypa.github.io/virtualenv` exist for the bootstrap dance.
- For C extensions, `python.org` exposes its include and lib dirs via standard pkg-config. The python-dev headers are part of the main bottle, not a separate `-dev` package.

## Self-bootstrapping toolchains

- **OpenJDK**: builds JDK N from source using a previously-built JDK N or N+1 as the boot JDK. For pkgx, fetch the boot JDK from Adoptium/Temurin inside `build.script` (it's not a pkgx package). See `openjdk.org` recipe for the pattern (the `boot-jdk` working-directory + tarball-fetch + strip-components dance).
- **GCC**: similar — builds gcc N from a gcc M (where M can be N or older). The pantry's `gnu.org/gcc/v8` is the canonical example of forking a legacy version into its own recipe rather than parameterizing the main one.

## Sub-package modularity

When a project has an optional heavy component (Java SDK alongside a C library, GPU bindings, GUI toolkit), prefer a sub-package recipe over silently omitting the component. Examples in-tree:

- `openjdk.org` + `openjdk.org/v8` — current vs legacy
- `gnu.org/gcc` + `gnu.org/gcc/v8` — current vs legacy
- `tcl-lang.org` + `tcl-lang.org/v8` + `tcl-lang.org/tk` + `tcl-lang.org/tk/v8` — split per language and per major-version generation

This keeps each recipe focused and surfaces the modularity to users (they `pkgx install cubefs.io/blobstore` explicitly, rather than discovering after the fact that the main bottle quietly lacks it).
Packaging-Guide.md — extended (existing tips kept verbatim) + "check how other packagers solved it" + ship-the-full-upstream-surface guideline + when-to-fork-vN sub-recipes (~70 lines)
# Packaging Guide

## Tips

- Consider the build a black box (as much as possible)
    - Only change inputs (configure args)
    - If it does things wrong that can't be changed with inputs, modify the outputs
    - Don't mess with the innards
    - Rationale: the maintainer will update the innards and your hacks will break
    - Rationale: we don't know much about the innards — changes will have unforeseen consequences

## Before writing the recipe

**Look at how other packagers solved the same problem.** Reading another distro's recipe is almost always faster than re-deriving the right configure flags by trial and error. Priority order (fastest signal first):

1. **nixpkgs**`https://github.com/NixOS/nixpkgs/blob/master/pkgs/<category>/<pkg>/default.nix`. Most thorough wire-up; their stdenv handles the cc-wrapper / libstdc++ question we hit on many C++ projects.
2. **homebrew core**`https://github.com/Homebrew/homebrew-core/blob/master/Formula/<pkg>.rb`. Brews are pinned to one specific macOS toolchain so they're less useful on Linux specifics, but they DO document the upstream's quirks (configure flags, env vars, patches).
3. **arch (AUR or official)**`https://gitlab.archlinux.org/archlinux/packaging/packages/<pkg>` or `https://aur.archlinux.org/cgit/aur.git/tree/PKGBUILD?h=<pkg>`. PKGBUILDs are very readable shell.
4. **guix**`https://github.com/guix-mirror/guix/tree/master/gnu/packages/`. Like nix but smaller surface; hermetic build matches pkgx's problem space.
5. **vcpkg / conan**`https://github.com/microsoft/vcpkg/blob/master/ports/<pkg>/` (CMake-driven; helpful for C++ toolchain).
6. **debian**`https://salsa.debian.org/.../<pkg>/-/tree/debian/`; the `debian/rules` + `debian/patches/` directory often holds the canonical fix for upstream bugs.

Even when their answer doesn't apply directly (different stdenv assumptions), reading their recipe surfaces:

1. The upstream bug thread they linked to
2. Which configure flags they pass
3. Which patches they keep (`debian/patches/*.patch`, `nixpkgs`'s `patches = [ ... ]`)
4. Which deps they actually link against (sanity-check your `dependencies:` block)

## Ship the full upstream surface

A pantry recipe should produce a bottle equivalent to a from-source upstream build. **Don't silently drop binaries** to make the bottle smaller or the build simpler — a user installing `cubefs` expects to get `cfs-server`, `cfs-client`, blobstore, the libsdk, etc., not a hand-picked subset.

If a component is genuinely broken and can't be fixed in the current iteration, leave a comment in the recipe explaining what's wrong and how to re-enable it. If a component must be excluded long-term (license restriction, build infeasible), **split it into a sub-package** — see [Specifics: Sub-package modularity](./Specifics#sub-package-modularity) for the in-tree examples.

Default position when adding a new recipe: enumerate every binary the upstream produces (`make help`, scan `cmd/` directories, read the upstream's `INSTALL`/`BUILDING.md`) and ship them all unless there's a hard blocker.

## When to fork into a `package-name/vN` sub-recipe

In-tree examples of legacy/current splits:

- `openjdk.org` (current) + `openjdk.org/v8` (legacy LTS)
- `gnu.org/gcc` (current) + `gnu.org/gcc/v8` (legacy, kept for ABI compat with old C++)
- `tcl-lang.org` (current 9.x) + `tcl-lang.org/v8` (legacy 8.6, pinned by Tk and git-gui)

Use this pattern when:

- The legacy version is **pinned by other recipes** in the pantry (you can't drop it without breaking dependents).
- The build flags, source layout, or configure surface differs enough that mixing them into a single recipe would clutter it heavily.
- An LTS or distro-pinned older version is in use by a sizeable user base (JDK 8 for Hadoop ecosystem, GCC 8 for legacy C++).

Don't fork when:

- The version difference is a minor build flag change (use moustaches or `if:` predicates on script items).
- The version isn't a dependency of anything (just bump the recipe).

## Self-bootstrapping toolchains

For toolchains that build themselves (gcc, openjdk), the boot stage usually fetches a prebuilt earlier-version tarball from upstream. Inside `build.script`:

- Pick a CI-pinned version of the boot toolchain (don't rely on `latest` — upstream tag layouts shift).
- Strip the archive into a single canonical layout (`bin/cc` at a known relative path) so cross-OS scripts work uniformly. On darwin Temurin tarballs nest `bin/` under `Contents/Home/`; `--strip-components=4` flattens that to the same shape as the Linux tarball.

See `openjdk.org/v8` for the canonical pattern.

## CI and test discipline

- Test step must actually exercise the binary, not just `--version` (which can pass even on broken bottles). For tools, run a one-line operation. For daemons, `--help` is fine but pair it with a smoke-test of the bundled config validator.
- Invoke own-bottle binaries by full path via an env var (`"$MY_BIN" ...`) — bare-name invocation goes through the pkgx resolver which can pick a *different* binary than the one you just installed. See [Troubleshooting: Test step](./Troubleshooting#test-step).
- Verify shared libraries by `nm -D --defined-only <libfoo.so> | grep -q <known_symbol>``--version` doesn't work for libs.

Sourcing

Most of the content comes from concrete debug sessions on my own recent PRs (binaryen, TiKV, cubefs, openjdk-v8, valgrind, the gcc-libc-wrapper work, etc.). Where I learned a pattern from another packager (nixpkgs' cc-wrapper, Arch's PKGBUILD for TiKV, etc.) I credit them. The "check how others did it" methodology has saved me hours and is worth promoting as a first-class part of the contributor flow.

Out of scope (suggestions for follow-up)

Things I noticed but didn't draft for this issue:

  • The existing Home.md is good as-is. Could link to the new Troubleshooting/cmake/Specifics from the sidebar.
  • Examples.md could use one or two more "complex case" examples (vendored C++ deps, cgo+rocksdb, self-bootstrapping toolchain) — let me know if you want a follow-up issue for those.
  • Building-for-Your-Language.md is currently very thin; it could absorb the per-language content I put in Specifics.md instead, if you prefer that split.

Happy to iterate.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions