diff options
author | Ben Noordhuis <info@bnoordhuis.nl> | 2012-02-20 11:47:03 +0100 |
---|---|---|
committer | Ben Noordhuis <info@bnoordhuis.nl> | 2012-02-20 11:47:03 +0100 |
commit | 4af673e161138fbed287a8e00cf7b2b0aafdd8b3 (patch) | |
tree | 90fb82bb98fdbcbb0c86c446e029c13bf999c8b0 /tools/gyp/pylib/gyp/generator/ninja.py | |
parent | 7ae0d473a6b6ac8cb3e55d665528667566cd8e60 (diff) | |
download | android-node-v8-4af673e161138fbed287a8e00cf7b2b0aafdd8b3.tar.gz android-node-v8-4af673e161138fbed287a8e00cf7b2b0aafdd8b3.tar.bz2 android-node-v8-4af673e161138fbed287a8e00cf7b2b0aafdd8b3.zip |
gyp: update to r1214
Diffstat (limited to 'tools/gyp/pylib/gyp/generator/ninja.py')
-rw-r--r-- | tools/gyp/pylib/gyp/generator/ninja.py | 869 |
1 files changed, 643 insertions, 226 deletions
diff --git a/tools/gyp/pylib/gyp/generator/ninja.py b/tools/gyp/pylib/gyp/generator/ninja.py index 193f295a55..8d6c6f52d1 100644 --- a/tools/gyp/pylib/gyp/generator/ninja.py +++ b/tools/gyp/pylib/gyp/generator/ninja.py @@ -1,12 +1,14 @@ -# Copyright (c) 2011 Google Inc. All rights reserved. +# Copyright (c) 2012 Google Inc. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. +import copy import gyp import gyp.common import gyp.system_test +import gyp.xcode_emulation import os.path -import pprint +import re import subprocess import sys @@ -15,7 +17,7 @@ import gyp.ninja_syntax as ninja_syntax generator_default_variables = { 'EXECUTABLE_PREFIX': '', 'EXECUTABLE_SUFFIX': '', - 'STATIC_LIB_PREFIX': '', + 'STATIC_LIB_PREFIX': 'lib', 'STATIC_LIB_SUFFIX': '.a', 'SHARED_LIB_PREFIX': 'lib', @@ -30,8 +32,6 @@ generator_default_variables = { 'INTERMEDIATE_DIR': '$!INTERMEDIATE_DIR', 'SHARED_INTERMEDIATE_DIR': '$!PRODUCT_DIR/gen', 'PRODUCT_DIR': '$!PRODUCT_DIR', - 'SHARED_LIB_DIR': '$!PRODUCT_DIR/lib', - 'LIB_DIR': '', # Special variables that may be used by gyp 'rule' targets. # We generate definitions for these variables on the fly when processing a @@ -57,6 +57,12 @@ def StripPrefix(arg, prefix): def QuoteShellArgument(arg): + """Quote a string such that it will be interpreted as a single argument + by the shell.""" + # Rather than attempting to enumerate the bad shell characters, just + # whitelist common OK ones and quote anything else. + if re.match(r'^[a-zA-Z0-9_=-]+$', arg): + return arg # No quoting necessary. return "'" + arg.replace("'", "'" + '"\'"' + "'") + "'" @@ -71,8 +77,68 @@ def InvertRelativePath(path): return path # Only need to handle relative paths into subdirectories for now. assert '..' not in path, path - depth = len(path.split('/')) - return '/'.join(['..'] * depth) + depth = len(path.split(os.path.sep)) + return os.path.sep.join(['..'] * depth) + + +class Target: + """Target represents the paths used within a single gyp target. + + Conceptually, building a single target A is a series of steps: + + 1) actions/rules/copies generates source/resources/etc. + 2) compiles generates .o files + 3) link generates a binary (library/executable) + 4) bundle merges the above in a mac bundle + + (Any of these steps can be optional.) + + From a build ordering perspective, a dependent target B could just + depend on the last output of this series of steps. + + But some dependent commands sometimes need to reach inside the box. + For example, when linking B it needs to get the path to the static + library generated by A. + + This object stores those paths. To keep things simple, member + variables only store concrete paths to single files, while methods + compute derived values like "the last output of the target". + """ + def __init__(self, type): + # Gyp type ("static_library", etc.) of this target. + self.type = type + # File representing whether any input dependencies necessary for + # dependent actions have completed. + self.preaction_stamp = None + # File representing whether any input dependencies necessary for + # dependent compiles have completed. + self.precompile_stamp = None + # File representing the completion of actions/rules/copies, if any. + self.actions_stamp = None + # Path to the output of the link step, if any. + self.binary = None + # Path to the file representing the completion of building the bundle, + # if any. + self.bundle = None + + def Linkable(self): + """Return true if this is a target that can be linked against.""" + return self.type in ('static_library', 'shared_library') + + def PreActionInput(self): + """Return the path, if any, that should be used as a dependency of + any dependent action step.""" + return self.FinalOutput() or self.preaction_stamp + + def PreCompileInput(self): + """Return the path, if any, that should be used as a dependency of + any dependent compile step.""" + return self.actions_stamp or self.precompile_stamp + + def FinalOutput(self): + """Return the last output of the target, which depends on all prior + steps.""" + return self.bundle or self.binary or self.actions_stamp # A small discourse on paths as used within the Ninja build: @@ -100,11 +166,13 @@ def InvertRelativePath(path): # to the input file name as well as the output target name. class NinjaWriter: - def __init__(self, target_outputs, base_dir, build_dir, output_file, flavor): + def __init__(self, target_outputs, base_dir, build_dir, output_file, flavor, + abs_build_dir=None): """ base_dir: path from source root to directory containing this gyp file, by gyp semantics, all input paths are relative to this build_dir: path from source root to build output + abs_build_dir: absolute path to the build directory """ self.target_outputs = target_outputs @@ -112,6 +180,8 @@ class NinjaWriter: self.build_dir = build_dir self.ninja = ninja_syntax.Writer(output_file) self.flavor = flavor + self.abs_build_dir = abs_build_dir + self.obj_ext = '.obj' if flavor == 'win' else '.o' # Relative path from build output dir to base dir. self.build_to_base = os.path.join(InvertRelativePath(build_dir), base_dir) @@ -132,6 +202,7 @@ class NinjaWriter: path = path.replace(PRODUCT_DIR, product_dir) else: path = path.replace(PRODUCT_DIR + '/', '') + path = path.replace(PRODUCT_DIR + '\\', '') path = path.replace(PRODUCT_DIR, '.') INTERMEDIATE_DIR = '$!INTERMEDIATE_DIR' @@ -142,7 +213,7 @@ class NinjaWriter: path = path.replace(INTERMEDIATE_DIR, os.path.join(product_dir or '', int_dir)) - return path + return os.path.normpath(path) def ExpandRuleVariables(self, path, root, dirname, source, ext, name): path = path.replace(generator_default_variables['RULE_INPUT_ROOT'], root) @@ -153,10 +224,13 @@ class NinjaWriter: path = path.replace(generator_default_variables['RULE_INPUT_NAME'], name) return path - def GypPathToNinja(self, path): - """Translate a gyp path to a ninja path. + def GypPathToNinja(self, path, env=None): + """Translate a gyp path to a ninja path, optionally expanding environment + variable references in |path| with |env|. See the above discourse on path conversions.""" + if env: + path = gyp.xcode_emulation.ExpandEnvVars(path, env) if path.startswith('$!'): return self.ExpandSpecial(path) assert '$' not in path, path @@ -196,34 +270,38 @@ class NinjaWriter: path_basename)) def WriteCollapsedDependencies(self, name, targets): - """Given a list of targets, return a dependency list for a single - file representing the result of building all the targets. + """Given a list of targets, return a path for a single file + representing the result of building all the targets or None. Uses a stamp file if necessary.""" + assert targets == filter(None, targets), targets + if len(targets) == 0: + return None if len(targets) > 1: stamp = self.GypPathToUniqueOutput(name + '.stamp') targets = self.ninja.build(stamp, 'stamp', targets) self.ninja.newline() - return targets + return targets[0] - def WriteSpec(self, spec, config): + def WriteSpec(self, spec, config_name): """The main entry point for NinjaWriter: write the build rules for a spec. - Returns the path to the build output, or None, and a list of targets for - dependencies of its compile steps.""" + Returns a Target object, which represents the output paths for this spec. + Returns None if there are no outputs (e.g. a settings-only 'none' type + target).""" + self.config_name = config_name self.name = spec['target_name'] self.toolset = spec['toolset'] + config = spec['configurations'][config_name] + self.target = Target(spec['type']) - if spec['type'] == 'settings': - # TODO: 'settings' is not actually part of gyp; it was - # accidentally introduced somehow into just the Linux build files. - # Remove this (or make it an error) once all the users are fixed. - print ("WARNING: %s uses invalid type 'settings'. " % self.name + - "Please fix the source gyp file to use type 'none'.") - print "See http://code.google.com/p/chromium/issues/detail?id=96629 ." - spec['type'] = 'none' + self.is_mac_bundle = gyp.xcode_emulation.IsMacBundle(self.flavor, spec) + if self.flavor == 'mac': + self.xcode_settings = gyp.xcode_emulation.XcodeSettings(spec) + else: + self.xcode_settings = None # Compute predepends for all rules. # actions_depends is the dependencies this target depends on before running @@ -232,66 +310,99 @@ class NinjaWriter: # any of its compile steps. actions_depends = [] compile_depends = [] + # TODO(evan): it is rather confusing which things are lists and which + # are strings. Fix these. if 'dependencies' in spec: for dep in spec['dependencies']: if dep in self.target_outputs: - input, precompile_input, linkable = self.target_outputs[dep] - actions_depends.append(input) - compile_depends.extend(precompile_input) + target = self.target_outputs[dep] + actions_depends.append(target.PreActionInput()) + compile_depends.append(target.PreCompileInput()) + actions_depends = filter(None, actions_depends) + compile_depends = filter(None, compile_depends) actions_depends = self.WriteCollapsedDependencies('actions_depends', actions_depends) + compile_depends = self.WriteCollapsedDependencies('compile_depends', + compile_depends) + self.target.preaction_stamp = actions_depends + self.target.precompile_stamp = compile_depends # Write out actions, rules, and copies. These must happen before we # compile any sources, so compute a list of predependencies for sources # while we do it. extra_sources = [] - sources_depends = self.WriteActionsRulesCopies(spec, extra_sources, - actions_depends) + mac_bundle_depends = [] + self.target.actions_stamp = self.WriteActionsRulesCopies( + spec, extra_sources, actions_depends, mac_bundle_depends) # If we have actions/rules/copies, we depend directly on those, but # otherwise we depend on dependent target's actions/rules/copies etc. # We never need to explicitly depend on previous target's link steps, # because no compile ever depends on them. - compile_depends = self.WriteCollapsedDependencies('compile_depends', - sources_depends or compile_depends) + compile_depends_stamp = (self.target.actions_stamp or compile_depends) # Write out the compilation steps, if any. link_deps = [] sources = spec.get('sources', []) + extra_sources if sources: - link_deps = self.WriteSources(config, sources, compile_depends) + link_deps = self.WriteSources( + config_name, config, sources, compile_depends_stamp, + gyp.xcode_emulation.MacPrefixHeader( + self.xcode_settings, self.GypPathToNinja, + lambda path, lang: self.GypPathToUniqueOutput(path + '-' + lang))) # Some actions/rules output 'sources' that are already object files. - link_deps += [self.GypPathToNinja(f) for f in sources if f.endswith('.o')] + link_deps += [self.GypPathToNinja(f) + for f in sources if f.endswith(self.obj_ext)] - # The final output of our target depends on the last output of the - # above steps. + # Write out a link step, if needed. output = None - final_deps = link_deps or sources_depends or actions_depends - if final_deps: - output = self.WriteTarget(spec, config, final_deps, - order_only=actions_depends) - if self.name != output and self.toolset == 'target': - # Write a short name to build this target. This benefits both the - # "build chrome" case as well as the gyp tests, which expect to be - # able to run actions and build libraries by their short name. - self.ninja.build(self.name, 'phony', output) - return output, compile_depends - - def WriteActionsRulesCopies(self, spec, extra_sources, prebuild): - """Write out the Actions, Rules, and Copies steps. Return any outputs - of these steps (or a stamp file if there are lots of outputs).""" + if link_deps or self.target.actions_stamp or actions_depends: + output = self.WriteTarget(spec, config_name, config, link_deps, + self.target.actions_stamp or actions_depends) + if self.is_mac_bundle: + mac_bundle_depends.append(output) + + # Bundle all of the above together, if needed. + if self.is_mac_bundle: + output = self.WriteMacBundle(spec, mac_bundle_depends) + + if not output: + return None + + if self.name != output and self.toolset == 'target': + # Write a short name to build this target. This benefits both the + # "build chrome" case as well as the gyp tests, which expect to be + # able to run actions and build libraries by their short name. + self.ninja.build(self.name, 'phony', output) + + assert self.target.FinalOutput(), output + return self.target + + def WriteActionsRulesCopies(self, spec, extra_sources, prebuild, + mac_bundle_depends): + """Write out the Actions, Rules, and Copies steps. Return a path + representing the outputs of these steps.""" outputs = [] + extra_mac_bundle_resources = [] if 'actions' in spec: - outputs += self.WriteActions(spec['actions'], extra_sources, prebuild) + outputs += self.WriteActions(spec['actions'], extra_sources, prebuild, + extra_mac_bundle_resources) if 'rules' in spec: - outputs += self.WriteRules(spec['rules'], extra_sources, prebuild) + outputs += self.WriteRules(spec['rules'], extra_sources, prebuild, + extra_mac_bundle_resources) if 'copies' in spec: outputs += self.WriteCopies(spec['copies'], prebuild) - outputs = self.WriteCollapsedDependencies('actions_rules_copies', outputs) + stamp = self.WriteCollapsedDependencies('actions_rules_copies', outputs) - return outputs + if self.is_mac_bundle: + mac_bundle_resources = spec.get('mac_bundle_resources', []) + \ + extra_mac_bundle_resources + self.WriteMacBundleResources(mac_bundle_resources, mac_bundle_depends) + self.WriteMacInfoPlist(mac_bundle_depends) + + return stamp def GenerateDescription(self, verb, message, fallback): """Generate and return a description of a build step. @@ -307,20 +418,26 @@ class NinjaWriter: else: return '%s %s: %s' % (verb, self.name, fallback) - def WriteActions(self, actions, extra_sources, prebuild): + def WriteActions(self, actions, extra_sources, prebuild, + extra_mac_bundle_resources): + # Actions cd into the base directory. + env = self.GetXcodeEnv() all_outputs = [] for action in actions: # First write out a rule for the action. - name = action['action_name'] + name = re.sub(r'[ {}$]', '_', action['action_name']) description = self.GenerateDescription('ACTION', action.get('message', None), name) - rule_name = self.WriteNewNinjaRule(name, action['action'], description) + rule_name = self.WriteNewNinjaRule(name, action['action'], description, + env=env) - inputs = [self.GypPathToNinja(i) for i in action['inputs']] + inputs = [self.GypPathToNinja(i, env) for i in action['inputs']] if int(action.get('process_outputs_as_sources', False)): extra_sources += action['outputs'] - outputs = [self.GypPathToNinja(o) for o in action['outputs']] + if int(action.get('process_outputs_as_mac_bundle_resources', False)): + extra_mac_bundle_resources += action['outputs'] + outputs = [self.GypPathToNinja(o, env) for o in action['outputs']] # Then write out an edge using the rule. self.ninja.build(outputs, rule_name, inputs, @@ -331,7 +448,8 @@ class NinjaWriter: return all_outputs - def WriteRules(self, rules, extra_sources, prebuild): + def WriteRules(self, rules, extra_sources, prebuild, + extra_mac_bundle_resources): all_outputs = [] for rule in rules: # First write out a rule for the rule action. @@ -369,6 +487,8 @@ class NinjaWriter: if int(rule.get('process_outputs_as_sources', False)): extra_sources += outputs + if int(rule.get('process_outputs_as_mac_bundle_resources', False)): + extra_mac_bundle_resources += outputs extra_bindings = [] for var in needed_variables: @@ -402,31 +522,67 @@ class NinjaWriter: def WriteCopies(self, copies, prebuild): outputs = [] + env = self.GetXcodeEnv() for copy in copies: for path in copy['files']: # Normalize the path so trailing slashes don't confuse us. path = os.path.normpath(path) basename = os.path.split(path)[1] - src = self.GypPathToNinja(path) - dst = self.GypPathToNinja(os.path.join(copy['destination'], basename)) - outputs += self.ninja.build(dst, 'copy', src, - order_only=prebuild) + src = self.GypPathToNinja(path, env) + dst = self.GypPathToNinja(os.path.join(copy['destination'], basename), + env) + outputs += self.ninja.build(dst, 'copy', src, order_only=prebuild) return outputs - def WriteSources(self, config, sources, predepends): + def WriteMacBundleResources(self, resources, bundle_depends): + """Writes ninja edges for 'mac_bundle_resources'.""" + for output, res in gyp.xcode_emulation.GetMacBundleResources( + self.ExpandSpecial(generator_default_variables['PRODUCT_DIR']), + self.xcode_settings, map(self.GypPathToNinja, resources)): + self.ninja.build(output, 'mac_tool', res, + variables=[('mactool_cmd', 'copy-bundle-resource')]) + bundle_depends.append(output) + + def WriteMacInfoPlist(self, bundle_depends): + """Write build rules for bundle Info.plist files.""" + info_plist, out, defines, extra_env = gyp.xcode_emulation.GetMacInfoPlist( + self.ExpandSpecial(generator_default_variables['PRODUCT_DIR']), + self.xcode_settings, self.GypPathToNinja) + if not info_plist: + return + if defines: + # Create an intermediate file to store preprocessed results. + intermediate_plist = self.GypPathToUniqueOutput( + os.path.basename(info_plist)) + defines = ' '.join( + [QuoteShellArgument(ninja_syntax.escape('-D' + d)) for d in defines]) + info_plist = self.ninja.build(intermediate_plist, 'infoplist', info_plist, + variables=[('defines',defines)]) + + env = self.GetXcodeEnv(additional_settings=extra_env) + env = self.ComputeExportEnvString(env) + + self.ninja.build(out, 'mac_tool', info_plist, + variables=[('mactool_cmd', 'copy-info-plist'), + ('env', env)]) + bundle_depends.append(out) + + def WriteSources(self, config_name, config, sources, predepends, + precompiled_header): """Write build rules to compile all of |sources|.""" if self.toolset == 'host': self.ninja.variable('cc', '$cc_host') self.ninja.variable('cxx', '$cxx_host') if self.flavor == 'mac': - # TODO(jeremya/thakis): Extract these from XcodeSettings instead. - cflags = [] - cflags_c = [] - cflags_cc = [] - cflags_objc = [] - cflags_objcc = [] + cflags = self.xcode_settings.GetCflags(config_name) + cflags_c = self.xcode_settings.GetCflagsC(config_name) + cflags_cc = self.xcode_settings.GetCflagsCC(config_name) + cflags_objc = ['$cflags_c'] + \ + self.xcode_settings.GetCflagsObjC(config_name) + cflags_objcc = ['$cflags_cc'] + \ + self.xcode_settings.GetCflagsObjCC(config_name) else: cflags = config.get('cflags', []) cflags_c = config.get('cflags_c', []) @@ -438,14 +594,26 @@ class NinjaWriter: self.WriteVariableList('includes', ['-I' + self.GypPathToNinja(i) for i in config.get('include_dirs', [])]) + + pch_commands = precompiled_header.GetGchBuildCommands() + if self.flavor == 'mac': + self.WriteVariableList('cflags_pch_c', + [precompiled_header.GetInclude('c')]) + self.WriteVariableList('cflags_pch_cc', + [precompiled_header.GetInclude('cc')]) + self.WriteVariableList('cflags_pch_objc', + [precompiled_header.GetInclude('m')]) + self.WriteVariableList('cflags_pch_objcc', + [precompiled_header.GetInclude('mm')]) + self.WriteVariableList('cflags', map(self.ExpandSpecial, cflags)) self.WriteVariableList('cflags_c', map(self.ExpandSpecial, cflags_c)) self.WriteVariableList('cflags_cc', map(self.ExpandSpecial, cflags_cc)) if self.flavor == 'mac': self.WriteVariableList('cflags_objc', map(self.ExpandSpecial, - cflags_objc)) + cflags_objc)) self.WriteVariableList('cflags_objcc', map(self.ExpandSpecial, - cflags_objcc)) + cflags_objcc)) self.ninja.newline() outputs = [] for source in sources: @@ -463,106 +631,232 @@ class NinjaWriter: # TODO: should we assert here on unexpected extensions? continue input = self.GypPathToNinja(source) - output = self.GypPathToUniqueOutput(filename + '.o') + output = self.GypPathToUniqueOutput(filename + self.obj_ext) + implicit = precompiled_header.GetObjDependencies([input], [output]) self.ninja.build(output, command, input, + implicit=[gch for _, _, gch in implicit], order_only=predepends) outputs.append(output) + + self.WritePchTargets(pch_commands) + self.ninja.newline() return outputs - def WriteTarget(self, spec, config, final_deps, order_only): - if spec['type'] == 'none': - # This target doesn't have any explicit final output, but is instead - # used for its effects before the final output (e.g. copies steps). - # Reuse the existing output if it's easy. - if len(final_deps) == 1: - return final_deps[0] - # Otherwise, fall through to writing out a stamp file. + def WritePchTargets(self, pch_commands): + """Writes ninja rules to compile prefix headers.""" + if not pch_commands: + return + + for gch, lang_flag, lang, input in pch_commands: + var_name = { + 'c': 'cflags_pch_c', + 'cc': 'cflags_pch_cc', + 'm': 'cflags_pch_objc', + 'mm': 'cflags_pch_objcc', + }[lang] + + cmd = { 'c': 'cc', 'cc': 'cxx', 'm': 'objc', 'mm': 'objcxx', }.get(lang) + self.ninja.build(gch, cmd, input, variables=[(var_name, lang_flag)]) - output = self.ComputeOutput(spec) - output_uses_linker = spec['type'] in ('executable', 'loadable_module', - 'shared_library') + def WriteLink(self, spec, config_name, config, link_deps): + """Write out a link step. Returns the path to the output.""" + + command = { + 'executable': 'link', + 'loadable_module': 'solink_module', + 'shared_library': 'solink', + }[spec['type']] implicit_deps = set() + if 'dependencies' in spec: # Two kinds of dependencies: # - Linkable dependencies (like a .a or a .so): add them to the link line. # - Non-linkable dependencies (like a rule that generates a file # and writes a stamp file): add them to implicit_deps - if output_uses_linker: - extra_deps = set() - for dep in spec['dependencies']: - input, _, linkable = self.target_outputs.get(dep, (None, [], False)) - if not input: - continue - if linkable: - extra_deps.add(input) - else: - # TODO: Chrome-specific HACK. Chrome runs this lastchange rule on - # every build, but we don't want to rebuild when it runs. - if 'lastchange' not in input: - implicit_deps.add(input) - final_deps.extend(list(extra_deps)) - command_map = { - 'executable': 'link', - 'static_library': 'alink', - 'loadable_module': 'solink_module', - 'shared_library': 'solink', - 'none': 'stamp', - } - command = command_map[spec['type']] - - if output_uses_linker: - if self.flavor == 'mac': - # TODO(jeremya/thakis): Get this from XcodeSettings. - ldflags = [] - else: - ldflags = config.get('ldflags', []) - self.WriteVariableList('ldflags', - gyp.common.uniquer(map(self.ExpandSpecial, - ldflags))) - self.WriteVariableList('libs', - gyp.common.uniquer(map(self.ExpandSpecial, - spec.get('libraries', [])))) + extra_link_deps = set() + for dep in spec['dependencies']: + target = self.target_outputs.get(dep) + if not target: + continue + linkable = target.Linkable() + if linkable: + extra_link_deps.add(target.binary) + + final_output = target.FinalOutput() + if not linkable or final_output != target.binary: + implicit_deps.add(final_output) + + link_deps.extend(list(extra_link_deps)) extra_bindings = [] + if self.is_mac_bundle: + output = self.ComputeMacBundleBinaryOutput() + else: + output = self.ComputeOutput(spec) + extra_bindings.append(('postbuilds', + self.GetPostbuildCommand(spec, output, output))) + + if self.flavor == 'mac': + ldflags = self.xcode_settings.GetLdflags(config_name, + self.ExpandSpecial(generator_default_variables['PRODUCT_DIR']), + self.GypPathToNinja) + else: + ldflags = config.get('ldflags', []) + self.WriteVariableList('ldflags', + gyp.common.uniquer(map(self.ExpandSpecial, + ldflags))) + + libraries = gyp.common.uniquer(map(self.ExpandSpecial, + spec.get('libraries', []))) + if self.flavor == 'mac': + libraries = self.xcode_settings.AdjustLibraries(libraries) + self.WriteVariableList('libs', libraries) + if command in ('solink', 'solink_module'): extra_bindings.append(('soname', os.path.split(output)[1])) - self.ninja.build(output, command, final_deps, + self.ninja.build(output, command, link_deps, implicit=list(implicit_deps), - order_only=order_only, variables=extra_bindings) + return output + def WriteTarget(self, spec, config_name, config, link_deps, compile_deps): + if spec['type'] == 'none': + # TODO(evan): don't call this function for 'none' target types, as + # it doesn't do anything, and we fake out a 'binary' with a stamp file. + self.target.binary = compile_deps + elif spec['type'] == 'static_library': + self.target.binary = self.ComputeOutput(spec) + self.ninja.build(self.target.binary, 'alink', link_deps, + order_only=compile_deps, + variables=[('postbuilds', self.GetPostbuildCommand( + spec, self.target.binary, self.target.binary))]) + else: + self.target.binary = self.WriteLink(spec, config_name, config, link_deps) + return self.target.binary + + def WriteMacBundle(self, spec, mac_bundle_depends): + assert self.is_mac_bundle + package_framework = spec['type'] in ('shared_library', 'loadable_module') + output = self.ComputeMacBundleOutput() + postbuild = self.GetPostbuildCommand(spec, output, self.target.binary, + is_command_start=not package_framework) + variables = [] + if postbuild: + variables.append(('postbuilds', postbuild)) + if package_framework: + variables.append(('version', self.xcode_settings.GetFrameworkVersion())) + self.ninja.build(output, 'package_framework', mac_bundle_depends, + variables=variables) + else: + self.ninja.build(output, 'stamp', mac_bundle_depends, + variables=variables) + self.target.bundle = output return output - def ComputeOutputFileName(self, spec): + def GetXcodeEnv(self, additional_settings=None): + """Returns the variables Xcode would set for build steps.""" + assert self.abs_build_dir + abs_build_dir = self.abs_build_dir + return gyp.xcode_emulation.GetXcodeEnv( + self.xcode_settings, abs_build_dir, + os.path.join(abs_build_dir, self.build_to_base), self.config_name, + additional_settings) + + def GetXcodePostbuildEnv(self): + """Returns the variables Xcode would set for postbuild steps.""" + postbuild_settings = {} + # CHROMIUM_STRIP_SAVE_FILE is a chromium-specific hack. + # TODO(thakis): It would be nice to have some general mechanism instead. + strip_save_file = self.xcode_settings.GetPerTargetSetting( + 'CHROMIUM_STRIP_SAVE_FILE') + if strip_save_file: + postbuild_settings['CHROMIUM_STRIP_SAVE_FILE'] = self.GypPathToNinja( + strip_save_file) + return self.GetXcodeEnv(additional_settings=postbuild_settings) + + def GetPostbuildCommand(self, spec, output, output_binary, + is_command_start=False): + """Returns a shell command that runs all the postbuilds, and removes + |output| if any of them fails. If |is_command_start| is False, then the + returned string will start with ' && '.""" + if not self.xcode_settings or spec['type'] == 'none' or not output: + return '' + output = QuoteShellArgument(output) + target_postbuilds = self.xcode_settings.GetTargetPostbuilds( + self.config_name, output, QuoteShellArgument(output_binary), quiet=True) + postbuilds = gyp.xcode_emulation.GetSpecPostbuildCommands( + spec, self.GypPathToNinja, quiet=True) + postbuilds = target_postbuilds + postbuilds + if not postbuilds: + return '' + env = self.ComputeExportEnvString(self.GetXcodePostbuildEnv()) + commands = env + ' F=0; ' + \ + ' '.join([ninja_syntax.escape(command) + ' || F=$$?;' + for command in postbuilds]) + command_string = env + commands + ' ((exit $$F) || rm -rf %s) ' % output + \ + '&& exit $$F)' + if is_command_start: + return '(' + command_string + ' && ' + else: + return '$ && (' + command_string + + def ComputeExportEnvString(self, env): + """Given an environment, returns a string looking like + 'export FOO=foo; export BAR="${FOO} bar;' + that exports |env| to the shell.""" + export_str = [] + for k in gyp.xcode_emulation.TopologicallySortedEnvVarKeys(env): + export_str.append('export %s=%s;' % + (k, ninja_syntax.escape(gyp.common.EncodePOSIXShellArgument(env[k])))) + return ' '.join(export_str) + + def ComputeMacBundleOutput(self): + """Return the 'output' (full output path) to a bundle output directory.""" + assert self.is_mac_bundle + path = self.ExpandSpecial(generator_default_variables['PRODUCT_DIR']) + return os.path.join(path, self.xcode_settings.GetWrapperName()) + + def ComputeMacBundleBinaryOutput(self): + """Return the 'output' (full output path) to the binary in a bundle.""" + assert self.is_mac_bundle + path = self.ExpandSpecial(generator_default_variables['PRODUCT_DIR']) + return os.path.join(path, self.xcode_settings.GetExecutablePath()) + + def ComputeOutputFileName(self, spec, type=None): """Compute the filename of the final output for the current target.""" + if not type: + type = spec['type'] + + default_variables = copy.copy(generator_default_variables) + CalculateVariables(default_variables, {'flavor': self.flavor}) # Compute filename prefix: the product prefix, or a default for # the product type. DEFAULT_PREFIX = { - 'loadable_module': 'lib', - 'shared_library': 'lib', + 'loadable_module': default_variables['SHARED_LIB_PREFIX'], + 'shared_library': default_variables['SHARED_LIB_PREFIX'], + 'static_library': default_variables['STATIC_LIB_PREFIX'], + 'executable': default_variables['EXECUTABLE_PREFIX'], } - prefix = spec.get('product_prefix', DEFAULT_PREFIX.get(spec['type'], '')) + prefix = spec.get('product_prefix', DEFAULT_PREFIX.get(type, '')) # Compute filename extension: the product extension, or a default # for the product type. DEFAULT_EXTENSION = { - 'static_library': 'a', - 'loadable_module': 'so', - 'shared_library': 'so', + 'loadable_module': default_variables['SHARED_LIB_SUFFIX'], + 'shared_library': default_variables['SHARED_LIB_SUFFIX'], + 'static_library': default_variables['STATIC_LIB_SUFFIX'], + 'executable': default_variables['EXECUTABLE_SUFFIX'], } - # TODO(thakis/jeremya): Remove once the mac path name computation is done - # by XcodeSettings. - if self.flavor == 'mac': - DEFAULT_EXTENSION['shared_library'] = 'dylib' - extension = spec.get('product_extension', - DEFAULT_EXTENSION.get(spec['type'], '')) + extension = spec.get('product_extension') if extension: extension = '.' + extension + else: + extension = DEFAULT_EXTENSION.get(type, '') if 'product_name' in spec: # If we were given an explicit name, use that. @@ -574,37 +868,44 @@ class NinjaWriter: # Snip out an extra 'lib' from libs if appropriate. target = StripPrefix(target, 'lib') - if spec['type'] in ('static_library', 'loadable_module', 'shared_library', + if type in ('static_library', 'loadable_module', 'shared_library', 'executable'): return '%s%s%s' % (prefix, target, extension) - elif spec['type'] == 'none': + elif type == 'none': return '%s.stamp' % target else: - raise 'Unhandled output type', spec['type'] + raise 'Unhandled output type', type - def ComputeOutput(self, spec): + def ComputeOutput(self, spec, type=None): """Compute the path for the final output of the spec.""" + assert not self.is_mac_bundle or type - filename = self.ComputeOutputFileName(spec) + if not type: + type = spec['type'] + + if self.flavor == 'mac' and type in ( + 'static_library', 'executable', 'shared_library', 'loadable_module'): + filename = self.xcode_settings.GetExecutablePath() + else: + filename = self.ComputeOutputFileName(spec, type) if 'product_dir' in spec: path = os.path.join(spec['product_dir'], filename) return self.ExpandSpecial(path) - # Executables and loadable modules go into the output root, - # libraries go into shared library dir, and everything else - # goes into the normal place. - if spec['type'] in ('executable', 'loadable_module'): + # Some products go into the output root, libraries go into shared library + # dir, and everything else goes into the normal place. + type_in_output_root = ['executable', 'loadable_module'] + if self.flavor == 'mac' and self.toolset == 'target': + type_in_output_root += ['shared_library', 'static_library'] + + if type in type_in_output_root: return filename - elif spec['type'] == 'shared_library': + elif type == 'shared_library': libdir = 'lib' if self.toolset != 'target': - libdir = 'lib/%s' % self.toolset + libdir = os.path.join('lib', '%s' % self.toolset) return os.path.join(libdir, filename) - # TODO(thakis/jeremya): Remove once the mac path name computation is done - # by XcodeSettings. - elif spec['type'] == 'static_library' and self.flavor == 'mac': - return filename else: return self.GypPathToUniqueOutput(filename, qualified=False) @@ -613,7 +914,7 @@ class NinjaWriter: values = [] self.ninja.variable(var, ' '.join(values)) - def WriteNewNinjaRule(self, name, args, description): + def WriteNewNinjaRule(self, name, args, description, env={}): """Write out a new ninja "rule" statement for a given command. Returns the name of the new rule.""" @@ -632,11 +933,29 @@ class NinjaWriter: # gyp dictates that commands are run from the base directory. # cd into the directory before running, and adjust paths in # the arguments to point to the proper locations. - cd = 'cd %s; ' % self.build_to_base + if self.flavor == 'win': + cd = 'cmd /s /c "cd %s && ' % self.build_to_base + else: + cd = 'cd %s; ' % self.build_to_base args = [self.ExpandSpecial(arg, self.base_to_build) for arg in args] - - command = cd + gyp.common.EncodePOSIXShellList(args) - self.ninja.rule(rule_name, command, description) + env = self.ComputeExportEnvString(env) + if self.flavor == 'win': + # TODO(scottmg): Really don't want encourage cygwin, but I'm not sure + # how much sh is depended upon. For now, double quote args to make most + # things work. + command = args[0] + ' "' + '" "'.join(args[1:]) + '""' + else: + command = gyp.common.EncodePOSIXShellList(args) + if env: + # If an environment is passed in, variables in the command should be + # read from it, instead of from ninja's internal variables. + command = ninja_syntax.escape(command) + + command = cd + env + command + # GYP rules/actions express being no-ops by not touching their outputs. + # Avoid executing downstream dependencies in this case by specifying + # restat=1 to ninja. + self.ninja.rule(rule_name, command, description, restat=True) self.ninja.newline() return rule_name @@ -645,15 +964,14 @@ class NinjaWriter: def CalculateVariables(default_variables, params): """Calculate additional variables for use in the build (called by gyp).""" cc_target = os.environ.get('CC.target', os.environ.get('CC', 'cc')) - default_variables['LINKER_SUPPORTS_ICF'] = \ - gyp.system_test.TestLinkerSupportsICF(cc_command=cc_target) - flavor = gyp.common.GetFlavor(params) if flavor == 'mac': default_variables.setdefault('OS', 'mac') default_variables.setdefault('SHARED_LIB_SUFFIX', '.dylib') - - # TODO(jeremya/thakis): Set SHARED_LIB_DIR / LIB_DIR. + default_variables.setdefault('SHARED_LIB_DIR', + generator_default_variables['PRODUCT_DIR']) + default_variables.setdefault('LIB_DIR', + generator_default_variables['PRODUCT_DIR']) # Copy additional generator configuration data from Xcode, which is shared # by the Mac Ninja generator. @@ -667,12 +985,22 @@ def CalculateVariables(default_variables, params): global generator_extra_sources_for_rules generator_extra_sources_for_rules = getattr(xcode_generator, 'generator_extra_sources_for_rules', []) + elif flavor == 'win': + default_variables['OS'] = 'win' + default_variables['EXECUTABLE_SUFFIX'] = '.exe' + default_variables['STATIC_LIB_PREFIX'] = '' + default_variables['STATIC_LIB_SUFFIX'] = '.lib' + default_variables['SHARED_LIB_PREFIX'] = '' + default_variables['SHARED_LIB_SUFFIX'] = '.dll' else: operating_system = flavor if flavor == 'android': operating_system = 'linux' # Keep this legacy behavior for now. default_variables.setdefault('OS', operating_system) default_variables.setdefault('SHARED_LIB_SUFFIX', '.so') + default_variables.setdefault('SHARED_LIB_DIR', + os.path.join('$!PRODUCT_DIR', 'lib')) + default_variables.setdefault('LIB_DIR', '') def OpenOutput(path): @@ -684,55 +1012,85 @@ def OpenOutput(path): return open(path, 'w') -def GenerateOutput(target_list, target_dicts, data, params): +def GenerateOutputForConfig(target_list, target_dicts, data, params, + config_name): options = params['options'] flavor = gyp.common.GetFlavor(params) generator_flags = params.get('generator_flags', {}) - if options.generator_output: - raise NotImplementedError, "--generator_output not implemented for ninja" - - config_name = generator_flags.get('config', None) - if config_name is None: - # Guess which config we want to use: pick the first one from the - # first target. - config_name = target_dicts[target_list[0]]['default_configuration'] - - # builddir: relative path from source root to our output files. + # build_dir: relative path from source root to our output files. # e.g. "out/Debug" - builddir = os.path.join(generator_flags.get('output_dir', 'out'), config_name) + build_dir = os.path.join(generator_flags.get('output_dir', 'out'), + config_name) master_ninja = ninja_syntax.Writer( - OpenOutput(os.path.join(options.toplevel_dir, builddir, 'build.ninja')), + OpenOutput(os.path.join(options.toplevel_dir, build_dir, 'build.ninja')), width=120) - # TODO: compute cc/cxx/ld/etc. by command-line arguments and system tests. - master_ninja.variable('cc', os.environ.get('CC', 'gcc')) - master_ninja.variable('cxx', os.environ.get('CXX', 'g++')) - # TODO(bradnelson): remove NOGOLD when this is resolved: - # http://code.google.com/p/chromium/issues/detail?id=108251 - if flavor != 'mac' and not os.environ.get('NOGOLD'): - master_ninja.variable('ld', '$cxx -Wl,--threads -Wl,--thread-count=4') + # Put build-time support tools in out/{config_name}. + gyp.common.CopyTool(flavor, os.path.join(options.toplevel_dir, build_dir)) + + # Grab make settings for CC/CXX. + if flavor == 'win': + cc = cxx = 'cl' else: - # TODO(jeremya/thakis): flock - master_ninja.variable('ld', '$cxx') + cc, cxx = 'gcc', 'g++' + build_file, _, _ = gyp.common.ParseQualifiedTarget(target_list[0]) + make_global_settings = data[build_file].get('make_global_settings', []) + build_to_root = InvertRelativePath(build_dir) + for key, value in make_global_settings: + if key == 'CC': cc = os.path.join(build_to_root, value) + if key == 'CXX': cxx = os.path.join(build_to_root, value) + + flock = 'flock' + if flavor == 'mac': + flock = './gyp-mac-tool flock' + master_ninja.variable('cc', os.environ.get('CC', cc)) + master_ninja.variable('cxx', os.environ.get('CXX', cxx)) + if flavor == 'win': + master_ninja.variable('ld', 'link') + else: + master_ninja.variable('ld', flock + ' linker.lock $cxx') master_ninja.variable('cc_host', '$cc') master_ninja.variable('cxx_host', '$cxx') + if flavor == 'mac': + master_ninja.variable('mac_tool', os.path.join('.', 'gyp-mac-tool')) master_ninja.newline() - master_ninja.rule( - 'cc', - description='CC $out', - command=('$cc -MMD -MF $out.d $defines $includes $cflags $cflags_c ' - '-c $in -o $out'), - depfile='$out.d') - master_ninja.rule( - 'cxx', - description='CXX $out', - command=('$cxx -MMD -MF $out.d $defines $includes $cflags $cflags_cc ' - '-c $in -o $out'), - depfile='$out.d') - if flavor != 'mac': + if flavor != 'win': + master_ninja.rule( + 'cc', + description='CC $out', + command=('$cc -MMD -MF $out.d $defines $includes $cflags $cflags_c ' + '$cflags_pch_c -c $in -o $out'), + depfile='$out.d') + master_ninja.rule( + 'cxx', + description='CXX $out', + command=('$cxx -MMD -MF $out.d $defines $includes $cflags $cflags_cc ' + '$cflags_pch_cc -c $in -o $out'), + depfile='$out.d') + else: + # TODO(scottmg): Requires deplist branch of ninja for now (for + # /showIncludes handling). + master_ninja.rule( + 'cc', + description='CC $out', + command=('cmd /c $cc /nologo /showIncludes ' + '$defines $includes $cflags $cflags_c ' + '$cflags_pch_c /c $in /Fo$out ' + '| ninja-deplist-helper -f cl -o $out.dl'), + deplist='$out.dl') + master_ninja.rule( + 'cxx', + description='CXX $out', + command=('cmd /c $cxx /nologo /showIncludes ' + '$defines $includes $cflags $cflags_cc ' + '$cflags_pch_cc /c $in /Fo$out ' + '| ninja-deplist-helper -f cl -o $out.dl'), + deplist='$out.dl') + + if flavor != 'mac' and flavor != 'win': master_ninja.rule( 'alink', description='AR $out', @@ -752,48 +1110,88 @@ def GenerateOutput(target_list, target_dicts, data, params): description='LINK $out', command=('$ld $ldflags -o $out -Wl,-rpath=\$$ORIGIN/lib ' '-Wl,--start-group $in -Wl,--end-group $libs')) + elif flavor == 'win': + master_ninja.rule( + 'alink', + description='AR $out', + command='lib /nologo /OUT:$out $in') + master_ninja.rule( + 'solink', + description='SOLINK $out', + command=('$ld /nologo /DLL $ldflags /OUT:$out $in $libs')) + master_ninja.rule( + 'solink_module', + description='SOLINK(module) $out', + command=('$ld /nologo /DLL $ldflags /OUT:$out $in $libs')) + master_ninja.rule( + 'link', + description='LINK $out', + command=('$ld /nologo $ldflags /OUT:$out $in $libs')) else: master_ninja.rule( 'objc', description='OBJC $out', - command=('$cc -MMD -MF $out.d $defines $includes $cflags $cflags_c ' - '$cflags_objc -c $in -o $out'), + command=('$cc -MMD -MF $out.d $defines $includes $cflags $cflags_objc ' + '$cflags_pch_objc -c $in -o $out'), depfile='$out.d') master_ninja.rule( 'objcxx', description='OBJCXX $out', - command=('$cxx -MMD -MF $out.d $defines $includes $cflags $cflags_cc ' - '$cflags_objcc -c $in -o $out'), + command=('$cxx -MMD -MF $out.d $defines $includes $cflags $cflags_objcc ' + '$cflags_pch_objcc -c $in -o $out'), depfile='$out.d') master_ninja.rule( 'alink', - description='LIBTOOL-STATIC $out', - command='rm -f $out && libtool -static -o $out $in') + description='LIBTOOL-STATIC $out, POSTBUILDS', + command='rm -f $out && ' + './gyp-mac-tool filter-libtool libtool -static -o $out $in' + '$postbuilds') # TODO(thakis): The solink_module rule is likely wrong. Xcode seems to pass # -bundle -single_module here (for osmesa.so). master_ninja.rule( 'solink', - description='SOLINK $out', + description='SOLINK $out, POSTBUILDS', command=('$ld -shared $ldflags -o $out ' - '$in $libs')) + '$in $libs$postbuilds')) master_ninja.rule( 'solink_module', - description='SOLINK(module) $out', + description='SOLINK(module) $out, POSTBUILDS', command=('$ld -shared $ldflags -o $out ' - '$in $libs')) + '$in $libs$postbuilds')) master_ninja.rule( 'link', - description='LINK $out', + description='LINK $out, POSTBUILDS', command=('$ld $ldflags -o $out ' - '$in $libs')) + '$in $libs$postbuilds')) + master_ninja.rule( + 'infoplist', + description='INFOPLIST $out', + command=('$cc -E -P -Wno-trigraphs -x c $defines $in -o $out && ' + 'plutil -convert xml1 $out $out')) + master_ninja.rule( + 'mac_tool', + description='MACTOOL $mactool_cmd $in', + command='$env $mac_tool $mactool_cmd $in $out') + master_ninja.rule( + 'package_framework', + description='PACKAGE FRAMEWORK $out, POSTBUILDS', + command='$mac_tool package-framework $out $version$postbuilds ' + '&& touch $out') master_ninja.rule( 'stamp', description='STAMP $out', - command='touch $out') - master_ninja.rule( - 'copy', - description='COPY $in $out', - command='ln -f $in $out 2>/dev/null || (rm -rf $out && cp -af $in $out)') + command='${postbuilds}touch $out') + if flavor == 'win': + # TODO(scottmg): Copy fallback? + master_ninja.rule( + 'copy', + description='COPY $in $out', + command='cmd /c mklink /h $out $in >nul || mklink /h /j $out $in >nul') + else: + master_ninja.rule( + 'copy', + description='COPY $in $out', + command='ln -f $in $out 2>/dev/null || (rm -rf $out && cp -af $in $out)') master_ninja.newline() all_targets = set() @@ -802,38 +1200,57 @@ def GenerateOutput(target_list, target_dicts, data, params): all_targets.add(target) all_outputs = set() + # target_outputs is a map from qualified target name to a Target object. target_outputs = {} for qualified_target in target_list: # qualified_target is like: third_party/icu/icu.gyp:icui18n#target build_file, name, toolset = \ gyp.common.ParseQualifiedTarget(qualified_target) - # TODO: what is options.depth and how is it different than - # options.toplevel_dir? - build_file = gyp.common.RelativePath(build_file, options.depth) + this_make_global_settings = data[build_file].get('make_global_settings', []) + assert make_global_settings == this_make_global_settings, ( + "make_global_settings needs to be the same for all targets.") + + spec = target_dicts[qualified_target] + if flavor == 'mac': + gyp.xcode_emulation.MergeGlobalXcodeSettingsToSpec(data[build_file], spec) + + build_file = gyp.common.RelativePath(build_file, options.toplevel_dir) base_path = os.path.dirname(build_file) obj = 'obj' if toolset != 'target': obj += '.' + toolset output_file = os.path.join(obj, base_path, name + '.ninja') - spec = target_dicts[qualified_target] - config = spec['configurations'][config_name] - writer = NinjaWriter(target_outputs, base_path, builddir, + abs_build_dir=os.path.abspath(os.path.join(options.toplevel_dir, build_dir)) + writer = NinjaWriter(target_outputs, base_path, build_dir, OpenOutput(os.path.join(options.toplevel_dir, - builddir, + build_dir, output_file)), - flavor) + flavor, abs_build_dir=abs_build_dir) master_ninja.subninja(output_file) - output, compile_depends = writer.WriteSpec(spec, config) - if output: - linkable = spec['type'] in ('static_library', 'shared_library') - target_outputs[qualified_target] = (output, compile_depends, linkable) - + target = writer.WriteSpec(spec, config_name) + if target: + target_outputs[qualified_target] = target if qualified_target in all_targets: - all_outputs.add(output) + all_outputs.add(target.FinalOutput()) if all_outputs: master_ninja.build('all', 'phony', list(all_outputs)) + + +def GenerateOutput(target_list, target_dicts, data, params): + if params['options'].generator_output: + raise NotImplementedError, "--generator_output not implemented for ninja" + + 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() + for config_name in config_names: + GenerateOutputForConfig(target_list, target_dicts, data, params, + config_name) |