Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions aws_lambda_builders/workflows/python_uv/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
91 changes: 20 additions & 71 deletions aws_lambda_builders/workflows/python_uv/packager.py
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,19 @@ 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-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:
Expand Down Expand Up @@ -358,86 +370,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
raise UvBuildError(reason=f"UV lock failed: {stderr}")

# 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

# 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,
Expand Down
15 changes: 8 additions & 7 deletions aws_lambda_builders/workflows/python_uv/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,20 +37,17 @@ 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 - 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

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",
Expand All @@ -64,6 +61,10 @@ def detect_uv_manifest(source_dir: str) -> Optional[str]:
if os.path.isfile(requirements_path):
return requirements_path

pyproject_path = os.path.join(source_dir, "pyproject.toml")
if os.path.isfile(pyproject_path):
return pyproject_path

return None


Expand Down
6 changes: 2 additions & 4 deletions tests/unit/workflows/python_uv/test_packager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/workflows/python_uv/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Loading