text: # Copyright 2021 Canonical Ltd.
  #
  # Licensed under the Apache License, Version 2.0 (the "License");
  # you may not use this file except in compliance with the License.
  # You may obtain a copy of the License at
  #
  # http://www.apache.org/licenses/LICENSE-2.0
  #
  # Unless required by applicable law or agreed to in writing, software
  # distributed under the License is distributed on an "AS IS" BASIS,
  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  # See the License for the specific language governing permissions and
  # limitations under the License.
  #
  # For further info, check https://github.com/canonical/charmcraft
  
  """Build environment provider support for charmcraft."""
  
  import contextlib
  import logging
  import os
  import pathlib
  import re
  import subprocess
  import tempfile
  from typing import Dict, List, Optional, Tuple, Union
  
  from craft_providers import Executor, bases, lxd
  from craft_providers.actions import snap_installer
  from craft_providers.lxd import LXDInstallationError
  from craft_providers.lxd import installer as lxd_installer
  from craft_providers.lxd.remotes import configure_buildd_image_remote
  
  from charmcraft.cmdbase import CommandError
  from charmcraft.config import Base
  from charmcraft.env import (
      get_managed_environment_log_path,
      get_managed_environment_project_path,
  )
  from charmcraft.utils import confirm_with_user, get_host_architecture
  
  logger = logging.getLogger(__name__)
  
  BASE_CHANNEL_TO_BUILDD_IMAGE_ALIAS = {
      "18.04": bases.BuilddBaseAlias.BIONIC,
      "20.04": bases.BuilddBaseAlias.FOCAL,
  }
  
  
  def capture_logs_from_instance(instance: Executor) -> None:
      """Retrieve logs from instance.
  
      :param instance: Instance to retrieve logs from.
  
      :returns: String of logs.
      """
      _, tmp_path = tempfile.mkstemp(prefix="charmcraft-")
      local_log_path = pathlib.Path(tmp_path)
      instance_log_path = get_managed_environment_log_path()
  
      try:
          instance.pull_file(source=instance_log_path, destination=local_log_path)
      except FileNotFoundError:
          logger.debug("No logs found in instance.")
          return
  
      logs = local_log_path.read_text()
      local_log_path.unlink()
  
      logger.debug("Logs captured from managed instance:\n%s", logs)
  
  
  def clean_project_environments(
      charm_name: str,
      project_path: pathlib.Path,
      *,
      lxd_project: str = "charmcraft",
      lxd_remote: str = "local",
  ) -> List[str]:
      """Clean up any environments created for project.
  
      :param charm_name: Name of project.
      :param project_path: Directory of charm project.
      :param lxd_project: Name of LXD project.
      :param lxd_remote: Name of LXD remote.
  
      :returns: List of containers deleted.
      """
      deleted: List[str] = []
  
      # Nothing to do if provider is not installed.
      if not is_provider_available():
          return deleted
  
      inode = str(project_path.stat().st_ino)
      lxc = lxd.LXC()
  
      for name in lxc.list_names(project=lxd_project, remote=lxd_remote):
          match_regex = f"^charmcraft-{charm_name}-{inode}-.+-.+-.+$"
          if re.match(match_regex, name):
              logger.debug("Deleting container %r.", name)
              lxc.delete(instance_name=name, force=True, project=lxd_project, remote=lxd_remote)
              deleted.append(name)
          else:
              logger.debug("Not deleting container %r.", name)
  
      return deleted
  
  
  def ensure_provider_is_available() -> None:
      """Ensure provider is available, prompting the user to install it if required.
  
      :raises CommandError: if provider is not available.
      """
      if is_provider_available():
          return
  
      if confirm_with_user(
          "LXD is required, but not installed. Do you wish to install LXD "
          "and configure it with the defaults?",
          default=False,
      ):
          try:
              lxd_installer.install()
          except LXDInstallationError as error:
              raise CommandError(
                  "Failed to install LXD. Please visit https://snapcraft.io/lxd for "
                  "instructions on how to install the LXD snap for your distribution"
              ) from error
      else:
          raise CommandError(
              "LXD is required, but not installed. Please visit https://snapcraft.io/lxd for "
              "instructions on how to install the LXD snap for your distribution"
          )
  
  
  def is_base_providable(base: Base) -> Tuple[bool, Union[str, None]]:
      """Check if provider can provide an environment matching given base.
  
      :param base: Base to check.
  
      :returns: Tuple of bool indicating whether it is a match, with optional
                reason if not a match.
      """
      host_arch = get_host_architecture()
      if host_arch not in base.architectures:
          return (
              False,
              f"host architecture {host_arch!r} not in base architectures {base.architectures!r}",
          )
  
      if base.name != "ubuntu":
          return (
              False,
              f"name {base.name!r} is not yet supported (must be 'ubuntu')",
          )
  
      if base.channel not in BASE_CHANNEL_TO_BUILDD_IMAGE_ALIAS.keys():
          return (
              False,
              f"channel {base.channel!r} is not yet supported (must be '18.04' or '20.04')",
          )
  
      return True, None
  
  
  def get_instance_name(
      *,
      bases_index: int,
      build_on_index: int,
      project_name: str,
      project_path: pathlib.Path,
      target_arch: str,
  ) -> str:
      """Formulate the name for an instance using each of the given parameters.
  
      Incorporate each of the parameters into the name to come up with a
      predictable naming schema that avoids name collisions across multiple,
      potentially complex, projects.
  
      :param bases_index: Index of `bases:` entry.
      :param build_on_index: Index of `build-on` within bases entry.
      :param project_name: Name of charm project.
      :param project_path: Directory of charm project.
      :param target_arch: Targetted architecture, used in the name to prevent
          collisions should future work enable multiple architectures on the same
          platform.
      """
      return "-".join(
          [
              "charmcraft",
              project_name,
              str(project_path.stat().st_ino),
              str(bases_index),
              str(build_on_index),
              target_arch,
          ]
      )
  
  
  def get_command_environment() -> Dict[str, str]:
      """Construct the required environment."""
      env = bases.buildd.default_command_environment()
      env["CHARMCRAFT_MANAGED_MODE"] = "1"
  
      # Pass-through host environment that target may need.
      for env_key in ["http_proxy", "https_proxy", "no_proxy"]:
          if env_key in os.environ:
              env[env_key] = os.environ[env_key]
  
      return env
  
  
  def is_provider_available() -> bool:
      """Check if provider is installed and available for use.
  
      :returns: True if installed.
      """
      return lxd_installer.is_installed()
  
  
  @contextlib.contextmanager
  def launched_environment(
      *,
      charm_name: str,
      project_path: pathlib.Path,
      base: Base,
      bases_index: int,
      build_on_index: int,
      lxd_project: str = "charmcraft",
      lxd_remote: str = "local",
  ):
      """Launch environment for specified base.
  
      :param charm_name: Name of project.
      :param project_path: Path to project.
      :param base: Base to create.
      :param bases_index: Index of `bases:` entry.
      :param build_on_index: Index of `build-on` within bases entry.
      """
      alias = BASE_CHANNEL_TO_BUILDD_IMAGE_ALIAS[base.channel]
      target_arch = get_host_architecture()
  
      instance_name = get_instance_name(
          bases_index=bases_index,
          build_on_index=build_on_index,
          project_name=charm_name,
          project_path=project_path,
          target_arch=target_arch,
      )
  
      environment = get_command_environment()
      image_remote = configure_buildd_image_remote()
      base_configuration = CharmcraftBuilddBaseConfiguration(
          alias=alias, environment=environment, hostname=instance_name
      )
      instance = lxd.launch(
          name=instance_name,
          base_configuration=base_configuration,
          image_name=base.channel,
          image_remote=image_remote,
          auto_clean=True,
          auto_create_project=True,
          map_user_uid=True,
          use_snapshots=True,
          project=lxd_project,
          remote=lxd_remote,
      )
  
      # Mount project.
      instance.mount(host_source=project_path, target=get_managed_environment_project_path())
  
      try:
          yield instance
      finally:
          # Ensure to unmount everything and stop instance upon completion.
          instance.unmount_all()
          instance.stop()
  
  
  class CharmcraftBuilddBaseConfiguration(bases.BuilddBase):
      """Base configuration for Charmcraft.
  
      :cvar compatibility_tag: Tag/Version for variant of build configuration and
          setup.  Any change to this version would indicate that prior [versioned]
          instances are incompatible and must be cleaned.  As such, any new value
          should be unique to old values (e.g. incrementing).  Charmcraft extends
          the buildd tag to include its own version indicator (.0) and namespace
          ("charmcraft").
      """
  
      compatibility_tag: str = f"charmcraft-{bases.BuilddBase.compatibility_tag}.0"
  
      def setup(
          self,
          *,
          executor: Executor,
          retry_wait: float = 0.25,
          timeout: Optional[float] = None,
      ) -> None:
          """Prepare base instance for use by the application.
  
          In addition to the guarantees provided by buildd:
  
              - charmcraft installed
  
              - python3 pip and setuptools installed
  
          :param executor: Executor for target container.
          :param retry_wait: Duration to sleep() between status checks (if
              required).
          :param timeout: Timeout in seconds.
  
          :raises BaseCompatibilityError: if instance is incompatible.
          :raises BaseConfigurationError: on other unexpected error.
          """
          super().setup(executor=executor, retry_wait=retry_wait, timeout=timeout)
  
          try:
              # XXX Claudio 2021-07-22: craft-parts uses sudo, install it until
              # we adjust it to detect if changing to superuser is needed.
              executor.execute_run(
                  [
                      "apt-get",
                      "install",
                      "-y",
                      "sudo",
                  ],
                  check=True,
                  capture_output=True,
              )
          except subprocess.CalledProcessError as error:
              raise bases.BaseConfigurationError(
                  brief="Failed to install the required dependencies.",
              ) from error
  
          try:
              snap_installer.inject_from_host(
                  executor=executor, snap_name="charmcraft", classic=True
              )
          except snap_installer.SnapInstallationError as error:
              raise bases.BaseConfigurationError(
                  brief="Failed to inject host Charmcraft snap into target environment.",
              ) from error
  