Source code for infra.context

import argparse
import dataclasses
import io
import logging
import os
from dataclasses import dataclass, field, fields
from datetime import datetime
from typing import (
    TYPE_CHECKING,
    Any,
    Callable,
    Dict,
    List,
    Optional,
    Type,
    TypeVar,
    Union,
)

T = TypeVar("T")


def slotted(cls: Type[T]) -> Type[T]:
    """
    Decorator to create a dataclass where only defined fields can be
    assigned to.

    This is purely for backwards compatibility; Once support for Python
    version below 3.10 is dropped, replace uses of this decorator with
    @dataclass(slots=True).
    """

    if TYPE_CHECKING:
        from _typeshed import DataclassInstance

    def slotted_setattr(self: "DataclassInstance", key: str, value: Any) -> None:
        if key not in (f.name for f in fields(self)):
            raise Exception(
                f"cannot set '{key}' in ctx: dynamically adding extra "
                "fields to ctx is deprecated"
            )
        super(type(self), self).__setattr__(key, value)

    setattr(cls, "__setattr__", slotted_setattr)
    return cls


[docs]@dataclass(frozen=True) class ContextPaths: """ Absolute, read-only, paths used throughout the infra. Normally instances, targets, and packages do not need to consult these pathsdirectly, but instead use their respective ``path`` method. """ #: Root dir of the infra itself. infra: str #: Path to the user's script that invoked the infra. setup: str #: Working directory when the infra was started. workdir: str @property def root(self) -> str: """Root directory, that contains the user's script invoking the infra.""" return os.path.dirname(self.setup) @property def buildroot(self) -> str: """Build directory.""" return os.path.join(self.root, "build") @property def log(self) -> str: """Directory containing all logs.""" return os.path.join(self.buildroot, "log") @property def debuglog(self) -> str: """Path to the debug log.""" return os.path.join(self.log, "debug.txt") @property def runlog(self) -> str: """Path to the log of all executed commands.""" return os.path.join(self.log, "commands.txt") @property def packages(self) -> str: """Build directory for packages.""" return os.path.join(self.buildroot, "packages") @property def targets(self) -> str: """Build directory for targets.""" return os.path.join(self.buildroot, "targets") @property def pool_results(self) -> str: """Directory containing all results of running targets.""" return os.path.join(self.root, "results")
[docs]@slotted @dataclass class ContextHooks: """Hooks (i.e., functions) that are executed at various stages during the building and running of targets.""" #: Hooks to execute before building a target. pre_build: List[Callable] = field(default_factory=list) #: Hooks to execute after a target is built. #: #: This can be used to do additional post-processing on the generated binaries. post_build: List[Callable] = field(default_factory=list)
[docs]@slotted @dataclass class Context: """ The global configuration context, used by all targets, instances, etc. For example, an instance can configure its compiler flags in this context, which are then used by targets. """ #: Absolute paths to be used (readonly) throughout the framework. paths: ContextPaths #: The logging object used for status updates. log: logging.Logger #: The logging level as requested by the user. #: #: Note that is differs from the logging object's log level, since all debug output #: is written to a file regardless of the requested loglevel. loglevel: int = logging.NOTSET #: Populated with processed command-line arguments. Targets and instances can add #: additional command-line arguments, which can be accessed through this object. args: argparse.Namespace = field(default_factory=argparse.Namespace) #: An object with hooks for various points in the building/running process. hooks: ContextHooks = field(default_factory=ContextHooks) #: Environment variables that are used when running a target. runenv: Dict[str, Union[str, List[str]]] = field(default_factory=dict) #: When the current run of the infra was started. starttime: datetime = field(default_factory=datetime.now) #: Command(s) to prepend in front of the target's run command (executed directly on #: the command line). This can be set to a custom shell script, or for example #: ``perf`` or ``valgrind``. target_run_wrapper: str = "" # TODO: merge this with Tools? #: File object used for writing all executed commands, if enabled. runlog_file: Optional[io.TextIOWrapper] = None #: Object used to redirect the output of executed commands to a file and stdout. runtee: Optional[io.IOBase] = None #: The amount of parallel jobs to use. Contains the value of the ``-j`` command-line #: option, defaulting to the number of CPU cores returned by #: :func:`multiprocessing.cpu_count`. jobs: int = 8 #: Architecture to build targets for. Initialized to :func:`platform.machine`. #: Valid values include ``x86_64`` and ``arm64``/``aarch64``; for more, refer to #: ``uname -m`` and :func:`platform.machine`. arch: str = "unknown" #: C compiler to use when building targets. cc: str = "cc" #: C++ compiler to use for building targets. cxx: str = "cxx" #: Fortran compiler to use for building targets. fc: str = "fc" #: Command for creating static library archives. ar: str = "ar" #: Command to read an object's symbols. nm: str = "nm" #: Command to generate the index of an archive. ranlib: str = "ranlib" #: C compilation flags to use when building targets. cflags: List[str] = field(default_factory=list) #: C++ compilation flags to use when building targets. cxxflags: List[str] = field(default_factory=list) #: Fortran compilation flags to use when building targets. fcflags: List[str] = field(default_factory=list) #: Linker flags to use when building targets. ldflags: List[str] = field(default_factory=list) #: Special set of linker flags set by some packages, and is passed when linking #: target libraries that will later be (statically) linked into the binary. #: #: In practice it is either empty or ``['-flto']`` when compiling with LLVM. lib_ldflags: List[str] = field(default_factory=list)
[docs] def copy(self) -> "Context": """ Make a partial deepcopy of this Context, copying only fields of type ``ContextPaths|list|dict``. """ changes: Dict[str, Any] = {"paths": dataclasses.replace(self.paths)} for attr in dir(self): if attr.startswith("_"): continue attr_val = getattr(self, attr) if isinstance(attr_val, (list, dict)): changes[attr] = attr_val.copy() return dataclasses.replace(self, **changes)