<filename>couplet_composer/composing_runner.py
# Copyright (c) 2021 <NAME>
# Licensed under the MIT License

"""A module that contains the class for the objects that run the
composing run mode of the build script.
"""

import logging
import os

from argparse import Namespace

from .support.cmake_generator import CMakeGenerator

from .support.cpp_standard import CppStandard

from .support.system import System

from .util import shell

from .runner_proper import RunnerProper

from .target import Target


class ComposingRunner(RunnerProper):
    """A class for creating callable objects that represent the
    composing mode runners of the build script.

    Attributes:
        cpp_std (CppStandard): The selected C++ standard.
    """

    def __init__(
        self,
        args: Namespace,
        source_root: str,
        target: Target
    ) -> None:
        """Initializes the runner object.

        Args:
            args (Namespace): A namespace that contains the
                parsed command line arguments.
            source_root (str): The current source root.
            target (Target): The target host that this runner is
                for.
        """
        super().__init__(args=args, source_root=source_root, target=target)
        self.cpp_std = CppStandard[self.args.cpp_std.replace("+", "p")]

    def __call__(self) -> int:
        """Runs the run mode of this runner.

        Returns:
            An 'int' that is equal to the exit code of the run.
        """
        super().__call__()

        if not os.path.isdir(self.build_dir.build):
            shell.makedirs(
                self.build_dir.build,
                dry_run=self.args.dry_run,
                echo=self.args.verbose
            )

        cmake_call = self._create_cmake_call()

        # Print the warnings related to the CMake build
        if self.args.build_docs and not self.toolchain.doxygen:
            logging.warning(
                "Building the documentation is enabled but Doxygen wasn't "
                "found, thus, the documentation isn't built"
            )

        # TODO Run the lint before installing the docs
        with shell.pushd(self.build_dir.build):
            shell.call(
                cmake_call,
                env=None,
                dry_run=self.args.dry_run,
                echo=self.args.verbose
            )
            # TODO Take into account all of the different build
            # systems.
            shell.call(
                [self.toolchain.ninja],
                dry_run=self.args.dry_run,
                echo=self.args.verbose
            )
            shell.call(
                [self.toolchain.ninja, "install"],
                dry_run=self.args.dry_run,
                echo=self.args.verbose
            )

            if self.args.lint:
                self._run_linter()

            if self.args.build_docs and self.toolchain.doxygen:
                self._install_docs()

        shell.copytree(
            os.path.join(
                self.source_root,
                self.args.repository,
                "util",
                "bin"
            ),
            os.path.join(self.build_dir.destination, "bin"),
            dry_run=self.args.dry_run,
            echo=self.args.verbose
        )

        super().post_run_cleanup()

        return 0

    def _create_cmake_call(self) -> list:
        """Creates the full CMake call to be executed for the current
        configuration.

        Returns:
            A list of strings that represents the CMake call.
        """
        build_docs_cmake_value = (
            "ON" if self.args.build_docs and self.toolchain.doxygen else "OFF"
        )

        cmake_call = [
            self.toolchain.cmake,
            os.path.join(
                self.source_root,
                self.args.repository
            ),
            "-G",
            self.cmake_generator.value,
            f"-DCMAKE_BUILD_TYPE={self.build_variant.value}",
            (
                "-DCMAKE_PREFIX_PATH="
                f"{self.build_dir.dependencies.replace(os.path.sep, '/')}"
            ),
            (
                "-DCMAKE_INSTALL_PREFIX="
                f"{self.build_dir.destination.replace(os.path.sep, '/')}"
            ),
            f"-DCOMPOSER_BUILD_TEST={'ON' if self.args.build_test else 'OFF'}",
            (
                "-DCOMPOSER_BUILD_BENCHMARK="
                f"{'ON' if self.args.build_benchmark else 'OFF'}"
            ),
            f"-DCOMPOSER_BUILD_DOCS={build_docs_cmake_value}",
            f"-DCOMPOSER_CPP_STD={self.cpp_std.value}",
            (
                "-DCOMPOSER_LOCAL_PREFIX="
                f"{self.build_dir.dependencies.replace(os.path.sep, '/')}"
            ),
            (
                "-DCOMPOSER_OPENGL_VERSION_MAJOR="
                f"{self.project.gl_version.split('.')[0]}"
            ),
            (
                "-DCOMPOSER_OPENGL_VERSION_MINOR="
                f"{self.project.gl_version.split('.')[1]}"
            )
        ]

        # Set the correct architecture for the build when the
        # host machine is an Apple processor macOS.
        if (self.target.system is System.darwin
                and self.target.architecture == "arm64"):
            cmake_call.append(
                f"-DCMAKE_APPLE_SILICON_PROCESSOR={self.target.architecture}"
            )

        if self._resolve_make_program():
            cmake_call.append(
                f"-DCMAKE_MAKE_PROGRAM={self._resolve_make_program()}"
            )

        for key in self.project.project_keys:
            cmake_call.append(
                f"-DCOMPOSER_{key.upper()}_VERSION="
                f"{getattr(self.project, f'{key}_version')}"
            )
            cmake_call.append(
                f"-DCOMPOSER_{key.upper()}_NAME="
                f"{getattr(self.project, f'{key}_name')}"
            )
            cmake_call.append(f"-DCOMPOSER_{key.upper()}_TARGET={key}")

        if self.project.cmake_options:
            for key, value in self.project.cmake_options.items():
                if isinstance(value, bool):
                    cmake_call.append(
                        f"-D{key.upper()}={'ON' if value else 'OFF'}"
                    )
                else:
                    cmake_call.append(f"-D{key.upper()}={value!s}")

        if self.args.cmake_options:
            for option in self.args.cmake_options:
                cmake_call.append(f"-D{option}")

        return cmake_call

    def _resolve_make_program(self) -> str:
        """Resolves the path to the correct Make program for CMake.

        Returns:
            The path to the Make program.
        """
        if self.cmake_generator is CMakeGenerator.ninja:
            return self.toolchain.ninja

        return None

    def _run_linter(self) -> None:
        """Runs the linter on the project.
        """
        logging.warning(
            "The run-clang-tidy is %s",
            self.toolchain.run_clang_tidy
        )

        linter_call = [
            self.toolchain.run_clang_tidy,
            "-clang-tidy-binary",
            self.toolchain.clang_tidy,
            "-j",
            str(self.args.jobs)
        ]
        shell.call(
            linter_call,
            env=None,
            dry_run=self.args.dry_run,
            echo=self.args.verbose
        )

    def _install_docs(self) -> str:
        """Runs a command during which system sleep is disabled.

        Args:
            data (Object): The data object read from the project
                data JSON file.
            key (str): The key for the data.

        Returns:
            The number, string, or object read from the project
            data JSON file.
        """
        if os.path.exists(self.build_dir.docs_destination):
            shell.rmtree(
                self.build_dir.docs_destination,
                dry_run=self.args.dry_run,
                echo=self.args.verbose
            )

        # TODO Add more possible docs formats besides HTML
        shell.makedirs(
            os.path.join(self.build_dir.docs_destination, "html"),
            dry_run=self.args.dry_run,
            echo=self.args.verbose
        )

        shell.copytree(
            os.path.join(self.build_dir.build, "docs", "doxygen", "html"),
            os.path.join(self.build_dir.docs_destination, "html"),
            dry_run=self.args.dry_run,
            echo=self.args.verbose
        )

    def clean(self) -> None:
        """Cleans the directories and files of the runner before
        building when clean build is run.
        """
        super().clean()

        shell.rmtree(
            self.build_dir.docs_destination,
            dry_run=self.args.dry_run,
            echo=self.args.verbose
        )

<filename>couplet_composer/composing_runner.py
# Copyright (c) 2021 <NAME>
# Licensed under the MIT License

"""A module that contains the class for the objects that run the
composing run mode of the build script.
"""

import logging
import os

from argparse import Namespace

from .support.cmake_generator import CMakeGenerator

from .support.cpp_standard import CppStandard

from .support.system import System

from .util import shell

from .runner_proper import RunnerProper

from .target import Target


class ComposingRunner(RunnerProper):
    """A class for creating callable objects that represent the
    composing mode runners of the build script.

    Attributes:
        cpp_std (CppStandard): The selected C++ standard.
    """

    def __init__(
        self,
        args: Namespace,
        source_root: str,
        target: Target
    ) -> None:
        """Initializes the runner object.

        Args:
            args (Namespace): A namespace that contains the
                parsed command line arguments.
            source_root (str): The current source root.
            target (Target): The target host that this runner is
                for.
        """
        super().__init__(args=args, source_root=source_root, target=target)
        self.cpp_std = CppStandard[self.args.cpp_std.replace("+", "p")]

    def __call__(self) -> int:
        """Runs the run mode of this runner.

        Returns:
            An 'int' that is equal to the exit code of the run.
        """
        super().__call__()

        if not os.path.isdir(self.build_dir.build):
            shell.makedirs(
                self.build_dir.build,
                dry_run=self.args.dry_run,
                echo=self.args.verbose
            )

        cmake_call = self._create_cmake_call()

        # Print the warnings related to the CMake build
        if self.args.build_docs and not self.toolchain.doxygen:
            logging.warning(
                "Building the documentation is enabled but Doxygen wasn't "
                "found, thus, the documentation isn't built"
            )

        # TODO Run the lint before installing the docs
        with shell.pushd(self.build_dir.build):
            shell.call(
                cmake_call,
                env=None,
                dry_run=self.args.dry_run,
                echo=self.args.verbose
            )
            # TODO Take into account all of the different build
            # systems.
            shell.call(
                [self.toolchain.ninja],
                dry_run=self.args.dry_run,
                echo=self.args.verbose
            )
            shell.call(
                [self.toolchain.ninja, "install"],
                dry_run=self.args.dry_run,
                echo=self.args.verbose
            )

            if self.args.lint:
                self._run_linter()

            if self.args.build_docs and self.toolchain.doxygen:
                self._install_docs()

        shell.copytree(
            os.path.join(
                self.source_root,
                self.args.repository,
                "util",
                "bin"
            ),
            os.path.join(self.build_dir.destination, "bin"),
            dry_run=self.args.dry_run,
            echo=self.args.verbose
        )

        super().post_run_cleanup()

        return 0

    def _create_cmake_call(self) -> list:
        """Creates the full CMake call to be executed for the current
        configuration.

        Returns:
            A list of strings that represents the CMake call.
        """
        build_docs_cmake_value = (
            "ON" if self.args.build_docs and self.toolchain.doxygen else "OFF"
        )

        cmake_call = [
            self.toolchain.cmake,
            os.path.join(
                self.source_root,
                self.args.repository
            ),
            "-G",
            self.cmake_generator.value,
            f"-DCMAKE_BUILD_TYPE={self.build_variant.value}",
            (
                "-DCMAKE_PREFIX_PATH="
                f"{self.build_dir.dependencies.replace(os.path.sep, '/')}"
            ),
            (
                "-DCMAKE_INSTALL_PREFIX="
                f"{self.build_dir.destination.replace(os.path.sep, '/')}"
            ),
            f"-DCOMPOSER_BUILD_TEST={'ON' if self.args.build_test else 'OFF'}",
            (
                "-DCOMPOSER_BUILD_BENCHMARK="
                f"{'ON' if self.args.build_benchmark else 'OFF'}"
            ),
            f"-DCOMPOSER_BUILD_DOCS={build_docs_cmake_value}",
            f"-DCOMPOSER_CPP_STD={self.cpp_std.value}",
            (
                "-DCOMPOSER_LOCAL_PREFIX="
                f"{self.build_dir.dependencies.replace(os.path.sep, '/')}"
            ),
            (
                "-DCOMPOSER_OPENGL_VERSION_MAJOR="
                f"{self.project.gl_version.split('.')[0]}"
            ),
            (
                "-DCOMPOSER_OPENGL_VERSION_MINOR="
                f"{self.project.gl_version.split('.')[1]}"
            )
        ]

        # Set the correct architecture for the build when the
        # host machine is an Apple processor macOS.
        if (self.target.system is System.darwin
                and self.target.architecture == "arm64"):
            cmake_call.append(
                f"-DCMAKE_APPLE_SILICON_PROCESSOR={self.target.architecture}"
            )

        if self._resolve_make_program():
            cmake_call.append(
                f"-DCMAKE_MAKE_PROGRAM={self._resolve_make_program()}"
            )

        for key in self.project.project_keys:
            cmake_call.append(
                f"-DCOMPOSER_{key.upper()}_VERSION="
                f"{getattr(self.project, f'{key}_version')}"
            )
            cmake_call.append(
                f"-DCOMPOSER_{key.upper()}_NAME="
                f"{getattr(self.project, f'{key}_name')}"
            )
            cmake_call.append(f"-DCOMPOSER_{key.upper()}_TARGET={key}")

        if self.project.cmake_options:
            for key, value in self.project.cmake_options.items():
                if isinstance(value, bool):
                    cmake_call.append(
                        f"-D{key.upper()}={'ON' if value else 'OFF'}"
                    )
                else:
                    cmake_call.append(f"-D{key.upper()}={value!s}")

        if self.args.cmake_options:
            for option in self.args.cmake_options:
                cmake_call.append(f"-D{option}")

        return cmake_call

    def _resolve_make_program(self) -> str:
        """Resolves the path to the correct Make program for CMake.

        Returns:
            The path to the Make program.
        """
        if self.cmake_generator is CMakeGenerator.ninja:
            return self.toolchain.ninja

        return None

    def _run_linter(self) -> None:
        """Runs the linter on the project.
        """
        logging.warning(
            "The run-clang-tidy is %s",
            self.toolchain.run_clang_tidy
        )

        linter_call = [
            self.toolchain.run_clang_tidy,
            "-clang-tidy-binary",
            self.toolchain.clang_tidy,
            "-j",
            str(self.args.jobs)
        ]
        shell.call(
            linter_call,
            env=None,
            dry_run=self.args.dry_run,
            echo=self.args.verbose
        )

    def _install_docs(self) -> str:
        """Runs a command during which system sleep is disabled.

        Args:
            data (Object): The data object read from the project
                data JSON file.
            key (str): The key for the data.

        Returns:
            The number, string, or object read from the project
            data JSON file.
        """
        if os.path.exists(self.build_dir.docs_destination):
            shell.rmtree(
                self.build_dir.docs_destination,
                dry_run=self.args.dry_run,
                echo=self.args.verbose
            )

        # TODO Add more possible docs formats besides HTML
        shell.makedirs(
            os.path.join(self.build_dir.docs_destination, "html"),
            dry_run=self.args.dry_run,
            echo=self.args.verbose
        )

        shell.copytree(
            os.path.join(self.build_dir.build, "docs", "doxygen", "html"),
            os.path.join(self.build_dir.docs_destination, "html"),
            dry_run=self.args.dry_run,
            echo=self.args.verbose
        )

    def clean(self) -> None:
        """Cleans the directories and files of the runner before
        building when clean build is run.
        """
        super().clean()

        shell.rmtree(
            self.build_dir.docs_destination,
            dry_run=self.args.dry_run,
            echo=self.args.verbose
        )

# Copyright (c) 2021 <NAME> # Licensed under the MIT License A module that contains the class for the objects that run the composing run mode of the build script. A class for creating callable objects that represent the     composing mode runners of the build script.      Attributes:         cpp_std (CppStandard): The selected C++ standard. Initializes the runner object.          Args:             args (Namespace): A namespace that contains the                 parsed command line arguments.             source_root (str): The current source root.             target (Target): The target host that this runner is                 for. Runs the run mode of this runner.          Returns:             An 'int' that is equal to the exit code of the run. # Print the warnings related to the CMake build # TODO Run the lint before installing the docs # TODO Take into account all of the different build # systems. Creates the full CMake call to be executed for the current         configuration.          Returns:             A list of strings that represents the CMake call. # Set the correct architecture for the build when the # host machine is an Apple processor macOS. Resolves the path to the correct Make program for CMake.          Returns:             The path to the Make program. Runs the linter on the project. Runs a command during which system sleep is disabled.          Args:             data (Object): The data object read from the project                 data JSON file.             key (str): The key for the data.          Returns:             The number, string, or object read from the project             data JSON file. # TODO Add more possible docs formats besides HTML Cleans the directories and files of the runner before         building when clean build is run.