77import time
88from http import HTTPStatus
99from pathlib import Path
10+ from tempfile import NamedTemporaryFile
1011from typing import Optional
1112
1213import docker
1516
1617from samcli .lib .build .utils import _get_host_architecture
1718from samcli .lib .clients .lambda_client import DurableFunctionsClient
18- from samcli .local .docker .utils import get_validated_container_client , is_image_current
19+ from samcli .lib .utils .tar import create_tarball
20+ from samcli .local .docker .utils import get_tar_filter_for_windows , get_validated_container_client , is_image_current
1921
2022LOG = logging .getLogger (__name__ )
2123
@@ -27,6 +29,7 @@ class DurableFunctionsEmulatorContainer:
2729
2830 _RAPID_SOURCE_PATH = Path (__file__ ).parent .joinpath (".." , "rapid" ).resolve ()
2931 _EMULATOR_IMAGE = "public.ecr.aws/ubuntu/ubuntu:24.04"
32+ _EMULATOR_IMAGE_PREFIX = "samcli/durable-execution-emulator"
3033 _CONTAINER_NAME = "sam-durable-execution-emulator"
3134 _EMULATOR_DATA_DIR_NAME = ".durable-executions-local"
3235 _EMULATOR_DEFAULT_STORE_TYPE = "sqlite"
@@ -190,6 +193,62 @@ def _get_emulator_binary_name(self):
190193 arch = _get_host_architecture ()
191194 return f"aws-durable-execution-emulator-{ arch } "
192195
196+ def _generate_emulator_dockerfile (self , emulator_binary_name : str ) -> str :
197+ """Generate Dockerfile content for emulator image."""
198+ return (
199+ f"FROM { self ._EMULATOR_IMAGE } \n "
200+ f"COPY { emulator_binary_name } /usr/local/bin/{ emulator_binary_name } \n "
201+ f"RUN chmod +x /usr/local/bin/{ emulator_binary_name } \n "
202+ )
203+
204+ def _get_emulator_image_tag (self , emulator_binary_name : str ) -> str :
205+ """Get the Docker image tag for the emulator."""
206+ return f"{ self ._EMULATOR_IMAGE_PREFIX } :{ emulator_binary_name } "
207+
208+ def _build_emulator_image (self ):
209+ """Build Docker image with emulator binary."""
210+ emulator_binary_name = self ._get_emulator_binary_name ()
211+ binary_path = self ._RAPID_SOURCE_PATH / emulator_binary_name
212+
213+ if not binary_path .exists ():
214+ raise RuntimeError (f"Durable Functions Emulator binary not found at { binary_path } " )
215+
216+ image_tag = self ._get_emulator_image_tag (emulator_binary_name )
217+
218+ # Check if image already exists
219+ try :
220+ self ._docker_client .images .get (image_tag )
221+ LOG .debug (f"Emulator image { image_tag } already exists" )
222+ return image_tag
223+ except docker .errors .ImageNotFound :
224+ LOG .debug (f"Building emulator image { image_tag } " )
225+
226+ # Generate Dockerfile content
227+ dockerfile_content = self ._generate_emulator_dockerfile (emulator_binary_name )
228+
229+ # Write Dockerfile to temp location and build image
230+ with NamedTemporaryFile (mode = "w" , suffix = "_Dockerfile" ) as dockerfile :
231+ dockerfile .write (dockerfile_content )
232+ dockerfile .flush ()
233+
234+ # Prepare tar paths for build context
235+ tar_paths = {
236+ dockerfile .name : "Dockerfile" ,
237+ str (binary_path ): emulator_binary_name ,
238+ }
239+
240+ # Use shared tar filter for Windows compatibility
241+ tar_filter = get_tar_filter_for_windows ()
242+
243+ # Build image using create_tarball utility
244+ with create_tarball (tar_paths , tar_filter = tar_filter , dereference = True ) as tarballfile :
245+ try :
246+ self ._docker_client .images .build (fileobj = tarballfile , custom_context = True , tag = image_tag , rm = True )
247+ LOG .info (f"Built emulator image { image_tag } " )
248+ return image_tag
249+ except Exception as e :
250+ raise ClickException (f"Failed to build emulator image: { e } " )
251+
193252 def _pull_image_if_needed (self ):
194253 """Pull the emulator image if it doesn't exist locally or is out of date."""
195254 try :
@@ -218,9 +277,6 @@ def start(self):
218277 return
219278
220279 emulator_binary_name = self ._get_emulator_binary_name ()
221- binary_path = self ._RAPID_SOURCE_PATH / emulator_binary_name
222- if not binary_path .exists ():
223- raise RuntimeError (f"Durable Functions Emulator binary not found at { binary_path } " )
224280
225281 """
226282 Create persistent volume for execution data to be stored in.
@@ -231,16 +287,15 @@ def start(self):
231287 os .makedirs (emulator_data_dir , exist_ok = True )
232288
233289 volumes = {
234- str (self ._RAPID_SOURCE_PATH ): {"bind" : "/usr/local/bin" , "mode" : "ro" },
235290 emulator_data_dir : {"bind" : "/tmp/.durable-executions-local" , "mode" : "rw" },
236291 }
237292
238- # Pull the image if needed
239- self ._pull_image_if_needed ()
293+ # Build image with emulator binary
294+ image_tag = self ._build_emulator_image ()
240295
241296 LOG .debug (f"Creating container with name={ self ._container_name } , port={ self .port } " )
242297 self .container = self ._docker_client .containers .create (
243- image = self . _EMULATOR_IMAGE ,
298+ image = image_tag ,
244299 command = [f"/usr/local/bin/{ emulator_binary_name } " , "--host" , "0.0.0.0" , "--port" , str (self .port )],
245300 name = self ._container_name ,
246301 ports = {f"{ self .port } /tcp" : self .port },
0 commit comments