| # This Source Code Form is subject to the terms of the Mozilla Public |
| # License, v. 2.0. If a copy of the MPL was not distributed with this |
| # file, You can obtain one at http://mozilla.org/MPL/2.0/. |
| |
| from __future__ import absolute_import, unicode_literals |
| |
| from abc import ( |
| ABCMeta, |
| abstractmethod, |
| ) |
| |
| import errno |
| import os |
| import time |
| |
| from contextlib import contextmanager |
| |
| from mach.mixin.logging import LoggingMixin |
| |
| import mozpack.path as mozpath |
| from ..preprocessor import Preprocessor |
| from ..pythonutil import iter_modules_in_path |
| from ..util import FileAvoidWrite |
| from ..frontend.data import ContextDerived |
| from .configenvironment import ConfigEnvironment |
| from mozbuild.base import ExecutionSummary |
| |
| |
| class BuildBackend(LoggingMixin): |
| """Abstract base class for build backends. |
| |
| A build backend is merely a consumer of the build configuration (the output |
| of the frontend processing). It does something with said data. What exactly |
| is the discretion of the specific implementation. |
| """ |
| |
| __metaclass__ = ABCMeta |
| |
| def __init__(self, environment): |
| assert isinstance(environment, ConfigEnvironment) |
| |
| self.populate_logger() |
| |
| self.environment = environment |
| |
| # Files whose modification should cause a new read and backend |
| # generation. |
| self.backend_input_files = set() |
| |
| # Files generated by the backend. |
| self._backend_output_files = set() |
| |
| # Previously generated files. |
| self._backend_output_list_file = mozpath.join(environment.topobjdir, |
| 'backend.%s' % self.__class__.__name__) |
| self._backend_output_list = set() |
| if os.path.exists(self._backend_output_list_file): |
| l = open(self._backend_output_list_file).read().split('\n') |
| self._backend_output_list.update(mozpath.normsep(p) for p in l) |
| |
| self._environments = {} |
| self._environments[environment.topobjdir] = environment |
| |
| # The number of backend files created. |
| self._created_count = 0 |
| |
| # The number of backend files updated. |
| self._updated_count = 0 |
| |
| # The number of unchanged backend files. |
| self._unchanged_count = 0 |
| |
| # The number of deleted backend files. |
| self._deleted_count = 0 |
| |
| # The total wall time spent in the backend. This counts the time the |
| # backend writes out files, etc. |
| self._execution_time = 0.0 |
| |
| # Mapping of changed file paths to diffs of the changes. |
| self.file_diffs = {} |
| |
| self._init() |
| |
| def summary(self): |
| return ExecutionSummary( |
| self.__class__.__name__.replace('Backend', '') + |
| ' backend executed in {execution_time:.2f}s\n ' |
| '{total:d} total backend files; ' |
| '{created:d} created; ' |
| '{updated:d} updated; ' |
| '{unchanged:d} unchanged; ' |
| '{deleted:d} deleted', |
| execution_time=self._execution_time, |
| total=self._created_count + self._updated_count + |
| self._unchanged_count, |
| created=self._created_count, |
| updated=self._updated_count, |
| unchanged=self._unchanged_count, |
| deleted=self._deleted_count) |
| |
| def _init(): |
| """Hook point for child classes to perform actions during __init__. |
| |
| This exists so child classes don't need to implement __init__. |
| """ |
| |
| def consume(self, objs): |
| """Consume a stream of TreeMetadata instances. |
| |
| This is the main method of the interface. This is what takes the |
| frontend output and does something with it. |
| |
| Child classes are not expected to implement this method. Instead, the |
| base class consumes objects and calls methods (possibly) implemented by |
| child classes. |
| """ |
| for obj in objs: |
| obj_start = time.time() |
| if not self.consume_object(obj): |
| raise Exception('Unhandled object of type %s' % type(obj)) |
| self._execution_time += time.time() - obj_start |
| |
| if isinstance(obj, ContextDerived): |
| self.backend_input_files |= obj.context_all_paths |
| |
| # Pull in all loaded Python as dependencies so any Python changes that |
| # could influence our output result in a rescan. |
| self.backend_input_files |= set(iter_modules_in_path( |
| self.environment.topsrcdir, self.environment.topobjdir)) |
| |
| finished_start = time.time() |
| self.consume_finished() |
| self._execution_time += time.time() - finished_start |
| |
| # Purge backend files created in previous run, but not created anymore |
| delete_files = self._backend_output_list - self._backend_output_files |
| for path in delete_files: |
| try: |
| os.unlink(mozpath.join(self.environment.topobjdir, path)) |
| self._deleted_count += 1 |
| except OSError: |
| pass |
| # Remove now empty directories |
| for dir in set(mozpath.dirname(d) for d in delete_files): |
| try: |
| os.removedirs(dir) |
| except OSError: |
| pass |
| |
| # Write out the list of backend files generated, if it changed. |
| if self._deleted_count or self._created_count or \ |
| not os.path.exists(self._backend_output_list_file): |
| with open(self._backend_output_list_file, 'w') as fh: |
| fh.write('\n'.join(sorted(self._backend_output_files))) |
| else: |
| # Always update its mtime. |
| with open(self._backend_output_list_file, 'a'): |
| os.utime(self._backend_output_list_file, None) |
| |
| @abstractmethod |
| def consume_object(self, obj): |
| """Consumes an individual TreeMetadata instance. |
| |
| This is the main method used by child classes to react to build |
| metadata. |
| """ |
| |
| def consume_finished(self): |
| """Called when consume() has completed handling all objects.""" |
| |
| @contextmanager |
| def _write_file(self, path=None, fh=None): |
| """Context manager to write a file. |
| |
| This is a glorified wrapper around FileAvoidWrite with integration to |
| update the summary data on this instance. |
| |
| Example usage: |
| |
| with self._write_file('foo.txt') as fh: |
| fh.write('hello world') |
| """ |
| |
| if path is not None: |
| assert fh is None |
| fh = FileAvoidWrite(path, capture_diff=True) |
| else: |
| assert fh is not None |
| |
| dirname = mozpath.dirname(fh.name) |
| try: |
| os.makedirs(dirname) |
| except OSError as error: |
| if error.errno != errno.EEXIST: |
| raise |
| |
| yield fh |
| |
| self._backend_output_files.add(mozpath.relpath(fh.name, self.environment.topobjdir)) |
| existed, updated = fh.close() |
| if not existed: |
| self._created_count += 1 |
| elif updated: |
| self._updated_count += 1 |
| if fh.diff: |
| self.file_diffs[fh.name] = fh.diff |
| else: |
| self._unchanged_count += 1 |
| |
| @contextmanager |
| def _get_preprocessor(self, obj): |
| '''Returns a preprocessor with a few predefined values depending on |
| the given BaseConfigSubstitution(-like) object, and all the substs |
| in the current environment.''' |
| pp = Preprocessor() |
| srcdir = mozpath.dirname(obj.input_path) |
| pp.context.update(obj.config.substs) |
| pp.context.update( |
| top_srcdir=obj.topsrcdir, |
| srcdir=srcdir, |
| relativesrcdir=mozpath.relpath(srcdir, obj.topsrcdir) or '.', |
| DEPTH=mozpath.relpath(obj.topobjdir, mozpath.dirname(obj.output_path)) or '.', |
| ) |
| pp.do_filter('attemptSubstitution') |
| pp.setMarker(None) |
| with self._write_file(obj.output_path) as fh: |
| pp.out = fh |
| yield pp |