A monorepo for AWS Lambda custom runtimes, with PyPy as the first implemented runtime.
The repository is now organized around runtime packages under runtimes/ and shared tooling under tools/.
- Runtime-specific code, checksums, examples, and release metadata live under
runtimes/<runtime-id>/ - Shared build, audit, publish, and local-test entrypoints live under
tools/bin/ - Runtime metadata is declared in
runtimes/<runtime-id>/runtime.json - GitHub Actions uses the runtime manifest list as the source of truth for CI and release matrices
runtimes/
pypy311/
runtime.json
bootstrap/
helpers/
checksums/
examples/
tools/
bin/
runtime_lib/
.github/workflows/
Implemented runtimes:
pypy311— PyPy 3.11 (portable Linux builds from python.org)graalpy312— GraalPy 3.12graalpy313— GraalPy 3.13bun13— Bun 1.3.xbun14— Bun 1.4.xdeno29— Deno 2.9.x (provided.al2023layer; bootstrap runsdenowith sandbox flags for the Runtime API)go126— Go 1.26.x toolchain layergo127— Go 1.27.x toolchain layerrust194— Rust 1.96.x musl toolchain layerrust198— Rust 1.98.x musl toolchain layer
Runtimes for line-based frameworks (bun/deno/go/rust) are named <framework><major><minor> and each tracks a single version_line (e.g. go126 tracks 1.26). Minor-line updates create a new runtime rather than mutating an existing one; patch updates stay on the same runtime.
The shared tooling is intentionally runtime-agnostic so additional runtimes can be added without reworking the root build and release flow.
Each runtime package declares its build and release contract in runtime.json, including:
- runtime family
- runtime version or distribution identifier
- any overrides to the family defaults
Most runtime details are now derived from runtime-family defaults in tools/runtime_lib/runtime_manifest.py. For runtimes that follow an established family layout, the manifest can stay very small.
Use the manifest tooling from the repo root:
make list-runtimes
python3 tools/runtime_lib/runtime_manifest.py validateSupported build environment:
- Linux or WSL is required for build and release commands
- The shared tooling assumes native Linux tools such as
bash,tar,zip,unzip,curl,sha256sum, andmake - Transient build work is staged under
${BUILD_ROOT:-$RUNNER_TEMP}or/tmpto avoid slow cross-OS archive operations- release and CI artifacts are built from temp storage by default
- set
EXPORT_ARTIFACT_DIR=/some/pathif you want a copy of the final zip preserved outside temp storage
Build a specific runtime:
make build RUNTIME=pypy311
make build RUNTIME=deno29Audit a built runtime:
make audit RUNTIME=pypy311Upload and publish a runtime layer:
make upload RUNTIME=pypy311
make publish RUNTIME=pypy311Publish and publicize a runtime layer:
make publicize RUNTIME=pypy311List the latest published layer versions:
make latest RUNTIME=pypy311Each runtime can carry its own local SAM assets. For PyPy they live under:
runtimes/pypy311/examples/sam/template.local.example.yamlruntimes/pypy311/examples/sam/events/hello.jsonruntimes/pypy311/examples/sam/hello/Makefile
Run the local smoke test from the repo root:
make local-build RUNTIME=pypy311
make local-invoke RUNTIME=pypy311This flow:
- builds the runtime package under temp storage
- expands the local layer under temp storage
- renders a temp SAM template with resolved local paths
- runs
sam build --use-containerwith a temp SAM build directory - assembles a temp invoke bundle for
sam local invoke
Requirements:
- Linux or WSL is required for Docker-backed SAM workflows
- For best local SAM performance, keep the repo on the WSL filesystem instead of
/mnt/c/... - Docker must be available
sammust be installed in the environment where you run the commands
The repo includes:
.github/workflows/ci.ymlrepo-checks(once per workflow): manifest validation, shell syntax, Python compile per runtimeruntime-checks(matrix): build, Grype audit, local SAM smoke tests on x86 when enabled- caches: upstream runtime downloads, Grype binary (
v0.98.0) and DB, pip forrequirements-ci.txt(SAM jobs only) - SAM x86 jobs pre-pull
public.ecr.aws/sam/build-provided.al2023:latestbeforesam build --use-container - workflow artifacts: built zips upload on push to
mainonly,retention-days: 7(PRs do not upload) - superseded PR runs cancel via workflow concurrency
.github/workflows/release-runtime.yml- manual runtime-scoped release flow
- rebuild, audit, upload, and publish steps
- same download and Grype caches as CI (no SAM)
The release workflow expects an AWS role secret named AWS_RELEASE_ROLE_ARN.
CI pins aws-sam-cli in requirements-ci.txt for reproducible SAM installs.
- Create a new runtime directory under
runtimes/<runtime-id>/. - Add a
runtime.jsonmanifest with artifact, Lambda, release, and local test metadata. - Add the runtime bootstrap, helper package, checksum file, and examples under that directory.
- Run:
python3 tools/runtime_lib/runtime_manifest.py validate --runtime <runtime-id>
bash tools/bin/check-runtime <runtime-id>
make build RUNTIME=<runtime-id>- Add or adapt runtime-specific examples under
runtimes/<runtime-id>/examples/. - Add or update CI expectations if the runtime needs extra validation steps beyond the shared defaults.
For bun/deno/go/rust, when an upstream release lands on a version line newer than
any tracked runtime, the check command reports it in its new_lines output and
bump-latest automatically creates the new runtime directory:
python3 tools/runtime_lib/bump_version.py check --json
# {"outdated": {...}, "new_lines": {"go-toolchain": {"id": "go127", "line": "1.27", "version": "1.27.0"}}}
python3 tools/runtime_lib/bump_version.py bump-latestThe new runtime is cloned from the newest existing runtime of the same family, its
runtime.json is rewritten for the new version_line, and checksums are fetched from
upstream. The weekly /check-runtime-updates workflow opens a single PR covering both
same-line patch bumps and new-line additions.
PyPy works the same way but its line is the Python version, so a new Python line is a
new runtime (pypy311 → pypy312):
python3 tools/runtime_lib/bump_version.py check --json
# "new_lines": {"portable-pypy": {"id": "pypy312", "line": "3.12", "version": "pypy3.12-v8.0.0"}}Only Python lines newer than the newest tracked one are proposed — upstream's older lines (2.7, 3.6–3.10) are never back-filled.
Without a bound the build matrix grows one cell per new line forever. roll-off keeps the
newest N lines per family and marks the rest deprecated:
make roll-off # report only (default: keep the newest 2 lines per family)
make roll-off ARGS="--keep 1" # see what a tighter policy would roll off
make roll-off ARGS="--apply" # write "deprecated": true
make roll-off ARGS="--apply --prune" # also delete the rolled-off directoriesDeprecated runtimes are dropped from manifest_matrix() (so CI stops building them) and
from check/bump-latest, but their files stay until --prune and they can still be
built on demand with make build RUNTIME=<id>. Pruning refuses to empty a whole family.
bump-latest treats every runtime independently. If one runtime cannot be updated —
an upstream archive was renamed or removed, a transient network error, an unexpected
tag format — that runtime is skipped and reported, and the remaining runtimes are still
bumped and committed:
pypy311: pypy3.11-v7.3.23 -> pypy3.11-v8.0.0
!! Failed to bump pypy311: RuntimeError: GET ... failed: HTTP 404 Not Found
...
1 runtime(s) could not be updated:
- pypy311: RuntimeError: GET https://downloads.python.org/pypy/pypy3.11-v8.0.0-linux64.tar.bz2 failed: HTTP 404 Not Found
The command exits non-zero when any runtime failed, so CI is red; the weekly workflow
still opens the PR for the runtimes that succeeded and lists the skipped ones under
Skipped Runtimes in the PR body. Pass --failures-json <path> to write a
machine-readable report of the failures.
No partial state is written: a runtime's checksums are resolved from upstream before
its runtime.json or checksum file is touched, and a new runtime directory is removed
again if any step fails.
bun/deno/graalpyuse the GitHub releases API, authenticated withGITHUB_TOKEN/GH_TOKENwhen present (avoids the unauthenticated 60 req/h limit) and paginated so a tracked line's newest patch is not missed behind pre-release tags.gousesgo.dev/dl/?mode=json;rustuseschannel-rust-stable.toml.pypyusesdownloads.python.org/pypy/versions.json. The newest release is chosen by numeric version (not list order), and the archive filename — including its extension — is read from that index, because PyPy has changed it between releases (.tar.bz2→.tar.gzat 8.0.0). The resolved extension is persisted asarchive_extin the runtime manifest so the build downloads the real upstream file. PyPy publishes no checksum index, so the archive is downloaded once and hashed.
The first runtime package, pypy311, still ships the hardened Lambda Runtime API implementation and the helper package for:
- structured logging
- init hooks for Provisioned Concurrency style warm-up
- optional X-Ray helper utilities
Apache 2.0