From 4826177a63f953c881f644d13b8166779a783440 Mon Sep 17 00:00:00 2001 From: Mohamed Elasmar <71043312+moelasmar@users.noreply.github.com> Date: Thu, 18 Aug 2022 13:47:09 -0700 Subject: [PATCH] feat: Add custom working directory to custom builder (#4112) * feat: Add custom working directory to custom builder * apply black * remove the test cases that depend on the new lambda builders version. --- samcli/lib/build/app_builder.py | 84 +++- ..._makefile_path_and_custom_working_dir.yaml | 24 + .../custom_build_with_custom_working_dir.yaml | 22 + .../custom_makefile/Makefile | 5 + .../working_dir/__init__.py | 0 .../working_dir/main.py | 4 + .../working_dir/requirements.txt | 1 + .../custom_working_dir_src_code/Makefile | 5 + .../working_dir/__init__.py | 0 .../working_dir/main.py | 4 + .../working_dir/requirements.txt | 1 + .../unit/lib/build_module/test_app_builder.py | 428 +++++++++++++++++- 12 files changed, 565 insertions(+), 13 deletions(-) create mode 100644 tests/integration/testdata/buildcmd/custom_build_with_custom_root_project_path_custom_makefile_path_and_custom_working_dir.yaml create mode 100644 tests/integration/testdata/buildcmd/custom_build_with_custom_working_dir.yaml create mode 100644 tests/integration/testdata/buildcmd/custom_root_dir_custom_makefile_and_custom_working_dir/custom_makefile/Makefile create mode 100644 tests/integration/testdata/buildcmd/custom_root_dir_custom_makefile_and_custom_working_dir/working_dir/__init__.py create mode 100644 tests/integration/testdata/buildcmd/custom_root_dir_custom_makefile_and_custom_working_dir/working_dir/main.py create mode 100644 tests/integration/testdata/buildcmd/custom_root_dir_custom_makefile_and_custom_working_dir/working_dir/requirements.txt create mode 100644 tests/integration/testdata/buildcmd/custom_working_dir_src_code/Makefile create mode 100644 tests/integration/testdata/buildcmd/custom_working_dir_src_code/working_dir/__init__.py create mode 100644 tests/integration/testdata/buildcmd/custom_working_dir_src_code/working_dir/main.py create mode 100644 tests/integration/testdata/buildcmd/custom_working_dir_src_code/working_dir/requirements.txt diff --git a/samcli/lib/build/app_builder.py b/samcli/lib/build/app_builder.py index 6930ea4240..ca1ace3e8b 100644 --- a/samcli/lib/build/app_builder.py +++ b/samcli/lib/build/app_builder.py @@ -514,7 +514,15 @@ def _build_layer( # By default prefer to build in-process for speed build_runtime = specified_workflow - options = ApplicationBuilder._get_build_options(layer_name, config.language, None) + options = ApplicationBuilder._get_build_options( + layer_name, + config.language, + self._base_dir, + None, + metadata=layer_metadata, + source_code_path=code_dir, + scratch_dir=scratch_dir, + ) if self._container_manager: # None key represents the global build image for all functions/layers if config.language == "provided": @@ -646,7 +654,14 @@ def _build_function( # pylint: disable=R1710 ) options = ApplicationBuilder._get_build_options( - function_name, config.language, handler, config.dependency_manager, metadata + function_name, + config.language, + self._base_dir, + handler, + config.dependency_manager, + metadata, + source_code_path=code_dir, + scratch_dir=scratch_dir, ) # By default prefer to build in-process for speed if self._container_manager: @@ -688,9 +703,12 @@ def _build_function( # pylint: disable=R1710 def _get_build_options( function_name: str, language: str, + base_dir: str, handler: Optional[str], dependency_manager: Optional[str] = None, metadata: Optional[dict] = None, + source_code_path: Optional[str] = None, + scratch_dir: Optional[str] = None, ) -> Optional[Dict]: """ Parameters @@ -699,12 +717,18 @@ def _get_build_options( current function resource name language str language of the runtime + base_dir str + Path to a folder. Use this folder as the root to resolve relative source code paths against handler str Handler value of the Lambda Function Resource dependency_manager str Dependency manager to check in addition to language - metadata + metadata Dict Metadata object to search for build properties + source_code_path str + The lambda function source code path that will be used to calculate the working directory + scratch_dir str + The temporary directory path where the lambda function code will be copied to. Returns ------- dict @@ -723,12 +747,62 @@ def _get_build_options( normalized_build_props["entry_points"] = entry_points return normalized_build_props - _build_options: Dict = { + _build_options: Dict[str, Dict] = { "go": {"artifact_executable_name": handler}, "provided": {"build_logical_id": function_name}, "nodejs": {"use_npm_ci": build_props.get("UseNpmCi", False)}, } - return _build_options.get(language, None) + options = _build_options.get(language, None) + if language == "provided": + options = options if options else {} + working_directory = ApplicationBuilder._get_working_directory_path( + base_dir, metadata, source_code_path, scratch_dir + ) + if working_directory: + options = {**options, "working_directory": working_directory} + return options + + @staticmethod + def _get_working_directory_path( + base_dir: str, metadata: Optional[Dict], source_code_path: Optional[str], scratch_dir: Optional[str] + ) -> Optional[str]: + """ + Get the working directory from the lambda resource metadata information, and check if it is not None, and it + is a child path to the source directory path, then return the working directory as a child to the scratch + directory. + + Parameters + ---------- + base_dir : str + Path to a folder. Use this folder as the root to resolve relative source code paths against + metadata Dict + Lambda resource metadata object to search for build properties + source_code_path str + The lambda resource source code path that will be used to calculate the working directory + scratch_dir str + The temporary directory path where the lambda resource code will be copied to. + Returns + ------- + str + The working directory path or None if there is no working_dir metadata info. + """ + working_directory = None + if metadata and isinstance(metadata, dict): + working_directory = metadata.get("WorkingDirectory") + if working_directory: + working_directory = str(pathlib.Path(base_dir, working_directory).resolve()) + + # check if the working directory is a child of the lambda resource source code path, to update the + # working directory to be child of the scratch directory + if ( + source_code_path + and scratch_dir + and os.path.commonpath([source_code_path, working_directory]) == os.path.normpath(source_code_path) + ): + working_directory = os.path.relpath(working_directory, source_code_path) + working_directory = os.path.normpath(os.path.join(scratch_dir, working_directory)) + + return working_directory def _build_function_in_process( self, diff --git a/tests/integration/testdata/buildcmd/custom_build_with_custom_root_project_path_custom_makefile_path_and_custom_working_dir.yaml b/tests/integration/testdata/buildcmd/custom_build_with_custom_root_project_path_custom_makefile_path_and_custom_working_dir.yaml new file mode 100644 index 0000000000..8f16eac758 --- /dev/null +++ b/tests/integration/testdata/buildcmd/custom_build_with_custom_root_project_path_custom_makefile_path_and_custom_working_dir.yaml @@ -0,0 +1,24 @@ +AWSTemplateFormatVersion : '2010-09-09' +Transform: AWS::Serverless-2016-10-31 + +Parameteres: + Runtime: + Type: String + CodeUri: + Type: String + Handler: + Type: String + +Resources: + + Function: + Type: AWS::Serverless::Function + Properties: + Handler: !Ref Handler + Runtime: !Ref Runtime + CodeUri: !Ref CodeUri + Timeout: 600 + Metadata: + ContextPath: "./custom_root_dir_custom_makefile_and_custom_working_dir/custom_makefile" + ProjectRootDirectory: "./custom_root_dir_custom_makefile_and_custom_working_dir" + WorkingDirectory: "./custom_root_dir_custom_makefile_and_custom_working_dir/working_dir" diff --git a/tests/integration/testdata/buildcmd/custom_build_with_custom_working_dir.yaml b/tests/integration/testdata/buildcmd/custom_build_with_custom_working_dir.yaml new file mode 100644 index 0000000000..498b87eabc --- /dev/null +++ b/tests/integration/testdata/buildcmd/custom_build_with_custom_working_dir.yaml @@ -0,0 +1,22 @@ +AWSTemplateFormatVersion : '2010-09-09' +Transform: AWS::Serverless-2016-10-31 + +Parameteres: + Runtime: + Type: String + CodeUri: + Type: String + Handler: + Type: String + +Resources: + + Function: + Type: AWS::Serverless::Function + Properties: + Handler: !Ref Handler + Runtime: !Ref Runtime + CodeUri: !Ref CodeUri + Timeout: 600 + Metadata: + WorkingDirectory: "./custom_working_dir_src_code/working_dir" diff --git a/tests/integration/testdata/buildcmd/custom_root_dir_custom_makefile_and_custom_working_dir/custom_makefile/Makefile b/tests/integration/testdata/buildcmd/custom_root_dir_custom_makefile_and_custom_working_dir/custom_makefile/Makefile new file mode 100644 index 0000000000..c6e094235a --- /dev/null +++ b/tests/integration/testdata/buildcmd/custom_root_dir_custom_makefile_and_custom_working_dir/custom_makefile/Makefile @@ -0,0 +1,5 @@ +build-Function: + cp *.py $(ARTIFACTS_DIR) + cp requirements.txt $(ARTIFACTS_DIR) + python -m pip install -r requirements.txt -t $(ARTIFACTS_DIR) + rm -rf $(ARTIFACTS_DIR)/bin \ No newline at end of file diff --git a/tests/integration/testdata/buildcmd/custom_root_dir_custom_makefile_and_custom_working_dir/working_dir/__init__.py b/tests/integration/testdata/buildcmd/custom_root_dir_custom_makefile_and_custom_working_dir/working_dir/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/integration/testdata/buildcmd/custom_root_dir_custom_makefile_and_custom_working_dir/working_dir/main.py b/tests/integration/testdata/buildcmd/custom_root_dir_custom_makefile_and_custom_working_dir/working_dir/main.py new file mode 100644 index 0000000000..0c66c2b32f --- /dev/null +++ b/tests/integration/testdata/buildcmd/custom_root_dir_custom_makefile_and_custom_working_dir/working_dir/main.py @@ -0,0 +1,4 @@ +import requests + +def handler(event, context): + return requests.__version__ diff --git a/tests/integration/testdata/buildcmd/custom_root_dir_custom_makefile_and_custom_working_dir/working_dir/requirements.txt b/tests/integration/testdata/buildcmd/custom_root_dir_custom_makefile_and_custom_working_dir/working_dir/requirements.txt new file mode 100644 index 0000000000..822be7520a --- /dev/null +++ b/tests/integration/testdata/buildcmd/custom_root_dir_custom_makefile_and_custom_working_dir/working_dir/requirements.txt @@ -0,0 +1 @@ +requests==2.23.0 \ No newline at end of file diff --git a/tests/integration/testdata/buildcmd/custom_working_dir_src_code/Makefile b/tests/integration/testdata/buildcmd/custom_working_dir_src_code/Makefile new file mode 100644 index 0000000000..c6e094235a --- /dev/null +++ b/tests/integration/testdata/buildcmd/custom_working_dir_src_code/Makefile @@ -0,0 +1,5 @@ +build-Function: + cp *.py $(ARTIFACTS_DIR) + cp requirements.txt $(ARTIFACTS_DIR) + python -m pip install -r requirements.txt -t $(ARTIFACTS_DIR) + rm -rf $(ARTIFACTS_DIR)/bin \ No newline at end of file diff --git a/tests/integration/testdata/buildcmd/custom_working_dir_src_code/working_dir/__init__.py b/tests/integration/testdata/buildcmd/custom_working_dir_src_code/working_dir/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/integration/testdata/buildcmd/custom_working_dir_src_code/working_dir/main.py b/tests/integration/testdata/buildcmd/custom_working_dir_src_code/working_dir/main.py new file mode 100644 index 0000000000..0c66c2b32f --- /dev/null +++ b/tests/integration/testdata/buildcmd/custom_working_dir_src_code/working_dir/main.py @@ -0,0 +1,4 @@ +import requests + +def handler(event, context): + return requests.__version__ diff --git a/tests/integration/testdata/buildcmd/custom_working_dir_src_code/working_dir/requirements.txt b/tests/integration/testdata/buildcmd/custom_working_dir_src_code/working_dir/requirements.txt new file mode 100644 index 0000000000..822be7520a --- /dev/null +++ b/tests/integration/testdata/buildcmd/custom_working_dir_src_code/working_dir/requirements.txt @@ -0,0 +1 @@ +requests==2.23.0 \ No newline at end of file diff --git a/tests/unit/lib/build_module/test_app_builder.py b/tests/unit/lib/build_module/test_app_builder.py index 6b469d1e7a..bf93496207 100644 --- a/tests/unit/lib/build_module/test_app_builder.py +++ b/tests/unit/lib/build_module/test_app_builder.py @@ -570,7 +570,139 @@ def test_must_build_layer_in_process(self, get_layer_subfolder_mock, osutils_moc @patch("samcli.lib.build.app_builder.get_workflow_config") @patch("samcli.lib.build.app_builder.osutils") @patch("samcli.lib.build.app_builder.get_layer_subfolder") - def test_must_custom_build_layer_with_metadata_in_process( + def test_must_custom_build_layer_with_custom_working_dir_metadata_in_process( + self, get_layer_subfolder_mock, osutils_mock, get_workflow_config_mock + ): + get_layer_subfolder_mock.return_value = "" + config_mock = Mock() + config_mock.manifest_name = "Makefile" + config_mock.language = "provided" + + scratch_dir = "scratch" + osutils_mock.mkdir_temp.return_value.__enter__ = Mock(return_value=scratch_dir) + osutils_mock.mkdir_temp.return_value.__exit__ = Mock() + + get_workflow_config_mock.return_value = config_mock + build_function_in_process_mock = Mock() + + metadata = { + "WorkingDirectory": "/working/dir", + } + options_mock = { + "logical_id": "layer1", + "working_directory": "working_dir", + } + + get_build_options_mock = Mock() + get_build_options_mock.return_value = options_mock + + builder = ApplicationBuilder( + self.resources_to_build_collector, "builddir", "basedir", "cachedir", stream_writer=StreamWriter(sys.stderr) + ) + + get_build_options = ApplicationBuilder._get_build_options + ApplicationBuilder._get_build_options = get_build_options_mock + builder._build_function_in_process = build_function_in_process_mock + builder._build_layer( + "layer_name", "code_uri", "provided", ["python3.8"], ARM64, "full_path", layer_metadata=metadata + ) + ApplicationBuilder._get_build_options = get_build_options + + build_function_in_process_mock.assert_called_once_with( + config_mock, + PathValidator(os.path.join("basedir", "code_uri")), + PathValidator("full_path"), + "scratch", + PathValidator(os.path.join("basedir", "code_uri", "Makefile")), + "provided", + ARM64, + options_mock, + None, + True, + True, + is_building_layer=True, + ) + + get_build_options_mock.assert_called_once_with( + "layer_name", + "provided", + "basedir", + None, + metadata=metadata, + source_code_path=PathValidator(os.path.join("basedir", "code_uri")), + scratch_dir="scratch", + ) + + @patch("samcli.lib.build.app_builder.get_workflow_config") + @patch("samcli.lib.build.app_builder.osutils") + @patch("samcli.lib.build.app_builder.get_layer_subfolder") + def test_must_custom_build_layer_with_custom_makefile_and_custom_project_root_metadata_properties_in_process( + self, get_layer_subfolder_mock, osutils_mock, get_workflow_config_mock + ): + get_layer_subfolder_mock.return_value = "" + config_mock = Mock() + config_mock.manifest_name = "Makefile" + config_mock.language = "provided" + + scratch_dir = "scratch" + osutils_mock.mkdir_temp.return_value.__enter__ = Mock(return_value=scratch_dir) + osutils_mock.mkdir_temp.return_value.__exit__ = Mock() + + get_workflow_config_mock.return_value = config_mock + build_function_in_process_mock = Mock() + + metadata = { + "ProjectRootDirectory": "/src/code/path", + "ContextPath": "/make/file/dir", + } + options_mock = { + "logical_id": "layer1", + } + + get_build_options_mock = Mock() + get_build_options_mock.return_value = options_mock + + builder = ApplicationBuilder( + self.resources_to_build_collector, "builddir", "basedir", "cachedir", stream_writer=StreamWriter(sys.stderr) + ) + + get_build_options = ApplicationBuilder._get_build_options + ApplicationBuilder._get_build_options = get_build_options_mock + builder._build_function_in_process = build_function_in_process_mock + builder._build_layer( + "layer_name", "code_uri", "provided", ["python3.8"], ARM64, "full_path", layer_metadata=metadata + ) + ApplicationBuilder._get_build_options = get_build_options + + build_function_in_process_mock.assert_called_once_with( + config_mock, + PathValidator(os.path.join("src", "code", "path")), + PathValidator("full_path"), + "scratch", + PathValidator(os.path.join("make", "file", "dir", "Makefile")), + "provided", + ARM64, + options_mock, + None, + True, + True, + is_building_layer=True, + ) + + get_build_options_mock.assert_called_once_with( + "layer_name", + "provided", + "basedir", + None, + metadata=metadata, + source_code_path=PathValidator(os.path.join("src", "code", "path")), + scratch_dir="scratch", + ) + + @patch("samcli.lib.build.app_builder.get_workflow_config") + @patch("samcli.lib.build.app_builder.osutils") + @patch("samcli.lib.build.app_builder.get_layer_subfolder") + def test_must_custom_build_layer_with_all_metadata_in_process( self, get_layer_subfolder_mock, osutils_mock, get_workflow_config_mock ): get_layer_subfolder_mock.return_value = "" @@ -588,9 +720,11 @@ def test_must_custom_build_layer_with_metadata_in_process( metadata = { "ProjectRootDirectory": "/src/code/path", "ContextPath": "/make/file/dir", + "WorkingDirectory": "/working/dir", } options_mock = { "logical_id": "layer1", + "working_directory": "working_dir", } get_build_options_mock = Mock() @@ -626,7 +760,11 @@ def test_must_custom_build_layer_with_metadata_in_process( get_build_options_mock.assert_called_once_with( "layer_name", "provided", + "basedir", None, + metadata=metadata, + source_code_path=PathValidator(os.path.join("src", "code", "path")), + scratch_dir="scratch", ) @patch("samcli.lib.build.app_builder.get_workflow_config") @@ -687,7 +825,11 @@ def test_must_custom_build_layer_with_context_path_metadata_in_process( get_build_options_mock.assert_called_once_with( "layer_name", "provided", + "basedir", None, + metadata=metadata, + source_code_path=PathValidator("code_uri"), + scratch_dir="scratch", ) @patch("samcli.lib.build.app_builder.get_workflow_config") @@ -748,7 +890,11 @@ def test_must_custom_build_layer_with_project_root_directory_only_metadata_in_pr get_build_options_mock.assert_called_once_with( "layer_name", "provided", + "basedir", None, + metadata=metadata, + source_code_path=PathValidator(os.path.join("src", "code", "path")), + scratch_dir="scratch", ) @patch("samcli.lib.build.app_builder.get_workflow_config") @@ -807,7 +953,11 @@ def test_must_custom_build_layer_with_empty_metadata_in_process( get_build_options_mock.assert_called_once_with( "layer_name", "provided", + "basedir", None, + metadata=metadata, + source_code_path=PathValidator("code_uri"), + scratch_dir="scratch", ) @patch("samcli.lib.build.app_builder.get_workflow_config") @@ -1512,7 +1662,85 @@ def test_must_build_in_process(self, osutils_mock, get_workflow_config_mock): @patch("samcli.lib.build.app_builder.get_workflow_config") @patch("samcli.lib.build.app_builder.osutils") - def test_must_custom_build_function_with_metadata_in_process(self, osutils_mock, get_workflow_config_mock): + def test_must_custom_build_function_with_working_dir_metadata_in_process( + self, osutils_mock, get_workflow_config_mock + ): + function_name = "function_name" + codeuri = "path/to/source" + packagetype = ZIP + runtime = "provided" + architecture = X86_64 + scratch_dir = "scratch" + handler = "handler.handle" + + config_mock = Mock() + config_mock.manifest_name = "Makefile" + config_mock.language = "provided" + dependency_manager_mock = Mock() + config_mock.dependency_manager = dependency_manager_mock + + code_dir = str(Path("/base/dir/path/to/source").resolve()) + artifacts_dir = str(Path("/build/dir/function_full_path")) + manifest_path = str(Path(os.path.join(code_dir, config_mock.manifest_name)).resolve()) + + osutils_mock.mkdir_temp.return_value.__enter__ = Mock(return_value=scratch_dir) + osutils_mock.mkdir_temp.return_value.__exit__ = Mock() + + get_workflow_config_mock.return_value = config_mock + build_function_in_process_mock = Mock() + + metadata = { + "WorkingDirectory": "/working/dir", + } + options_mock = { + "logical_id": function_name, + "working_directory": "working_dir", + } + + get_build_options_mock = Mock() + get_build_options_mock.return_value = options_mock + + builder = ApplicationBuilder( + Mock(), "/build/dir", "/base/dir", "cachedir", stream_writer=StreamWriter(sys.stderr) + ) + + get_build_options = ApplicationBuilder._get_build_options + ApplicationBuilder._get_build_options = get_build_options_mock + builder._build_function_in_process = build_function_in_process_mock + builder._build_function(function_name, codeuri, ZIP, runtime, architecture, handler, artifacts_dir, metadata) + + ApplicationBuilder._get_build_options = get_build_options + + build_function_in_process_mock.assert_called_once_with( + config_mock, + PathValidator(code_dir), + PathValidator("function_full_path"), + "scratch", + PathValidator(manifest_path), + "provided", + architecture, + options_mock, + None, + True, + True, + ) + + get_build_options_mock.assert_called_once_with( + function_name, + "provided", + "/base/dir", + handler, + dependency_manager_mock, + metadata, + source_code_path=PathValidator(code_dir), + scratch_dir="scratch", + ) + + @patch("samcli.lib.build.app_builder.get_workflow_config") + @patch("samcli.lib.build.app_builder.osutils") + def test_must_custom_build_function_with_custom_makefile_and_custom_project_root_metadata_properties_in_process( + self, osutils_mock, get_workflow_config_mock + ): function_name = "function_name" codeuri = "path/to/source" packagetype = ZIP @@ -1576,9 +1804,90 @@ def test_must_custom_build_function_with_metadata_in_process(self, osutils_mock, get_build_options_mock.assert_called_once_with( function_name, "provided", + "/base/dir", handler, dependency_manager_mock, metadata, + source_code_path=PathValidator(os.path.join("src", "code", "path")), + scratch_dir="scratch", + ) + + @patch("samcli.lib.build.app_builder.get_workflow_config") + @patch("samcli.lib.build.app_builder.osutils") + def test_must_custom_build_function_with_all_metadata_sutom_paths_properties_in_process( + self, osutils_mock, get_workflow_config_mock + ): + function_name = "function_name" + codeuri = "path/to/source" + packagetype = ZIP + runtime = "provided" + architecture = X86_64 + scratch_dir = "scratch" + handler = "handler.handle" + + config_mock = Mock() + config_mock.manifest_name = "Makefile" + config_mock.language = "provided" + dependency_manager_mock = Mock() + config_mock.dependency_manager = dependency_manager_mock + + code_dir = str(Path("/base/dir/path/to/source").resolve()) + artifacts_dir = str(Path("/build/dir/function_full_path")) + manifest_path = str(Path(os.path.join(code_dir, config_mock.manifest_name)).resolve()) + + osutils_mock.mkdir_temp.return_value.__enter__ = Mock(return_value=scratch_dir) + osutils_mock.mkdir_temp.return_value.__exit__ = Mock() + + get_workflow_config_mock.return_value = config_mock + build_function_in_process_mock = Mock() + + metadata = { + "ProjectRootDirectory": "/src/code/path", + "ContextPath": "/make/file/dir", + "WorkingDirectory": "/working/dir", + } + options_mock = { + "logical_id": function_name, + "working_directory": "working_dir", + } + + get_build_options_mock = Mock() + get_build_options_mock.return_value = options_mock + + builder = ApplicationBuilder( + Mock(), "/build/dir", "/base/dir", "cachedir", stream_writer=StreamWriter(sys.stderr) + ) + + get_build_options = ApplicationBuilder._get_build_options + ApplicationBuilder._get_build_options = get_build_options_mock + builder._build_function_in_process = build_function_in_process_mock + builder._build_function(function_name, codeuri, ZIP, runtime, architecture, handler, artifacts_dir, metadata) + + ApplicationBuilder._get_build_options = get_build_options + + build_function_in_process_mock.assert_called_once_with( + config_mock, + PathValidator(os.path.join("src", "code", "path")), + PathValidator("function_full_path"), + "scratch", + PathValidator(os.path.join("make", "file", "dir", "Makefile")), + "provided", + architecture, + options_mock, + None, + True, + True, + ) + + get_build_options_mock.assert_called_once_with( + function_name, + "provided", + "/base/dir", + handler, + dependency_manager_mock, + metadata, + source_code_path=PathValidator(os.path.join("src", "code", "path")), + scratch_dir="scratch", ) @patch("samcli.lib.build.app_builder.get_workflow_config") @@ -1649,9 +1958,12 @@ def test_must_custom_build_function_with_only_context_path_metadata_in_process( get_build_options_mock.assert_called_once_with( function_name, "provided", + "/base/dir", handler, dependency_manager_mock, metadata, + source_code_path=code_dir, + scratch_dir="scratch", ) @patch("samcli.lib.build.app_builder.get_workflow_config") @@ -1721,9 +2033,12 @@ def test_must_custom_build_function_with_only_project_root_dir_metadata_in_proce get_build_options_mock.assert_called_once_with( function_name, "provided", + "/base/dir", handler, dependency_manager_mock, metadata, + source_code_path=PathValidator(os.path.join("src", "code", "path")), + scratch_dir="scratch", ) @patch("samcli.lib.build.app_builder.get_workflow_config") @@ -1789,9 +2104,12 @@ def test_must_custom_build_function_with_empty_metadata_in_process(self, osutils get_build_options_mock.assert_called_once_with( function_name, "provided", + "/base/dir", handler, dependency_manager_mock, metadata, + source_code_path=code_dir, + scratch_dir="scratch", ) @patch("samcli.lib.build.app_builder.get_workflow_config") @@ -2338,14 +2656,18 @@ def test_get_options_from_metadata(self): build_properties = {"Minify": False, "Target": "es2017", "Sourcemap": False, "EntryPoints": ["app.ts"]} metadata = {"BuildMethod": "esbuild", "BuildProperties": build_properties} expected_properties = {"minify": False, "target": "es2017", "sourcemap": False, "entry_points": ["app.ts"]} - options = ApplicationBuilder._get_build_options("Function", "Node.js", "handler", "npm-esbuild", metadata) + options = ApplicationBuilder._get_build_options( + "Function", "Node.js", "base_dir", "handler", "npm-esbuild", metadata + ) self.assertEqual(options, expected_properties) def test_get_options_from_metadata_no_entry_points_defined(self): build_properties = {"Minify": False, "Target": "es2017", "Sourcemap": False} metadata = {"BuildMethod": "esbuild", "BuildProperties": build_properties} expected_properties = {"minify": False, "target": "es2017", "sourcemap": False, "entry_points": ["handler"]} - options = ApplicationBuilder._get_build_options("Function", "Node.js", "handler", "npm-esbuild", metadata) + options = ApplicationBuilder._get_build_options( + "Function", "Node.js", "base_dir", "handler", "npm-esbuild", metadata + ) self.assertEqual(options, expected_properties) def test_get_options_from_metadata_correctly_separates_source_and_handler(self): @@ -2358,13 +2680,15 @@ def test_get_options_from_metadata_correctly_separates_source_and_handler(self): "entry_points": ["src/handlers/post"], } options = ApplicationBuilder._get_build_options( - "Function", "Node.js", "src/handlers/post.handler", "npm-esbuild", metadata + "Function", "Node.js", "base_dir", "src/handlers/post.handler", "npm-esbuild", metadata ) self.assertEqual(options, expected_properties) @parameterized.expand([(None, None), ({}, None)]) def test_invalid_metadata_cases(self, metadata, expected_output): - options = ApplicationBuilder._get_build_options("Function", "Node.js", "handler", "npm-esbuild", metadata) + options = ApplicationBuilder._get_build_options( + "Function", "Node.js", "base_dir", "handler", "npm-esbuild", metadata + ) self.assertEqual(options, expected_output) @parameterized.expand( @@ -2380,7 +2704,7 @@ def test_get_options_various_languages_dependency_managers(self, language, depen build_properties = {"UseNpmCi": True} metadata = {"BuildProperties": build_properties} options = ApplicationBuilder._get_build_options( - "Function", language, "app.handler", dependency_manager, metadata + "Function", language, "base_dir", "app.handler", dependency_manager, metadata ) self.assertEqual(options, expected_options) @@ -2394,6 +2718,94 @@ def test_get_options_various_languages_dependency_managers(self, language, depen ) def test_nodejs_metadata_not_defined(self, metadata, language, dependency_manager, expected_options): options = ApplicationBuilder._get_build_options( - "Function", language, "app.handler", dependency_manager, metadata + "Function", language, "base_dir", "app.handler", dependency_manager, metadata ) self.assertEqual(options, expected_options) + + def test_provided_metadata(self): + metadata = { + "WorkingDirectory": "/working/dir", + } + expected_properties = {"build_logical_id": "Function", "working_directory": "/working/dir"} + + get_working_directory_path_mock = Mock() + get_working_directory_path_mock.return_value = "/working/dir" + + get_working_directory_path = ApplicationBuilder._get_working_directory_path + ApplicationBuilder._get_working_directory_path = get_working_directory_path_mock + + options = ApplicationBuilder._get_build_options( + "Function", + "provided", + "base_dir", + "handler", + None, + metadata, + "source_dir", + "scratch_dir", + ) + ApplicationBuilder._get_working_directory_path = get_working_directory_path + self.assertEqual(options, expected_properties) + get_working_directory_path_mock.assert_called_once_with("base_dir", metadata, "source_dir", "scratch_dir") + + def test_provided_metadata_get_working_dir_return_None(self): + metadata = {} + expected_properties = {"build_logical_id": "Function"} + + get_working_directory_path_mock = Mock() + get_working_directory_path_mock.return_value = None + + get_working_directory_path = ApplicationBuilder._get_working_directory_path + ApplicationBuilder._get_working_directory_path = get_working_directory_path_mock + + options = ApplicationBuilder._get_build_options( + "Function", + "provided", + "base_dir", + "handler", + None, + metadata, + "source_dir", + "scratch_dir", + ) + ApplicationBuilder._get_working_directory_path = get_working_directory_path + self.assertEqual(options, expected_properties) + get_working_directory_path_mock.assert_called_once_with("base_dir", metadata, "source_dir", "scratch_dir") + + +class TestApplicationBuilderGetWorkingDirectoryPath(TestCase): + def test_empty_metadata(self): + metadata = {} + working_dir = ApplicationBuilder._get_working_directory_path("base_dir", metadata, "source_dir", "scratch_dir") + self.assertIsNone(working_dir) + + @patch("samcli.lib.build.app_builder.pathlib") + @patch("samcli.lib.build.app_builder.os.path") + def test_metadata_with_working_dir_not_child_source_dir(self, os_path_mock, pathlib_mock): + metadata = { + "WorkingDirectory": str(os.path.join("working", "dir")), + } + os_path_mock.commonpath.return_value = "/not/source/dir" + os_path_mock.normpath.return_value = "source_dir" + path_mock = Mock() + pathlib_mock.Path.return_value = path_mock + path_mock.resolve.return_value = str(os.path.join("working", "dir")) + working_dir = ApplicationBuilder._get_working_directory_path("base_dir", metadata, "source_dir", "scratch_dir") + self.assertEquals(working_dir, PathValidator(str(os.path.join("working", "dir")))) + + @patch("samcli.lib.build.app_builder.pathlib") + @patch("samcli.lib.build.app_builder.os.path") + def test_metadata_with_working_dir_child_source_dir(self, os_path_mock, pathlib_mock): + metadata = { + "WorkingDirectory": str(os.path.join("source_dir", "working", "dir")), + } + os_path_mock.commonpath.return_value = "source_dir" + os_path_mock.normpath.side_effect = ["source_dir", os.path.join("source_dir", "working", "dir")] + os_path_mock.relpath.return_value = "./working/dir" + os_path_mock.join.return_value = "source_dir/working/dir" + path_mock = Mock() + pathlib_mock.Path.return_value = path_mock + path_mock.resolve.return_value = str(os.path.join("source_dir", "working", "dir")) + + working_dir = ApplicationBuilder._get_working_directory_path("base_dir", metadata, "source_dir", "scratch_dir") + self.assertEquals(working_dir, PathValidator(str(os.path.join("source_dir", "working", "dir"))))