feat(lambda-python-alpha): opt-in local (docker-less) bundling for PythonFunction#37858
Open
CoreOxide wants to merge 10 commits into
Open
feat(lambda-python-alpha): opt-in local (docker-less) bundling for PythonFunction#37858CoreOxide wants to merge 10 commits into
CoreOxide wants to merge 10 commits into
Conversation
added 6 commits
May 11, 2026 17:44
aws-cdk-automation
previously requested changes
May 13, 2026
✅ Updated pull request passes all PRLinter validations. Dismissing previous PRLinter review.
2 tasks
Contributor
|
Comments on closed issues and PRs are hard for our team to see. |
Contributor
|
This issue has been reopened and is now available for discussion. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #18290.
Motivation
PythonFunction/PythonLayerVersioncurrently require a running Docker daemon to bundle dependencies: the module shells out todocker buildon every synth to construct a Lambda-compatible image and runspip installinside it. That's a hard blocker for:cdk synthdominates wall time.python3+piponPATHand just want the bundler to use it (i.e. caching of results).This PR adds an opt-in
bundling.local: booleanflag that, when enabled, installs dependencies directly on the host viapip, producing a binary-compatible artifact for the target Lambda runtime regardless of the host OS / CPU architecture.This feature also allows cross building, and can be expanded to support any and all runtimes, such that a singular runner can build Python, Golang and JS functions.
Design
The opt-in surface
Defaults to
false, so existing apps are unchanged — no feature flag needed.manulinuxtags also have defaults for each Python version.How local bundling works
LocalBundling.tryBundle(outputDir)performs:pipenv/poetry/uv, fail fast withPythonLocalBundlingWindowsExportUnsupported(the export commands embed POSIX&&/rm -rf/>redirection that doesn't survivecmd.exe). Plainrequirements.txtstill works on Windows. Support can be added if demand arises, in a subsequent PR.python3first, fall back topython. Cached at module scope so N functions in a stack do one probe.FileSystem.copyDirectory(reuses the existing CDK ignore-strategy) — always follows symlinks, honoursassetExcludes.beforeBundlinghooks withcwd=outputDir— hooks operate on the staged copy, never on the user's source tree. All hook strings joined with&&into a single shell invocation to match the documentedICommandHookscontract.Packaging.fromEntry(...)result is computed once in theBundlingconstructor and threaded intoLocalBundlingso Docker and local paths can't disagree on which packaging tool to use (avoids a TOCTOU race between constructor time andtryBundletime).pip installwith:--platform manylinux_2_28_{arch}thenmanylinux2014_{arch}for py3.12+ (AL2023 base image),manylinux2014_{arch}alone for py3.7–3.11 (AL2 base image).--python-version X.Y --abi cpXY --implementation cpderived fromRuntime.--only-binary=:all:so source-only distributions (which would build against the host OS/arch) are rejected rather than silently producing an incompatible artifact. A sweep test asserts this flag rides on every runtime in the matrix.afterBundlinghooks.If local bundling fails for any reason (missing tools, no compatible wheel, hook error) synthesis fails — there is no silent fallback to Docker. That's by design and documented in the README.
Docker-image short-circuit
The
Bundlingconstructor previously calledDockerImage.fromBuild(...)unconditionally, which shells out todocker buildat synth time. That madelocal: trueuseless without Docker (exactly the case it's supposed to cover). Now, whenlocal === trueand the user hasn't supplied a custom image, we useDockerImage.fromRegistry(runtime.bundlingImage.image)— a lazy factory that never touches the daemon. Covered by a regression test.Default manylinux tag mapping
Derived from the runtime's Lambda base image:
manylinux2014_{arch}manylinux_2_28_{arch}, falling back tomanylinux2014_{arch}Users can override with
manyLinuxTags— e.g. a dependency only published asmusllinux→ supply['musllinux_1_2_x86_64', 'manylinux_2_28_x86_64']and pip will try each in order.Error model
All new failures surface as
UnscopedValidationErrorwithlit-tagged codes per the repo's error-handling convention:PythonLocalBundlingPythonMissingPythonLocalBundlingCommandFailed(pip / export / hook subprocess failures — stderr/stdout captured in message)PythonLocalBundlingWindowsExportUnsupportedPythonLocalBundlingUnsupportedArchitecturePythonLocalBundlingUnsupportedRuntimePythonLocalBundlingUnparseableRuntimeWhat didn't change
BundlingOptionsgets two new optional props (local,manyLinuxTags); no public API breakage.Alternatives considered
dockerand pick local when Docker is absent. Rejected — too much magic, and host Python/pip versions vary widely; explicit opt-in is safer.local: boolean | ILocalBundlingunion. Rejected — jsii doesn't allow union types in public props. Users who want to fully customize the local path can still wrapBundlingthemselves.How I verified this works
Unit tests — 97 new tests, 98.57% coverage
test/local-bundling.test.ts(40 tests) — covers AL2 & AL2023 tag paths, both arches, the python3/python fallback,&&-joined hooks,cwd=outputDirfor both hooks, post-copy ordering, the Windows export guard, themanyLinuxTagsoverride, all six error codes, and thepip installinvariant--only-binary=:all:that runs as part of the runtime sweep.test/platform.test.ts(26 tests) —runtimeToPythonVersion/runtimeToAbiTag/defaultManyLinuxTags/validateArchitectureacross py3.6–3.14.test/bundling.test.ts— added 4 wiring tests:local: trueexposes a local bundler with atryBundlefunctionlocal: truedoes not triggerDockerImage.fromBuild(regression for the bug below)local: false/ default leaveslocalundefinedIntegration test —
test/integ.function.local.ts, deployed and invoked11-cell matrix deployed to
us-east-1and each function invoked viainteg.assertions.invokeFunction, expectingPayload: '200':manyLinuxTagsoverridemusllinux_1_2_aarch64first,manylinux_2_28_aarch64fallback)Last run: 1 passed, 242s. Snapshot checked in at
test/integ.function.local.js.snapshot/. The integ file carries/// !cdk-integ pragma:disable-update-workflowandstackUpdateWorkflow: false, matching the sibling integ tests in this module (python bundling asset hashes change frequently and the snapshot would otherwise churn).End-to-end smoke (off-PR, macOS arm64 host)
Built a standalone CDK app outside the repo with a
requests-using handler,yarn link-ed the alpha andaws-cdk-libin, and rancdk synthfor every matrix cell. Each produced a distinct bundled asset. For each, I verified:docker psintentionally failed on the host)..sofiles carried the correct ABI + arch tag (e.g.*.cpython-312-aarch64-linux-gnu.sofor py3.12+arm64).manyLinuxTags: ['musllinux_1_2_aarch64', 'manylinux_2_28_aarch64'],charset_normalizerwas selected as the musl wheel (charset_normalizer/cd.cpython-312-aarch64-linux-musl.so) while other deps fell through to the manylinux wheel — the tag-priority fallback works as documented.Failure mode spot-checks
Exercised against the real host (not just mocked
spawnSync) to confirm the error surface matches the unit tests:pysqlcipher3) →No matching distribution foundfrom pip, surfaced viaPythonLocalBundlingCommandFailed.python3/python) →PythonLocalBundlingPythonMissingwith both candidates listed.PythonLocalBundlingCommandFailedwith the pipenv command in the message.os.platformstub returningwin32on a pipenv project →PythonLocalBundlingWindowsExportUnsupported.Architecture.custom('weird', 'linux/mips64')→PythonLocalBundlingUnsupportedArchitecture.Build & lint
yarn buildunderpackages/@aws-cdk/aws-lambda-python-alpha— clean (jsii, eslint, awslint, pkglint).yarn test— all non-Docker-dependent tests pass.Files changed at a glance
New:
lib/local-bundling.ts—LocalBundlingimplementation ofILocalBundling.lib/platform.ts— runtime/architecture helpers (runtimeToPythonVersion,runtimeToAbiTag,defaultManyLinuxTags,validateArchitecture).lib/util.ts—runCommandwrapper aroundspawnSyncthat surfaces stdout/stderr in the validation error.test/integ.function.local.ts+ snapshot.test/local-bundling.test.ts,test/platform.test.ts.Modified:
lib/bundling.ts— precomputesPackaging+ excludes once; wiresLocalBundling; short-circuits the Docker image build whenlocal: true.lib/types.ts— addslocalandmanyLinuxTagsprops with full JSDoc.test/bundling.test.ts— wiring tests.README.md— newLocal Bundling (Docker-less)section.Checklist
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license