3 Script to convert CMT projects/packages to CMake Gaudi-based configuration.
14 from pyparsing
import ( Word, QuotedString, Keyword, Literal, SkipTo, StringEnd,
15 ZeroOrMore, Optional, Combine,
16 alphas, alphanums, printables )
17 dblQuotedString = QuotedString(quoteChar=
'"', escChar=
'\\', unquoteResults=
False)
18 sglQuotedString = QuotedString(quoteChar=
"'", escChar=
'\\', unquoteResults=
False)
19 value = dblQuotedString | sglQuotedString | Word(printables)
21 tag_name = Word(alphas +
"_", alphanums +
"_-")
22 tag_expression = Combine(tag_name + ZeroOrMore(
'&' + tag_name))
23 values = value + ZeroOrMore(tag_expression + value)
25 identifier = Word(alphas +
"_", alphanums +
"_")
26 variable = Combine(identifier +
'=' + value)
28 constituent_option = (Keyword(
'-no_share')
29 | Keyword(
'-no_static')
30 | Keyword(
'-prototypes')
31 | Keyword(
'-no_prototypes')
33 | Keyword(
'-target_tag')
34 | Combine(
'-group=' + value)
35 | Combine(
'-suffix=' + value)
36 | Combine(
'-import=' + value)
39 | Keyword(
'-windows'))
40 source = (Word(alphanums +
"_*./$()")
41 | Combine(
'-s=' + value)
42 | Combine(
'-k=' + value)
43 | Combine(
'-x=' + value))
46 comment = (Literal(
"#") + SkipTo(StringEnd())).suppress()
48 package = Keyword(
'package') + Word(printables)
49 version = Keyword(
"version") + Word(printables)
50 use = Keyword(
"use") + identifier + Word(printables) + Optional(identifier) + Optional(Keyword(
"-no_auto_imports"))
52 constituent = ((Keyword(
'library') | Keyword(
'application') | Keyword(
'document'))
53 + identifier + ZeroOrMore(constituent_option | source))
54 macro = (Keyword(
'macro') | Keyword(
'macro_append')) + identifier + values
55 setenv = (Keyword(
'set') | Keyword(
'path_append') | Keyword(
'path_prepend')) + identifier + values
56 alias = Keyword(
'alias') + identifier + values
58 apply_pattern = Keyword(
"apply_pattern") + identifier + ZeroOrMore(variable)
60 direct_patterns = reduce(operator.or_, map(Keyword, set(patterns)))
62 direct_patterns.addParseAction(
lambda toks: toks.insert(0,
'apply_pattern'))
63 apply_pattern = apply_pattern | (direct_patterns + ZeroOrMore(variable))
65 statement = (package | version | use | constituent | macro | setenv | alias | apply_pattern)
67 return Optional(statement) + Optional(comment) + StringEnd()
79 _shelve_file = os.environ.get(
'CMT2CMAKECACHE',
80 os.path.join(os.path.dirname(__file__),
82 cache = shelve.open(_shelve_file)
85 _shelve_file = os.path.join(os.path.expanduser(
'~'),
'.cmt2cmake.cache')
87 cache = shelve.open(_shelve_file)
96 for k
in [
'ignored_packages',
'data_packages',
'needing_python',
'no_pedantic',
101 ignored_packages = config[
'ignored_packages']
102 data_packages = config[
'data_packages']
105 needing_python = config[
'needing_python']
108 no_pedantic = config[
'no_pedantic']
110 ignore_env = config[
'ignore_env']
114 Merge the content of the JSON file with the configuration dictionary.
117 if os.path.exists(config_file):
118 data = json.load(open(config_file))
122 config[k].
update(map(str, data[k]))
125 loadConfig(os.path.join(os.path.dirname(__file__),
'cmt2cmake.cfg'))
129 Mapping between the name of the LCG_Interface name and the Find*.cmake name
132 mapping = {
'Reflex':
'ROOT',
133 'Python':
'PythonLibs',
134 'neurobayes_expert':
'NeuroBayesExpert',
141 'fastjet':
'FastJet',
144 return mapping.get(n, n)
147 return os.path.isfile(os.path.join(path,
"cmt",
"requirements"))
150 return os.path.isfile(os.path.join(path,
"cmt",
"project.cmt"))
153 return {
'DAVINCI':
'DaVinci',
154 'LHCB':
'LHCb'}.
get(name.upper(), name.capitalize())
158 Produce a string for a call of a command with indented arguments.
160 >>> print callStringWithIndent('example_command', ['arg1', 'arg2', 'arg3'])
164 >>> print callStringWithIndent('example_command', ['', 'arg2', 'arg3'])
168 indent =
'\n' +
' ' * (len(cmd) + 1)
169 return cmd +
'(' + indent.join(filter(
None, arglines)) +
')'
173 Write the generated CMakeLists.txt.
175 if log
and os.path.exists(filename):
176 log.info(
'overwriting %s', filename)
177 f = open(filename,
"w")
183 self.
path = os.path.realpath(path)
185 raise ValueError(
"%s is not a package" % self.
path)
206 "install_more_includes",
"god_headers",
"god_dictionary",
207 "PyQtResource",
"PyQtUIC"])
218 self.
multi_patterns = set([
'reflex_dictionary',
'component_library',
'linker_library',
219 'copy_relax_rootmap'])
229 self.
log = logging.getLogger(
'Package(%s)' % self.
name)
246 "# Package: %s" % self.
name,
252 subdirs = [n
for n
in sorted(self.
uses)
253 if not n.startswith(
"LCG_Interfaces/")
254 and n
not in ignored_packages
255 and n
not in data_packages]
260 missing_subdirs = set([s.rsplit(
'/')[-1]
for s
in subdirs]) - set(cache)
262 self.log.warning(
'Missing info cache for subdirs %s',
' '.join(sorted(missing_subdirs)))
269 inc_only =
lambda s: cache.get(s.rsplit(
'/')[-1], {}).
get(
'includes')
270 inc_dirs = filter(inc_only, subdirs)
276 for n
in sorted(self.
uses):
277 if n.startswith(
"LCG_Interfaces/"):
280 if n ==
"PythonLibs":
281 if self.
name not in needing_python:
284 linkopts = self.macros.get(n +
'_linkopts',
'')
285 components = [m.group(1)
or m.group(2)
286 for m
in re.finditer(
r'(?:\$\(%s_linkopts_([^)]*)\))|(?:-l(\w*))' % n,
290 components = [
'CoolKernel',
'CoolApplication']
292 components = [
'CoralBase',
'CoralKernel',
'RelationalAccess']
296 find_packages[n] = find_packages.get(n, []) + components
299 for n
in sorted(find_packages):
301 components = find_packages[n]
304 args.append(
'REQUIRED')
305 args.append(
'COMPONENTS')
306 args.extend(components)
307 data.append(
'find_package(%s)' %
' '.join(args))
311 if self.
name in no_pedantic:
312 data.append(
'string(REPLACE "-pedantic" "" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}")\n')
316 headers = [d
for d
in self.install_more_includes.values()
317 if os.path.isdir(os.path.join(self.
path, d))]
322 data.append(
"include(GaudiObjDesc)")
325 god_headers_dest =
None
327 godargs = [self.
god_headers[
"files"].replace(
"../",
"")]
329 godflags = self.macros.get(
'%sObj2Doth_GODflags' % self.
name,
"")
330 godflags = re.search(
r'-s\s*(\S+)', godflags)
332 god_headers_dest = os.path.normpath(
'Event/' + godflags.group(1))
333 if god_headers_dest ==
'src':
335 godargs.append(
'PRIVATE')
337 godargs.append(
'DESTINATION ' + god_headers_dest)
344 god_dict = [(
'--GOD--',
352 v = v.replace(
"$(%sROOT)/" % self.name.upper(),
"")
353 v = v.replace(
"../",
"")
355 imports = [i.strip(
'"').replace(
'-import=',
'')
for i
in d.get(
'imports',
'').strip().split()]
356 rflx_dict.append((d[
'dictionary'] +
'Dict',
357 [d[
'headerfiles'], d[
'selectionfile']],
362 global_imports = [
extName(name[15:])
363 for name
in self.
uses
364 if name.startswith(
'LCG_Interfaces/')
and self.
uses[name][1]]
365 if 'PythonLibs' in global_imports
and self.
name not in needing_python:
366 global_imports.remove(
'PythonLibs')
368 subdir_imports = [s.rsplit(
'/')[-1]
for s
in subdirs
if self.
uses[s][1]]
370 applications_names = set([a[0]
for a
in self.
applications])
373 isGODDict = isRflxDict = isComp = isApp = isLinker =
False
374 if name ==
'--GOD--':
382 elif name
in applications_names:
386 self.log.warning(
'library %s not declared as component or linker, assume linker', name)
391 cmd =
'gaudi_add_module'
393 cmd =
'god_build_dictionary'
395 cmd =
'gaudi_add_dictionary'
397 cmd =
'gaudi_add_executable'
399 cmd =
'gaudi_add_library'
402 self.log.warning(
"Missing sources for target %s", name)
407 args.append(
'PUBLIC_HEADERS ' +
' '.join(headers))
409 args.append(
'NO_PUBLIC_HEADERS')
412 args.append(
'HEADERS_DESTINATION ' + god_headers_dest)
414 for docname, _, docsources
in self.
documents:
415 if docname ==
'customdict':
416 args.append(
'EXTEND ' + docsources[0].replace(
'../',
''))
427 subdirsnames = [s.rsplit(
'/')[-1]
for s
in subdirs]
428 subdir_local_imports = [i
for i
in imports
if i
in subdirsnames]
429 ext_local_imports = [
extName(i)
for i
in imports
if i
not in subdir_local_imports]
432 links = global_imports + ext_local_imports
433 if links
or inc_dirs:
435 args.append(
'INCLUDE_DIRS ' +
' '.join(links + inc_dirs))
438 not_included = set(links).difference(find_packages, set([s.rsplit(
'/')[-1]
for s
in subdirs]))
440 self.log.warning(
'imports without use: %s',
', '.join(sorted(not_included)))
443 for s
in subdir_imports + subdir_local_imports:
445 links.extend(cache[s][
'libraries'])
447 links.extend(local_links)
453 args.append(
'LINK_LIBRARIES ' +
' '.join([l.strip(
'"')
for l
in links]))
459 local_links.append(name)
462 if name ==
'garbage' and self.
name ==
'FileStager':
463 data.append(
'# only for the applications\nfind_package(Boost COMPONENTS program_options)\n')
466 if not (isGODDict
or isRflxDict):
468 sources = [os.path.normpath(
'src/' + s)
for s
in sources]
470 sources = [s.replace(
'src/$(GAUDICONFROOT)',
'${CMAKE_SOURCE_DIR}/GaudiConf')
for s
in sources]
474 if group
in (
'tests',
'test'):
476 libdata = [
' ' + l
for l
in libdata.splitlines()]
478 libdata.insert(0,
'if(GAUDI_BUILD_TESTS)')
479 libdata.append(
'endif()')
480 libdata =
'\n'.join(libdata)
486 data.append(
"# gen_pyqt_* functions are provided by 'pygraphics'")
488 qrc_files = self.
PyQtResource[
"qrc_files"].replace(
"../",
"")
489 qrc_dest = self.
PyQtResource[
"outputdir"].replace(
"../python/",
"")
490 qrc_target = qrc_dest.replace(
'/',
'.') +
'.Resources'
491 data.append(
'gen_pyqt_resource(%s %s %s)' % (qrc_target, qrc_dest, qrc_files))
493 ui_files = self.
PyQtUIC[
"ui_files"].replace(
"../",
"")
494 ui_dest = self.
PyQtUIC[
"outputdir"].replace(
"../python/",
"")
495 ui_target = qrc_dest.replace(
'/',
'.') +
'.UI'
496 data.append(
'gen_pyqt_uic(%s %s %s)' % (ui_target, ui_dest, ui_files))
501 data.extend([
'# Merge the RELAX rootmaps',
502 'set(rootmapfile ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/relax.rootmap)',
504 [
'OUTPUT ${rootmapfile}',
505 'COMMAND ${merge_cmd} ${RELAX_ROOTMAPS} ${rootmapfile}',
506 'DEPENDS ${RELAX_ROOTMAPS}']),
507 'add_custom_target(RelaxRootmap ALL DEPENDS ${rootmapfile})',
508 '\n# Install the merged file',
509 'install(FILES ${rootmapfile} DESTINATION lib)\n'])
514 installs.append(
"gaudi_install_headers(%s)" % (
" ".join(headers)))
518 if (self.
name +
'ConfUserModules')
in self.
macros:
519 installs.append(
'set_property(DIRECTORY PROPERTY CONFIGURABLE_USER_MODULES %s)'
521 installs.append(
"gaudi_install_python_modules()")
523 installs.append(
"gaudi_install_scripts()")
525 data.extend(installs)
529 data.extend([
'gaudi_alias({0}\n {1})'.
format(name,
' '.join(alias))
530 for name, alias
in self.aliases.iteritems()])
536 Convert environment variable values from CMT to CMake.
539 s = re.sub(
r'(?<!\\)\$',
'\\$', s)
541 s = re.sub(
r'\$\(([^()]*)\)',
r'${\1}', s)
543 v = re.compile(
r'\$\{(\w*)_root\}')
546 s = s[:m.start()] + (
'${%sROOT}' % m.group(1).upper()) + s[m.end():]
552 [
'SET %s %s' % (v, fixSetValue(self.
sets[v]))
553 for v
in sorted(self.
sets)]))
558 data.append(
"\ngaudi_add_test(QMTest QMTEST)")
560 return "\n".join(data) +
"\n"
565 Return the list of data packages used by this package in the form of a
566 dictionary {name: version_pattern}.
568 return dict([ (n, self.
uses[n][0])
for n
in self.
uses if n
in data_packages ])
571 cml = os.path.join(self.
path,
"CMakeLists.txt")
572 if ((overwrite ==
'force')
573 or (
not os.path.exists(cml))
574 or ((overwrite ==
'update')
575 and (os.path.getmtime(cml) < os.path.getmtime(self.
requirements)))):
580 self.log.warning(
"file %s already exists", cml)
592 if statement.endswith(
'\\'):
594 statement = statement[:-1] +
' '
600 yield list(self.CMTParser.parseString(statement))
603 self.log.debug(
"Failed to parse statement: %r", statement)
611 if "-no_auto_imports" in args:
613 args.remove(
"-no_auto_imports")
618 name =
"%s/%s" % (args[2], args[0])
621 self.
uses[name] = (args[1], imp)
623 elif cmd ==
"apply_pattern":
624 pattern = args.pop(0)
625 args = dict([x.split(
'=', 1)
for x
in args])
627 setattr(self, pattern, args
or True)
629 getattr(self, pattern).append(args)
631 elif cmd ==
'library':
638 if a.startswith(
'-'):
639 if a.startswith(
'-import='):
640 imports.append(a[8:])
641 elif a.startswith(
'-group='):
647 self.libraries.append((name, sources, group, imports))
649 elif cmd ==
'application':
656 if a.startswith(
'-'):
657 if a.startswith(
'-import='):
658 imports.append(a[8:])
659 elif a.startswith(
'-group='):
667 if 'test' in name.lower()
or [s
for s
in sources
if 'test' in s.lower()]:
670 self.applications.append((name, sources, group, imports))
672 elif cmd ==
'document':
674 constituent = args.pop(0)
676 self.documents.append((name, constituent, sources))
681 value = args[0].strip(
'"').strip(
"'")
684 elif cmd ==
'macro_append':
687 value = args[0].strip(
'"').strip(
"'")
688 self.
macros[name] = self.macros.get(name,
"") + value
692 if name
not in ignore_env:
693 value = args[0].strip(
'"').strip(
"'")
694 self.
sets[name] = value
699 value = args[0].strip(
'"').strip(
"'").split()
703 unquote =
lambda x: x.strip(
'"').strip(
"'")
709 toolchain_template =
'''# Special wrapper to load the declared version of the heptools toolchain.
710 set(heptools_version {0})
712 # Remove the reference to this file from the cache.
713 unset(CMAKE_TOOLCHAIN_FILE CACHE)
715 # Find the actual toolchain file.
716 find_file(CMAKE_TOOLCHAIN_FILE
717 NAMES heptools-${{heptools_version}}.cmake
718 HINTS ENV CMTPROJECTPATH
719 PATHS ${{CMAKE_CURRENT_LIST_DIR}}/cmake/toolchain
720 PATH_SUFFIXES toolchain)
722 if(NOT CMAKE_TOOLCHAIN_FILE)
723 message(FATAL_ERROR "Cannot find heptools-${{heptools_version}}.cmake.")
726 # Reset the cache variable to have proper documentation.
727 set(CMAKE_TOOLCHAIN_FILE ${{CMAKE_TOOLCHAIN_FILE}}
728 CACHE FILEPATH "The CMake toolchain file" FORCE)
730 include(${{CMAKE_TOOLCHAIN_FILE}})
736 Create a project instance from the root directory of the project.
738 self.
path = os.path.realpath(path)
740 raise ValueError(
"%s is not a project" % self.
path)
749 Dictionary of packages contained in the project.
753 for root, dirs, _files
in os.walk(self.
path):
756 name = os.path.relpath(p.path, self.
path)
764 Name of the container package of the project.
766 The name of the container is deduced using the usual LHCb convention
767 (instead of the content of project.cmt).
770 for suffix
in [
"Release",
"Sys"]:
775 if p.endswith(suffix)
and "/" not in p).next()
778 except StopIteration:
786 return self.container.name.replace(
"Release",
"").replace(
"Sys",
"")
790 return self.container.version
795 if l
and l[0] ==
"use" and l[1] !=
"LCGCMT" and len(l) == 3:
800 Return the version of heptools (LCGCMT) used by this project.
803 def updateCache(value):
805 helper function to update the cache and return the value
809 d[
'heptools'] = value
814 exp = re.compile(
r'^\s*use\s+LCGCMT\s+LCGCMT[_-](\S+)')
818 return updateCache(m.group(1))
824 if u
in cache
and 'heptools' in cache[u]:
825 return updateCache(cache[u][
'heptools'])
833 Return the list of data packages used by this project (i.e. by all the
834 packages in this project) in the form of a dictionary
835 {name: version_pattern}.
838 def appendDict(d, kv):
840 helper function to extend a dictionary of lists
850 for pkgname, pkg
in self.packages.items():
851 for dpname, dpversion
in pkg.data_packages.items():
852 appendDict(dp2pkg, (dpname, (pkgname, dpversion)))
856 for dp
in sorted(dp2pkg):
857 versions = set([v
for _, v
in dp2pkg[dp]])
859 version = sorted(versions)[-1]
862 if len(versions) != 1:
863 logging.warning(
'Different versions for data package %s, using %s from %s', dp, version, dp2pkg[dp])
870 data = [
"CMAKE_MINIMUM_REQUIRED(VERSION 2.8.5)",
872 "#---------------------------------------------------------------",
873 "# Load macros and functions for Gaudi-based projects",
874 "find_package(GaudiProject)",
875 "#---------------------------------------------------------------",
877 "# Declare project name and version"]
878 l =
"gaudi_project(%s %s" % (self.
name, self.
version)
879 use =
"\n ".join([
"%s %s" % u
for u
in self.
uses()])
884 for p, v
in sorted(self.data_packages.items()):
888 data_pkgs.append(
"%s VERSION %s" % (p, v))
891 "\n ".join(data_pkgs))
894 return "\n".join(data) +
"\n"
899 return toolchain_template.format(heptools_version)
904 def produceFile(name, generator):
905 cml = os.path.join(self.
path, name)
906 if ((overwrite ==
'force')
907 or (
not os.path.exists(cml))
908 or ((overwrite ==
'update')
909 and (os.path.getmtime(cml) < os.path.getmtime(self.
requirements)))):
915 logging.info(
"file %s not generated (empty)", cml)
917 logging.warning(
"file %s already exists", cml)
919 produceFile(
"CMakeLists.txt", self.
generate)
928 from optparse
import OptionParser
929 parser = OptionParser(usage=
"%prog [options] [path to project or package]",
930 description=
"Convert CMT-based projects/packages to CMake (Gaudi project)")
931 parser.add_option(
"-f",
"--force", action=
"store_const",
932 dest=
'overwrite', const=
'force',
933 help=
"overwrite existing files")
934 parser.add_option(
'--cache-only', action=
'store_true',
935 help=
'just update the cache without creating the CMakeLists.txt files.')
936 parser.add_option(
'-u' ,
'--update', action=
'store_const',
937 dest=
'overwrite', const=
'update',
938 help=
'modify the CMakeLists.txt files if they are older than '
939 'the corresponding requirements.')
943 opts, args = parser.parse_args(args=args)
945 logging.basicConfig(level=logging.INFO)
947 top_dir = os.getcwd()
950 if not os.path.isdir(top_dir):
951 parser.error(
"%s is not a directory" % top_dir)
953 loadConfig(os.path.join(top_dir,
'cmt2cmake.cfg'))
963 raise ValueError(
"%s is neither a project nor a package" % top_dir)
970 root.process(opts.overwrite)
973 if __name__ ==
'__main__':