blob: 37918f407365800ad288032e9c02584223661d1b [file] [log] [blame]
#!/usr/bin/env python3.8
# Copyright 2020 The Fuchsia Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""A helper script for verifying dependencies are released in an SDK.
This script is intended to run as a build time check to make sure that dependencies
are released in a "public" or "partner" SDK. This is to ensure that a target depends
on stable API / ABI.
"""
import argparse
import json
import os
import re
import sys
CTF_EXTENSION = '.this_is_ctf'
class VerifyDepsInSDK:
"""Helper class to verify GN dependencies are released in a public SDK.
Args:
root_build_dir (string): An absolute path to the GN's $root_build_dir.
output_file (string): The path to (including) the file to be generated.
invoker_label (string): The label of the invoker of verify_deps_in_sdk.
deps_to_verify (list(string)): A list of fully qualified GN labels.
allowed_deps (list(string)): A list of allowed deps found in //sdk/ctf/allowed_ctf_deps.gni"
allowed_dirs (list(string)): A list of allowed directories found in //sdk/ctf/allowed_ctf_deps.gni"
sdk_manifests (list(string)): A list of absolute paths to SDK manifest files.
Raises:
ValueError if any parameter is empty, if root_build_dir does not exist, or if the sdk_manifests do not exist.
"""
def __init__(
self, root_build_dir, output_file, invoker_label, deps_to_verify,
allowed_deps, allowed_dirs, sdk_manifests):
if not (root_build_dir and os.path.exists(root_build_dir)):
raise ValueError('root_build_dir cannot be empty and must exist.')
if not output_file:
raise ValueError('output_file cannot be empty.')
if not invoker_label:
raise ValueError('invoker_label cannot be empty.')
if not deps_to_verify:
raise ValueError('deps_to_verify cannot be empty.')
if not allowed_deps:
raise ValueError('allowed_deps cannot be empty')
if not allowed_dirs:
raise ValueError('allowed_dirs cannot be empty')
if sdk_manifests:
for manifest in sdk_manifests:
if not os.path.isfile(manifest):
raise ValueError(f'manifest {manifest} does not exist')
else:
raise ValueError('sdk_manifests cannot be empty')
self.root_build_dir = root_build_dir
self.output_file = output_file
self.invoker_label = invoker_label
self.deps_to_verify = deps_to_verify
self.allowed_deps = allowed_deps
self.allowed_dirs = allowed_dirs
self.sdk_manifests = sdk_manifests
def get_ctf_file_path(self, dep):
"""Returns the path to a CTF file.
The CTF creates a file for each CTF target at GN gen time. This CTF file is used to indicate
that a target is acceptable in CTF. Each CTF target verifies its own dependencies using this script.
If a dependency does not have a CTF file, the script will look for that fully qualified dependency
in allowed_deps. If it does have a CTF file, verification of that target will be deferred such
that the target may verify its own dependencies.
Args:
dep (string): A GN label.
Returns:
A string containing the absolute path to the target's CTF file.
"""
# Remove the leading slashes from the label.
dep = dep[2:]
# Get the target_name from the label.
if ':' in dep:
# path/to/lib:lib
dep, target_name = dep.split(':')
elif '/' in dep:
# path/to/lib
_, target_name = dep.rsplit('/', 1)
else:
# lib
target_name = dep
return self.root_build_dir + '/ctf/' + dep + '/' + target_name + CTF_EXTENSION
def verify_deps(self):
"""Verifies the element's dependencies are released in a public SDK.
All dependencies must be in allowed_deps, allowed_dirs, or be CTF targets themselves.
Note: At the time this script runs, these dependencies (self.deps_to_verify)
have not necessarily been built. The verification target does not depend on them.
Returns:
A list of labels that are not allowed to be used. The list will be empty if all deps are allowed.
"""
unaccepted_deps = []
unverified_deps = []
for dep in self.deps_to_verify:
# Removes FIDL binding suffixes because the SDK manifests will only
# contain the FIDL name.
for suffix in ["_hlcpp", "_rust", "_cpp_wire"]:
if dep.endswith(suffix):
dep = dep[:-len(suffix)]
break
dep_found = False
if dep in self.allowed_deps or os.path.exists(
self.get_ctf_file_path(dep)):
dep_found = True
else:
# Dep isn't in the allow list and a CTF file doesn't exist. Check if
# all targets in dep's directory are allowed (//third_party/dart-pkg/pub/*).
for allowed_dir in self.allowed_dirs:
pattern = re.compile(allowed_dir)
if pattern.match(dep):
dep_found = True
# Check if dep is an SDK target.
if not dep_found:
unverified_deps.append(dep)
if len(unverified_deps) > 0:
unaccepted_deps = self.verify_deps_in_sdk(unverified_deps)
return unaccepted_deps
def verify_deps_in_sdk(self, deps):
"""Verifies that dependencies are released in an SDK.
Returns:
The list of dependencies that are not released in an SDK.
"""
# SDK atoms are appended with one of the following and a
# toolchain, so we want to ignore them to match against the
# provided label.
match_label = re.compile(
"(?:(_sdk)|(_sdk_manifest)|(_sdk_legacy))\(.*\)")
sdk_atom_label_to_category = {}
for sdk_manifest in self.sdk_manifests:
try:
with open(sdk_manifest, 'r') as manifest:
data = json.load(manifest)
except json.JSONDecodeError:
continue
for atom in data['atoms']:
label = re.sub(match_label, '', atom['gn-label'])
sdk_atom_label_to_category[label] = atom['category']
unaccepted_deps = []
for dep in deps:
if dep not in sdk_atom_label_to_category or sdk_atom_label_to_category[
dep] not in ['partner', 'public']:
unaccepted_deps.append(dep)
return unaccepted_deps
def create_output_file(self):
"""Create an output file containing the verified dependencies.
Should only be run after dependencies are verified using verify_deps.
"""
target_gen_dir, _ = self.output_file.rsplit('/', 1)
if not os.path.exists(target_gen_dir):
os.makedirs(target_gen_dir)
with open(self.output_file, 'w') as f:
for dep in self.deps_to_verify:
f.write(f'{dep}\n')
def main():
parser = argparse.ArgumentParser()
parser.add_argument(
'--root_build_dir',
required=True,
help='Path to the GN $root_build_dir. The default is //out/default.')
parser.add_argument(
'--output',
required=True,
help='The path to (including) the file to be generated.')
parser.add_argument(
'--invoker_label',
required=True,
help='The label of the invoker of verify_deps_in_sdk.')
parser.add_argument(
'--deps_to_verify',
nargs='+',
required=True,
help=
'A list of at least one GN label representing the target\'s dependencies.'
)
parser.add_argument(
'--allowed_deps',
nargs='+',
required=True,
help='A list of allowed dependencies.')
parser.add_argument(
'--allowed_dirs',
nargs='+',
required=True,
help=
'A list of directories where any dependency is allowed. Directories should end in /*'
)
parser.add_argument(
'--sdk_manifests',
nargs='+',
required=True,
help='The list of paths to public and partner SDK manifests')
args = parser.parse_args()
try:
target = VerifyDepsInSDK(
args.root_build_dir, args.output, args.invoker_label,
args.deps_to_verify, args.allowed_deps, args.allowed_dirs,
args.sdk_manifests)
except ValueError as e:
print(f'ValueError: {e}')
return 1
unaccepted_deps = target.verify_deps()
if not unaccepted_deps:
target.create_output_file()
else:
print(
f'The following dependencies of "{args.invoker_label}" are not allowed: {unaccepted_deps}'
)
return 1
return 0
if __name__ == '__main__':
sys.exit(main())