Thanks for your interest in Perry. This document covers everything you need to land a PR — what to build, how to test, and what the review flow looks like.
For building and installing Perry as a user, see README.md. For runtime/compiler architecture, see CLAUDE.md. This file is about contributing, not using.
All of these are welcome and none are ranked:
- Fix a bug. Check the issue tracker, especially
good first issueandhelp wanted. - Close a TypeScript parity gap. The gap test suite tracks divergences from
node --experimental-strip-types; CLAUDE.md's "TypeScript Parity Status" table is the current scoreboard. - Add a platform backend or widget. See the "To add a new widget" and "Native UI" sections in CLAUDE.md.
- Improve docs or examples. Everything under
docs/src/anddocs/examples/is fair game. - Report an issue. File one even if you don't have a fix — reproducers are themselves a contribution.
Not sure where to start? Open a Discussion and describe what you'd like to work on; we'll help you scope it.
Prerequisites match what CI installs — see .github/workflows/test.yml for the authoritative list. In short:
| Component | Version | Needed for |
|---|---|---|
| Rust | stable (≥ 1.94) | Everything. The workspace pins no toolchain, so an older stable fails deep in the dependency graph with a sqlx@0.9.0 requires rustc 1.94.0 MSRV error rather than a clear message — rustup update stable fixes it. |
Rust nightly + rust-src |
latest | tvOS / watchOS cross-compile only (-Zbuild-std) |
| Node.js | see .node-version |
Parity tests (run_parity_tests.sh). Use that exact version — an older Node makes the harness classify tests node_fail and silently DROP them from the gate instead of failing. |
| C linker | any | Linking compiled binaries (xcode-select --install / build-essential / MSVC) |
| libclang | any | bindgen, via the libsqlite3-sys build script. Without it the build dies with Unable to find libclang. Debian/Ubuntu: libclang-dev; Fedora: clang-devel; Arch: clang. If it lives somewhere non-standard, point at it with LIBCLANG_PATH=/path/to/dir (and, if bindgen then can't find stdarg.h, BINDGEN_EXTRA_CLANG_ARGS="-isystem /path/to/clang/<ver>/include"). |
| clang | ≥ 15 | Perry's own LLVM codegen shells out to clang -c. Separate from the linker above — see the installation guide. perry doctor verifies it. |
Platform-specific extras — only required if you're touching that backend:
- macOS UI (
perry-ui-macos): Xcode Command Line Tools - iOS / tvOS / watchOS: Xcode + matching SDKs;
rustup target add aarch64-apple-ios-sim - Android (
perry-ui-android): Android NDK ($ANDROID_NDK_HOMEor$ANDROID_HOME/ndk/*);rustup target add aarch64-linux-android - Linux UI (
perry-ui-gtk4):libgtk-4-dev libadwaita-1-dev libpulse-dev pkg-config(Xvfb for headless UI tests) - Windows UI (
perry-ui-windows): MSVC (ilammy/msvc-dev-cmdor avcvars64.batsession)
Build the default (platform-independent) workspace and run the affected crates' fast unit tests:
cargo build --release
./scripts/test_affected_crates.sh --base origin/mainThe runner includes committed, staged, and unstaged paths relative to the base
revision; stage new files before running it because untracked paths are ignored.
Use --dry-run to inspect its per-crate Cargo commands, or set
PERRY_TEST_BASE to change the default base. It mirrors CI's
scripts/ci_test_scope.py selection and unit-target filtering, including the
single-threaded perry-runtime run. It deliberately skips integration tests
under crates/*/tests/; CI runs those through its scoped and nightly jobs.
Never replace it with cargo test --workspace: Perry's per-crate feature sets
must remain isolated.
For a faster inner loop, cargo check -p perry (correctness only) or
cargo build --profile perry-dev -p perry (optimized local dev build). The
.a static archives now come from dedicated wrapper crates — build them with
cargo build --release -p perry-runtime-static -p perry-stdlib-static when you
need libperry_{runtime,stdlib}.a. See docs/src/contributing/building.md for
the full dev/release/dist taxonomy and the slim --features dev-cli CLI (#5422).
The full README Development section has more cargo run recipes (HIR dumps, per-crate rebuilds).
For fixture-only parity reruns, PERRY_SKIP_BUILD=1 ./run_parity_tests.sh
reuses PERRY_BIN and the matching runtime/stdlib archives from
PERRY_RUNTIME_DIR (or the binary's directory) with auto-optimization disabled.
Rebuild instead whenever Rust or Cargo inputs, or compiler, runtime, stdlib, or
required extension sources differ; reuse does not prove that prebuilt artifacts
match the checkout.
- One logical change. Small PRs land faster. If you find yourself writing "also, I fixed …", split.
- Tests. New behavior needs a test; a bug fix needs a regression test. For compiler changes, drop a
.tsfile undertest-files/exercising the path. For runtime/stdlib,#[test]in the relevant crate. - Docs where user-visible. New CLI flags, new perry.toml fields, new stdlib APIs → update
docs/src/.
- Do not bump
[workspace.package] versioninCargo.toml. - Do not edit
**Current Version:**at the top ofCLAUDE.md. - Do not add a "Recent Changes" entry to
CLAUDE.mdorCHANGELOG.md.
Perry releases frequently — often several patch versions between a PR being opened and merged. If contributors bump the version on their branch, those bumps conflict on merge day through no fault of the contributor. The maintainer folds version + changelog metadata in when landing your PR, so it always matches the actually-shipped release.
We loosely follow Conventional Commits — not enforced, but you'll see feat: / fix: / docs: / chore: / refactor: prefixes in the log. Match that style and your PR reads better in the changelog. A good commit message answers "why" more than "what"; the diff already shows what.
Comment on the issue saying you'd like to take it. We'll assign it to you. If you go quiet for a week or two, we may un-assign to let someone else pick it up — no hard feelings, just keeping the board moving.
Most red PRs come from gates that cargo build never looks at — a file
that crossed the 2000-line cap, fmt drift, or an unbarriered GC store site.
Run the fast, non-compiling subset with:
./scripts/pre-tag-check.sh --quickThat is every gate in the lint job that doesn't compile anything, so it
costs seconds. Drop --quick to add the docs linter, cargo check, and
Clippy; it also runs cargo deny when installed. The script prints every
failure in one pass rather than one per push.
Behavioral changes (HIR, codegen, runtime) also need the conformance suite, which diffs compiled programs byte-for-byte against Node:
./scripts/run_gap_tests.sh # 401 tests; ~1h serially
./scripts/run_doc_tests.sh # Compile + run every docs/examples/*.tsUse the Node version pinned in .node-version, not whatever you have.
Node is the oracle the suite diffs against, so its version is a correctness
input: on a Node too old for a test, node fails, the harness classifies the
test node_fail, and it is dropped from the run instead of going red. CI sat
on Node 22 while the suite grew Node 24/26 features and hid 14 tests that way.
Any PR touching crates/ also needs a changelog.d/<PR-number>-<slug>.md
fragment; see changelog.d/README.md.
UI doc-tests launch real windows. On headless hosts, wrap in xvfb-run -a (Linux) or rely on PERRY_UI_TEST_MODE=1 which auto-exits after one frame.
- Bug: open an issue — the template asks for Perry version, target, host OS, and a minimal repro.
- Feature request: open an issue with the "feature request" template.
- Open-ended question, design discussion, "is Perry right for my use case": start a Discussion.
- Security issue: email
ralph@skelpo.comdirectly; please don't file a public issue for anything exploitable.
Participation is governed by the Code of Conduct — tl;dr: be kind, assume good faith, disagree in the open on technical merits. Reports go to ralph@skelpo.com.
Perry is MIT-licensed. By contributing, you agree your contributions are licensed under the same terms. We do not require a CLA or DCO sign-off.