99 CompleteSandboxImageBuildParams ,
1010 CreateSandboxParams ,
1111 CreateSandboxImageBuildParams ,
12+ ReuseSandboxDockerImageParams ,
1213 SandboxDetail ,
1314 SandboxExecParams ,
1415 SandboxExposeParams ,
1516 SandboxExposeResult ,
1617 SandboxImageBuild ,
1718 SandboxImageBuildCreateResult ,
19+ SandboxDockerImageReuseResult ,
1820 SandboxImageBuildListParams ,
1921 SandboxImageBuildListResponse ,
2022 SandboxImageListParams ,
3941from ....types import (
4042 CompleteSandboxImageBuildParams as CompleteSandboxImageBuildParamsDict ,
4143 CreateSandboxImageBuildParams as CreateSandboxImageBuildParamsDict ,
44+ ReuseSandboxDockerImageParams as ReuseSandboxDockerImageParamsDict ,
4245 CreateSandboxParams as CreateSandboxParamsDict ,
4346 SandboxExecParams as SandboxExecParamsDict ,
4447 SandboxExposeParams as SandboxExposeParamsDict ,
6770 build_docker_image_from_dockerfile ,
6871 is_terminal_image_build_status ,
6972 make_temp_docker_tag ,
70- package_docker_image ,
73+ merge_image_init ,
74+ package_docker_build_context_manifest ,
75+ package_docker_image_manifest ,
76+ prepare_docker_image_manifest_source ,
7177 remove_docker_image ,
72- upload_image_build_artifact ,
78+ upload_missing_image_build_artifacts ,
7379)
7480from .sandboxes .sandbox_files import (
7581 DEFAULT_WATCH_TIMEOUT_MS ,
@@ -494,6 +500,20 @@ async def get_image_build(self, build_id: str) -> SandboxImageBuild:
494500 payload = await self ._request ("GET" , f"/images/builds/{ build_id } " )
495501 return SandboxImageBuild (** payload ["build" ])
496502
503+ async def reuse_docker_image (
504+ self ,
505+ params : Union [
506+ ReuseSandboxDockerImageParamsDict ,
507+ ReuseSandboxDockerImageParams ,
508+ ],
509+ ) -> SandboxDockerImageReuseResult :
510+ payload = await self ._request (
511+ "POST" ,
512+ "/images/builds/reuse" ,
513+ data = dump_request (params , ReuseSandboxDockerImageParams ),
514+ )
515+ return SandboxDockerImageReuseResult (** payload )
516+
497517 async def complete_image_build (
498518 self ,
499519 build_id : str ,
@@ -566,63 +586,204 @@ async def build_image_from_docker_image(
566586 temp_dir : Optional [str ] = None ,
567587 upload_timeout : Optional [float ] = None ,
568588 ) -> SandboxImageBuild :
569- artifact = await _run_blocking (
570- package_docker_image ,
589+ source = await _run_blocking (
590+ prepare_docker_image_manifest_source ,
571591 docker_image ,
572592 platform = platform ,
593+ )
594+ try :
595+ explicit_image_init = (
596+ coerce_request (image_init , SandboxImageInit , name = "image_init" )
597+ if image_init is not None
598+ else None
599+ )
600+ normalized_image_init = merge_image_init (
601+ source .image_init ,
602+ explicit_image_init ,
603+ )
604+ normalized_image_config_user = (
605+ image_config_user
606+ if image_config_user is not None
607+ else source .image_config_user
608+ )
609+ try :
610+ reused = await self .reuse_docker_image (
611+ ReuseSandboxDockerImageParams (
612+ image_name = image_name ,
613+ source_image_digest = source .image_digest ,
614+ source_platform = platform ,
615+ image_config_user = normalized_image_config_user ,
616+ image_init = normalized_image_init ,
617+ )
618+ )
619+ except HyperbrowserError :
620+ reused = None
621+ if reused is not None and reused .hit :
622+ if reused .build is None :
623+ raise RuntimeError (
624+ "exact image cache response is missing its completed build"
625+ )
626+ return reused .build
627+
628+ packaged = await _run_blocking (
629+ package_docker_image_manifest ,
630+ docker_image ,
631+ source .image_digest ,
632+ source .config ,
633+ platform = platform ,
634+ temp_dir = temp_dir ,
635+ )
636+ build_id = None
637+ build_started = False
638+ try :
639+ artifact = packaged .artifact
640+ create_result = await self .create_image_build (
641+ CreateSandboxImageBuildParams (
642+ image_name = image_name ,
643+ input_sha256 = artifact .sha256_hex ,
644+ input_size_bytes = artifact .size_bytes ,
645+ input_format = artifact .input_format ,
646+ source_platform = artifact .source_platform ,
647+ image_config_user = normalized_image_config_user ,
648+ image_init = normalized_image_init ,
649+ docker_image_manifest = packaged .manifest ,
650+ )
651+ )
652+ build_id = create_result .build .id
653+ await _run_blocking (
654+ upload_missing_image_build_artifacts ,
655+ create_result .uploads ,
656+ packaged .layers ,
657+ label = "Docker image layer" ,
658+ timeout = upload_timeout ,
659+ )
660+ build = await self ._complete_image_build_resilient (
661+ build_id ,
662+ artifact ,
663+ )
664+ build_started = True
665+ if wait :
666+ return await self .wait_for_image_build (
667+ build .id ,
668+ poll_interval = poll_interval ,
669+ timeout = wait_timeout ,
670+ )
671+ return build
672+ except Exception :
673+ if build_id is not None and not build_started :
674+ try :
675+ await self .cancel_image_build (build_id )
676+ except Exception :
677+ pass
678+ raise
679+ finally :
680+ await _run_blocking (packaged .cleanup )
681+ finally :
682+ await _run_blocking (source .cleanup )
683+
684+ async def _complete_image_build_resilient (
685+ self ,
686+ build_id : str ,
687+ artifact ,
688+ ) -> SandboxImageBuild :
689+ params = CompleteSandboxImageBuildParams (
690+ input_sha256 = artifact .sha256_hex ,
691+ input_size_bytes = artifact .size_bytes ,
692+ input_format = artifact .input_format ,
693+ )
694+ try :
695+ return await self .complete_image_build (build_id , params )
696+ except HyperbrowserError as exc :
697+ if exc .status_code != 409 :
698+ raise
699+ if "already in progress" in str (exc ).lower ():
700+ return await self .get_image_build (build_id )
701+ current = await self .get_image_build (build_id )
702+ if current .status not in ("awaiting_upload" , "upload_verified" ):
703+ raise
704+ await asyncio .sleep (2 )
705+ return await self .complete_image_build (build_id , params )
706+
707+ async def _build_image_from_remote_dockerfile (
708+ self ,
709+ * ,
710+ context_path ,
711+ image_name : str ,
712+ dockerfile ,
713+ platform : str ,
714+ remote_full_context : bool ,
715+ image_init : Optional [Union [SandboxImageInitDict , SandboxImageInit ]],
716+ image_config_user : Optional [str ],
717+ wait : bool ,
718+ poll_interval : float ,
719+ wait_timeout : Optional [float ],
720+ temp_dir : Optional [str ],
721+ upload_timeout : Optional [float ],
722+ ) -> SandboxImageBuild :
723+ packaged = await _run_blocking (
724+ package_docker_build_context_manifest ,
725+ context_path ,
726+ dockerfile = dockerfile ,
727+ force_full_context = remote_full_context ,
573728 temp_dir = temp_dir ,
574729 )
730+ build_id = None
731+ build_started = False
575732 try :
576733 normalized_image_init = (
577734 coerce_request (image_init , SandboxImageInit , name = "image_init" )
578735 if image_init is not None
579- else artifact . image_init
736+ else None
580737 )
738+ artifact = packaged .artifact
581739 create_result = await self .create_image_build (
582740 CreateSandboxImageBuildParams (
583741 image_name = image_name ,
584742 input_sha256 = artifact .sha256_hex ,
585743 input_size_bytes = artifact .size_bytes ,
586744 input_format = artifact .input_format ,
587745 source_platform = artifact .source_platform ,
588- image_config_user = (
589- image_config_user
590- if image_config_user is not None
591- else artifact .image_config_user
592- ),
746+ dockerfile_path = packaged .manifest .dockerfile_path ,
747+ image_config_user = image_config_user ,
593748 image_init = normalized_image_init ,
749+ context_manifest = packaged .manifest ,
594750 )
595751 )
752+ build_id = create_result .build .id
596753 await _run_blocking (
597- upload_image_build_artifact ,
598- create_result .upload ,
599- artifact .path ,
754+ upload_missing_image_build_artifacts ,
755+ create_result .uploads ,
756+ packaged .bundles ,
757+ label = "build context bundle" ,
600758 timeout = upload_timeout ,
601759 )
602- build = await self .complete_image_build (
603- create_result .build .id ,
604- CompleteSandboxImageBuildParams (
605- input_sha256 = artifact .sha256_hex ,
606- input_size_bytes = artifact .size_bytes ,
607- input_format = artifact .input_format ,
608- ),
609- )
760+ build = await self ._complete_image_build_resilient (build_id , artifact )
761+ build_started = True
610762 if wait :
611763 return await self .wait_for_image_build (
612764 build .id ,
613765 poll_interval = poll_interval ,
614766 timeout = wait_timeout ,
615767 )
616768 return build
769+ except Exception :
770+ if build_id is not None and not build_started :
771+ try :
772+ await self .cancel_image_build (build_id )
773+ except Exception :
774+ pass
775+ raise
617776 finally :
618- artifact .cleanup ( )
777+ await _run_blocking ( packaged .cleanup )
619778
620779 async def build_image_from_dockerfile (
621780 self ,
622781 * ,
623782 context_path ,
624783 image_name : str ,
625784 dockerfile = "Dockerfile" ,
785+ remote : bool = True ,
786+ remote_full_context : bool = False ,
626787 docker_tag : Optional [str ] = None ,
627788 platform : str = IMAGE_BUILD_SOURCE_PLATFORM ,
628789 build_args : Optional [Dict [str , str ]] = None ,
@@ -634,6 +795,26 @@ async def build_image_from_dockerfile(
634795 temp_dir : Optional [str ] = None ,
635796 upload_timeout : Optional [float ] = None ,
636797 ) -> SandboxImageBuild :
798+ if remote :
799+ if docker_tag is not None or build_args :
800+ raise ValueError (
801+ "docker_tag and build_args require remote=False; remote "
802+ "Dockerfile builds send the build context to Hyperbrowser"
803+ )
804+ return await self ._build_image_from_remote_dockerfile (
805+ context_path = context_path ,
806+ image_name = image_name ,
807+ dockerfile = dockerfile ,
808+ platform = platform ,
809+ remote_full_context = remote_full_context ,
810+ image_init = image_init ,
811+ image_config_user = image_config_user ,
812+ wait = wait ,
813+ poll_interval = poll_interval ,
814+ wait_timeout = wait_timeout ,
815+ temp_dir = temp_dir ,
816+ upload_timeout = upload_timeout ,
817+ )
637818 tag = docker_tag or make_temp_docker_tag ()
638819 remove_tag = docker_tag is None
639820 try :
0 commit comments