"""Core objects for building e3 modules."""
import logging
import pathlib
from typing import Dict, List, Optional, Tuple
from git import GitCommandError, InvalidGitRepositoryError, NoSuchPathError, Repo
from e3_build_tools import utils
from e3_build_tools.exceptions import ModuleBuildFailException, NotInitialisedError
from e3_build_tools.fs.environment import InstalledEnvironment
from e3_build_tools.module import EPICSBaseSource, ModuleSource
[docs]
logger = logging.getLogger(__name__)
[docs]
class Builder:
"""Class for building e3 modules."""
def __init__(
self, build_dir: pathlib.Path, use_ssh: bool = False, jobs: int = 1
) -> None:
"""Initialize object."""
[docs]
self.build_dir = build_dir
[docs]
def build(
self,
module: ModuleSource,
target_environment: InstalledEnvironment,
with_substitutions: Optional[Dict[str, Dict[str, str]]] = None,
) -> Tuple[List[Tuple[str, str]], List[Tuple[str, str]]]:
"""Build all of the versions of a Module.
Raises:
NotInitialisedError: If module was not initialised.
GitCommandError: If clone failed.
InvalidGitRepositoryError: If directory exists but is not a git repository.
"""
successes: List[Tuple[str, str]] = []
failures: List[Tuple[str, str]] = []
self.build_dir.mkdir(parents=True, exist_ok=True)
if module.versions:
try:
module.repo = self._clone_repo(module)
except (NotInitialisedError, InvalidGitRepositoryError, GitCommandError):
failures.extend([(module.name, ref) for ref in module.versions])
# FIXME: This is so that we "fail fast"
return successes, failures
for ref in module.versions:
try:
self._build_version(module, ref, target_environment, with_substitutions)
except ModuleBuildFailException:
failures.append((module.name, ref))
# FIXME: This is so that we "fail fast"
break
else:
successes.append((module.name, ref))
return successes, failures
def _build_version(
self,
module: ModuleSource,
ref: str,
target_environment: InstalledEnvironment,
with_substitutions: Optional[Dict[str, Dict[str, str]]] = None,
) -> None:
"""Build a fixed version of a module."""
logger.debug(f"Attempting to build {ref!r} of {module.name!r}")
try:
module.repo.git.checkout(ref)
except GitCommandError as e:
raise ModuleBuildFailException(
f"Skipping {module.name} {ref} - failure when checking out due to gitlab error: "
+ str(e)
)
if with_substitutions is not None:
Builder.modify_makefiles_in_directory(
pathlib.Path(str(module.repo.working_tree_dir)),
with_substitutions,
)
Builder.modify_install_path(
module,
repo_path=pathlib.Path(str(module.repo.working_tree_dir)),
install_path=target_environment.root
if isinstance(module, EPICSBaseSource)
else target_environment.base_directory,
)
logger.info(
f"Building {module.name}, {ref} ({module.versions[ref]['version_string']})..."
)
for target in module.targets:
result = utils.run_make(
pathlib.Path(str(module.repo.working_tree_dir)),
f"-j{self.jobs}",
target,
)
if result.returncode:
raise ModuleBuildFailException(
f"Failure for {module.name} version {ref} on 'make {target}'"
)
module.repo.git.reset("--hard")
def _clone_repo(self, module: ModuleSource) -> Repo:
"""Clone a module repository to the build directory.
If the module has already been cloned, fetches new changes.
Raises:
NotInitialisedError: If module was not initialised
GitCommandError: If clone failed
InvalidGitRepositoryError: If directory exists but is not a git repository
"""
module_path = self.build_dir / f"e3-{module.name}"
logger.debug(f"Cloning {module.name!r} to {module_path.resolve()!s}")
try:
remote_url = (
module.project.ssh_url_to_repo
if self.use_ssh
else module.project.http_url_to_repo
)
except NotInitialisedError as e:
raise NotInitialisedError(
f"Wrapper {module.name!r} is not initialised (no URL to the repository)"
) from e
try:
repo = Repo(module_path)
logger.debug(
f"Directory exists - trying to update remotes for {module.name!r} ({remote_url!r})"
)
for remote in repo.remotes:
remote.fetch()
return repo
except NoSuchPathError:
try:
return Repo.clone_from(remote_url, module_path)
except GitCommandError as e:
raise GitCommandError(f"Failure when cloning {module.name!r}") from e
except InvalidGitRepositoryError as e:
raise InvalidGitRepositoryError(
f"Directory {module_path!r} exists, but it is not a git repository"
) from e
@staticmethod
[docs]
def modify_makefiles_in_directory(
directory: pathlib.Path,
substitutions: Dict[str, Dict[str, str]],
) -> None:
"""Apply substitutions."""
for file, change in substitutions.items():
path = directory / file
old_fcontent = path.read_text()
new_fcontent = utils.modify_makefile_definitions(old_fcontent, change)
if new_fcontent and new_fcontent != old_fcontent:
logger.debug(f"Substituting {file!r} with {change!r}")
path.write_text(new_fcontent)
else:
logger.debug(f"No substitutions to be made in {file!r} ({change!r})")
@staticmethod
[docs]
def modify_install_path(
module: ModuleSource,
*,
repo_path: pathlib.Path,
install_path: pathlib.Path,
) -> None:
"""Modify the install path for a module."""
utils.modify_config_file(
repo_path / module.install_config_file,
module.install_config_var,
str(install_path),
)