/
mcconf.py
executable file
·623 lines (532 loc) · 25.3 KB
/
mcconf.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
#!/usr/bin/env python
import sys
import os
# used to access the mcconf default files
mcconf_dir = os.path.dirname(sys.argv[0])
import toml
from pathlib2 import Path
import logging
import argparse
import re
#import pydotplus.graphviz as pydot
import pydot
import shutil
import mako.template
import mako.runtime
def findFiles(basedir, patterns):
"""find files relative to a base directory according to a list of patterns.
Returns a set of file names."""
files = set()
path = Path(basedir)
for pattern in patterns:
files.update([m.relative_to(basedir).as_posix() for m in path.glob(pattern)])
return files
class ModFile:
"""represents a source to destination file mapping and is associated to its origin module.
Attributes:
module the module this file belongs to, used for path resolution.
srcname the path to the source file as given in the module definition,
relative to the module file.
srcfile the absolute path to the source file.
dstfile the relative path to the destination file, relative to the target root.
installMode how this file will be installed (link, hardlink, cinclude, copy).
"""
def __init__(self, module, filename):
self.installMode = 'link'
self.module = module
self.srcname = filename
self.srcfile = os.path.join(self.module.moduledir, self.srcname)
dstpath = os.path.join(self.module.dstdir, os.path.dirname(filename))
dstname = os.path.basename(filename)
if dstname.startswith("mako_"):
self.installMode = 'mako'
dstname = dstname[len("mako_"):]
self.dstfile = os.path.join(dstpath, dstname)
def __repr__(self): return self.dstfile
@property
def dependencies(self):
"""a list(string) with all C/C++ include dependencies"""
incrgx = re.compile('^#include\\s+[<\\"]([\\w./-]+)[>\\"]', re.MULTILINE)
# TODO detect the file type and choose a respective scanner instead
# of handling all files as source files
# TODO scan for special syntax that declares required and provided symbols for mcconf
includes = list()
srcdir = os.path.dirname(self.srcfile)
try:
with open(self.srcfile) as fin:
for m in incrgx.finditer(fin.read()):
inc = m.group(1)
# if file is locally referenced e.g. 'foo' instead of 'path/to/foo'
# TODO this will break when we begin to rename files during composition
# ie. will have to check if moduledir/inc is one of the modules dstfiles
# would be better, to prohibit this per convention,
# ie "" always relative to sourcefile, <> always relative to the logical root
if os.path.exists(os.path.join(srcdir,inc)):
inc = os.path.relpath(os.path.join(srcdir,inc),
self.module.moduledir)
includes.append(inc)
except Exception as e:
logging.warning("could not load %s from %s: %s", fpath, self.modulefile, e)
return includes
@property
def isCopy(self): return self.installMode not in ["link","hardlink"]
def install(self, tgtdir, tmplenv):
"""install the file into the target directory."""
srcfile = os.path.abspath(self.srcfile)
tgtfile = os.path.abspath(os.path.join(tgtdir, self.dstfile))
logging.debug('installing file %s to %s mode %s from module %s',
srcfile, tgtfile, self.installMode, self.module)
if not os.path.isfile(srcfile):
logging.warning('file %s is missing or not regular file, provided by %s from %s',
self.srcfile, self.module, self.module.modulefile)
if not os.path.exists(os.path.dirname(tgtfile)):
os.makedirs(os.path.dirname(tgtfile))
# TODO ideally we should never overwrite files, reconfig run should delete previously installed files
if os.path.exists(tgtfile) or os.path.islink(tgtfile):
os.unlink(tgtfile)
if self.installMode=='link':
os.symlink(os.path.relpath(srcfile, os.path.dirname(tgtfile)), tgtfile)
elif self.installMode=='hardlink':
os.link(srcfile, tgtfile)
elif self.installMode=='cinclude':
with open(tgtfile, 'w') as f:
f.write('#include "'+os.path.relpath(srcfile, tgtfile)+'"\n')
elif self.installMode=='mako':
with open(tgtfile, 'w') as f:
tmpl = mako.template.Template(filename=self.srcfile,
imports=['import os'])
ctx = mako.runtime.Context(f, **tmplenv)
tmpl.render_context(ctx)
shutil.copymode(srcfile, tgtfile)
else: # copy the file
shutil.copy2(srcfile, tgtfile)
class Module:
"""represents a code module as collection of source files and dependencies.
Attributes:
name the name of the module as given in its definition.
modulefile the absolute path to the file containing this definition.
moduledir the absolute path to the directory that contains the module,
used to resolve relative source files.
dstdir can be used to modify the destination path.
files a dictionary from role (e.g. incfiles, kernelfiles) to list(ModFile).
"""
def __init__(self, name, modulefile):
self.name = name
self.modulefile = os.path.abspath(modulefile)
self.moduledir = os.path.dirname(self.modulefile)
self.dstdir = ""
self.files = dict()
self._requires = set()
self._provides = set()
self.modules = set()
self.copyfiles = set()
self.vars = dict() # all unknown fields from the configuration
self.providedFiles = set()
self.requiredFiles = set()
self.noauto = False
def __repr__(self): return self.name
def addFiles(self, role, names):
if role not in self.files: self.files[role] = list()
self.files[role] += [ModFile(self, name) for name in names]
@property
def requires(self): return self._requires | self.requiredFiles
def addRequires(self, s): self._requires.update(s)
@property
def provides(self): return self._provides | self.providedFiles
def addProvides(self, s): self._provides.update(s)
def finish(self):
"""finish the initialization of the module after all fields are set."""
for role in self.files:
for m in self.files[role]:
self.providedFiles.add(m.dstfile)
self.requiredFiles.update(m.dependencies)
if m.srcname in self.copyfiles: m.installMode = 'copy'
self.requiredFiles -= self.providedFiles
class ModuleDB:
"""a collection of modules and dependency relationships."""
def __init__(self):
self.modules = dict()
self.provides = dict()
self.requires = dict()
def addModule(self, mod):
"""Add a module to the database and extend the dependency tables."""
# TODO should be done after importing all modules
# in order to support overriding module definitions
if mod.name in self.modules:
logging.warning('ignoring duplicate module %s from %s and %s',
mod.name, mod.modulefile,
self.modules[mod.name].modulefile)
return
logging.debug('loaded %s from %s', mod.name, mod.modulefile)
self.modules[mod.name] = mod
# this module can be loaded automatically to solve following depedencies
if not mod.noauto:
for tag in mod.provides:
if tag not in self.provides: self.provides[tag] = set()
self.provides[tag].add(mod)
# this module requires following dependencies in order to be useable
for tag in mod.requires:
if tag not in self.requires: self.requires[tag] = set()
self.requires[tag].add(mod)
def __getitem__(self,index):
return self.modules[index]
def has(self, key):
return key in self.modules
def getProvides(self, tag):
"""Return a set of modules that provide this tag."""
if tag not in self.provides:
return set()
return self.provides[tag]
def isResolvable(self, mod, provided):
""" Test whether there is a chance that the dependencies of a module are resolvable. """
for req in mod.requires:
if not (req in provided or len(self.getProvides(req))):
logging.debug('Ignoring module %s because of unresolvable dependency on %s', mod.name, req)
return False
return True
def getResolvableProvides(self, tag, provided):
return set([mod for mod in self.getProvides(tag) if self.isResolvable(mod, provided)])
def getRequires(self, tag):
"""Return a set of modules that require this tag."""
if tag not in self.requires:
return set()
return self.requires[tag]
def getModules(self):
return self.modules.values()
def getSolutionCandidates(self, mod):
"""Find all modules that satisfy at least one dependency of the module.
Returns a dictionary mapping modules to the tags they would satisfy."""
dstmods = dict()
for req in mod.requires:
for dst in self.getProvides(req):
if dst not in dstmods: dstmods[dst] = set()
dstmods[dst].add(req)
return dstmods
def getConflictingModules(self, mod):
"""inefficient method to find all modules that are in conflict with the given one.
Returns a dictionary mapping modules to the conflicting tags."""
dstmods = dict()
for prov in mod.provides:
for dst in self.getProvides(prov):
if dst.name == mod.name: continue
if dst not in dstmods: dstmods[dst] = set()
dstmods[dst].add(prov)
return dstmods
def checkConsistency(self):
for mod in self.getModules():
includes = mod.requiredFiles
dups = mod.requires & includes # without required files!
if dups:
logging.warning('Module %s(%s) contains unnecessary requires: %s',
mod.name, mod.modulefile, str(dups))
requires = set(self.requires.keys())
provides = set(self.provides.keys())
unsat = requires - provides
if unsat != set():
for require in unsat:
names = [m.name+'('+m.modulefile+')' for m in self.getRequires(require)]
logging.info('Tag %s required by %s not provided by any module',
require, str(names))
class Configuration:
def __init__(self, conffile):
self.moduledirs = list()
self.provides = set()
self.requires = set()
self.modules = set()
self.dstdir = '.'
self.acceptedMods = set() # set of selected module objects
self.files = dict() # dict role -> dict dstfile -> ModFile
self.allfiles = dict() # dict dstfile -> ModFile
self.vars = {"config_file": os.path.abspath(conffile)}
self.modDB = ModuleDB()
def applyModules(self, pendingMods):
'''add all modules of the list to the configuration, including referenced modules'''
pendingMods = pendingMods.copy()
while len(pendingMods) > 0:
modname = pendingMods.pop()
# 1) error if module not available
if not self.modDB.has(modname):
raise Exception("Didn't find module " + modname)
# 2) ignore if module already selected
mod = self.modDB[modname]
if mod in self.acceptedMods: continue
# 3) error if conflict with previously selected module
# conflict if one of the provides is already provided
conflicts = self.provides & mod.provides
if conflicts:
for tag in conflicts:
conflictMods = self.modDB.getProvides(tag) & self.acceptedMods
cnames = [m.name for m in conflictMods]
logging.warning("requested module %s tag %s conflicts with %s",
mod.name, tag, str(cnames))
raise Exception("requested module " + modname +
" conflicts with previously selected modules")
# conflictMods = self.modDB.getConflictingModules(mod)
# conflicts = conflictMods.keys() & self.acceptedMods
# if conflicts:
# # TODO add more diagnostigs: which tags/files do conflict?
# cnames = [m.name for m in (conflicts|set(mod))]
# logging.warning("selected conflicting modules: "+cnames)
self.acceptedMods.add(mod)
pendingMods |= mod.modules
self.requires |= mod.requires
self.provides |= mod.provides
for role in mod.files:
for mf in mod.files[role]:
if mf.dstfile in self.allfiles:
logging.warning('duplicate file %s from module %s and %s',
mf, mod. self.allfiles[mf.dstfile].module)
if role not in self.files: self.files[role] = dict()
self.files[role][mf.dstfile] = mf
self.allfiles[mf.dstfile] = mf
def getMissingRequires(self):
return self.requires - self.provides
def processModules(self, resolveDeps):
'''if resolveDeps is true, this method tries to resolve missing dependencies
by including additional modules from the module DB'''
self.applyModules(self.modules)
if resolveDeps:
additionalMods = self.resolveDependencies()
names = ", ".join(sorted([m.name for m in additionalMods]))
logging.info('added modules to resolve dependencies: %s', names)
missingRequires = self.getMissingRequires()
for tag in missingRequires:
reqMods = self.modDB.getRequires(tag) & self.acceptedMods
req = [m.name for m in reqMods]
prov = [m.name for m in self.modDB.getProvides(tag)]
logging.warning('unresolved dependency %s required by [%s] provided by [%s]',
tag, ', '.join(req), ', '.join(prov))
def resolveDependencies(self):
additionalMods = set()
missingRequires = self.getMissingRequires()
count = 1
while count:
count = 0
for tag in missingRequires:
solutions = self.modDB.getResolvableProvides(tag, self.provides)
# 1) ignore modules that have conflicts already
good_solutions = set()
for mod in solutions:
conflicts = self.provides & mod.provides
if conflicts:
logging.debug('Ignoring module %s for dependency %s because of conflicts with already selected modules providing %s',
mod.name, tag, [str(c) for c in conflicts])
else:
good_solutions.add(mod)
# 2) ignore if none or multiple solutions
if len(good_solutions) < 1:
logging.debug('Did not satisfy dependency %s because of no possible solution.',
tag)
continue
elif len(good_solutions) > 1:
logging.debug('Did not satisfy dependency %s because of ambiguous solutions %s',
tag, [mod.name for mod in good_solutions])
continue
# 3) select the module
mod = good_solutions.pop()
logging.debug('Selecting module %s for dependency %s', mod.name, tag)
additionalMods.add(mod)
count += 1
self.applyModules(set([mod.name]))
missingRequires = self.getMissingRequires()
break
# return set of additionally selected modules
return additionalMods
def checkConsistency(self):
selected = set([self.modDB[n] for n in self.modules])
removable = set()
for mod in selected:
# 1) modules with conflicts should be selected
conflictMods = self.modDB.getConflictingModules(mod)
if conflictMods: continue
# 2) modules that do not satisfy any dependency should be selected
providesAnything = False
for tag in mod.provides:
if self.modDB.getRequires(tag) & selected:
providesAnything = True
if not providesAnything: continue
# remember all other modules
removable.add(mod)
if removable:
logging.info("following modules could be resolved automatically: %s",
str(removable))
def install(self):
tmplenv = {"vars": argparse.Namespace(**self.vars), "modules": self.acceptedMods,
"dstdir": os.path.abspath(self.dstdir),
"files": self.files, "allfiles":self.allfiles,
}
tmplenv['replaceSuffix'] = lambda str, osuf, nsuf: str[:-len(osuf)] + nsuf
tmplenv['relpath'] = lambda str: os.path.relpath(str, os.path.abspath(self.dstdir))
def tmplIncludeModules(var, ctx):
for mod in sorted(ctx["modules"],key=lambda x:x.name):
if var in mod.vars:
ctx.write("#--- "+var+" from module "+mod.name+"\n")
mako.template.Template(mod.vars[var], imports=['import os']).render_context(ctx)
ctx.write("#--- end module "+mod.name+"\n\n")
return ''
tmplenv['includeModules'] = tmplIncludeModules
if not os.path.exists(self.dstdir): os.makedirs(self.dstdir)
for k in self.allfiles: self.allfiles[k].install(self.dstdir, tmplenv)
def parseTomlModule(modulefile):
"""parses a mcconf module file and returns a list(Module)."""
modules = list()
with open(modulefile, 'r') as f:
content = toml.load(f)
for name in content['module']:
fields = content['module'][name]
mod = Module(name, modulefile)
for field in fields:
if field.endswith('files'):
mod.addFiles(field.upper(), findFiles(mod.moduledir, fields[field]))
elif field == 'copy': mod.copyfiles = set(fields['copy'])
elif field == 'requires': mod.addRequires(fields['requires'])
elif field == 'provides': mod.addProvides(fields['provides'])
elif field == 'modules': mod.modules = set(fields['modules'])
elif field == 'dstdir': mod.dstdir = fields['dstdir']
elif field == 'noauto': mod.noauto = bool(fields['noauto'])
else: mod.vars[field] = fields[field]
#mod.provides.add(name) # should not require specific modules, use 'modules' instead
mod.finish()
modules.append(mod)
return modules
def loadModules(moddb, basedir, paths):
for path in paths:
path = os.path.join(basedir, path)
logging.info("searching modules in %s", path)
for f in findFiles(path, ["**/*.module", "**/mcconf.toml", "**/*.mcconf"]):
try:
for mod in parseTomlModule(os.path.join(path,f)): moddb.addModule(mod)
except:
logging.error('parsing modulefile %s failed', f)
raise
def parseTomlConfiguration(conffile):
logging.info("processing configuration %s", conffile)
with open(conffile, 'r') as fin:
configf = toml.load(fin)
configf = configf['config']
config = Configuration(conffile)
config.moduledirs.append(os.path.join(mcconf_dir, "stdmodules"))
for field in configf:
if field == 'vars': config.vars.update(configf[field])
elif field == 'moduledirs': config.moduledirs.extend(configf['moduledirs'])
elif field == 'requires': config.requires.update(configf['requires'])
elif field == 'provides': config.provides.update(configf['provides'])
elif field == 'modules': config.modules.update(configf['modules'])
elif field == 'destdir':
config.dstdir = os.path.join(os.path.dirname(conffile), configf['destdir'])
loadModules(config.modDB, os.path.dirname(conffile), config.moduledirs)
return config
# TODO rewrite as mako template and drop pydot dependency
def createModulesGraph(moddb):
graph = pydot.Dot(graph_type='digraph')
nodes = dict()
# add modules as nodes
for mod in moddb.getModules():
tt = ", ".join(mod.provides) + " "
node = pydot.Node(mod.name, tooltip=tt)
nodes[mod.name] = node
graph.add_node(node)
# add directed edges from modules to modules that satisfy at least one dependency
for src in moddb.getModules():
dstmods = moddb.getSolutionCandidates(src)
for dst in dstmods:
tt = ", ".join(dstmods[dst]) + " "
edge = pydot.Edge(src.name, dst.name, tooltip=tt)
graph.add_edge(edge)
# add special directed edges for "modules" inclusion
for src in moddb.getModules():
for dstname in src.modules:
dst = moddb[dstname]
edge = pydot.Edge(src.name, dst.name, color="green")
graph.add_edge(edge)
# add undirected edges for conflicts
for src in moddb.getModules():
conflicts = moddb.getConflictingModules(src)
for dst in conflicts:
if (dst.name < src.name):
tt = ", ".join(conflicts[dst]) + " "
edge = pydot.Edge(src.name, dst.name, color="red", dir="none", tooltip=tt)
graph.add_edge(edge)
graph.write('dependencies.dot')
# TODO rewrite as mako template and drop pydot dependency
def createConfigurationGraph(modules, selectedmods, moddb, filename):
graph = pydot.Dot(graph_name="G", graph_type='digraph')
nodes = dict()
# add modules as nodes
for mod in modules:
tt = ", ".join(mod.provides) + " "
if mod.name in selectedmods:
fc = "#BEF781"
else:
fc = "white"
if moddb.getConflictingModules(mod):
nc = "#DF0101"
else:
nc = "black"
node = pydot.Node(mod.name, tooltip=tt,
style='filled', fillcolor=fc, color=nc, fontcolor=nc)
# node = pydot.Node(mod.name)
nodes[mod.name] = node
graph.add_node(node)
# add directed edges from modules to modules that satisfy at least one dependency
for src in modules:
dstmods = moddb.getSolutionCandidates(src)
#print(str(src) + ' --> ' + str(dstmods))
# don't show modules that are not in 'modules'
for dst in dstmods:
if dst not in modules: continue
tt = ", ".join(dstmods[dst]) + " "
edge = pydot.Edge(src.name, dst.name, tooltip=tt)
# edge = pydot.Edge(src.name, dst.name)
graph.add_edge(edge)
# add special directed edges for "modules" inclusion
for src in modules:
for dstname in src.modules:
dst = moddb[dstname]
edge = pydot.Edge(src.name, dst.name, color="green")
graph.add_edge(edge)
graph.write(filename)
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('-i', "--configfile", default = 'project.config')
parser.add_argument('-d', "--destpath")
parser.add_argument("--check", action = 'store_true')
parser.add_argument('-v', "--verbose", action = 'store_true')
parser.add_argument('-g', "--modulegraph", action = 'store_true')
parser.add_argument("--nodepsolve", help = 'disables the solver', action = 'store_true')
args = parser.parse_args()
# make destination path absolute (was relative to caller's working directory)
if args.destpath is not None:
args.destpath = os.path.abspath(args.destpath)
# configure the logging
logFormatter = logging.Formatter("%(message)s")
rootLogger = logging.getLogger()
logfile = args.configfile+'.log'
if os.path.exists(logfile): os.unlink(logfile)
fileHandler = logging.FileHandler(logfile)
fileHandler.setFormatter(logFormatter)
fileHandler.setLevel(logging.DEBUG)
rootLogger.addHandler(fileHandler)
consoleHandler = logging.StreamHandler(sys.stdout)
consoleHandler.setFormatter(logFormatter)
if args.verbose:
consoleHandler.setLevel(logging.DEBUG)
else:
consoleHandler.setLevel(logging.INFO)
rootLogger.addHandler(consoleHandler)
rootLogger.setLevel(logging.DEBUG)
args.configfile = os.path.abspath(args.configfile)
config = parseTomlConfiguration(args.configfile)
config.vars["mcconf"] = os.path.abspath(sys.argv[0])
if args.destpath is not None:
config.dstdir = args.destpath
if(args.check):
config.modDB.checkConsistency()
config.checkConsistency()
else:
config.processModules(not args.nodepsolve)
config.install()
if args.modulegraph:
createModulesGraph(config.modDB)
createConfigurationGraph(config.acceptedMods, config.modules, config.modDB, config.dstdir+'/config.dot')
sys.exit(0)