"""This modules provides the base classes for research tools that allow
developers to setup and configure their own research tool by inheriting and
implementing the base classes ``ResearchTool`` and ``CodeBase``."""
import abc
import typing as tp
from enum import Enum
from pathlib import Path
from typing import Protocol, runtime_checkable
import distro as distribution
from benchbuild.utils.cmd import apt, pacman
from varats.tools.research_tools.vara_manager import BuildType
from varats.utils.filesystem_util import FolderAlreadyPresentError
from varats.utils.git_commands import (
get_branches,
fetch_remote,
get_tags,
init_all_submodules,
update_all_submodules,
pull_current_branch,
push_current_branch,
checkout_branch_or_commit,
checkout_new_branch,
download_repo,
add_remote,
show_status,
)
from varats.utils.git_util import (
get_current_branch,
has_branch,
has_remote_branch,
branch_has_upstream,
RepositoryHandle,
)
from varats.utils.logger_util import log_without_linesep
if tp.TYPE_CHECKING:
from varats.containers import containers # pylint: disable=W0611
[docs]
class Distro(Enum):
"""Linux distributions supported by the tool suite."""
value: str
DEBIAN = "debian"
ARCH = "arch"
FEDORA = "fedora"
[docs]
@staticmethod
def get_current_distro() -> tp.Optional['Distro']:
"""Returns the current Linux distribution or None."""
if distribution.id() == "debian":
return Distro.DEBIAN
if distribution.id() == "arch":
return Distro.ARCH
if distribution.id() == "fedora":
return Distro.FEDORA
return None
def __str__(self) -> str:
return str(self.value)
_install_commands = {
Distro.DEBIAN: "apt install -y",
Distro.ARCH: "pacman -S --noconfirm",
Distro.FEDORA: "dnf install"
}
_checker_commands = {Distro.DEBIAN: apt["list"], Distro.ARCH: pacman["-Qi"]}
_expected_check_output = {Distro.DEBIAN: "installed", Distro.ARCH: "Installed"}
[docs]
class Dependencies:
"""Models the dependencies for a research tool."""
def __init__(self, dependencies: tp.Dict[Distro, tp.List[str]]):
self.__dependencies = dependencies
@property
def distros(self) -> tp.List[Distro]:
return list(self.__dependencies.keys())
[docs]
def has_dependencies_for_distro(self, distro: Distro) -> bool:
"""
Check whether the deendency object has any entries for the given distro.
Args:
distro: the distro to check
Returns:
whether there are any dependencies for the given distro
Test:
>>> deps = Dependencies({Distro.DEBIAN: ["foo", "bar"]})
>>> deps.has_dependencies_for_distro(Distro.DEBIAN)
True
>>> deps.has_dependencies_for_distro(Distro.ARCH)
False
>>> deps = Dependencies({Distro.DEBIAN: []})
>>> deps.has_dependencies_for_distro(Distro.DEBIAN)
False
"""
return bool(self.__dependencies.get(distro, None))
[docs]
def has_all_dependencies_for_distro(self, distro: Distro) -> bool:
"""
Given a distro, return if all specified dependencies are installed.
Args:
distro: the distro tu use
Returns:
True if all dependencies are installed and False otherwise
"""
return len(self.get_missing_dependencies_for_distro(distro)) == 0
[docs]
def get_missing_dependencies_for_distro(self,
distro: Distro) -> tp.List[str]:
"""
Given a distro, return all not installed dependencies.
Args:
distro: the distro to use
Returns:
a list containing all not installed dependencies
"""
not_installed: tp.List[str] = []
if distro not in _checker_commands or \
distro not in _expected_check_output:
raise NotImplementedError(
"Check/Expected commands are currently " +
f"not implemented for {distro}"
)
base_command = _checker_commands[distro]
for package in self.__dependencies[distro]:
output = base_command(package)
output_list = output.split()
if _expected_check_output[distro] not in output_list:
not_installed.append(package)
return not_installed
[docs]
def get_install_command(self, distro: Distro) -> str:
"""
Given a distro, return a command how the dependencies can be installed.
Args:
distro: the distro to use
Returns:
the command how the dependencies can be installed
Test:
>>> deps = Dependencies({Distro.DEBIAN: ["foo", "bar"], \
Distro.ARCH: ["baz"]})
>>> deps.get_install_command(Distro.DEBIAN)
'apt install -y foo bar'
>>> deps.get_install_command(Distro.ARCH)
'pacman -S --noconfirm baz'
"""
return f"{_install_commands[distro]} " \
f"{' '.join(self.__dependencies[distro])}"
[docs]
class SubProject():
"""Encapsulates a sub project, e.g., a library or tool, defining how it can
be downloaded and integrated inside a ``CodeBase``."""
def __init__(
self,
parent_base_dir: Path,
name: str,
url: str,
remote: str,
sub_path: str,
is_submodule: bool = False
):
self.__name = name
self.__parent_base_dir = parent_base_dir
self.__url = url
self.__remote = remote
self.__sub_path = Path(sub_path)
self.__is_submodule = is_submodule
self.__repo = RepositoryHandle(self.__parent_base_dir / self.__sub_path)
@property
def name(self) -> str:
"""Name of the sub project."""
return self.__name
@property
def url(self) -> str:
"""Repository URL."""
return self.__url
@property
def remote(self) -> str:
"""Git remote, for interacting with upstream repositories."""
return self.__remote
@property
def path(self) -> Path:
"""
Path to the sub project folder within a ``CodeBase``.
For example:
``CodeBase.base_dir / self.path``
Specifies the absolute path to the sub project folder.
"""
return self.__sub_path
@property
def is_submodule(self) -> bool:
"""
Determine if this project is a submodule and shouldn't be cloned and
pulled automatically when a `CodeBase` is initialized or updated.
Returns:
True, if it should be automatically cloned
"""
return self.__is_submodule
[docs]
def init_and_update_submodules(self) -> None:
"""
Initialize and update all submodules of this sub project.
Args:
cb_base_dir: base directory for the ``CodeBase``
"""
init_all_submodules(self.__repo)
update_all_submodules(self.__repo)
[docs]
def clone(self) -> None:
"""Clone the sub project into the specified folder relative to the base
dir of the ``CodeBase``."""
print(f"Cloning {self.name} into {self.__parent_base_dir}")
if self.__repo.worktree_path.exists():
raise FolderAlreadyPresentError(self.__repo.worktree_path)
download_repo(
self.__repo.worktree_path.parent, self.url, self.path.name,
self.remote, log_without_linesep(print)
)
[docs]
def has_branch(
self,
branch_name: str,
remote_to_check: tp.Optional[str] = None
) -> bool:
"""
Check if the sub project has a branch with the specified ``branch
name``.
Args:
branch_name: name of the branch
remote_to_check: name of the remote to check, if None, only a local
check will be performed
Returns:
True, if the branch exists
"""
if remote_to_check is None:
return has_branch(self.__repo, branch_name)
return has_remote_branch(self.__repo, branch_name, remote_to_check)
[docs]
def get_branches(self,
extra_args: tp.Optional[tp.List[str]
] = None) -> tp.List[str]:
"""
Get branch names from this sub project.
Args:
extra_args: extra arguments passed to `git branch`
Returns:
list of branch names
"""
return get_branches(self.__repo, extra_args).split()
[docs]
def add_remote(self, remote: str, url: str) -> None:
"""
Add a new remote to the sub project.
Args:
remote: name of the new remote
url: to the remote
"""
add_remote(self.__repo, remote, url)
fetch_remote(self.__repo, remote)
[docs]
def checkout_branch(self, branch_name: str) -> None:
"""
Checkout out branch in sub project.
Args:
branch_name: name of the branch, should exists in the repo
"""
checkout_branch_or_commit(self.__repo, branch_name)
[docs]
def checkout_new_branch(
self, branch_name: str, remote_branch: tp.Optional[str] = None
) -> None:
"""
Create and checkout out a new branch in the sub project.
Args:
branch_name: name of the new branch, should not exists in the repo
"""
checkout_new_branch(self.__repo, branch_name, remote_branch)
[docs]
def fetch(
self,
remote: tp.Optional[str] = None,
extra_args: tp.Optional[tp.List[str]] = None
) -> None:
"""Fetch updates from the remote."""
fetch_remote(self.__repo, remote, extra_args)
[docs]
def pull(self) -> None:
"""Pull updates from the remote of the current branch into the sub
project."""
pull_current_branch(self.__repo)
[docs]
def push(self) -> None:
"""Push updates from the current branch to the remote branch."""
branch_name = get_current_branch(self.__repo)
if branch_has_upstream(self.__repo, branch_name):
push_current_branch(self.__repo)
else:
push_current_branch(self.__repo, "origin", branch_name)
[docs]
def show_status(self) -> None:
"""Show the current status of the sub project."""
show_status(self.__repo)
def __str__(self) -> str:
return f"{self.name} [{self.url}:{self.remote}] {self.path}"
[docs]
class CodeBase():
"""
A ``CodeBase`` depicts the layout of a project, specifying where the a
research tool lives and how different sub projects should be cloned.
In addition, it allows access to the sub projects, e.g., for checkout or
other repository manipulations.
"""
def __init__(self, base_dir: Path, sub_projects: tp.List[SubProject]):
self.__sub_projects = sub_projects
self.__base_dir = base_dir
@property
def base_dir(self) -> Path:
return self.__base_dir
[docs]
def get_sub_project(self, name: str) -> SubProject:
"""
Lookup a sub project of this ``CodeBase``
Args:
name: of the sub project
"""
for sub_project in self.__sub_projects:
if sub_project.name == name:
return sub_project
raise LookupError
[docs]
def clone(self, cb_base_dir: Path) -> None:
"""
Clones the full code base into the specified folder ``cb_base_dir``,
which marks the base folder of the code base structure.
Args:
cb_base_dir: new base dir of the code base
"""
self.__base_dir = cb_base_dir
for sub_project in self.__sub_projects:
if not sub_project.is_submodule:
sub_project.clone()
[docs]
def map_sub_projects(
self,
func: tp.Callable[[SubProject], None],
exclude_submodules: bool = False
) -> None:
"""
Execute a callable ``func`` on all sub projects of the code base.
Args:
func: function to execute on the sub projects
exclude_submodules: if True sub projects that
are managed using git submodules will be
excluded
"""
for sub_project in self.__sub_projects:
if exclude_submodules and sub_project.is_submodule:
continue
func(sub_project)
SpecificCodeBase = tp.TypeVar("SpecificCodeBase", bound=CodeBase)
[docs]
@runtime_checkable
class ContainerInstallable(Protocol):
"""Protocol for installing a research tool inside a container."""
[docs]
def container_install_dependencies(
self, stage_builder: 'containers.StageBuilder'
) -> None:
"""
Add layers for installing this research tool's dependencies to the given
container.
Args:
stage_builder: the builder object for the current container stage
"""