blob: 2131ef6f43808edbe7952f694774936aa962691c [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.
"""SDK Frontend generator for transforming the IDK into the GN SDK.
This class accepts a directory or tarball of an instance of the Integrator
Developer Kit (IDK) and uses the metadata from the IDK to drive the
construction of a specific SDK for use in projects using GN.
"""
import argparse
import glob
import json
import os
import shutil
import stat
import subprocess
import sys
import tarfile
import xml.etree.ElementTree
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
FUCHSIA_ROOT = os.path.dirname( # $root
os.path.dirname( # scripts
os.path.dirname( # sdk
SCRIPT_DIR))) # gn
sys.path += [os.path.join(FUCHSIA_ROOT, 'scripts', 'sdk', 'common')]
from files import copy_tree
from frontend import Frontend
import template_model as model
from collections import namedtuple
# Arguments for self.copy_file. Using the 'src' path, a relative path will be
# calculated from 'base', then the file is copied to that path under 'dest'.
# E.g. to copy {in}/foo/bar/baz to {out}/blam/baz, use
# CopyArgs(src = 'foo/bar/baz', base = 'foo/bar', dest = 'blam')
CopyArgs = namedtuple('CopyArgs', ['src', 'base', 'dest'])
# Any extra files which need to be added manually to the SDK can be specified
# here. Entries are structured as args for self.copy_file.
EXTRA_COPY = [
# Copy various files to support femu.sh from implementation of fx emu.
# See base/bin/femu-meta.json for more detail about the files needed.
# {fuchsia}/tools/devshell/emu -> {out}/bin/devshell/emu
CopyArgs(
src=os.path.join(FUCHSIA_ROOT, 'tools', 'devshell', 'emu'),
base=os.path.join(FUCHSIA_ROOT, 'tools'),
dest='bin'),
# {fuchsia}/tools/devshell/lib/fvm.sh -> {out}/bin/devshell/lib/fvm.sh
CopyArgs(
src=os.path.join(FUCHSIA_ROOT, 'tools', 'devshell', 'lib', 'fvm.sh'),
base=os.path.join(FUCHSIA_ROOT, 'tools'),
dest='bin'),
# {fuchsia}/scripts/start-unsecure-internet.sh -> {out}/bin/start-unsecure-internet.sh
CopyArgs(
src=os.path.join(FUCHSIA_ROOT, 'scripts', 'start-unsecure-internet.sh'),
base=os.path.join(FUCHSIA_ROOT, 'scripts'),
dest='bin'),
# {fuchsia}/tools/devshell/lib/emu-ifup-macos.sh -> {out}/bin/devshell/lib/emu-ifup-macos.sh
CopyArgs(
src=os.path.join(
FUCHSIA_ROOT, 'tools', 'devshell', 'lib', 'emu-ifup-macos.sh'),
base=os.path.join(FUCHSIA_ROOT, 'tools'),
dest='bin'),
# Copy scripts to support SSH credential handling.
# {fuchsia}/tools/devshell/lib/verify-default-keys.sh -> {out}/bin/devshell/lib/verify-default-keys.sh
CopyArgs(
src=os.path.join(
FUCHSIA_ROOT, 'tools', 'devshell', 'lib', 'verify-default-keys.sh'),
base=os.path.join(FUCHSIA_ROOT, 'tools'),
dest='bin'),
]
# Capture the version of required prebuilts from the jiri manifest. Note
# that ${platform} is actually part of the XML package name, so should
# not be interpreted.
EXTRA_PREBUILTS = {
'fuchsia/third_party/aemu/${platform}': 'aemu',
'fuchsia/third_party/grpcwebproxy/${platform}': 'grpcwebproxy',
'fuchsia/vdl/${platform}': 'device_launcher',
}
class GNBuilder(Frontend):
"""Frontend for GN.
Based on the metadata in the IDK used, this frontend generates GN
specific build rules for using the contents of the IDK. It also adds a
collection of tools for interacting with devices and emulators running
Fuchsia.
The templates used are in the templates directory, and the static content that
is added to the SDK is in the base directory.
"""
def __init__(
self,
local_dir,
output,
archive='',
directory='',
jiri_manifest=None):
"""Initializes an instance of the GNBuilder.
Note that only one of archive or directory should be specified.
Args:
local_dir: The local directory used to find additional resources used
during the transformation.
output: The output directory. The contents of this directory are removed
before processing the IDK.
archive: The tarball archive to process. Can be empty meaning use the
directory parameter as input.
directory: The directory containing an unpackaged IDK. Can be empty
meaning use the archive parameter as input.
jiri_manifest: File containing external references in XML format.
This is typically $FUCHSIA_ROOT/.jiri_root/update_history/latest
"""
super(GNBuilder, self).__init__(
local_dir=local_dir,
output=output,
archive=archive,
directory=directory)
self.target_arches = []
self.fidl_targets = [] # List of all FIDL library targets generated
self.cc_prebuilt_targets = [
] # List of all CC prebuilt library targets generated
self.cc_source_targets = [
] # List of all CC source library targets generated
self.loadable_module_targets = [
] # List of all loadable module targets generated
self.sysroot_targets = [] # List of all sysroot targets generated
self.build_files = []
if jiri_manifest:
self.jiri_manifest = jiri_manifest
else:
self.jiri_manifest = os.path.join(
FUCHSIA_ROOT, '.jiri_root', 'update_history', 'latest')
def prepare(self, arch, types):
"""Called before elements are processed.
This is called by the base class before the elements in the IDK are
processed.
At this time, the contents of the 'base' directory are copied to the
output directory, as well as the 'meta' directory from the IDK.
Args:
arch: A json object describing the host and target architectures.
types: The list of atom types contained in the metadata.
"""
del types # types is not used.
self.target_arches = arch['target']
# Capture versions of various prebuilts contained in the jiri manifest
prebuilt_results = get_prebuilts(EXTRA_PREBUILTS, self.jiri_manifest)
for prebuilt, version in prebuilt_results.items():
with open(self.dest('bin', prebuilt + '.version'), 'w') as output:
output.write(version)
# Propagate the manifest for the Core SDK into the GN SDK. Metadata
# schemas are copied by the metadata_schemas documentation atom.
self.copy_file(os.path.join('meta', 'manifest.json'))
def finalize(self, arch, types):
# Copy the common files. The destination of these files is relative to
# the root of the fuchsia_sdk.
copy_tree(self.local('base'), self.output, allow_overwrite=False)
# Copy any additional files that would normally not be included
for each in EXTRA_COPY:
self.copy_file(
each.src, each.base, each.dest, allow_overwrite=False)
self.write_additional_files()
# Find CIPD prebuilt gn.
gn_path = os.path.join(
FUCHSIA_ROOT, 'prebuilt', 'third_party', 'gn', '*', 'gn')
gn = glob.glob(gn_path)[0]
# Format gn files.
for root, _, files in os.walk(self.output):
for f in (f for f in files if f.endswith(('.gn', '.gni'))):
subprocess.call([gn, 'format', os.path.join(root, f)])
def write_additional_files(self):
self.write_file(self.dest('.gitignore'), 'gitignore', self)
self.write_file(
self.dest("build", "test_targets.gni"), 'test_targets', self)
self.write_build_file_metadata(
'gn-build-files', 'build/gn-build-files-meta.json')
# Propagate the Core SDK manifest, incorporating our changes.
self.update_metadata(
[
'build/gn-rules-meta.json', 'build/gn-build-files-meta.json',
'readme-meta.json'
] + [
os.path.relpath(file, self.output)
for file in glob.glob(self.dest('bin/*.json'))
], 'meta/manifest.json')
def update_metadata(self, entries, metafile):
with open(self.dest(metafile), 'r') as input:
metadata = json.load(input)
# Documentation and host_tool metadata descriptions are named
# inconsistently, so read the manifest and copy them explicitly.
for atom in metadata['parts']:
if atom['type'] in ['documentation', 'host_tool']:
self.copy_file(atom['meta'])
# There are dart components and device_profile in the Core SDK which
# are not part of the GN SDK. Remove them from the manifest.
metadata['parts'] = [
atom for atom in metadata['parts']
if not atom['type'] in ['dart_library', 'device_profile']
]
for entry in entries:
with open(self.dest(entry), 'r') as entry_meta:
new_meta = json.load(entry_meta)
metadata['parts'].append(
{
'meta': entry,
'type': new_meta['type']
})
# sort parts list so it is in a stable order
def meta_type_key(part):
return '%s_%s' % (part['meta'], part['type'])
metadata['parts'].sort(key=meta_type_key)
with open(self.dest(metafile), 'w') as output:
json.dump(
metadata,
output,
indent=2,
sort_keys=True,
separators=(',', ': '))
def write_build_file_metadata(self, name, filename):
"""Writes a metadata atom defining the build files.
Args:
name: the name of atom.
filename: the relative filename to write the metadata file.
"""
self.build_files.sort()
data = {'name': name, 'type': 'documentation', 'docs': self.build_files}
with open(self.dest(filename), 'w') as output:
json.dump(
data, output, indent=2, sort_keys=True, separators=(',', ': '))
def write_atom_metadata(self, filename, atom):
full_name = self.dest(filename)
if not os.path.exists(os.path.dirname(full_name)):
os.makedirs(os.path.dirname(full_name))
with open(full_name, 'w') as atom_metadata:
json.dump(
atom,
atom_metadata,
indent=2,
sort_keys=True,
separators=(',', ': '))
# Handlers for SDK atoms
def install_documentation_atom(self, atom):
self.copy_files(atom['docs'])
def install_cc_prebuilt_library_atom(self, atom):
name = atom['name']
# Add atom to test targets
self.cc_prebuilt_targets.append(name)
base = self.dest('pkg', name)
library = model.CppPrebuiltLibrary(name)
library.relative_path_to_root = os.path.relpath(self.output, start=base)
library.is_static = atom['format'] == 'static'
self.copy_files(atom['headers'], atom['root'], base, library.hdrs)
for arch in self.target_arches:
binaries = atom['binaries'][arch]
prebuilt_set = model.CppPrebuiltSet(binaries['link'])
self.copy_file(binaries['link'])
if 'dist' in binaries:
prebuilt_set.dist_lib = binaries['dist']
prebuilt_set.dist_path = binaries['dist_path']
self.copy_file(binaries['dist'])
if 'debug' in binaries:
self.copy_file(binaries['debug'])
library.prebuilts[arch] = prebuilt_set
for dep in atom['deps']:
library.deps.append('../' + dep)
library.includes = os.path.relpath(atom['include_dir'], atom['root'])
if not os.path.exists(base):
os.makedirs(base)
self.write_file(
os.path.join(base, 'BUILD.gn'), 'cc_prebuilt_library', library)
self.write_atom_metadata(os.path.join(base, 'meta.json'), atom)
self.build_files.append(
os.path.relpath(os.path.join(base, 'BUILD.gn'), self.output))
def install_cc_source_library_atom(self, atom):
name = atom['name']
# Add atom to test targets
self.cc_source_targets.append(name)
base = self.dest('pkg', name)
library = model.CppSourceLibrary(name)
library.relative_path_to_root = os.path.relpath(self.output, start=base)
self.copy_files(atom['headers'], atom['root'], base, library.hdrs)
self.copy_files(atom['sources'], atom['root'], base, library.srcs)
for dep in atom['deps']:
library.deps.append('../' + dep)
for dep in atom['fidl_deps']:
dep_name = dep
library.deps.append('../../fidl/' + dep_name)
library.includes = os.path.relpath(atom['include_dir'], atom['root'])
self.write_file(os.path.join(base, 'BUILD.gn'), 'cc_library', library)
self.write_atom_metadata(os.path.join(base, 'meta.json'), atom)
self.build_files.append(
os.path.relpath(os.path.join(base, 'BUILD.gn'), self.output))
def install_fidl_library_atom(self, atom):
name = atom['name']
base = self.dest('fidl', name)
data = model.FidlLibrary(name, atom['name'])
data.relative_path_to_root = os.path.relpath(self.output, start=base)
data.short_name = name.split('.')[-1]
data.namespace = '.'.join(name.split('.')[0:-1])
self.copy_files(atom['sources'], atom['root'], base, data.srcs)
for dep in atom['deps']:
data.deps.append(dep)
self.write_file(os.path.join(base, 'BUILD.gn'), 'fidl_library', data)
self.fidl_targets.append(name)
self.write_atom_metadata(os.path.join(base, 'meta.json'), atom)
self.build_files.append(
os.path.relpath(os.path.join(base, 'BUILD.gn'), self.output))
def install_host_tool_atom(self, atom):
if 'files' in atom:
self.copy_files(atom['files'], atom['root'], 'tools')
if 'target_files' in atom:
for files in atom['target_files'].values():
self.copy_files(files, atom['root'], 'tools')
def install_loadable_module_atom(self, atom):
name = atom['name']
if name != 'vulkan_layers':
raise RuntimeError('Unsupported loadable_module: %s' % name)
# Add loadable modules to test targets
self.loadable_module_targets.append(name)
# Copy resources and binaries
resources = atom['resources']
self.copy_files(resources)
binaries = atom['binaries']
for arch in self.target_arches:
self.copy_files(binaries[arch])
def _filename_no_ext(name):
return os.path.splitext(os.path.basename(name))[0]
data = model.VulkanLibrary()
# Pair each json resource with its corresponding binary. Each such pair
# is a "layer". We only need to check one arch because each arch has the
# same list of binaries.
arch = next(iter(binaries))
binary_names = binaries[arch]
# TODO(jdkoren): use atom[root] once fxr/360094 lands
local_pkg = os.path.join('pkg', name)
for res in resources:
layer_name = _filename_no_ext(res)
# Special case: VkLayer_standard_validation does not have a binary
# and also depends on VkLayer_khronos_validation. Currently atom
# metadata does not contain this information (fxbug.dev/46250).
if layer_name == 'VkLayer_standard_validation':
layer = model.VulkanLayer(
name=layer_name,
config=os.path.relpath(res, start=local_pkg),
binary='',
data_deps=[':VkLayer_khronos_validation'])
else:
# Filter binaries for a matching name.
filtered = [
n for n in binary_names if _filename_no_ext(n) == layer_name
]
if not filtered:
# If the binary could not be found then do not generate a
# target for this layer. The missing targets will cause a
# mismatch with the "golden" outputs.
continue
# Replace harcoded arch in the found binary filename.
binary = filtered[0].replace(
'/' + arch + '/', "/${target_cpu}/")
layer = model.VulkanLayer(
name=layer_name,
config=os.path.relpath(res, start=local_pkg),
binary=os.path.relpath(binary, start=local_pkg))
# Special case: VkLayer_image_pipe_swapchain has an undocumented
# data_dep on trace-engine. Currently atom metadata does not
# contain this information (fxbug.dev/46250).
if layer_name == 'VkLayer_image_pipe_swapchain':
layer.data_deps.append('../trace-engine')
data.layers.append(layer)
base = self.dest(local_pkg)
self.write_file(os.path.join(base, 'BUILD.gn'), 'vulkan_module', data)
self.build_files.append(
os.path.relpath(os.path.join(base, 'BUILD.gn'), self.output))
self.write_atom_metadata(os.path.join(base, 'meta.json'), atom)
def install_data_atom(self, atom):
self.write_atom_metadata(
self.dest('data', 'config', atom['name'], 'meta.json'), atom)
self.copy_files(atom['data'])
def install_sysroot_atom(self, atom):
for arch in self.target_arches:
base = self.dest('arch', arch, 'sysroot')
arch_data = atom['versions'][arch]
arch_root = arch_data['root']
self.copy_files(arch_data['headers'], arch_root, base)
self.copy_files(arch_data['link_libs'], arch_root, base)
# We maintain debug files in their original location.
self.copy_files(arch_data['debug_libs'])
# Files in dist_libs are required for toolchain config. They are the
# same for all architectures and shouldn't change names, so we
# hardcode those names in build/config/BUILD.gn. This may need to
# change if/when we support sanitized builds.
self.copy_files(arch_data['dist_libs'], arch_root, base)
self.write_atom_metadata(self.dest('pkg', 'sysroot', 'meta.json'), atom)
class TestData(object):
"""Class representing test data to be added to the run_py mako template"""
def __init__(self):
self.fuchsia_root = FUCHSIA_ROOT
def make_executable(path):
st = os.stat(path)
os.chmod(path, st.st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
def get_prebuilts(in_extra_prebuilts, jiri_manifest):
""" Lookup version strings for a set of prebuilts
Given a file such as $FUCHSIA_ROOT/.jiri_root/update_history/latest, find matches for each of the name
strings, capture the version string, and return back a dictionary with the mappings.
Args:
in_extra_prebuilts: Dictionary of name strings to search, and the corresponding short name
jiri_manifest: XML file to read, such as $FUCHSIA_ROOT/.jiri_root/update_history/latest
Returns:
Dictionary of short name to version mappings
Raises:
RuntimeError: If in_extra_prebuilts contains items not found in the prebuilts file
or the jiri manifest can't be parsed
"""
# Copy the input list since we modify it in the loop
extra_prebuilts = in_extra_prebuilts.copy()
prebuilt_results = {}
try:
manifest_root = xml.etree.ElementTree.parse(jiri_manifest).getroot()
except Exception as e:
raise RuntimeError(
'Unable to parse jiri manifest at %s: %s' % (jiri_manifest, e))
remaining_prebuilts = extra_prebuilts
for packages in manifest_root.iter('package'):
prebuilt = remaining_prebuilts.pop(packages.attrib['name'], None)
if prebuilt:
prebuilt_results[prebuilt] = packages.attrib['version']
if remaining_prebuilts:
raise RuntimeError(
'Could not find entries in %s for the remaining EXTRA_PREBUILTS: %s'
% (jiri_manifest, remaining_prebuilts))
return prebuilt_results
def create_test_workspace(output):
# Remove any existing output.
shutil.rmtree(output, True)
# Copy the base tests.
copy_tree(os.path.join(SCRIPT_DIR, 'test_project'), output)
# run.py file
builder = Frontend(local_dir=SCRIPT_DIR)
run_py_path = os.path.join(output, 'run.py')
builder.write_file(
path=run_py_path, template_name='run_py', data=TestData())
make_executable(run_py_path)
return True
def create_archive(output_archive, output):
# If file already exists remove it
if os.path.exists(output_archive):
os.remove(output_archive)
# If GN SDK directory is empty throw an error
if not os.path.exists(output):
return False
# Create GN SDK archive
with tarfile.open(output_archive, "w:gz") as archive_file:
archive_file.add(output, arcname='')
return True
def main(args_list=None):
parser = argparse.ArgumentParser(
description='Creates a GN SDK for a given SDK tarball.')
source_group = parser.add_mutually_exclusive_group(required=True)
source_group.add_argument(
'--archive', help='Path to the SDK archive to ingest', default='')
source_group.add_argument(
'--directory', help='Path to the SDK directory to ingest', default='')
parser.add_argument(
'--output',
help='Path to the directory where to install the SDK',
required=True)
parser.add_argument(
'--output-archive',
help='Path to add the SDK archive file (e.g. "path/to/gn.tar.gz")')
parser.add_argument(
'--output-archive-only',
help=
'Whether to output the SDK archive and skip installing to a directory',
default=False,
)
parser.add_argument(
'--tests', help='Path to the directory where to generate tests')
parser.add_argument(
'--jiri-manifest',
help=
'File containing external references in XML format (e.g. ".jiri_root/update_history/latest")'
)
if args_list:
args = parser.parse_args(args_list)
else:
args = parser.parse_args()
return run_generator(
archive=args.archive,
directory=args.directory,
output=args.output,
tests=args.tests,
output_archive=args.output_archive,
output_archive_only=args.output_archive_only,
jiri_manifest=args.jiri_manifest)
def run_generator(
archive,
directory,
output,
output_archive_only=False,
tests='',
output_archive='',
jiri_manifest=None):
"""Run the generator. Returns 0 on success, non-zero otherwise.
Note that only one of archive or directory should be specified.
Args:
archive: Path to an SDK tarball archive.
directory: Path to an unpackaged SDK.
output: The output directory.
tests: The directory where to generate tests. Defaults to empty string.
"""
# Remove any existing output.
if os.path.exists(output):
shutil.rmtree(output)
builder = GNBuilder(
archive=archive,
directory=directory,
output=output,
local_dir=SCRIPT_DIR,
jiri_manifest=jiri_manifest)
if not builder.run():
return 1
if tests:
# Create the tests workspace
if not create_test_workspace(tests):
return 1
# Copy the GN SDK to the test workspace
wrkspc_sdk_dir = os.path.join(tests, 'third_party', 'fuchsia-sdk')
copy_tree(output, wrkspc_sdk_dir)
# Copy test metadata to the workspace. meta/manifest.json tells the
# emulator tools where the SDK for integration testing is located.
# This must be done after the GN SDK is copied into the test workspace,
# which would otherwise overwrite these contents.
testdata = os.path.join(SCRIPT_DIR, 'testdata')
copy_tree(
os.path.join(testdata, 'meta'),
os.path.join(wrkspc_sdk_dir, 'meta'))
if output_archive:
if not create_archive(output_archive, output):
return 1
if output_archive_only:
shutil.rmtree(output)
return 0
if __name__ == '__main__':
sys.exit(main())