7070import contextlib
7171import enum
7272import glob
73+ import hashlib
74+ import functools
75+ import json
7376import logging
7477import os
7578import pathlib
7881import shutil
7982import subprocess
8083import sys
84+ import time
8185
8286log = logging .getLogger ()
8387
@@ -149,6 +153,18 @@ class DidNotExecute(Exception):
149153 pass
150154
151155
156+ _CONTAINER_SOURCES = [
157+ "Dockerfile.build" ,
158+ "src/script/lib-build.sh" ,
159+ "src/script/run-make.sh" ,
160+ "ceph.spec.in" ,
161+ "do_cmake.sh" ,
162+ "install-deps.sh" ,
163+ "run-make-check.sh" ,
164+ "src/script/buildcontainer-setup.sh" ,
165+ ]
166+
167+
152168def _cmdstr (cmd ):
153169 return " " .join (shlex .quote (c ) for c in cmd )
154170
@@ -218,6 +234,9 @@ def _git_command(ctx, args):
218234 return cmd
219235
220236
237+ # Assume that the git version will not be changing after the 1st time
238+ # the command is run.
239+ @functools .cache
221240def _git_current_branch (ctx ):
222241 cmd = _git_command (ctx , ["rev-parse" , "--abbrev-ref" , "HEAD" ])
223242 res = _run (cmd , check = True , capture_output = True )
@@ -234,6 +253,20 @@ def _git_current_sha(ctx, short=True):
234253 return res .stdout .decode ("utf8" ).strip ()
235254
236255
256+ @functools .cache
257+ def _hash_sources (bsize = 4096 ):
258+ hh = hashlib .sha256 ()
259+ buf = bytearray (bsize )
260+ for path in sorted (_CONTAINER_SOURCES ):
261+ with open (path , "rb" ) as fh :
262+ while True :
263+ rlen = fh .readinto (buf )
264+ hh .update (buf [:rlen ])
265+ if rlen < len (buf ):
266+ break
267+ return f"sha256:{ hh .hexdigest ()} "
268+
269+
237270class Steps (StrEnum ):
238271 DNF_CACHE = "dnfcache"
239272 BUILD_CONTAINER = "build-container"
@@ -396,6 +429,7 @@ class Builder:
396429
397430 def __init__ (self ):
398431 self ._did_steps = set ()
432+ self ._reported_failed = False
399433
400434 def wants (self , step , ctx , * , force = False , top = False ):
401435 log .info ("want to execute build step: %s" , step )
@@ -407,13 +441,35 @@ def wants(self, step, ctx, *, force=False, top=False):
407441 return
408442 if not self ._did_steps :
409443 prepare_env_once (ctx )
410- self ._steps [ step ]( ctx )
411- self ._did_steps . add ( step )
412- log . info ( "step done: %s" , step )
444+ with self ._timer ( step ):
445+ self ._steps [ step ]( ctx )
446+ self . _did_steps . add ( step )
413447
414448 def available_steps (self ):
415449 return [str (k ) for k in self ._steps ]
416450
451+ @contextlib .contextmanager
452+ def _timer (self , step ):
453+ ns = argparse .Namespace (start = time .monotonic ())
454+ status = "not-started"
455+ try :
456+ yield ns
457+ status = "completed"
458+ except Exception :
459+ status = "failed"
460+ raise
461+ finally :
462+ ns .end = time .monotonic ()
463+ ns .duration = int (ns .end - ns .start )
464+ hrs , _rest = map (int , divmod (ns .duration , 3600 ))
465+ mins , secs = map (int , divmod (_rest , 60 ))
466+ ns .duration_hms = f"{ hrs :02} :{ mins :02} :{ secs :02} "
467+ if not self ._reported_failed :
468+ log .info (
469+ "step done: %s %s in %s" , step , status , ns .duration_hms
470+ )
471+ self ._reported_failed = status == "failed"
472+
417473 @classmethod
418474 def set (self , step ):
419475 def wrap (f ):
@@ -462,6 +518,7 @@ def build_container(ctx):
462518 "--pull" ,
463519 "-t" ,
464520 ctx .image_name ,
521+ f"--label=io.ceph.build-with-container.src={ _hash_sources ()} " ,
465522 f"--build-arg=JENKINS_HOME={ ctx .cli .homedir } " ,
466523 f"--build-arg=CEPH_BASE_BRANCH={ ctx .base_branch ()} " ,
467524 ]
@@ -482,32 +539,58 @@ def build_container(ctx):
482539 _run (cmd , check = True , ctx = ctx )
483540
484541
485- @Builder .set (Steps .CONTAINER )
486- def get_container (ctx ):
487- """Build or fetch a container image that we will build in."""
542+ def _check_cached_image (ctx ):
488543 inspect_cmd = [
489544 ctx .container_engine ,
490545 "image" ,
491546 "inspect" ,
492547 ctx .image_name ,
493548 ]
549+ res = _run (inspect_cmd , check = False , capture_output = True )
550+ if res .returncode != 0 :
551+ log .info ("Container image %s not present" , ctx .image_name )
552+ return False , False
553+
554+ log .info ("Container image %s present" , ctx .image_name )
555+ ctr_info = json .loads (res .stdout )[0 ]
556+ labels = {}
557+ if "Labels" in ctr_info :
558+ labels = ctr_info ["Labels" ]
559+ elif "Labels" in ctr_info .get ("ContainerConfig" , {}):
560+ labels = ctr_info ["ContainerConfig" ]["Labels" ]
561+ elif "Labels" in ctr_info .get ("Config" , {}):
562+ labels = ctr_info ["Config" ]["Labels" ]
563+ saved_hash = labels .get ("io.ceph.build-with-container.src" , "" )
564+ curr_hash = _hash_sources ()
565+ if saved_hash == curr_hash :
566+ log .info ("Container passes source check" )
567+ return True , True
568+ log .info ("Container sources do not match: %s" , curr_hash )
569+ return True , False
570+
571+
572+ @Builder .set (Steps .CONTAINER )
573+ def get_container (ctx ):
574+ """Build or fetch a container image that we will build in."""
494575 pull_cmd = [
495576 ctx .container_engine ,
496577 "pull" ,
497578 ctx .image_name ,
498579 ]
499580 allowed = ctx .cli .image_sources or ImageSource
500581 if ImageSource .CACHE in allowed :
501- res = _run ( inspect_cmd , check = False , capture_output = True )
502- if res . returncode == 0 :
503- log . info ( "Container image %s present" , ctx . image_name )
582+ log . info ( "Checking for cached image" )
583+ present , hash_ok = _check_cached_image ( ctx )
584+ if present and hash_ok or len ( allowed ) == 1 :
504585 return
505- log .info ("Container image %s not present" , ctx .image_name )
506586 if ImageSource .PULL in allowed :
587+ log .info ("Checking for image in remote repository" )
507588 res = _run (pull_cmd , check = False , capture_output = True )
508589 if res .returncode == 0 :
509590 log .info ("Container image %s pulled successfully" , ctx .image_name )
510- return
591+ present , hash_ok = _check_cached_image (ctx )
592+ if present and hash_ok :
593+ return
511594 log .info ("Container image %s needed" , ctx .image_name )
512595 if ImageSource .BUILD in allowed :
513596 ctx .build .wants (Steps .BUILD_CONTAINER , ctx )
@@ -598,6 +681,11 @@ def bc_make_source_rpm(ctx):
598681 _run (cmd , check = True , ctx = ctx )
599682
600683
684+ def _glob_search (ctx , pattern ):
685+ overlay = ctx .overlay ()
686+ return glob .glob (pattern , root_dir = overlay .upper if overlay else None )
687+
688+
601689@Builder .set (Steps .RPM )
602690def bc_build_rpm (ctx ):
603691 """Build RPMs from SRPM."""
@@ -618,7 +706,7 @@ def bc_build_rpm(ctx):
618706 ctx .cli .ceph_version
619707 )
620708 srpm_glob = f"ceph-{ srpm_version } .*.src.rpm"
621- paths = glob . glob ( srpm_glob )
709+ paths = _glob_search ( ctx , srpm_glob )
622710 if len (paths ) > 1 :
623711 raise RuntimeError (
624712 "too many matching source rpms"
@@ -628,8 +716,11 @@ def bc_build_rpm(ctx):
628716 if not paths :
629717 # no matches. build a new srpm
630718 ctx .build .wants (Steps .SOURCE_RPM , ctx )
631- paths = glob .glob (srpm_glob )
632- assert paths
719+ paths = _glob_search (ctx , srpm_glob )
720+ if not paths :
721+ raise RuntimeError (
722+ f"unable to find source rpm(s) matching { srpm_glob } "
723+ )
633724 srpm_path = pathlib .Path (ctx .cli .homedir ) / paths [0 ]
634725 topdir = pathlib .Path (ctx .cli .homedir ) / "rpmbuild"
635726 if ctx .cli .build_dir :
@@ -640,7 +731,7 @@ def bc_build_rpm(ctx):
640731 'rpmbuild' ,
641732 '--rebuild' ,
642733 f'-D_topdir { topdir } ' ,
643- ] + list (ctx .cli .rpmbuild_arg ) + [str (srpm_path )]
734+ ] + list (ctx .cli .rpmbuild_arg or [] ) + [str (srpm_path )]
644735 rpmbuild_cmd = ' ' .join (shlex .quote (cmd ) for cmd in rpmbuild_args )
645736 cmd = _container_cmd (
646737 ctx ,
0 commit comments