blob: 6f3b610c9ff5dbf5e17a1967592d16b5fb427fe6 [file] [log] [blame]
# Copyright (c) 2014 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.
"""
This script is intended for use as a GYP_GENERATOR. It takes as input (by way of
the generator flag config_path) the path of a json file that dictates the files
and targets to search for. The following keys are supported:
files: list of paths (relative) of the files to search for.
targets: list of targets to search for. The target names are unqualified.
The following (as JSON) is output:
error: only supplied if there is an error.
targets: the set of targets passed in via targets that either directly or
indirectly depend upon the set of paths supplied in files.
status: indicates if any of the supplied files matched at least one target.
"""
import gyp.common
import gyp.ninja_syntax as ninja_syntax
import json
import os
import posixpath
import sys
debug = False
found_dependency_string = 'Found dependency'
no_dependency_string = 'No dependencies'
# MatchStatus is used indicate if and how a target depends upon the supplied
# sources.
# The target's sources contain one of the supplied paths.
MATCH_STATUS_MATCHES = 1
# The target has a dependency on another target that contains one of the
# supplied paths.
MATCH_STATUS_MATCHES_BY_DEPENDENCY = 2
# The target's sources weren't in the supplied paths and none of the target's
# dependencies depend upon a target that matched.
MATCH_STATUS_DOESNT_MATCH = 3
# The target doesn't contain the source, but the dependent targets have not yet
# been visited to determine a more specific status yet.
MATCH_STATUS_TBD = 4
generator_supports_multiple_toolsets = True
generator_wants_static_library_dependencies_adjusted = False
generator_default_variables = {
}
for dirname in ['INTERMEDIATE_DIR', 'SHARED_INTERMEDIATE_DIR', 'PRODUCT_DIR',
'LIB_DIR', 'SHARED_LIB_DIR']:
generator_default_variables[dirname] = '!!!'
for unused in ['RULE_INPUT_PATH', 'RULE_INPUT_ROOT', 'RULE_INPUT_NAME',
'RULE_INPUT_DIRNAME', 'RULE_INPUT_EXT',
'EXECUTABLE_PREFIX', 'EXECUTABLE_SUFFIX',
'STATIC_LIB_PREFIX', 'STATIC_LIB_SUFFIX',
'SHARED_LIB_PREFIX', 'SHARED_LIB_SUFFIX',
'CONFIGURATION_NAME']:
generator_default_variables[unused] = ''
def __ExtractBasePath(target):
"""Extracts the path components of the specified gyp target path."""
last_index = target.rfind('/')
if last_index == -1:
return ''
return target[0:(last_index + 1)]
def __ResolveParent(path, base_path_components):
"""Resolves |path|, which starts with at least one '../'. Returns an empty
string if the path shouldn't be considered. See __AddSources() for a
description of |base_path_components|."""
depth = 0
while path.startswith('../'):
depth += 1
path = path[3:]
# Relative includes may go outside the source tree. For example, an action may
# have inputs in /usr/include, which are not in the source tree.
if depth > len(base_path_components):
return ''
if depth == len(base_path_components):
return path
return '/'.join(base_path_components[0:len(base_path_components) - depth]) + \
'/' + path
def __AddSources(sources, base_path, base_path_components, result):
"""Extracts valid sources from |sources| and adds them to |result|. Each
source file is relative to |base_path|, but may contain '..'. To make
resolving '..' easier |base_path_components| contains each of the
directories in |base_path|. Additionally each source may contain variables.
Such sources are ignored as it is assumed dependencies on them are expressed
and tracked in some other means."""
# NOTE: gyp paths are always posix style.
for source in sources:
if not len(source) or source.startswith('!!!') or source.startswith('$'):
continue
# variable expansion may lead to //.
org_source = source
source = source[0] + source[1:].replace('//', '/')
if source.startswith('../'):
source = __ResolveParent(source, base_path_components)
if len(source):
result.append(source)
continue
result.append(base_path + source)
if debug:
print 'AddSource', org_source, result[len(result) - 1]
def __ExtractSourcesFromAction(action, base_path, base_path_components,
results):
if 'inputs' in action:
__AddSources(action['inputs'], base_path, base_path_components, results)
def __ExtractSources(target, target_dict, toplevel_dir):
# |target| is either absolute or relative and in the format of the OS. Gyp
# source paths are always posix. Convert |target| to a posix path relative to
# |toplevel_dir_|. This is done to make it easy to build source paths.
if os.sep == '\\' and os.altsep == '/':
base_path = target.replace('\\', '/')
else:
base_path = target
if base_path == toplevel_dir:
base_path = ''
elif base_path.startswith(toplevel_dir + '/'):
base_path = base_path[len(toplevel_dir) + len('/'):]
base_path = posixpath.dirname(base_path)
base_path_components = base_path.split('/')
# Add a trailing '/' so that __AddSources() can easily build paths.
if len(base_path):
base_path += '/'
if debug:
print 'ExtractSources', target, base_path
results = []
if 'sources' in target_dict:
__AddSources(target_dict['sources'], base_path, base_path_components,
results)
# Include the inputs from any actions. Any changes to these effect the
# resulting output.
if 'actions' in target_dict:
for action in target_dict['actions']:
__ExtractSourcesFromAction(action, base_path, base_path_components,
results)
if 'rules' in target_dict:
for rule in target_dict['rules']:
__ExtractSourcesFromAction(rule, base_path, base_path_components, results)
return results
class Target(object):
"""Holds information about a particular target:
deps: set of the names of direct dependent targets.
match_staus: one of the MatchStatus values"""
def __init__(self):
self.deps = set()
self.match_status = MATCH_STATUS_TBD
class Config(object):
"""Details what we're looking for
look_for_dependency_only: if true only search for a target listing any of
the files in files.
files: set of files to search for
targets: see file description for details"""
def __init__(self):
self.look_for_dependency_only = True
self.files = []
self.targets = []
def Init(self, params):
"""Initializes Config. This is a separate method as it may raise an
exception if there is a parse error."""
generator_flags = params.get('generator_flags', {})
# TODO(sky): nuke file_path and look_for_dependency_only once migrate
# recipes.
file_path = generator_flags.get('file_path', None)
if file_path:
self._InitFromFilePath(file_path)
return
# If |file_path| wasn't specified then we look for config_path.
# TODO(sky): always look for config_path once migrated recipes.
config_path = generator_flags.get('config_path', None)
if not config_path:
return
self.look_for_dependency_only = False
try:
f = open(config_path, 'r')
config = json.load(f)
f.close()
except IOError:
raise Exception('Unable to open file ' + config_path)
except ValueError as e:
raise Exception('Unable to parse config file ' + config_path + str(e))
if not isinstance(config, dict):
raise Exception('config_path must be a JSON file containing a dictionary')
self.files = config.get('files', [])
# Coalesce duplicates
self.targets = list(set(config.get('targets', [])))
def _InitFromFilePath(self, file_path):
try:
f = open(file_path, 'r')
for file_name in f:
if file_name.endswith('\n'):
file_name = file_name[0:len(file_name) - 1]
if len(file_name):
self.files.append(file_name)
f.close()
except IOError:
raise Exception('Unable to open file', file_path)
def __GenerateTargets(target_list, target_dicts, toplevel_dir, files):
"""Generates a dictionary with the key the name of a target and the value a
Target. |toplevel_dir| is the root of the source tree. If the sources of
a target match that of |files|, then |target.matched| is set to True.
This returns a tuple of the dictionary and whether at least one target's
sources listed one of the paths in |files|."""
targets = {}
# Queue of targets to visit.
targets_to_visit = target_list[:]
matched = False
while len(targets_to_visit) > 0:
target_name = targets_to_visit.pop()
if target_name in targets:
continue
target = Target()
targets[target_name] = target
sources = __ExtractSources(target_name, target_dicts[target_name],
toplevel_dir)
for source in sources:
if source in files:
target.match_status = MATCH_STATUS_MATCHES
matched = True
break
for dep in target_dicts[target_name].get('dependencies', []):
targets[target_name].deps.add(dep)
targets_to_visit.append(dep)
return targets, matched
def _GetUnqualifiedToQualifiedMapping(all_targets, to_find):
"""Returns a mapping (dictionary) from unqualified name to qualified name for
all the targets in |to_find|."""
result = {}
if not to_find:
return result
to_find = set(to_find)
for target_name in all_targets.keys():
extracted = gyp.common.ParseQualifiedTarget(target_name)
if len(extracted) > 1 and extracted[1] in to_find:
to_find.remove(extracted[1])
result[extracted[1]] = target_name
if not to_find:
return result
return result
def _DoesTargetDependOn(target, all_targets):
"""Returns true if |target| or any of its dependencies matches the supplied
set of paths. This updates |matches| of the Targets as it recurses.
target: the Target to look for.
all_targets: mapping from target name to Target.
matching_targets: set of targets looking for."""
if target.match_status == MATCH_STATUS_DOESNT_MATCH:
return False
if target.match_status == MATCH_STATUS_MATCHES or \
target.match_status == MATCH_STATUS_MATCHES_BY_DEPENDENCY:
return True
for dep_name in target.deps:
dep_target = all_targets[dep_name]
if _DoesTargetDependOn(dep_target, all_targets):
dep_target.match_status = MATCH_STATUS_MATCHES_BY_DEPENDENCY
return True
dep_target.match_status = MATCH_STATUS_DOESNT_MATCH
return False
def _GetTargetsDependingOn(all_targets, possible_targets):
"""Returns the list of targets in |possible_targets| that depend (either
directly on indirectly) on the matched files.
all_targets: mapping from target name to Target.
possible_targets: targets to search from."""
found = []
for target in possible_targets:
if _DoesTargetDependOn(all_targets[target], all_targets):
# possible_targets was initially unqualified, keep it unqualified.
found.append(gyp.common.ParseQualifiedTarget(target)[1])
return found
def CalculateVariables(default_variables, params):
"""Calculate additional variables for use in the build (called by gyp)."""
flavor = gyp.common.GetFlavor(params)
if flavor == 'mac':
default_variables.setdefault('OS', 'mac')
elif flavor == 'win':
default_variables.setdefault('OS', 'win')
# Copy additional generator configuration data from VS, which is shared
# by the Windows Ninja generator.
import gyp.generator.msvs as msvs_generator
generator_additional_non_configuration_keys = getattr(msvs_generator,
'generator_additional_non_configuration_keys', [])
generator_additional_path_sections = getattr(msvs_generator,
'generator_additional_path_sections', [])
gyp.msvs_emulation.CalculateCommonVariables(default_variables, params)
else:
operating_system = flavor
if flavor == 'android':
operating_system = 'linux' # Keep this legacy behavior for now.
default_variables.setdefault('OS', operating_system)
def GenerateOutput(target_list, target_dicts, data, params):
"""Called by gyp as the final stage. Outputs results."""
config = Config()
try:
config.Init(params)
if not config.files:
if config.look_for_dependency_only:
print 'Must specify files to analyze via file_path generator flag'
return
raise Exception('Must specify files to analyze via config_path generator '
'flag')
toplevel_dir = os.path.abspath(params['options'].toplevel_dir)
if os.sep == '\\' and os.altsep == '/':
toplevel_dir = toplevel_dir.replace('\\', '/')
if debug:
print 'toplevel_dir', toplevel_dir
all_targets, matched = __GenerateTargets(target_list, target_dicts,
toplevel_dir,
frozenset(config.files))
# Set of targets that refer to one of the files.
if config.look_for_dependency_only:
print found_dependency_string if matched else no_dependency_string
return
if matched:
unqualified_mapping = _GetUnqualifiedToQualifiedMapping(
all_targets, config.targets)
if len(unqualified_mapping) != len(config.targets):
not_found = []
for target in config.targets:
if not target in unqualified_mapping:
not_found.append(target)
raise Exception('Unable to find all targets: ' + str(not_found))
qualified_targets = [unqualified_mapping[x] for x in config.targets]
output_targets = _GetTargetsDependingOn(all_targets, qualified_targets)
else:
output_targets = []
print json.dumps(
{'targets': output_targets,
'status': found_dependency_string if matched else no_dependency_string })
except Exception as e:
print json.dumps({'error': str(e)})