repro-lambda 0.8.2__tar.gz → 0.9.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- repro_lambda-0.9.0/.release-please-manifest.json +3 -0
- {repro_lambda-0.8.2 → repro_lambda-0.9.0}/CHANGELOG.md +7 -0
- {repro_lambda-0.8.2 → repro_lambda-0.9.0}/PKG-INFO +1 -1
- {repro_lambda-0.8.2 → repro_lambda-0.9.0}/USAGE.md +23 -0
- {repro_lambda-0.8.2 → repro_lambda-0.9.0}/pyproject.toml +1 -1
- {repro_lambda-0.8.2 → repro_lambda-0.9.0}/src/repro_lambda/__init__.py +1 -1
- {repro_lambda-0.8.2 → repro_lambda-0.9.0}/src/repro_lambda/build.py +1 -0
- {repro_lambda-0.8.2 → repro_lambda-0.9.0}/src/repro_lambda/cli.py +18 -3
- {repro_lambda-0.8.2 → repro_lambda-0.9.0}/src/repro_lambda/docker_runner.py +8 -2
- {repro_lambda-0.8.2 → repro_lambda-0.9.0}/src/repro_lambda/hasher.py +2 -0
- {repro_lambda-0.8.2 → repro_lambda-0.9.0}/src/repro_lambda/manifest.py +14 -0
- {repro_lambda-0.8.2 → repro_lambda-0.9.0}/src/repro_lambda/verify.py +1 -0
- {repro_lambda-0.8.2 → repro_lambda-0.9.0}/src/repro_lambda/zip_packager.py +44 -0
- repro_lambda-0.9.0/tests/test_compile_bytecode.py +114 -0
- {repro_lambda-0.8.2 → repro_lambda-0.9.0}/tests/test_hash_contract.py +5 -4
- {repro_lambda-0.8.2 → repro_lambda-0.9.0}/uv.lock +2 -2
- repro_lambda-0.8.2/.release-please-manifest.json +0 -3
- {repro_lambda-0.8.2 → repro_lambda-0.9.0}/.github/workflows/build.yml +0 -0
- {repro_lambda-0.8.2 → repro_lambda-0.9.0}/.github/workflows/ci.yml +0 -0
- {repro_lambda-0.8.2 → repro_lambda-0.9.0}/.github/workflows/move-major-tag.yml +0 -0
- {repro_lambda-0.8.2 → repro_lambda-0.9.0}/.github/workflows/promote.yml +0 -0
- {repro_lambda-0.8.2 → repro_lambda-0.9.0}/.github/workflows/publish.yml +0 -0
- {repro_lambda-0.8.2 → repro_lambda-0.9.0}/.github/workflows/release.yml +0 -0
- {repro_lambda-0.8.2 → repro_lambda-0.9.0}/.gitignore +0 -0
- {repro_lambda-0.8.2 → repro_lambda-0.9.0}/.pre-commit-config.yaml +0 -0
- {repro_lambda-0.8.2 → repro_lambda-0.9.0}/LICENSE +0 -0
- {repro_lambda-0.8.2 → repro_lambda-0.9.0}/README.md +0 -0
- {repro_lambda-0.8.2 → repro_lambda-0.9.0}/SETUP.md +0 -0
- {repro_lambda-0.8.2 → repro_lambda-0.9.0}/assets/social-preview.html +0 -0
- {repro_lambda-0.8.2 → repro_lambda-0.9.0}/assets/social-preview.png +0 -0
- {repro_lambda-0.8.2 → repro_lambda-0.9.0}/examples/complete/README.md +0 -0
- {repro_lambda-0.8.2 → repro_lambda-0.9.0}/examples/complete/builds/catalog.json +0 -0
- {repro_lambda-0.8.2 → repro_lambda-0.9.0}/examples/complete/lambdas.toml +0 -0
- {repro_lambda-0.8.2 → repro_lambda-0.9.0}/examples/complete/main.tf +0 -0
- {repro_lambda-0.8.2 → repro_lambda-0.9.0}/examples/complete/outputs.tf +0 -0
- {repro_lambda-0.8.2 → repro_lambda-0.9.0}/examples/complete/src/app/app.py +0 -0
- {repro_lambda-0.8.2 → repro_lambda-0.9.0}/examples/complete/src/app/requirements.arm64.lock +0 -0
- {repro_lambda-0.8.2 → repro_lambda-0.9.0}/examples/complete/variables.tf +0 -0
- {repro_lambda-0.8.2 → repro_lambda-0.9.0}/examples/complete/versions.tf +0 -0
- {repro_lambda-0.8.2 → repro_lambda-0.9.0}/release-please-config.json +0 -0
- {repro_lambda-0.8.2 → repro_lambda-0.9.0}/src/repro_lambda/__main__.py +0 -0
- {repro_lambda-0.8.2 → repro_lambda-0.9.0}/src/repro_lambda/catalog.py +0 -0
- {repro_lambda-0.8.2 → repro_lambda-0.9.0}/src/repro_lambda/git_guard.py +0 -0
- {repro_lambda-0.8.2 → repro_lambda-0.9.0}/src/repro_lambda/promote.py +0 -0
- {repro_lambda-0.8.2 → repro_lambda-0.9.0}/src/repro_lambda/s3_uploader.py +0 -0
- {repro_lambda-0.8.2 → repro_lambda-0.9.0}/src/repro_lambda/source_locker.py +0 -0
- {repro_lambda-0.8.2 → repro_lambda-0.9.0}/src/repro_lambda/source_stager.py +0 -0
- {repro_lambda-0.8.2 → repro_lambda-0.9.0}/src/repro_lambda/sources.py +0 -0
- {repro_lambda-0.8.2 → repro_lambda-0.9.0}/tests/__init__.py +0 -0
- {repro_lambda-0.8.2 → repro_lambda-0.9.0}/tests/conftest.py +0 -0
- {repro_lambda-0.8.2 → repro_lambda-0.9.0}/tests/fixtures/sample_nodejs_lambda/handler/index.js +0 -0
- {repro_lambda-0.8.2 → repro_lambda-0.9.0}/tests/fixtures/sample_nodejs_lambda/handler/package-lock.json +0 -0
- {repro_lambda-0.8.2 → repro_lambda-0.9.0}/tests/fixtures/sample_nodejs_lambda/handler/package.json +0 -0
- {repro_lambda-0.8.2 → repro_lambda-0.9.0}/tests/fixtures/sample_nodejs_lambda/lambdas.toml +0 -0
- {repro_lambda-0.8.2 → repro_lambda-0.9.0}/tests/fixtures/sample_python_lambda/handler/app.py +0 -0
- {repro_lambda-0.8.2 → repro_lambda-0.9.0}/tests/fixtures/sample_python_lambda/handler/requirements.arm64.lock +0 -0
- {repro_lambda-0.8.2 → repro_lambda-0.9.0}/tests/fixtures/sample_python_lambda/handler/requirements.in +0 -0
- {repro_lambda-0.8.2 → repro_lambda-0.9.0}/tests/fixtures/sample_python_lambda/lambdas.toml +0 -0
- {repro_lambda-0.8.2 → repro_lambda-0.9.0}/tests/test_build_integration.py +0 -0
- {repro_lambda-0.8.2 → repro_lambda-0.9.0}/tests/test_build_nodejs.py +0 -0
- {repro_lambda-0.8.2 → repro_lambda-0.9.0}/tests/test_catalog.py +0 -0
- {repro_lambda-0.8.2 → repro_lambda-0.9.0}/tests/test_cli_build.py +0 -0
- {repro_lambda-0.8.2 → repro_lambda-0.9.0}/tests/test_cli_lock.py +0 -0
- {repro_lambda-0.8.2 → repro_lambda-0.9.0}/tests/test_cli_smoke.py +0 -0
- {repro_lambda-0.8.2 → repro_lambda-0.9.0}/tests/test_docker_runner.py +0 -0
- {repro_lambda-0.8.2 → repro_lambda-0.9.0}/tests/test_docker_runner_nodejs.py +0 -0
- {repro_lambda-0.8.2 → repro_lambda-0.9.0}/tests/test_e2e_nodejs_lambda.py +0 -0
- {repro_lambda-0.8.2 → repro_lambda-0.9.0}/tests/test_e2e_python_lambda.py +0 -0
- {repro_lambda-0.8.2 → repro_lambda-0.9.0}/tests/test_extra_files.py +0 -0
- {repro_lambda-0.8.2 → repro_lambda-0.9.0}/tests/test_git_guard.py +0 -0
- {repro_lambda-0.8.2 → repro_lambda-0.9.0}/tests/test_hasher.py +0 -0
- {repro_lambda-0.8.2 → repro_lambda-0.9.0}/tests/test_manifest.py +0 -0
- {repro_lambda-0.8.2 → repro_lambda-0.9.0}/tests/test_per_lambda_builder.py +0 -0
- {repro_lambda-0.8.2 → repro_lambda-0.9.0}/tests/test_promote.py +0 -0
- {repro_lambda-0.8.2 → repro_lambda-0.9.0}/tests/test_python_byte_compat_regression.py +0 -0
- {repro_lambda-0.8.2 → repro_lambda-0.9.0}/tests/test_s3_uploader.py +0 -0
- {repro_lambda-0.8.2 → repro_lambda-0.9.0}/tests/test_source_locker.py +0 -0
- {repro_lambda-0.8.2 → repro_lambda-0.9.0}/tests/test_source_stager.py +0 -0
- {repro_lambda-0.8.2 → repro_lambda-0.9.0}/tests/test_sources.py +0 -0
- {repro_lambda-0.8.2 → repro_lambda-0.9.0}/tests/test_sources_hash.py +0 -0
- {repro_lambda-0.8.2 → repro_lambda-0.9.0}/tests/test_sources_schema.py +0 -0
- {repro_lambda-0.8.2 → repro_lambda-0.9.0}/tests/test_verify.py +0 -0
- {repro_lambda-0.8.2 → repro_lambda-0.9.0}/tests/test_zip_excludes.py +0 -0
- {repro_lambda-0.8.2 → repro_lambda-0.9.0}/tests/test_zip_packager.py +0 -0
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.9.0](https://github.com/antonbabenko/repro-lambda/compare/v0.8.2...v0.9.0) (2026-10-30)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* compile_bytecode ships checked-hash bytecode in python lambdas ([#22](https://github.com/antonbabenko/repro-lambda/issues/22)) ([47677cf](https://github.com/antonbabenko/repro-lambda/commit/47677cfdafe3832e5d0a570c5ce1fa7b509864c1))
|
|
9
|
+
|
|
3
10
|
## [0.8.2](https://github.com/antonbabenko/repro-lambda/compare/v0.8.1...v0.8.2) (2026-09-23)
|
|
4
11
|
|
|
5
12
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: repro-lambda
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.0
|
|
4
4
|
Summary: Build reproducible AWS Lambda packages outside Terraform, optimized for terraform-aws-lambda by serverless.tf.
|
|
5
5
|
Project-URL: Homepage, https://github.com/antonbabenko/repro-lambda
|
|
6
6
|
Project-URL: Repository, https://github.com/antonbabenko/repro-lambda
|
|
@@ -63,6 +63,29 @@ Pin the `base_image_python` to a specific digest with `docker pull <image> &&
|
|
|
63
63
|
docker inspect --format='{{index .RepoDigests 0}}' <image>` - never use a
|
|
64
64
|
floating tag in production.
|
|
65
65
|
|
|
66
|
+
### Shipping bytecode (`compile_bytecode`)
|
|
67
|
+
|
|
68
|
+
A Lambda's `/var/task` is read-only, so a package shipped without `.pyc` files is
|
|
69
|
+
recompiled from source by every new Python process that imports it. For a large
|
|
70
|
+
package (or a handler that starts Python subprocesses) that can cost seconds per
|
|
71
|
+
invocation. Set `compile_bytecode = true` on a pip `[[lambda]]` to compile the whole
|
|
72
|
+
package in the builder image (the runtime's own interpreter) and keep the bytecode
|
|
73
|
+
in the zip:
|
|
74
|
+
|
|
75
|
+
```toml
|
|
76
|
+
[[lambda]]
|
|
77
|
+
logical_name = "app"
|
|
78
|
+
# ...
|
|
79
|
+
compile_bytecode = true
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
The zip step removes any bytecode already in the tree, then runs `compileall` with
|
|
83
|
+
`--invalidation-mode checked-hash` (no source mtimes; the interpreter checks each
|
|
84
|
+
`.pyc` against its source hash on import) and `PYTHONHASHSEED=0`, recording file paths
|
|
85
|
+
as `/var/task/...` so the bytes do not depend on the staging directory. The zip stays
|
|
86
|
+
byte-reproducible. The flag is part of the content key (only when set, so lambdas
|
|
87
|
+
without it keep their keys). A file that does not compile ships as source only.
|
|
88
|
+
|
|
66
89
|
### Per-lambda builder overrides
|
|
67
90
|
|
|
68
91
|
`[builder]` sets the defaults for every lambda. Any `[[lambda]]` may override
|
|
@@ -381,18 +381,33 @@ _LAMBDA_ZIP_EXCLUDES = [
|
|
|
381
381
|
]
|
|
382
382
|
|
|
383
383
|
|
|
384
|
-
|
|
384
|
+
_BYTECODE_PATTERNS = ("*__pycache__*", "*.pyc")
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def _zip_impl(src: Path, out: Path, compile_bytecode: bool = False) -> None:
|
|
385
388
|
"""Pack a directory into a deterministic zip (used inside container)."""
|
|
389
|
+
from repro_lambda.zip_packager import compile_bytecode as compile_tree
|
|
386
390
|
from repro_lambda.zip_packager import pack_directory
|
|
387
391
|
|
|
388
|
-
|
|
392
|
+
excludes = _LAMBDA_ZIP_EXCLUDES
|
|
393
|
+
if compile_bytecode:
|
|
394
|
+
compile_tree(src)
|
|
395
|
+
excludes = [p for p in excludes if p not in _BYTECODE_PATTERNS]
|
|
396
|
+
pack_directory(src, out, exclude_glob=excludes)
|
|
389
397
|
|
|
390
398
|
|
|
391
399
|
@app.command(name="zip")
|
|
392
400
|
def zip_(
|
|
393
401
|
src: Annotated[Path, typer.Option("--src", help="Staged package directory.")],
|
|
394
402
|
out: Annotated[Path, typer.Option("--out", help="Output zip path.")],
|
|
403
|
+
compile_bytecode: Annotated[
|
|
404
|
+
bool,
|
|
405
|
+
typer.Option(
|
|
406
|
+
"--compile-bytecode",
|
|
407
|
+
help="Compile checked-hash bytecode with this interpreter and keep it in the zip.",
|
|
408
|
+
),
|
|
409
|
+
] = False,
|
|
395
410
|
) -> None:
|
|
396
411
|
"""Pack a directory into a deterministic zip (used inside container)."""
|
|
397
|
-
_zip_impl(src, out)
|
|
412
|
+
_zip_impl(src, out, compile_bytecode)
|
|
398
413
|
raise typer.Exit(0)
|
|
@@ -91,8 +91,11 @@ pip install \
|
|
|
91
91
|
|
|
92
92
|
# Byte-output cleanup (caches + non-deterministic dist-info metadata) happens in the
|
|
93
93
|
# Python zip step below, so this script needs no findutils/xargs (both absent from the
|
|
94
|
-
# minimal AWS Lambda base images).
|
|
95
|
-
|
|
94
|
+
# minimal AWS Lambda base images). With COMPILE_BYTECODE=1 the zip step first compiles
|
|
95
|
+
# the tree with this image's interpreter (the Lambda runtime) and keeps the bytecode.
|
|
96
|
+
ZIP_ARGS=""
|
|
97
|
+
if [ "$COMPILE_BYTECODE" = "1" ]; then ZIP_ARGS="--compile-bytecode"; fi
|
|
98
|
+
python3 -m repro_lambda zip --src "$PKG" --out /out/lambda.zip $ZIP_ARGS
|
|
96
99
|
"""
|
|
97
100
|
|
|
98
101
|
_NODEJS_INSTALL_SCRIPT = r"""
|
|
@@ -161,6 +164,7 @@ def build_python_lambda(
|
|
|
161
164
|
base_image: str,
|
|
162
165
|
arch: str,
|
|
163
166
|
python_version: str,
|
|
167
|
+
compile_bytecode: bool = False,
|
|
164
168
|
) -> None:
|
|
165
169
|
"""v0.1-compatible: install + pack inside the Python container."""
|
|
166
170
|
if arch not in ARCH_TO_PIP_PLATFORMS:
|
|
@@ -196,6 +200,8 @@ def build_python_lambda(
|
|
|
196
200
|
f"PIP_ABI=cp{pyver_compact}",
|
|
197
201
|
"-e",
|
|
198
202
|
f"PIP_PYVER={pyver}",
|
|
203
|
+
"-e",
|
|
204
|
+
f"COMPILE_BYTECODE={int(compile_bytecode)}",
|
|
199
205
|
"--entrypoint",
|
|
200
206
|
"bash",
|
|
201
207
|
base_image,
|
|
@@ -85,6 +85,8 @@ def compute_content_hash(
|
|
|
85
85
|
h.update(f"package_manager={spec.package_manager}\n".encode())
|
|
86
86
|
h.update(f"lambda_at_edge={int(spec.lambda_at_edge)}\n".encode())
|
|
87
87
|
h.update(f"hash_extra={spec.hash_extra}\n".encode())
|
|
88
|
+
if spec.compile_bytecode: # only when set, so specs without it keep their hashes
|
|
89
|
+
h.update(b"compile_bytecode=1\n")
|
|
88
90
|
h.update(f"base_image={base_image}\n".encode())
|
|
89
91
|
h.update(f"builder_version={builder_version}\n".encode())
|
|
90
92
|
|
|
@@ -131,6 +131,9 @@ class LambdaSpec:
|
|
|
131
131
|
include_patterns: list[str] | None = None
|
|
132
132
|
exclude_patterns: list[str] | None = None
|
|
133
133
|
sources: tuple[Source, ...] = ()
|
|
134
|
+
# pip only: ship checked-hash bytecode compiled in the builder image, so a read-only
|
|
135
|
+
# /var/task does not recompile every imported module in every new process.
|
|
136
|
+
compile_bytecode: bool = False
|
|
134
137
|
|
|
135
138
|
@property
|
|
136
139
|
def resolved_requirements_lock(self) -> str:
|
|
@@ -450,6 +453,16 @@ def load_manifest(path: Path) -> Manifest:
|
|
|
450
453
|
|
|
451
454
|
extra_files = _parse_extra_files(path, entry)
|
|
452
455
|
sources = _parse_sources(path, entry)
|
|
456
|
+
compile_bytecode = entry.get("compile_bytecode", False)
|
|
457
|
+
if not isinstance(compile_bytecode, bool):
|
|
458
|
+
raise ValueError(
|
|
459
|
+
f"{path}: lambda {entry.get('logical_name')!r} compile_bytecode must be a boolean "
|
|
460
|
+
f"(got {compile_bytecode!r})"
|
|
461
|
+
)
|
|
462
|
+
if compile_bytecode and pkg != "pip":
|
|
463
|
+
raise ValueError(
|
|
464
|
+
f"{path}: lambda {entry.get('logical_name')!r} compile_bytecode is for pip lambdas"
|
|
465
|
+
)
|
|
453
466
|
|
|
454
467
|
override_base_image = entry.get("base_image_python")
|
|
455
468
|
if override_base_image is not None and "@sha256:" not in override_base_image:
|
|
@@ -489,6 +502,7 @@ def load_manifest(path: Path) -> Manifest:
|
|
|
489
502
|
include_patterns=list(override_include) if override_include is not None else None,
|
|
490
503
|
exclude_patterns=list(override_exclude) if override_exclude is not None else None,
|
|
491
504
|
sources=sources,
|
|
505
|
+
compile_bytecode=compile_bytecode,
|
|
492
506
|
)
|
|
493
507
|
)
|
|
494
508
|
|
|
@@ -68,6 +68,7 @@ def verify_reproducible(
|
|
|
68
68
|
base_image=builder.base_image_python,
|
|
69
69
|
arch=spec.arch,
|
|
70
70
|
python_version=spec.runtime.removeprefix("python"),
|
|
71
|
+
compile_bytecode=spec.compile_bytecode,
|
|
71
72
|
)
|
|
72
73
|
else: # npm
|
|
73
74
|
node_ver = spec.runtime.removeprefix("nodejs").removesuffix(".x")
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import fnmatch
|
|
6
|
+
import os
|
|
6
7
|
import sys
|
|
7
8
|
from pathlib import Path
|
|
8
9
|
from zipfile import ZIP_DEFLATED, ZipFile, ZipInfo
|
|
@@ -76,3 +77,46 @@ def pack_directory(
|
|
|
76
77
|
else:
|
|
77
78
|
data = p.read_bytes()
|
|
78
79
|
zf.writestr(zinfo, data)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def compile_bytecode(src: Path, deploy_root: str = "/var/task") -> None:
|
|
83
|
+
"""Replace any bytecode under `src` with checked-hash .pyc built by this interpreter.
|
|
84
|
+
|
|
85
|
+
checked-hash bytecode carries no source mtime (the zip resets every mtime) and the
|
|
86
|
+
interpreter checks it against the source hash on import, so it stays valid in a
|
|
87
|
+
read-only deploy dir. Recorded file paths are rewritten from `src` to `deploy_root`
|
|
88
|
+
(tracebacks show the deployed paths, and the bytes do not depend on the staging dir);
|
|
89
|
+
PYTHONHASHSEED=0 keeps the marshalled output stable. A file that does not compile gets
|
|
90
|
+
no bytecode and is compiled at import as before; compileall's report is printed.
|
|
91
|
+
"""
|
|
92
|
+
import shutil
|
|
93
|
+
import subprocess
|
|
94
|
+
|
|
95
|
+
root = src.resolve()
|
|
96
|
+
for cache in sorted(root.rglob("__pycache__"), reverse=True):
|
|
97
|
+
if cache.is_dir() and not cache.is_symlink():
|
|
98
|
+
shutil.rmtree(cache)
|
|
99
|
+
for stale in root.rglob("*.pyc"):
|
|
100
|
+
stale.unlink()
|
|
101
|
+
result = subprocess.run(
|
|
102
|
+
[
|
|
103
|
+
sys.executable,
|
|
104
|
+
"-m",
|
|
105
|
+
"compileall",
|
|
106
|
+
"-q",
|
|
107
|
+
"--invalidation-mode",
|
|
108
|
+
"checked-hash",
|
|
109
|
+
"-s",
|
|
110
|
+
str(root),
|
|
111
|
+
"-p",
|
|
112
|
+
deploy_root,
|
|
113
|
+
str(root),
|
|
114
|
+
],
|
|
115
|
+
env={**os.environ, "PYTHONHASHSEED": "0"},
|
|
116
|
+
capture_output=True,
|
|
117
|
+
text=True,
|
|
118
|
+
)
|
|
119
|
+
if result.returncode != 0:
|
|
120
|
+
print(
|
|
121
|
+
f"warning: some files did not compile:\n{result.stdout}{result.stderr}", file=sys.stderr
|
|
122
|
+
)
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""compile_bytecode: checked-hash .pyc built in the zip step, keyed into the content hash."""
|
|
2
|
+
|
|
3
|
+
import dataclasses
|
|
4
|
+
import importlib.util
|
|
5
|
+
import marshal
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from zipfile import ZipFile
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
from typer.testing import CliRunner
|
|
11
|
+
|
|
12
|
+
from repro_lambda.cli import app
|
|
13
|
+
from repro_lambda.hasher import compute_content_hash
|
|
14
|
+
from repro_lambda.manifest import LambdaSpec, load_manifest
|
|
15
|
+
|
|
16
|
+
runner = CliRunner()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _pkg(root: Path) -> Path:
|
|
20
|
+
pkg = root / "pkg"
|
|
21
|
+
(pkg / "mymod" / "__pycache__").mkdir(parents=True)
|
|
22
|
+
(pkg / "mymod" / "__init__.py").write_text("VALUES = frozenset({'a', 'b', 'c'})\n")
|
|
23
|
+
(pkg / "mymod" / "__pycache__" / "stale.cpython-313.pyc").write_bytes(b"\x00")
|
|
24
|
+
(pkg / "mymod" / "orphan.pyc").write_bytes(b"\x00")
|
|
25
|
+
(pkg / "broken.py").write_text("def broken(:\n")
|
|
26
|
+
return pkg
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _zip(pkg: Path, out: Path, *extra: str) -> list[str]:
|
|
30
|
+
result = runner.invoke(app, ["zip", "--src", str(pkg), "--out", str(out), *extra])
|
|
31
|
+
assert result.exit_code == 0, result.output
|
|
32
|
+
return ZipFile(out).namelist()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_zip_compiles_checked_hash_bytecode_with_deployed_paths(tmp_path: Path):
|
|
36
|
+
names = _zip(_pkg(tmp_path), tmp_path / "lambda.zip", "--compile-bytecode")
|
|
37
|
+
pycs = [n for n in names if n.endswith(".pyc")]
|
|
38
|
+
assert len(pycs) == 1 and pycs[0].startswith("mymod/__pycache__/__init__.")
|
|
39
|
+
data = ZipFile(tmp_path / "lambda.zip").read(pycs[0])
|
|
40
|
+
assert data[:4] == importlib.util.MAGIC_NUMBER
|
|
41
|
+
assert int.from_bytes(data[4:8], "little") == 0b11 # hash-based, checked
|
|
42
|
+
assert marshal.loads(data[16:]).co_filename == "/var/task/mymod/__init__.py"
|
|
43
|
+
assert "mymod/orphan.pyc" not in names # stale bytecode is replaced, not shipped
|
|
44
|
+
assert "broken.py" in names # a file that does not compile ships as source only
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_compiled_zip_is_byte_identical_across_builds(tmp_path: Path):
|
|
48
|
+
a, b = tmp_path / "a", tmp_path / "b" # different staging dirs, same tree
|
|
49
|
+
_zip(_pkg(a), a / "lambda.zip", "--compile-bytecode")
|
|
50
|
+
_zip(_pkg(b), b / "lambda.zip", "--compile-bytecode")
|
|
51
|
+
assert (a / "lambda.zip").read_bytes() == (b / "lambda.zip").read_bytes()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_zip_without_the_flag_still_strips_bytecode(tmp_path: Path):
|
|
55
|
+
names = _zip(_pkg(tmp_path), tmp_path / "lambda.zip")
|
|
56
|
+
assert not any(n.endswith(".pyc") or "__pycache__" in n for n in names)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _spec(**kw) -> LambdaSpec:
|
|
60
|
+
base = LambdaSpec(
|
|
61
|
+
logical_name="app",
|
|
62
|
+
source_dir="handler",
|
|
63
|
+
requirements_lock="handler/requirements.${arch}.lock",
|
|
64
|
+
runtime="python3.13",
|
|
65
|
+
arch="arm64",
|
|
66
|
+
handler="app.lambda_handler",
|
|
67
|
+
)
|
|
68
|
+
return dataclasses.replace(base, **kw)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_flag_changes_the_content_hash_and_default_keeps_it(tmp_path: Path):
|
|
72
|
+
src = tmp_path / "source"
|
|
73
|
+
src.mkdir()
|
|
74
|
+
(src / "app.py").write_text("x = 1\n")
|
|
75
|
+
lock = tmp_path / "requirements.arm64.lock"
|
|
76
|
+
lock.write_text("# empty\n")
|
|
77
|
+
off = compute_content_hash(src, lock, _spec(), "x@sha256:0", "1")
|
|
78
|
+
on = compute_content_hash(src, lock, _spec(compile_bytecode=True), "x@sha256:0", "1")
|
|
79
|
+
assert off != on
|
|
80
|
+
assert off == compute_content_hash(src, lock, _spec(compile_bytecode=False), "x@sha256:0", "1")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _manifest(tmp_path: Path, extra: str, package_manager: str = "pip") -> Path:
|
|
84
|
+
path = tmp_path / "lambdas.toml"
|
|
85
|
+
path.write_text(
|
|
86
|
+
f"""
|
|
87
|
+
[[lambda]]
|
|
88
|
+
logical_name = "app"
|
|
89
|
+
source_dir = "handler"
|
|
90
|
+
requirements_lock = "handler/requirements.${{arch}}.lock"
|
|
91
|
+
runtime = "python3.13"
|
|
92
|
+
arch = "arm64"
|
|
93
|
+
handler = "app.lambda_handler"
|
|
94
|
+
package_manager = "{package_manager}"
|
|
95
|
+
{extra}
|
|
96
|
+
|
|
97
|
+
[builder]
|
|
98
|
+
base_image_python = "public.ecr.aws/lambda/python:3.13@sha256:abc"
|
|
99
|
+
"""
|
|
100
|
+
)
|
|
101
|
+
return path
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_manifest_reads_the_flag(tmp_path: Path):
|
|
105
|
+
assert (
|
|
106
|
+
load_manifest(_manifest(tmp_path, "compile_bytecode = true")).lambdas[0].compile_bytecode
|
|
107
|
+
is True
|
|
108
|
+
)
|
|
109
|
+
assert load_manifest(_manifest(tmp_path, "")).lambdas[0].compile_bytecode is False
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_manifest_rejects_a_non_boolean_flag(tmp_path: Path):
|
|
113
|
+
with pytest.raises(ValueError, match="compile_bytecode must be a boolean"):
|
|
114
|
+
load_manifest(_manifest(tmp_path, 'compile_bytecode = "yes"'))
|
|
@@ -18,10 +18,11 @@ _PKG = Path(repro_lambda.__file__).parent
|
|
|
18
18
|
# Known gap: npm packaging uses base_image_python, which is not in the key (issue #20).
|
|
19
19
|
PACKAGING_FINGERPRINTS = {
|
|
20
20
|
"0.8.0": {
|
|
21
|
-
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"
|
|
21
|
+
# compile_bytecode: off by default and hashed only when on, so no bump.
|
|
22
|
+
"build.py": "21b10b59acf04dae78431e1c50ebbb89d300dd5d32bbdd36e0ee9c2cb3673ea9",
|
|
23
|
+
"hasher.py": "1061efdcb802598001c9638e34222b25b083c6123ff9b59a18dff996a4e0a70f",
|
|
24
|
+
"zip_packager.py": "40d48633ece8d5d9ffffdb786d7479b8588ec7b1fae99a7db748e3963f9b333a",
|
|
25
|
+
"docker_runner.py": "e53208a62ee7ee40e33068b6c3ba936c5e7d75037b1502f1af8fed92391f8f52",
|
|
25
26
|
"source_stager.py": "38186277fb94ab6feac8a07cad2ae35f5adfaa52bde3d51ac71daccb003d3c68",
|
|
26
27
|
"sources.py": "10415ccc89b85f521fb64ef199f084f0bea1842032d414cb3d3537334f6caa0b",
|
|
27
28
|
},
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
version = 1
|
|
2
|
-
revision =
|
|
2
|
+
revision = 5
|
|
3
3
|
requires-python = ">=3.11"
|
|
4
4
|
|
|
5
5
|
[[package]]
|
|
@@ -646,7 +646,7 @@ wheels = [
|
|
|
646
646
|
|
|
647
647
|
[[package]]
|
|
648
648
|
name = "repro-lambda"
|
|
649
|
-
version = "0.
|
|
649
|
+
version = "0.9.0"
|
|
650
650
|
source = { editable = "." }
|
|
651
651
|
dependencies = [
|
|
652
652
|
{ name = "boto3" },
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{repro_lambda-0.8.2 → repro_lambda-0.9.0}/tests/fixtures/sample_nodejs_lambda/handler/index.js
RENAMED
|
File without changes
|
|
File without changes
|
{repro_lambda-0.8.2 → repro_lambda-0.9.0}/tests/fixtures/sample_nodejs_lambda/handler/package.json
RENAMED
|
File without changes
|
|
File without changes
|
{repro_lambda-0.8.2 → repro_lambda-0.9.0}/tests/fixtures/sample_python_lambda/handler/app.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|