We need to determine if this should be replaced by Android's version of those libraries. This can be done via "stl" property on dependent targets (we would additionally have to configure the version). Test: n/a Change-Id: Ibd54cc0b58086ec77d5eee40708f28b237d70c3f
318 lines
12 KiB
Python
318 lines
12 KiB
Python
# Copyright (C) 2022 The Android Open Source Project
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
# A collection of utilities for extracting build rule information from GN
|
|
# projects.
|
|
|
|
from __future__ import print_function
|
|
import collections
|
|
import errno
|
|
import filecmp
|
|
import json
|
|
import os
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
|
|
BUILDFLAGS_TARGET = '//gn:gen_buildflags'
|
|
GEN_VERSION_TARGET = '//src/base:version_gen_h'
|
|
TARGET_TOOLCHAIN = '//gn/standalone/toolchain:gcc_like_host'
|
|
HOST_TOOLCHAIN = '//gn/standalone/toolchain:gcc_like_host'
|
|
LINKER_UNIT_TYPES = ('executable', 'shared_library', 'static_library')
|
|
|
|
# TODO(primiano): investigate these, they require further componentization.
|
|
ODR_VIOLATION_IGNORE_TARGETS = {
|
|
'//test/cts:perfetto_cts_deps',
|
|
'//:perfetto_integrationtests',
|
|
}
|
|
|
|
|
|
def repo_root():
|
|
"""Returns an absolute path to the repository root."""
|
|
return os.path.join(
|
|
os.path.realpath(os.path.dirname(__file__)), os.path.pardir)
|
|
|
|
|
|
def label_to_path(label):
|
|
"""Turn a GN output label (e.g., //some_dir/file.cc) into a path."""
|
|
assert label.startswith('//')
|
|
return label[2:] or "./"
|
|
|
|
|
|
def label_without_toolchain(label):
|
|
"""Strips the toolchain from a GN label.
|
|
|
|
Return a GN label (e.g //buildtools:protobuf(//gn/standalone/toolchain:
|
|
gcc_like_host) without the parenthesised toolchain part.
|
|
"""
|
|
return label.split('(')[0]
|
|
|
|
|
|
def label_to_target_name_with_path(label):
|
|
"""
|
|
Turn a GN label into a target name involving the full path.
|
|
e.g., //src/perfetto:tests -> src_perfetto_tests
|
|
"""
|
|
name = re.sub(r'^//:?', '', label)
|
|
name = re.sub(r'[^a-zA-Z0-9_]', '_', name)
|
|
return name
|
|
|
|
|
|
class GnParser(object):
|
|
"""A parser with some cleverness for GN json desc files
|
|
|
|
The main goals of this parser are:
|
|
1) Deal with the fact that other build systems don't have an equivalent
|
|
notion to GN's source_set. Conversely to Bazel's and Soong's filegroups,
|
|
GN source_sets expect that dependencies, cflags and other source_set
|
|
properties propagate up to the linker unit (static_library, executable or
|
|
shared_library). This parser simulates the same behavior: when a
|
|
source_set is encountered, some of its variables (cflags and such) are
|
|
copied up to the dependent targets. This is to allow gen_xxx to create
|
|
one filegroup for each source_set and then squash all the other flags
|
|
onto the linker unit.
|
|
2) Detect and special-case protobuf targets, figuring out the protoc-plugin
|
|
being used.
|
|
"""
|
|
|
|
class Target(object):
|
|
"""Reperesents A GN target.
|
|
|
|
Maked properties are propagated up the dependency chain when a
|
|
source_set dependency is encountered.
|
|
"""
|
|
|
|
def __init__(self, name, type):
|
|
self.name = name # e.g. //src/ipc:ipc
|
|
|
|
VALID_TYPES = ('static_library', 'shared_library', 'executable', 'group',
|
|
'action', 'source_set', 'proto_library')
|
|
assert (type in VALID_TYPES)
|
|
self.type = type
|
|
self.testonly = False
|
|
self.toolchain = None
|
|
|
|
# These are valid only for type == proto_library.
|
|
# This is typically: 'proto', 'protozero', 'ipc'.
|
|
self.proto_plugin = None
|
|
self.proto_paths = set()
|
|
self.proto_exports = set()
|
|
|
|
self.sources = set()
|
|
# TODO(primiano): consider whether the public section should be part of
|
|
# bubbled-up sources.
|
|
self.public_headers = set() # 'public'
|
|
|
|
# These are valid only for type == 'action'
|
|
self.inputs = set()
|
|
self.outputs = set()
|
|
self.script = None
|
|
self.args = []
|
|
|
|
# These variables are propagated up when encountering a dependency
|
|
# on a source_set target.
|
|
self.cflags = set()
|
|
self.defines = set()
|
|
self.deps = set()
|
|
self.libs = set()
|
|
self.include_dirs = set()
|
|
self.ldflags = set()
|
|
self.source_set_deps = set() # Transitive set of source_set deps.
|
|
self.proto_deps = set()
|
|
self.transitive_proto_deps = set()
|
|
|
|
# Deps on //gn:xxx have this flag set to True. These dependencies
|
|
# are special because they pull third_party code from buildtools/.
|
|
# We don't want to keep recursing into //buildtools in generators,
|
|
# this flag is used to stop the recursion and create an empty
|
|
# placeholder target once we hit //gn:protoc or similar.
|
|
self.is_third_party_dep_ = False
|
|
|
|
def __lt__(self, other):
|
|
if isinstance(other, self.__class__):
|
|
return self.name < other.name
|
|
raise TypeError(
|
|
'\'<\' not supported between instances of \'%s\' and \'%s\'' %
|
|
(type(self).__name__, type(other).__name__))
|
|
|
|
def __repr__(self):
|
|
return json.dumps({
|
|
k: (list(sorted(v)) if isinstance(v, set) else v)
|
|
for (k, v) in self.__dict__.items()
|
|
},
|
|
indent=4,
|
|
sort_keys=True)
|
|
|
|
def update(self, other):
|
|
for key in ('cflags', 'defines', 'deps', 'include_dirs', 'ldflags',
|
|
'source_set_deps', 'proto_deps', 'transitive_proto_deps',
|
|
'libs', 'proto_paths'):
|
|
self.__dict__[key].update(other.__dict__.get(key, []))
|
|
|
|
def __init__(self, gn_desc):
|
|
self.gn_desc_ = gn_desc
|
|
self.all_targets = {}
|
|
self.linker_units = {} # Executables, shared or static libraries.
|
|
self.source_sets = {}
|
|
self.actions = {}
|
|
self.proto_libs = {}
|
|
|
|
def get_target(self, gn_target_name):
|
|
"""Returns a Target object from the fully qualified GN target name.
|
|
|
|
It bubbles up variables from source_set dependencies as described in the
|
|
class-level comments.
|
|
"""
|
|
target = self.all_targets.get(gn_target_name)
|
|
if target is not None:
|
|
return target # Target already processed.
|
|
|
|
desc = self.gn_desc_[gn_target_name]
|
|
target = GnParser.Target(gn_target_name, desc['type'])
|
|
target.testonly = desc.get('testonly', False)
|
|
target.toolchain = desc.get('toolchain', None)
|
|
self.all_targets[gn_target_name] = target
|
|
|
|
# TODO: determine if below comment should apply for cronet builds in Android.
|
|
# We should never have GN targets directly depend on buidtools. They
|
|
# should hop via //gn:xxx, so we can give generators an opportunity to
|
|
# override them.
|
|
# Specifically allow targets to depend on libc++ and libunwind.
|
|
if not any(match in gn_target_name for match in ['libc++', 'libunwind']):
|
|
assert (not gn_target_name.startswith('//buildtools'))
|
|
|
|
|
|
# Don't descend further into third_party targets. Genrators are supposed
|
|
# to either ignore them or route to other externally-provided targets.
|
|
if gn_target_name.startswith('//gn'):
|
|
target.is_third_party_dep_ = True
|
|
return target
|
|
|
|
proto_target_type, proto_desc = self.get_proto_target_type(target)
|
|
if proto_target_type is not None:
|
|
self.proto_libs[target.name] = target
|
|
target.type = 'proto_library'
|
|
target.proto_plugin = proto_target_type
|
|
target.proto_paths.update(self.get_proto_paths(proto_desc))
|
|
target.proto_exports.update(self.get_proto_exports(proto_desc))
|
|
target.sources.update(proto_desc.get('sources', []))
|
|
assert (all(x.endswith('.proto') for x in target.sources))
|
|
elif target.type == 'source_set':
|
|
self.source_sets[gn_target_name] = target
|
|
target.sources.update(desc.get('sources', []))
|
|
elif target.type in LINKER_UNIT_TYPES:
|
|
self.linker_units[gn_target_name] = target
|
|
target.sources.update(desc.get('sources', []))
|
|
elif target.type == 'action':
|
|
self.actions[gn_target_name] = target
|
|
target.inputs.update(desc.get('inputs', []))
|
|
target.sources.update(desc.get('sources', []))
|
|
outs = [re.sub('^//out/.+?/gen/', '', x) for x in desc['outputs']]
|
|
target.outputs.update(outs)
|
|
target.script = desc['script']
|
|
# Args are typically relative to the root build dir (../../xxx)
|
|
# because root build dir is typically out/xxx/).
|
|
target.args = [re.sub('^../../', '//', x) for x in desc['args']]
|
|
|
|
# Default for 'public' is //* - all headers in 'sources' are public.
|
|
# TODO(primiano): if a 'public' section is specified (even if empty), then
|
|
# the rest of 'sources' is considered inaccessible by gn. Consider
|
|
# emulating that, so that generated build files don't end up with overly
|
|
# accessible headers.
|
|
public_headers = [x for x in desc.get('public', []) if x != '*']
|
|
target.public_headers.update(public_headers)
|
|
|
|
target.cflags.update(desc.get('cflags', []) + desc.get('cflags_cc', []))
|
|
target.libs.update(desc.get('libs', []))
|
|
target.ldflags.update(desc.get('ldflags', []))
|
|
target.defines.update(desc.get('defines', []))
|
|
target.include_dirs.update(desc.get('include_dirs', []))
|
|
|
|
# Recurse in dependencies.
|
|
for dep_name in desc.get('deps', []):
|
|
dep = self.get_target(dep_name)
|
|
if dep.is_third_party_dep_:
|
|
target.deps.add(dep_name)
|
|
elif dep.type == 'proto_library':
|
|
target.proto_deps.add(dep_name)
|
|
target.transitive_proto_deps.add(dep_name)
|
|
target.proto_paths.update(dep.proto_paths)
|
|
target.transitive_proto_deps.update(dep.transitive_proto_deps)
|
|
elif dep.type == 'source_set':
|
|
target.source_set_deps.add(dep_name)
|
|
target.update(dep) # Bubble up source set's cflags/ldflags etc.
|
|
elif dep.type == 'group':
|
|
target.update(dep) # Bubble up groups's cflags/ldflags etc.
|
|
elif dep.type == 'action':
|
|
if proto_target_type is None:
|
|
target.deps.add(dep_name)
|
|
elif dep.type in LINKER_UNIT_TYPES:
|
|
target.deps.add(dep_name)
|
|
|
|
return target
|
|
|
|
def get_proto_exports(self, proto_desc):
|
|
# exports in metadata will be available for source_set targets.
|
|
metadata = proto_desc.get('metadata', {})
|
|
return metadata.get('exports', [])
|
|
|
|
def get_proto_paths(self, proto_desc):
|
|
# import_dirs in metadata will be available for source_set targets.
|
|
metadata = proto_desc.get('metadata', {})
|
|
return metadata.get('import_dirs', [])
|
|
|
|
def get_proto_target_type(self, target):
|
|
""" Checks if the target is a proto library and return the plugin.
|
|
|
|
Returns:
|
|
(None, None): if the target is not a proto library.
|
|
(plugin, proto_desc) where |plugin| is 'proto' in the default (lite)
|
|
case or 'protozero' or 'ipc' or 'descriptor'; |proto_desc| is the GN
|
|
json desc of the target with the .proto sources (_gen target for
|
|
non-descriptor types or the target itself for descriptor type).
|
|
"""
|
|
parts = target.name.split('(', 1)
|
|
name = parts[0]
|
|
toolchain = '(' + parts[1] if len(parts) > 1 else ''
|
|
|
|
# Descriptor targets don't have a _gen target; instead we look for the
|
|
# characteristic flag in the args of the target itself.
|
|
desc = self.gn_desc_.get(target.name)
|
|
if '--descriptor_set_out' in desc.get('args', []):
|
|
return 'descriptor', desc
|
|
|
|
# Source set proto targets have a non-empty proto_library_sources in the
|
|
# metadata of the description.
|
|
metadata = desc.get('metadata', {})
|
|
if 'proto_library_sources' in metadata:
|
|
return 'source_set', desc
|
|
|
|
# In all other cases, we want to look at the _gen target as that has the
|
|
# important information.
|
|
gen_desc = self.gn_desc_.get('%s_gen%s' % (name, toolchain))
|
|
if gen_desc is None or gen_desc['type'] != 'action':
|
|
return None, None
|
|
args = gen_desc.get('args', [])
|
|
if '/protoc' not in args[0]:
|
|
return None, None
|
|
plugin = 'proto'
|
|
for arg in (arg for arg in args if arg.startswith('--plugin=')):
|
|
# |arg| at this point looks like:
|
|
# --plugin=protoc-gen-plugin=gcc_like_host/protozero_plugin
|
|
# or
|
|
# --plugin=protoc-gen-plugin=protozero_plugin
|
|
plugin = arg.split('=')[-1].split('/')[-1].replace('_plugin', '')
|
|
return plugin, gen_desc
|