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
57 apply_pattern = Keyword(
"apply_pattern") + identifier + ZeroOrMore(variable)
59 direct_patterns = reduce(operator.or_, map(Keyword, set(patterns)))
61 direct_patterns.addParseAction(
lambda toks: toks.insert(0,
'apply_pattern'))
62 apply_pattern = apply_pattern | (direct_patterns + ZeroOrMore(variable))
64 statement = (package | version | use | constituent | macro | setenv | apply_pattern)
66 return Optional(statement) + Optional(comment) + StringEnd()
78 _shelve_file = os.environ.get(
'CMT2CMAKECACHE',
79 os.path.join(os.path.dirname(__file__),
81 cache = shelve.open(_shelve_file)
84 _shelve_file = os.path.join(os.path.expanduser(
'~'),
'.cmt2cmake.cache')
86 cache = shelve.open(_shelve_file)
95 for k
in [
'ignored_packages',
'data_packages',
'needing_python',
'no_pedantic',
100 ignored_packages = config[
'ignored_packages']
101 data_packages = config[
'data_packages']
104 needing_python = config[
'needing_python']
107 no_pedantic = config[
'no_pedantic']
109 ignore_env = config[
'ignore_env']
113 Merge the content of the JSON file with the configuration dictionary.
116 if os.path.exists(config_file):
117 data = json.load(open(config_file))
121 config[k].
update(map(str, data[k]))
124 loadConfig(os.path.join(os.path.dirname(__file__),
'cmt2cmake.cfg'))
128 Mapping between the name of the LCG_Interface name and the Find*.cmake name
131 mapping = {
'Reflex':
'ROOT',
132 'Python':
'PythonLibs',
133 'neurobayes_expert':
'NeuroBayesExpert',
141 return mapping.get(n, n)
144 return os.path.isfile(os.path.join(path,
"cmt",
"requirements"))
147 return os.path.isfile(os.path.join(path,
"cmt",
"project.cmt"))
150 return {
'DAVINCI':
'DaVinci',
151 'LHCB':
'LHCb'}.
get(name.upper(), name.capitalize())
155 Produce a string for a call of a command with indented arguments.
157 >>> print callStringWithIndent('example_command', ['arg1', 'arg2', 'arg3'])
161 >>> print callStringWithIndent('example_command', ['', 'arg2', 'arg3'])
165 indent =
'\n' +
' ' * (len(cmd) + 1)
166 return cmd +
'(' + indent.join(filter(
None, arglines)) +
')'
170 Write the generated CMakeLists.txt.
172 if log
and os.path.exists(filename):
173 log.info(
'overwriting %s', filename)
174 f = open(filename,
"w")
180 self.
path = os.path.realpath(path)
182 raise ValueError(
"%s is not a package" % self.
path)
202 "install_more_includes",
"god_headers",
"god_dictionary",
203 "PyQtResource",
"PyQtUIC"])
214 self.
multi_patterns = set([
'reflex_dictionary',
'component_library',
'linker_library',
215 'copy_relax_rootmap'])
225 self.
log = logging.getLogger(
'Package(%s)' % self.
name)
242 "# Package: %s" % self.
name,
248 subdirs = [n
for n
in sorted(self.
uses)
249 if not n.startswith(
"LCG_Interfaces/")
250 and n
not in ignored_packages
251 and n
not in data_packages]
256 missing_subdirs = set([s.rsplit(
'/')[-1]
for s
in subdirs]) - set(cache)
258 self.log.warning(
'Missing info cache for subdirs %s',
' '.join(sorted(missing_subdirs)))
265 inc_only =
lambda s: cache.get(s.rsplit(
'/')[-1], {}).
get(
'includes')
266 inc_dirs = filter(inc_only, subdirs)
272 for n
in sorted(self.
uses):
273 if n.startswith(
"LCG_Interfaces/"):
276 if n ==
"PythonLibs":
277 if self.
name not in needing_python:
280 linkopts = self.macros.get(n +
'_linkopts',
'')
281 components = [m.group(1)
or m.group(2)
282 for m
in re.finditer(
r'(?:\$\(%s_linkopts_([^)]*)\))|(?:-l(\w*))' % n,
286 components = [
'CoolKernel',
'CoolApplication']
288 components = [
'CoralBase',
'CoralKernel',
'RelationalAccess']
292 find_packages[n] = find_packages.get(n, []) + components
295 for n
in sorted(find_packages):
297 components = find_packages[n]
300 args.append(
'REQUIRED')
301 args.append(
'COMPONENTS')
302 args.extend(components)
303 data.append(
'find_package(%s)' %
' '.join(args))
307 if self.
name in no_pedantic:
308 data.append(
'string(REPLACE "-pedantic" "" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}")\n')
312 headers = [d
for d
in self.install_more_includes.values()
313 if os.path.isdir(os.path.join(self.
path, d))]
318 data.append(
"include(GaudiObjDesc)")
321 god_headers_dest =
None
323 godargs = [self.
god_headers[
"files"].replace(
"../",
"")]
325 godflags = self.macros.get(
'%sObj2Doth_GODflags' % self.
name,
"")
326 godflags = re.search(
r'-s\s*(\S+)', godflags)
328 god_headers_dest = os.path.normpath(
'Event/' + godflags.group(1))
329 if god_headers_dest ==
'src':
331 godargs.append(
'PRIVATE')
333 godargs.append(
'DESTINATION ' + god_headers_dest)
340 god_dict = [(
'--GOD--',
348 v = v.replace(
"$(%sROOT)/" % self.name.upper(),
"")
349 v = v.replace(
"../",
"")
351 imports = [i.strip(
'"').replace(
'-import=',
'')
for i
in d.get(
'imports',
'').strip().split()]
352 rflx_dict.append((d[
'dictionary'] +
'Dict',
353 [d[
'headerfiles'], d[
'selectionfile']],
358 global_imports = [
extName(name[15:])
359 for name
in self.
uses
360 if name.startswith(
'LCG_Interfaces/')
and self.
uses[name][1]]
361 if 'PythonLibs' in global_imports
and self.
name not in needing_python:
362 global_imports.remove(
'PythonLibs')
364 subdir_imports = [s.rsplit(
'/')[-1]
for s
in subdirs
if self.
uses[s][1]]
366 applications_names = set([a[0]
for a
in self.
applications])
369 isGODDict = isRflxDict = isComp = isApp = isLinker =
False
370 if name ==
'--GOD--':
378 elif name
in applications_names:
382 self.log.warning(
'library %s not declared as component or linker, assume linker', name)
387 cmd =
'gaudi_add_module'
389 cmd =
'god_build_dictionary'
391 cmd =
'gaudi_add_dictionary'
393 cmd =
'gaudi_add_executable'
395 cmd =
'gaudi_add_library'
398 self.log.warning(
"Missing sources for target %s", name)
403 args.append(
'PUBLIC_HEADERS ' +
' '.join(headers))
405 args.append(
'NO_PUBLIC_HEADERS')
408 args.append(
'HEADERS_DESTINATION ' + god_headers_dest)
410 for docname, _, docsources
in self.
documents:
411 if docname ==
'customdict':
412 args.append(
'EXTEND ' + docsources[0].replace(
'../',
''))
423 subdirsnames = [s.rsplit(
'/')[-1]
for s
in subdirs]
424 subdir_local_imports = [i
for i
in imports
if i
in subdirsnames]
425 ext_local_imports = [
extName(i)
for i
in imports
if i
not in subdir_local_imports]
428 links = global_imports + ext_local_imports
429 if links
or inc_dirs:
431 args.append(
'INCLUDE_DIRS ' +
' '.join(links + inc_dirs))
434 not_included = set(links).difference(find_packages, set([s.rsplit(
'/')[-1]
for s
in subdirs]))
436 self.log.warning(
'imports without use: %s',
', '.join(sorted(not_included)))
439 for s
in subdir_imports + subdir_local_imports:
441 links.extend(cache[s][
'libraries'])
443 links.extend(local_links)
449 args.append(
'LINK_LIBRARIES ' +
' '.join([l.strip(
'"')
for l
in links]))
455 local_links.append(name)
458 if name ==
'garbage' and self.
name ==
'FileStager':
459 data.append(
'# only for the applications\nfind_package(Boost COMPONENTS program_options)\n')
462 if not (isGODDict
or isRflxDict):
464 sources = [os.path.normpath(
'src/' + s)
for s
in sources]
466 sources = [s.replace(
'src/$(GAUDICONFROOT)',
'${CMAKE_SOURCE_DIR}/GaudiConf')
for s
in sources]
470 if group
in (
'tests',
'test'):
472 libdata = [
' ' + l
for l
in libdata.splitlines()]
474 libdata.insert(0,
'if(GAUDI_BUILD_TESTS)')
475 libdata.append(
'endif()')
476 libdata =
'\n'.join(libdata)
482 data.append(
"# gen_pyqt_* functions are provided by 'pygraphics'")
484 qrc_files = self.
PyQtResource[
"qrc_files"].replace(
"../",
"")
485 qrc_dest = self.
PyQtResource[
"outputdir"].replace(
"../python/",
"")
486 qrc_target = qrc_dest.replace(
'/',
'.') +
'.Resources'
487 data.append(
'gen_pyqt_resource(%s %s %s)' % (qrc_target, qrc_dest, qrc_files))
489 ui_files = self.
PyQtUIC[
"ui_files"].replace(
"../",
"")
490 ui_dest = self.
PyQtUIC[
"outputdir"].replace(
"../python/",
"")
491 ui_target = qrc_dest.replace(
'/',
'.') +
'.UI'
492 data.append(
'gen_pyqt_uic(%s %s %s)' % (ui_target, ui_dest, ui_files))
497 data.extend([
'# Merge the RELAX rootmaps',
498 'set(rootmapfile ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/relax.rootmap)',
500 [
'OUTPUT ${rootmapfile}',
501 'COMMAND ${merge_cmd} ${RELAX_ROOTMAPS} ${rootmapfile}',
502 'DEPENDS ${RELAX_ROOTMAPS}']),
503 'add_custom_target(RelaxRootmap ALL DEPENDS ${rootmapfile})',
504 '\n# Install the merged file',
505 'install(FILES ${rootmapfile} DESTINATION lib)\n'])
510 installs.append(
"gaudi_install_headers(%s)" % (
" ".join(headers)))
514 if (self.
name +
'ConfUserModules')
in self.
macros:
515 installs.append(
'set_property(DIRECTORY PROPERTY CONFIGURABLE_USER_MODULES %s)'
517 installs.append(
"gaudi_install_python_modules()")
519 installs.append(
"gaudi_install_scripts()")
521 data.extend(installs)
527 Convert environment variable values from CMT to CMake.
530 s = re.sub(
r'(?<!\\)\$',
'\\$', s)
532 s = re.sub(
r'\$\(([^()]*)\)',
r'${\1}', s)
534 v = re.compile(
r'\$\{(\w*)_root\}')
537 s = s[:m.start()] + (
'${%sROOT}' % m.group(1).upper()) + s[m.end():]
543 [
'SET %s %s' % (v, fixSetValue(self.
sets[v]))
544 for v
in sorted(self.
sets)]))
549 data.append(
"\ngaudi_add_test(QMTest QMTEST)")
551 return "\n".join(data) +
"\n"
556 Return the list of data packages used by this package in the form of a
557 dictionary {name: version_pattern}.
559 return dict([ (n, self.
uses[n][0])
for n
in self.
uses if n
in data_packages ])
562 cml = os.path.join(self.
path,
"CMakeLists.txt")
563 if ((overwrite ==
'force')
564 or (
not os.path.exists(cml))
565 or ((overwrite ==
'update')
566 and (os.path.getmtime(cml) < os.path.getmtime(self.
requirements)))):
571 self.log.warning(
"file %s already exists", cml)
583 if statement.endswith(
'\\'):
585 statement = statement[:-1] +
' '
591 yield list(self.CMTParser.parseString(statement))
594 self.log.debug(
"Failed to parse statement: %r", statement)
602 if "-no_auto_imports" in args:
604 args.remove(
"-no_auto_imports")
609 name =
"%s/%s" % (args[2], args[0])
612 self.
uses[name] = (args[1], imp)
614 elif cmd ==
"apply_pattern":
615 pattern = args.pop(0)
616 args = dict([x.split(
'=', 1)
for x
in args])
618 setattr(self, pattern, args
or True)
620 getattr(self, pattern).append(args)
622 elif cmd ==
'library':
629 if a.startswith(
'-'):
630 if a.startswith(
'-import='):
631 imports.append(a[8:])
632 elif a.startswith(
'-group='):
638 self.libraries.append((name, sources, group, imports))
640 elif cmd ==
'application':
647 if a.startswith(
'-'):
648 if a.startswith(
'-import='):
649 imports.append(a[8:])
650 elif a.startswith(
'-group='):
658 if 'test' in name.lower()
or [s
for s
in sources
if 'test' in s.lower()]:
661 self.applications.append((name, sources, group, imports))
663 elif cmd ==
'document':
665 constituent = args.pop(0)
667 self.documents.append((name, constituent, sources))
672 value = args[0].strip(
'"').strip(
"'")
675 elif cmd ==
'macro_append':
678 value = args[0].strip(
'"').strip(
"'")
679 self.
macros[name] = self.macros.get(name,
"") + value
683 if name
not in ignore_env:
684 value = args[0].strip(
'"').strip(
"'")
685 self.
sets[name] = value
688 unquote =
lambda x: x.strip(
'"').strip(
"'")
694 toolchain_template =
'''# Special wrapper to load the declared version of the heptools toolchain.
695 set(heptools_version {0})
697 # Remove the reference to this file from the cache.
698 unset(CMAKE_TOOLCHAIN_FILE CACHE)
700 # Find the actual toolchain file.
701 find_file(CMAKE_TOOLCHAIN_FILE
702 NAMES heptools-${{heptools_version}}.cmake
703 HINTS ENV CMTPROJECTPATH
704 PATHS ${{CMAKE_CURRENT_LIST_DIR}}/cmake/toolchain
705 PATH_SUFFIXES toolchain)
707 if(NOT CMAKE_TOOLCHAIN_FILE)
708 message(FATAL_ERROR "Cannot find heptools-${{heptools_version}}.cmake.")
711 # Reset the cache variable to have proper documentation.
712 set(CMAKE_TOOLCHAIN_FILE ${{CMAKE_TOOLCHAIN_FILE}}
713 CACHE FILEPATH "The CMake toolchain file" FORCE)
715 include(${{CMAKE_TOOLCHAIN_FILE}})
721 Create a project instance from the root directory of the project.
723 self.
path = os.path.realpath(path)
725 raise ValueError(
"%s is not a project" % self.
path)
734 Dictionary of packages contained in the project.
738 for root, dirs, _files
in os.walk(self.
path):
741 name = os.path.relpath(p.path, self.
path)
749 Name of the container package of the project.
751 The name of the container is deduced using the usual LHCb convention
752 (instead of the content of project.cmt).
755 for suffix
in [
"Release",
"Sys"]:
760 if p.endswith(suffix)
and "/" not in p).next()
763 except StopIteration:
771 return self.container.name.replace(
"Release",
"").replace(
"Sys",
"")
775 return self.container.version
780 if l
and l[0] ==
"use" and l[1] !=
"LCGCMT" and len(l) == 3:
785 Return the version of heptools (LCGCMT) used by this project.
788 def updateCache(value):
790 helper function to update the cache and return the value
794 d[
'heptools'] = value
799 exp = re.compile(
r'^\s*use\s+LCGCMT\s+LCGCMT[_-](\S+)')
803 return updateCache(m.group(1))
809 if u
in cache
and 'heptools' in cache[u]:
810 return updateCache(cache[u][
'heptools'])
818 Return the list of data packages used by this project (i.e. by all the
819 packages in this project) in the form of a dictionary
820 {name: version_pattern}.
823 def appendDict(d, kv):
825 helper function to extend a dictionary of lists
835 for pkgname, pkg
in self.packages.items():
836 for dpname, dpversion
in pkg.data_packages.items():
837 appendDict(dp2pkg, (dpname, (pkgname, dpversion)))
841 for dp
in sorted(dp2pkg):
842 versions = set([v
for _, v
in dp2pkg[dp]])
844 version = sorted(versions)[-1]
847 if len(versions) != 1:
848 logging.warning(
'Different versions for data package %s, using %s from %s', dp, version, dp2pkg[dp])
855 data = [
"CMAKE_MINIMUM_REQUIRED(VERSION 2.8.5)",
857 "#---------------------------------------------------------------",
858 "# Load macros and functions for Gaudi-based projects",
859 "find_package(GaudiProject)",
860 "#---------------------------------------------------------------",
862 "# Declare project name and version"]
863 l =
"gaudi_project(%s %s" % (self.
name, self.
version)
864 use =
"\n ".join([
"%s %s" % u
for u
in self.
uses()])
869 for p, v
in sorted(self.data_packages.items()):
873 data_pkgs.append(
"%s VERSION %s" % (p, v))
876 "\n ".join(data_pkgs))
879 return "\n".join(data) +
"\n"
884 return toolchain_template.format(heptools_version)
889 def produceFile(name, generator):
890 cml = os.path.join(self.
path, name)
891 if ((overwrite ==
'force')
892 or (
not os.path.exists(cml))
893 or ((overwrite ==
'update')
894 and (os.path.getmtime(cml) < os.path.getmtime(self.
requirements)))):
900 logging.info(
"file %s not generated (empty)", cml)
902 logging.warning(
"file %s already exists", cml)
904 produceFile(
"CMakeLists.txt", self.
generate)
913 from optparse
import OptionParser
914 parser = OptionParser(usage=
"%prog [options] [path to project or package]",
915 description=
"Convert CMT-based projects/packages to CMake (Gaudi project)")
916 parser.add_option(
"-f",
"--force", action=
"store_const",
917 dest=
'overwrite', const=
'force',
918 help=
"overwrite existing files")
919 parser.add_option(
'--cache-only', action=
'store_true',
920 help=
'just update the cache without creating the CMakeLists.txt files.')
921 parser.add_option(
'-u' ,
'--update', action=
'store_const',
922 dest=
'overwrite', const=
'update',
923 help=
'modify the CMakeLists.txt files if they are older than '
924 'the corresponding requirements.')
928 opts, args = parser.parse_args(args=args)
930 logging.basicConfig(level=logging.INFO)
932 top_dir = os.getcwd()
935 if not os.path.isdir(top_dir):
936 parser.error(
"%s is not a directory" % top_dir)
938 loadConfig(os.path.join(top_dir,
'cmt2cmake.cfg'))
948 raise ValueError(
"%s is neither a project nor a package" % top_dir)
955 root.process(opts.overwrite)
958 if __name__ ==
'__main__':