44import random
55import shlex
66import subprocess
7+ import tarfile
8+ import tempfile
79import time
810import uuid
911from concurrent .futures import ThreadPoolExecutor
12+ from io import BytesIO
1013from pathlib import Path
1114from typing import Any
1215
16+ import ray
17+
1318from typing_extensions import Self
1419
1520from rock import env_vars
2025from rock .deployments .hooks .abstract import CombinedDeploymentHook , DeploymentHook
2126from rock .deployments .runtime_env import DockerRuntimeEnv , LocalRuntimeEnv , PipRuntimeEnv , UvRuntimeEnv
2227from rock .deployments .sandbox_validator import DockerSandboxValidator
23- from rock .deployments .status import PersistedServiceStatus , ServiceStatus
28+ from rock .deployments .status import PhaseStatus , PersistedServiceStatus , ServiceStatus
2429from rock .logger import init_logger
2530from rock .rocklet import PACKAGE_NAME , REMOTE_EXECUTABLE_NAME
2631from rock .rocklet .exceptions import DeploymentNotStartedError , DockerPullError
@@ -63,6 +68,8 @@ def __init__(
6368 self ._check_stop_task = None
6469 self ._container_name = None
6570 self ._service_status = PersistedServiceStatus ()
71+ if getattr (self ._config , "env_dir_tar_ref" , None ) is not None :
72+ self ._service_status .add_phase ("docker_build" , PhaseStatus ())
6673 if self ._config .container_name :
6774 self .set_container_name (self ._config .container_name )
6875 if env_vars .ROCK_WORKER_ENV_TYPE == "docker" :
@@ -285,6 +292,40 @@ def _memory(self):
285292 def _cpus (self ):
286293 return [f"--cpus={ self .config .cpus } " ]
287294
295+ def _build_image_from_env_dir_tar (self , tar_bytes : bytes ) -> str :
296+ """Extract env_dir tar.gz and run docker build; return image tag (self._config.image)."""
297+ self ._service_status .update_status (
298+ phase_name = "docker_build" , status = Status .RUNNING , message = "building from env_dir context"
299+ )
300+ with tempfile .TemporaryDirectory (prefix = "rock_env_" ) as tmpdir :
301+ path = Path (tmpdir )
302+ with tarfile .open (fileobj = BytesIO (tar_bytes ), mode = "r:gz" ) as tar :
303+ tar .extractall (path )
304+ context_dir = path
305+ dockerfile_path = context_dir / "Dockerfile"
306+ if not dockerfile_path .exists ():
307+ raise FileNotFoundError (f"Dockerfile not found in env_dir context: { dockerfile_path } " )
308+ build_cmd = [
309+ "docker" ,
310+ "build" ,
311+ "-f" ,
312+ str (dockerfile_path ),
313+ "-t" ,
314+ self ._config .image ,
315+ str (context_dir ),
316+ ]
317+ logger .info ("Running docker build from env_dir context: %s" , shlex .join (build_cmd ))
318+ try :
319+ subprocess .run (build_cmd , check = True , capture_output = True , timeout = 1800 )
320+ except subprocess .CalledProcessError as e :
321+ msg = f"docker build failed: { e .stderr .decode () if e .stderr else str (e )} "
322+ self ._service_status .update_status (phase_name = "docker_build" , status = Status .FAILED , message = msg )
323+ raise RuntimeError (msg ) from e
324+ self ._service_status .update_status (
325+ phase_name = "docker_build" , status = Status .SUCCESS , message = "docker build success"
326+ )
327+ return self ._config .image
328+
288329 async def start (self ):
289330 """Starts the runtime."""
290331 if not self .sandbox_validator .check_availability ():
@@ -295,11 +336,18 @@ async def start(self):
295336 self ._service_status .set_sandbox_id (self ._container_name )
296337 executor = get_executor ()
297338 loop = asyncio .get_running_loop ()
298- await loop .run_in_executor (executor , self ._pull_image )
299- if self ._config .python_standalone_dir is not None :
300- image_id = self ._build_image ()
339+
340+ if self ._config .env_dir_tar_ref is not None :
341+ tar_bytes = ray .get (self ._config .env_dir_tar_ref )
342+ image_id = await loop .run_in_executor (
343+ executor , self ._build_image_from_env_dir_tar , tar_bytes
344+ )
301345 else :
302- image_id = self ._config .image
346+ await loop .run_in_executor (executor , self ._pull_image )
347+ if self ._config .python_standalone_dir is not None :
348+ image_id = self ._build_image ()
349+ else :
350+ image_id = self ._config .image
303351
304352 if not self .sandbox_validator .check_resource (image_id ):
305353 raise Exception (f"Image { image_id } is not valid" )
0 commit comments