Chromium Code Reviews| Index: pylib/gyp/generator/cmake.py |
| =================================================================== |
| --- pylib/gyp/generator/cmake.py (revision 0) |
| +++ pylib/gyp/generator/cmake.py (working copy) |
| @@ -0,0 +1,1144 @@ |
| +# Copyright (c) 2012 Google Inc. All rights reserved. |
|
Nico
2013/11/19 16:35:28
2013
bungeman-chromium
2013/11/20 20:54:51
Done. Sigh... it has been a long time since I star
|
| +# Use of this source code is governed by a BSD-style license that can be |
| +# found in the LICENSE file. |
| + |
| +"""cmake output module |
| + |
| +This module is under development and should be considered experimental. |
| + |
| +This module produces cmake (2.8.8+) input as its output. One CMakeLists.txt is |
| +created for each configuration. |
| + |
| +This module's original purpose was to support editing in IDEs like KDevelop |
| +which use CMake for project management. It is also possible to use CMake to |
| +generate projects for other IDEs such as eclipse cdt and code::blocks. QtCreator |
| +will convert the CMakeLists.txt to a code::blocks cbp for the editor to read, |
| +but build using CMake. As a result QtCreator editor is unaware of compiler |
| +defines. The generated CMakeLists.txt can also be used to build on Linux. There |
| +is currently no support for building on platforms other than Linux. |
| + |
| +The generated CMakeLists.txt should properly compile all projects. However, |
| +there is a mismatch between gyp and cmake with regard to linking. All attempts |
| +are made to work around this, but CMake sometimes sees -Wl,--start-group as a |
| +library and incorrectly repeats it. As a result the output of this generator |
| +should not be relied on for building. |
| + |
| +When using with kdevelop, use version 4.4+. Previous versions of kdevelop will |
| +not be able to find the header file directories described in the generated |
| +CMakeLists.txt file. |
| +""" |
| + |
| +import multiprocessing |
| +import os |
| +import signal |
| +import string |
| +import subprocess |
| +import gyp.common |
| + |
| +generator_default_variables = { |
| + 'EXECUTABLE_PREFIX': '', |
| + 'EXECUTABLE_SUFFIX': '', |
| + 'STATIC_LIB_PREFIX': 'lib', |
| + 'STATIC_LIB_SUFFIX': '.a', |
| + 'SHARED_LIB_PREFIX': 'lib', |
| + 'SHARED_LIB_SUFFIX': '.so', |
| + 'SHARED_LIB_DIR': '${builddir}/lib.${TOOLSET}', |
| + 'LIB_DIR': '${obj}.${TOOLSET}', |
| + 'INTERMEDIATE_DIR': '${obj}.${TOOLSET}/${TARGET}/geni', |
| + 'SHARED_INTERMEDIATE_DIR': '${obj}/gen', |
| + 'PRODUCT_DIR': '${builddir}', |
| + 'RULE_INPUT_PATH': '${RULE_INPUT_PATH}', |
| + 'RULE_INPUT_DIRNAME': '${RULE_INPUT_DIRNAME}', |
| + 'RULE_INPUT_NAME': '${RULE_INPUT_NAME}', |
| + 'RULE_INPUT_ROOT': '${RULE_INPUT_ROOT}', |
| + 'RULE_INPUT_EXT': '${RULE_INPUT_EXT}', |
| + 'CONFIGURATION_NAME': '${configuration}', |
| +} |
| + |
| +FULL_PATH_VARS = ('${CMAKE_SOURCE_DIR}', '${builddir}', '${obj}') |
| + |
| +generator_supports_multiple_toolsets = True |
| +generator_wants_static_library_dependencies_adjusted = True |
| + |
| +COMPILABLE_EXTENSIONS = { |
| + '.c': 'cc', |
| + '.cc': 'cxx', |
| + '.cpp': 'cxx', |
| + '.cxx': 'cxx', |
| + '.s': 's', #cc |
| + '.S': 's', #cc |
| +} |
| + |
| +def RemovePrefix(a, prefix): |
| + """Returns 'a' without 'prefix' if it starts with 'prefix'.""" |
| + return a[len(prefix):] if a.startswith(prefix) else a |
|
Nico
2013/11/19 16:35:28
(nit: 2 newlines between toplevel things)
bungeman-chromium
2013/11/20 20:54:51
Done.
|
| + |
| +def CalculateVariables(default_variables, params): |
| + """Calculate additional variables for use in the build (called by gyp).""" |
| + default_variables.setdefault('OS', gyp.common.GetFlavor(params)) |
| + |
| + |
| +def Compilable(filename): |
| + """Return true if the file is compilable (should be in OBJS).""" |
| + return any(filename.endswith(e) for e in COMPILABLE_EXTENSIONS) |
| + |
| + |
| +def Linkable(filename): |
| + """Return true if the file is linkable (should be on the link line).""" |
| + return filename.endswith('.o') |
| + |
| + |
| +def NormjoinPathForceCMakeSource(base_path, rel_path): |
| + """Resolves rel_path against base_path and returns the result. |
| + |
| + If rel_path is an absolute path it is returned unchanged. |
| + Otherwise it is resolved against base_path and normalized. |
| + If the result is a relative path, it is forced to be relative to the |
| + CMakeLists.txt. |
| + """ |
| + if os.path.isabs(rel_path): |
| + return rel_path |
| + if any([rel_path.startswith(var) for var in FULL_PATH_VARS]): |
| + return rel_path |
| + #TODO: do we need to check base_path for absolute variables as well? |
|
Nico
2013/11/19 16:35:28
space after #
bungeman-chromium
2013/11/20 20:54:51
Done. Here and many other places.
|
| + return os.path.join('${CMAKE_SOURCE_DIR}', |
| + os.path.normpath(os.path.join(base_path, rel_path))) |
| + |
| + |
| +def NormjoinPath(base_path, rel_path): |
| + """Resolves rel_path against base_path and returns the result. |
| + TODO: what is this really used for? |
| + If rel_path begins with '$' it is returned unchanged. |
| + Otherwise it is resolved against base_path if relative, then normalized. |
| + """ |
| + if rel_path.startswith('$') and not rel_path.startswith('${configuration}'): |
| + return rel_path |
| + return os.path.normpath(os.path.join(base_path, rel_path)) |
| + |
| + |
| +def ensure_directory_exists(path): |
|
Nico
2013/11/19 16:35:28
EnsureDirectoryExists? Or rename above methods to
bungeman-chromium
2013/11/20 20:54:51
Done. Yes, why are these different? It must be wha
|
| + """Python version of 'mkdir -p'.""" |
| + dirPath = os.path.dirname(path) |
| + if dirPath and not os.path.exists(dirPath): |
| + os.makedirs(dirPath) |
| + |
| + |
| +def cmake_string_escape(a): |
| + """Escapes the string 'a' for use inside a CMake string. |
| + |
| + This means escaping |
| + '\' otherwise it may be seen as modifying the next character |
| + '"' otherwise it will end the string |
| + ';' otherwise the string becomes a list |
| + |
| + The following do not need to be escaped |
| + '#' when the lexer is in string state, this does not start a comment |
| + |
| + The following are yet unknown |
| + '$' generator variables (like ${obj}) must not be escaped, |
| + but text $ should be escaped |
| + what is wanted is to know which $ come from generator variables |
| + """ |
| + return a.replace('\\', '\\\\').replace(';', '\\;').replace('"', '\\"') |
| + |
| + |
| +def set_file_property(output, source_name, property_name, values, sep): |
| + """Given a set of source file, sets the given property on them.""" |
| + output.write('set_source_files_properties(') |
| + output.write(source_name) |
| + output.write(' PROPERTIES ') |
| + output.write(property_name) |
| + output.write(' "') |
| + for value in values: |
| + output.write(cmake_string_escape(value)) |
| + output.write(sep) |
| + output.write('")\n') |
| + |
| + |
| +def set_files_property(output, source_names, property_name, values, sep): |
| + """Given a set of source files, sets the given property on them.""" |
| + output.write('set_source_files_properties(\n') |
| + for source_name in source_names: |
| + output.write(' ') |
| + output.write(source_name) |
| + output.write('\n') |
| + output.write(' PROPERTIES\n ') |
| + output.write(property_name) |
| + output.write(' "') |
| + for value in values: |
| + output.write(cmake_string_escape(value)) |
| + output.write(sep) |
| + output.write('"\n)\n') |
| + |
| + |
| +def set_target_property(output, target_name, property_name, values, sep=''): |
| + """Given a target, sets the given property.""" |
| + output.write('set_target_properties(') |
| + output.write(target_name) |
| + output.write(' PROPERTIES ') |
| + output.write(property_name) |
| + output.write(' "') |
| + for value in values: |
| + output.write(cmake_string_escape(value)) |
| + output.write(sep) |
| + output.write('")\n') |
| + |
| + |
| +def set_variable(output, variable_name, value): |
| + """Sets a CMake variable.""" |
| + output.write('set(') |
| + output.write(variable_name) |
| + output.write(' "') |
| + output.write(cmake_string_escape(value)) |
| + output.write('")\n') |
| + |
| + |
| +def set_variable_list(output, variable_name, values): |
| + """Sets a CMake variable to a list.""" |
| + if not values: |
| + return set_variable(output, variable_name, "") |
| + if len(values) == 1: |
| + return set_variable(output, variable_name, values[0]) |
| + output.write('list(APPEND ') |
| + output.write(variable_name) |
| + output.write('\n "') |
| + output.write('"\n "'.join([cmake_string_escape(value) for value in values])) |
| + output.write('")\n') |
| + |
| + |
| +def unset_variable(output, variable_name): |
| + """Unsets a CMake variable.""" |
| + output.write('unset(') |
| + output.write(variable_name) |
| + output.write(')\n') |
| + |
| + |
| +def WriteVariable(output, variable_name, prepend=None): |
| + if prepend: |
| + output.write(prepend) |
| + output.write('${') |
| + output.write(variable_name) |
| + output.write('}') |
| + |
| + |
| +class CMakeTargetType: |
| + def __init__(self, command, modifier, property_modifier): |
| + self.command = command |
| + self.modifier = modifier |
| + self.property_modifier = property_modifier |
| + |
| + |
| +cmake_target_type_from_gyp_target_type = { |
| + 'executable': CMakeTargetType('add_executable', None, 'RUNTIME'), |
| + 'static_library': CMakeTargetType('add_library', 'STATIC', 'ARCHIVE'), |
| + 'shared_library': CMakeTargetType('add_library', 'SHARED', 'LIBRARY'), |
| + 'loadable_module': CMakeTargetType('add_library', 'MODULE', 'LIBRARY'), |
| + 'none': CMakeTargetType('add_custom_target', 'SOURCES', None), |
| +} |
| + |
| + |
| +def to_cmake_target_name(a): |
| + """Converts the given string 'a' to a valid CMake target name. |
| + |
| + All invalid characters are replaced by '_'. |
| + Invalid for cmake: ' ', '/', '(', ')' |
| + Invalid for make: ':' |
| + Invalid for unknown reasons but cause failures: '.' |
| + """ |
| + return a.translate(string.maketrans(' /():.', '______')) |
| + |
| + |
| +def WriteActions(target_name, actions, extra_sources, extra_deps, |
| + path_to_gyp, output): |
| + """Write CMake for the 'actions' in the target. |
| + |
| + Args: |
| + target_name: the name of the CMake target being generated. |
| + actions: the Gyp 'actions' dict for this target. |
| + extra_sources: [(<cmake_src>, <src>)] to append with generated source files. |
| + extra_deps: [<cmake_taget>] to append with generated targets. |
| + path_to_gyp: relative path from CMakeLists.txt being generated to |
| + the Gyp file in which the target being generated is defined. |
| + """ |
| + for action in actions: |
| + action_name = to_cmake_target_name(action['action_name']) |
| + action_target_name = '%s__%s' % (target_name, action_name) |
| + |
| + inputs = action['inputs'] |
| + inputs_name = action_target_name + '__input' |
| + set_variable_list(output, inputs_name, |
| + [NormjoinPathForceCMakeSource(path_to_gyp, dep) for dep in inputs]) |
| + |
| + outputs = action['outputs'] |
| + cmake_outputs = [NormjoinPathForceCMakeSource(path_to_gyp, out) |
| + for out in outputs] |
| + outputs_name = action_target_name + '__output' |
| + set_variable_list(output, outputs_name, cmake_outputs) |
| + |
| + # Build up a list of outputs. |
| + # Collect the output dirs we'll need. |
| + dirs = set(dir for dir in (os.path.dirname(o) for o in outputs) if dir) |
| + |
| + if int(action.get('process_outputs_as_sources', False)): |
| + extra_sources.extend(zip(cmake_outputs, outputs)) |
| + |
| + #add_custom_command |
|
Nico
2013/11/19 16:35:28
space (also below)
bungeman-chromium
2013/11/20 20:54:51
Done.
|
| + output.write('add_custom_command(OUTPUT ') |
| + WriteVariable(output, outputs_name) |
| + output.write('\n') |
| + |
| + if len(dirs) > 0: |
| + for directory in dirs: |
|
Nico
2013/11/19 16:35:28
I think sets don't have deterministic iteration or
bungeman-chromium
2013/11/20 20:54:51
Don't care about the order here, just want to ensu
|
| + output.write(' COMMAND ${CMAKE_COMMAND} -E make_directory ') |
| + output.write(directory) |
| + output.write('\n') |
| + |
| + output.write(' COMMAND ') |
| + output.write(gyp.common.EncodePOSIXShellList(action['action'])) |
| + output.write('\n') |
| + |
| + output.write(' DEPENDS ') |
| + WriteVariable(output, inputs_name) |
| + output.write('\n') |
| + |
| + output.write(' WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/') |
| + output.write(path_to_gyp) |
| + output.write('\n') |
| + |
| + output.write(' COMMENT ') |
| + if 'message' in action: |
| + output.write(action['message']) |
| + else: |
| + output.write(action_target_name) |
| + output.write('\n') |
| + |
| + output.write(' VERBATIM\n') |
| + output.write(')\n') |
| + |
| + #add_custom_target |
| + output.write('add_custom_target(') |
| + output.write(action_target_name) |
| + output.write('\n DEPENDS ') |
| + WriteVariable(output, outputs_name) |
| + output.write('\n SOURCES ') |
| + WriteVariable(output, inputs_name) |
| + output.write('\n)\n') |
| + |
| + extra_deps.append(action_target_name) |
| + |
| + |
| +def NormjoinRulePathForceCMakeSource(base_path, rel_path, rule_source): |
| + if rel_path.startswith(("${RULE_INPUT_PATH}","${RULE_INPUT_DIRNAME}")): |
| + if any([rule_source.startswith(var) for var in FULL_PATH_VARS]): |
| + return rel_path |
| + return NormjoinPathForceCMakeSource(base_path, rel_path) |
| + |
| + |
| +def WriteRules(target_name, rules, extra_sources, extra_deps, |
| + path_to_gyp, output): |
| + """Write CMake for the 'rules' in the target. |
| + |
| + Args: |
| + target_name: the name of the CMake target being generated. |
| + actions: the Gyp 'actions' dict for this target. |
| + extra_sources: [(<cmake_src>, <src>)] to append with generated source files. |
| + extra_deps: [<cmake_taget>] to append with generated targets. |
| + path_to_gyp: relative path from CMakeLists.txt being generated to |
| + the Gyp file in which the target being generated is defined. |
| + """ |
| + for rule in rules: |
| + rule_name = to_cmake_target_name(target_name + '__' + rule['rule_name']) |
| + |
| + inputs = rule.get('inputs', []) |
| + inputs_name = rule_name + '__input' |
| + set_variable_list(output, inputs_name, |
| + [NormjoinPathForceCMakeSource(path_to_gyp, dep) for dep in inputs]) |
| + outputs = rule['outputs'] |
| + var_outputs = [] |
| + |
| + for count, rule_source in enumerate(rule.get('rule_sources', [])): |
| + action_name = rule_name + '_' + str(count) |
| + |
| + rule_source_dirname, rule_source_basename = os.path.split(rule_source) |
| + rule_source_root, rule_source_ext = os.path.splitext(rule_source_basename) |
| + |
| + set_variable(output, 'RULE_INPUT_PATH', rule_source) |
| + set_variable(output, 'RULE_INPUT_DIRNAME', rule_source_dirname) |
| + set_variable(output, 'RULE_INPUT_NAME', rule_source_basename) |
| + set_variable(output, 'RULE_INPUT_ROOT', rule_source_root) |
| + set_variable(output, 'RULE_INPUT_EXT', rule_source_ext) |
| + |
| + # Build up a list of outputs. |
| + # Collect the output dirs we'll need. |
| + dirs = set(dir for dir in (os.path.dirname(o) for o in outputs) if dir) |
| + |
| + #Create variables for the output, as 'local' variable will be unset. |
| + these_outputs = [] |
| + for output_count, out in enumerate(outputs): |
|
Nico
2013/11/19 16:35:28
output_index?
bungeman-chromium
2013/11/20 20:54:51
Done.
|
| + output_name = action_name + '_' + str(output_count) |
| + set_variable(output, output_name, |
| + NormjoinRulePathForceCMakeSource(path_to_gyp, out, |
| + rule_source)) |
| + if int(rule.get('process_outputs_as_sources', False)): |
| + extra_sources.append(('${' + output_name + '}', out)) |
| + these_outputs.append('${' + output_name + '}') |
| + var_outputs.append('${' + output_name + '}') |
| + |
| + #add_custom_command |
| + output.write('add_custom_command(OUTPUT\n') |
| + for out in these_outputs: |
| + output.write(' ') |
| + output.write(out) |
| + output.write('\n') |
| + |
| + for directory in dirs: |
| + output.write(' COMMAND ${CMAKE_COMMAND} -E make_directory ') |
| + output.write(directory) |
| + output.write('\n') |
| + |
| + output.write(' COMMAND ') |
| + output.write(gyp.common.EncodePOSIXShellList(rule['action'])) |
| + output.write('\n') |
| + |
| + output.write(' DEPENDS ') |
| + WriteVariable(output, inputs_name) |
| + output.write(' ') |
| + output.write(NormjoinPath(path_to_gyp, rule_source)) |
| + output.write('\n') |
| + |
| + #CMAKE_SOURCE_DIR is where the CMakeLists.txt lives. |
| + #The cwd is the current build directory. |
| + output.write(' WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/') |
| + output.write(path_to_gyp) |
| + output.write('\n') |
| + |
| + output.write(' COMMENT ') |
| + if 'message' in rule: |
| + output.write(rule['message']) |
| + else: |
| + output.write(action_name) |
| + output.write('\n') |
| + |
| + output.write(' VERBATIM\n') |
| + output.write(')\n') |
| + |
| + unset_variable(output, 'RULE_INPUT_PATH') |
| + unset_variable(output, 'RULE_INPUT_DIRNAME') |
| + unset_variable(output, 'RULE_INPUT_NAME') |
| + unset_variable(output, 'RULE_INPUT_ROOT') |
| + unset_variable(output, 'RULE_INPUT_EXT') |
| + |
| + #add_custom_target |
| + output.write('add_custom_target(') |
| + output.write(rule_name) |
| + output.write(' DEPENDS\n') |
| + for out in var_outputs: |
| + output.write(' ') |
| + output.write(out) |
| + output.write('\n') |
| + output.write('SOURCES ') |
| + WriteVariable(output, inputs_name) |
| + output.write('\n') |
| + for rule_source in rule.get('rule_sources', []): |
| + output.write(' ') |
| + output.write(NormjoinPath(path_to_gyp, rule_source)) |
| + output.write('\n') |
| + output.write(')\n') |
| + |
| + extra_deps.append(rule_name) |
| + |
| + |
| +def WriteCopies(target_name, copies, extra_deps, path_to_gyp, output): |
| + """Write CMake for the 'copies' in the target. |
| + |
| + Args: |
| + target_name: the name of the CMake target being generated. |
| + actions: the Gyp 'actions' dict for this target. |
| + extra_deps: [<cmake_taget>] to append with generated targets. |
| + path_to_gyp: relative path from CMakeLists.txt being generated to |
| + the Gyp file in which the target being generated is defined. |
| + """ |
| + copy_name = target_name + '__copies' |
| + |
| + #CMake gets upset with custom targets with OUTPUT which specify no output. |
| + have_copies = any(copy['files'] for copy in copies) |
| + if not have_copies: |
| + output.write('add_custom_target(') |
| + output.write(copy_name) |
| + output.write(')\n') |
| + extra_deps.append(copy_name) |
| + return |
| + |
| + class Copy: |
| + def __init__(self, ext, command): |
| + self.cmake_inputs = [] |
| + self.cmake_outputs = [] |
| + self.gyp_inputs = [] |
| + self.gyp_outputs = [] |
| + self.ext = ext |
| + self.inputs_name = None |
| + self.outputs_name = None |
| + self.command = command |
| + |
| + file_copy = Copy('', 'copy') |
| + dir_copy = Copy('_dirs', 'copy_directory') |
| + |
| + for copy in copies: |
| + files = copy['files'] |
| + destination = copy['destination'] |
| + for src in files: |
| + path = os.path.normpath(src) |
| + basename = os.path.split(path)[1] |
| + dst = os.path.join(destination, basename) |
| + |
| + copy = file_copy if os.path.basename(src) else dir_copy |
| + |
| + copy.cmake_inputs.append(NormjoinPath(path_to_gyp, src)) |
| + copy.cmake_outputs.append(NormjoinPathForceCMakeSource(path_to_gyp, dst)) |
| + copy.gyp_inputs.append(src) |
| + copy.gyp_outputs.append(dst) |
| + |
| + for copy in (file_copy, dir_copy): |
| + if copy.cmake_inputs: |
| + copy.inputs_name = copy_name + '__input' + copy.ext |
| + set_variable_list(output, copy.inputs_name, copy.cmake_inputs) |
| + |
| + copy.outputs_name = copy_name + '__output' + copy.ext |
| + set_variable_list(output, copy.outputs_name, copy.cmake_outputs) |
| + |
| + #add_custom_command |
| + output.write('add_custom_command(\n') |
| + |
| + output.write('OUTPUT') |
| + for copy in (file_copy, dir_copy): |
| + if copy.outputs_name: |
| + WriteVariable(output, copy.outputs_name, ' ') |
| + output.write('\n') |
| + |
| + for copy in (file_copy, dir_copy): |
| + for src, dst in zip(copy.gyp_inputs, copy.gyp_outputs): |
| + #'cmake -E copy src dst' will create the 'dst' directory if needed. |
| + output.write('COMMAND ${CMAKE_COMMAND} -E %s ' % copy.command) |
| + output.write(src) |
| + output.write(' ') |
| + output.write(dst) |
| + output.write("\n") |
| + |
| + output.write('DEPENDS') |
| + for copy in (file_copy, dir_copy): |
| + if copy.inputs_name: |
| + WriteVariable(output, copy.inputs_name, ' ') |
| + output.write('\n') |
| + |
| + output.write('WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/') |
| + output.write(path_to_gyp) |
| + output.write('\n') |
| + |
| + output.write('COMMENT Copying for ') |
| + output.write(target_name) |
| + output.write('\n') |
| + |
| + output.write('VERBATIM\n') |
| + output.write(')\n') |
| + |
| + #add_custom_target |
| + output.write('add_custom_target(') |
| + output.write(copy_name) |
| + output.write('\n DEPENDS') |
| + for copy in (file_copy, dir_copy): |
| + if copy.outputs_name: |
| + WriteVariable(output, copy.outputs_name, ' ') |
| + output.write('\n SOURCES') |
| + if file_copy.inputs_name: |
| + WriteVariable(output, file_copy.inputs_name, ' ') |
| + output.write('\n)\n') |
| + |
| + extra_deps.append(copy_name) |
| + |
| + |
| +def create_cmake_target_base_name(qualified_target): |
| + """This is the name we would like the target to have.""" |
| + _, gyp_target_name, gyp_target_toolset = ( |
| + gyp.common.ParseQualifiedTarget(qualified_target)) |
| + cmake_target_base_name = gyp_target_name |
| + if gyp_target_toolset and gyp_target_toolset != 'target': |
| + cmake_target_base_name += '_' + gyp_target_toolset |
| + return to_cmake_target_name(cmake_target_base_name) |
| + |
| + |
| +def create_cmake_target_full_name(qualified_target): |
| + """An unambiguous name for the target.""" |
| + gyp_file, gyp_target_name, gyp_target_toolset = ( |
| + gyp.common.ParseQualifiedTarget(qualified_target)) |
| + cmake_target_full_name = gyp_file + ':' + gyp_target_name |
| + if gyp_target_toolset and gyp_target_toolset != 'target': |
| + cmake_target_full_name += '_' + gyp_target_toolset |
| + return to_cmake_target_name(cmake_target_full_name) |
| + |
| + |
| +class CMakeNamer: |
|
Nico
2013/11/19 16:35:28
(object)
bungeman-chromium
2013/11/20 20:54:51
Done.
|
| + """Converts Gyp target names into CMake target names. |
| + |
| + CMake requires that target names be globally unique. One way to ensure |
| + this is to fully qualify the names of the targets. Unfortunatly, this |
| + ends up with all targets looking like "chrome_chrome_gyp_chrome" instead |
| + of just "chrome". If this generator were only interested in building, it |
| + would be possible to fully qualify all target names, then create |
| + unqualified target names which depend on all qualified targets which |
| + should have had that name. This is more or less what the 'make' generator |
| + does with aliases. However, one goal of this generator is to create CMake |
| + files for use with IDEs, and fully qualified names are not as user |
| + friendly. |
| + |
| + Since target name collision is rare, we do the above only when required. |
| + |
| + Toolset variants are always qualified from the base, as this is required for |
| + building. However, it also makes sense for an IDE, as it is possible for |
| + defines to be different. |
| + """ |
| + def __init__(self, target_list): |
| + self.cmake_target_base_names_conficting = set() |
| + |
| + cmake_target_base_names_seen = set() |
| + for qualified_target in target_list: |
| + cmake_target_base_name = create_cmake_target_base_name(qualified_target) |
| + |
| + if cmake_target_base_name not in cmake_target_base_names_seen: |
| + cmake_target_base_names_seen.add(cmake_target_base_name) |
| + else: |
| + self.cmake_target_base_names_conficting.add(cmake_target_base_name) |
| + |
| + def create_cmake_target_name(self, qualified_target): |
| + base_name = create_cmake_target_base_name(qualified_target) |
| + if base_name in self.cmake_target_base_names_conficting: |
| + return create_cmake_target_full_name(qualified_target) |
| + return base_name |
| + |
| + |
| +def WriteTarget(namer, qualified_target, target_dicts, build_dir, config_to_use, |
| + options, generator_flags, all_qualified_targets, output): |
| + |
| + #The make generator does this always. |
| + #TODO: It would be nice to be able to tell CMake all dependencies. |
| + circular_libs = generator_flags.get('circular', True) |
| + |
| + if not generator_flags.get('standalone', False): |
| + output.write('\n#') |
| + output.write(qualified_target) |
| + output.write('\n') |
| + |
| + gyp_file, _, _ = gyp.common.ParseQualifiedTarget(qualified_target) |
| + rel_gyp_file = gyp.common.RelativePath(gyp_file, options.toplevel_dir) |
| + rel_gyp_dir = os.path.dirname(rel_gyp_file) |
| + |
| + # Relative path from build dir to top dir. |
| + build_to_top = gyp.common.InvertRelativePath(build_dir, options.toplevel_dir) |
| + # Relative path from build dir to gyp dir. |
| + build_to_gyp = os.path.join(build_to_top, rel_gyp_dir) |
| + |
| + path_from_cmakelists_to_gyp = build_to_gyp |
| + |
| + spec = target_dicts.get(qualified_target, {}) |
| + config = spec.get('configurations', {}).get(config_to_use, {}) |
| + |
| + target_name = spec.get('target_name', '<missing target name>') |
| + target_type = spec.get('type', '<missing target type>') |
| + target_toolset = spec.get('toolset') |
| + |
| + set_variable(output, 'TARGET', target_name) |
| + set_variable(output, 'TOOLSET', target_toolset) |
| + |
| + cmake_target_name = namer.create_cmake_target_name(qualified_target) |
| + |
| + extra_sources = [] |
| + extra_deps = [] |
| + |
| + # Actions must come first, since they can generate more OBJs for use below. |
| + if 'actions' in spec: |
| + WriteActions(cmake_target_name, spec['actions'], extra_sources, extra_deps, |
| + path_from_cmakelists_to_gyp, output) |
| + |
| + # Rules must be early like actions. |
| + if 'rules' in spec: |
| + WriteRules(cmake_target_name, spec['rules'], extra_sources, extra_deps, |
| + path_from_cmakelists_to_gyp, output) |
| + |
| + #Copies |
| + if 'copies' in spec: |
| + WriteCopies(cmake_target_name, spec['copies'], extra_deps, |
| + path_from_cmakelists_to_gyp, output) |
| + |
| + #Target and sources |
| + srcs = spec.get('sources', []) |
| + |
| + #Gyp separates the sheep from the goats based on file extensions. |
| + def partition(l, p): |
| + return reduce(lambda x, e: x[not p(e)].append(e) or x, l, ([], [])) |
| + compilable_srcs, other_srcs = partition(srcs, Compilable) |
| + |
| + if target_type == 'executable' and not compilable_srcs and not extra_sources: |
| + print ('Executable %s has no complilable sources, treating as "none".' % |
| + target_name ) |
| + target_type = 'none' |
|
Nico
2013/11/19 16:35:28
Do other generators support this?
bungeman-chromium
2013/11/20 20:54:51
No, I don't think the other generators do this. I
|
| + |
| + cmake_target_type = cmake_target_type_from_gyp_target_type.get(target_type) |
| + if cmake_target_type is None: |
| + print ('Target %s has unknown target type %s, skipping.' % |
| + ( target_name, target_type ) ) |
| + return |
| + |
| + other_srcs_name = None |
| + if other_srcs: |
| + other_srcs_name = cmake_target_name + '__other_srcs' |
| + set_variable_list(output, other_srcs_name, |
| + [NormjoinPath(path_from_cmakelists_to_gyp, src) for src in other_srcs]) |
| + |
| + #CMake is opposed to setting linker directories as dangerous; |
|
Nico
2013/11/19 16:35:28
doesn't parse
bungeman-chromium
2013/11/20 20:54:51
The tyranny of short lines. Updated so that it sho
|
| + #it favors find_library and passing absolute paths to target_link_libraries. |
| + #The link_directories command adds link directories to targets defined later. |
| + #So link_directories must come before the target definition. |
| + #CMake unfortunately has no means of removing from LINK_DIRECTORIES. |
| + library_dirs = config.get('library_dirs') |
| + if library_dirs is not None: |
| + output.write('link_directories(') |
| + for library_dir in library_dirs: |
| + output.write(' ') |
| + output.write(NormjoinPath(path_from_cmakelists_to_gyp, library_dir)) |
| + output.write('\n') |
| + output.write(')\n') |
| + |
| + output.write(cmake_target_type.command) |
| + output.write('(') |
| + output.write(cmake_target_name) |
| + |
| + if cmake_target_type.modifier is not None: |
| + output.write(' ') |
| + output.write(cmake_target_type.modifier) |
| + |
| + if other_srcs_name: |
| + WriteVariable(output, other_srcs_name, ' ') |
| + |
| + output.write('\n') |
| + |
| + for src in compilable_srcs: |
| + output.write(' ') |
| + output.write(NormjoinPath(path_from_cmakelists_to_gyp, src)) |
| + output.write('\n') |
| + for extra_source in extra_sources: |
| + output.write(' ') |
| + src, _ = extra_source |
| + output.write(NormjoinPath(path_from_cmakelists_to_gyp, src)) |
| + output.write('\n') |
| + |
| + output.write(')\n') |
| + |
| + #Output name and location. |
| + if target_type != 'none': |
| + #Mark uncompiled sources as uncompiled. |
| + if other_srcs_name: |
| + output.write('set_source_files_properties(') |
| + WriteVariable(output, other_srcs_name, '') |
| + output.write(' PROPERTIES HEADER_FILE_ONLY "TRUE")\n') |
| + |
| + #Output directory |
| + target_output_directory = spec.get('product_dir') |
| + if target_output_directory is None: |
| + if target_type in ('executable', 'loadable_module'): |
| + target_output_directory = generator_default_variables['PRODUCT_DIR'] |
| + elif target_type in ('shared_library'): |
| + target_output_directory = '${builddir}/lib.${TOOLSET}' |
| + elif spec.get('standalone_static_library', False): |
| + target_output_directory = generator_default_variables['PRODUCT_DIR'] |
| + else: |
| + base_path = gyp.common.RelativePath(os.path.dirname(gyp_file), |
| + options.toplevel_dir) |
| + target_output_directory = '${obj}.${TOOLSET}' |
| + target_output_directory = ( |
| + os.path.join(target_output_directory, base_path)) |
| + |
| + cmake_target_output_directory = NormjoinPathForceCMakeSource( |
| + path_from_cmakelists_to_gyp, |
| + target_output_directory) |
| + set_target_property(output, |
| + cmake_target_name, |
| + cmake_target_type.property_modifier + '_OUTPUT_DIRECTORY', |
| + cmake_target_output_directory) |
| + |
| + #Output name |
| + default_product_prefix = '' |
| + default_product_name = target_name |
| + default_product_ext = '' |
| + if target_type == 'static_library': |
| + static_library_prefix = generator_default_variables['STATIC_LIB_PREFIX'] |
| + default_product_name = RemovePrefix(default_product_name, |
| + static_library_prefix) |
| + default_product_prefix = static_library_prefix |
| + default_product_ext = generator_default_variables['STATIC_LIB_SUFFIX'] |
| + |
| + elif target_type in ('loadable_module', 'shared_library'): |
| + shared_library_prefix = generator_default_variables['SHARED_LIB_PREFIX'] |
| + default_product_name = RemovePrefix(default_product_name, |
| + shared_library_prefix) |
| + default_product_prefix = shared_library_prefix |
| + default_product_ext = generator_default_variables['SHARED_LIB_SUFFIX'] |
| + |
| + elif target_type != 'executable': |
| + print ('ERROR: What output file should be generated?', |
| + 'type', target_type, 'target', target_name) |
| + |
| + product_prefix = spec.get('product_prefix', default_product_prefix) |
| + product_name = spec.get('product_name', default_product_name) |
| + product_ext = spec.get('product_extension') |
| + if product_ext: |
| + product_ext = '.' + product_ext |
| + else: |
| + product_ext = default_product_ext |
| + |
| + set_target_property(output, cmake_target_name, 'PREFIX', product_prefix) |
| + set_target_property(output, cmake_target_name, |
| + cmake_target_type.property_modifier + '_OUTPUT_NAME', |
| + product_name) |
| + set_target_property(output, cmake_target_name, 'SUFFIX', product_ext) |
| + |
| + #Make the output of this target referenceable as a source. |
| + cmake_target_output_basename = product_prefix + product_name + product_ext |
| + cmake_target_output = os.path.join(cmake_target_output_directory, |
| + cmake_target_output_basename) |
| + set_file_property(output, cmake_target_output, 'GENERATED', ['TRUE'], '') |
| + |
| + #Let CMake know if the 'all' target should depend on this target. |
| + exclude_from_all = ('TRUE' if qualified_target not in all_qualified_targets |
| + else 'FALSE') |
| + set_target_property(output, cmake_target_name, |
| + 'EXCLUDE_FROM_ALL', exclude_from_all) |
| + for extra_target_name in extra_deps: |
| + set_target_property(output, extra_target_name, |
| + 'EXCLUDE_FROM_ALL', exclude_from_all) |
| + |
| + #Includes |
| + includes = config.get('include_dirs') |
| + if includes: |
| + #This (target include directories) is what requires CMake 2.8.8 |
| + includes_name = cmake_target_name + '__include_dirs' |
| + set_variable_list(output, includes_name, |
| + [NormjoinPathForceCMakeSource(path_from_cmakelists_to_gyp, include) |
| + for include in includes]) |
| + output.write('set_property(TARGET ') |
| + output.write(cmake_target_name) |
| + output.write(' APPEND PROPERTY INCLUDE_DIRECTORIES ') |
| + WriteVariable(output, includes_name, '') |
| + output.write(')\n') |
| + |
| + #Defines |
| + defines = config.get('defines') |
| + if defines is not None: |
| + set_target_property(output, |
| + cmake_target_name, |
| + 'COMPILE_DEFINITIONS', |
| + defines, |
| + ';') |
| + |
| + #Compile Flags - http://www.cmake.org/Bug/view.php?id=6493 |
| + #CMake currently does not have target C and CXX flags. |
| + #So, instead of doing... |
| + |
| + #cflags_c = config.get('cflags_c') |
| + #if cflags_c is not None: |
| + # set_target_property(output, cmake_target_name, |
| + # 'C_COMPILE_FLAGS', cflags_c, ' ') |
| + |
| + #cflags_cc = config.get('cflags_cc') |
| + #if cflags_cc is not None: |
| + # set_target_property(output, cmake_target_name, |
| + # 'CXX_COMPILE_FLAGS', cflags_cc, ' ') |
| + |
| + #Instead we must... |
| + s_sources = [] |
| + c_sources = [] |
| + cxx_sources = [] |
| + for src in srcs: |
| + _, ext = os.path.splitext(src) |
| + src_type = COMPILABLE_EXTENSIONS.get(ext, None) |
| + |
| + if src_type == 's': |
| + s_sources.append(NormjoinPath(path_from_cmakelists_to_gyp, src)) |
| + |
| + if src_type == 'cc': |
| + c_sources.append(NormjoinPath(path_from_cmakelists_to_gyp, src)) |
| + |
| + if src_type == 'cxx': |
| + cxx_sources.append(NormjoinPath(path_from_cmakelists_to_gyp, src)) |
| + |
| + for extra_source in extra_sources: |
| + src, real_source = extra_source |
| + _, ext = os.path.splitext(real_source) |
| + src_type = COMPILABLE_EXTENSIONS.get(ext, None) |
| + |
| + if src_type == 's': |
| + s_sources.append(NormjoinPath(path_from_cmakelists_to_gyp, src)) |
| + |
| + if src_type == 'cc': |
| + c_sources.append(NormjoinPath(path_from_cmakelists_to_gyp, src)) |
| + |
| + if src_type == 'cxx': |
| + cxx_sources.append(NormjoinPath(path_from_cmakelists_to_gyp, src)) |
| + |
| + cflags = config.get('cflags', []) |
| + cflags_c = config.get('cflags_c', []) |
| + cflags_cxx = config.get('cflags_cc', []) |
| + if c_sources and not (s_sources or cxx_sources): |
| + flags = [] |
| + flags.extend(cflags) |
| + flags.extend(cflags_c) |
| + set_target_property(output, cmake_target_name, 'COMPILE_FLAGS', flags, ' ') |
| + |
| + elif cxx_sources and not (s_sources or c_sources): |
| + flags = [] |
| + flags.extend(cflags) |
| + flags.extend(cflags_cxx) |
| + set_target_property(output, cmake_target_name, 'COMPILE_FLAGS', flags, ' ') |
| + |
| + else: |
| + if s_sources and cflags: |
| + set_files_property(output, s_sources, 'COMPILE_FLAGS', cflags, ' ') |
| + |
| + if c_sources and (cflags or cflags_c): |
| + flags = [] |
| + flags.extend(cflags) |
| + flags.extend(cflags_c) |
| + set_files_property(output, c_sources, 'COMPILE_FLAGS', flags, ' ') |
| + |
| + if cxx_sources and (cflags or cflags_cxx): |
| + flags = [] |
| + flags.extend(cflags) |
| + flags.extend(cflags_cxx) |
| + set_files_property(output, cxx_sources, 'COMPILE_FLAGS', flags, ' ') |
| + |
| + #Have assembly link as c if there are no other files |
| + if not c_sources and not cxx_sources and s_sources: |
| + set_target_property(output, cmake_target_name, 'LINKER_LANGUAGE', ['C']) |
| + |
| + #Linker flags |
| + ldflags = config.get('ldflags') |
| + if ldflags is not None: |
| + set_target_property(output, cmake_target_name, 'LINK_FLAGS', ldflags, ' ') |
| + |
| + #Note on Dependencies and Libraries: |
| + #CMake wants to handle link order, resolving the link line up front. |
| + #Gyp does not retain or enforce specifying enough information to do so. |
| + #So do as other gyp generators and use --start-group and --end-group. |
| + #Give CMake as little information as possible so that it doesn't mess it up. |
| + |
| + #Dependencies |
| + rawDeps = spec.get('dependencies', []) |
| + |
| + static_deps = [] |
| + shared_deps = [] |
| + other_deps = [] |
| + for rawDep in rawDeps: |
| + dep_cmake_name = namer.create_cmake_target_name(rawDep) |
| + dep_spec = target_dicts.get(rawDep, {}) |
| + dep_target_type = dep_spec.get('type', None) |
| + |
| + if dep_target_type == 'static_library': |
| + static_deps.append(dep_cmake_name) |
| + elif dep_target_type == 'shared_library': |
| + shared_deps.append(dep_cmake_name) |
| + else: |
| + other_deps.append(dep_cmake_name) |
| + |
| + #ensure all external dependencies are complete before internal dependencies |
| + #extra_deps currently only depend on their own deps, so otherwise run early |
| + if static_deps or shared_deps or other_deps: |
| + for extra_dep in extra_deps: |
| + output.write('add_dependencies(') |
| + output.write(extra_dep) |
| + output.write('\n') |
| + for deps in (static_deps, shared_deps, other_deps): |
| + for dep in gyp.common.uniquer(deps): |
| + output.write(' ') |
| + output.write(dep) |
| + output.write('\n') |
| + output.write(')\n') |
| + |
| + linkable = target_type in ('executable', 'loadable_module', 'shared_library') |
| + other_deps.extend(extra_deps) |
| + if other_deps or (not linkable and (static_deps or shared_deps)): |
| + output.write('add_dependencies(') |
| + output.write(cmake_target_name) |
| + output.write('\n') |
| + for dep in gyp.common.uniquer(other_deps): |
| + output.write(' ') |
| + output.write(dep) |
| + output.write('\n') |
| + if not linkable: |
| + for deps in (static_deps, shared_deps): |
| + for lib_dep in gyp.common.uniquer(deps): |
| + output.write(' ') |
| + output.write(lib_dep) |
| + output.write('\n') |
| + output.write(')\n') |
| + |
| + #Libraries |
| + if linkable: |
| + external_libs = [lib for lib in spec.get('libraries', []) if len(lib) > 0] |
| + if external_libs or static_deps or shared_deps: |
| + output.write('target_link_libraries(') |
| + output.write(cmake_target_name) |
| + output.write('\n') |
| + if static_deps: |
| + write_group = circular_libs and len(static_deps) > 1 |
| + if write_group: |
| + output.write('-Wl,--start-group\n') |
| + for dep in gyp.common.uniquer(static_deps): |
| + output.write(' ') |
| + output.write(dep) |
| + output.write('\n') |
| + if write_group: |
| + output.write('-Wl,--end-group\n') |
| + if shared_deps: |
| + for dep in gyp.common.uniquer(shared_deps): |
| + output.write(' ') |
| + output.write(dep) |
| + output.write('\n') |
| + if external_libs: |
| + for lib in gyp.common.uniquer(external_libs): |
| + output.write(' ') |
| + output.write(lib) |
| + output.write('\n') |
| + |
| + output.write(')\n') |
| + |
| + unset_variable(output, 'TOOLSET') |
| + unset_variable(output, 'TARGET') |
| + |
|
Nico
2013/11/19 16:35:28
(nit: newline)
bungeman-chromium
2013/11/20 20:54:51
Done.
|
| +def GenerateOutputForConfig(target_list, target_dicts, data, |
| + params, config_to_use): |
| + options = params['options'] |
| + generator_flags = params['generator_flags'] |
| + |
| + # generator_dir: relative path from pwd to where make puts build files. |
| + # Makes migrating from make to cmake easier, cmake doesn't put anything here. |
| + # Each Gyp configuration creates a different CMakeLists.txt file |
| + # to avoid incompatibilities between Gyp and CMake configurations. |
| + generator_dir = os.path.relpath(options.generator_output or '.') |
| + |
| + # output_dir: relative path from generator_dir to the build directory. |
| + output_dir = generator_flags.get('output_dir', 'out') |
| + |
| + # build_dir: relative path from source root to our output files. |
| + # e.g. "out/Debug" |
| + build_dir = os.path.normpath(os.path.join(generator_dir, |
| + output_dir, |
| + config_to_use)) |
| + |
| + toplevel_build = os.path.join(options.toplevel_dir, build_dir) |
| + |
| + output_file = os.path.join(toplevel_build, 'CMakeLists.txt') |
| + ensure_directory_exists(output_file) |
| + |
| + output = open(output_file, 'w') |
| + output.write('cmake_minimum_required(VERSION 2.8.8 FATAL_ERROR)\n') |
| + output.write('cmake_policy(VERSION 2.8.8)\n') |
| + |
| + _, project_target, _ = gyp.common.ParseQualifiedTarget(target_list[-1]) |
| + output.write('project(') |
| + output.write(project_target) |
| + output.write(')\n') |
| + |
| + set_variable(output, 'configuration', config_to_use) |
| + |
| + #The following appears to be as-yet undocumented. |
| + #http://public.kitware.com/Bug/view.php?id=8392 |
| + output.write('enable_language(ASM)\n') |
| + #ASM-ATT does not support .S files. |
| + #output.write('enable_language(ASM-ATT)\n') |
| + |
| + set_variable(output, 'builddir', '${CMAKE_BINARY_DIR}') |
| + set_variable(output, 'obj', '${builddir}/obj') |
| + output.write('\n') |
| + |
| + #XXX: Undocumented and unsupported (the CMake Java generator depends on it). |
|
Nico
2013/11/19 16:35:28
"TODO"
bungeman-chromium
2013/11/20 20:54:51
Done.
|
| + #CMake by default names the object resulting from foo.c to be foo.c.o. |
| + #Gyp traditionally names the object resulting from foo.c foo.o. |
| + #This should be irrelevent, but some targets extract .o files from .a |
|
Nico
2013/11/19 16:35:28
irrelevant
bungeman-chromium
2013/11/20 20:54:51
Done.
|
| + #and depend on the name of the extracted .o files. |
| + output.write('set(CMAKE_C_OUTPUT_EXTENSION_REPLACE 1)\n') |
| + output.write('set(CMAKE_CXX_OUTPUT_EXTENSION_REPLACE 1)\n') |
| + output.write('\n') |
| + |
| + namer = CMakeNamer(target_list) |
| + |
| + #The list of targets upon which the 'all' target should depend. |
| + #CMake has it's own implicit 'all' target, one is not created explicitly. |
| + all_qualified_targets = set() |
| + for build_file in params['build_files']: |
| + for qualified_target in gyp.common.AllTargets(target_list, |
| + target_dicts, |
| + os.path.normpath(build_file)): |
| + all_qualified_targets.add(qualified_target) |
| + |
| + for qualified_target in target_list: |
| + WriteTarget(namer, qualified_target, target_dicts, build_dir, config_to_use, |
| + options, generator_flags, all_qualified_targets, output) |
| + |
| + output.close() |
| + |
| + |
| +def PerformBuild(data, configurations, params): |
| + options = params['options'] |
| + generator_flags = params['generator_flags'] |
| + |
| + # generator_dir: relative path from pwd to where make puts build files. |
| + # Makes migrating from make to cmake easier, cmake doesn't put anything here. |
| + generator_dir = os.path.relpath(options.generator_output or '.') |
| + |
| + # output_dir: relative path from generator_dir to the build directory. |
| + output_dir = generator_flags.get('output_dir', 'out') |
| + |
| + for config_name in configurations: |
| + # build_dir: relative path from source root to our output files. |
| + # e.g. "out/Debug" |
| + build_dir = os.path.normpath(os.path.join(generator_dir, |
| + output_dir, |
| + config_name)) |
| + arguments = ['cmake', '-G', 'Ninja'] |
| + print 'Generating [%s]: %s' % (config_name, arguments) |
| + subprocess.check_call(arguments, cwd=build_dir) |
| + |
| + arguments = ['ninja', '-C', build_dir] |
| + print 'Building [%s]: %s' % (config_name, arguments) |
| + subprocess.check_call(arguments) |
| + |
| + |
| +def CallGenerateOutputForConfig(arglist): |
| + # Ignore the interrupt signal so that the parent process catches it and |
| + # kills all multiprocessing children. |
| + signal.signal(signal.SIGINT, signal.SIG_IGN) |
| + |
| + target_list, target_dicts, data, params, config_name = arglist |
| + GenerateOutputForConfig(target_list, target_dicts, data, params, config_name) |
| + |
| + |
| +def GenerateOutput(target_list, target_dicts, data, params): |
| + user_config = params.get('generator_flags', {}).get('config', None) |
| + if user_config: |
| + GenerateOutputForConfig(target_list, target_dicts, data, |
| + params, user_config) |
| + else: |
| + config_names = target_dicts[target_list[0]]['configurations'].keys() |
| + if params['parallel']: |
| + try: |
| + pool = multiprocessing.Pool(len(config_names)) |
| + arglists = [] |
| + for config_name in config_names: |
| + arglists.append((target_list, target_dicts, data, |
| + params, config_name)) |
| + pool.map(CallGenerateOutputForConfig, arglists) |
| + except KeyboardInterrupt, e: |
| + pool.terminate() |
| + raise e |
| + else: |
| + for config_name in config_names: |
| + GenerateOutputForConfig(target_list, target_dicts, data, |
| + params, config_name) |