@@ -65,17 +65,67 @@ def hashWorkspace(step):
6565 return hashDirectory (step .getStoragePath (),
6666 os .path .join (os .path .dirname (step .getWorkspacePath ()), "cache.bin" ))
6767
68+ CHECKOUT_STATE_VARIANT_ID = None # Key in checkout directory state for step variant-id
69+ CHECKOUT_STATE_BUILD_ONLY = 1 # Key for checkout state of build-only builds
70+
71+ # Keys in checkout getDirectoryState that are not directories
72+ CHECKOUT_NON_DIR_KEYS = {CHECKOUT_STATE_VARIANT_ID , CHECKOUT_STATE_BUILD_ONLY }
73+
6874def compareDirectoryState (left , right ):
6975 """Compare two directory states while ignoring the SCM specs.
7076
7177 The SCM specs might change even though the digest stays the same (e.g. the
7278 URL changes but the commit id stays the same). This function filters the
7379 spec to detect real changes.
80+
81+ It also compares the CHECKOUT_STATE_VARIANT_ID to detect recipe changes. In
82+ contrast, the CHECKOUT_STATE_BUILD_ONLY sub-state is ignored as this is
83+ only relevant for build-only builds that have their dedicated functions
84+ below.
7485 """
75- left = { d : v [0 ] for d , v in left .items () }
76- right = { d : v [0 ] for d , v in right .items () }
86+ left = { d : v [0 ] for d , v in left .items () if d != CHECKOUT_STATE_BUILD_ONLY }
87+ right = { d : v [0 ] for d , v in right .items () if d != CHECKOUT_STATE_BUILD_ONLY }
7788 return left == right
7889
90+ def checkoutsFromState (state ):
91+ """Return only the tuples related to SCMs from the checkout state."""
92+ return [ (k , v ) for k , v in state .items () if k not in CHECKOUT_NON_DIR_KEYS ]
93+
94+ def checkoutBuildOnlyState (checkoutStep , inputHashes ):
95+ """Obtain state for build-only checkout updates.
96+
97+ The assumption is that we can run updates as long as all local SCMs stayed
98+ the same. As this is just for build-only builds, we can assume that
99+ dependencies have been setup correctly (direct deps, tools).
100+
101+ Because of the fixImportScmVariant bug, we include the directory too. This
102+ should not have been necessary otherwise. Needs to return a tuple because
103+ that's what is expected in the checkout SCM state (will be silently
104+ upgraded to a tuple by BobState otherwise).
105+ """
106+ return ("\n " .join ("{} {}" .format (scm .getDirectory (), scm .asDigestScript ())
107+ for scm in checkoutStep .getScmList ()
108+ if scm .isLocal ()),
109+ checkoutStep .getUpdateScriptDigest (),
110+ inputHashes )
111+
112+ def checkoutBuildOnlyStateCompatible (left , right ):
113+ """Returns True if it's safe to run build-only checkout updates.
114+
115+ Current policy is to just require that local SCMs are compatible. A
116+ different step variant-id, changed update script or dependency changes do
117+ *not* prohibit an in-place update.
118+ """
119+ left = left .get (CHECKOUT_STATE_BUILD_ONLY , (None , None , None ))[0 ]
120+ right = right .get (CHECKOUT_STATE_BUILD_ONLY , (None , None , None ))[0 ]
121+ return left == right
122+
123+ def checkoutBuildOnlyStateChanged (left , right ):
124+ """Returns True if the update script has changed"""
125+ left = left .get (CHECKOUT_STATE_BUILD_ONLY , None )
126+ right = right .get (CHECKOUT_STATE_BUILD_ONLY , None )
127+ return left != right
128+
79129def dissectPackageInputState (oldInputBuildId ):
80130 """Take a package step input hashes and convert them to a common
81131 representation.
@@ -1016,17 +1066,25 @@ async def _cookCheckoutStep(self, checkoutStep, depth):
10161066 checkoutExecuted = False
10171067 checkoutDigest = checkoutStep .getVariantId ()
10181068 checkoutState = checkoutStep .getScmDirectories ().copy ()
1019- checkoutState [None ] = (checkoutDigest , None )
1069+ checkoutState [CHECKOUT_STATE_VARIANT_ID ] = (checkoutDigest , None )
1070+ checkoutState [CHECKOUT_STATE_BUILD_ONLY ] = checkoutBuildOnlyState (checkoutStep , checkoutInputHashes )
10201071 if self .__buildOnly and (BobState ().getResultHash (prettySrcPath ) is not None ):
1021- inputChanged = checkoutInputHashes != BobState (). getInputHashes ( prettySrcPath )
1072+ inputChanged = checkoutBuildOnlyStateChanged ( checkoutState , oldCheckoutState )
10221073 rehash = lambda : hashWorkspace (checkoutStep )
1023- if not compareDirectoryState (checkoutState , oldCheckoutState ):
1074+ if checkoutStep .mayUpdate (inputChanged , BobState ().getResultHash (prettySrcPath ), rehash ):
1075+ if checkoutBuildOnlyStateCompatible (checkoutState , oldCheckoutState ):
1076+ with stepExec (checkoutStep , "UPDATE" ,
1077+ "{} {}" .format (prettySrcPath , overridesString )) as a :
1078+ await self ._runShell (checkoutStep , "checkout" , a , mode = InvocationMode .UPDATE )
1079+ newCheckoutState = oldCheckoutState .copy ()
1080+ newCheckoutState [CHECKOUT_STATE_BUILD_ONLY ] = checkoutState [CHECKOUT_STATE_BUILD_ONLY ]
1081+ BobState ().setDirectoryState (prettySrcPath , newCheckoutState )
1082+ else :
1083+ stepMessage (checkoutStep , "UPDATE" , "WARNING: recipe changed - cannot update ({})"
1084+ .format (prettySrcPath ), WARNING )
1085+ elif not compareDirectoryState (checkoutState , oldCheckoutState ):
10241086 stepMessage (checkoutStep , "CHECKOUT" , "WARNING: recipe changed but skipped due to --build-only ({})"
10251087 .format (prettySrcPath ), WARNING )
1026- elif checkoutStep .mayUpdate (inputChanged , BobState ().getResultHash (prettySrcPath ), rehash ):
1027- with stepExec (checkoutStep , "UPDATE" ,
1028- "{} {}" .format (prettySrcPath , overridesString )) as a :
1029- await self ._runShell (checkoutStep , "checkout" , a , mode = InvocationMode .UPDATE )
10301088 else :
10311089 stepMessage (checkoutStep , "CHECKOUT" , "skipped due to --build-only ({}) {}" .format (prettySrcPath , overridesString ),
10321090 SKIPPED , IMPORTANT )
@@ -1036,8 +1094,7 @@ async def _cookCheckoutStep(self, checkoutStep, depth):
10361094
10371095 if self .__cleanCheckout :
10381096 # check state of SCMs and invalidate if the directory is dirty
1039- for (scmDir , (scmDigest , scmSpec )) in oldCheckoutState .copy ().items ():
1040- if scmDir is None : continue
1097+ for (scmDir , (scmDigest , scmSpec )) in checkoutsFromState (oldCheckoutState ):
10411098 if scmDigest != checkoutState .get (scmDir , (None , None ))[0 ]: continue
10421099 if not os .path .exists (os .path .join (prettySrcPath , scmDir )): continue
10431100 if scmMap [scmDir ].status (checkoutStep .getWorkspacePath ()).dirty :
@@ -1050,8 +1107,8 @@ async def _cookCheckoutStep(self, checkoutStep, depth):
10501107 not compareDirectoryState (checkoutState , oldCheckoutState ) or
10511108 (checkoutInputHashes != BobState ().getInputHashes (prettySrcPath ))):
10521109 # Switch or move away old or changed source directories
1053- for (scmDir , (scmDigest , scmSpec )) in oldCheckoutState . copy (). items ( ):
1054- if ( scmDir is not None ) and ( scmDigest != checkoutState .get (scmDir , (None , None ))[0 ]) :
1110+ for (scmDir , (scmDigest , scmSpec )) in checkoutsFromState ( oldCheckoutState ):
1111+ if scmDigest != checkoutState .get (scmDir , (None , None ))[0 ]:
10551112 scmPath = os .path .normpath (os .path .join (prettySrcPath , scmDir ))
10561113 canSwitch = (scmDir in scmMap ) and scmDigest and \
10571114 scmSpec is not None and \
@@ -1087,8 +1144,8 @@ async def _cookCheckoutStep(self, checkoutStep, depth):
10871144 # workspace. Do it before we store the new SCM state to
10881145 # check again if the step is rerun.
10891146 if not checkoutStep .JENKINS :
1090- for scmDir in checkoutState . keys () :
1091- if scmDir is None or scmDir == "." : continue
1147+ for scmDir in [ k for k , v in checkoutsFromState ( checkoutState )] :
1148+ if scmDir == "." : continue
10921149 if scmDir in oldCheckoutState : continue
10931150 scmPath = os .path .normpath (os .path .join (prettySrcPath , scmDir ))
10941151 if os .path .exists (scmPath ):
@@ -1100,7 +1157,7 @@ async def _cookCheckoutStep(self, checkoutStep, depth):
11001157 # record the SCM directories as some checkouts might already
11011158 # succeeded before the step ultimately fails.
11021159 BobState ().setDirectoryState (prettySrcPath ,
1103- { d :s for (d ,s ) in checkoutState .items () if d is not None })
1160+ { d :s for (d ,s ) in checkoutState .items () if d != CHECKOUT_STATE_VARIANT_ID })
11041161
11051162 # Forge checkout result before we run the step again.
11061163 # Normally the correct result is set directly after the
0 commit comments