text: # Copyright 2020 The Meson development team
  
  # 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.
  
  from pathlib import Path
  import os
  import shlex
  import subprocess
  import typing as T
  
  from . import ExtensionModule, ModuleReturnValue, NewExtensionModule
  from .. import mlog, build
  from ..compilers.compilers import CFLAGS_MAPPING
  from ..envconfig import ENV_VAR_PROG_MAP
  from ..dependencies import InternalDependency, PkgConfigDependency
  from ..interpreterbase import FeatureNew
  from ..interpreter.type_checking import ENV_KW
  from ..interpreterbase.decorators import ContainerTypeInfo, KwargInfo, typed_kwargs, typed_pos_args
  from ..mesonlib import (EnvironmentException, MesonException, Popen_safe, MachineChoice,
                          get_variable_regex, do_replacement, join_args, OptionKey)
  
  if T.TYPE_CHECKING:
      from typing_extensions import TypedDict
  
      from . import ModuleState
      from ..interpreter import Interpreter
      from ..interpreterbase import TYPE_var
  
      class Dependency(TypedDict):
  
          subdir: str
  
      class AddProject(TypedDict):
  
          configure_options: T.List[str]
          cross_configure_options: T.List[str]
          verbose: bool
          env: build.EnvironmentVariables
  
  
  class ExternalProject(NewExtensionModule):
      def __init__(self,
                   state: 'ModuleState',
                   configure_command: str,
                   configure_options: T.List[str],
                   cross_configure_options: T.List[str],
                   env: build.EnvironmentVariables,
                   verbose: bool):
          super().__init__()
          self.methods.update({'dependency': self.dependency_method,
                               })
  
          self.subdir = Path(state.subdir)
          self.project_version = state.project_version
          self.subproject = state.subproject
          self.env = state.environment
          self.build_machine = state.build_machine
          self.host_machine = state.host_machine
          self.configure_command = configure_command
          self.configure_options = configure_options
          self.cross_configure_options = cross_configure_options
          self.verbose = verbose
          self.user_env = env
  
          self.src_dir = Path(self.env.get_source_dir(), self.subdir)
          self.build_dir = Path(self.env.get_build_dir(), self.subdir, 'build')
          self.install_dir = Path(self.env.get_build_dir(), self.subdir, 'dist')
          _p = self.env.coredata.get_option(OptionKey('prefix'))
          assert isinstance(_p, str), 'for mypy'
          self.prefix = Path(_p)
          _l = self.env.coredata.get_option(OptionKey('libdir'))
          assert isinstance(_l, str), 'for mypy'
          self.libdir = Path(_l)
          _i = self.env.coredata.get_option(OptionKey('includedir'))
          assert isinstance(_i, str), 'for mypy'
          self.includedir = Path(_i)
          self.name = self.src_dir.name
  
          # On Windows if the prefix is "c:/foo" and DESTDIR is "c:/bar", `make`
          # will install files into "c:/bar/c:/foo" which is an invalid path.
          # Work around that issue by removing the drive from prefix.
          if self.prefix.drive:
              self.prefix = self.prefix.relative_to(self.prefix.drive)
  
          # self.prefix is an absolute path, so we cannot append it to another path.
          self.rel_prefix = self.prefix.relative_to(self.prefix.root)
  
          self._configure(state)
  
          self.targets = self._create_targets()
  
      def _configure(self, state: 'ModuleState') -> None:
          if self.configure_command == 'waf':
              FeatureNew('Waf external project', '0.60.0').use(self.subproject, state.current_node)
              waf = state.find_program('waf')
              configure_cmd = waf.get_command()
              configure_cmd += ['configure', '-o', str(self.build_dir)]
              workdir = self.src_dir
              self.make = waf.get_command() + ['build']
          else:
              # Assume it's the name of a script in source dir, like 'configure',
              # 'autogen.sh', etc).
              configure_path = Path(self.src_dir, self.configure_command)
              configure_prog = state.find_program(configure_path.as_posix())
              configure_cmd = configure_prog.get_command()
              workdir = self.build_dir
              self.make = state.find_program('make').get_command()
  
          d = [('PREFIX', '--prefix=@PREFIX@', self.prefix.as_posix()),
               ('LIBDIR', '--libdir=@PREFIX@/@LIBDIR@', self.libdir.as_posix()),
               ('INCLUDEDIR', None, self.includedir.as_posix()),
               ]
          self._validate_configure_options(d, state)
  
          configure_cmd += self._format_options(self.configure_options, d)
  
          if self.env.is_cross_build():
              host = '{}-{}-{}'.format(self.host_machine.cpu_family,
                                       self.build_machine.system,
                                       self.host_machine.system)
              d = [('HOST', None, host)]
              configure_cmd += self._format_options(self.cross_configure_options, d)
  
          # Set common env variables like CFLAGS, CC, etc.
          link_exelist: T.List[str] = []
          link_args: T.List[str] = []
          self.run_env = os.environ.copy()
          for lang, compiler in self.env.coredata.compilers[MachineChoice.HOST].items():
              if any(lang not in i for i in (ENV_VAR_PROG_MAP, CFLAGS_MAPPING)):
                  continue
              cargs = self.env.coredata.get_external_args(MachineChoice.HOST, lang)
              assert isinstance(cargs, list), 'for mypy'
              self.run_env[ENV_VAR_PROG_MAP[lang]] = self._quote_and_join(compiler.get_exelist())
              self.run_env[CFLAGS_MAPPING[lang]] = self._quote_and_join(cargs)
              if not link_exelist:
                  link_exelist = compiler.get_linker_exelist()
                  _l = self.env.coredata.get_external_link_args(MachineChoice.HOST, lang)
                  assert isinstance(_l, list), 'for mypy'
                  link_args = _l
          if link_exelist:
              # FIXME: Do not pass linker because Meson uses CC as linker wrapper,
              # but autotools often expects the real linker (e.h. GNU ld).
              # self.run_env['LD'] = self._quote_and_join(link_exelist)
              pass
          self.run_env['LDFLAGS'] = self._quote_and_join(link_args)
  
          self.run_env = self.user_env.get_env(self.run_env)
  
          PkgConfigDependency.setup_env(self.run_env, self.env, MachineChoice.HOST,
                                        Path(self.env.get_build_dir(), 'meson-uninstalled').as_posix())
  
          self.build_dir.mkdir(parents=True, exist_ok=True)
          self._run('configure', configure_cmd, workdir)
  
      def _quote_and_join(self, array: T.List[str]) -> str:
          return ' '.join([shlex.quote(i) for i in array])
  
      def _validate_configure_options(self, variables: T.List[T.Tuple[str, str, str]], state: 'ModuleState') -> None:
          # Ensure the user at least try to pass basic info to the build system,
          # like the prefix, libdir, etc.
          for key, default, val in variables:
              if default is None:
                  continue
              key_format = f'@{key}@'
              for option in self.configure_options:
                  if key_format in option:
                      break
              else:
                  FeatureNew('Default configure_option', '0.57.0').use(self.subproject, state.current_node)
                  self.configure_options.append(default)
  
      def _format_options(self, options: T.List[str], variables: T.List[T.Tuple[str, str, str]]) -> T.List[str]:
          out: T.List[str] = []
          missing = set()
          regex = get_variable_regex('meson')
          confdata: T.Dict[str, T.Tuple[str, T.Optional[str]]] = {k: (v, None) for k, _, v in variables}
          for o in options:
              arg, missing_vars = do_replacement(regex, o, 'meson', confdata)
              missing.update(missing_vars)
              out.append(arg)
          if missing:
              var_list = ", ".join(map(repr, sorted(missing)))
              raise EnvironmentException(
                  f"Variables {var_list} in configure options are missing.")
          return out
  
      def _run(self, step: str, command: T.List[str], workdir: Path) -> None:
          mlog.log(f'External project {self.name}:', mlog.bold(step))
          m = 'Running command ' + str(command) + ' in directory ' + str(workdir) + '\n'
          log_filename = Path(mlog.log_dir, f'{self.name}-{step}.log')
          output = None
          if not self.verbose:
              output = open(log_filename, 'w', encoding='utf-8')
              output.write(m + '\n')
              output.flush()
          else:
              mlog.log(m)
          p, *_ = Popen_safe(command, cwd=workdir, env=self.run_env,
                             stderr=subprocess.STDOUT,
                             stdout=output)
          if p.returncode != 0:
              m = f'{step} step returned error code {p.returncode}.'
              if not self.verbose:
                  m += '\nSee logs: ' + str(log_filename)
              raise MesonException(m)
  
      def _create_targets(self) -> T.List['TYPE_var']:
          cmd = self.env.get_build_command()
          cmd += ['--internal', 'externalproject',
                  '--name', self.name,
                  '--srcdir', self.src_dir.as_posix(),
                  '--builddir', self.build_dir.as_posix(),
                  '--installdir', self.install_dir.as_posix(),
                  '--logdir', mlog.log_dir,
                  '--make', join_args(self.make),
                  ]
          if self.verbose:
              cmd.append('--verbose')
  
          self.target = build.CustomTarget(
              self.name,
              self.subdir.as_posix(),
              self.subproject,
              self.env,
              cmd + ['@OUTPUT@', '@DEPFILE@'],
              [],
              [f'{self.name}.stamp'],
              depfile=f'{self.name}.d',
              console=True,
          )
  
          idir = build.InstallDir(self.subdir.as_posix(),
                                  Path('dist', self.rel_prefix).as_posix(),
                                  install_dir='.',
                                  install_mode=None,
                                  exclude=None,
                                  strip_directory=True,
                                  from_source_dir=False,
                                  subproject=self.subproject)
  
          return [self.target, idir]
  
      @typed_pos_args('external_project.dependency', str)
      @typed_kwargs('external_project.dependency', KwargInfo('subdir', str, default=''))
      def dependency_method(self, state: 'ModuleState', args: T.Tuple[str], kwargs: 'Dependency') -> InternalDependency:
          libname = args[0]
  
          abs_includedir = Path(self.install_dir, self.rel_prefix, self.includedir)
          if kwargs['subdir']:
              abs_includedir = Path(abs_includedir, kwargs['subdir'])
          abs_libdir = Path(self.install_dir, self.rel_prefix, self.libdir)
  
          version = self.project_version
          compile_args = [f'-I{abs_includedir}']
          link_args = [f'-L{abs_libdir}', f'-l{libname}']
          sources = self.target
          dep = InternalDependency(version, [], compile_args, link_args, [],
                                   [], [sources], [], {}, [], [])
          return dep
  
  
  class ExternalProjectModule(ExtensionModule):
      @FeatureNew('External build system Module', '0.56.0')
      def __init__(self, interpreter: 'Interpreter'):
          super().__init__(interpreter)
          self.methods.update({'add_project': self.add_project,
                               })
  
      @typed_pos_args('external_project_mod.add_project', str)
      @typed_kwargs(
          'external_project.add_project',
          KwargInfo('configure_options', ContainerTypeInfo(list, str), default=[], listify=True),
          KwargInfo('cross_configure_options', ContainerTypeInfo(list, str), default=['--host=@HOST@'], listify=True),
          KwargInfo('verbose', bool, default=False),
          ENV_KW,
      )
      def add_project(self, state: 'ModuleState', args: T.Tuple[str], kwargs: 'AddProject') -> ModuleReturnValue:
          configure_command = args[0]
          project = ExternalProject(state,
                                    configure_command,
                                    kwargs['configure_options'],
                                    kwargs['cross_configure_options'],
                                    kwargs['env'],
                                    kwargs['verbose'])
          return ModuleReturnValue(project, project.targets)
  
  
  def initialize(interp: 'Interpreter') -> ExternalProjectModule:
      return ExternalProjectModule(interp)
  