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))