import os
import shutil
from abc import ABCMeta, abstractmethod
from typing import Any, Iterable, Iterator, Tuple, Union
from .context import Context
PkgConfigOption = Tuple[str, str, Union[str, Iterable[str]]]
[docs]class Package(metaclass=ABCMeta):
"""
Abstract base class for package definitions. Built-in derived classes are
listed :doc:`here <packages>`.
Each package must define a :func:`ident` method that returns a unique ID
for the package instance. This is similar to :py:attr:`Target.name`, except
that each instantiation of a package can return a different ID depending on
its parameters. For example a ``Bash`` package might be initialized with a
version number and be identified as ``bash-4.1`` and ``bash-4.3``, which
are different packages with different build directories.
A dependency is built in three steps:
#. :func:`fetch` downloads the source code, typically to
``build/packages/<ident>/src``.
#. :func:`build` builds the code, typically in
``build/packages/<ident>/obj``.
#. :func:`install` installs the built binaries/libraries, typically into
``build/packages/<ident>/install``.
The functions above are only called if :func:`is_fetched`, :func:`is_built`
and :func:`is_installed` return ``False`` respectively. Additionally, if
:func:`is_installed` returns ``True``, fetching and building is skipped
altogether. All these methods are abstract and thus require an
implementation in a pacakge definition.
:func:`clean` removes all generated package files when the :ref:`clean
<usage-clean>` command is run. By default, this removes
``build/packages/<ident>``.
The package needs to be able to install itself into ``ctx.runenv`` so that
it can be used by targets/instances/packages that depend on it. This is
done by :func:`install_env`, which by default adds
``build/packages/<ident>/install/bin`` to the ``PATH`` and
``build/packages/<ident>/install/lib`` to the ``LD_LIBRARY_PATH``.
Finally, the setup script has a :ref:`pkg-config <usage-pkg-config>`
command that prints package information such as the installation prefix of
compilation flags required to build software that uses the package. These
options are configured by :func:`pkg_config_options`.
"""
def __eq__(self, other: object) -> bool:
return isinstance(other, self.__class__) and other.ident() == self.ident()
def __hash__(self) -> int:
return hash("package-" + self.ident())
[docs] @abstractmethod
def ident(self) -> str:
"""
Returns a unique identifier to this package instantiation.
Two packages are considered identical if their identifiers are equal.
This means that if multiple targets/instances/packages return different
instantiations of a package as dependency that share the same
identifier, they are assumed to be equal and only the first will be
built. This way, different implementations of :func:`dependencies` can
instantiate the same class in order to share a dependency.
"""
pass
[docs] def dependencies(self) -> Iterator["Package"]:
"""
Specify dependencies that should be built and installed in the run
environment before building this package.
"""
yield from []
[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 is_built(self, ctx: Context) -> bool:
"""
Returns ``True`` if :func:`build` should be called before installing.
:param ctx: the configuration context
"""
pass
[docs] @abstractmethod
def is_installed(self, ctx: Context) -> bool:
"""
Returns ``True`` if the pacakge has not been installed yet, and thus
needs to be fetched, built and installed.
:param ctx: the configuration context
"""
pass
[docs] @abstractmethod
def fetch(self, ctx: Context) -> None:
"""
Fetches the source code for this package. 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) -> None:
"""
Build the package. Usually amounts to running ``make -j<ctx.jobs>``
using :func:`util.run`.
:param ctx: the configuration context
"""
pass
[docs] @abstractmethod
def install(self, ctx: Context) -> None:
"""
Install the package. Usually amounts to running ``make install`` using
:func:`util.run`. It is recommended to install to ``self.path(ctx,
'install')``, which results in ``build/packages/<ident>/install``.
Assuming that a `bin` and/or `lib` directories are generated in the
install directory, the default behaviour of :func:`install_env` will
automatically add those to ``[LD_LIBRARY_]PATH``.
:param ctx: the configuration context
"""
pass
[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/packages/<ident>``.
:param ctx: the configuration context
"""
shutil.rmtree(self.path(ctx))
[docs] def path(self, ctx: Context, *args: str) -> str:
"""
Get the absolute path to the build directory of this package,
optionally suffixed with a subpath.
:param ctx: the configuration context
:param args: additional subpath to pass to :func:`os.path.join`
:returns: ``build/packages/<ident>[/<subpath>]``
"""
return os.path.join(ctx.paths.packages, self.ident(), *args)
[docs] def install_env(self, ctx: Context) -> None:
"""
Install the package into ``ctx.runenv`` so that it can be used in
subsequent calls to :func:`util.run`. By default, it adds
``build/packages/<ident>/install/bin`` to the ``PATH`` and
``build/packages/<ident>/install/lib`` to the ``LD_LIBRARY_PATH`` (but
only if the directories exist).
:param ctx: the configuration context
"""
# XXX rename 'install_env' to 'load'?
binpath = self.path(ctx, "install/bin")
if os.path.exists(binpath):
curbinpath = os.getenv("PATH", "").split(":")
prevbinpath = ctx.runenv.setdefault("PATH", curbinpath)
assert isinstance(prevbinpath, list)
prevbinpath.insert(0, binpath)
libpath = self.path(ctx, "install/lib")
if os.path.exists(libpath):
curlibpath = os.getenv("LD_LIBRARY_PATH", "").split(":")
prevlibpath = ctx.runenv.setdefault("LD_LIBRARY_PATH", curlibpath)
assert isinstance(prevlibpath, list)
prevlibpath.insert(0, libpath)
def goto_rootdir(self, ctx: Context) -> None:
path = self.path(ctx)
os.makedirs(path, exist_ok=True)
os.chdir(path)
[docs] def pkg_config_options(self, ctx: Context) -> Iterator[PkgConfigOption]:
"""
Yield options for the :ref:`pkg-config <usage-pkg-config>` command.
Each option is an (option, description, value) triple. The defaults are
``--root`` which returns the root directory ``build/packages/<ident>``,
and ``--prefix`` which returns the install directory populated by
:func:`install`: ``build/packages/<ident>/install``.
When reimplementing this method in a derived package class, it is
recommended to end the implementation with ``yield from
super().pkg_config_options(ctx)`` to add the two default options.
:param ctx: the configuration context
"""
yield ("--root", "absolute root path", self.path(ctx))
yield ("--prefix", "absolute install path", self.path(ctx, "install"))
class NoEnvLoad(Package):
"""
Wrapper class for packages that avoids them being loaded into PATH and
LD_LIBRARY_PATH by the default :func:`Package.install_env` method.
This is useful for packages that are used by referening direct paths,
instead of counting on their presence when calling :func:`util.run`.
"""
def __init__(self, package: Package):
"""
:param package: the package to wrap
"""
self.package = package
def install_env(self, ctx: Context) -> None:
ctx.log.debug(f"cancel installation of {self.ident()} in env")
def __eq__(self, other: object) -> bool:
return self.package == other
def __hash__(self) -> int:
return hash(self.package)
def ident(self) -> str:
return self.package.ident()
def is_fetched(self, ctx: Context) -> bool:
return self.package.is_fetched(ctx)
def is_built(self, ctx: Context) -> bool:
return self.package.is_built(ctx)
def is_installed(self, ctx: Context) -> bool:
return self.package.is_installed(ctx)
def fetch(self, ctx: Context) -> None:
self.package.fetch(ctx)
def build(self, ctx: Context) -> None:
self.package.build(ctx)
def install(self, ctx: Context) -> None:
self.package.install(ctx)
def __getattr__(self, key: str) -> Any:
return getattr(self.package, key)