127 lines
4.4 KiB
Python
127 lines
4.4 KiB
Python
|
from __future__ import absolute_import, division, print_function, unicode_literals
|
||
|
|
||
|
import logging
|
||
|
import inspect
|
||
|
import os
|
||
|
import shutil
|
||
|
import traceback
|
||
|
|
||
|
from stone.backend import (
|
||
|
Backend,
|
||
|
remove_aliases_from_api,
|
||
|
)
|
||
|
|
||
|
|
||
|
class BackendException(Exception):
|
||
|
"""Saves the traceback of an exception raised by a backend."""
|
||
|
|
||
|
def __init__(self, backend_name, tb):
|
||
|
"""
|
||
|
:type backend_name: str
|
||
|
:type tb: str
|
||
|
"""
|
||
|
super(BackendException, self).__init__()
|
||
|
self.backend_name = backend_name
|
||
|
self.traceback = tb
|
||
|
|
||
|
|
||
|
class Compiler(object):
|
||
|
"""
|
||
|
Applies a collection of backends found in a single backend module to an
|
||
|
API specification.
|
||
|
"""
|
||
|
|
||
|
backend_extension = '.stoneg'
|
||
|
|
||
|
def __init__(self,
|
||
|
api,
|
||
|
backend_module,
|
||
|
backend_args,
|
||
|
build_path,
|
||
|
clean_build=False):
|
||
|
"""
|
||
|
Creates a Compiler.
|
||
|
|
||
|
:param stone.ir.Api api: A Stone description of the API.
|
||
|
:param backend_module: Python module that contains at least one
|
||
|
top-level class definition that descends from a
|
||
|
:class:`stone.backend.Backend`.
|
||
|
:param list(str) backend_args: A list of command-line arguments to
|
||
|
pass to the backend.
|
||
|
:param str build_path: Location to save compiled sources to. If None,
|
||
|
source files are compiled into the same directories.
|
||
|
:param bool clean_build: If True, the build_path is removed before
|
||
|
source files are compiled into them.
|
||
|
"""
|
||
|
self._logger = logging.getLogger('stone.compiler')
|
||
|
|
||
|
self.api = api
|
||
|
self.backend_module = backend_module
|
||
|
self.backend_args = backend_args
|
||
|
self.build_path = build_path
|
||
|
|
||
|
# Remove existing build directory if it's a clean build
|
||
|
if clean_build and os.path.exists(self.build_path):
|
||
|
logging.info('Cleaning existing build directory %s...',
|
||
|
self.build_path)
|
||
|
shutil.rmtree(self.build_path)
|
||
|
|
||
|
def build(self):
|
||
|
"""Creates outputs. Outputs are files made by a backend."""
|
||
|
if os.path.exists(self.build_path) and not os.path.isdir(self.build_path):
|
||
|
self._logger.error('Output path must be a folder if it already exists')
|
||
|
return
|
||
|
Compiler._mkdir(self.build_path)
|
||
|
self._execute_backend_on_spec()
|
||
|
|
||
|
@staticmethod
|
||
|
def _mkdir(path):
|
||
|
"""
|
||
|
Creates a directory at path if it doesn't exist. If it does exist,
|
||
|
this function does nothing. Note that if path is a file, it will not
|
||
|
be converted to a directory.
|
||
|
"""
|
||
|
try:
|
||
|
os.makedirs(path)
|
||
|
except OSError as e:
|
||
|
if e.errno != 17:
|
||
|
raise
|
||
|
|
||
|
@classmethod
|
||
|
def is_stone_backend(cls, path):
|
||
|
"""
|
||
|
Returns True if the file name matches the format of a stone backend,
|
||
|
ie. its inner extension of "stoneg". For example: xyz.stoneg.py
|
||
|
"""
|
||
|
path_without_ext, _ = os.path.splitext(path)
|
||
|
_, second_ext = os.path.splitext(path_without_ext)
|
||
|
return second_ext == cls.backend_extension
|
||
|
|
||
|
def _execute_backend_on_spec(self):
|
||
|
"""Renders a source file into its final form."""
|
||
|
|
||
|
api_no_aliases_cache = None
|
||
|
for attr_key in dir(self.backend_module):
|
||
|
attr_value = getattr(self.backend_module, attr_key)
|
||
|
if (inspect.isclass(attr_value) and
|
||
|
issubclass(attr_value, Backend) and
|
||
|
not inspect.isabstract(attr_value)):
|
||
|
self._logger.info('Running backend: %s', attr_value.__name__)
|
||
|
backend = attr_value(self.build_path, self.backend_args)
|
||
|
|
||
|
if backend.preserve_aliases:
|
||
|
api = self.api
|
||
|
else:
|
||
|
if not api_no_aliases_cache:
|
||
|
api_no_aliases_cache = remove_aliases_from_api(self.api)
|
||
|
api = api_no_aliases_cache
|
||
|
|
||
|
try:
|
||
|
backend.generate(api)
|
||
|
except Exception:
|
||
|
# Wrap this exception so that it isn't thought of as a bug
|
||
|
# in the stone parser, but rather a bug in the backend.
|
||
|
# Remove the last char of the traceback b/c it's a newline.
|
||
|
raise BackendException(
|
||
|
attr_value.__name__, traceback.format_exc()[:-1])
|