Skip to content

Commit 841aeb9

Browse files
authored
feat: add experimental uv support (#8549)
* feat: add experimental uv support * test: add uv build integration test * feat: make uv accessible by BuildMethod template parameter * fix: clean up experimental flag method * chore misc formatting * nit: remove reference to refactored method * test: Add unit test for experimental build method * test: add unit test for uv workflow selection * test: add integration tests for uv * ci: setup uv in tests * fix: lint and making param optional * feat: allow for manifest_path to be None * nit: get rid of debug option * nit: add comment to uv config
1 parent 12edcf2 commit 841aeb9

17 files changed

Lines changed: 454 additions & 51 deletions

File tree

.github/workflows/build.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ jobs:
6868
- uses: actions/setup-python@v6
6969
with:
7070
python-version: ${{ matrix.python }}
71+
- uses: astral-sh/setup-uv@v7
7172
- run: test -f "./.github/ISSUE_TEMPLATE/Bug_report.md" # prevent Bug_report.md from being renamed or deleted
7273
- run: make pr
7374

.github/workflows/integration-tests.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,9 @@ jobs:
121121
3.13
122122
3.14
123123
124+
- name: Install uv
125+
uses: astral-sh/setup-uv@v7
126+
124127
- name: Set up Node.js
125128
if: contains(fromJSON('["build-integ", "build-integ-java-python-provided", "build-integ-dotnet-node-ruby", "build-integ-arm64"]'), matrix.test_suite) && matrix.container_runtime == 'no-container'
126129
uses: actions/setup-node@v6

samcli/commands/_utils/experimental.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ class ExperimentalFlag:
5555
)
5656
}
5757
RustCargoLambda = ExperimentalEntry("experimentalCargoLambda", EXPERIMENTAL_ENV_VAR_PREFIX + "RUST_CARGO_LAMBDA")
58+
UvPackageManager = ExperimentalEntry(
59+
"experimentalUvPackageManager", EXPERIMENTAL_ENV_VAR_PREFIX + "UV_PACKAGE_MANAGER"
60+
)
5861

5962

6063
def is_experimental_enabled(config_entry: ExperimentalEntry) -> bool:

samcli/commands/build/build_context.py

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,7 @@ def run(self) -> None:
281281
)
282282

283283
self._check_exclude_warning()
284-
self._check_rust_cargo_experimental_flag()
284+
self._check_build_method_experimental_flag()
285285

286286
for f in self.get_resources_to_build().functions:
287287
EventTracker.track_event(EventName.BUILD_FUNCTION_RUNTIME.value, f.runtime)
@@ -696,24 +696,26 @@ def _check_exclude_warning(self) -> None:
696696
if self._resource_identifier in excludes:
697697
LOG.warning(self._EXCLUDE_WARNING_MESSAGE)
698698

699-
def _check_rust_cargo_experimental_flag(self) -> None:
699+
def _check_build_method_experimental_flag(self) -> None:
700700
"""
701701
Prints warning message and confirms if user wants to use beta feature
702702
"""
703-
WARNING_MESSAGE = (
704-
'Build method "rust-cargolambda" is a beta feature.\n'
705-
"Please confirm if you would like to proceed\n"
706-
'You can also enable this beta feature with "sam build --beta-features".'
707-
)
703+
EXPERIMENTAL_BUILD_METHODS = {
704+
"rust-cargolambda": ExperimentalFlag.RustCargoLambda,
705+
"python-uv": ExperimentalFlag.UvPackageManager,
706+
}
707+
708708
resources_to_build = self.get_resources_to_build()
709-
is_building_rust = False
710709
for function in resources_to_build.functions:
711-
if function.metadata and function.metadata.get("BuildMethod", "") == "rust-cargolambda":
712-
is_building_rust = True
713-
break
710+
if function.metadata and function.metadata.get("BuildMethod", "") in EXPERIMENTAL_BUILD_METHODS:
711+
build_method = function.metadata.get("BuildMethod", "")
712+
WARNING_MESSAGE = (
713+
f'Build method "{build_method}" is a beta feature.\n'
714+
"Please confirm if you would like to proceed\n"
715+
'You can also enable this beta feature with "sam build --beta-features".'
716+
)
714717

715-
if is_building_rust:
716-
prompt_experimental(ExperimentalFlag.RustCargoLambda, WARNING_MESSAGE)
718+
prompt_experimental(EXPERIMENTAL_BUILD_METHODS[build_method], WARNING_MESSAGE)
717719

718720
@property
719721
def build_in_source(self) -> Optional[bool]:

samcli/lib/build/app_builder.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -576,7 +576,11 @@ def _build_layer(
576576
manifest_context_path = str(
577577
pathlib.Path(self._base_dir, layer_metadata.get("ContextPath", code_dir)).resolve()
578578
)
579-
manifest_path = self._manifest_path_override or os.path.join(manifest_context_path, config.manifest_name)
579+
manifest_path = (
580+
self._manifest_path_override
581+
if self._manifest_path_override
582+
else (os.path.join(manifest_context_path, config.manifest_name) if config.manifest_name else None)
583+
)
580584

581585
# By default prefer to build in-process for speed
582586
scratch_dir_path = (
@@ -731,8 +735,10 @@ def _build_function( # pylint: disable=R1710
731735
manifest_context_path = str(
732736
pathlib.Path(self._base_dir, metadata.get("ContextPath", code_dir)).resolve()
733737
)
734-
manifest_path = self._manifest_path_override or os.path.join(
735-
manifest_context_path, config.manifest_name
738+
manifest_path = (
739+
self._manifest_path_override
740+
if self._manifest_path_override
741+
else (os.path.join(manifest_context_path, config.manifest_name) if config.manifest_name else None)
736742
)
737743
scratch_dir_path = (
738744
LambdaBuildContainer.get_container_dirs(code_dir, manifest_path)["scratch_dir"]
@@ -916,7 +922,7 @@ def _build_function_in_process(
916922
source_dir: str,
917923
artifacts_dir: str,
918924
scratch_dir: str,
919-
manifest_path: str,
925+
manifest_path: Optional[str],
920926
runtime: str,
921927
architecture: str,
922928
options: Optional[Dict],
@@ -962,7 +968,7 @@ def _build_function_on_container(
962968
config: CONFIG,
963969
source_dir: str,
964970
artifacts_dir: str,
965-
manifest_path: str,
971+
manifest_path: Optional[str],
966972
runtime: str,
967973
architecture: str,
968974
options: Optional[Dict],

samcli/lib/build/workflow_config.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
NODEJS_NPM_ESBUILD_CONFIG,
1818
PROVIDED_MAKE_CONFIG,
1919
PYTHON_PIP_CONFIG,
20+
PYTHON_UV_CONFIG,
2021
RUBY_BUNDLER_CONFIG,
2122
RUST_CARGO_LAMBDA_CONFIG,
2223
)
@@ -155,6 +156,7 @@ def get_workflow_config(
155156
"dotnet7": BasicWorkflowSelector(DOTNET_CLIPACKAGE_CONFIG),
156157
"dotnet": BasicWorkflowSelector(DOTNET_CLIPACKAGE_CONFIG),
157158
"rust-cargolambda": BasicWorkflowSelector(RUST_CARGO_LAMBDA_CONFIG),
159+
"python-uv": BasicWorkflowSelector(PYTHON_UV_CONFIG),
158160
}
159161

160162
selectors_by_runtime = {

samcli/lib/build/workflows.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,17 @@
2424
must_mount_with_write_in_container=False,
2525
)
2626

27+
PYTHON_UV_CONFIG = CONFIG(
28+
language="python",
29+
dependency_manager="uv",
30+
application_framework=None,
31+
# Leaving manifest_name as None here because technically uv can read from pyproject or requirements.
32+
# Having it set to either will mess with the autodetection logic in lambda builders.
33+
manifest_name=None,
34+
executable_search_paths=None,
35+
must_mount_with_write_in_container=False,
36+
)
37+
2738
NODEJS_NPM_CONFIG = CONFIG(
2839
language="nodejs",
2940
dependency_manager="npm",
@@ -117,6 +128,7 @@
117128

118129
ALL_CONFIGS: List[CONFIG] = [
119130
PYTHON_PIP_CONFIG,
131+
PYTHON_UV_CONFIG,
120132
NODEJS_NPM_CONFIG,
121133
RUBY_BUNDLER_CONFIG,
122134
JAVA_GRADLE_CONFIG,

tests/integration/buildcmd/build_integ_base.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -743,12 +743,11 @@ def _verify_built_artifact(self, build_dir, function_logical_id, expected_files,
743743

744744
@pytest.mark.python
745745
class BuildIntegPythonBase(BuildIntegBase):
746-
EXPECTED_FILES_PROJECT_MANIFEST = {
746+
EXPECTED_FILES = {
747747
"__init__.py",
748748
"main.py",
749749
"numpy",
750750
# 'cryptography',
751-
"requirements.txt",
752751
}
753752

754753
FUNCTION_LOGICAL_ID = "Function"
@@ -760,19 +759,23 @@ def _test_with_default_requirements(
760759
codeuri,
761760
use_container,
762761
relative_path,
762+
manifest="requirements.txt",
763763
do_override=True,
764764
check_function_only=False,
765765
architecture=None,
766+
beta_features=False,
766767
):
767768
if use_container and (SKIP_DOCKER_TESTS or SKIP_DOCKER_BUILD):
768769
self.skipTest(SKIP_DOCKER_MESSAGE)
769770
overrides = self.get_override(runtime, codeuri, architecture, "main.handler") if do_override else None
770-
cmdlist = self.get_command_list(use_container=use_container, parameter_overrides=overrides)
771+
cmdlist = self.get_command_list(
772+
use_container=use_container, parameter_overrides=overrides, beta_features=beta_features
773+
)
771774

772775
run_command(cmdlist, cwd=self.working_dir)
773776

774777
self._verify_built_artifact(
775-
self.default_build_dir, self.FUNCTION_LOGICAL_ID, self.EXPECTED_FILES_PROJECT_MANIFEST
778+
self.default_build_dir, self.FUNCTION_LOGICAL_ID, (self.EXPECTED_FILES | {manifest})
776779
)
777780

778781
if not check_function_only:

0 commit comments

Comments
 (0)