From 5713899f0429185ac7451e68207655c897628115 Mon Sep 17 00:00:00 2001 From: Sriram Madapusi Vasudevan Date: Tue, 27 Jan 2026 20:23:59 +0000 Subject: [PATCH] fix: skip Python binary validation when pip is not used (#678) When no requirements.txt exists or download_dependencies=False, the workflow only copies files and doesn't run pip. In these cases, skip PythonRuntimeValidator and use the base RuntimeValidator instead. This allows builds to succeed even when the target Python runtime isn't installed locally, as long as no dependencies need to be installed. --- .../workflows/python_pip/workflow.py | 6 ++++ .../workflows/python_pip/test_python_pip.py | 33 +++++++++++++++++ .../workflows/python_pip/test_workflow.py | 36 +++++++++++++++++++ 3 files changed, 75 insertions(+) diff --git a/aws_lambda_builders/workflows/python_pip/workflow.py b/aws_lambda_builders/workflows/python_pip/workflow.py index 6273277aa..d6c84e198 100644 --- a/aws_lambda_builders/workflows/python_pip/workflow.py +++ b/aws_lambda_builders/workflows/python_pip/workflow.py @@ -6,6 +6,7 @@ from aws_lambda_builders.actions import CleanUpAction, CopySourceAction, LinkSourceAction from aws_lambda_builders.path_resolver import PathResolver +from aws_lambda_builders.validator import RuntimeValidator from aws_lambda_builders.workflow import BaseWorkflow, BuildDirectory, BuildInSourceSupport, Capability from aws_lambda_builders.workflows.python_pip.validator import PythonRuntimeValidator @@ -78,6 +79,8 @@ def __init__(self, source_dir, artifacts_dir, scratch_dir, manifest_path, runtim if osutils is None: osutils = OSUtils() + self._use_pip = False + if not self.download_dependencies and not self.dependencies_dir: LOG.info( "download_dependencies is False and dependencies_dir is None. Copying the source files into the " @@ -92,6 +95,7 @@ def __init__(self, source_dir, artifacts_dir, scratch_dir, manifest_path, runtim # If a requirements.txt exists, run pip builder before copy action. if self.download_dependencies: + self._use_pip = True if self.dependencies_dir: # clean up the dependencies folder before installing self._actions.append(CleanUpAction(self.dependencies_dir)) @@ -161,4 +165,6 @@ def _should_create_parent_packages(self): return False def get_validators(self): + if not self._use_pip: + return [RuntimeValidator(runtime=self.runtime, architecture=self.architecture)] return [PythonRuntimeValidator(runtime=self.runtime, architecture=self.architecture)] diff --git a/tests/integration/workflows/python_pip/test_python_pip.py b/tests/integration/workflows/python_pip/test_python_pip.py index 80591b40a..28b439e6a 100644 --- a/tests/integration/workflows/python_pip/test_python_pip.py +++ b/tests/integration/workflows/python_pip/test_python_pip.py @@ -303,6 +303,39 @@ def test_must_log_warning_if_requirements_not_found(self): "requirements.txt file not found. Continuing the build without dependencies." ) + def test_build_succeeds_with_mismatched_runtime_when_no_requirements(self): + """ + When no requirements.txt exists, build should succeed even if the target runtime + binary validation would fail (issue #678). The base RuntimeValidator is used + which only checks if the runtime is supported, not if the binary exists. + """ + # Mock PythonRuntimeValidator to always fail - simulating missing Python binary + with mock.patch( + "aws_lambda_builders.workflows.python_pip.workflow.PythonRuntimeValidator" + ) as mock_validator_class: + mock_validator = mock.Mock() + mock_validator.validate.side_effect = Exception("Python binary not found") + mock_validator_class.return_value = mock_validator + + # This should succeed because without requirements.txt, we use base RuntimeValidator + # not PythonRuntimeValidator + self.builder.build( + self.source_dir, + self.artifacts_dir, + self.scratch_dir, + os.path.join("non", "existent", "manifest"), + runtime=self.runtime, + experimental_flags=self.experimental_flags, + ) + + # Should just copy source files + expected_files = self.test_data_files + output_files = set(os.listdir(self.artifacts_dir)) + self.assertEqual(expected_files, output_files) + + # Verify PythonRuntimeValidator was never instantiated (we used base RuntimeValidator) + mock_validator_class.assert_not_called() + @skipIf(IS_WINDOWS, "Skip in windows tests") def test_without_download_dependencies_with_dependencies_dir(self): source_dir = os.path.join(self.source_dir, "local-dependencies") diff --git a/tests/unit/workflows/python_pip/test_workflow.py b/tests/unit/workflows/python_pip/test_workflow.py index 71d5b7e19..b8ec16da8 100644 --- a/tests/unit/workflows/python_pip/test_workflow.py +++ b/tests/unit/workflows/python_pip/test_workflow.py @@ -5,6 +5,7 @@ from aws_lambda_builders.actions import CopySourceAction, CleanUpAction, LinkSourceAction from aws_lambda_builders.path_resolver import PathResolver +from aws_lambda_builders.validator import RuntimeValidator from aws_lambda_builders.workflows.python_pip.utils import OSUtils, EXPERIMENTAL_FLAG_BUILD_PERFORMANCE from aws_lambda_builders.workflows.python_pip.validator import PythonRuntimeValidator from aws_lambda_builders.workflows.python_pip.workflow import PythonPipBuildAction, PythonPipWorkflow @@ -60,6 +61,41 @@ def test_workflow_validator(self): for validator in self.workflow.get_validators(): self.assertTrue(isinstance(validator, PythonRuntimeValidator)) + def test_workflow_validator_without_requirements_skips_python_validation(self): + """When no requirements.txt exists, should use base RuntimeValidator (no Python binary check).""" + self.osutils_mock.file_exists.return_value = False + self.workflow = PythonPipWorkflow( + "source", + "artifacts", + "scratch_dir", + "manifest", + runtime="python3.12", + osutils=self.osutils_mock, + experimental_flags=self.experimental_flags, + ) + validators = self.workflow.get_validators() + self.assertEqual(len(validators), 1) + self.assertIsInstance(validators[0], RuntimeValidator) + self.assertNotIsInstance(validators[0], PythonRuntimeValidator) + + def test_workflow_validator_without_download_dependencies_skips_python_validation(self): + """When download_dependencies=False, should use base RuntimeValidator (no Python binary check).""" + self.osutils_mock.file_exists.return_value = True + self.workflow = PythonPipWorkflow( + "source", + "artifacts", + "scratch_dir", + "manifest", + runtime="python3.12", + osutils=self.osutils_mock, + download_dependencies=False, + experimental_flags=self.experimental_flags, + ) + validators = self.workflow.get_validators() + self.assertEqual(len(validators), 1) + self.assertIsInstance(validators[0], RuntimeValidator) + self.assertNotIsInstance(validators[0], PythonRuntimeValidator) + def test_workflow_resolver(self): for resolver in self.workflow.get_resolvers(): self.assertTrue(isinstance(resolver, PathResolver))