blob: 55608b7ee6a9aee656967816670461322d3a2179 [file] [log] [blame]
#!/usr/bin/env python2.7
# Copyright 2018 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.
"""Base class for generating SDKs from the Fuchsia IDK.
This base 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.
"""
import contextlib
import json
import os
import shutil
import sys
import tarfile
import tempfile
from files import make_dir
_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))) # common
sys.path += [os.path.join(_FUCHSIA_ROOT, 'third_party', 'mako')]
from mako.lookup import TemplateLookup
from mako.template import Template
class Frontend(object):
"""Processes the IDK by runnings them through various transformation methods.
In order to process atoms of type "foo", a frontend needs to define a
`install_foo_atom` method that accepts a single argument representing
the atom's metadata in JSON format.
"""
def __init__(self, output='', archive='', directory='', local_dir=''):
"""Initializes a Frontend instance.
Note that only one of archive or directory should be specified.
Args:
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.
local_dir: The local directory used to find additional resources used
during the transformation.
"""
self._archive = archive
self._directory = directory
self._local_dir = local_dir
self.output = os.path.realpath(output)
self._source_dir = ''
def source(self, *args):
"""Builds a path to a source file.
Only available while the frontend is running.
Args:
*args: the collection of path elements to join
Returns:
The path string to the file.
Raises:
Exception: if this method is called when not processing the IDK or if
source_dir was empty.
"""
if not self._source_dir:
raise Exception('Error: accessing sources while inactive')
return os.path.join(self._source_dir, *args)
def dest(self, *args):
"""Builds a path in the output directory.
This method also ensures that the directory hierarchy exists in the
output directory.
Behaves correctly if the first argument is already within the output
directory.
Args:
*args: the collection of path elements to joing.
Returns:
The path string to the file.
"""
if (os.path.commonprefix([os.path.realpath(args[0]),
self.output]) == self.output):
path = os.path.join(*args)
else:
path = os.path.join(self.output, *args)
return make_dir(path)
def local(self, *args):
"""Builds a path in the local directory.
Args:
*args: the collection of path elements to joing.
Returns:
The path string to the file.
"""
return os.path.join(self._local_dir, *args)
def copy_file(
self,
filename,
root='',
destination='',
result=[],
allow_overwrite=True):
"""Copies a file from a given root directory with a collector.
Copies the file from the root directory into the same path in the
destination directory.
If result is not None, the relative path to the file is added to the
list.
Args:
filename: The path to a file in the root directory.
root: The root directory used to calcualte a relative path to the file.
destination: The destination root directory.
result: A collector list if not None, has the relative path of the file
appended to the list.
allow_overwrite: Whether to allow the destination file to be overwritten.
Raises:
Exception: If the path in file is not within the root directory.
Exception: If allow_overwrite is False and the destination file exists.
"""
if os.path.commonprefix([root, filename]) != root:
raise Exception('%s is not within %s' % (filename, root))
relative_path = os.path.relpath(filename, root)
dest = self.dest(destination, relative_path)
if not allow_overwrite and os.path.exists(dest):
raise Exception(
'Attempt to overwrite file: %s -> %s' % (filename, dest))
shutil.copy2(self.source(filename), dest)
result.append(relative_path)
def copy_files(
self,
files,
root='',
destination='',
result=[],
allow_overwrite=True):
"""Copies files from a given root directory with a collector.
This is done by calling copy_file() iteratively.
If result is not None, the relative path to the file is added to the
list.
Args:
files: The path to a file in the root directory.
root: The root directory used to calcualte a relative path to the file.
destination: The destination root directory.
result: A collector list if not None, has the relative path of the file
appended to the list.
allow_overwrite: Whether to allow destination files to be overwritten.
"""
for f in files:
self.copy_file(f, root, destination, result, allow_overwrite)
def write_file(self, path, template_name, data):
"""Writes a file based on a Mako template.
The templates are found in the local directory named templates.
Args:
path: The output file path.
template_name: The name of the Mako template without the '.mako'
extension.
data: The data model used to render the template.
"""
lookup = TemplateLookup(directories=[self.local('templates')])
template = lookup.get_template(template_name + '.mako')
with open(path, 'w') as outfile:
outfile.write(template.render(data=data))
def prepare(self, arch, atom_types):
"""Called before elements are processed."""
pass
def finalize(self, arch, atom_types):
"""Called after all elements have been processed."""
pass
def run(self):
"""Runs this frontend through the contents of the archive.
Returns true if successful.
"""
with self._create_archive_dir() as archive_dir:
self._source_dir = archive_dir
manifest = load_metadata(self.source('meta', 'manifest.json'))
if not manifest:
return False
types = set([p['type'] for p in manifest['parts']])
try:
self.prepare(manifest['arch'], types)
except Exception as e:
print('Failed preparing elements: %s' % e)
return False
# Process each SDK atom.
for part in manifest['parts']:
type = part['type']
atom = load_metadata(self.source(part['meta']))
if not atom:
return False
getattr(self, 'install_%s_atom' % type, self._handle_atom)(atom)
self.finalize(manifest['arch'], types)
# Reset the source directory, which may be about to disappear.
self._source_dir = ''
return True
def _handle_atom(self, atom):
"""Default atom handler."""
print('Ignored %s (%s)' % (atom['name'], atom['type']))
@contextlib.contextmanager
def _create_archive_dir(self):
if self._directory:
yield self._directory
elif self._archive:
temp_dir = tempfile.mkdtemp(prefix='fuchsia-sdk-archive')
# Extract the tarball into the temporary directory.
# This is vastly more efficient than accessing files one by one via
# the tarfile API.
with tarfile.open(self._archive) as archive:
archive.extractall(temp_dir)
try:
yield temp_dir
finally:
shutil.rmtree(temp_dir, ignore_errors=True)
else:
raise Exception('Error: archive or directory must be set')
def load_metadata(filename):
"""Loads metadata from a file.
Args:
filename: The absolute path to the file to load metadata from.
Returns:
A dictionary representing the metadata from the file.
Returns False if the file doesn't exist or is invalid JSON.
"""
try:
with open(filename, 'r') as meta_file:
return json.load(meta_file)
except IOError:
print('Metadata file not found: %s' % filename)
return False
except ValueError as e:
print('Unable to parse %s: %s' % (filename, e))
return False