From 1d8c9925b2a9cfd0913a77e56dc7c60e58532c69 Mon Sep 17 00:00:00 2001 From: Reed Hamilton Date: Wed, 14 Jan 2026 15:08:22 -0800 Subject: [PATCH 1/3] chore: refactor pyproject flow to use lockfile flow --- .../workflows/python_uv/packager.py | 92 +++++-------------- .../unit/workflows/python_uv/test_packager.py | 6 +- 2 files changed, 23 insertions(+), 75 deletions(-) diff --git a/aws_lambda_builders/workflows/python_uv/packager.py b/aws_lambda_builders/workflows/python_uv/packager.py index 67008a5bc..19dccbc44 100644 --- a/aws_lambda_builders/workflows/python_uv/packager.py +++ b/aws_lambda_builders/workflows/python_uv/packager.py @@ -323,7 +323,20 @@ def _build_from_lock_file( # Export lock file to requirements.txt for platform-specific install temp_requirements = os.path.join(scratch_dir, "lock_requirements.txt") - export_args = ["export", "--format", "requirements-txt", "--no-hashes", "-o", temp_requirements] + export_args = [ + "export", + "--format", + "requirements.txt", + "--no-emit-project", # Don't include the project itself, only dependencies + "--no-header", # Skip comment header + "--no-hashes", # Skip hashes for cleaner output (optional) + "--output-file", + temp_requirements, + # We want to specify the version because `uv export` might default to using a different one + # This is important for dependencies that use different versions depending on python version + "--python", + python_version, + ] rc, stdout, stderr = self._uv_runner._uv.run_uv_command(export_args, cwd=project_dir) if rc != 0: @@ -358,86 +371,23 @@ def _build_from_pyproject( LOG.info("Building from pyproject.toml using UV lock and export") try: - # Use UV's native workflow: lock -> export -> install - temp_requirements = self._export_pyproject_to_requirements(pyproject_path, scratch_dir, python_version) - - if temp_requirements: - self._uv_runner.install_requirements( - requirements_path=temp_requirements, - target_dir=target_dir, - scratch_dir=scratch_dir, - config=config, - python_version=python_version, - platform="linux", - architecture=architecture, - ) - else: - LOG.info("No dependencies found in pyproject.toml") - - except Exception as e: - raise UvBuildError(reason=f"Failed to build from pyproject.toml: {str(e)}") - - def _export_pyproject_to_requirements( - self, pyproject_path: str, scratch_dir: str, python_version: str - ) -> Optional[str]: - """Use UV's native lock and export to convert pyproject.toml to requirements.txt. - - This conversion is necessary when pyproject.toml exists without a uv.lock file. - UV's pip install command provides better platform targeting capabilities - (--python-platform) compared to uv sync, which is essential for Lambda's - cross-platform builds (x86_64/ARM64). The workflow is: - 1. uv lock: Generate lock file from pyproject.toml - 2. uv export: Convert lock file to requirements.txt format - 3. Use requirements.txt with uv pip install for platform-specific builds - """ - project_dir = os.path.dirname(pyproject_path) - - try: - # Step 1: Create lock file using UV + # Generate lock file from pyproject.toml LOG.debug("Creating lock file from pyproject.toml") lock_args = ["lock", "--no-progress"] - if python_version: lock_args.extend(["--python", python_version]) + project_dir = os.path.dirname(pyproject_path) rc, stdout, stderr = self._uv_runner._uv.run_uv_command(lock_args, cwd=project_dir) - - if rc != 0: - LOG.warning(f"UV lock failed: {stderr}") - return None - - # Step 2: Export lock file to requirements.txt format - LOG.debug("Exporting lock file to requirements.txt format") - temp_requirements = os.path.join(scratch_dir, "exported_requirements.txt") - - export_args = [ - "export", - "--format", - "requirements.txt", - "--no-emit-project", # Don't include the project itself, only dependencies - "--no-header", # Skip comment header - "--no-hashes", # Skip hashes for cleaner output (optional) - "--output-file", - temp_requirements, - ] - - rc, stdout, stderr = self._uv_runner._uv.run_uv_command(export_args, cwd=project_dir) - if rc != 0: - LOG.warning(f"UV export failed: {stderr}") - return None + raise UvBuildError(reason=f"UV lock failed: {stderr}") - # Verify the requirements file was created and has content - if os.path.exists(temp_requirements) and os.path.getsize(temp_requirements) > 0: - LOG.debug(f"Successfully exported dependencies to {temp_requirements}") - return temp_requirements - else: - LOG.info("No dependencies to export from pyproject.toml") - return None + # Reuse lock file build logic + lock_path = os.path.join(project_dir, "uv.lock") + self._build_from_lock_file(lock_path, target_dir, scratch_dir, python_version, architecture, config) except Exception as e: - LOG.warning(f"Failed to export pyproject.toml using UV native workflow: {e}") - return None + raise UvBuildError(reason=f"Failed to build from pyproject.toml: {str(e)}") def _build_from_requirements( self, diff --git a/tests/unit/workflows/python_uv/test_packager.py b/tests/unit/workflows/python_uv/test_packager.py index af99a52fa..dcea912e8 100644 --- a/tests/unit/workflows/python_uv/test_packager.py +++ b/tests/unit/workflows/python_uv/test_packager.py @@ -244,13 +244,11 @@ def test_build_dependencies_pyproject_without_uv_lock(self): """Test that pyproject.toml without uv.lock uses standard pyproject build.""" with patch("os.path.basename", return_value="pyproject.toml"), patch( "os.path.dirname", return_value=os.path.join("path", "to") - ), patch("os.path.exists") as mock_exists, patch.object( - self.builder, "_export_pyproject_to_requirements", return_value="/temp/requirements.txt" - ): - + ), patch("os.path.exists") as mock_exists: # Mock that uv.lock does NOT exist alongside pyproject.toml mock_exists.return_value = False + self.mock_uv_runner._uv.run_uv_command.return_value = (0, b"", b"") self.builder.build_dependencies( artifacts_dir_path="/artifacts", scratch_dir_path="/scratch", From 40a595b8f9c6500f105c7a95ef28c983b939afeb Mon Sep 17 00:00:00 2001 From: Reed Hamilton Date: Thu, 15 Jan 2026 10:26:47 -0800 Subject: [PATCH 2/3] fix: change manifest priority for uv workflow --- aws_lambda_builders/workflows/python_uv/utils.py | 16 +++++++++------- tests/unit/workflows/python_uv/test_utils.py | 2 +- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/aws_lambda_builders/workflows/python_uv/utils.py b/aws_lambda_builders/workflows/python_uv/utils.py index 88523d02c..5e40b5bf1 100644 --- a/aws_lambda_builders/workflows/python_uv/utils.py +++ b/aws_lambda_builders/workflows/python_uv/utils.py @@ -37,8 +37,10 @@ def detect_uv_manifest(source_dir: str) -> Optional[str]: Note: uv.lock is NOT a manifest - it's a lock file that accompanies pyproject.toml. UV workflows support these manifest types: - 1. pyproject.toml (preferred) - may have accompanying uv.lock - 2. requirements.txt and variants - traditional pip-style manifests + 1. requirements.txt and variants - traditional pip-style manifests + 2. pyproject.toml (preferred) - may have accompanying uv.lock + + We favor requirements.txt because it is possible to have both, but the pyproject could just be project metadata. Args: source_dir: Directory to search for manifest files @@ -46,11 +48,6 @@ def detect_uv_manifest(source_dir: str) -> Optional[str]: Returns: Path to the detected manifest file, or None if not found """ - # Check for pyproject.toml first (preferred manifest) - pyproject_path = os.path.join(source_dir, "pyproject.toml") - if os.path.isfile(pyproject_path): - return pyproject_path - # Check for requirements.txt variants (in order of preference) requirements_variants = [ "requirements.txt", @@ -64,6 +61,11 @@ def detect_uv_manifest(source_dir: str) -> Optional[str]: if os.path.isfile(requirements_path): return requirements_path + # Check for pyproject.toml first (preferred manifest) + pyproject_path = os.path.join(source_dir, "pyproject.toml") + if os.path.isfile(pyproject_path): + return pyproject_path + return None diff --git a/tests/unit/workflows/python_uv/test_utils.py b/tests/unit/workflows/python_uv/test_utils.py index ca6475172..f15e3e550 100644 --- a/tests/unit/workflows/python_uv/test_utils.py +++ b/tests/unit/workflows/python_uv/test_utils.py @@ -73,7 +73,7 @@ def test_detect_uv_manifest_pyproject_priority(self): result = detect_uv_manifest(temp_dir) # pyproject.toml should have priority over requirements.txt - self.assertEqual(result, pyproject_path) + self.assertEqual(result, req_path) def test_detect_uv_manifest_requirements_variants(self): with tempfile.TemporaryDirectory() as temp_dir: From 040526a95a783d32898878914874db7422af66f9 Mon Sep 17 00:00:00 2001 From: Reed Hamilton Date: Thu, 15 Jan 2026 13:00:23 -0800 Subject: [PATCH 3/3] fix: pr feedback --- aws_lambda_builders/workflows/python_uv/DESIGN.md | 9 +++++---- aws_lambda_builders/workflows/python_uv/packager.py | 1 - aws_lambda_builders/workflows/python_uv/utils.py | 3 +-- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/aws_lambda_builders/workflows/python_uv/DESIGN.md b/aws_lambda_builders/workflows/python_uv/DESIGN.md index 11df8206a..4c5de87d9 100644 --- a/aws_lambda_builders/workflows/python_uv/DESIGN.md +++ b/aws_lambda_builders/workflows/python_uv/DESIGN.md @@ -109,11 +109,12 @@ The general algorithm for preparing a python package using UV for use on AWS Lam The workflow uses a smart dispatch system that recognizes actual manifest files: **Supported Manifests:** -- `pyproject.toml` - Modern Python project manifest (preferred) +- `pyproject.toml` - Modern Python project manifest - `requirements.txt` - Traditional pip requirements file - `requirements-*.txt` - Environment-specific variants (dev, prod, test, etc.) **Smart Lock File Detection:** +- Look for requirements.txt first - When `pyproject.toml` is the manifest, automatically checks for `uv.lock` in the same directory - If `uv.lock` exists alongside `pyproject.toml`, uses lock-based build for precise dependencies - If no `uv.lock`, uses standard pyproject.toml build with UV's lock and export workflow @@ -258,9 +259,9 @@ CAPABILITY = Capability( The workflow uses intelligent manifest detection: **Supported Manifests (in order of preference):** -1. `pyproject.toml` - Modern Python project manifest (preferred) -2. `requirements.txt` - Standard pip format -3. `requirements-*.txt` - Environment-specific variants (dev, test, prod, etc.) +1. `requirements.txt` - Standard pip format +2. `requirements-*.txt` - Environment-specific variants (dev, test, prod, etc.) +3. `pyproject.toml` - Modern Python project manifest **Smart Lock File Enhancement:** - When `pyproject.toml` is used, automatically detects `uv.lock` in the same directory diff --git a/aws_lambda_builders/workflows/python_uv/packager.py b/aws_lambda_builders/workflows/python_uv/packager.py index 19dccbc44..fc2ef696f 100644 --- a/aws_lambda_builders/workflows/python_uv/packager.py +++ b/aws_lambda_builders/workflows/python_uv/packager.py @@ -328,7 +328,6 @@ def _build_from_lock_file( "--format", "requirements.txt", "--no-emit-project", # Don't include the project itself, only dependencies - "--no-header", # Skip comment header "--no-hashes", # Skip hashes for cleaner output (optional) "--output-file", temp_requirements, diff --git a/aws_lambda_builders/workflows/python_uv/utils.py b/aws_lambda_builders/workflows/python_uv/utils.py index 5e40b5bf1..cb7bfeb5b 100644 --- a/aws_lambda_builders/workflows/python_uv/utils.py +++ b/aws_lambda_builders/workflows/python_uv/utils.py @@ -38,7 +38,7 @@ def detect_uv_manifest(source_dir: str) -> Optional[str]: Note: uv.lock is NOT a manifest - it's a lock file that accompanies pyproject.toml. UV workflows support these manifest types: 1. requirements.txt and variants - traditional pip-style manifests - 2. pyproject.toml (preferred) - may have accompanying uv.lock + 2. pyproject.toml - may have accompanying uv.lock We favor requirements.txt because it is possible to have both, but the pyproject could just be project metadata. @@ -61,7 +61,6 @@ def detect_uv_manifest(source_dir: str) -> Optional[str]: if os.path.isfile(requirements_path): return requirements_path - # Check for pyproject.toml first (preferred manifest) pyproject_path = os.path.join(source_dir, "pyproject.toml") if os.path.isfile(pyproject_path): return pyproject_path