Source code for curry.toolchain._makecurry

from .. import config
from . import plans, _findcurry, _productlock
from ..utility import formatDocstring
import importlib.util, logging, os, shutil, sys, time

__all__ = ['compile_seconds', 'makecurry']
logger = logging.getLogger(__name__)

class CompileClock(object):
  '''
  Accumulates the wall time this process spends in the steps of the
  toolchain: the Curry front end, the ICurry-JSON conversion, the code
  generator, and the C++ compiler.  A nested call (a step that imports a
  package) counts once, within the outermost call.  ``Interpreter.stats``
  reports the total as ``compile``.
  '''
  def __init__(self):
    self.seconds = 0.0
    self._depth = 0
    self._start = None

  def __enter__(self):
    if self._depth == 0:
      self._start = time.perf_counter()
    self._depth += 1
    return self

  def __exit__(self, *exc_info):
    self._depth -= 1
    if self._depth == 0:
      self.seconds += time.perf_counter() - self._start

compile_clock = CompileClock()

[docs] def compile_seconds(): ''' The seconds this process has spent in the steps of the toolchain. Zero when every file was current. ''' return compile_clock.seconds
[docs] @formatDocstring(config.python_package_name()) def makecurry(plan, name, currypath=None, **kwds): ''' Run the build toolchain for a Curry target. Following this, the specified file is up-to-date and can be loaded. This function uses the timestamps of prerequisite files to avoid repeating steps. Args: plan: The build plan. name: The module or source file name. currypath: A sequence of paths to search (i.e., CURRYPATH split on ':'). By default, ``{0}.path`` is used. is_sourcefile: If true, the name arguments is interpreted as a source file. Otherwise, it is interpreted as a module name. **kwds: See documentation for :ref:`sprite-make`. Returns: The name of the final file in the build pipeline. ''' if currypath is None: from .. import path as currypath # A program given as a source file has its directory searched first, as # PAKCS searches (_findcurry.program_path): the steps import the imports # of the module through this path. currypath = _findcurry.program_path(name, currypath, **kwds) do_tidy = kwds.pop('tidy', False) output = kwds.pop('output', None) kwds['zip'] = bool(kwds.get('zip', plan.flags & plans.ZIP_JSON)) with ToolchainContext(tidy=do_tidy, output=output) as pipeline: pipeline.currentfile = _findcurry.currentfile( plan, name, currypath, **kwds ) if not os.path.isdir(pipeline.currentfile): Maker(plan, pipeline, name, currypath, kwds).make() return pipeline.currentfile
class Maker(object): ''' Executes a compilation plan. ''' def __init__(self, plan, pipeline, name, currypath, kwds): self.plan = plan self.pipeline = pipeline self.name = name self.currypath = currypath self.kwds = kwds @property def current_position(self): return self.plan.position(self.pipeline.currentfile) @property def done(self): position = self.current_position if position == len(self.plan): return True # A step may end the plan at its input: the C++ backend interprets a # module from its JSON instead of compiling it (see Json2Cpp.ends_plan). step = self.plan.stages[position].step ends_plan = getattr(step, 'ends_plan', None) return ends_plan is not None and ends_plan(self.pipeline.currentfile) def make(self): ''' Runs the steps of the plan from the current file to the end. Before each step the plan may place the cached products of the module instead (``Plan.restore``; the product cache), also before a step that would end the plan at its input. The clock counts the steps; a restore that placed files counts itself. A plan that is done with nothing to restore costs no time on the clock. The steps run under the lock of the products of the module (``_productlock``): a second process that compiles the same module waits for the first, and once it holds the lock it looks at the files again, so when the first wrote a current object it makes nothing. ''' if self._restored_and_done(): return with _productlock.locked(self.pipeline.currentfile) as waited: if waited: self.pipeline.currentfile = _findcurry.currentfile( self.plan, self.name, self.currypath, **self.kwds ) while True: if self._restored_and_done(): return stage = self.plan.stages[self.current_position] with compile_clock: self.pipeline.currentfile = stage.step( self.pipeline.currentfile, self.currypath, **self.kwds ) if not self.done: self.pipeline.intermediates.append(self.pipeline.currentfile) def _restored_and_done(self): ''' Places the cached products of the module, if the cache holds them, and tells whether the plan is done. ''' restored = self.plan.restore( self.pipeline.currentfile, self.currypath, **self.kwds ) if restored is not None: self.pipeline.currentfile = restored return self.done class ToolchainContext(object): ''' Context manager to handle a toolchain invocation. Handles the removal of intermediate files. Moves the output file to its proper location, if the external tool could not manage that. ''' def __init__(self, currentfile=None, tidy=False, output=None): self.currentfile = currentfile self.intermediates = [] self.output = output self.tidy = tidy def __enter__(self): return self def __exit__(self, excty, excva, exctb): if excty is None: # Move the file if "output" was specified. if self.copy_required: shutil.copy(self.currentfile, self.output) if self.currentfile not in self.intermediates: self.intermediates.append(self.currentfile) if self.tidy: for intermediate in self.intermediates: logger.debug('Removing intermediate %r', intermediate) os.unlink(intermediate) if intermediate.endswith('.py'): # The toolchain writes the bytecode cache beside a Python file. cache = importlib.util.cache_from_source(intermediate) if os.path.exists(cache): os.unlink(cache) @property def copy_required(self): if self.output is None or self.currentfile is None: return False else: return not (os.path.exists(self.output) and os.path.samefile(self.output, self.currentfile))