Skip to content

fix(python-uv): preserve compiled bytecode after packaging - #926

Open
xujiantop-crypto wants to merge 6 commits into
aws:developfrom
xujiantop-crypto:fix/python-uv-compile-bytecode
Open

xujiantop-crypto wants to merge 6 commits into
aws:developfrom
xujiantop-crypto:fix/python-uv-compile-bytecode

Conversation

@xujiantop-crypto

@xujiantop-crypto xujiantop-crypto commented Sep 16, 2026

Copy link
Copy Markdown

Issue #, if available: Fixes #924

Fixes #925

Description of changes

  • make the existing UvConfig.compile_bytecode option effective in real builds and retain its documented default of True
  • resolve an already-installed interpreter for the target runtime with uv python find --no-project --no-python-downloads <version>
  • pin that exact interpreter for dependency installation with --python, while retaining --python-version for dependency resolution
  • always pass --no-compile-bytecode to uv pip install, preventing uv, UV_COMPILE_BYTECODE, or project uv configuration from producing timestamp-based bytecode that SAM packaging would invalidate
  • after a successful install, run the target interpreter with python -m compileall -f -q --invalidation-mode unchecked-hash <target> so packaged .pyc files remain valid after ZIP timestamps are normalized
  • keep missing target interpreters and bytecode compilation failures non-fatal, while logging the exit code and available diagnostics
  • decode uv/Python subprocess output as UTF-8 with replacement on Windows so UTF-8 diagnostics do not trigger locale-decoding failures
  • cover the default enabled path, explicit disablement, missing runtime/interpreter, compile failures, command construction, and missing diagnostics in unit tests

-f is intentional: it replaces any timestamp-based .pyc already present in an incremental target instead of allowing compileall to skip it.

Description of how you validated changes

  • focused SubprocessUv and UvRunner coverage: 34 tests passed
  • broader test_packager.py and test_utils.py run: 49 tests passed; 6 existing Windows environment-dependent tests failed because the suite expects a Unix ls executable and the host temporary directory rejects file creation
  • verified the real default PythonUvBuildAction configuration enables bytecode compilation
  • live uv 0.10.12 flow found the installed Python 3.13 interpreter, installed into the target, and produced sample.cpython-313.pyc
  • inspected the generated .pyc header and confirmed flags=1 (unchecked-hash), rather than timestamp invalidation
  • confirmed the unchecked-hash .pyc remained loadable after the source timestamp/content no longer matched
  • Ruff passed
  • Black 26.5.1 passed
  • git diff --check passed

Checklist

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

@xujiantop-crypto
xujiantop-crypto requested a review from a team as a code owner September 16, 2026 15:15
@github-actions github-actions Bot added pr/external stage/needs-triage Automatically applied to new issues and PRs, indicating they haven't been looked at. labels Sep 16, 2026

@aws-sam-tooling-bot aws-sam-tooling-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Results

Reviewed: 587257c..c2d25c3
Files: 2
Comments: 2

Comment thread aws_lambda_builders/workflows/python_uv/utils.py Outdated
Comment thread aws_lambda_builders/workflows/python_uv/utils.py Outdated

@aws-sam-tooling-bot aws-sam-tooling-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Results

Reviewed: 587257c..4300971
Files: 5
Comments: 1

Comment thread aws_lambda_builders/workflows/python_uv/utils.py Outdated

@aws-sam-tooling-bot aws-sam-tooling-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Results

Reviewed: 587257c..6848bc2
Files: 5
Comments: 2

Comment thread aws_lambda_builders/workflows/python_uv/packager.py Outdated
Comment thread aws_lambda_builders/workflows/python_uv/utils.py Outdated

@aws-sam-tooling-bot aws-sam-tooling-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Results

Reviewed: 587257c..514513d
Files: 5
Comments: 2

Comment thread aws_lambda_builders/workflows/python_uv/utils.py Outdated
Comment thread aws_lambda_builders/workflows/python_uv/packager.py Outdated

@aws-sam-tooling-bot aws-sam-tooling-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Results

Reviewed: 587257c..0facd03
Files: 4
Comments: 1

Comment thread aws_lambda_builders/workflows/python_uv/packager.py Outdated
@xujiantop-crypto xujiantop-crypto changed the title fix(python-uv): pass compile bytecode option to uv fix(python-uv): preserve compiled bytecode after packaging Sep 16, 2026

@aws-sam-tooling-bot aws-sam-tooling-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Results

Reviewed: 587257c..34b4881
Files: 5
Comments: 3


# Add configuration arguments
target_python = None
if config.compile_bytecode:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[GENERAL] Bytecode compilation is effectively enabled for every build, and there is no way to turn it off.

UvConfig.compile_bytecode still defaults to True (utils.py:107, and the new test test_uv_config_defaults now asserts self.assertTrue(config.compile_bytecode)), while no caller ever constructs a UvConfig with arguments: PythonUvWorkflow._setup_build_actions() builds PythonUvBuildAction without config, and the action falls back to self.config = config or UvConfig(). So on every build this now:

  1. shells out to uv python find,
  2. pins --python to the discovered host interpreter, and
  3. runs compileall -f over the entire installed dependency tree.

Before this PR nothing was compiled, because to_uv_args() never emitted a bytecode flag — uv's own default is no compilation. The PR description states the opposite ("preserve the existing effective default by keeping bytecode compilation disabled unless explicitly enabled"), so the shipped default appears unintended.

The impact is concrete: compileall -f over a large dependency set (e.g. boto3/botocore, numpy) adds noticeable build time, and the generated __pycache__ trees roughly double the on-disk footprint of pure-Python dependencies, which counts against Lambda's 250 MB unzipped package limit. Functions currently near that limit could start failing to deploy after a plain sam build.

If opt-in was the intent, flip the default:

compile_bytecode: bool = False,

If default-on is intentional, please say so explicitly, since it changes artifact size and build time for all existing users of this workflow.

# Use the exact interpreter found by UV instead of repeating a version request that
# could resolve differently when the install command runs.
if target_python:
args.extend(["--python", target_python])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[GENERAL] The --python pin is no longer needed and makes an optional optimization change how the install itself runs.

_compile_bytecode() invokes the discovered interpreter directly:

command = [python_executable, "-m", "compileall", "-f", "-q", "--invalidation-mode", "unchecked-hash", target_dir]

The resulting .pyc files depend only on that interpreter and the installed sources — not on which interpreter uv used. Meanwhile the installed sources are already governed by --python-version and --python-platform (both handlers always pass platform="linux" plus an architecture). So --python contributes nothing to the bytecode outcome, but it does override uv's own interpreter selection for the whole uv pip install invocation — including the interpreter used for any sdist build — replacing whatever uv would have chosen (e.g. an active virtualenv) with whatever uv python find happens to return.

Dropping the pin keeps the install behavior identical to today and confines the new code path to the compilation step:

if python_version:
   args.extend(["--python-version", python_version])

with target_python used only for _compile_bytecode().

if rc == 0 and stdout:
return stdout.strip()
diagnostic = (stderr or "").strip() or (stdout or "").strip() or "no diagnostic output"
LOG.warning("Could not locate target Python %s via uv (exit code %d): %s", python_version, rc, diagnostic)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[GENERAL] A missing target interpreter is the normal case for cross-version builds, so LOG.warning here will surface a scary message on healthy builds.

LOG.warning("Could not locate target Python %s via uv (exit code %d): %s", python_version, rc, diagnostic)

Building a python3.9 function on a host that only has 3.12 installed — or any build in an image whose interpreter differs from the runtime — hits this path, and --no-python-downloads guarantees uv will not fill the gap. Since bytecode compilation is a best-effort optimization whose absence has no effect on the produced artifact, this belongs at LOG.debug (or LOG.info), otherwise users see a warning in sam build output for a build that succeeded exactly as intended. Note this is amplified by the default in comment 1: with compilation on by default, most cross-version builds will emit it.

The same applies to the "Target Python version is unavailable" warning at line 155 — _extract_python_version() raises when there is no runtime, so a falsy python_version reaching install_requirements() only happens for direct library callers.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr/external stage/needs-triage Automatically applied to new issues and PRs, indicating they haven't been looked at.

Projects

None yet

1 participant