Skip to content

Commit 7b00892

Browse files
authored
Finalize scripts (#687)
Add support for scripts that are run after all "main" scripts.
2 parents 8c5544f + f6a53f4 commit 7b00892

3 files changed

Lines changed: 167 additions & 13 deletions

File tree

doc/manual/configuration.rst

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -682,6 +682,32 @@ the ``checkoutScript``.
682682
Other than the above differences setup scripts are identical to
683683
:ref:`configuration-recipes-scripts`.
684684

685+
.. _configuration-recipes-finalize:
686+
687+
{checkout,build,package}Finalize[{Bash,Pwsh}]
688+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
689+
690+
Type: String
691+
692+
Finalize scripts are appended after the regular scripts defined by
693+
:ref:`configuration-recipes-scripts`. They are intended for classes that need
694+
to run some cleanup or post-processing after the recipe (and any other
695+
inherited classes) already did their work, e.g. stripping binaries or fixing up
696+
permissions regardless of what the actual recipe script did.
697+
698+
The scripts of all inherited classes and the recipe are joined into a single
699+
script per step. While the regular scripts are joined in inheritance order
700+
(classes first, recipe last -- see :ref:`configuration-recipes-scripts`), the
701+
finalize scripts are appended afterwards in *reverse* order, that is the
702+
recipe's finalize script runs first, followed by the finalize scripts of the
703+
inherited classes, innermost class last. This mirrors the usual "wrapping"
704+
pattern where the outermost class is expected to run its finalization last.
705+
706+
Other than the above differences finalize scripts are identical to
707+
:ref:`configuration-recipes-scripts`. In particular a ``checkoutFinalize``
708+
script also renders the checkout indeterministic by default -- see
709+
:ref:`configuration-recipes-checkoutdeterministic`.
710+
685711
.. _configuration-recipes-tools:
686712

687713
{checkout,build,package}Tools
@@ -908,14 +934,14 @@ that extra care must be taken for a script to fetch always the same sources. If
908934
you are sure that the result of the checkout script is always the same you may
909935
set this to ``True``.
910936

911-
The ``checkoutDeterministic`` keyword only relates to the ``checkoutScript`` at
912-
the same level. Each recipe or class must declare the determinism of its
913-
``checkoutScript``. If there is no ``checkoutScript`` then
914-
``checkoutDeterministic`` implicitly defaults to ``True``. Everything in
915-
``checkoutSCM`` is *not* affected by ``checkoutDeterministic``. All SCMs
916-
included in Bob will determine their determinism based on the configuration
917-
automatically, e.g. using a commit or tag is considered deterministic while
918-
using a branch is indeterministic.
937+
The ``checkoutDeterministic`` keyword only relates to the ``checkoutScript``
938+
and ``checkoutFinalize`` scripts at the same level. Each recipe or class must
939+
declare the determinism of its scripts. If neither a ``checkoutScript`` nor a
940+
``checkoutFinalize`` script is defined then ``checkoutDeterministic``
941+
implicitly defaults to ``True``. Everything in ``checkoutSCM`` is *not*
942+
affected by ``checkoutDeterministic``. All SCMs included in Bob will determine
943+
their determinism based on the configuration automatically, e.g. using a commit
944+
or tag is considered deterministic while using a branch is indeterministic.
919945

920946
If the checkout is deemed deterministic it enables Bob to apply various
921947
optimizations. Deterministic checkouts do not need to be executed every time

pym/bob/input.py

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -131,26 +131,37 @@ def fetchScripts(recipe, prefix, resolveBash, resolvePwsh):
131131
prefix + "Setup[Bash]"),
132132
resolveBash(recipe.get(prefix + "ScriptBash", recipe.get(prefix + "Script")),
133133
prefix + "Script[Bash]"),
134+
resolveBash(recipe.get(prefix + "FinalizeBash", recipe.get(prefix + "Finalize")),
135+
prefix + "Finalize[Bash]"),
134136
),
135137
ScriptLanguage.PWSH : (
136138
resolvePwsh(recipe.get(prefix + "SetupPwsh", recipe.get(prefix + "Setup")),
137139
prefix + "Setup[Pwsh]"),
138140
resolvePwsh(recipe.get(prefix + "ScriptPwsh", recipe.get(prefix + "Script")),
139141
prefix + "Script[Pwsh]"),
142+
resolvePwsh(recipe.get(prefix + "FinalizePwsh", recipe.get(prefix + "Finalize")),
143+
prefix + "Finalize[Pwsh]"),
140144
)
141145
}
142146

143147
def mergeScripts(fragments, glue):
144148
"""Join all scripts of the recipe and its classes.
145149
146-
The result is a tuple with (setupScript, mainScript, digestScript)
150+
The result is a tuple with (setupScript, mainScript, digestScript). Note
151+
that the mainScript contains all "normal" scripts and all "Finalize"
152+
scripts in reverse order.
147153
"""
148154
return (
155+
# The "Setup" scripts
149156
joinScripts((f[0][0] for f in fragments), glue),
150-
joinScripts((f[1][0] for f in fragments), glue),
157+
# The "normal" scripts and the "Finalize" scripts
158+
joinScripts(chain((f[1][0] for f in fragments),
159+
(f[2][0] for f in reversed(fragments))), glue),
160+
# All the digest scripts
151161
joinScripts(
152162
( joinScripts((f[0][1] for f in fragments), "\n"),
153163
joinScripts((f[1][1] for f in fragments), "\n"),
164+
joinScripts((f[2][1] for f in fragments), "\n"),
154165
), "\n")
155166
)
156167

@@ -2340,13 +2351,14 @@ def resolveClasses(self, rootEnv):
23402351
self.__scriptLanguage = scriptLanguage
23412352
glue = getLanguage(self.__scriptLanguage).glue
23422353

2343-
# Consider checkout deterministic by default if no checkoutScript is
2344-
# involved. A potential checkoutSetup is ignored.
2354+
# Consider checkout deterministic by default if neither checkoutScript
2355+
# nor checkoutFinalize is involved. A potential checkoutSetup is
2356+
# ignored.
23452357
def coDet(r):
23462358
ret = r.__checkoutDeterministic
23472359
if ret is not None:
23482360
return ret
2349-
return r.__checkout[self.__scriptLanguage][1][0] is None
2361+
return all(s[0] is None for s in r.__checkout[self.__scriptLanguage][1:])
23502362

23512363
checkoutDeterministic = [ coDet(i) for i in inheritAll ]
23522364
self.__checkoutDeterministic = all(checkoutDeterministic)
@@ -4171,19 +4183,28 @@ def __createSchemas(self):
41714183
dependsInnerClause[schema.Optional('depends')] = dependsClause
41724184

41734185
classSchemaSpec = {
4186+
schema.Optional('checkoutFinalize') : str,
4187+
schema.Optional('checkoutFinalizeBash') : str,
4188+
schema.Optional('checkoutFinalizePwsh') : str,
41744189
schema.Optional('checkoutScript') : str,
41754190
schema.Optional('checkoutScriptBash') : str,
41764191
schema.Optional('checkoutScriptPwsh') : str,
41774192
schema.Optional('checkoutSetup') : str,
41784193
schema.Optional('checkoutSetupBash') : str,
41794194
schema.Optional('checkoutSetupPwsh') : str,
41804195
schema.Optional('checkoutUpdateIf', default=False) : schema.Or(None, str, bool, IfExpression),
4196+
schema.Optional('buildFinalize') : str,
4197+
schema.Optional('buildFinalizeBash') : str,
4198+
schema.Optional('buildFinalizePwsh') : str,
41814199
schema.Optional('buildScript') : str,
41824200
schema.Optional('buildScriptBash') : str,
41834201
schema.Optional('buildScriptPwsh') : str,
41844202
schema.Optional('buildSetup') : str,
41854203
schema.Optional('buildSetupBash') : str,
41864204
schema.Optional('buildSetupPwsh') : str,
4205+
schema.Optional('packageFinalize') : str,
4206+
schema.Optional('packageFinalizeBash') : str,
4207+
schema.Optional('packageFinalizePwsh') : str,
41874208
schema.Optional('packageScript') : str,
41884209
schema.Optional('packageScriptBash') : str,
41894210
schema.Optional('packageScriptPwsh') : str,

test/unit/test_input_recipe.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -817,3 +817,110 @@ def testIfExpr(self):
817817
self.assertTrue(p.isShared())
818818
p = self.parseAndPrepare(recipe, env={"ENABLE" : "no"}).getPackageStep()
819819
self.assertFalse(p.isShared())
820+
821+
822+
# Added in Python 3.10: itertools.pairwise
823+
def pairwise(seq):
824+
prev = seq[0]
825+
for i in seq[1:]:
826+
yield (prev, i)
827+
prev = i
828+
829+
class TestFinalizeScripts(RecipeCommon, TestCase):
830+
831+
def testOrder(self):
832+
"""Finalize scripts are evaluated in reverse order"""
833+
recipe = {
834+
"inherit" : ["a", "b"],
835+
"packageScript" : "RECIPE-SCRIPT",
836+
"packageFinalize" : "RECIPE-FINALIZE",
837+
}
838+
classes = {
839+
"a" : {
840+
"packageScript" : "CLASS-A-SCRIPT",
841+
"packageFinalize" : "CLASS-A-FINALIZE",
842+
},
843+
"b" : {
844+
"packageScript" : "CLASS-B-SCRIPT",
845+
"packageFinalize" : "CLASS-B-FINALIZE",
846+
}
847+
}
848+
849+
mainScript = self.parseAndPrepare(recipe, classes).getPackageStep().getMainScript()
850+
851+
snippets = (
852+
"CLASS-A-SCRIPT",
853+
"CLASS-B-SCRIPT",
854+
"RECIPE-SCRIPT",
855+
"RECIPE-FINALIZE",
856+
"CLASS-B-FINALIZE",
857+
"CLASS-A-FINALIZE",
858+
)
859+
860+
for s in snippets:
861+
self.assertIn(s, mainScript)
862+
863+
indexes = [ mainScript.index(s) for s in snippets ]
864+
diffs = [ b-a for a, b in pairwise(indexes) ]
865+
self.assertEqual(len(indexes), len(diffs)+1)
866+
self.assertTrue(all(d > 0 for d in diffs))
867+
868+
def testCheckoutDeterministic(self):
869+
"""checkoutDeterministic applies equally to checkoutScript and checkoutFinalize"""
870+
871+
# checkoutFinalize alone -> indeterministic
872+
recipe = {
873+
"checkoutFinalize" : "asdf",
874+
"buildScript" : "asdf",
875+
"packageScript" : "asdf",
876+
}
877+
c = self.parseAndPrepare(recipe).getCheckoutStep()
878+
self.assertFalse(c.isDeterministic())
879+
880+
# checkoutScript and checkoutFinalize -> indeterministic
881+
recipe = {
882+
"checkoutScript" : "asdf",
883+
"checkoutFinalize" : "asdf",
884+
"buildScript" : "asdf",
885+
"packageScript" : "asdf",
886+
}
887+
c = self.parseAndPrepare(recipe).getCheckoutStep()
888+
self.assertFalse(c.isDeterministic())
889+
890+
# checkoutFinalize can be overridden by checkoutDeterministic just
891+
# like checkoutScript
892+
893+
recipe = {
894+
"checkoutFinalize" : "asdf",
895+
"checkoutDeterministic" : True,
896+
"buildScript" : "asdf",
897+
"packageScript" : "asdf",
898+
}
899+
c = self.parseAndPrepare(recipe).getCheckoutStep()
900+
self.assertTrue(c.isDeterministic())
901+
902+
recipe = {
903+
"checkoutScript" : "asdf",
904+
"checkoutFinalize" : "asdf",
905+
"checkoutDeterministic" : True,
906+
"buildScript" : "asdf",
907+
"packageScript" : "asdf",
908+
}
909+
c = self.parseAndPrepare(recipe).getCheckoutStep()
910+
self.assertTrue(c.isDeterministic())
911+
912+
def testCheckoutDeterministicInherit(self):
913+
"""checkoutFinalize of inherited classes also affects determinism"""
914+
915+
recipe = {
916+
"inherit" : [ "bar" ],
917+
"buildScript" : "asdf",
918+
"packageScript" : "asdf",
919+
}
920+
classes = {
921+
"bar" : {
922+
"checkoutFinalize" : "asdf",
923+
},
924+
}
925+
c = self.parseAndPrepare(recipe, classes).getCheckoutStep()
926+
self.assertFalse(c.isDeterministic())

0 commit comments

Comments
 (0)