import argparse
import os
import shutil
from abc import ABCMeta, abstractmethod
from typing import Iterable, Iterator, Mapping, Optional
from .context import Context
from .instance import Instance
from .package import Package
from .parallel import Pool
from .util import ResultDict
[docs]class Target(metaclass=ABCMeta):
"""
Abstract base class for target definitions. Built-in derived classes are
listed :doc:`here <targets>`.
Each target must define a :py:attr:`name` attribute that is used to
reference the target on the command line. The name must be unique among all
registered targets. Each target must also implement a number of methods
that are called by :class:`Setup` when running commands.
The :ref:`build <usage-build>` command follows the following steps for each
target:
#. It calls :func:`add_build_args` to include any custom command-line
arguments for this target, and then parses the command-line arguments.
#. It calls :func:`is_fetched` to see if the source code for this target
has been downloaded yet.
#. If ``is_fetched() == False``, it calls :func:`fetch`.
#. It calls :func:`Instance.configure` on the instance that will be passed
to :func:`build`.
#. All packages listed by :func:`dependencies` are built and installed into
the environment (i.e., ``PATH`` and such are set).
#. It calls :func:`build` to build the target binaries.
#. If any post-build hooks are installed by the current instance, it calls
:func:`binary_paths` to get paths to all built binaries. These are then
passed directly to the build hooks.
For the :ref:`run <usage-run>` command:
#. It calls :func:`add_run_args` to include any custom command-line
arguments for this target.
#. If ``--build`` was specified, it performs all build steps above.
#. It calls :func:`Instance.prepare_run` on the instance that will be passed
to :func:`run`.
#. It calls :func:`run` to run the target binaries.
For the :ref:`clean <usage-clean>` command:
#. It calls :func:`is_clean` to see if any build files exist for this target.
#. If ``is_clean() == False``, it calls :func:`clean`.
For the :ref:`report <usage-report>` command:
#. It calls :func:`parse_outfile` for every log file before creating the
report.
Naturally, when defining your own target, all the methods listed above must
have working implementations. Some implementations are optional and some
have a default implementation that works for almost all cases (see docs
below), but the following are mandatory to implement for each new target:
:func:`is_fetched`, :func:`fetch`, :func:`build` and :func:`run`.
"""
#: :class:`str` The target's name, must be unique.
name: str
#: :class:`str` The default reportable field to group by when
# aggregating results.
aggregation_field: str = ""
def __eq__(self, other: object) -> bool:
return isinstance(other, self.__class__) and other.name == self.name
def __hash__(self) -> int:
return hash("target-" + self.name)
[docs] def reportable_fields(self) -> Mapping[str, str]:
"""
Run-time statistics reported by this target. Examples include the
runtime of the benchmark, its memory or CPU utilization, or
benchmarks-specific measurements such as throughput and latency of
requests.
The format is a dictionary mapping the name of the statistic to a
(human-readable) description. For each entry, the name is looked up
in the logs and saved per run.
"""
return {}
[docs] def add_build_args(self, parser: argparse.ArgumentParser) -> None:
"""
Extend the command-line arguments for the :ref:`build <usage-build>`
command with custom arguments for this target. These arguments end up
in the global context, so it is a good idea to prefix them with the
target name to avoid collisions with other targets and instances.
For example, :class:`SPEC2006 <targets.SPEC2006>` defines
``--spec2006-benchmarks`` (rather than ``--benchmarks``).
:param parser: the argument parser to extend
"""
pass
[docs] def add_run_args(self, parser: argparse.ArgumentParser) -> None:
"""
Extend the command-line arguments for the :ref:`run <usage-run>`
command with custom arguments for this target. Since only a single
target can be run at a time, prefixing to avoid naming conflicts with
other targets is not necessary here.
For example, :class:`SPEC2006 <targets.SPEC2006>` defines
``--benchmarks`` and ``--test``.
:param parser: the argument parser to extend
"""
pass
[docs] def dependencies(self) -> Iterator[Package]:
"""
Specify dependencies that should be built and installed in the run
environment before building this target.
"""
yield from []
[docs] def path(self, ctx: Context, *args: str) -> str:
"""
Get the absolute path to the build directory of this target, optionally
suffixed with a subpath.
:param ctx: the configuration context
:param args: additional subpath to pass to :func:`os.path.join`
:returns: ``build/targets/<name>[/<subpath>]``
"""
return os.path.join(ctx.paths.targets, self.name, *args)
def goto_rootdir(self, ctx: Context) -> None:
path = self.path(ctx)
os.makedirs(path, exist_ok=True)
os.chdir(path)
[docs] @abstractmethod
def is_fetched(self, ctx: Context) -> bool:
"""
Returns ``True`` if :func:`fetch` should be called before building.
:param ctx: the configuration context
"""
pass
[docs] @abstractmethod
def fetch(self, ctx: Context) -> None:
"""
Fetches the source code for this target. This step is separated from
:func:`build` because the ``build`` command first fetches all packages
and targets before starting the build process.
:param ctx: the configuration context
"""
pass
[docs] @abstractmethod
def build(
self, ctx: Context, instance: Instance, pool: Optional[Pool] = None
) -> None:
"""
Build the target object files. Called some time after :func:`fetch` (see
:class:`above <Target>`).
``ctx.runenv`` will have been populated with the exported environments
of all packages returned by :func:`dependencies` (i.e.,
:func:`Package.install_env` has been called for each dependency). This
means that when you call :func:`util.run` here, the programs and
libraries from the dependencies are available in ``PATH`` and
``LD_LIBRARY_PATH``, so you don't need to reference them with absolute
paths.
The build function should respect variables set in the configuration
context such as ``ctx.cc`` and ``ctx.cflags``, passing them to the
underlying build system as required. :py:attr:`Setup.ctx` shows default
variables in the context that should at least be respected, but complex
instances may optionally overwrite them to be used by custom targets.
Any custom command-line arguments set by :func:`add_build_args` are
available here in ``ctx.args``.
If ``pool`` is defined (i.e., when ``--parallel`` is passed), the
target is expected to use :func:`pool.run() <parallel.Pool.run>`
instead of :func:`util.run` to invoke build commands.
:param ctx: the configuration context
:param instance: instance to build
:param pool: parallel process pool if ``--parallel`` is specified
"""
pass
[docs] @abstractmethod
def run(
self, ctx: Context, instance: Instance, pool: Optional[Pool] = None
) -> None:
"""
Run the target binaries. This should be done using :func:`util.run` so
that ``ctx.runenv`` is used (which can be set by an instance or
dependencies). It is recommended to pass ``teeout=True`` to make the
output of the process stream to ``stdout``.
Any custom command-line arguments set by :func:`add_run_args` are
available here in ``ctx.args``.
If ``pool`` is defined (i.e., when ``--parallel`` is passed), the
target is expected to use :func:`pool.run() <parallel.Pool.run>`
instead of :func:`util.run` to launch runs.
Implementations of this method should respect the ``--iterations``
option of the run command.
:param ctx: the configuration context
:param instance: instance to run
:param pool: parallel process pool if ``--parallel`` is specified
"""
pass
[docs] def parse_outfile(self, ctx: Context, outfile: str) -> Iterator[ResultDict]:
"""
Callback method for :func:`commands.report.parse_logs`. Used by report
command to get reportable results.
:param ctx: the configuration context
:param outfile: path to outfile to parse
:raises NotImplementedError: unless implemented
"""
raise NotImplementedError(self.__class__.__name__)
[docs] def is_clean(self, ctx: Context) -> bool:
"""
Returns ``True`` if :func:`clean` should be called before cleaning.
:param ctx: the configuration context
"""
return not os.path.exists(self.path(ctx))
[docs] def clean(self, ctx: Context) -> None:
"""
Clean generated files for this target, called by the :ref:`clean
<usage-clean>` command. By default, this removes
``build/targets/<name>``.
:param ctx: the configuration context
"""
shutil.rmtree(self.path(ctx))
[docs] def binary_paths(self, ctx: Context, instance: Instance) -> Iterable[str]:
"""
If implemented, this should return a list of absolute paths to binaries
created by :func:`build` for the given instance. This is only used if
the instance specifies post-build hooks. Each hook is called for each of
the returned paths.
:param ctx: the configuration context
:param instance: instance to get paths for
:returns: paths to binaries
:raises NotImplementedError: unless implemented
"""
raise NotImplementedError(self.__class__.__name__)
def run_hooks_pre_build(self, ctx: Context, instance: Instance) -> None:
if ctx.hooks.pre_build:
self.goto_rootdir(ctx) # Run hooks in target root
for hook in ctx.hooks.pre_build:
ctx.log.info(f"Running pre-build hook {hook} in {self.path(ctx)}")
hook(ctx, self.path(ctx))
def run_hooks_post_build(self, ctx: Context, instance: Instance) -> None:
if ctx.hooks.post_build:
for binary in self.binary_paths(ctx, instance):
absbin = os.path.abspath(binary)
basedir = os.path.dirname(absbin)
for hook in ctx.hooks.post_build:
ctx.log.info(
f"Running post-build hook {hook} on {absbin} in {basedir}"
)
os.chdir(basedir)
hook(ctx, absbin)