Source code for craftr

# Copyright (C) 2016  Niklas Rosenstein
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
''' Craftr is a powerful meta build system for Ninja. '''

__author__ = 'Niklas Rosenstein <rosensteinniklas(at)gmail.com>'
__version__ = '1.0.0'

import os
import sys
if sys.version < '3.4':
  raise EnvironmentError('craftr requires Python3.4')

from os import environ
from craftr import magic

session = magic.new_context('session')
module = magic.new_context('module')

import craftr
import collections
import functools
import os
import stat
import types
import warnings

# This object is used to indicate a state where a parameter was
# not specified. This is used when None would be an accepted value.
_sentinel = object()


[docs]class Session(object): ''' This class manages a build session and encapsulates all Craftr modules and :class:`Targets<Target>`. .. attribute:: cwd The original working directory from which Craftr was invoked, or the directory specified with the ``-p`` command-line option. This is different than the current working directory since Craftr changes to the build directory immediately. .. attribute:: env A dictionary of environment variables, initialized as a copy of :data:`os.environ`. In a Craftfile, you can use :data:`os.environ` or the alias :data:`craftr.environ` instead, which is more convenient that accessing ``session.env``. .. attribute:: path A list of search paths for Craftr extension modules. See :doc:`ext`. .. attribute:: modules A dictionary of Craftr extension modules. Key is the module name without the ``craftr.ext.`` prefix. .. attribute:: targets A dictionary mapping the full identifier to :class:`Target` objects that have been declared during the build session. When the Session is created, a ``clean`` Target which calls ``ninja -t clean`` is always created automatically. .. attribute:: server An :class:`rts.CraftrRuntimeServer` object that is started when the session context is entered with :func:`magic.enter_context` and stopped when the context is exited. See :meth:`on_context_enter`. .. attribute:: server_bind A tuple of ``(host, port)`` which the :attr:`server` will be bound to when it is started. Defaults to None, in which case the server is bound to the localhost on a random port. .. attribute:: rts_funcs A dictionary that maps target names to functions. The function will be invoked when you call ``craftr-rts <func_name>`` (note: the Craftr Runtime Server must be running and the environment variable ``CRAFTR_RTS`` must be set). The functions added to this dictionary must accept a list of command-line arguments. .. attribute:: ext_importer A :class:`ext.CraftrImporter` object that handles the importing of Craftr extension modules. See :doc:`ext`. .. attribute:: var A dictionary of variables that will be exported to the Ninja manifest. .. attribute:: verbosity The logging verbosity level. Defaults to 0. Used by the logging functions :func:`debug`, :func:`info`, :func:`warn` and :func:`error`. .. attribute:: strace_depth: The logging functions may print a stack trace of the log call when the verbosity is high enough. This defines the depth of the stack trace. Defaults to 3. .. attribute:: export This is set to True when the ``-e`` option was specified on the command-line, meaning that a Ninja manifest will be exported. Some projects eventually need to export additional files before running Ninja, for example with :meth:`TargetBuilder.write_command_file`. ''' def __init__(self, cwd=None, path=None, server_bind=None, verbosity=0, strace_depth=3, export=False): super().__init__() self.cwd = cwd or os.getcwd() self.env = environ.copy() self.server = rts.CraftrRuntimeServer(self) self.server_bind = server_bind self.rts_funcs = {} self.ext_importer = ext.CraftrImporter(self) self.path = [craftr.path.join(craftr.path.dirname(__file__), 'lib')] self.modules = {} self.targets = {} self.var = {} self.verbosity = verbosity self.strace_depth = strace_depth self.export = export if path is not None: self.path.extend(path) self.targets['clean'] = Target( command = 'ninja -t clean', inputs = None, outputs = None, name = 'clean', module = None, explicit = True)
[docs] def exec_if_exists(self, filename): ''' Executes *filename* if it exists. Used for running the Craftr environment files before the modules are loaded. Returns None if the file does not exist, a `types.ModuleType` object if it was executed. ''' if not os.path.isfile(filename): return None # Create a fake module so we can enter a module context # for the environment script. temp_mod = types.ModuleType('craftr.ext.__temp__:' + filename) temp_mod.__file__ = filename init_module(temp_mod) temp_mod.__name__ = '__craftenv__' with open(filename, 'r') as fp: code = compile(fp.read(), filename, 'exec') with magic.enter_context(module, temp_mod): exec(code, vars(module)) return temp_mod
[docs] def update(self): ''' Alias for :meth:`Session.ext_importer.update()< ext.CraftrImporter.update>`. ''' self.ext_importer.update()
[docs] def start_server(self): ''' Start the Craftr RTS server (see :attr:`Session.server`). It will automatically be stopped when the session context is exited. ''' # Start the Craftr Server to enable cross-process invocation # of Python functions. if self.server_bind: self.server.bind(*self.server_bind) else: self.server.bind() self.server.serve_forever_async() environ['CRAFTR_RTS'] = '{0}:{1}'.format(self.server.host, self.server.port) debug('Started Craftr RTS at {0}:{1}'.format(self.server.host, self.server.port))
def _stop_server(self): if self.server.running: self.server.stop() self.server.close()
[docs] def on_context_enter(self, prev): ''' Called when entering the Session context with :func:`magic.enter_context`. Does the following things: * Sets up the :data`os.environ` with the values from :attr:`Session.env` * Adds the :attr:`Session.ext_importer` to :data:`sys.meta_path` * Starts the Craftr Runtime Server (:attr:`Session.server`) and sets the ``CRAFTR_RTS`` environment variable .. note:: A copy of the original :data:`os.environ` is saved and later restored in :meth:`on_context_leave`. The :data:`os.environ` object can not be replaced by another object, that is why we change its values in-place. ''' if prev is not None: raise RuntimeError('session context can not be nested') # We can not change os.environ effectively, we must update the # dictionary instead. self._old_environ = environ.copy() environ.clear() environ.update(self.env) self.env = environ sys.meta_path.append(self.ext_importer) self.update()
[docs] def on_context_leave(self): ''' Called when the context manager entered with :func:`magic.enter_context` is exited. Undos all of the stuff that :meth:`on_context_enter` did and more. * Stop the Craftr Runtime Server * Restore the :data:`os.environ` dictionary * Removes all ``craftr.ext.`` modules from :data:`sys.modules` and ensures they are in :attr:`Session.modules` (they are expected to be put there from the :class:`ext.CraftrImporter`). ''' self._stop_server() # Restore the original values of os.environ. self.env = environ.copy() environ.clear() environ.update(self._old_environ) del self._old_environ sys.meta_path.remove(self.ext_importer) for key, module in list(sys.modules.items()): if key.startswith('craftr.ext.'): name = key[11:] assert name in self.modules and self.modules[name] is module, key del sys.modules[key] try: # Remove the module from the `craftr.ext` modules contents, too. delattr(ext, name.split('.')[0]) except AttributeError: pass
[docs]class Target(object): ''' This class is a direct representation of a Ninja rule and the corresponding in- and output files. Will be rendered into a ``rule`` and one or many ``build`` statements in the Ninja manifest. .. attribute:: name The name of the target. This is usually deduced from the variable the target is assigned to if no explicit name was passed to the :class:`Target` constructor. Note that the actual name of the generated Ninja rule must be read from :attr:`fullname`. .. attribute:: module The Craftr extension module this target belongs to. Defaults to the currently executed module (retrieved from the thread-local :data:`module`). Can be None, but only if there is no module currently being executed. .. attribute:: command A list of strings that represents the command to execute. A string can be passed to the constructor in which case it is parsed with :func:`shell.split`. .. attribute:: inputs A list of filenames that are listed as inputs to the target and that are substituted for ``$in`` and ``$in_newline`` during the Ninja execution. Can be None. The :class:`Target` constructor expands the passed argument with :func:`expand_inputs`, thus also accepts a single filename, Target or a list with Targets and/or filenames. .. attribute:: outputs A list of filenames that are listed as outputs of the target and that are substituted for ``$out`` during the Ninja execution. Can be None. The :class:`Target` constructor accepts a list of filenames or a single filename for this attribute. .. attribute:: implicit_deps A list of filenames that are required to build the Target, additionally to the :attr:`inputs`, but are not expanded by the ``$in`` variable in Ninja. See "Implicit dependencies" in the `Ninja Manual`_. .. attribute:: order_only_deps See "Order-only dependencies" in the `Ninja Manual`_. .. attribute:: foreach If this is set to True, the number of :attr:`inputs` must match the number of :attr:`outputs`. Instead of generating a single ``build`` instruction in the Ninja manifest, an instruction for each input/output pair will be created instead. Defaults to False. .. attribute:: description A description of the Target. Will be added to the generated Ninja rule. Defaults to None. .. attribute:: pool The name of the build pool. Defaults to None. Can be ``"console"`` for Targets that don't actually build files but run a program. Craftr will treat Targets in that pool as if :attr:`explicit` is True. .. attribute:: deps The mode for automatic dependency detection for C/C++ targets. See the "C/C++ Header Depenencies" section in the `Ninja Manual`_. .. attribute:: depfile A filename that contains additional dependencies. .. attribute:: msvc_deps_prefix The MSVC dependencies prefix to be used for the rule. .. attribute:: frameworks A list of :class:`Frameworks<Framework>` that are used by the Target. Rule functions that take other Targets as inputs can include this list. For example, a C++ compiler might add a Framework with ``libs = ['c++']`` to a Target so that the Linker to which the C++ object files target is passed automatically knows to link with the ``c++`` library. Usually, a rule function uses the :class:`TargetBuilder` (which internally uses :meth:`expand_inputs`) to collect all Frameworks used in the input targets. .. attribute:: explicit If True, the target will only be built by Ninja if it is explicitly specified on the command-line or if it is required by another target. Defaults to False. .. attribute:: meta A dictionary of meta variables that can be set from anywhere. Usually, rule functions use this dictionary to promote additional information to the caller, for example what the actual computed output filename of a compilation is. .. automethod:: Target.__lshift__ .. _Ninja Manual: https://ninja-build.org/manual.html ''' def __init__(self, command, inputs=None, outputs=None, implicit_deps=None, order_only_deps=None, foreach=False, description=None, pool=None, var=None, deps=None, depfile=None, msvc_deps_prefix=None, explicit=False, frameworks=None, meta=None, module=None, name=None): if not module and craftr.module: module = craftr.module() if not name: name = Target._get_name(module) if isinstance(command, str): command = shell.split(command) else: command = _check_list_of_str('command', command) if not command: raise ValueError('command can not be empty') if inputs is not None: if isinstance(inputs, str): inputs = [inputs] inputs = expand_inputs(inputs) inputs = _check_list_of_str('inputs', inputs) if outputs is not None: if isinstance(outputs, str): outputs = [outputs] elif callable(outputs): outputs = outputs(inputs) outputs = _check_list_of_str('outputs', outputs) if foreach and len(inputs) != len(outputs): raise ValueError('len(inputs) must match len(outputs) in foreach Target') if implicit_deps is not None: implicit_deps = _check_list_of_str('implicit_deps', implicit_deps) if order_only_deps is not None: order_only_deps = _check_list_of_str('order_only_deps', order_only_deps) if meta is None: meta = {} if not isinstance(meta, dict): raise TypeError('meta must be a dictionary') self.module = module self.name = name self.command = command self.inputs = inputs self.outputs = outputs self.implicit_deps = implicit_deps or [] self.order_only_deps = order_only_deps or [] self.foreach = foreach self.pool = pool self.description = description self.deps = deps self.depfile = depfile self.msvc_deps_prefix = msvc_deps_prefix self.frameworks = expand_frameworks(frameworks or []) self.explicit = explicit self.meta = meta if module: targets = module.__session__.targets if self.fullname in targets: raise ValueError('target {0!r} already exists'.format(self.fullname)) targets[self.fullname] = self def __repr__(self): pool = ' in "{0}"'.format(self.pool) if self.pool else '' command = ' running "{0}"'.format(self.command[0]) return '<Target {self.fullname!r}{command}{pool}>'.format(**locals())
[docs] def __lshift__(self, other): ''' Shift operator to add to the list of :attr:`implicit_deps`. .. note:: If *other* is or contains a :class:`Target`, the targets frameworks are *not* added to this Target's framework list! ''' # xxx: Should we append the frameworks of the input targets to the # frameworks of this target? self.implicit_deps += expand_inputs(other) return self
__ilshift__ = __lshift__ @staticmethod def _get_name(module): # Always use the frame of the current module, though. if craftr.module() != module: raise RuntimeError('target name deduction only available when ' 'used from the currently executed module') return magic.get_assigned_name(magic.get_module_frame(module, allow_local=False)) @property def fullname(self): ''' The full identifier of the Target. If the Target is assigned to a :attr:`module`, this is the module name and the :attr:`Target.name`, otherwise the same as :attr:`Target.name`. ''' if self.module: return self.module.project_name + '.' + self.name else: return self.name
[docs]class TargetBuilder(object): ''' This is a helper class to make it easy to implement rule functions that create a :class:`Target`. Rule functions usually depend on inputs (being files or other Targets that can also contain additional frameworks), rule-level settings and :class:`Frameworks<Framework>`. The TargetBuilder takes all of this into account and prepares the data conveniently. :param inputs: Inputs for the target. Processed by :func:`expand_inputs`. Use :attr:`TargetBuilder.inputs` instead of the argument you passed here to access the inputs. Must be a string, :class:`Target`, list or None. :param frameworks: A list of frameworks to take into account. :param kwargs: Additional keyword-arguments that have been passed to the rule-function. These will be turned into a new :class:`Framework` object. :meth:`create_target` will check if all arguments of this dictionary have been taken into account and will yield a warning if not. :param module: :param name: :param stacklevel: .. attribute:: caller .. attribute:: inputs .. attribute:: frameworks .. attribute:: kwargs .. attribute:: options A :class:`FrameworkJoin` object that is used to read settings from the list of frameworks collected from the input Targets, the additional frameworks specified to the :class:`TargetBuilder` constructor and the specified ``kwargs`` dictionary. .. attribute:: module .. attribute:: name The name of the Target that is being built. .. attribute:: target_attrs A dictonary of arguments that are set to the target after construction in :meth:`create_target`. Can only set attributes that are already attributes of the :class:`Target`. .. attribute:: meta Meta data for the Target that is passed directly to :attr:`Target.meta`. .. note:: If the *meta* parameter to the constructor is None, it will be read from the *kwargs* dictionary (thus inheriting the option from the called rule function). .. automethod:: TargetBuilder.__getitem__ ''' def __init__(self, inputs, frameworks, kwargs, meta=None, module=None, name=None, stacklevel=1): if meta is None: meta = {} if not isinstance(meta, dict): raise TypeError('meta must be a dictionary') self.meta = meta self.caller = magic.get_caller(stacklevel + 1) frameworks = list(frameworks) self.inputs = expand_inputs(inputs, frameworks) self.frameworks = expand_frameworks(frameworks) self.kwargs = kwargs self.options = FrameworkJoin(Framework(self.caller, kwargs), *self.frameworks) self.module = module or craftr.module() self.name = name self.target_attrs = {} if not self.name: try: self.name = Target._get_name(self.module) except ValueError: index = 0 while True: name = '{0}_{1:0>4}'.format(self.caller, index) if self.module.project_name + '.' + name not in session.targets: break index += 1 self.name = name @property def fullname(self): ''' The full name of the Target that is being built. ''' return self.module.project_name + '.' + self.name @property def target(self): ''' A dictonary of arguments that are set to the target after construction in :meth:`create_target`. Can only set attributes that are already attributes of the :class:`Target`. .. deprecated:: Use :attr:`target_attrs` instead. ''' return self.target_attrs
[docs] def __getitem__(self, key): ''' Alias for :meth:`FrameworkJoin.__getitem__`. ''' return self.options[key]
[docs] def get(self, key, default=None): ''' Alias for :meth:`FrameworkJoin.get`. ''' return self.options.get(key, default)
[docs] def merge(self, key): ''' Alias for :meth:`FrameworkJoin.merge`. ''' return self.options.merge(key)
[docs] def log(self, level, *args, stacklevel=1, **kwargs): ''' Log function that includes the :attr:`fullname`. ''' module_name = '{0}'.format(self.module.project_name, self.name) log(level, *args, module_name=module_name, stacklevel=stacklevel + 1, **kwargs)
[docs] def invalid_option(self, option_name, option_value=_sentinel, cause=None): ''' Use this method in a rule function if you found the value of an option has an invalid option. You should raise a :class:`ValueError` on a fatal error instead. ''' if option_value is _sentinel: option_value = self[option_name] message = 'invalid option: {0} = {1!r}'.format(option_name, option_value) if cause: message = '{0} ({1})'.format(message, cause) self.log('warn', message, stacklevel=2)
[docs] def add_framework(self, __fw_or_name, __fw_dict=None, **kwargs): ''' Add or create a new Framework and add it to :attr:`options` and :attr:`frameworks`. ''' if not isinstance(__fw_or_name, Framework): fw = Framework(__fw_or_name, __fw_dict, **kwargs) else: fw = __fw_or_name if fw not in self.frameworks: self.frameworks.append(fw) self.options += [fw] return fw
[docs] def expand_inputs(self, inputs): ''' Wrapper for :func:`expand_inputs` that will add the Frameworks extracted from the *inputs* to :attr:`options` and :attr:`frameworks`. ''' frameworks = [] result = expand_inputs(inputs, frameworks) self.frameworks += frameworks self.options += frameworks return result
[docs] def create_target(self, command, inputs=None, outputs=None, **kwargs): ''' Create a :class:`Target` and return it. :param command: The command-line for the Target. :param inputs: The inputs for the Target. If None, the :attr:`TargetBuilder.inputs` will be used instead. :param outputs: THe outputs for the Target. :param kwargs: Additional keyword arguments for the Target constructor. Make sure that none conflicts with the :attr:`target` dictionary. .. note:: This function will yield a warning when there are any keys in the :attr:`kwargs` dictionary that have not been read from the :class:`options`. ''' # Complain about unhandled options. unused_options = self.kwargs.keys() - self.options.used_keys if unused_options: self.log('info', 'unused options for {0}():'.format(self.caller), unused_options, stacklevel=2) if inputs is None: inputs = self.inputs kwargs.setdefault('frameworks', self.frameworks) target = Target(command=command, inputs=inputs, outputs=outputs, meta=self.meta, module=self.module, name=self.name, **kwargs) for key, value in self.target_attrs.items(): # We can only set attributes that the target already has. getattr(target, key) setattr(target, key, value) return target
[docs] def write_command_file(self, arguments, suffix=None, always=False): ''' Writes a file to the `.cmd` folder in the builder directory (ie. the current directory) that contains the command-line arguments specified in *arguments*. The name of that file is the name of the Target that is created with this builder. Optionally, a suffix for that file can be specified to be able to write multiple such files. Returns the filename of the generated file. If *always* is set to True, the file will always be created even if `Session.export` is set to False. ''' filename = '.cmd/' + self.fullname if suffix: filename += suffix if not always and not session.export: return path.makedirs('.cmd') with open(filename, 'w') as fp: for arg in arguments: fp.write(shell.quote(arg)) fp.write(' ') return filename
[docs] def write_multicommand_file(self, commands, cwd = None, exit_on_error = True, suffix = None, always = False): ''' Write a platform dependent script that executes the specified *commands* in order. If *exit_on_error* is True, the script will exit if an error is encountered while executing the commands. Returns a list representing the command-line to run the script. :param commands: A list of strings or command lists that are written into the script file. :param cwd: Optionally, the working directory to change to when the script is executed. :param exit_on_error: If this is True, the script will exit immediately if any command returned a non-zero exit code. :param suffix: An optional file suffix. Note that on Windows, ``.cmd`` is added to the filename after that suffix. :param always: If this is true, the file is always created, not only if a Ninja manifest is being exported (see :attr:`Session.export`). :return: A tuple of two elements. The first element is a command list that represents the command used to invoke the created script. The second element is the actual command file that was written. ''' from craftr.ext import platform filename = '.cmd/' + self.fullname if suffix: filename += suffix filename = path.normpath(filename, abs=False) if platform.name == platform.WIN32: filename += '.cmd' result = ['cmd', '/Q', '/c', filename] else: result = [filename] if always or session.export: # Make sure we only have strings of commands. prep_commands = [] for command in commands: if not isinstance(command, str): command = ' '.join(map(shell.quote, command)) prep_commands.append(command) commands = prep_commands # Write the script file. path.makedirs('.cmd') with open(filename, 'w') as fp: if platform.name == platform.WIN32: if cwd: fp.write('cd ' + shell.quote(cwd) + '\n\n') for cmd in commands: # XXX For some reason, we need to invoke these commands # using "cmd /Q /c" too instead of just the command, otherwise # there seem to be some problems for example with CMake which # causes the bash script to exit immediately after CMake # is finished. fp.write('cmd /Q /c ' + cmd) fp.write('\n') fp.write('if %errorlevel% neq 0 exit %errorlevel%\n\n') else: # XXX Are there differences from bash to other shells we need to # take into account? fp.write('#!' + utils.find_program(environ['SHELL']) + '\n') fp.write('set -e\n') if cwd: fp.write('cd ' + shell.quote(cwd) + '\n') fp.write('\n') for cmd in commands: fp.write(cmd) fp.write('\n') os.chmod(filename, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR | stat.S_IRGRP | stat.S_IWGRP | stat.S_IROTH) # rwxrw-r-- return result, filename
[docs]class Framework(dict): ''' A Framework represents a set of options that are to be taken into account by compiler classes. Eg. you might create a framework that contains the additional information and options required to compile code using OpenCL and pass that to the compiler interface. Compiler interfaces may also add items to :attr:`Target.frameworks` that can be taken into account by other target rules. :func:`expand_inputs()` returns a list of frameworks that are being used in the inputs. Use the :class:`FrameworkJoin` class to create an object to process the data from multiple frameworks. :param __fw_name: The name of the Framework. If omitted, the assigned name of the calling module will be used. :param __init_dict: A dictionary to initialize the Framework with. :param kwargs: Additional key/value pairs for the Framework. ''' def __init__(self, __fw_name=None, __init_dict=None, **kwargs): super().__init__() if not __fw_name: __fw_name = Target._get_name(module()) if __init_dict is not None: self.update(__init_dict) self.update(kwargs) self.name = __fw_name def __repr__(self): return 'Framework(name={0!r}, {1})'.format(self.name, super().__repr__())
[docs]class FrameworkJoin(object): ''' This class is used to process a set of :class:`Frameworks<Framework>` and retreive relevant information from it. For some options, you might want to read the first value that is specified in any of the frameworks, for another you may want to create a list of all values in the frameworks. This is what the FrameworkJoin allows you to do. .. note:: The :class:`FrameworkJoin` does not use :func:`expand_frameworks` but uses the list of frameworks passed to the constructor as-is. .. code-block:: python >>> fw1 = Framework('fw2', defines=['DEBUG']) >>> fw2 = Framework(defines=['DO_STUFF']) >>> print(fw2.name) 'fw2' >>> FrameworkJoin(fw1, fw2).merge('defines') ['DEBUG', 'DO_STUFF'] .. attribute:: used_keys A set of keys that have been accessed via :meth:`__getitem__`, :meth:`get` and :meth:`merge`. .. attribute:: frameworks The list of :class:`Framework` objects. .. automethod:: FrameworkJoin.__iadd__ ''' def __init__(self, *frameworks): super().__init__() self.used_keys = set() self.frameworks = [] self += frameworks
[docs] def __iadd__(self, frameworks): for fw in frameworks: if not isinstance(fw, Framework): raise TypeError('expected Framework, got {0}'.format(type(fw).__name__)) if fw not in self.frameworks: self.frameworks.append(fw) return self
def __getitem__(self, key): self.used_keys.add(key) for fw in self.frameworks: try: return fw[key] except KeyError: pass raise KeyError(key)
[docs] def get(self, key, default=None): ''' Get the first available value of *key* from the frameworks. ''' try: return self[key] except KeyError: return default
[docs] def merge(self, key): ''' Merge all values of *key* in the frameworks into one list, assuming that every key is a non-string sequence and can be appended to a list. ''' self.used_keys.add(key) result = [] for fw in self.frameworks: try: value = fw[key] except KeyError: continue if not isinstance(value, collections.Sequence) or isinstance(value, str): raise TypeError('expected a non-string sequence for {0!r} ' 'in framework {1!r}, got {2}'.format(key, fw.name, type(value).__name__)) result += value return result
[docs] def keys(self): ''' Returns a set of all keys in all frameworks. ''' keys = set() for fw in self.frameworks: keys |= fw.keys() return keys
class ModuleError(RuntimeError): def __init__(self, module=None): self.module = module or craftr.module() class ModuleReturn(Exception): pass
[docs]def return_(): ''' Raise a :class:`ModuleReturn` exception, causing the module execution to be aborted and returning back to the parent module. Note that this function can only be called from a Craftr modules global stack frame, otherwise a :class:`RuntimeError` will be raised. ''' if magic.get_frame(1).f_globals is not vars(module): raise RuntimeError('return_() can not be called outside the current ' 'modules global stack frame') raise ModuleReturn()
[docs]def expand_inputs(inputs, frameworks=None): ''' Expands a list of inputs into a list of filenames. An input is a string (filename) or a :class:`Target` object from which the :attr:`Target.outputs` are used. Returns a list of strings. If *frameworks* is specified, it must be a :class:`list` to which the frameworks of all input :class:`Target` objects will be appended. The frameworks need to be expanded with :func:`expand_frameworks`. ''' if frameworks is not None and not isinstance(frameworks, list): raise TypeError('frameworks must be None or list') result = [] # We also accept a single string or Target as input. if isinstance(inputs, (str, Target)): inputs = [inputs] for item in inputs: if isinstance(item, Target): if frameworks is not None: frameworks += item.frameworks result += item.outputs elif isinstance(item, str): result.append(item) else: raise TypeError('input must be Target or str, got {0}'.format(type(item).__name__)) return result
[docs]def expand_frameworks(frameworks, result=None): ''' Given a list of :class:`Framework` objects, this function creates a new list that contains all objects of *frameworks* and additionally all objects that are listed in each of the frameworks ``"frameworks"`` key recursively. Duplicates are also elimated. ''' if result is None: result = [] for fw in frameworks: # xxx: does this compare the dictionary contents? We do NOT want that. if fw not in result: result.append(fw) expand_frameworks(fw.get('frameworks', []), result) return result
[docs]def import_file(filename): ''' Import a Craftr module by filename. ''' if not path.isabs(filename): filename = path.local(filename) return session.ext_importer.import_file(filename)
[docs]def import_module(modname, globals=None, fromlist=None): ''' Similar to `importlib.import_module()`, but this function can also improt contents of *modname* into *globals*. If *globals* is specified, the module will be directly imported into the dictionary. If *fromlist* list `*`, a wildcard import into *globals* will be perfromed. *fromlist* can also be a list of names to import. This function always returns the root module. ''' wildcard = False if fromlist == '*': wildcard = True fromlist = None root = module = __import__(modname, globals, fromlist=fromlist) for part in modname.split('.')[1:]: module = getattr(module, part) if globals is not None: if wildcard: if hasattr(module, '__all__'): for key in module.__all__: globals[key] = getattr(module, key) else: for key, value in vars(module).items(): if not key.startswith('_'): globals[key] = value elif fromlist is not None: for key in fromlist: globals[key] = value else: globals[modname.partition('.')[0]] = root return root
def memoize_tool(func): ''' Decorator for functions that take the path to a program as an argument and extract information from it such as its version or raising a :class:`craftr.ext.compiler.ToolDetectionError` if the tool could not be detected. Basically, this is just a memoize decorator but it applies :func:`path.normpath` to the argument passed to the wrapped function. :-) ''' cache = {} @functools.wraps(func) def wrapper(program): program = path.normpath(program, abs=False) try: result = cache[program] except KeyError: result = cache[program] = func(program) return result return wrapper def init_module(module): ''' Called when a craftr module is being imported before it is executed to initialize its contents. ''' assert module.__name__.startswith('craftr.ext.') module.__session__ = session() module.project_dir = path.dirname(module.__file__) module.project_name = module.__name__[11:] def finish_module(module): ''' Called when a craftr extension module was imported. This function makes sure that there is a `__all__` member on the module that excludes all the built-in names and that are not module objects. ''' if not hasattr(module, '__all__'): module.__all__ = [] for key in dir(module): if key.startswith('_') or key in ('project_dir', 'project_name'): continue if isinstance(getattr(module, key), types.ModuleType): continue module.__all__.append(key) def _check_list_of_str(name, value): ''' Helper function to check if a given *value* is a list of strings or is convertible to a list of strings. Will raise a `ValueError` if not using the specified *name* as a hint. ''' if not isinstance(value, str) and isinstance(value, collections.Iterable): value = list(value) if not isinstance(value, list): raise TypeError('expected list of str for {0}, got {1}'.format( name, type(value).__name__)) for item in value: if not isinstance(item, str): raise TypeError('expected list of str for {0}, found {1} inside'.format( name, type(item).__name__)) return value
[docs]def craftr_min_version(version_string): ''' Ensure the current version of Craftr is at least the version specified with *version_string*, otherwise call :func:`error()`. ''' if __version__ < version_string: error('requires at least Craftr v{0}'.format(version_string))
from craftr.logging import log, debug, info, warn, error from craftr import ext, path, shell, ninja, rts, utils __all__ = ['session', 'module', 'path', 'shell', 'utils', 'environ', 'Target', 'TargetBuilder', 'Framework', 'FrameworkJoin', 'debug', 'info', 'warn', 'error', 'return_', 'expand_inputs', 'import_file', 'import_module', 'memoize_tool', 'craftr_min_version']