Skip to content

Commit 9a17f65

Browse files
authored
Merge pull request #599 from jkloetzke/managed-layers-audit
Add managed layers to audit trail
2 parents b9cf8fe + c2e6a00 commit 9a17f65

File tree

12 files changed

+171
-58
lines changed

12 files changed

+171
-58
lines changed

pym/bob/audit.py

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ def digestData(d, h):
4242
elif isinstance(d, bytes):
4343
h.update(struct.pack("<BI", 6, len(d)))
4444
h.update(d)
45+
elif d is None:
46+
h.update(struct.pack("<B", 7))
4547
else:
4648
assert False, "Cannot digest " + str(type(d))
4749

@@ -80,6 +82,7 @@ class Artifact:
8082
schema.Optional('metaEnv') : { schema.Optional(str) : str },
8183
"scms" : [ dict ],
8284
schema.Optional("recipes") : dict,
85+
schema.Optional("layers") : dict,
8386
"dependencies" : {
8487
schema.Optional('args') : [ HexValidator() ],
8588
schema.Optional('tools') : { str : HexValidator() },
@@ -122,6 +125,7 @@ def reset(self, variantId, buildId, resultHash):
122125
self.__buildId = buildId
123126
self.__resultHash = resultHash
124127
self.__recipes = None
128+
self.__layers = {}
125129
self.__defines = {}
126130
u = platform.uname()
127131
self.__build = {
@@ -155,6 +159,13 @@ def load(self, data):
155159
else:
156160
self.__recipes = None
157161

162+
layers = data.get("layers")
163+
if layers:
164+
self.__layers = { name : (audit and auditFromData(audit))
165+
for name, audit in layers.items() }
166+
else:
167+
self.__layers = {}
168+
158169
self.__defines = data["meta"]
159170
self.__build = data["build"]
160171
self.__env = data["env"]
@@ -206,11 +217,18 @@ def __dump(self):
206217
if self.__recipes is not None:
207218
ret["recipes"] = self.__recipes.dump()
208219

220+
if self.__layers: # Explicitly filter empty layers
221+
ret["layers"] = { name : (audit and audit.dump())
222+
for name, audit in self.__layers.items() }
223+
209224
return ret
210225

211226
def setRecipes(self, recipes):
212227
self.__recipes = recipes
213228

229+
def setLayers(self, layers):
230+
self.__layers = layers
231+
214232
def setEnv(self, env):
215233
try:
216234
with open(env) as f:
@@ -338,7 +356,8 @@ def load(self, file, name):
338356
r["artifact-id"] : Artifact.fromData(r) for r in tree["references"]
339357
}
340358
except schema.SchemaError as e:
341-
raise ParseError(name + ": Invalid audit record: " + str(e))
359+
raise ParseError(name + ": Invalid audit record: " + str(e),
360+
help="Try updating to the latest Bob version. The audit probably contains new record types.")
342361
except ValueError as e:
343362
raise ParseError(name + ": Invalid json: " + str(e))
344363
self.__validate()
@@ -385,11 +404,11 @@ def getReferencedBuildIds(self):
385404
refs.update(artifact.getReferences())
386405
return sorted(ret)
387406

388-
def setRecipesAudit(self, recipes):
389-
self.__artifact.setRecipes(recipes)
390-
391-
def setRecipesData(self, xml):
392-
self.__artifact.setRecipes(auditFromData(xml))
407+
def setRecipesAudit(self, recipesAudit):
408+
self.__artifact.setRecipes(recipesAudit.get(""))
409+
self.__artifact.setLayers({
410+
layer : audit for layer, audit in recipesAudit.items() if layer != ""
411+
})
393412

394413
def setEnv(self, env):
395414
self.__artifact.setEnv(env)

pym/bob/cmds/graph.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ def getColorId(package, level):
5959
yield ('link', d.getPackage(), package)
6060
yield from findPackages(d.getPackage(), excludes, highlights, done, maxdepth, donePackages, level+1)
6161

62-
def makeD3Graph(packages, p, filename, options, excludes, highlights, maxdepth):
62+
def makeD3Graph(recipes, packages, p, filename, options, excludes, highlights, maxdepth):
6363
def getHover(package, options):
6464
hover = collections.OrderedDict(package.getMetaEnv())
6565
if options.get('d3.showScm', False) and package.getCheckoutStep().isValid():
@@ -469,7 +469,7 @@ def getHover(package, options):
469469
simulation.force("link").links(links)
470470
</script>
471471
<div class='info' style="width: 100%; text-align: center;">
472-
<div id='innerLeft' style="float: left"> Recipes: """ + runInEventLoop(RecipeSet().getScmStatus()) + """</div>
472+
<div id='innerLeft' style="float: left"> Recipes: """ + runInEventLoop(recipes.getScmStatus()) + """</div>
473473
<div id='innerRight' style="float: right">Bob version: """ + BOB_VERSION +"""</div>
474474
<div id='innerMiddle' style="display: inline-block">Generated using <a href="https://www.d3js.org">D3JS</a></div>
475475
</div>
@@ -584,6 +584,6 @@ def doGraph(argv, bobRoot):
584584
print(colorize(" GRAPH {} ({})".format(p, args.type), "32"))
585585

586586
if args.type == 'd3':
587-
makeD3Graph(packages, p, os.path.join(destination, filename), options, excludes, highlights, args.max_depth)
587+
makeD3Graph(recipes, packages, p, os.path.join(destination, filename), options, excludes, highlights, args.max_depth)
588588
elif args.type == 'dot':
589589
makeDotGraph(packages, p, os.path.join(destination, filename), excludes, highlights, args.max_depth)

pym/bob/cmds/jenkins/exec.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ def recipesAudit(self):
8686
if self.__recipesAudit:
8787
return json.loads("".join(self.__recipesAudit))
8888
else:
89-
return None
89+
return {}
9090

9191
@property
9292
def execIR(self):

pym/bob/cmds/jenkins/intermediate.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,8 @@ class PartialRecipeSet(PartialIRBase, RecipeSetIR):
7474
@classmethod
7575
def fromData(cls, data, scmAudit):
7676
self = super(PartialRecipeSet, cls).fromData(data)
77-
self.__scmAudit = scmAudit and auditFromData(scmAudit)
77+
self.__scmAudit = scmAudit and { name : (audit and auditFromData(audit))
78+
for name, audit in scmAudit.items() }
7879
return self
7980

8081
async def getScmAudit(self):
@@ -94,7 +95,7 @@ def __init__(self):
9495
self.packages = {}
9596
self.recipes = {}
9697
self.recipeSet = None
97-
self.scmAudit = None
98+
self.scmAudit = {}
9899

99100
@classmethod
100101
def fromData(cls, data):

pym/bob/cmds/jenkins/jenkins.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1281,8 +1281,10 @@ def doJenkinsPush(recipes, argv):
12811281
date = str(datetime.datetime.now())
12821282

12831283
recipesAudit = runInEventLoop(recipes.getScmAudit())
1284-
if recipesAudit is not None:
1285-
recipesAudit = json.dumps(recipesAudit.dump(), sort_keys=True)
1284+
if recipesAudit:
1285+
recipesAudit = json.dumps({ name : (audit and audit.dump())
1286+
for name, audit in recipesAudit.items() },
1287+
sort_keys=True)
12861288

12871289
def printLine(level, job, *args):
12881290
if level <= verbose:

pym/bob/input.py

Lines changed: 47 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from .languages import getLanguage, ScriptLanguage, BashLanguage, PwshLanguage
99
from .pathspec import PackageSet
1010
from .scm import CvsScm, GitScm, ImportScm, SvnScm, UrlScm, ScmOverride, \
11-
auditFromDir, getScm, SYNTHETIC_SCM_PROPS
11+
auditFromDir, auditFromProperties, getScm, SYNTHETIC_SCM_PROPS
1212
from .state import BobState
1313
from .stringparser import checkGlobList, Env, DEFAULT_STRING_FUNS, IfExpression
1414
from .tty import InfoOnce, Warn, WarnOnce, setColorMode, setParallelTUIThreshold
@@ -1739,13 +1739,28 @@ def getScm(self):
17391739
return self.__scm
17401740

17411741
class LayerValidator:
1742+
@staticmethod
1743+
def __validateName(name):
1744+
if name == "":
1745+
raise schema.SchemaError("Layer name must not be empty")
1746+
if any((c in name) for c in '\\/'):
1747+
raise schema.SchemaError("Invalid character in layer name")
1748+
17421749
def validate(self, data):
1743-
if isinstance(data,str):
1750+
if isinstance(data, str):
1751+
self.__validateName(data)
17441752
return LayerSpec(data)
1753+
elif not isinstance(data, dict):
1754+
raise schema.SchemaUnexpectedTypeError("Layer entry must be a string or a dict", None)
1755+
17451756
if 'name' not in data:
17461757
raise schema.SchemaMissingKeyError("Missing 'name' key in {}".format(data), None)
1758+
elif not isinstance(data['name'], str):
1759+
raise schema.SchemaUnexpectedTypeError("Layer name must be a string", None)
1760+
17471761
_data = data.copy();
17481762
name = _data.get('name')
1763+
self.__validateName(name)
17491764
del _data['name']
17501765

17511766
return LayerSpec(name, RecipeSet.LAYERS_SCM_SCHEMA.validate(_data)[0])
@@ -2957,11 +2972,12 @@ class RecipeSet:
29572972
})
29582973

29592974
# We do not support the "import" SCM for layers. It just makes no sense.
2975+
# Also, all SCMs lack the "dir" and "if" attributes.
29602976
LAYERS_SCM_SCHEMA = ScmValidator({
2961-
'git' : GitScm.SCHEMA,
2962-
'svn' : SvnScm.SCHEMA,
2963-
'cvs' : CvsScm.SCHEMA,
2964-
'url' : UrlScm.SCHEMA,
2977+
'git' : GitScm.LAYERS_SCHEMA,
2978+
'svn' : SvnScm.LAYERS_SCHEMA,
2979+
'cvs' : CvsScm.LAYERS_SCHEMA,
2980+
'url' : UrlScm.LAYERS_SCHEMA,
29652981
})
29662982

29672983
SCM_SCHEMA = ScmValidator({
@@ -3101,7 +3117,7 @@ def __init__(self):
31013117
self.__commandConfig = {}
31023118
self.__uiConfig = {}
31033119
self.__shareConfig = {}
3104-
self.__layers = []
3120+
self.__layers = {}
31053121
self.__buildHooks = {}
31063122
self.__sandboxOpts = {}
31073123
self.__scmDefaults = {}
@@ -3424,20 +3440,34 @@ async def getScmAudit(self):
34243440
try:
34253441
ret = self.__recipeScmAudit
34263442
except AttributeError:
3443+
ret = {}
34273444
try:
3428-
ret = await auditFromDir(".")
3445+
ret[""] = await auditFromDir(".")
34293446
except BobError as e:
34303447
Warn("could not determine recipes state").warn(e.slogan)
3431-
ret = None
3448+
3449+
# We look into every layers directory. If the Bob state knows it,
3450+
# use the SCM information from there. Otherwise make a best guess.
3451+
for layer, path in sorted(self.__layers.items()):
3452+
state = None
3453+
try:
3454+
scmProps = (BobState().getLayerState(path) or {}).get("prop")
3455+
if scmProps is None:
3456+
state = await auditFromDir(path)
3457+
else:
3458+
state = await auditFromProperties(path, scmProps)
3459+
except BobError as e:
3460+
Warn(f"could not determine layer '{layer}' state").warn(e.slogan)
3461+
ret[layer] = state
3462+
34323463
self.__recipeScmAudit = ret
34333464
return ret
34343465

34353466
async def getScmStatus(self):
3436-
audit = await self.getScmAudit()
3437-
if audit is None:
3438-
return "unknown"
3439-
else:
3440-
return audit.getStatusLine()
3467+
scmAudit = await self.getScmAudit()
3468+
return ", ".join(( ((f"layer {name}: " if name else "")
3469+
+ ("unknown" if audit is None else audit.getStatusLine()))
3470+
for name, audit in sorted(scmAudit.items()) ))
34413471

34423472
def getBuildHook(self, name):
34433473
return self.__buildHooks.get(name)
@@ -3481,7 +3511,7 @@ def __parse(self, envOverrides, platform, recipesRoot=""):
34813511
if platform not in ('cygwin', 'darwin', 'linux', 'msys', 'win32'):
34823512
raise ParseError("Invalid platform: " + platform)
34833513
self.__platform = platform
3484-
self.__layers = []
3514+
self.__layers = {}
34853515
self.__whiteList = getPlatformEnvWhiteList(platform)
34863516
self.__pluginPropDeps = b''
34873517
self.__pluginSettingsDeps = b''
@@ -3585,7 +3615,6 @@ def __parseLayer(self, layerSpec, maxVer, recipesRoot, upperLayer):
35853615

35863616
if layer in self.__layers:
35873617
return
3588-
self.__layers.append(layer)
35893618

35903619
if managedLayers:
35913620
# SCM backed layers are in build dir, regular layers are in
@@ -3602,6 +3631,8 @@ def __parseLayer(self, layerSpec, maxVer, recipesRoot, upperLayer):
36023631
for l in layer.split("/") ))
36033632
if not os.path.isdir(rootDir):
36043633
raise ParseError(f"Layer '{layer}' does not exist!")
3634+
3635+
self.__layers[layer] = rootDir
36053636
else:
36063637
rootDir = recipesRoot
36073638

pym/bob/layers.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,6 @@ def __init__(self, name, upperConfig, defines, projectRoot, scm=None):
100100
async def __checkoutTask(self, verbose, attic):
101101
if self.__scm is None:
102102
return
103-
dir = self.__scm.getProperties(False).get("dir")
104103

105104
invoker = Invoker(spec=LayerStepSpec(self.__layerDir, self.__upperConfig.envWhiteList()),
106105
preserveEnv= False,

pym/bob/scm/__init__.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,21 @@ async def auditFromDir(dir):
2121
else:
2222
return None
2323

24+
async def auditFromProperties(baseDir, props):
25+
auditSpec = getScm(props).getAuditSpec()
26+
if auditSpec is None:
27+
return None
28+
29+
SCMS = {
30+
'git' : GitAudit,
31+
'svn' : SvnAudit,
32+
'url' : UrlAudit,
33+
'import' : ImportAudit,
34+
}
35+
36+
(typ, subDir, extra) = auditSpec
37+
return await SCMS[typ].fromDir(baseDir, subDir, extra)
38+
2439
def auditFromData(data):
2540
typ = data.get("type")
2641
if typ == "git":

pym/bob/scm/cvs.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,17 @@ class CvsScm(Scm):
1919
'scm' : 'cvs',
2020
'cvsroot' : str,
2121
'module' : str,
22-
schema.Optional('if') : str,
2322
schema.Optional('rev') : str
2423
}
2524

26-
SCHEMA = schema.Schema({**__SCHEMA, **DEFAULTS})
25+
SCHEMA = schema.Schema({
26+
**__SCHEMA,
27+
**DEFAULTS,
28+
schema.Optional('if') : str,
29+
})
30+
31+
# Layers have no "dir" and no "if"
32+
LAYERS_SCHEMA = schema.Schema({ **__SCHEMA })
2733

2834
# Checkout using CVS
2935
# - mandatory parameters: cvsroot, module

0 commit comments

Comments
 (0)