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))
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',
145 return mapping.get(n, n)
148 return os.path.isfile(os.path.join(path,
"cmt",
"requirements"))
151 return os.path.isfile(os.path.join(path,
"cmt",
"project.cmt"))
154 return {
'DAVINCI':
'DaVinci',
155 'LHCB':
'LHCb'}.
get(name.upper(), name.capitalize())
159 Produce a string for a call of a command with indented arguments.
161 >>> print callStringWithIndent('example_command', ['arg1', 'arg2', 'arg3'])
165 >>> print callStringWithIndent('example_command', ['', 'arg2', 'arg3'])
169 indent =
'\n' +
' ' * (len(cmd) + 1)
170 return cmd +
'(' + indent.join(filter(
None, arglines)) +
')'
174 Write the generated CMakeLists.txt.
176 if log
and os.path.exists(filename):
177 log.info(
'overwriting %s', filename)
178 f = open(filename,
"w")
184 self.
path = os.path.realpath(path)
186 raise ValueError(
"%s is not a package" % self.
path)
207 "install_more_includes",
"god_headers",
"god_dictionary",
208 "PyQtResource",
"PyQtUIC"])
219 self.
multi_patterns = set([
'reflex_dictionary',
'component_library',
'linker_library',
220 'copy_relax_rootmap'])
230 self.
log = logging.getLogger(
'Package(%s)' % self.
name)
247 "# Package: %s" % self.
name,
253 subdirs = [n
for n
in sorted(self.
uses)
254 if not n.startswith(
"LCG_Interfaces/")
255 and n
not in ignored_packages
256 and n
not in data_packages]
261 missing_subdirs = set([s.rsplit(
'/')[-1]
for s
in subdirs]) - set(cache)
263 self.log.warning(
'Missing info cache for subdirs %s',
' '.join(sorted(missing_subdirs)))
270 inc_only =
lambda s: cache.get(s.rsplit(
'/')[-1], {}).
get(
'includes')
271 inc_dirs = filter(inc_only, subdirs)
277 for n
in sorted(self.
uses):
278 if n.startswith(
"LCG_Interfaces/"):
281 if n ==
"PythonLibs":
282 if self.
name not in needing_python:
285 linkopts = self.macros.get(n +
'_linkopts',
'')
286 components = [m.group(1)
or m.group(2)
287 for m
in re.finditer(
r'(?:\$\(%s_linkopts_([^)]*)\))|(?:-l(\w*))' % n,
291 components = [
'CoolKernel',
'CoolApplication']
293 components = [
'CoralBase',
'CoralKernel',
'RelationalAccess']
297 find_packages[n] = find_packages.get(n, []) + components
300 for n
in sorted(find_packages):
302 components = find_packages[n]
305 args.append(
'REQUIRED')
306 args.append(
'COMPONENTS')
307 args.extend(components)
308 data.append(
'find_package(%s)' %
' '.join(args))
312 if self.
name in no_pedantic:
313 data.append(
'string(REPLACE "-pedantic" "" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}")\n')
317 headers = [d
for d
in self.install_more_includes.values()
318 if os.path.isdir(os.path.join(self.
path, d))]
323 data.append(
"include(GaudiObjDesc)")
326 god_headers_dest =
None
328 godargs = [self.
god_headers[
"files"].replace(
"../",
"")]
330 godflags = self.macros.get(
'%sObj2Doth_GODflags' % self.
name,
"")
331 godflags = re.search(
r'-s\s*(\S+)', godflags)
333 god_headers_dest = os.path.normpath(
'Event/' + godflags.group(1))
334 if god_headers_dest ==
'src':
336 godargs.append(
'PRIVATE')
338 godargs.append(
'DESTINATION ' + god_headers_dest)
345 god_dict = [(
'--GOD--',
353 v = v.replace(
"$(%sROOT)/" % self.name.upper(),
"")
354 v = v.replace(
"../",
"")
356 imports = [i.strip(
'"').replace(
'-import=',
'')
for i
in d.get(
'imports',
'').strip().split()]
357 rflx_dict.append((d[
'dictionary'] +
'Dict',
358 [d[
'headerfiles'], d[
'selectionfile']],
363 global_imports = [
extName(name[15:])
364 for name
in self.
uses
365 if name.startswith(
'LCG_Interfaces/')
and self.
uses[name][1]]
366 if 'PythonLibs' in global_imports
and self.
name not in needing_python:
367 global_imports.remove(
'PythonLibs')
369 subdir_imports = [s.rsplit(
'/')[-1]
for s
in subdirs
if self.
uses[s][1]]
371 applications_names = set([a[0]
for a
in self.
applications])
374 isGODDict = isRflxDict = isComp = isApp = isLinker =
False
375 if name ==
'--GOD--':
383 elif name
in applications_names:
387 self.log.warning(
'library %s not declared as component or linker, assume linker', name)
392 cmd =
'gaudi_add_module'
394 cmd =
'god_build_dictionary'
396 cmd =
'gaudi_add_dictionary'
398 cmd =
'gaudi_add_executable'
400 cmd =
'gaudi_add_library'
403 self.log.warning(
"Missing sources for target %s", name)
408 args.append(
'PUBLIC_HEADERS ' +
' '.join(headers))
410 args.append(
'NO_PUBLIC_HEADERS')
413 args.append(
'HEADERS_DESTINATION ' + god_headers_dest)
415 for docname, _, docsources
in self.
documents:
416 if docname ==
'customdict':
417 args.append(
'EXTEND ' + docsources[0].replace(
'../',
''))
428 subdirsnames = [s.rsplit(
'/')[-1]
for s
in subdirs]
429 subdir_local_imports = [i
for i
in imports
if i
in subdirsnames]
430 ext_local_imports = [
extName(i)
for i
in imports
if i
not in subdir_local_imports]
433 links = global_imports + ext_local_imports
434 if links
or inc_dirs:
436 args.append(
'INCLUDE_DIRS ' +
' '.join(links + inc_dirs))
439 not_included = set(links).difference(find_packages, set([s.rsplit(
'/')[-1]
for s
in subdirs]))
441 self.log.warning(
'imports without use: %s',
', '.join(sorted(not_included)))
444 for s
in subdir_imports + subdir_local_imports:
446 links.extend(cache[s][
'libraries'])
448 links.extend(local_links)
454 args.append(
'LINK_LIBRARIES ' +
' '.join([l.strip(
'"')
for l
in links]))
460 local_links.append(name)
463 if name ==
'garbage' and self.
name ==
'FileStager':
464 data.append(
'# only for the applications\nfind_package(Boost COMPONENTS program_options)\n')
467 if not (isGODDict
or isRflxDict):
469 sources = [os.path.normpath(
'src/' + s)
for s
in sources]
471 sources = [s.replace(
'src/$(GAUDICONFROOT)',
'${CMAKE_SOURCE_DIR}/GaudiConf')
for s
in sources]
475 if group
in (
'tests',
'test'):
477 libdata = [
' ' + l
for l
in libdata.splitlines()]
479 libdata.insert(0,
'if(GAUDI_BUILD_TESTS)')
480 libdata.append(
'endif()')
481 libdata =
'\n'.join(libdata)
487 data.append(
"# gen_pyqt_* functions are provided by 'pygraphics'")
489 qrc_files = self.
PyQtResource[
"qrc_files"].replace(
"../",
"")
490 qrc_dest = self.
PyQtResource[
"outputdir"].replace(
"../python/",
"")
491 qrc_target = qrc_dest.replace(
'/',
'.') +
'.Resources'
492 data.append(
'gen_pyqt_resource(%s %s %s)' % (qrc_target, qrc_dest, qrc_files))
494 ui_files = self.
PyQtUIC[
"ui_files"].replace(
"../",
"")
495 ui_dest = self.
PyQtUIC[
"outputdir"].replace(
"../python/",
"")
496 ui_target = qrc_dest.replace(
'/',
'.') +
'.UI'
497 data.append(
'gen_pyqt_uic(%s %s %s)' % (ui_target, ui_dest, ui_files))
502 data.extend([
'# Merge the RELAX rootmaps',
503 'set(rootmapfile ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/relax.rootmap)',
505 [
'OUTPUT ${rootmapfile}',
506 'COMMAND ${merge_cmd} ${RELAX_ROOTMAPS} ${rootmapfile}',
507 'DEPENDS ${RELAX_ROOTMAPS}']),
508 'add_custom_target(RelaxRootmap ALL DEPENDS ${rootmapfile})',
509 '\n# Install the merged file',
510 'install(FILES ${rootmapfile} DESTINATION lib)\n'])
515 installs.append(
"gaudi_install_headers(%s)" % (
" ".join(headers)))
519 if (self.
name +
'ConfUserModules')
in self.
macros:
520 installs.append(
'set_property(DIRECTORY PROPERTY CONFIGURABLE_USER_MODULES %s)'
522 installs.append(
"gaudi_install_python_modules()")
524 installs.append(
"gaudi_install_scripts()")
526 data.extend(installs)
530 data.extend([
'gaudi_alias({0}\n {1})'.
format(name,
' '.join(alias))
531 for name, alias
in self.aliases.iteritems()])
537 Convert environment variable values from CMT to CMake.
540 s = re.sub(
r'(?<!\\)\$',
'\\$', s)
542 s = re.sub(
r'\$\(([^()]*)\)',
r'${\1}', s)
544 v = re.compile(
r'\$\{(\w*)_root\}')
547 s = s[:m.start()] + (
'${%sROOT}' % m.group(1).upper()) + s[m.end():]
553 [
'SET %s %s' % (v, fixSetValue(self.
sets[v]))
554 for v
in sorted(self.
sets)]))
559 data.append(
"\ngaudi_add_test(QMTest QMTEST)")
561 return "\n".join(data) +
"\n"
566 Return the list of data packages used by this package in the form of a
567 dictionary {name: version_pattern}.
569 return dict([ (n, self.
uses[n][0])
for n
in self.
uses if n
in data_packages ])
572 cml = os.path.join(self.
path,
"CMakeLists.txt")
573 if ((overwrite ==
'force')
574 or (
not os.path.exists(cml))
575 or ((overwrite ==
'update')
576 and (os.path.getmtime(cml) < os.path.getmtime(self.
requirements)))):
581 self.log.warning(
"file %s already exists", cml)
593 if statement.endswith(
'\\'):
595 statement = statement[:-1] +
' '
601 yield list(self.CMTParser.parseString(statement))
604 self.log.debug(
"Failed to parse statement: %r", statement)
612 if "-no_auto_imports" in args:
614 args.remove(
"-no_auto_imports")
619 name =
"%s/%s" % (args[2], args[0])
622 self.
uses[name] = (args[1], imp)
624 elif cmd ==
"apply_pattern":
625 pattern = args.pop(0)
626 args = dict([x.split(
'=', 1)
for x
in args])
628 setattr(self, pattern, args
or True)
630 getattr(self, pattern).append(args)
632 elif cmd ==
'library':
639 if a.startswith(
'-'):
640 if a.startswith(
'-import='):
641 imports.append(a[8:])
642 elif a.startswith(
'-group='):
648 self.libraries.append((name, sources, group, imports))
650 elif cmd ==
'application':
657 if a.startswith(
'-'):
658 if a.startswith(
'-import='):
659 imports.append(a[8:])
660 elif a.startswith(
'-group='):
668 if 'test' in name.lower()
or [s
for s
in sources
if 'test' in s.lower()]:
671 self.applications.append((name, sources, group, imports))
673 elif cmd ==
'document':
675 constituent = args.pop(0)
677 self.documents.append((name, constituent, sources))
682 value = args[0].strip(
'"').strip(
"'")
685 elif cmd ==
'macro_append':
688 value = args[0].strip(
'"').strip(
"'")
689 self.
macros[name] = self.macros.get(name,
"") + value
693 if name
not in ignore_env:
694 value = args[0].strip(
'"').strip(
"'")
695 self.
sets[name] = value
700 value = args[0].strip(
'"').strip(
"'").split()
704 unquote =
lambda x: x.strip(
'"').strip(
"'")
710 toolchain_template =
'''# Special wrapper to load the declared version of the heptools toolchain.
711 set(heptools_version {0})
713 # Remove the reference to this file from the cache.
714 unset(CMAKE_TOOLCHAIN_FILE CACHE)
716 # Find the actual toolchain file.
717 find_file(CMAKE_TOOLCHAIN_FILE
718 NAMES heptools-${{heptools_version}}.cmake
719 HINTS ENV CMTPROJECTPATH
720 PATHS ${{CMAKE_CURRENT_LIST_DIR}}/cmake/toolchain
721 PATH_SUFFIXES toolchain)
723 if(NOT CMAKE_TOOLCHAIN_FILE)
724 message(FATAL_ERROR "Cannot find heptools-${{heptools_version}}.cmake.")
727 # Reset the cache variable to have proper documentation.
728 set(CMAKE_TOOLCHAIN_FILE ${{CMAKE_TOOLCHAIN_FILE}}
729 CACHE FILEPATH "The CMake toolchain file" FORCE)
731 include(${{CMAKE_TOOLCHAIN_FILE}})
737 Create a project instance from the root directory of the project.
739 self.
path = os.path.realpath(path)
741 raise ValueError(
"%s is not a project" % self.
path)
750 Dictionary of packages contained in the project.
754 for root, dirs, _files
in os.walk(self.
path):
757 name = os.path.relpath(p.path, self.
path)
765 Name of the container package of the project.
767 The name of the container is deduced using the usual LHCb convention
768 (instead of the content of project.cmt).
771 for suffix
in [
"Release",
"Sys"]:
776 if p.endswith(suffix)
and "/" not in p).next()
779 except StopIteration:
787 return self.container.name.replace(
"Release",
"").replace(
"Sys",
"")
791 return self.container.version
796 if l
and l[0] ==
"use" and l[1] !=
"LCGCMT" and len(l) == 3:
801 Return the version of heptools (LCGCMT) used by this project.
804 def updateCache(value):
806 helper function to update the cache and return the value
810 d[
'heptools'] = value
815 exp = re.compile(
r'^\s*use\s+LCGCMT\s+LCGCMT[_-](\S+)')
819 return updateCache(m.group(1))
825 if u
in cache
and 'heptools' in cache[u]:
826 return updateCache(cache[u][
'heptools'])
834 Return the list of data packages used by this project (i.e. by all the
835 packages in this project) in the form of a dictionary
836 {name: version_pattern}.
839 def appendDict(d, kv):
841 helper function to extend a dictionary of lists
851 for pkgname, pkg
in self.packages.items():
852 for dpname, dpversion
in pkg.data_packages.items():
853 appendDict(dp2pkg, (dpname, (pkgname, dpversion)))
857 for dp
in sorted(dp2pkg):
858 versions = set([v
for _, v
in dp2pkg[dp]])
860 version = sorted(versions)[-1]
863 if len(versions) != 1:
864 logging.warning(
'Different versions for data package %s, using %s from %s', dp, version, dp2pkg[dp])
871 data = [
"CMAKE_MINIMUM_REQUIRED(VERSION 2.8.5)",
873 "#---------------------------------------------------------------",
874 "# Load macros and functions for Gaudi-based projects",
875 "find_package(GaudiProject)",
876 "#---------------------------------------------------------------",
878 "# Declare project name and version"]
879 l =
"gaudi_project(%s %s" % (self.
name, self.
version)
880 use =
"\n ".join([
"%s %s" % u
for u
in self.
uses()])
885 for p, v
in sorted(self.data_packages.items()):
889 data_pkgs.append(
"%s VERSION %s" % (p, v))
892 "\n ".join(data_pkgs))
895 return "\n".join(data) +
"\n"
900 return toolchain_template.format(heptools_version)
905 def produceFile(name, generator):
906 cml = os.path.join(self.
path, name)
907 if ((overwrite ==
'force')
908 or (
not os.path.exists(cml))
909 or ((overwrite ==
'update')
910 and (os.path.getmtime(cml) < os.path.getmtime(self.
requirements)))):
916 logging.info(
"file %s not generated (empty)", cml)
918 logging.warning(
"file %s already exists", cml)
920 produceFile(
"CMakeLists.txt", self.
generate)
929 from optparse
import OptionParser
930 parser = OptionParser(usage=
"%prog [options] [path to project or package]",
931 description=
"Convert CMT-based projects/packages to CMake (Gaudi project)")
932 parser.add_option(
"-f",
"--force", action=
"store_const",
933 dest=
'overwrite', const=
'force',
934 help=
"overwrite existing files")
935 parser.add_option(
'--cache-only', action=
'store_true',
936 help=
'just update the cache without creating the CMakeLists.txt files.')
937 parser.add_option(
'-u' ,
'--update', action=
'store_const',
938 dest=
'overwrite', const=
'update',
939 help=
'modify the CMakeLists.txt files if they are older than '
940 'the corresponding requirements.')
944 opts, args = parser.parse_args(args=args)
946 logging.basicConfig(level=logging.INFO)
948 top_dir = os.getcwd()
951 if not os.path.isdir(top_dir):
952 parser.error(
"%s is not a directory" % top_dir)
954 loadConfig(os.path.join(top_dir,
'cmt2cmake.cfg'))
964 raise ValueError(
"%s is neither a project nor a package" % top_dir)
971 root.process(opts.overwrite)
974 if __name__ ==
'__main__':