diff --git a/samcli/lib/build/app_builder.py b/samcli/lib/build/app_builder.py index 3ca2056769..417b818058 100644 --- a/samcli/lib/build/app_builder.py +++ b/samcli/lib/build/app_builder.py @@ -2,10 +2,13 @@ Builds the application """ +import base64 +import operator import os import io import json import logging +from itertools import groupby try: import pathlib @@ -100,12 +103,23 @@ def build(self): result = {} - for lambda_function in self._functions_to_build: + grouped_functions = groupby(sorted(self._functions_to_build, + key=operator.attrgetter("runtime", "codeuri", "name")), + operator.attrgetter("runtime", "codeuri")) - LOG.info("Building resource '%s'", lambda_function.name) - result[lambda_function.name] = self._build_function(lambda_function.name, - lambda_function.codeuri, - lambda_function.runtime) + groups = dict() + for group_key, iterator in grouped_functions: + groups[group_key] = list(iterator) + + for group_key in groups: + runtime = group_key[0] + codeuri = group_key[1] + LOG.info("Building artifact for runtime %s and CodeUri %s used by the following " + + "resources: %s", + runtime, codeuri, ", ".join([x.name for x in groups[group_key]])) + artifact_path = self._build_function(codeuri, runtime) + for func in groups[group_key]: + result[func.name] = artifact_path return result @@ -152,16 +166,13 @@ def update_template(self, template_dict, original_template_path, built_artifacts return template_dict - def _build_function(self, function_name, codeuri, runtime): + def _build_function(self, codeuri, runtime): """ Given the function information, this method will build the Lambda function. Depending on the configuration it will either build the function in process or by spinning up a Docker container. Parameters ---------- - function_name : str - Name or LogicalId of the function - codeuri : str Path to where the code lives @@ -181,7 +192,9 @@ def _build_function(self, function_name, codeuri, runtime): config = get_workflow_config(runtime, code_dir, self._base_dir) # artifacts directory will be created by the builder - artifacts_dir = str(pathlib.Path(self._build_dir, function_name)) + artifacts_dir = str(pathlib.Path( + self._build_dir, + "{}_{}".format(runtime, base64.b64encode(codeuri.encode('utf-8')).decode('utf-8')))) with osutils.mkdir_temp() as scratch_dir: manifest_path = self._manifest_path_override or os.path.join(code_dir, config.manifest_name) diff --git a/tests/unit/lib/build_module/test_app_builder.py b/tests/unit/lib/build_module/test_app_builder.py index 8450af2e51..c5ec4c0b1f 100644 --- a/tests/unit/lib/build_module/test_app_builder.py +++ b/tests/unit/lib/build_module/test_app_builder.py @@ -1,4 +1,5 @@ +import base64 import os import docker import json @@ -6,6 +7,7 @@ from unittest import TestCase from mock import Mock, call, patch +from samcli.commands.local.lib.provider import Function from samcli.lib.build.app_builder import ApplicationBuilder,\ UnsupportedBuilderLibraryVersionError, BuildError, \ LambdaBuilderError, ContainerBuildNotSupported @@ -13,29 +15,86 @@ class TestApplicationBuilder_build(TestCase): - def setUp(self): - self.func1 = Mock() - self.func2 = Mock() - self.builder = ApplicationBuilder([self.func1, self.func2], - "builddir", - "basedir") - - def test_must_iterate_on_functions(self): + def test_artifacts_for_identical_functions(self): + func1 = Function(name="function_name1", + runtime="runtime", + memory=1234, + timeout=12, + handler="handler", + codeuri="codeuri", + environment=None, + rolearn=None, + layers=[]) + func2 = Function(name="function_name2", + runtime="runtime", + memory=1234, + timeout=12, + handler="handler", + codeuri="codeuri", + environment=None, + rolearn=None, + layers=[]) + + builder = ApplicationBuilder([func1, func2], "builddir", "basedir") build_function_mock = Mock() + builder._build_function = build_function_mock - self.builder._build_function = build_function_mock + result = builder.build() - result = self.builder.build() + self.assertEquals(result, { + func1.name: build_function_mock.return_value, + func2.name: build_function_mock.return_value, + }) + + build_function_mock.assert_has_calls([ + call(func1.codeuri, func1.runtime)], any_order=False) + + def test_artifacts_for_differing_functions(self): + func1 = Function(name="function_name1", + runtime="runtime", + memory=1234, + timeout=12, + handler="handler", + codeuri="codeuri", + environment=None, + rolearn=None, + layers=[]) + func2 = Function(name="function_name2", + runtime="runtime2", + memory=1234, + timeout=12, + handler="handler", + codeuri="codeuri", + environment=None, + rolearn=None, + layers=[]) + func3 = Function(name="function_name3", + runtime="runtime3", + memory=1234, + timeout=12, + handler="handler", + codeuri="codeuri3", + environment=None, + rolearn=None, + layers=[]) + + builder = ApplicationBuilder([func1, func2, func3], "builddir", "basedir") + build_function_mock = Mock() + builder._build_function = build_function_mock + + result = builder.build() self.assertEquals(result, { - self.func1.name: build_function_mock.return_value, - self.func2.name: build_function_mock.return_value, + func1.name: build_function_mock.return_value, + func2.name: build_function_mock.return_value, + func3.name: build_function_mock.return_value, }) build_function_mock.assert_has_calls([ - call(self.func1.name, self.func1.codeuri, self.func1.runtime), - call(self.func2.name, self.func2.codeuri, self.func2.runtime), - ], any_order=False) + call(func1.codeuri, func1.runtime), + call(func2.codeuri, func2.runtime), + call(func3.codeuri, func3.runtime), + ], any_order=True) class TestApplicationBuilder_update_template(TestCase): @@ -116,10 +175,13 @@ def setUp(self): "/build/dir", "/base/dir") + @staticmethod + def _build_artifacts_dir(codeuri, runtime): + return "{}_{}".format(runtime, base64.b64encode(codeuri.encode('utf-8')).decode('utf-8')) + @patch("samcli.lib.build.app_builder.get_workflow_config") @patch("samcli.lib.build.app_builder.osutils") def test_must_build_in_process(self, osutils_mock, get_workflow_config_mock): - function_name = "function_name" codeuri = "path/to/source" runtime = "runtime" scratch_dir = "scratch" @@ -132,10 +194,10 @@ def test_must_build_in_process(self, osutils_mock, get_workflow_config_mock): self.builder._build_function_in_process = Mock() code_dir = "/base/dir/path/to/source" - artifacts_dir = "/build/dir/function_name" + artifacts_dir = os.path.join("/build/dir/", self._build_artifacts_dir(codeuri, runtime)) manifest_path = os.path.join(code_dir, config_mock.manifest_name) - self.builder._build_function(function_name, codeuri, runtime) + self.builder._build_function(codeuri, runtime) self.builder._build_function_in_process.assert_called_with(config_mock, code_dir, @@ -147,7 +209,6 @@ 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_build_in_container(self, osutils_mock, get_workflow_config_mock): - function_name = "function_name" codeuri = "path/to/source" runtime = "runtime" scratch_dir = "scratch" @@ -160,12 +221,12 @@ def test_must_build_in_container(self, osutils_mock, get_workflow_config_mock): self.builder._build_function_on_container = Mock() code_dir = "/base/dir/path/to/source" - artifacts_dir = "/build/dir/function_name" + artifacts_dir = os.path.join("/build/dir/", self._build_artifacts_dir(codeuri, runtime)) manifest_path = os.path.join(code_dir, config_mock.manifest_name) # Settting the container manager will make us use the container self.builder._container_manager = Mock() - self.builder._build_function(function_name, codeuri, runtime) + self.builder._build_function(codeuri, runtime) self.builder._build_function_on_container.assert_called_with(config_mock, code_dir,