2424logger = get_logger ("docker.context" )
2525
2626
27+ def build_base_image (client : docker .DockerClient , ctx : DockerContext ) -> str :
28+ base_key = hash (ctx )
29+ base_tag = f"asv-base-rev-{ base_key } :base"
30+
31+ res = ctx .build_container_streaming (
32+ client = client ,
33+ image_name = base_tag ,
34+ build_args = {},
35+ probe = True ,
36+ force = False ,
37+ pull = False ,
38+ timeout_s = 1800 ,
39+ )
40+ if not res .ok :
41+ logger .exception ("Failed to build base image %s error=%s" , base_tag , res .stderr_tail )
42+ raise RuntimeError ("Failed to build base image" )
43+ return base_tag
44+
45+
2746@dataclass
2847class BuildResult :
2948 ok : bool
@@ -141,43 +160,32 @@ def __init__(
141160 self .base_building_data = base_building_data
142161 self .building_data = building_data
143162
163+ @staticmethod
164+ def add_bytes (tar : tarfile .TarFile , name : str , data : bytes , mode : int = 0o644 ) -> None :
165+ info = tarfile .TarInfo (name = name )
166+ info .size = len (data )
167+ info .mode = mode
168+ info .mtime = 0
169+ info .uid = info .gid = 0
170+ info .uname = info .gname = ""
171+ tar .addfile (info , io .BytesIO (data ))
172+
144173 def build_tarball_stream (self , probe : bool = False ) -> io .BytesIO :
145174 tar_stream = io .BytesIO ()
146175 with tarfile .open (fileobj = tar_stream , mode = "w" ) as tar :
147176 # Add Dockerfile
148- dockerfile_bytes = self .dockerfile_data .encode ("utf-8" )
149- dockerfile_info = tarfile .TarInfo (name = "Dockerfile" )
150- dockerfile_info .size = len (dockerfile_bytes )
151- tar .addfile (dockerfile_info , io .BytesIO (dockerfile_bytes ))
152-
177+ DockerContext .add_bytes (tar , "Dockerfile" , self .dockerfile_data .encode ("utf-8" ))
153178 # Add entrypoint.sh
154- entrypoint_data = self .entrypoint_data .encode ("utf-8" )
155- entrypoint_info = tarfile .TarInfo (name = "entrypoint.sh" )
156- entrypoint_info .size = len (entrypoint_data )
157- entrypoint_info .mode = 0o755 # Make it executable
158- tar .addfile (entrypoint_info , io .BytesIO (entrypoint_data ))
159-
179+ DockerContext .add_bytes (tar , "entrypoint.sh" , self .entrypoint_data .encode ("utf-8" ), mode = 0o755 )
160180 # Add docker_build_env.sh
161- env_building_data = self .env_building_data .encode ("utf-8" )
162- env_building_info = tarfile .TarInfo (name = "docker_build_env.sh" )
163- env_building_info .size = len (env_building_data )
164- env_building_info .mode = 0o755 # Make it executable
165- tar .addfile (env_building_info , io .BytesIO (env_building_data ))
181+ DockerContext .add_bytes (tar , "docker_build_env.sh" , self .env_building_data .encode ("utf-8" ), mode = 0o755 )
166182
167183 # Add docker_build_base.sh
168- base_building_data = self .base_building_data .encode ("utf-8" )
169- base_building_info = tarfile .TarInfo (name = "docker_build_base.sh" )
170- base_building_info .size = len (base_building_data )
171- base_building_info .mode = 0o755 # Make it executable
172- tar .addfile (base_building_info , io .BytesIO (base_building_data ))
184+ DockerContext .add_bytes (tar , "docker_build_base.sh" , self .base_building_data .encode ("utf-8" ), mode = 0o755 )
173185
174186 if not probe :
175187 # Add docker_build_pkg.sh
176- building_data = self .building_data .encode ("utf-8" )
177- building_info = tarfile .TarInfo (name = "docker_build_pkg.sh" )
178- building_info .size = len (building_data )
179- building_info .mode = 0o755 # Make it executable
180- tar .addfile (building_info , io .BytesIO (building_data ))
188+ DockerContext .add_bytes (tar , "docker_build_pkg.sh" , self .building_data .encode ("utf-8" ), mode = 0o755 )
181189
182190 # Reset the stream position to the beginning
183191 tar_stream .seek (0 )
@@ -216,6 +224,11 @@ def build_container(
216224 pass # Image doesn't exist or was removed, proceed to build
217225
218226 if not image_exists :
227+ cache_from = None
228+ if base_image := os .environ .get ("DOCKER_CACHE_FROM" , None ):
229+ build_args = {** build_args , "BASE_IMAGE" : base_image }
230+ cache_from = [base_image ]
231+
219232 if len (build_args ):
220233 build_args_str = " --build-arg " .join (f"{ k } ={ v } " for k , v in build_args .items ())
221234 logger .info ("$ docker build -t %s src/datasmith/docker/ --build-arg %s" , image_name , build_args_str )
@@ -229,6 +242,7 @@ def build_container(
229242 rm = True ,
230243 labels = run_labels ,
231244 network_mode = os .environ .get ("DOCKER_NETWORK_MODE" , None ),
245+ cache_from = cache_from ,
232246 )
233247 except DockerException :
234248 logger .exception ("Failed to build Docker image '%s'" , image_name )
@@ -289,6 +303,11 @@ def build_container_streaming( # noqa: C901
289303 stdout_buf : deque [str ] = deque (maxlen = 2000 ) # chunk-tail buffers
290304 stderr_buf : deque [str ] = deque (maxlen = 2000 )
291305
306+ cache_from = None
307+ if base_image := os .environ .get ("DOCKER_CACHE_FROM" , None ):
308+ build_args = {** build_args , "BASE_IMAGE" : base_image }
309+ cache_from = [base_image ]
310+
292311 # Pretty log line for transparency
293312 if build_args :
294313 build_args_str = " --build-arg " .join (f"{ k } ={ v } " for k , v in build_args .items ())
@@ -308,6 +327,7 @@ def build_container_streaming( # noqa: C901
308327 target = target ,
309328 labels = run_labels ,
310329 network_mode = os .environ .get ("DOCKER_NETWORK_MODE" , None ),
330+ cache_from = cache_from ,
311331 )
312332 except DockerException :
313333 logger .exception ("Failed to initiate build for '%s'" , image_name )
@@ -424,6 +444,15 @@ def from_dict(cls, data: Mapping[str, Any]) -> DockerContext:
424444 base_building_data = data .get ("base_building_data" , None ),
425445 )
426446
447+ def __hash__ (self ) -> int :
448+ return hash ((
449+ self .dockerfile_data ,
450+ self .entrypoint_data ,
451+ self .building_data ,
452+ self .env_building_data ,
453+ self .base_building_data ,
454+ ))
455+
427456
428457class ContextRegistry :
429458 """Registry for Docker contexts keyed by owner/repo[/sha], independent of tag.
0 commit comments