files.new_contents: import os, subprocess
  import argparse
  import asyncio
  import threading
  import copy
  import shutil
  from concurrent.futures.thread import ThreadPoolExecutor
  from pathlib import Path
  import typing as T
  
  from . import mlog
  from .mesonlib import quiet_git, GitException, Popen_safe, MesonException, windows_proof_rmtree
  from .wrap.wrap import PackageDefinition, Resolver, WrapException, ALL_TYPES
  from .wrap import wraptool
  
  ALL_TYPES_STRING = ', '.join(ALL_TYPES)
  
  class Logger:
      def __init__(self, total_tasks: int) -> None:
          self.lock = threading.Lock()
          self.total_tasks = total_tasks
          self.completed_tasks = 0
          self.running_tasks = set()
          self.should_erase_line = ''
  
      def flush(self) -> None:
          if self.should_erase_line:
              print(self.should_erase_line, end='\r')
              self.should_erase_line = ''
  
      def print_progress(self) -> None:
          line = f'Progress: {self.completed_tasks} / {self.total_tasks}'
          max_len = shutil.get_terminal_size().columns - len(line)
          running = ', '.join(self.running_tasks)
          if len(running) + 3 > max_len:
              running = running[:max_len - 6] + '...'
          line = line + f' ({running})'
          print(self.should_erase_line, line, sep='', end='\r')
          self.should_erase_line = '\x1b[K'
  
      def start(self, wrap_name: str) -> None:
          with self.lock:
              self.running_tasks.add(wrap_name)
              self.print_progress()
  
      def done(self, wrap_name: str, log_queue: T.List[T.Tuple[mlog.TV_LoggableList, T.Any]]) -> None:
          with self.lock:
              self.flush()
              for args, kwargs in log_queue:
                  mlog.log(*args, **kwargs)
              self.running_tasks.remove(wrap_name)
              self.completed_tasks += 1
              self.print_progress()
  
  
  class Runner:
      def __init__(self, logger: Logger, r: Resolver, wrap: PackageDefinition, repo_dir: str, options: argparse.Namespace) -> None:
          # FIXME: Do a copy because Resolver.resolve() is stateful method that
          # cannot be called from multiple threads.
          self.wrap_resolver = copy.copy(r)
          self.wrap = wrap
          self.repo_dir = repo_dir
          self.options = options
          self.run_method = options.subprojects_func.__get__(self)
          self.log_queue = []
          self.logger = logger
  
      def log(self, *args, **kwargs):
          self.log_queue.append((args, kwargs))
  
      def run(self):
          self.logger.start(self.wrap.name)
          result = self.run_method()
          self.logger.done(self.wrap.name, self.log_queue)
          return result
  
      def update_wrapdb_file(self):
          try:
              patch_url = self.wrap.get('patch_url')
              branch, revision = wraptool.parse_patch_url(patch_url)
          except WrapException:
              return
          new_branch, new_revision = wraptool.get_latest_version(self.wrap.name)
          if new_branch != branch or new_revision != revision:
              wraptool.update_wrap_file(self.wrap.filename, self.wrap.name, new_branch, new_revision)
              self.log('  -> New wrap file downloaded.')
  
      def update_file(self):
          self.update_wrapdb_file()
          if not os.path.isdir(self.repo_dir):
              # The subproject is not needed, or it is a tarball extracted in
              # 'libfoo-1.0' directory and the version has been bumped and the new
              # directory is 'libfoo-2.0'. In that case forcing a meson
              # reconfigure will download and use the new tarball.
              self.log('  -> Not used.')
              return True
          elif self.options.reset:
              # Delete existing directory and redownload. It is possible that nothing
              # changed but we have no way to know. Hopefully tarballs are still
              # cached.
              windows_proof_rmtree(self.repo_dir)
              try:
                  self.wrap_resolver.resolve(self.wrap.name, 'meson')
                  self.log('  -> New version extracted')
                  return True
              except WrapException as e:
                  self.log('  ->', mlog.red(str(e)))
                  return False
          else:
              # The subproject has not changed, or the new source and/or patch
              # tarballs should be extracted in the same directory than previous
              # version.
              self.log('  -> Subproject has not changed, or the new source/patch needs to be extracted on the same location.')
              self.log('     Pass --reset option to delete directory and redownload.')
              return False
  
      def git_output(self, cmd):
          return quiet_git(cmd, self.repo_dir, check=True)[1]
  
      def git_verbose(self, cmd):
          self.log(self.git_output(cmd))
  
      def git_stash(self):
          # That git command return 1 (failure) when there is something to stash.
          # We don't want to stash when there is nothing to stash because that would
          # print spurious "No local changes to save".
          if not quiet_git(['diff', '--quiet', 'HEAD'], self.repo_dir)[0]:
              # Don't pipe stdout here because we want the user to see their changes have
              # been saved.
              self.git_verbose(['stash'])
  
      def git_show(self):
          commit_message = self.git_output(['show', '--quiet', '--pretty=format:%h%n%d%n%s%n[%an]'])
          parts = [s.strip() for s in commit_message.split('\n')]
          self.log('  ->', mlog.yellow(parts[0]), mlog.red(parts[1]), parts[2], mlog.blue(parts[3]))
  
      def git_rebase(self, revision):
          try:
              self.git_output(['-c', 'rebase.autoStash=true', 'rebase', 'FETCH_HEAD'])
          except GitException as e:
              self.log('  -> Could not rebase', mlog.bold(self.repo_dir), 'onto', mlog.bold(revision))
              self.log(mlog.red(e.output))
              self.log(mlog.red(str(e)))
              return False
          return True
  
      def git_reset(self, revision):
          try:
              # Stash local changes, commits can always be found back in reflog, to
              # avoid any data lost by mistake.
              self.git_stash()
              self.git_output(['reset', '--hard', 'FETCH_HEAD'])
          except GitException as e:
              self.log('  -> Could not reset', mlog.bold(repo_dir), 'to', mlog.bold(revision))
              self.log(mlog.red(e.output))
              self.log(mlog.red(str(e)))
              return False
          return True
  
      def git_checkout(self, revision, create=False):
          cmd = ['checkout', '--ignore-other-worktrees', revision, '--']
          if create:
              cmd.insert('-b', 1)
          try:
              # Stash local changes, commits can always be found back in reflog, to
              # avoid any data lost by mistake.
              self.git_stash()
              self.git_output(cmd)
          except GitException as e:
              self.log('  -> Could not checkout', mlog.bold(revision), 'in', mlog.bold(self.repo_dir))
              self.log(mlog.red(e.output))
              self.log(mlog.red(str(e)))
              return False
          return True
  
      def git_checkout_and_reset(self, revision):
          # revision could be a branch that already exists but is outdated, so we still
          # have to reset after the checkout.
          success = self.git_checkout(revision)
          if success:
              success = self.git_reset(revision)
          return success
  
      def git_checkout_and_rebase(self, revision):
          # revision could be a branch that already exists but is outdated, so we still
          # have to rebase after the checkout.
          success = self.git_checkout(revision)
          if success:
              success = self.git_rebase(revision)
          return success
  
      def update_git(self):
          if not os.path.isdir(self.repo_dir):
              self.log('  -> Not used.')
              return True
          if not os.path.exists(os.path.join(self.repo_dir, '.git')):
              if self.options.reset:
                  # Delete existing directory and redownload
                  windows_proof_rmtree(self.repo_dir)
                  try:
                      self.wrap_resolver.resolve(self.wrap.name, 'meson')
                      self.update_git_done()
                      return True
                  except WrapException as e:
                      self.log('  ->', mlog.red(str(e)))
                      return False
              else:
                  self.log('  -> Not a git repository.')
                  self.log('Pass --reset option to delete directory and redownload.')
                  return False
          revision = self.wrap.values.get('revision')
          url = self.wrap.values.get('url')
          push_url = self.wrap.values.get('push-url')
          if not revision or not url:
              # It could be a detached git submodule for example.
              self.log('  -> No revision or URL specified.')
              return True
          try:
              origin_url = self.git_output(['remote', 'get-url', 'origin']).strip()
          except GitException as e:
              self.log('  -> Failed to determine current origin URL in', mlog.bold(self.repo_dir))
              self.log(mlog.red(e.output))
              self.log(mlog.red(str(e)))
              return False
          if self.options.reset:
              try:
                  self.git_output(['remote', 'set-url', 'origin', url])
                  if push_url:
                      self.git_output(['remote', 'set-url', '--push', 'origin', push_url])
              except GitException as e:
                  self.log('  -> Failed to reset origin URL in', mlog.bold(self.repo_dir))
                  self.log(mlog.red(e.output))
                  self.log(mlog.red(str(e)))
                  return False
          elif url != origin_url:
              self.log(f'  -> URL changed from {origin_url!r} to {url!r}')
              return False
          try:
              # Same as `git branch --show-current` but compatible with older git version
              branch = self.git_output(['rev-parse', '--abbrev-ref', 'HEAD']).strip()
              branch = branch if branch != 'HEAD' else ''
          except GitException as e:
              self.log('  -> Failed to determine current branch in', mlog.bold(self.repo_dir))
              self.log(mlog.red(e.output))
              self.log(mlog.red(str(e)))
              return False
          try:
              # Fetch only the revision we need, this avoids fetching useless branches.
              # revision can be either a branch, tag or commit id. In all cases we want
              # FETCH_HEAD to be set to the desired commit and "git checkout <revision>"
              # to to either switch to existing/new branch, or detach to tag/commit.
              # It is more complicated than it first appear, see discussion there:
              # https://github.com/mesonbuild/meson/pull/7723#discussion_r488816189.
              heads_refmap = '+refs/heads/*:refs/remotes/origin/*'
              tags_refmap = '+refs/tags/*:refs/tags/*'
              self.git_output(['fetch', '--refmap', heads_refmap, '--refmap', tags_refmap, 'origin', revision])
          except GitException as e:
              self.log('  -> Could not fetch revision', mlog.bold(revision), 'in', mlog.bold(self.repo_dir))
              self.log(mlog.red(e.output))
              self.log(mlog.red(str(e)))
              return False
  
          if branch == '':
              # We are currently in detached mode
              if self.options.reset:
                  success = self.git_checkout_and_reset(revision)
              else:
                  success = self.git_checkout_and_rebase(revision)
          elif branch == revision:
              # We are in the same branch. A reset could still be needed in the case
              # a force push happened on remote repository.
              if self.options.reset:
                  success = self.git_reset(revision)
              else:
                  success = self.git_rebase(revision)
          else:
              # We are in another branch, either the user created their own branch and
              # we should rebase it, or revision changed in the wrap file and we need
              # to checkout the new branch.
              if self.options.reset:
                  success = self.git_checkout_and_reset(revision)
              else:
                  success = self.git_rebase(revision)
          if success:
              self.update_git_done()
          return success
  
      def update_git_done(self):
          self.git_output(['submodule', 'update', '--checkout', '--recursive'])
          self.git_show()
  
      def update_hg(self):
          if not os.path.isdir(self.repo_dir):
              self.log('  -> Not used.')
              return True
          revno = self.wrap.get('revision')
          if revno.lower() == 'tip':
              # Failure to do pull is not a fatal error,
              # because otherwise you can't develop without
              # a working net connection.
              subprocess.call(['hg', 'pull'], cwd=self.repo_dir)
          else:
              if subprocess.call(['hg', 'checkout', revno], cwd=self.repo_dir) != 0:
                  subprocess.check_call(['hg', 'pull'], cwd=self.repo_dir)
                  subprocess.check_call(['hg', 'checkout', revno], cwd=self.repo_dir)
          return True
  
      def update_svn(self):
          if not os.path.isdir(self.repo_dir):
              self.log('  -> Not used.')
              return True
          revno = self.wrap.get('revision')
          p, out, _ = Popen_safe(['svn', 'info', '--show-item', 'revision', self.repo_dir])
          current_revno = out
          if current_revno == revno:
              return True
          if revno.lower() == 'head':
              # Failure to do pull is not a fatal error,
              # because otherwise you can't develop without
              # a working net connection.
              subprocess.call(['svn', 'update'], cwd=self.repo_dir)
          else:
              subprocess.check_call(['svn', 'update', '-r', revno], cwd=self.repo_dir)
          return True
  
      def update(self):
          self.log(f'Updating {self.wrap.name}...')
          if self.wrap.type == 'file':
              return self.update_file()
          elif self.wrap.type == 'git':
              return self.update_git()
          elif self.wrap.type == 'hg':
              return self.update_hg()
          elif self.wrap.type == 'svn':
              return self.update_svn()
          elif self.wrap.type is None:
              self.log('  -> Cannot update subproject with no wrap file')
          else:
              self.log('  -> Cannot update', self.wrap.type, 'subproject')
          return True
  
      def checkout(self):
          if self.wrap.type != 'git' or not os.path.isdir(self.repo_dir):
              return True
          branch_name = self.options.branch_name if self.options.branch_name else self.wrap.get('revision')
          if not branch_name:
              # It could be a detached git submodule for example.
              return True
          self.log(f'Checkout {branch_name} in {self.wrap.name}...')
          if self.git_checkout(branch_name, create=self.options.b):
              self.git_show()
              return True
          return False
  
      def download(self):
          self.log(f'Download {self.wrap.name}...')
          if os.path.isdir(self.repo_dir):
              self.log('  -> Already downloaded')
              return True
          try:
              self.wrap_resolver.resolve(self.wrap.name, 'meson')
              self.log('  -> done')
          except WrapException as e:
              self.log('  ->', mlog.red(str(e)))
              return False
          return True
  
      def foreach(self):
          self.log(f'Executing command in {self.repo_dir}')
          if not os.path.isdir(self.repo_dir):
              self.log('  -> Not downloaded yet')
              return True
          cmd = [self.options.command] + self.options.args
          p, out, _ = Popen_safe(cmd, stderr=subprocess.STDOUT, cwd=self.repo_dir)
          if p.returncode != 0:
              err_message = "Command '{}' returned non-zero exit status {}.".format(" ".join(cmd), p.returncode)
              self.log('  -> ', mlog.red(err_message))
              self.log(out, end='')
              return False
  
          self.log(out, end='')
          return True
  
      def purge(self) -> bool:
          # if subproject is not wrap-based, then don't remove it
          if not self.wrap.type:
              return True
  
          if self.wrap.redirected:
              redirect_file = Path(self.wrap.original_filename).resolve()
              if self.options.confirm:
                  redirect_file.unlink()
              mlog.log(f'Deleting {redirect_file}')
  
          if self.wrap.type == 'redirect':
              redirect_file = Path(self.wrap.filename).resolve()
              if self.options.confirm:
                  redirect_file.unlink()
              self.log(f'Deleting {redirect_file}')
  
          if self.options.include_cache:
              packagecache = Path(self.wrap_resolver.cachedir).resolve()
              try:
                  subproject_cache_file = packagecache / self.wrap.get("source_filename")
                  if subproject_cache_file.is_file():
                      if self.options.confirm:
                          subproject_cache_file.unlink()
                      self.log(f'Deleting {subproject_cache_file}')
              except WrapException:
                  pass
  
              try:
                  subproject_patch_file = packagecache / self.wrap.get("patch_filename")
                  if subproject_patch_file.is_file():
                      if self.options.confirm:
                          subproject_patch_file.unlink()
                      self.log(f'Deleting {subproject_patch_file}')
              except WrapException:
                  pass
  
              # Don't log that we will remove an empty directory. Since purge is
              # parallelized, another thread could have deleted it already.
              try:
                  if not any(packagecache.iterdir()):
                      windows_proof_rmtree(str(packagecache))
              except FileNotFoundError:
                  pass
  
          subproject_source_dir = Path(self.repo_dir).resolve()
  
          # Don't follow symlink. This is covered by the next if statement, but why
          # not be doubly sure.
          if subproject_source_dir.is_symlink():
              if self.options.confirm:
                  subproject_source_dir.unlink()
              self.log(f'Deleting {subproject_source_dir}')
              return True
          if not subproject_source_dir.is_dir():
              return True
  
          try:
              if self.options.confirm:
                  windows_proof_rmtree(str(subproject_source_dir))
              self.log(f'Deleting {subproject_source_dir}')
          except OSError as e:
              mlog.error(f'Unable to remove: {subproject_source_dir}: {e}')
              return False
  
          return True
  
      @staticmethod
      def post_purge(options):
          if not options.confirm:
              mlog.log('')
              mlog.log('Nothing has been deleted, run again with --confirm to apply.')
  
  def add_common_arguments(p):
      p.add_argument('--sourcedir', default='.',
                     help='Path to source directory')
      p.add_argument('--types', default='',
                     help=f'Comma-separated list of subproject types. Supported types are: {ALL_TYPES_STRING} (default: all)')
      p.add_argument('--num-processes', default=None, type=int,
                     help='How many parallel processes to use (Since 0.59.0).')
  
  def add_subprojects_argument(p):
      p.add_argument('subprojects', nargs='*',
                     help='List of subprojects (default: all)')
  
  def add_arguments(parser):
      subparsers = parser.add_subparsers(title='Commands', dest='command')
      subparsers.required = True
  
      p = subparsers.add_parser('update', help='Update all subprojects from wrap files')
      p.add_argument('--rebase', default=True, action='store_true',
                     help='Rebase your branch on top of wrap\'s revision. ' + \
                          'Deprecated, it is now the default behaviour. (git only)')
      p.add_argument('--reset', default=False, action='store_true',
                     help='Checkout wrap\'s revision and hard reset to that commit. (git only)')
      add_common_arguments(p)
      add_subprojects_argument(p)
      p.set_defaults(subprojects_func=Runner.update)
  
      p = subparsers.add_parser('checkout', help='Checkout a branch (git only)')
      p.add_argument('-b', default=False, action='store_true',
                     help='Create a new branch')
      p.add_argument('branch_name', nargs='?',
                     help='Name of the branch to checkout or create (default: revision set in wrap file)')
      add_common_arguments(p)
      add_subprojects_argument(p)
      p.set_defaults(subprojects_func=Runner.checkout)
  
      p = subparsers.add_parser('download', help='Ensure subprojects are fetched, even if not in use. ' +
                                                 'Already downloaded subprojects are not modified. ' +
                                                 'This can be used to pre-fetch all subprojects and avoid downloads during configure.')
      add_common_arguments(p)
      add_subprojects_argument(p)
      p.set_defaults(subprojects_func=Runner.download)
  
      p = subparsers.add_parser('foreach', help='Execute a command in each subproject directory.')
      p.add_argument('command', metavar='command ...',
                     help='Command to execute in each subproject directory')
      p.add_argument('args', nargs=argparse.REMAINDER,
                     help=argparse.SUPPRESS)
      add_common_arguments(p)
      p.set_defaults(subprojects=[])
      p.set_defaults(subprojects_func=Runner.foreach)
  
      p = subparsers.add_parser('purge', help='Remove all wrap-based subproject artifacts')
      add_common_arguments(p)
      add_subprojects_argument(p)
      p.add_argument('--include-cache', action='store_true', default=False, help='Remove the package cache as well')
      p.add_argument('--confirm', action='store_true', default=False, help='Confirm the removal of subproject artifacts')
      p.set_defaults(subprojects_func=Runner.purge)
      p.set_defaults(post_func=Runner.post_purge)
  
  def run(options):
      src_dir = os.path.relpath(os.path.realpath(options.sourcedir))
      if not os.path.isfile(os.path.join(src_dir, 'meson.build')):
          mlog.error('Directory', mlog.bold(src_dir), 'does not seem to be a Meson source directory.')
          return 1
      subprojects_dir = os.path.join(src_dir, 'subprojects')
      if not os.path.isdir(subprojects_dir):
          mlog.log('Directory', mlog.bold(src_dir), 'does not seem to have subprojects.')
          return 0
      r = Resolver(src_dir, 'subprojects')
      if options.subprojects:
          wraps = [wrap for name, wrap in r.wraps.items() if name in options.subprojects]
      else:
          wraps = r.wraps.values()
      types = [t.strip() for t in options.types.split(',')] if options.types else []
      for t in types:
          if t not in ALL_TYPES:
              raise MesonException(f'Unknown subproject type {t!r}, supported types are: {ALL_TYPES_STRING}')
      tasks = []
      task_names = []
      loop = asyncio.get_event_loop()
      executor = ThreadPoolExecutor(options.num_processes)
      if types:
          wraps = [wrap for wrap in wraps if wrap.type in types]
      logger = Logger(len(wraps))
      for wrap in wraps:
          dirname = Path(subprojects_dir, wrap.directory).as_posix()
          runner = Runner(logger, r, wrap, dirname, options)
          task = loop.run_in_executor(executor, runner.run)
          tasks.append(task)
          task_names.append(wrap.name)
      results = loop.run_until_complete(asyncio.gather(*tasks))
      logger.flush()
      post_func = getattr(options, 'post_func', None)
      if post_func:
          post_func(options)
      failures = [name for name, success in zip(task_names, results) if not success]
      if failures:
          m = 'Please check logs above as command failed in some subprojects which could have been left in conflict state: '
          m += ', '.join(failures)
          mlog.warning(m)
      return len(failures)
  
  
  [... the rest is omitted ...]
files.new_path: mesonbuild/msubprojects.py
files.old_contents: import os, subprocess
  import argparse
  import asyncio
  import threading
  import copy
  import shutil
  from concurrent.futures.thread import ThreadPoolExecutor
  from pathlib import Path
  import typing as T
  
  from . import mlog
  from .mesonlib import quiet_git, GitException, Popen_safe, MesonException, windows_proof_rmtree
  from .wrap.wrap import PackageDefinition, Resolver, WrapException, ALL_TYPES
  from .wrap import wraptool
  
  ALL_TYPES_STRING = ', '.join(ALL_TYPES)
  
  class Logger:
      def __init__(self, total_tasks: int) -> None:
          self.lock = threading.Lock()
          self.total_tasks = total_tasks
          self.completed_tasks = 0
          self.running_tasks = set()
          self.should_erase_line = ''
  
      def flush(self) -> None:
          if self.should_erase_line:
              print(self.should_erase_line, end='\r')
              self.should_erase_line = ''
  
      def print_progress(self) -> None:
          line = f'Progress: {self.completed_tasks} / {self.total_tasks}'
          max_len = shutil.get_terminal_size().columns - len(line)
          running = ', '.join(self.running_tasks)
          if len(running) + 3 > max_len:
              running = running[:max_len - 6] + '...'
          line = line + f' ({running})'
          print(self.should_erase_line, line, sep='', end='\r')
          self.should_erase_line = '\x1b[K'
  
      def start(self, wrap_name: str) -> None:
          with self.lock:
              self.running_tasks.add(wrap_name)
              self.print_progress()
  
      def done(self, wrap_name: str, log_queue: T.List[T.Tuple[mlog.TV_LoggableList, T.Any]]) -> None:
          with self.lock:
              self.flush()
              for args, kwargs in log_queue:
                  mlog.log(*args, **kwargs)
              self.running_tasks.remove(wrap_name)
              self.completed_tasks += 1
              self.print_progress()
  
  
  class Runner:
      def __init__(self, logger: Logger, r: Resolver, wrap: PackageDefinition, repo_dir: str, options: argparse.Namespace) -> None:
          # FIXME: Do a copy because Resolver.resolve() is stateful method that
          # cannot be called from multiple threads.
          self.wrap_resolver = copy.copy(r)
          self.wrap = wrap
          self.repo_dir = repo_dir
          self.options = options
          self.run_method = options.subprojects_func.__get__(self)
          self.log_queue = []
          self.logger = logger
  
      def log(self, *args, **kwargs):
          self.log_queue.append((args, kwargs))
  
      def run(self):
          self.logger.start(self.wrap.name)
          result = self.run_method()
          self.logger.done(self.wrap.name, self.log_queue)
          return result
  
      def update_wrapdb_file(self):
          try:
              patch_url = self.wrap.get('patch_url')
              branch, revision = wraptool.parse_patch_url(patch_url)
          except WrapException:
              return
          new_branch, new_revision = wraptool.get_latest_version(self.wrap.name)
          if new_branch != branch or new_revision != revision:
              wraptool.update_wrap_file(self.wrap.filename, self.wrap.name, new_branch, new_revision)
              self.log('  -> New wrap file downloaded.')
  
      def update_file(self):
          self.update_wrapdb_file()
          if not os.path.isdir(self.repo_dir):
              # The subproject is not needed, or it is a tarball extracted in
              # 'libfoo-1.0' directory and the version has been bumped and the new
              # directory is 'libfoo-2.0'. In that case forcing a meson
              # reconfigure will download and use the new tarball.
              self.log('  -> Not used.')
              return True
          elif self.options.reset:
              # Delete existing directory and redownload. It is possible that nothing
              # changed but we have no way to know. Hopefully tarballs are still
              # cached.
              windows_proof_rmtree(self.repo_dir)
              try:
                  self.wrap_resolver.resolve(self.wrap.name, 'meson')
                  self.log('  -> New version extracted')
                  return True
              except WrapException as e:
                  self.log('  ->', mlog.red(str(e)))
                  return False
          else:
              # The subproject has not changed, or the new source and/or patch
              # tarballs should be extracted in the same directory than previous
              # version.
              self.log('  -> Subproject has not changed, or the new source/patch needs to be extracted on the same location.')
              self.log('     Pass --reset option to delete directory and redownload.')
              return False
  
      def git_output(self, cmd):
          return quiet_git(cmd, self.repo_dir, check=True)[1]
  
      def git_verbose(self, cmd):
          self.log(self.git_output(cmd))
  
      def git_stash(self):
          # That git command return 1 (failure) when there is something to stash.
          # We don't want to stash when there is nothing to stash because that would
          # print spurious "No local changes to save".
          if not quiet_git(['diff', '--quiet', 'HEAD'], self.repo_dir)[0]:
              # Don't pipe stdout here because we want the user to see their changes have
              # been saved.
              self.git_verbose(['stash'])
  
      def git_show(self):
          commit_message = self.git_output(['show', '--quiet', '--pretty=format:%h%n%d%n%s%n[%an]'])
          parts = [s.strip() for s in commit_message.split('\n')]
          self.log('  ->', mlog.yellow(parts[0]), mlog.red(parts[1]), parts[2], mlog.blue(parts[3]))
  
      def git_rebase(self, revision):
          try:
              self.git_output(['-c', 'rebase.autoStash=true', 'rebase', 'FETCH_HEAD'])
          except GitException as e:
              self.log('  -> Could not rebase', mlog.bold(self.repo_dir), 'onto', mlog.bold(revision))
              self.log(mlog.red(e.output))
              self.log(mlog.red(str(e)))
              return False
          return True
  
      def git_reset(self, revision):
          try:
              # Stash local changes, commits can always be found back in reflog, to
              # avoid any data lost by mistake.
              self.git_stash()
              self.git_output(['reset', '--hard', 'FETCH_HEAD'])
          except GitException as e:
              self.log('  -> Could not reset', mlog.bold(repo_dir), 'to', mlog.bold(revision))
              self.log(mlog.red(e.output))
              self.log(mlog.red(str(e)))
              return False
          return True
  
      def git_checkout(self, revision, create=False):
          cmd = ['checkout', '--ignore-other-worktrees', revision, '--']
          if create:
              cmd.insert('-b', 1)
          try:
              # Stash local changes, commits can always be found back in reflog, to
              # avoid any data lost by mistake.
              self.git_stash()
              self.git_output(cmd)
          except GitException as e:
              self.log('  -> Could not checkout', mlog.bold(revision), 'in', mlog.bold(self.repo_dir))
              self.log(mlog.red(e.output))
              self.log(mlog.red(str(e)))
              return False
          return True
  
      def git_checkout_and_reset(self, revision):
          # revision could be a branch that already exists but is outdated, so we still
          # have to reset after the checkout.
          success = self.git_checkout(revision)
          if success:
              success = self.git_reset(revision)
          return success
  
      def git_checkout_and_rebase(self, revision):
          # revision could be a branch that already exists but is outdated, so we still
          # have to rebase after the checkout.
          success = self.git_checkout(revision)
          if success:
              success = self.git_rebase(revision)
          return success
  
      def update_git(self):
          if not os.path.isdir(self.repo_dir):
              self.log('  -> Not used.')
              return True
          if not os.path.exists(os.path.join(self.repo_dir, '.git')):
              if self.options.reset:
                  # Delete existing directory and redownload
                  windows_proof_rmtree(self.repo_dir)
                  try:
                      self.wrap_resolver.resolve(self.wrap.name, 'meson')
                      self.update_git_done()
                      return True
                  except WrapException as e:
                      self.log('  ->', mlog.red(str(e)))
                      return False
              else:
                  self.log('  -> Not a git repository.')
                  self.log('Pass --reset option to delete directory and redownload.')
                  return False
          revision = self.wrap.values.get('revision')
          url = self.wrap.values.get('url')
          push_url = self.wrap.values.get('push-url')
          if not revision or not url:
              # It could be a detached git submodule for example.
              self.log('  -> No revision or URL specified.')
              return True
          try:
              origin_url = self.git_output(['remote', 'get-url', 'origin']).strip()
          except GitException as e:
              self.log('  -> Failed to determine current origin URL in', mlog.bold(self.repo_dir))
              self.log(mlog.red(e.output))
              self.log(mlog.red(str(e)))
              return False
          if self.options.reset:
              try:
                  self.git_output(['remote', 'set-url', 'origin', url])
                  if push_url:
                      self.git_output(['remote', 'set-url', '--push', 'origin', push_url])
              except GitException as e:
                  self.log('  -> Failed to reset origin URL in', mlog.bold(self.repo_dir))
                  self.log(mlog.red(e.output))
                  self.log(mlog.red(str(e)))
                  return False
          elif url != origin_url:
              self.log(f'  -> URL changed from {origin_url!r} to {url!r}')
              return False
          try:
              # Same as `git branch --show-current` but compatible with older git version
              branch = self.git_output(['rev-parse', '--abbrev-ref', 'HEAD']).strip()
              branch = branch if branch != 'HEAD' else ''
          except GitException as e:
              self.log('  -> Failed to determine current branch in', mlog.bold(self.repo_dir))
              self.log(mlog.red(e.output))
              self.log(mlog.red(str(e)))
              return False
          try:
              # Fetch only the revision we need, this avoids fetching useless branches.
              # revision can be either a branch, tag or commit id. In all cases we want
              # FETCH_HEAD to be set to the desired commit and "git checkout <revision>"
              # to to either switch to existing/new branch, or detach to tag/commit.
              # It is more complicated than it first appear, see discussion there:
              # https://github.com/mesonbuild/meson/pull/7723#discussion_r488816189.
              heads_refmap = '+refs/heads/*:refs/remotes/origin/*'
              tags_refmap = '+refs/tags/*:refs/tags/*'
              self.git_output(['fetch', '--refmap', heads_refmap, '--refmap', tags_refmap, 'origin', revision])
          except GitException as e:
              self.log('  -> Could not fetch revision', mlog.bold(revision), 'in', mlog.bold(self.repo_dir))
              self.log(mlog.red(e.output))
              self.log(mlog.red(str(e)))
              return False
  
          if branch == '':
              # We are currently in detached mode
              if self.options.reset:
                  success = self.git_checkout_and_reset(revision)
              else:
                  success = self.git_checkout_and_rebase(revision)
          elif branch == revision:
              # We are in the same branch. A reset could still be needed in the case
              # a force push happened on remote repository.
              if self.options.reset:
                  success = self.git_reset(revision)
              else:
                  success = self.git_rebase(revision)
          else:
              # We are in another branch, either the user created their own branch and
              # we should rebase it, or revision changed in the wrap file and we need
              # to checkout the new branch.
              if self.options.reset:
                  success = self.git_checkout_and_reset(revision)
              else:
                  success = self.git_rebase(revision)
          if success:
              self.update_git_done()
          return success
  
      def update_git_done(self):
          self.git_output(['submodule', 'update', '--checkout', '--recursive'])
          self.git_show()
  
      def update_hg(self):
          if not os.path.isdir(self.repo_dir):
              self.log('  -> Not used.')
              return True
          revno = self.wrap.get('revision')
          if revno.lower() == 'tip':
              # Failure to do pull is not a fatal error,
              # because otherwise you can't develop without
              # a working net connection.
              subprocess.call(['hg', 'pull'], cwd=self.repo_dir)
          else:
              if subprocess.call(['hg', 'checkout', revno], cwd=self.repo_dir) != 0:
                  subprocess.check_call(['hg', 'pull'], cwd=self.repo_dir)
                  subprocess.check_call(['hg', 'checkout', revno], cwd=self.repo_dir)
          return True
  
      def update_svn(self):
          if not os.path.isdir(self.repo_dir):
              self.log('  -> Not used.')
              return True
          revno = self.wrap.get('revision')
          p, out, _ = Popen_safe(['svn', 'info', '--show-item', 'revision', self.repo_dir])
          current_revno = out
          if current_revno == revno:
              return True
          if revno.lower() == 'head':
              # Failure to do pull is not a fatal error,
              # because otherwise you can't develop without
              # a working net connection.
              subprocess.call(['svn', 'update'], cwd=self.repo_dir)
          else:
              subprocess.check_call(['svn', 'update', '-r', revno], cwd=self.repo_dir)
          return True
  
      def update(self):
          self.log(f'Updating {self.wrap.name}...')
          if self.wrap.type == 'file':
              return self.update_file()
          elif self.wrap.type == 'git':
              return self.update_git()
          elif self.wrap.type == 'hg':
              return self.update_hg()
          elif self.wrap.type == 'svn':
              return self.update_svn()
          elif self.wrap.type is None:
              self.log('  -> Cannot update subproject with no wrap file')
          else:
              self.log('  -> Cannot update', self.wrap.type, 'subproject')
          return True
  
      def checkout(self):
          if self.wrap.type != 'git' or not os.path.isdir(self.repo_dir):
              return True
          branch_name = self.options.branch_name if self.options.branch_name else self.wrap.get('revision')
          if not branch_name:
              # It could be a detached git submodule for example.
              return True
          self.log(f'Checkout {branch_name} in {self.wrap.name}...')
          if self.git_checkout(branch_name, create=self.options.b):
              self.git_show()
              return True
          return False
  
      def download(self):
          self.log(f'Download {self.wrap.name}...')
          if os.path.isdir(self.repo_dir):
              self.log('  -> Already downloaded')
              return True
          try:
              self.wrap_resolver.resolve(self.wrap.name, 'meson')
              self.log('  -> done')
          except WrapException as e:
              self.log('  ->', mlog.red(str(e)))
              return False
          return True
  
      def foreach(self):
          self.log(f'Executing command in {self.repo_dir}')
          if not os.path.isdir(self.repo_dir):
              self.log('  -> Not downloaded yet')
              return True
          cmd = [self.options.command] + self.options.args
          p, out, _ = Popen_safe(cmd, stderr=subprocess.STDOUT, cwd=self.repo_dir)
          if p.returncode != 0:
              err_message = "Command '{}' returned non-zero exit status {}.".format(" ".join(cmd), p.returncode)
              self.log('  -> ', mlog.red(err_message))
              self.log(out, end='')
              return False
  
          self.log(out, end='')
          return True
  
      def purge(self) -> bool:
          # if subproject is not wrap-based, then don't remove it
          if not self.wrap.type:
              return True
  
          if self.wrap.type == 'redirect':
              redirect_file = Path(self.wrap.filename).resolve()
              if self.options.confirm:
                  redirect_file.unlink()
              self.log(f'Deleting {redirect_file}')
  
          if self.options.include_cache:
              packagecache = Path(self.wrap_resolver.cachedir).resolve()
              try:
                  subproject_cache_file = packagecache / self.wrap.get("source_filename")
                  if subproject_cache_file.is_file():
                      if self.options.confirm:
                          subproject_cache_file.unlink()
                      self.log(f'Deleting {subproject_cache_file}')
              except WrapException:
                  pass
  
              try:
                  subproject_patch_file = packagecache / self.wrap.get("patch_filename")
                  if subproject_patch_file.is_file():
                      if self.options.confirm:
                          subproject_patch_file.unlink()
                      self.log(f'Deleting {subproject_patch_file}')
              except WrapException:
                  pass
  
              # Don't log that we will remove an empty directory. Since purge is
              # parallelized, another thread could have deleted it already.
              try:
                  if not any(packagecache.iterdir()):
                      packagecache.rmdir()
              except FileNotFoundError:
                  pass
  
          subproject_source_dir = Path(self.repo_dir).resolve()
  
          # Don't follow symlink. This is covered by the next if statement, but why
          # not be doubly sure.
          if subproject_source_dir.is_symlink():
              if self.options.confirm:
                  subproject_source_dir.unlink()
              self.log(f'Deleting {subproject_source_dir}')
              return True
          if not subproject_source_dir.is_dir():
              return True
  
          try:
              if self.options.confirm:
                  windows_proof_rmtree(str(subproject_source_dir))
              self.log(f'Deleting {subproject_source_dir}')
          except OSError as e:
              mlog.error(f'Unable to remove: {subproject_source_dir}: {e}')
              return False
  
          return True
  
      @staticmethod
      def post_purge(options):
          if not options.confirm:
              mlog.log('')
              mlog.log('Nothing has been deleted, run again with --confirm to apply.')
  
  def add_common_arguments(p):
      p.add_argument('--sourcedir', default='.',
                     help='Path to source directory')
      p.add_argument('--types', default='',
                     help=f'Comma-separated list of subproject types. Supported types are: {ALL_TYPES_STRING} (default: all)')
      p.add_argument('--num-processes', default=None, type=int,
                     help='How many parallel processes to use (Since 0.59.0).')
  
  def add_subprojects_argument(p):
      p.add_argument('subprojects', nargs='*',
                     help='List of subprojects (default: all)')
  
  def add_arguments(parser):
      subparsers = parser.add_subparsers(title='Commands', dest='command')
      subparsers.required = True
  
      p = subparsers.add_parser('update', help='Update all subprojects from wrap files')
      p.add_argument('--rebase', default=True, action='store_true',
                     help='Rebase your branch on top of wrap\'s revision. ' + \
                          'Deprecated, it is now the default behaviour. (git only)')
      p.add_argument('--reset', default=False, action='store_true',
                     help='Checkout wrap\'s revision and hard reset to that commit. (git only)')
      add_common_arguments(p)
      add_subprojects_argument(p)
      p.set_defaults(subprojects_func=Runner.update)
  
      p = subparsers.add_parser('checkout', help='Checkout a branch (git only)')
      p.add_argument('-b', default=False, action='store_true',
                     help='Create a new branch')
      p.add_argument('branch_name', nargs='?',
                     help='Name of the branch to checkout or create (default: revision set in wrap file)')
      add_common_arguments(p)
      add_subprojects_argument(p)
      p.set_defaults(subprojects_func=Runner.checkout)
  
      p = subparsers.add_parser('download', help='Ensure subprojects are fetched, even if not in use. ' +
                                                 'Already downloaded subprojects are not modified. ' +
                                                 'This can be used to pre-fetch all subprojects and avoid downloads during configure.')
      add_common_arguments(p)
      add_subprojects_argument(p)
      p.set_defaults(subprojects_func=Runner.download)
  
      p = subparsers.add_parser('foreach', help='Execute a command in each subproject directory.')
      p.add_argument('command', metavar='command ...',
                     help='Command to execute in each subproject directory')
      p.add_argument('args', nargs=argparse.REMAINDER,
                     help=argparse.SUPPRESS)
      add_common_arguments(p)
      p.set_defaults(subprojects=[])
      p.set_defaults(subprojects_func=Runner.foreach)
  
      p = subparsers.add_parser('purge', help='Remove all wrap-based subproject artifacts')
      add_common_arguments(p)
      add_subprojects_argument(p)
      p.add_argument('--include-cache', action='store_true', default=False, help='Remove the package cache as well')
      p.add_argument('--confirm', action='store_true', default=False, help='Confirm the removal of subproject artifacts')
      p.set_defaults(subprojects_func=Runner.purge)
      p.set_defaults(post_func=Runner.post_purge)
  
  def run(options):
      src_dir = os.path.relpath(os.path.realpath(options.sourcedir))
      if not os.path.isfile(os.path.join(src_dir, 'meson.build')):
          mlog.error('Directory', mlog.bold(src_dir), 'does not seem to be a Meson source directory.')
          return 1
      subprojects_dir = os.path.join(src_dir, 'subprojects')
      if not os.path.isdir(subprojects_dir):
          mlog.log('Directory', mlog.bold(src_dir), 'does not seem to have subprojects.')
          return 0
      r = Resolver(src_dir, 'subprojects')
      if options.subprojects:
          wraps = [wrap for name, wrap in r.wraps.items() if name in options.subprojects]
      else:
          wraps = r.wraps.values()
      types = [t.strip() for t in options.types.split(',')] if options.types else []
      for t in types:
          if t not in ALL_TYPES:
              raise MesonException(f'Unknown subproject type {t!r}, supported types are: {ALL_TYPES_STRING}')
      tasks = []
      task_names = []
      loop = asyncio.get_event_loop()
      executor = ThreadPoolExecutor(options.num_processes)
      if types:
          wraps = [wrap for wrap in wraps if wrap.type in types]
      logger = Logger(len(wraps))
      for wrap in wraps:
          dirname = Path(subprojects_dir, wrap.directory).as_posix()
          runner = Runner(logger, r, wrap, dirname, options)
          task = loop.run_in_executor(executor, runner.run)
          tasks.append(task)
          task_names.append(wrap.name)
      results = loop.run_until_complete(asyncio.gather(*tasks))
      logger.flush()
      post_func = getattr(options, 'post_func', None)
      if post_func:
          post_func(options)
      failures = [name for name, success in zip(task_names, results) if not success]
      if failures:
          m = 'Please check logs above as command failed in some subprojects which could have been left in conflict state: '
          m += ', '.join(failures)
          mlog.warning(m)
      return len(failures)
  
  
  [... the rest is omitted ...]
files.old_path: mesonbuild/msubprojects.py
files.new_contents: # Copyright 2015 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 .. import mlog
  import contextlib
  import urllib.request
  import urllib.error
  import urllib.parse
  import os
  import hashlib
  import shutil
  import tempfile
  import stat
  import subprocess
  import sys
  import configparser
  import typing as T
  import textwrap
  
  from pathlib import Path
  from . import WrapMode
  from .. import coredata
  from ..mesonlib import quiet_git, GIT, ProgressBar, MesonException
  from  .. import mesonlib
  
  if T.TYPE_CHECKING:
      import http.client
  
  try:
      # Importing is just done to check if SSL exists, so all warnings
      # regarding 'imported but unused' can be safely ignored
      import ssl  # noqa
      has_ssl = True
  except ImportError:
      has_ssl = False
  
  REQ_TIMEOUT = 600.0
  SSL_WARNING_PRINTED = False
  WHITELIST_SUBDOMAIN = 'wrapdb.mesonbuild.com'
  
  ALL_TYPES = ['file', 'git', 'hg', 'svn']
  
  def whitelist_wrapdb(urlstr: str) -> urllib.parse.ParseResult:
      """ raises WrapException if not whitelisted subdomain """
      url = urllib.parse.urlparse(urlstr)
      if not url.hostname:
          raise WrapException(f'{urlstr} is not a valid URL')
      if not url.hostname.endswith(WHITELIST_SUBDOMAIN):
          raise WrapException(f'{urlstr} is not a whitelisted WrapDB URL')
      if has_ssl and not url.scheme == 'https':
          raise WrapException(f'WrapDB did not have expected SSL https url, instead got {urlstr}')
      return url
  
  def open_wrapdburl(urlstring: str) -> 'http.client.HTTPResponse':
      global SSL_WARNING_PRINTED
  
      url = whitelist_wrapdb(urlstring)
      if has_ssl:
          try:
              return T.cast('http.client.HTTPResponse', urllib.request.urlopen(urllib.parse.urlunparse(url), timeout=REQ_TIMEOUT))
          except urllib.error.URLError as excp:
              raise WrapException(f'WrapDB connection failed to {urlstring} with error {excp}')
  
      # following code is only for those without Python SSL
      nossl_url = url._replace(scheme='http')
      if not SSL_WARNING_PRINTED:
          mlog.warning(f'SSL module not available in {sys.executable}: WrapDB traffic not authenticated.')
          SSL_WARNING_PRINTED = True
      try:
          return T.cast('http.client.HTTPResponse', urllib.request.urlopen(urllib.parse.urlunparse(nossl_url), timeout=REQ_TIMEOUT))
      except urllib.error.URLError as excp:
          raise WrapException(f'WrapDB connection failed to {urlstring} with error {excp}')
  
  
  class WrapException(MesonException):
      pass
  
  class WrapNotFoundException(WrapException):
      pass
  
  class PackageDefinition:
      def __init__(self, fname: str):
          self.filename = fname
          self.type = None  # type: T.Optional[str]
          self.values = {} # type: T.Dict[str, str]
          self.provided_deps = {} # type: T.Dict[str, T.Optional[str]]
          self.provided_programs = [] # type: T.List[str]
          self.basename = os.path.basename(fname)
          self.has_wrap = self.basename.endswith('.wrap')
          self.name = self.basename[:-5] if self.has_wrap else self.basename
          self.directory = self.name
          self.provided_deps[self.name] = None
          self.original_filename = fname
          self.redirected = False
          if self.has_wrap:
              self.parse_wrap()
          self.directory = self.values.get('directory', self.name)
          if os.path.dirname(self.directory):
              raise WrapException('Directory key must be a name and not a path')
          if self.type and self.type not in ALL_TYPES:
              raise WrapException(f'Unknown wrap type {self.type!r}')
          self.filesdir = os.path.join(os.path.dirname(self.filename), 'packagefiles')
          # What the original file name was before redirection
  
      def parse_wrap(self) -> None:
          try:
              config = configparser.ConfigParser(interpolation=None)
              config.read(self.filename)
          except configparser.Error as e:
              raise WrapException('Failed to parse {}: {}'.format(self.basename, str(e)))
          self.parse_wrap_section(config)
          if self.type == 'redirect':
              # [wrap-redirect] have a `filename` value pointing to the real wrap
              # file we should parse instead. It must be relative to the current
              # wrap file location and must be in the form foo/subprojects/bar.wrap.
              dirname = Path(self.filename).parent
              fname = Path(self.values['filename'])
              for i, p in enumerate(fname.parts):
                  if i % 2 == 0:
                      if p == '..':
                          raise WrapException('wrap-redirect filename cannot contain ".."')
                  else:
                      if p != 'subprojects':
                          raise WrapException('wrap-redirect filename must be in the form foo/subprojects/bar.wrap')
              if fname.suffix != '.wrap':
                  raise WrapException('wrap-redirect filename must be a .wrap file')
              fname = dirname / fname
              if not fname.is_file():
                  raise WrapException(f'wrap-redirect {fname} filename does not exist')
              self.filename = str(fname)
              self.parse_wrap()
              self.redirected = True
              return
          self.parse_provide_section(config)
  
      def parse_wrap_section(self, config: configparser.ConfigParser) -> None:
          if len(config.sections()) < 1:
              raise WrapException(f'Missing sections in {self.basename}')
          self.wrap_section = config.sections()[0]
          if not self.wrap_section.startswith('wrap-'):
              m = '{!r} is not a valid first section in {}'
              raise WrapException(m.format(self.wrap_section, self.basename))
          self.type = self.wrap_section[5:]
          self.values = dict(config[self.wrap_section])
  
      def parse_provide_section(self, config: configparser.ConfigParser) -> None:
          if config.has_section('provide'):
              for k, v in config['provide'].items():
                  if k == 'dependency_names':
                      # A comma separated list of dependency names that does not
                      # need a variable name
                      names_dict = {n.strip(): None for n in v.split(',')}
                      self.provided_deps.update(names_dict)
                      continue
                  if k == 'program_names':
                      # A comma separated list of program names
                      names_list = [n.strip() for n in v.split(',')]
                      self.provided_programs += names_list
                      continue
                  if not v:
                      m = ('Empty dependency variable name for {!r} in {}. '
                           'If the subproject uses meson.override_dependency() '
                           'it can be added in the "dependency_names" special key.')
                      raise WrapException(m.format(k, self.basename))
                  self.provided_deps[k] = v
  
      def get(self, key: str) -> str:
          try:
              return self.values[key]
          except KeyError:
              m = 'Missing key {!r} in {}'
              raise WrapException(m.format(key, self.basename))
  
  def get_directory(subdir_root: str, packagename: str) -> str:
      fname = os.path.join(subdir_root, packagename + '.wrap')
      if os.path.isfile(fname):
          wrap = PackageDefinition(fname)
          return wrap.directory
      return packagename
  
  def verbose_git(cmd: T.List[str], workingdir: str, check: bool = False) -> bool:
      '''
      Wrapper to convert GitException to WrapException caught in interpreter.
      '''
      try:
          return mesonlib.verbose_git(cmd, workingdir, check=check)
      except mesonlib.GitException as e:
          raise WrapException(str(e))
  
  class Resolver:
      def __init__(self, source_dir: str, subdir: str, wrap_mode: WrapMode = WrapMode.default) -> None:
          self.source_dir = source_dir
          self.subdir = subdir
          self.wrap_mode = wrap_mode
          self.subdir_root = os.path.join(source_dir, subdir)
          self.cachedir = os.path.join(self.subdir_root, 'packagecache')
          self.wraps = {} # type: T.Dict[str, PackageDefinition]
          self.provided_deps = {} # type: T.Dict[str, PackageDefinition]
          self.provided_programs = {} # type: T.Dict[str, PackageDefinition]
          self.load_wraps()
  
      def load_wraps(self) -> None:
          if not os.path.isdir(self.subdir_root):
              return
          root, dirs, files = next(os.walk(self.subdir_root))
          for i in files:
              if not i.endswith('.wrap'):
                  continue
              fname = os.path.join(self.subdir_root, i)
              wrap = PackageDefinition(fname)
              self.wraps[wrap.name] = wrap
              if wrap.directory in dirs:
                  dirs.remove(wrap.directory)
          # Add dummy package definition for directories not associated with a wrap file.
          for i in dirs:
              if i in ['packagecache', 'packagefiles']:
                  continue
              fname = os.path.join(self.subdir_root, i)
              wrap = PackageDefinition(fname)
              self.wraps[wrap.name] = wrap
  
          for wrap in self.wraps.values():
              for k in wrap.provided_deps.keys():
                  if k in self.provided_deps:
                      prev_wrap = self.provided_deps[k]
                      m = 'Multiple wrap files provide {!r} dependency: {} and {}'
                      raise WrapException(m.format(k, wrap.basename, prev_wrap.basename))
                  self.provided_deps[k] = wrap
              for k in wrap.provided_programs:
                  if k in self.provided_programs:
                      prev_wrap = self.provided_programs[k]
                      m = 'Multiple wrap files provide {!r} program: {} and {}'
                      raise WrapException(m.format(k, wrap.basename, prev_wrap.basename))
                  self.provided_programs[k] = wrap
  
      def merge_wraps(self, other_resolver: 'Resolver') -> None:
          for k, v in other_resolver.wraps.items():
              self.wraps.setdefault(k, v)
          for k, v in other_resolver.provided_deps.items():
              self.provided_deps.setdefault(k, v)
          for k, v in other_resolver.provided_programs.items():
              self.provided_programs.setdefault(k, v)
  
      def find_dep_provider(self, packagename: str) -> T.Tuple[T.Optional[str], T.Optional[str]]:
          # Python's ini parser converts all key values to lowercase.
          # Thus the query name must also be in lower case.
          packagename = packagename.lower()
          wrap = self.provided_deps.get(packagename)
          if wrap:
              dep_var = wrap.provided_deps.get(packagename)
              return wrap.name, dep_var
          return None, None
  
      def get_varname(self, subp_name: str, depname: str) -> T.Optional[str]:
          wrap = self.wraps.get(subp_name)
          return wrap.provided_deps.get(depname) if wrap else None
  
      def find_program_provider(self, names: T.List[str]) -> T.Optional[str]:
          for name in names:
              wrap = self.provided_programs.get(name)
              if wrap:
                  return wrap.name
          return None
  
      def resolve(self, packagename: str, method: str, current_subproject: str = '') -> str:
          self.current_subproject = current_subproject
          self.packagename = packagename
          self.directory = packagename
          self.wrap = self.wraps.get(packagename)
          if not self.wrap:
              m = 'Neither a subproject directory nor a {}.wrap file was found.'
              raise WrapNotFoundException(m.format(self.packagename))
          self.directory = self.wrap.directory
  
          if self.wrap.has_wrap:
              # We have a .wrap file, source code will be placed into main
              # project's subproject_dir even if the wrap file comes from another
              # subproject.
              self.dirname = os.path.join(self.subdir_root, self.directory)
              # Check if the wrap comes from the main project.
              main_fname = os.path.join(self.subdir_root, self.wrap.basename)
              if self.wrap.filename != main_fname:
                  rel = os.path.relpath(self.wrap.filename, self.source_dir)
                  mlog.log('Using', mlog.bold(rel))
                  # Write a dummy wrap file in main project that redirect to the
                  # wrap we picked.
                  with open(main_fname, 'w', encoding='utf-8') as f:
                      f.write(textwrap.dedent('''\
                          [wrap-redirect]
                          filename = {}
                          '''.format(os.path.relpath(self.wrap.filename, self.subdir_root))))
          else:
              # No wrap file, it's a dummy package definition for an existing
              # directory. Use the source code in place.
              self.dirname = self.wrap.filename
          rel_path = os.path.relpath(self.dirname, self.source_dir)
  
          meson_file = os.path.join(self.dirname, 'meson.build')
          cmake_file = os.path.join(self.dirname, 'CMakeLists.txt')
  
          if method not in ['meson', 'cmake']:
              raise WrapException('Only the methods "meson" and "cmake" are supported')
  
          # The directory is there and has meson.build? Great, use it.
          if method == 'meson' and os.path.exists(meson_file):
              return rel_path
          if method == 'cmake' and os.path.exists(cmake_file):
              return rel_path
  
          # Check if the subproject is a git submodule
          self.resolve_git_submodule()
  
          if os.path.exists(self.dirname):
              if not os.path.isdir(self.dirname):
                  raise WrapException('Path already exists but is not a directory')
          else:
              if self.wrap.type == 'file':
                  self.get_file()
              else:
                  self.check_can_download()
                  if self.wrap.type == 'git':
                      self.get_git()
                  elif self.wrap.type == "hg":
                      self.get_hg()
                  elif self.wrap.type == "svn":
                      self.get_svn()
                  else:
                      raise WrapException(f'Unknown wrap type {self.wrap.type!r}')
              self.apply_patch()
  
          # A meson.build or CMakeLists.txt file is required in the directory
          if method == 'meson' and not os.path.exists(meson_file):
              raise WrapException('Subproject exists but has no meson.build file')
          if method == 'cmake' and not os.path.exists(cmake_file):
              raise WrapException('Subproject exists but has no CMakeLists.txt file')
  
          return rel_path
  
      def check_can_download(self) -> None:
          # Don't download subproject data based on wrap file if requested.
          # Git submodules are ok (see above)!
          if self.wrap_mode is WrapMode.nodownload:
              m = 'Automatic wrap-based subproject downloading is disabled'
              raise WrapException(m)
  
      def resolve_git_submodule(self) -> bool:
          # Is git installed? If not, we're probably not in a git repository and
          # definitely cannot try to conveniently set up a submodule.
          if not GIT:
              return False
          # Are we in a git repository?
          ret, out = quiet_git(['rev-parse'], self.subdir_root)
          if not ret:
              return False
          # Is `dirname` a submodule?
          ret, out = quiet_git(['submodule', 'status', self.dirname], self.subdir_root)
          if not ret:
              return False
          # Submodule has not been added, add it
          if out.startswith('+'):
              mlog.warning('git submodule might be out of date')
              return True
          elif out.startswith('U'):
              raise WrapException('git submodule has merge conflicts')
          # Submodule exists, but is deinitialized or wasn't initialized
          elif out.startswith('-'):
              if verbose_git(['submodule', 'update', '--init', self.dirname], self.subdir_root):
                  return True
              raise WrapException('git submodule failed to init')
          # Submodule looks fine, but maybe it wasn't populated properly. Do a checkout.
          elif out.startswith(' '):
              verbose_git(['checkout', '.'], self.dirname)
              # Even if checkout failed, try building it anyway and let the user
              # handle any problems manually.
              return True
          elif out == '':
              # It is not a submodule, just a folder that exists in the main repository.
              return False
          m = 'Unknown git submodule output: {!r}'
          raise WrapException(m.format(out))
  
      def get_file(self) -> None:
          path = self.get_file_internal('source')
          extract_dir = self.subdir_root
          # Some upstreams ship packages that do not have a leading directory.
          # Create one for them.
          if 'lead_directory_missing' in self.wrap.values:
              os.mkdir(self.dirname)
              extract_dir = self.dirname
          shutil.unpack_archive(path, extract_dir)
  
      def get_git(self) -> None:
          if not GIT:
              raise WrapException('Git program not found.')
          revno = self.wrap.get('revision')
          is_shallow = False
          depth_option = []    # type: T.List[str]
          if self.wrap.values.get('depth', '') != '':
              is_shallow = True
              depth_option = ['--depth', self.wrap.values.get('depth')]
          # for some reason git only allows commit ids to be shallowly fetched by fetch not with clone
          if is_shallow and self.is_git_full_commit_id(revno):
              # git doesn't support directly cloning shallowly for commits,
              # so we follow https://stackoverflow.com/a/43136160
              verbose_git(['init', self.directory], self.subdir_root, check=True)
              verbose_git(['remote', 'add', 'origin', self.wrap.get('url')], self.dirname, check=True)
              revno = self.wrap.get('revision')
              verbose_git(['fetch', *depth_option, 'origin', revno], self.dirname, check=True)
              verbose_git(['checkout', revno, '--'], self.dirname, check=True)
              if self.wrap.values.get('clone-recursive', '').lower() == 'true':
                  verbose_git(['submodule', 'update', '--init', '--checkout',
                               '--recursive', *depth_option], self.dirname, check=True)
              push_url = self.wrap.values.get('push-url')
              if push_url:
                  verbose_git(['remote', 'set-url', '--push', 'origin', push_url], self.dirname, check=True)
          else:
              if not is_shallow:
                  verbose_git(['clone', self.wrap.get('url'), self.directory], self.subdir_root, check=True)
                  if revno.lower() != 'head':
                      if not verbose_git(['checkout', revno, '--'], self.dirname):
                          verbose_git(['fetch', self.wrap.get('url'), revno], self.dirname, check=True)
                          verbose_git(['checkout', revno, '--'], self.dirname, check=True)
              else:
                  verbose_git(['clone', *depth_option, '--branch', revno, self.wrap.get('url'),
                               self.directory], self.subdir_root, check=True)
              if self.wrap.values.get('clone-recursive', '').lower() == 'true':
                  verbose_git(['submodule', 'update', '--init', '--checkout', '--recursive', *depth_option],
                              self.dirname, check=True)
              push_url = self.wrap.values.get('push-url')
              if push_url:
                  verbose_git(['remote', 'set-url', '--push', 'origin', push_url], self.dirname, check=True)
  
      def is_git_full_commit_id(self, revno: str) -> bool:
          result = False
          if len(revno) in (40, 64): # 40 for sha1, 64 for upcoming sha256
              result = all(ch in '0123456789AaBbCcDdEeFf' for ch in revno)
          return result
  
      def get_hg(self) -> None:
          revno = self.wrap.get('revision')
          hg = shutil.which('hg')
          if not hg:
              raise WrapException('Mercurial program not found.')
          subprocess.check_call([hg, 'clone', self.wrap.get('url'),
                                 self.directory], cwd=self.subdir_root)
          if revno.lower() != 'tip':
              subprocess.check_call([hg, 'checkout', revno],
                                    cwd=self.dirname)
  
      def get_svn(self) -> None:
          revno = self.wrap.get('revision')
          svn = shutil.which('svn')
          if not svn:
              raise WrapException('SVN program not found.')
          subprocess.check_call([svn, 'checkout', '-r', revno, self.wrap.get('url'),
                                 self.directory], cwd=self.subdir_root)
  
      def get_data(self, urlstring: str) -> T.Tuple[str, str]:
          blocksize = 10 * 1024
          h = hashlib.sha256()
          tmpfile = tempfile.NamedTemporaryFile(mode='wb', dir=self.cachedir, delete=False)
          url = urllib.parse.urlparse(urlstring)
          if url.hostname and url.hostname.endswith(WHITELIST_SUBDOMAIN):
              resp = open_wrapdburl(urlstring)
          elif WHITELIST_SUBDOMAIN in urlstring:
              raise WrapException(f'{urlstring} may be a WrapDB-impersonating URL')
          else:
              try:
                  req = urllib.request.Request(urlstring, headers={'User-Agent': f'mesonbuild/{coredata.version}'})
                  resp = urllib.request.urlopen(req, timeout=REQ_TIMEOUT)
              except urllib.error.URLError as e:
                  mlog.log(str(e))
                  raise WrapException(f'could not get {urlstring} is the internet available?')
          with contextlib.closing(resp) as resp:
              try:
                  dlsize = int(resp.info()['Content-Length'])
              except TypeError:
                  dlsize = None
              if dlsize is None:
                  print('Downloading file of unknown size.')
                  while True:
                      block = resp.read(blocksize)
                      if block == b'':
                          break
                      h.update(block)
                      tmpfile.write(block)
                  hashvalue = h.hexdigest()
                  return hashvalue, tmpfile.name
              sys.stdout.flush()
              progress_bar = ProgressBar(bar_type='download', total=dlsize,
                                         desc='Downloading')
              while True:
                  block = resp.read(blocksize)
                  if block == b'':
                      break
                  h.update(block)
                  tmpfile.write(block)
                  progress_bar.update(len(block))
              progress_bar.close()
              hashvalue = h.hexdigest()
          return hashvalue, tmpfile.name
  
      def check_hash(self, what: str, path: str, hash_required: bool = True) -> None:
          if what + '_hash' not in self.wrap.values and not hash_required:
              return
          expected = self.wrap.get(what + '_hash').lower()
          h = hashlib.sha256()
          with open(path, 'rb') as f:
              h.update(f.read())
          dhash = h.hexdigest()
          if dhash != expected:
              raise WrapException(f'Incorrect hash for {what}:\n {expected} expected\n {dhash} actual.')
  
      def download(self, what: str, ofname: str, fallback: bool = False) -> None:
          self.check_can_download()
          srcurl = self.wrap.get(what + ('_fallback_url' if fallback else '_url'))
          mlog.log('Downloading', mlog.bold(self.packagename), what, 'from', mlog.bold(srcurl))
          try:
              dhash, tmpfile = self.get_data(srcurl)
              expected = self.wrap.get(what + '_hash').lower()
              if dhash != expected:
                  os.remove(tmpfile)
                  raise WrapException(f'Incorrect hash for {what}:\n {expected} expected\n {dhash} actual.')
          except WrapException:
              if not fallback:
                  if what + '_fallback_url' in self.wrap.values:
                      return self.download(what, ofname, fallback=True)
                  mlog.log('A fallback URL could be specified using',
                           mlog.bold(what + '_fallback_url'), 'key in the wrap file')
              raise
          os.rename(tmpfile, ofname)
  
      def get_file_internal(self, what: str) -> str:
          filename = self.wrap.get(what + '_filename')
          if what + '_url' in self.wrap.values:
              cache_path = os.path.join(self.cachedir, filename)
  
              if os.path.exists(cache_path):
                  self.check_hash(what, cache_path)
                  mlog.log('Using', mlog.bold(self.packagename), what, 'from cache.')
                  return cache_path
  
              if not os.path.isdir(self.cachedir):
                  os.mkdir(self.cachedir)
              self.download(what, cache_path)
              return cache_path
          else:
              from ..interpreterbase import FeatureNew
              FeatureNew(f'Local wrap patch files without {what}_url', '0.55.0').use(self.current_subproject)
              path = Path(self.wrap.filesdir) / filename
  
              if not path.exists():
                  raise WrapException(f'File "{path}" does not exist')
              self.check_hash(what, path.as_posix(), hash_required=False)
  
              return path.as_posix()
  
      def apply_patch(self) -> None:
          if 'patch_filename' in self.wrap.values and 'patch_directory' in self.wrap.values:
              m = 'Wrap file {!r} must not have both "patch_filename" and "patch_directory"'
              raise WrapException(m.format(self.wrap.basename))
          if 'patch_filename' in self.wrap.values:
              path = self.get_file_internal('patch')
              try:
                  shutil.unpack_archive(path, self.subdir_root)
              except Exception:
                  with tempfile.TemporaryDirectory() as workdir:
                      shutil.unpack_archive(path, workdir)
                      self.copy_tree(workdir, self.subdir_root)
          elif 'patch_directory' in self.wrap.values:
              from ..interpreterbase import FeatureNew
              FeatureNew('patch_directory', '0.55.0').use(self.current_subproject)
              patch_dir = self.wrap.values['patch_directory']
              src_dir = os.path.join(self.wrap.filesdir, patch_dir)
              if not os.path.isdir(src_dir):
                  raise WrapException(f'patch directory does not exists: {patch_dir}')
              self.copy_tree(src_dir, self.dirname)
  
      def copy_tree(self, root_src_dir: str, root_dst_dir: str) -> None:
          """
          Copy directory tree. Overwrites also read only files.
          """
          for src_dir, _, files in os.walk(root_src_dir):
              dst_dir = src_dir.replace(root_src_dir, root_dst_dir, 1)
              if not os.path.exists(dst_dir):
                  os.makedirs(dst_dir)
              for file_ in files:
                  src_file = os.path.join(src_dir, file_)
                  dst_file = os.path.join(dst_dir, file_)
                  if os.path.exists(dst_file):
                      try:
                          os.remove(dst_file)
                      except PermissionError:
                          os.chmod(dst_file, stat.S_IWUSR)
                          os.remove(dst_file)
                  shutil.copy2(src_file, dst_dir)
  
  
  [... the rest is omitted ...]
files.new_path: mesonbuild/wrap/wrap.py
files.old_contents: # Copyright 2015 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 .. import mlog
  import contextlib
  import urllib.request
  import urllib.error
  import urllib.parse
  import os
  import hashlib
  import shutil
  import tempfile
  import stat
  import subprocess
  import sys
  import configparser
  import typing as T
  import textwrap
  
  from pathlib import Path
  from . import WrapMode
  from .. import coredata
  from ..mesonlib import quiet_git, GIT, ProgressBar, MesonException
  from  .. import mesonlib
  
  if T.TYPE_CHECKING:
      import http.client
  
  try:
      # Importing is just done to check if SSL exists, so all warnings
      # regarding 'imported but unused' can be safely ignored
      import ssl  # noqa
      has_ssl = True
  except ImportError:
      has_ssl = False
  
  REQ_TIMEOUT = 600.0
  SSL_WARNING_PRINTED = False
  WHITELIST_SUBDOMAIN = 'wrapdb.mesonbuild.com'
  
  ALL_TYPES = ['file', 'git', 'hg', 'svn']
  
  def whitelist_wrapdb(urlstr: str) -> urllib.parse.ParseResult:
      """ raises WrapException if not whitelisted subdomain """
      url = urllib.parse.urlparse(urlstr)
      if not url.hostname:
          raise WrapException(f'{urlstr} is not a valid URL')
      if not url.hostname.endswith(WHITELIST_SUBDOMAIN):
          raise WrapException(f'{urlstr} is not a whitelisted WrapDB URL')
      if has_ssl and not url.scheme == 'https':
          raise WrapException(f'WrapDB did not have expected SSL https url, instead got {urlstr}')
      return url
  
  def open_wrapdburl(urlstring: str) -> 'http.client.HTTPResponse':
      global SSL_WARNING_PRINTED
  
      url = whitelist_wrapdb(urlstring)
      if has_ssl:
          try:
              return T.cast('http.client.HTTPResponse', urllib.request.urlopen(urllib.parse.urlunparse(url), timeout=REQ_TIMEOUT))
          except urllib.error.URLError as excp:
              raise WrapException(f'WrapDB connection failed to {urlstring} with error {excp}')
  
      # following code is only for those without Python SSL
      nossl_url = url._replace(scheme='http')
      if not SSL_WARNING_PRINTED:
          mlog.warning(f'SSL module not available in {sys.executable}: WrapDB traffic not authenticated.')
          SSL_WARNING_PRINTED = True
      try:
          return T.cast('http.client.HTTPResponse', urllib.request.urlopen(urllib.parse.urlunparse(nossl_url), timeout=REQ_TIMEOUT))
      except urllib.error.URLError as excp:
          raise WrapException(f'WrapDB connection failed to {urlstring} with error {excp}')
  
  
  class WrapException(MesonException):
      pass
  
  class WrapNotFoundException(WrapException):
      pass
  
  class PackageDefinition:
      def __init__(self, fname: str):
          self.filename = fname
          self.type = None  # type: T.Optional[str]
          self.values = {} # type: T.Dict[str, str]
          self.provided_deps = {} # type: T.Dict[str, T.Optional[str]]
          self.provided_programs = [] # type: T.List[str]
          self.basename = os.path.basename(fname)
          self.has_wrap = self.basename.endswith('.wrap')
          self.name = self.basename[:-5] if self.has_wrap else self.basename
          self.directory = self.name
          self.provided_deps[self.name] = None
          if self.has_wrap:
              self.parse_wrap()
          self.directory = self.values.get('directory', self.name)
          if os.path.dirname(self.directory):
              raise WrapException('Directory key must be a name and not a path')
          if self.type and self.type not in ALL_TYPES:
              raise WrapException(f'Unknown wrap type {self.type!r}')
          self.filesdir = os.path.join(os.path.dirname(self.filename), 'packagefiles')
  
      def parse_wrap(self) -> None:
          try:
              config = configparser.ConfigParser(interpolation=None)
              config.read(self.filename)
          except configparser.Error as e:
              raise WrapException('Failed to parse {}: {}'.format(self.basename, str(e)))
          self.parse_wrap_section(config)
          if self.type == 'redirect':
              # [wrap-redirect] have a `filename` value pointing to the real wrap
              # file we should parse instead. It must be relative to the current
              # wrap file location and must be in the form foo/subprojects/bar.wrap.
              dirname = Path(self.filename).parent
              fname = Path(self.values['filename'])
              for i, p in enumerate(fname.parts):
                  if i % 2 == 0:
                      if p == '..':
                          raise WrapException('wrap-redirect filename cannot contain ".."')
                  else:
                      if p != 'subprojects':
                          raise WrapException('wrap-redirect filename must be in the form foo/subprojects/bar.wrap')
              if fname.suffix != '.wrap':
                  raise WrapException('wrap-redirect filename must be a .wrap file')
              fname = dirname / fname
              if not fname.is_file():
                  raise WrapException(f'wrap-redirect {fname} filename does not exist')
              self.filename = str(fname)
              self.parse_wrap()
              return
          self.parse_provide_section(config)
  
      def parse_wrap_section(self, config: configparser.ConfigParser) -> None:
          if len(config.sections()) < 1:
              raise WrapException(f'Missing sections in {self.basename}')
          self.wrap_section = config.sections()[0]
          if not self.wrap_section.startswith('wrap-'):
              m = '{!r} is not a valid first section in {}'
              raise WrapException(m.format(self.wrap_section, self.basename))
          self.type = self.wrap_section[5:]
          self.values = dict(config[self.wrap_section])
  
      def parse_provide_section(self, config: configparser.ConfigParser) -> None:
          if config.has_section('provide'):
              for k, v in config['provide'].items():
                  if k == 'dependency_names':
                      # A comma separated list of dependency names that does not
                      # need a variable name
                      names_dict = {n.strip(): None for n in v.split(',')}
                      self.provided_deps.update(names_dict)
                      continue
                  if k == 'program_names':
                      # A comma separated list of program names
                      names_list = [n.strip() for n in v.split(',')]
                      self.provided_programs += names_list
                      continue
                  if not v:
                      m = ('Empty dependency variable name for {!r} in {}. '
                           'If the subproject uses meson.override_dependency() '
                           'it can be added in the "dependency_names" special key.')
                      raise WrapException(m.format(k, self.basename))
                  self.provided_deps[k] = v
  
      def get(self, key: str) -> str:
          try:
              return self.values[key]
          except KeyError:
              m = 'Missing key {!r} in {}'
              raise WrapException(m.format(key, self.basename))
  
  def get_directory(subdir_root: str, packagename: str) -> str:
      fname = os.path.join(subdir_root, packagename + '.wrap')
      if os.path.isfile(fname):
          wrap = PackageDefinition(fname)
          return wrap.directory
      return packagename
  
  def verbose_git(cmd: T.List[str], workingdir: str, check: bool = False) -> bool:
      '''
      Wrapper to convert GitException to WrapException caught in interpreter.
      '''
      try:
          return mesonlib.verbose_git(cmd, workingdir, check=check)
      except mesonlib.GitException as e:
          raise WrapException(str(e))
  
  class Resolver:
      def __init__(self, source_dir: str, subdir: str, wrap_mode: WrapMode = WrapMode.default) -> None:
          self.source_dir = source_dir
          self.subdir = subdir
          self.wrap_mode = wrap_mode
          self.subdir_root = os.path.join(source_dir, subdir)
          self.cachedir = os.path.join(self.subdir_root, 'packagecache')
          self.wraps = {} # type: T.Dict[str, PackageDefinition]
          self.provided_deps = {} # type: T.Dict[str, PackageDefinition]
          self.provided_programs = {} # type: T.Dict[str, PackageDefinition]
          self.load_wraps()
  
      def load_wraps(self) -> None:
          if not os.path.isdir(self.subdir_root):
              return
          root, dirs, files = next(os.walk(self.subdir_root))
          for i in files:
              if not i.endswith('.wrap'):
                  continue
              fname = os.path.join(self.subdir_root, i)
              wrap = PackageDefinition(fname)
              self.wraps[wrap.name] = wrap
              if wrap.directory in dirs:
                  dirs.remove(wrap.directory)
          # Add dummy package definition for directories not associated with a wrap file.
          for i in dirs:
              if i in ['packagecache', 'packagefiles']:
                  continue
              fname = os.path.join(self.subdir_root, i)
              wrap = PackageDefinition(fname)
              self.wraps[wrap.name] = wrap
  
          for wrap in self.wraps.values():
              for k in wrap.provided_deps.keys():
                  if k in self.provided_deps:
                      prev_wrap = self.provided_deps[k]
                      m = 'Multiple wrap files provide {!r} dependency: {} and {}'
                      raise WrapException(m.format(k, wrap.basename, prev_wrap.basename))
                  self.provided_deps[k] = wrap
              for k in wrap.provided_programs:
                  if k in self.provided_programs:
                      prev_wrap = self.provided_programs[k]
                      m = 'Multiple wrap files provide {!r} program: {} and {}'
                      raise WrapException(m.format(k, wrap.basename, prev_wrap.basename))
                  self.provided_programs[k] = wrap
  
      def merge_wraps(self, other_resolver: 'Resolver') -> None:
          for k, v in other_resolver.wraps.items():
              self.wraps.setdefault(k, v)
          for k, v in other_resolver.provided_deps.items():
              self.provided_deps.setdefault(k, v)
          for k, v in other_resolver.provided_programs.items():
              self.provided_programs.setdefault(k, v)
  
      def find_dep_provider(self, packagename: str) -> T.Tuple[T.Optional[str], T.Optional[str]]:
          # Python's ini parser converts all key values to lowercase.
          # Thus the query name must also be in lower case.
          packagename = packagename.lower()
          wrap = self.provided_deps.get(packagename)
          if wrap:
              dep_var = wrap.provided_deps.get(packagename)
              return wrap.name, dep_var
          return None, None
  
      def get_varname(self, subp_name: str, depname: str) -> T.Optional[str]:
          wrap = self.wraps.get(subp_name)
          return wrap.provided_deps.get(depname) if wrap else None
  
      def find_program_provider(self, names: T.List[str]) -> T.Optional[str]:
          for name in names:
              wrap = self.provided_programs.get(name)
              if wrap:
                  return wrap.name
          return None
  
      def resolve(self, packagename: str, method: str, current_subproject: str = '') -> str:
          self.current_subproject = current_subproject
          self.packagename = packagename
          self.directory = packagename
          self.wrap = self.wraps.get(packagename)
          if not self.wrap:
              m = 'Neither a subproject directory nor a {}.wrap file was found.'
              raise WrapNotFoundException(m.format(self.packagename))
          self.directory = self.wrap.directory
  
          if self.wrap.has_wrap:
              # We have a .wrap file, source code will be placed into main
              # project's subproject_dir even if the wrap file comes from another
              # subproject.
              self.dirname = os.path.join(self.subdir_root, self.directory)
              # Check if the wrap comes from the main project.
              main_fname = os.path.join(self.subdir_root, self.wrap.basename)
              if self.wrap.filename != main_fname:
                  rel = os.path.relpath(self.wrap.filename, self.source_dir)
                  mlog.log('Using', mlog.bold(rel))
                  # Write a dummy wrap file in main project that redirect to the
                  # wrap we picked.
                  with open(main_fname, 'w', encoding='utf-8') as f:
                      f.write(textwrap.dedent('''\
                          [wrap-redirect]
                          filename = {}
                          '''.format(os.path.relpath(self.wrap.filename, self.subdir_root))))
          else:
              # No wrap file, it's a dummy package definition for an existing
              # directory. Use the source code in place.
              self.dirname = self.wrap.filename
          rel_path = os.path.relpath(self.dirname, self.source_dir)
  
          meson_file = os.path.join(self.dirname, 'meson.build')
          cmake_file = os.path.join(self.dirname, 'CMakeLists.txt')
  
          if method not in ['meson', 'cmake']:
              raise WrapException('Only the methods "meson" and "cmake" are supported')
  
          # The directory is there and has meson.build? Great, use it.
          if method == 'meson' and os.path.exists(meson_file):
              return rel_path
          if method == 'cmake' and os.path.exists(cmake_file):
              return rel_path
  
          # Check if the subproject is a git submodule
          self.resolve_git_submodule()
  
          if os.path.exists(self.dirname):
              if not os.path.isdir(self.dirname):
                  raise WrapException('Path already exists but is not a directory')
          else:
              if self.wrap.type == 'file':
                  self.get_file()
              else:
                  self.check_can_download()
                  if self.wrap.type == 'git':
                      self.get_git()
                  elif self.wrap.type == "hg":
                      self.get_hg()
                  elif self.wrap.type == "svn":
                      self.get_svn()
                  else:
                      raise WrapException(f'Unknown wrap type {self.wrap.type!r}')
              self.apply_patch()
  
          # A meson.build or CMakeLists.txt file is required in the directory
          if method == 'meson' and not os.path.exists(meson_file):
              raise WrapException('Subproject exists but has no meson.build file')
          if method == 'cmake' and not os.path.exists(cmake_file):
              raise WrapException('Subproject exists but has no CMakeLists.txt file')
  
          return rel_path
  
      def check_can_download(self) -> None:
          # Don't download subproject data based on wrap file if requested.
          # Git submodules are ok (see above)!
          if self.wrap_mode is WrapMode.nodownload:
              m = 'Automatic wrap-based subproject downloading is disabled'
              raise WrapException(m)
  
      def resolve_git_submodule(self) -> bool:
          # Is git installed? If not, we're probably not in a git repository and
          # definitely cannot try to conveniently set up a submodule.
          if not GIT:
              return False
          # Are we in a git repository?
          ret, out = quiet_git(['rev-parse'], self.subdir_root)
          if not ret:
              return False
          # Is `dirname` a submodule?
          ret, out = quiet_git(['submodule', 'status', self.dirname], self.subdir_root)
          if not ret:
              return False
          # Submodule has not been added, add it
          if out.startswith('+'):
              mlog.warning('git submodule might be out of date')
              return True
          elif out.startswith('U'):
              raise WrapException('git submodule has merge conflicts')
          # Submodule exists, but is deinitialized or wasn't initialized
          elif out.startswith('-'):
              if verbose_git(['submodule', 'update', '--init', self.dirname], self.subdir_root):
                  return True
              raise WrapException('git submodule failed to init')
          # Submodule looks fine, but maybe it wasn't populated properly. Do a checkout.
          elif out.startswith(' '):
              verbose_git(['checkout', '.'], self.dirname)
              # Even if checkout failed, try building it anyway and let the user
              # handle any problems manually.
              return True
          elif out == '':
              # It is not a submodule, just a folder that exists in the main repository.
              return False
          m = 'Unknown git submodule output: {!r}'
          raise WrapException(m.format(out))
  
      def get_file(self) -> None:
          path = self.get_file_internal('source')
          extract_dir = self.subdir_root
          # Some upstreams ship packages that do not have a leading directory.
          # Create one for them.
          if 'lead_directory_missing' in self.wrap.values:
              os.mkdir(self.dirname)
              extract_dir = self.dirname
          shutil.unpack_archive(path, extract_dir)
  
      def get_git(self) -> None:
          if not GIT:
              raise WrapException('Git program not found.')
          revno = self.wrap.get('revision')
          is_shallow = False
          depth_option = []    # type: T.List[str]
          if self.wrap.values.get('depth', '') != '':
              is_shallow = True
              depth_option = ['--depth', self.wrap.values.get('depth')]
          # for some reason git only allows commit ids to be shallowly fetched by fetch not with clone
          if is_shallow and self.is_git_full_commit_id(revno):
              # git doesn't support directly cloning shallowly for commits,
              # so we follow https://stackoverflow.com/a/43136160
              verbose_git(['init', self.directory], self.subdir_root, check=True)
              verbose_git(['remote', 'add', 'origin', self.wrap.get('url')], self.dirname, check=True)
              revno = self.wrap.get('revision')
              verbose_git(['fetch', *depth_option, 'origin', revno], self.dirname, check=True)
              verbose_git(['checkout', revno, '--'], self.dirname, check=True)
              if self.wrap.values.get('clone-recursive', '').lower() == 'true':
                  verbose_git(['submodule', 'update', '--init', '--checkout',
                               '--recursive', *depth_option], self.dirname, check=True)
              push_url = self.wrap.values.get('push-url')
              if push_url:
                  verbose_git(['remote', 'set-url', '--push', 'origin', push_url], self.dirname, check=True)
          else:
              if not is_shallow:
                  verbose_git(['clone', self.wrap.get('url'), self.directory], self.subdir_root, check=True)
                  if revno.lower() != 'head':
                      if not verbose_git(['checkout', revno, '--'], self.dirname):
                          verbose_git(['fetch', self.wrap.get('url'), revno], self.dirname, check=True)
                          verbose_git(['checkout', revno, '--'], self.dirname, check=True)
              else:
                  verbose_git(['clone', *depth_option, '--branch', revno, self.wrap.get('url'),
                               self.directory], self.subdir_root, check=True)
              if self.wrap.values.get('clone-recursive', '').lower() == 'true':
                  verbose_git(['submodule', 'update', '--init', '--checkout', '--recursive', *depth_option],
                              self.dirname, check=True)
              push_url = self.wrap.values.get('push-url')
              if push_url:
                  verbose_git(['remote', 'set-url', '--push', 'origin', push_url], self.dirname, check=True)
  
      def is_git_full_commit_id(self, revno: str) -> bool:
          result = False
          if len(revno) in (40, 64): # 40 for sha1, 64 for upcoming sha256
              result = all(ch in '0123456789AaBbCcDdEeFf' for ch in revno)
          return result
  
      def get_hg(self) -> None:
          revno = self.wrap.get('revision')
          hg = shutil.which('hg')
          if not hg:
              raise WrapException('Mercurial program not found.')
          subprocess.check_call([hg, 'clone', self.wrap.get('url'),
                                 self.directory], cwd=self.subdir_root)
          if revno.lower() != 'tip':
              subprocess.check_call([hg, 'checkout', revno],
                                    cwd=self.dirname)
  
      def get_svn(self) -> None:
          revno = self.wrap.get('revision')
          svn = shutil.which('svn')
          if not svn:
              raise WrapException('SVN program not found.')
          subprocess.check_call([svn, 'checkout', '-r', revno, self.wrap.get('url'),
                                 self.directory], cwd=self.subdir_root)
  
      def get_data(self, urlstring: str) -> T.Tuple[str, str]:
          blocksize = 10 * 1024
          h = hashlib.sha256()
          tmpfile = tempfile.NamedTemporaryFile(mode='wb', dir=self.cachedir, delete=False)
          url = urllib.parse.urlparse(urlstring)
          if url.hostname and url.hostname.endswith(WHITELIST_SUBDOMAIN):
              resp = open_wrapdburl(urlstring)
          elif WHITELIST_SUBDOMAIN in urlstring:
              raise WrapException(f'{urlstring} may be a WrapDB-impersonating URL')
          else:
              try:
                  req = urllib.request.Request(urlstring, headers={'User-Agent': f'mesonbuild/{coredata.version}'})
                  resp = urllib.request.urlopen(req, timeout=REQ_TIMEOUT)
              except urllib.error.URLError as e:
                  mlog.log(str(e))
                  raise WrapException(f'could not get {urlstring} is the internet available?')
          with contextlib.closing(resp) as resp:
              try:
                  dlsize = int(resp.info()['Content-Length'])
              except TypeError:
                  dlsize = None
              if dlsize is None:
                  print('Downloading file of unknown size.')
                  while True:
                      block = resp.read(blocksize)
                      if block == b'':
                          break
                      h.update(block)
                      tmpfile.write(block)
                  hashvalue = h.hexdigest()
                  return hashvalue, tmpfile.name
              sys.stdout.flush()
              progress_bar = ProgressBar(bar_type='download', total=dlsize,
                                         desc='Downloading')
              while True:
                  block = resp.read(blocksize)
                  if block == b'':
                      break
                  h.update(block)
                  tmpfile.write(block)
                  progress_bar.update(len(block))
              progress_bar.close()
              hashvalue = h.hexdigest()
          return hashvalue, tmpfile.name
  
      def check_hash(self, what: str, path: str, hash_required: bool = True) -> None:
          if what + '_hash' not in self.wrap.values and not hash_required:
              return
          expected = self.wrap.get(what + '_hash').lower()
          h = hashlib.sha256()
          with open(path, 'rb') as f:
              h.update(f.read())
          dhash = h.hexdigest()
          if dhash != expected:
              raise WrapException(f'Incorrect hash for {what}:\n {expected} expected\n {dhash} actual.')
  
      def download(self, what: str, ofname: str, fallback: bool = False) -> None:
          self.check_can_download()
          srcurl = self.wrap.get(what + ('_fallback_url' if fallback else '_url'))
          mlog.log('Downloading', mlog.bold(self.packagename), what, 'from', mlog.bold(srcurl))
          try:
              dhash, tmpfile = self.get_data(srcurl)
              expected = self.wrap.get(what + '_hash').lower()
              if dhash != expected:
                  os.remove(tmpfile)
                  raise WrapException(f'Incorrect hash for {what}:\n {expected} expected\n {dhash} actual.')
          except WrapException:
              if not fallback:
                  if what + '_fallback_url' in self.wrap.values:
                      return self.download(what, ofname, fallback=True)
                  mlog.log('A fallback URL could be specified using',
                           mlog.bold(what + '_fallback_url'), 'key in the wrap file')
              raise
          os.rename(tmpfile, ofname)
  
      def get_file_internal(self, what: str) -> str:
          filename = self.wrap.get(what + '_filename')
          if what + '_url' in self.wrap.values:
              cache_path = os.path.join(self.cachedir, filename)
  
              if os.path.exists(cache_path):
                  self.check_hash(what, cache_path)
                  mlog.log('Using', mlog.bold(self.packagename), what, 'from cache.')
                  return cache_path
  
              if not os.path.isdir(self.cachedir):
                  os.mkdir(self.cachedir)
              self.download(what, cache_path)
              return cache_path
          else:
              from ..interpreterbase import FeatureNew
              FeatureNew(f'Local wrap patch files without {what}_url', '0.55.0').use(self.current_subproject)
              path = Path(self.wrap.filesdir) / filename
  
              if not path.exists():
                  raise WrapException(f'File "{path}" does not exist')
              self.check_hash(what, path.as_posix(), hash_required=False)
  
              return path.as_posix()
  
      def apply_patch(self) -> None:
          if 'patch_filename' in self.wrap.values and 'patch_directory' in self.wrap.values:
              m = 'Wrap file {!r} must not have both "patch_filename" and "patch_directory"'
              raise WrapException(m.format(self.wrap.basename))
          if 'patch_filename' in self.wrap.values:
              path = self.get_file_internal('patch')
              try:
                  shutil.unpack_archive(path, self.subdir_root)
              except Exception:
                  with tempfile.TemporaryDirectory() as workdir:
                      shutil.unpack_archive(path, workdir)
                      self.copy_tree(workdir, self.subdir_root)
          elif 'patch_directory' in self.wrap.values:
              from ..interpreterbase import FeatureNew
              FeatureNew('patch_directory', '0.55.0').use(self.current_subproject)
              patch_dir = self.wrap.values['patch_directory']
              src_dir = os.path.join(self.wrap.filesdir, patch_dir)
              if not os.path.isdir(src_dir):
                  raise WrapException(f'patch directory does not exists: {patch_dir}')
              self.copy_tree(src_dir, self.dirname)
  
      def copy_tree(self, root_src_dir: str, root_dst_dir: str) -> None:
          """
          Copy directory tree. Overwrites also read only files.
          """
          for src_dir, _, files in os.walk(root_src_dir):
              dst_dir = src_dir.replace(root_src_dir, root_dst_dir, 1)
              if not os.path.exists(dst_dir):
                  os.makedirs(dst_dir)
              for file_ in files:
                  src_file = os.path.join(src_dir, file_)
                  dst_file = os.path.join(dst_dir, file_)
                  if os.path.exists(dst_file):
                      try:
                          os.remove(dst_file)
                      except PermissionError:
                          os.chmod(dst_file, stat.S_IWUSR)
                          os.remove(dst_file)
                  shutil.copy2(src_file, dst_dir)
  
  
  [... the rest is omitted ...]
files.old_path: mesonbuild/wrap/wrap.py
files.new_contents: #!/usr/bin/env python3
  # Copyright 2016-2021 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.
  
  # Work around some pathlib bugs...
  from mesonbuild import _pathlib
  import sys
  sys.modules['pathlib'] = _pathlib
  
  import time
  import stat
  import subprocess
  import re
  import json
  import tempfile
  import textwrap
  import os
  import shutil
  import unittest
  import platform
  import pickle
  import functools
  import io
  import operator
  import threading
  import zipfile, tarfile
  import hashlib
  from itertools import chain
  from unittest import mock
  from configparser import ConfigParser
  from contextlib import contextmanager
  from glob import glob
  from pathlib import (PurePath, Path)
  from distutils.dir_util import copy_tree
  import typing as T
  
  import mesonbuild.mlog
  import mesonbuild.depfile
  import mesonbuild.dependencies.base
  import mesonbuild.dependencies.factory
  import mesonbuild.compilers
  import mesonbuild.envconfig
  import mesonbuild.environment
  import mesonbuild.mesonlib
  import mesonbuild.coredata
  import mesonbuild.modules.gnome
  from mesonbuild.interpreter import Interpreter
  from mesonbuild.interpreterbase import typed_pos_args, InvalidArguments, ObjectHolder
  from mesonbuild.interpreterbase import typed_pos_args, InvalidArguments, typed_kwargs, ContainerTypeInfo, KwargInfo
  from mesonbuild.ast import AstInterpreter
  from mesonbuild.mesonlib import (
      BuildDirLock, LibType, MachineChoice, PerMachine, Version, is_windows,
      is_osx, is_cygwin, is_dragonflybsd, is_openbsd, is_haiku, is_sunos,
      windows_proof_rmtree, windows_proof_rm, python_command,
      version_compare, split_args, quote_arg, relpath, is_linux, git,
      search_version
  )
  from mesonbuild.compilers import (
      detect_static_linker, detect_c_compiler, detect_cpp_compiler,
      detect_objc_compiler, detect_objcpp_compiler, detect_d_compiler,
      detect_swift_compiler, compiler_from_language
  )
  from mesonbuild.environment import detect_ninja
  from mesonbuild.mesonlib import MesonException, EnvironmentException, OptionKey
  from mesonbuild.dependencies import PkgConfigDependency
  from mesonbuild.programs import ExternalProgram
  import mesonbuild.dependencies.base
  from mesonbuild.build import Target, ConfigurationData
  import mesonbuild.modules.pkgconfig
  from mesonbuild.scripts import destdir_join
  
  from mesonbuild.mtest import TAPParser, TestResult
  from mesonbuild.mesonmain import setup_vsenv
  from mesonbuild.wrap.wrap import PackageDefinition, WrapException
  
  from run_tests import (
      Backend, FakeBuild, FakeCompilerOptions,
      ensure_backend_detects_changes, exe_suffix, get_backend_commands,
      get_builddir_target_args, get_fake_env, get_fake_options, get_meson_script,
      run_configure_inprocess, run_mtest_inprocess
  )
  
  if T.TYPE_CHECKING:
      from mesonbuild.compilers import Compiler
  
  URLOPEN_TIMEOUT = 5
  
  @contextmanager
  def chdir(path: str):
      curdir = os.getcwd()
      os.chdir(path)
      try:
          yield
      finally:
          os.chdir(curdir)
  
  
  def get_dynamic_section_entry(fname: str, entry: str) -> T.Optional[str]:
      if is_cygwin() or is_osx():
          raise unittest.SkipTest('Test only applicable to ELF platforms')
  
      try:
          raw_out = subprocess.check_output(['readelf', '-d', fname],
                                            universal_newlines=True)
      except FileNotFoundError:
          # FIXME: Try using depfixer.py:Elf() as a fallback
          raise unittest.SkipTest('readelf not found')
      pattern = re.compile(entry + r': \[(.*?)\]')
      for line in raw_out.split('\n'):
          m = pattern.search(line)
          if m is not None:
              return str(m.group(1))
      return None # The file did not contain the specified entry.
  
  def get_soname(fname: str) -> T.Optional[str]:
      return get_dynamic_section_entry(fname, 'soname')
  
  def get_rpath(fname: str) -> T.Optional[str]:
      raw = get_dynamic_section_entry(fname, r'(?:rpath|runpath)')
      # Get both '' and None here
      if not raw:
          return None
      # nix/nixos adds a bunch of stuff to the rpath out of necessity that we
      # don't check for, so clear those
      final = ':'.join([e for e in raw.split(':') if not e.startswith('/nix')])
      return final
  
  def is_tarball():
      if not os.path.isdir('docs'):
          return True
      return False
  
  def is_ci():
      if 'CI' in os.environ:
          return True
      return False
  
  def _git_init(project_dir):
      # If a user has git configuration init.defaultBranch set we want to override that
      with tempfile.TemporaryDirectory() as d:
          out = git(['--version'], str(d))[1]
      if version_compare(search_version(out), '>= 2.28'):
          extra_cmd = ['--initial-branch', 'master']
      else:
          extra_cmd = []
  
      subprocess.check_call(['git', 'init'] + extra_cmd, cwd=project_dir, stdout=subprocess.DEVNULL)
      subprocess.check_call(['git', 'config',
                             'user.name', 'Author Person'], cwd=project_dir)
      subprocess.check_call(['git', 'config',
                             'user.email', 'teh_coderz@example.com'], cwd=project_dir)
      _git_add_all(project_dir)
  
  def _git_add_all(project_dir):
      subprocess.check_call('git add *', cwd=project_dir, shell=True,
                            stdout=subprocess.DEVNULL)
      subprocess.check_call(['git', 'commit', '-a', '-m', 'I am a project'], cwd=project_dir,
                            stdout=subprocess.DEVNULL)
  
  @functools.lru_cache()
  def is_real_gnu_compiler(path):
      '''
      Check if the gcc we have is a real gcc and not a macOS wrapper around clang
      '''
      if not path:
          return False
      out = subprocess.check_output([path, '--version'], universal_newlines=True, stderr=subprocess.STDOUT)
      return 'Free Software Foundation' in out
  
  def skipIfNoExecutable(exename):
      '''
      Skip this test if the given executable is not found.
      '''
      def wrapper(func):
          @functools.wraps(func)
          def wrapped(*args, **kwargs):
              if shutil.which(exename) is None:
                  raise unittest.SkipTest(exename + ' not found')
              return func(*args, **kwargs)
          return wrapped
      return wrapper
  
  def skipIfNoPkgconfig(f):
      '''
      Skip this test if no pkg-config is found, unless we're on CI.
      This allows users to run our test suite without having
      pkg-config installed on, f.ex., macOS, while ensuring that our CI does not
      silently skip the test because of misconfiguration.
  
      Note: Yes, we provide pkg-config even while running Windows CI
      '''
      @functools.wraps(f)
      def wrapped(*args, **kwargs):
          if not is_ci() and shutil.which('pkg-config') is None:
              raise unittest.SkipTest('pkg-config not found')
          return f(*args, **kwargs)
      return wrapped
  
  def skipIfNoPkgconfigDep(depname):
      '''
      Skip this test if the given pkg-config dep is not found, unless we're on CI.
      '''
      def wrapper(func):
          @functools.wraps(func)
          def wrapped(*args, **kwargs):
              if not is_ci() and shutil.which('pkg-config') is None:
                  raise unittest.SkipTest('pkg-config not found')
              if not is_ci() and subprocess.call(['pkg-config', '--exists', depname]) != 0:
                  raise unittest.SkipTest(f'pkg-config dependency {depname} not found.')
              return func(*args, **kwargs)
          return wrapped
      return wrapper
  
  def skip_if_no_cmake(f):
      '''
      Skip this test if no cmake is found, unless we're on CI.
      This allows users to run our test suite without having
      cmake installed on, f.ex., macOS, while ensuring that our CI does not
      silently skip the test because of misconfiguration.
      '''
      @functools.wraps(f)
      def wrapped(*args, **kwargs):
          if not is_ci() and shutil.which('cmake') is None:
              raise unittest.SkipTest('cmake not found')
          return f(*args, **kwargs)
      return wrapped
  
  def skip_if_not_language(lang: str):
      def wrapper(func):
          @functools.wraps(func)
          def wrapped(*args, **kwargs):
              try:
                  compiler_from_language(get_fake_env(), lang, MachineChoice.HOST)
              except EnvironmentException:
                  raise unittest.SkipTest(f'No {lang} compiler found.')
              return func(*args, **kwargs)
          return wrapped
      return wrapper
  
  def skip_if_env_set(key):
      '''
      Skip a test if a particular env is set, except when running under CI
      '''
      def wrapper(func):
          @functools.wraps(func)
          def wrapped(*args, **kwargs):
              old = None
              if key in os.environ:
                  if not is_ci():
                      raise unittest.SkipTest(f'Env var {key!r} set, skipping')
                  old = os.environ.pop(key)
              try:
                  return func(*args, **kwargs)
              finally:
                  if old is not None:
                      os.environ[key] = old
          return wrapped
      return wrapper
  
  def skip_if_not_base_option(feature):
      """Skip tests if The compiler does not support a given base option.
  
      for example, ICC doesn't currently support b_sanitize.
      """
      def actual(f):
          @functools.wraps(f)
          def wrapped(*args, **kwargs):
              env = get_fake_env()
              cc = detect_c_compiler(env, MachineChoice.HOST)
              key = OptionKey(feature)
              if key not in cc.base_options:
                  raise unittest.SkipTest(
                      f'{feature} not available with {cc.id}')
              return f(*args, **kwargs)
          return wrapped
      return actual
  
  
  @contextmanager
  def temp_filename():
      '''A context manager which provides a filename to an empty temporary file.
  
      On exit the file will be deleted.
      '''
  
      fd, filename = tempfile.mkstemp()
      os.close(fd)
      try:
          yield filename
      finally:
          try:
              os.remove(filename)
          except OSError:
              pass
  
  @contextmanager
  def no_pkgconfig():
      '''
      A context manager that overrides shutil.which and ExternalProgram to force
      them to return None for pkg-config to simulate it not existing.
      '''
      old_which = shutil.which
      old_search = ExternalProgram._search
  
      def new_search(self, name, search_dir):
          if name == 'pkg-config':
              return [None]
          return old_search(self, name, search_dir)
  
      def new_which(cmd, *kwargs):
          if cmd == 'pkg-config':
              return None
          return old_which(cmd, *kwargs)
  
      shutil.which = new_which
      ExternalProgram._search = new_search
      try:
          yield
      finally:
          shutil.which = old_which
          ExternalProgram._search = old_search
  
  
  class InternalTests(unittest.TestCase):
  
      def test_version_number(self):
          self.assertEqual(search_version('foobar 1.2.3'), '1.2.3')
          self.assertEqual(search_version('1.2.3'), '1.2.3')
          self.assertEqual(search_version('foobar 2016.10.28 1.2.3'), '1.2.3')
          self.assertEqual(search_version('2016.10.28 1.2.3'), '1.2.3')
          self.assertEqual(search_version('foobar 2016.10.128'), '2016.10.128')
          self.assertEqual(search_version('2016.10.128'), '2016.10.128')
          self.assertEqual(search_version('2016.10'), '2016.10')
          self.assertEqual(search_version('2016.10 1.2.3'), '1.2.3')
          self.assertEqual(search_version('oops v1.2.3'), '1.2.3')
          self.assertEqual(search_version('2016.oops 1.2.3'), '1.2.3')
          self.assertEqual(search_version('2016.x'), 'unknown version')
  
      def test_mode_symbolic_to_bits(self):
          modefunc = mesonbuild.mesonlib.FileMode.perms_s_to_bits
          self.assertEqual(modefunc('---------'), 0)
          self.assertEqual(modefunc('r--------'), stat.S_IRUSR)
          self.assertEqual(modefunc('---r-----'), stat.S_IRGRP)
          self.assertEqual(modefunc('------r--'), stat.S_IROTH)
          self.assertEqual(modefunc('-w-------'), stat.S_IWUSR)
          self.assertEqual(modefunc('----w----'), stat.S_IWGRP)
          self.assertEqual(modefunc('-------w-'), stat.S_IWOTH)
          self.assertEqual(modefunc('--x------'), stat.S_IXUSR)
          self.assertEqual(modefunc('-----x---'), stat.S_IXGRP)
          self.assertEqual(modefunc('--------x'), stat.S_IXOTH)
          self.assertEqual(modefunc('--S------'), stat.S_ISUID)
          self.assertEqual(modefunc('-----S---'), stat.S_ISGID)
          self.assertEqual(modefunc('--------T'), stat.S_ISVTX)
          self.assertEqual(modefunc('--s------'), stat.S_ISUID | stat.S_IXUSR)
          self.assertEqual(modefunc('-----s---'), stat.S_ISGID | stat.S_IXGRP)
          self.assertEqual(modefunc('--------t'), stat.S_ISVTX | stat.S_IXOTH)
          self.assertEqual(modefunc('rwx------'), stat.S_IRWXU)
          self.assertEqual(modefunc('---rwx---'), stat.S_IRWXG)
          self.assertEqual(modefunc('------rwx'), stat.S_IRWXO)
          # We could keep listing combinations exhaustively but that seems
          # tedious and pointless. Just test a few more.
          self.assertEqual(modefunc('rwxr-xr-x'),
                           stat.S_IRWXU |
                           stat.S_IRGRP | stat.S_IXGRP |
                           stat.S_IROTH | stat.S_IXOTH)
          self.assertEqual(modefunc('rw-r--r--'),
                           stat.S_IRUSR | stat.S_IWUSR |
                           stat.S_IRGRP |
                           stat.S_IROTH)
          self.assertEqual(modefunc('rwsr-x---'),
                           stat.S_IRWXU | stat.S_ISUID |
                           stat.S_IRGRP | stat.S_IXGRP)
  
      def test_compiler_args_class_none_flush(self):
          cc = mesonbuild.compilers.ClangCCompiler([], 'fake', MachineChoice.HOST, False, mock.Mock())
          a = cc.compiler_args(['-I.'])
          #first we are checking if the tree construction deduplicates the correct -I argument
          a += ['-I..']
          a += ['-I./tests/']
          a += ['-I./tests2/']
          #think this here as assertion, we cannot apply it, otherwise the CompilerArgs would already flush the changes:
          # assertEqual(a, ['-I.', '-I./tests2/', '-I./tests/', '-I..', '-I.'])
          a += ['-I.']
          a += ['-I.', '-I./tests/']
          self.assertEqual(a, ['-I.', '-I./tests/', '-I./tests2/', '-I..'])
  
          #then we are checking that when CompilerArgs already have a build container list, that the deduplication is taking the correct one
          a += ['-I.', '-I./tests2/']
          self.assertEqual(a, ['-I.', '-I./tests2/', '-I./tests/', '-I..'])
  
      def test_compiler_args_class_d(self):
          d = mesonbuild.compilers.DmdDCompiler([], 'fake', MachineChoice.HOST, 'info', 'arch')
          # check include order is kept when deduplicating
          a = d.compiler_args(['-Ifirst', '-Isecond', '-Ithird'])
          a += ['-Ifirst']
          self.assertEqual(a, ['-Ifirst', '-Isecond', '-Ithird'])
  
      def test_compiler_args_class_clike(self):
          cc = mesonbuild.compilers.ClangCCompiler([], 'fake', MachineChoice.HOST, False, mock.Mock())
          # Test that empty initialization works
          a = cc.compiler_args()
          self.assertEqual(a, [])
          # Test that list initialization works
          a = cc.compiler_args(['-I.', '-I..'])
          self.assertEqual(a, ['-I.', '-I..'])
          # Test that there is no de-dup on initialization
          self.assertEqual(cc.compiler_args(['-I.', '-I.']), ['-I.', '-I.'])
  
          ## Test that appending works
          a.append('-I..')
          self.assertEqual(a, ['-I..', '-I.'])
          a.append('-O3')
          self.assertEqual(a, ['-I..', '-I.', '-O3'])
  
          ## Test that in-place addition works
          a += ['-O2', '-O2']
          self.assertEqual(a, ['-I..', '-I.', '-O3', '-O2', '-O2'])
          # Test that removal works
          a.remove('-O2')
          self.assertEqual(a, ['-I..', '-I.', '-O3', '-O2'])
          # Test that de-dup happens on addition
          a += ['-Ifoo', '-Ifoo']
          self.assertEqual(a, ['-Ifoo', '-I..', '-I.', '-O3', '-O2'])
  
          # .extend() is just +=, so we don't test it
  
          ## Test that addition works
          # Test that adding a list with just one old arg works and yields the same array
          a = a + ['-Ifoo']
          self.assertEqual(a, ['-Ifoo', '-I..', '-I.', '-O3', '-O2'])
          # Test that adding a list with one arg new and one old works
          a = a + ['-Ifoo', '-Ibaz']
          self.assertEqual(a, ['-Ifoo', '-Ibaz', '-I..', '-I.', '-O3', '-O2'])
          # Test that adding args that must be prepended and appended works
          a = a + ['-Ibar', '-Wall']
          self.assertEqual(a, ['-Ibar', '-Ifoo', '-Ibaz', '-I..', '-I.', '-O3', '-O2', '-Wall'])
  
          ## Test that reflected addition works
          # Test that adding to a list with just one old arg works and yields the same array
          a = ['-Ifoo'] + a
          self.assertEqual(a, ['-Ibar', '-Ifoo', '-Ibaz', '-I..', '-I.', '-O3', '-O2', '-Wall'])
          # Test that adding to a list with just one new arg that is not pre-pended works
          a = ['-Werror'] + a
          self.assertEqual(a, ['-Ibar', '-Ifoo', '-Ibaz', '-I..', '-I.', '-Werror', '-O3', '-O2', '-Wall'])
          # Test that adding to a list with two new args preserves the order
          a = ['-Ldir', '-Lbah'] + a
          self.assertEqual(a, ['-Ibar', '-Ifoo', '-Ibaz', '-I..', '-I.', '-Ldir', '-Lbah', '-Werror', '-O3', '-O2', '-Wall'])
          # Test that adding to a list with old args does nothing
          a = ['-Ibar', '-Ibaz', '-Ifoo'] + a
          self.assertEqual(a, ['-Ibar', '-Ifoo', '-Ibaz', '-I..', '-I.', '-Ldir', '-Lbah', '-Werror', '-O3', '-O2', '-Wall'])
  
          ## Test that adding libraries works
          l = cc.compiler_args(['-Lfoodir', '-lfoo'])
          self.assertEqual(l, ['-Lfoodir', '-lfoo'])
          # Adding a library and a libpath appends both correctly
          l += ['-Lbardir', '-lbar']
          self.assertEqual(l, ['-Lbardir', '-Lfoodir', '-lfoo', '-lbar'])
          # Adding the same library again does nothing
          l += ['-lbar']
          self.assertEqual(l, ['-Lbardir', '-Lfoodir', '-lfoo', '-lbar'])
  
          ## Test that 'direct' append and extend works
          l = cc.compiler_args(['-Lfoodir', '-lfoo'])
          self.assertEqual(l, ['-Lfoodir', '-lfoo'])
          # Direct-adding a library and a libpath appends both correctly
          l.extend_direct(['-Lbardir', '-lbar'])
          self.assertEqual(l, ['-Lfoodir', '-lfoo', '-Lbardir', '-lbar'])
          # Direct-adding the same library again still adds it
          l.append_direct('-lbar')
          self.assertEqual(l, ['-Lfoodir', '-lfoo', '-Lbardir', '-lbar', '-lbar'])
          # Direct-adding with absolute path deduplicates
          l.append_direct('/libbaz.a')
          self.assertEqual(l, ['-Lfoodir', '-lfoo', '-Lbardir', '-lbar', '-lbar', '/libbaz.a'])
          # Adding libbaz again does nothing
          l.append_direct('/libbaz.a')
          self.assertEqual(l, ['-Lfoodir', '-lfoo', '-Lbardir', '-lbar', '-lbar', '/libbaz.a'])
  
      def test_compiler_args_class_gnuld(self):
          ## Test --start/end-group
          linker = mesonbuild.linkers.GnuBFDDynamicLinker([], MachineChoice.HOST, '-Wl,', [])
          gcc = mesonbuild.compilers.GnuCCompiler([], 'fake', False, MachineChoice.HOST, mock.Mock(), linker=linker)
          ## Ensure that the fake compiler is never called by overriding the relevant function
          gcc.get_default_include_dirs = lambda: ['/usr/include', '/usr/share/include', '/usr/local/include']
          ## Test that 'direct' append and extend works
          l = gcc.compiler_args(['-Lfoodir', '-lfoo'])
          self.assertEqual(l.to_native(copy=True), ['-Lfoodir', '-Wl,--start-group', '-lfoo', '-Wl,--end-group'])
          # Direct-adding a library and a libpath appends both correctly
          l.extend_direct(['-Lbardir', '-lbar'])
          self.assertEqual(l.to_native(copy=True), ['-Lfoodir', '-Wl,--start-group', '-lfoo', '-Lbardir', '-lbar', '-Wl,--end-group'])
          # Direct-adding the same library again still adds it
          l.append_direct('-lbar')
          self.assertEqual(l.to_native(copy=True), ['-Lfoodir', '-Wl,--start-group', '-lfoo', '-Lbardir', '-lbar', '-lbar', '-Wl,--end-group'])
          # Direct-adding with absolute path deduplicates
          l.append_direct('/libbaz.a')
          self.assertEqual(l.to_native(copy=True), ['-Lfoodir', '-Wl,--start-group', '-lfoo', '-Lbardir', '-lbar', '-lbar', '/libbaz.a', '-Wl,--end-group'])
          # Adding libbaz again does nothing
          l.append_direct('/libbaz.a')
          self.assertEqual(l.to_native(copy=True), ['-Lfoodir', '-Wl,--start-group', '-lfoo', '-Lbardir', '-lbar', '-lbar', '/libbaz.a', '-Wl,--end-group'])
          # Adding a non-library argument doesn't include it in the group
          l += ['-Lfoo', '-Wl,--export-dynamic']
          self.assertEqual(l.to_native(copy=True), ['-Lfoo', '-Lfoodir', '-Wl,--start-group', '-lfoo', '-Lbardir', '-lbar', '-lbar', '/libbaz.a', '-Wl,--end-group', '-Wl,--export-dynamic'])
          # -Wl,-lfoo is detected as a library and gets added to the group
          l.append('-Wl,-ldl')
          self.assertEqual(l.to_native(copy=True), ['-Lfoo', '-Lfoodir', '-Wl,--start-group', '-lfoo', '-Lbardir', '-lbar', '-lbar', '/libbaz.a', '-Wl,--export-dynamic', '-Wl,-ldl', '-Wl,--end-group'])
  
      def test_compiler_args_remove_system(self):
          ## Test --start/end-group
          linker = mesonbuild.linkers.GnuBFDDynamicLinker([], MachineChoice.HOST, '-Wl,', [])
          gcc = mesonbuild.compilers.GnuCCompiler([], 'fake', False, MachineChoice.HOST, mock.Mock(), linker=linker)
          ## Ensure that the fake compiler is never called by overriding the relevant function
          gcc.get_default_include_dirs = lambda: ['/usr/include', '/usr/share/include', '/usr/local/include']
          ## Test that 'direct' append and extend works
          l = gcc.compiler_args(['-Lfoodir', '-lfoo'])
          self.assertEqual(l.to_native(copy=True), ['-Lfoodir', '-Wl,--start-group', '-lfoo', '-Wl,--end-group'])
          ## Test that to_native removes all system includes
          l += ['-isystem/usr/include', '-isystem=/usr/share/include', '-DSOMETHING_IMPORTANT=1', '-isystem', '/usr/local/include']
          self.assertEqual(l.to_native(copy=True), ['-Lfoodir', '-Wl,--start-group', '-lfoo', '-Wl,--end-group', '-DSOMETHING_IMPORTANT=1'])
  
      def test_string_templates_substitution(self):
          dictfunc = mesonbuild.mesonlib.get_filenames_templates_dict
          substfunc = mesonbuild.mesonlib.substitute_values
          ME = mesonbuild.mesonlib.MesonException
  
          # Identity
          self.assertEqual(dictfunc([], []), {})
  
          # One input, no outputs
          inputs = ['bar/foo.c.in']
          outputs = []
          ret = dictfunc(inputs, outputs)
          d = {'@INPUT@': inputs, '@INPUT0@': inputs[0],
               '@PLAINNAME@': 'foo.c.in', '@BASENAME@': 'foo.c'}
          # Check dictionary
          self.assertEqual(ret, d)
          # Check substitutions
          cmd = ['some', 'ordinary', 'strings']
          self.assertEqual(substfunc(cmd, d), cmd)
          cmd = ['@INPUT@.out', 'ordinary', 'strings']
          self.assertEqual(substfunc(cmd, d), [inputs[0] + '.out'] + cmd[1:])
          cmd = ['@INPUT0@.out', '@PLAINNAME@.ok', 'strings']
          self.assertEqual(substfunc(cmd, d),
                           [inputs[0] + '.out'] + [d['@PLAINNAME@'] + '.ok'] + cmd[2:])
          cmd = ['@INPUT@', '@BASENAME@.hah', 'strings']
          self.assertEqual(substfunc(cmd, d),
                           inputs + [d['@BASENAME@'] + '.hah'] + cmd[2:])
          cmd = ['@OUTPUT@']
          self.assertRaises(ME, substfunc, cmd, d)
  
          # One input, one output
          inputs = ['bar/foo.c.in']
          outputs = ['out.c']
          ret = dictfunc(inputs, outputs)
          d = {'@INPUT@': inputs, '@INPUT0@': inputs[0],
               '@PLAINNAME@': 'foo.c.in', '@BASENAME@': 'foo.c',
               '@OUTPUT@': outputs, '@OUTPUT0@': outputs[0], '@OUTDIR@': '.'}
          # Check dictionary
          self.assertEqual(ret, d)
          # Check substitutions
          cmd = ['some', 'ordinary', 'strings']
          self.assertEqual(substfunc(cmd, d), cmd)
          cmd = ['@INPUT@.out', '@OUTPUT@', 'strings']
          self.assertEqual(substfunc(cmd, d),
                           [inputs[0] + '.out'] + outputs + cmd[2:])
          cmd = ['@INPUT0@.out', '@PLAINNAME@.ok', '@OUTPUT0@']
          self.assertEqual(substfunc(cmd, d),
                           [inputs[0] + '.out', d['@PLAINNAME@'] + '.ok'] + outputs)
          cmd = ['@INPUT@', '@BASENAME@.hah', 'strings']
          self.assertEqual(substfunc(cmd, d),
                           inputs + [d['@BASENAME@'] + '.hah'] + cmd[2:])
  
          # One input, one output with a subdir
          outputs = ['dir/out.c']
          ret = dictfunc(inputs, outputs)
          d = {'@INPUT@': inputs, '@INPUT0@': inputs[0],
               '@PLAINNAME@': 'foo.c.in', '@BASENAME@': 'foo.c',
               '@OUTPUT@': outputs, '@OUTPUT0@': outputs[0], '@OUTDIR@': 'dir'}
          # Check dictionary
          self.assertEqual(ret, d)
  
          # Two inputs, no outputs
          inputs = ['bar/foo.c.in', 'baz/foo.c.in']
          outputs = []
          ret = dictfunc(inputs, outputs)
          d = {'@INPUT@': inputs, '@INPUT0@': inputs[0], '@INPUT1@': inputs[1]}
          # Check dictionary
          self.assertEqual(ret, d)
          # Check substitutions
          cmd = ['some', 'ordinary', 'strings']
          self.assertEqual(substfunc(cmd, d), cmd)
          cmd = ['@INPUT@', 'ordinary', 'strings']
          self.assertEqual(substfunc(cmd, d), inputs + cmd[1:])
          cmd = ['@INPUT0@.out', 'ordinary', 'strings']
          self.assertEqual(substfunc(cmd, d), [inputs[0] + '.out'] + cmd[1:])
          cmd = ['@INPUT0@.out', '@INPUT1@.ok', 'strings']
          self.assertEqual(substfunc(cmd, d), [inputs[0] + '.out', inputs[1] + '.ok'] + cmd[2:])
          cmd = ['@INPUT0@', '@INPUT1@', 'strings']
          self.assertEqual(substfunc(cmd, d), inputs + cmd[2:])
          # Many inputs, can't use @INPUT@ like this
          cmd = ['@INPUT@.out', 'ordinary', 'strings']
          self.assertRaises(ME, substfunc, cmd, d)
          # Not enough inputs
          cmd = ['@INPUT2@.out', 'ordinary', 'strings']
          self.assertRaises(ME, substfunc, cmd, d)
          # Too many inputs
          cmd = ['@PLAINNAME@']
          self.assertRaises(ME, substfunc, cmd, d)
          cmd = ['@BASENAME@']
          self.assertRaises(ME, substfunc, cmd, d)
          # No outputs
          cmd = ['@OUTPUT@']
          self.assertRaises(ME, substfunc, cmd, d)
          cmd = ['@OUTPUT0@']
          self.assertRaises(ME, substfunc, cmd, d)
          cmd = ['@OUTDIR@']
          self.assertRaises(ME, substfunc, cmd, d)
  
          # Two inputs, one output
          outputs = ['dir/out.c']
          ret = dictfunc(inputs, outputs)
          d = {'@INPUT@': inputs, '@INPUT0@': inputs[0], '@INPUT1@': inputs[1],
               '@OUTPUT@': outputs, '@OUTPUT0@': outputs[0], '@OUTDIR@': 'dir'}
          # Check dictionary
          self.assertEqual(ret, d)
          # Check substitutions
          cmd = ['some', 'ordinary', 'strings']
          self.assertEqual(substfunc(cmd, d), cmd)
          cmd = ['@OUTPUT@', 'ordinary', 'strings']
          self.assertEqual(substfunc(cmd, d), outputs + cmd[1:])
          cmd = ['@OUTPUT@.out', 'ordinary', 'strings']
          self.assertEqual(substfunc(cmd, d), [outputs[0] + '.out'] + cmd[1:])
          cmd = ['@OUTPUT0@.out', '@INPUT1@.ok', 'strings']
          self.assertEqual(substfunc(cmd, d), [outputs[0] + '.out', inputs[1] + '.ok'] + cmd[2:])
          # Many inputs, can't use @INPUT@ like this
          cmd = ['@INPUT@.out', 'ordinary', 'strings']
          self.assertRaises(ME, substfunc, cmd, d)
          # Not enough inputs
          cmd = ['@INPUT2@.out', 'ordinary', 'strings']
          self.assertRaises(ME, substfunc, cmd, d)
          # Not enough outputs
          cmd = ['@OUTPUT2@.out', 'ordinary', 'strings']
          self.assertRaises(ME, substfunc, cmd, d)
  
          # Two inputs, two outputs
          outputs = ['dir/out.c', 'dir/out2.c']
          ret = dictfunc(inputs, outputs)
          d = {'@INPUT@': inputs, '@INPUT0@': inputs[0], '@INPUT1@': inputs[1],
               '@OUTPUT@': outputs, '@OUTPUT0@': outputs[0], '@OUTPUT1@': outputs[1],
               '@OUTDIR@': 'dir'}
          # Check dictionary
          self.assertEqual(ret, d)
          # Check substitutions
          cmd = ['some', 'ordinary', 'strings']
          self.assertEqual(substfunc(cmd, d), cmd)
          cmd = ['@OUTPUT@', 'ordinary', 'strings']
          self.assertEqual(substfunc(cmd, d), outputs + cmd[1:])
          cmd = ['@OUTPUT0@', '@OUTPUT1@', 'strings']
          self.assertEqual(substfunc(cmd, d), outputs + cmd[2:])
          cmd = ['@OUTPUT0@.out', '@INPUT1@.ok', '@OUTDIR@']
          self.assertEqual(substfunc(cmd, d), [outputs[0] + '.out', inputs[1] + '.ok', 'dir'])
          # Many inputs, can't use @INPUT@ like this
          cmd = ['@INPUT@.out', 'ordinary', 'strings']
          self.assertRaises(ME, substfunc, cmd, d)
          # Not enough inputs
          cmd = ['@INPUT2@.out', 'ordinary', 'strings']
          self.assertRaises(ME, substfunc, cmd, d)
          # Not enough outputs
          cmd = ['@OUTPUT2@.out', 'ordinary', 'strings']
          self.assertRaises(ME, substfunc, cmd, d)
          # Many outputs, can't use @OUTPUT@ like this
          cmd = ['@OUTPUT@.out', 'ordinary', 'strings']
          self.assertRaises(ME, substfunc, cmd, d)
  
      def test_needs_exe_wrapper_override(self):
          config = ConfigParser()
          config['binaries'] = {
              'c': '\'/usr/bin/gcc\'',
          }
          config['host_machine'] = {
              'system': '\'linux\'',
              'cpu_family': '\'arm\'',
              'cpu': '\'armv7\'',
              'endian': '\'little\'',
          }
          # Can not be used as context manager because we need to
          # open it a second time and this is not possible on
          # Windows.
          configfile = tempfile.NamedTemporaryFile(mode='w+', delete=False)
          configfilename = configfile.name
          config.write(configfile)
          configfile.flush()
          configfile.close()
          opts = get_fake_options()
          opts.cross_file = (configfilename,)
          env = get_fake_env(opts=opts)
          detected_value = env.need_exe_wrapper()
          os.unlink(configfilename)
  
          desired_value = not detected_value
          config['properties'] = {
              'needs_exe_wrapper': 'true' if desired_value else 'false'
          }
  
          configfile = tempfile.NamedTemporaryFile(mode='w+', delete=False)
          configfilename = configfile.name
          config.write(configfile)
          configfile.close()
          opts = get_fake_options()
          opts.cross_file = (configfilename,)
          env = get_fake_env(opts=opts)
          forced_value = env.need_exe_wrapper()
          os.unlink(configfilename)
  
          self.assertEqual(forced_value, desired_value)
  
      def test_listify(self):
          listify = mesonbuild.mesonlib.listify
          # Test sanity
          self.assertEqual([1], listify(1))
          self.assertEqual([], listify([]))
          self.assertEqual([1], listify([1]))
          # Test flattening
          self.assertEqual([1, 2, 3], listify([1, [2, 3]]))
          self.assertEqual([1, 2, 3], listify([1, [2, [3]]]))
          self.assertEqual([1, [2, [3]]], listify([1, [2, [3]]], flatten=False))
          # Test flattening and unholdering
          class TestHeldObj(mesonbuild.mesonlib.HoldableObject):
              def __init__(self, val: int) -> None:
                  self._val = val
          class MockInterpreter:
              def __init__(self) -> None:
                  self.subproject = ''
                  self.environment = None
          heldObj1 = TestHeldObj(1)
          holder1 = ObjectHolder(heldObj1, MockInterpreter())
          self.assertEqual([holder1], listify(holder1))
          self.assertEqual([holder1], listify([holder1]))
          self.assertEqual([holder1, 2], listify([holder1, 2]))
          self.assertEqual([holder1, 2, 3], listify([holder1, 2, [3]]))
  
      def test_extract_as_list(self):
          extract = mesonbuild.mesonlib.extract_as_list
          # Test sanity
          kwargs = {'sources': [1, 2, 3]}
          self.assertEqual([1, 2, 3], extract(kwargs, 'sources'))
          self.assertEqual(kwargs, {'sources': [1, 2, 3]})
          self.assertEqual([1, 2, 3], extract(kwargs, 'sources', pop=True))
          self.assertEqual(kwargs, {})
  
          class TestHeldObj(mesonbuild.mesonlib.HoldableObject):
              pass
          class MockInterpreter:
              def __init__(self) -> None:
                  self.subproject = ''
                  self.environment = None
          heldObj = TestHeldObj()
  
          # Test unholding
          holder3 = ObjectHolder(heldObj, MockInterpreter())
          kwargs = {'sources': [1, 2, holder3]}
          self.assertEqual(kwargs, {'sources': [1, 2, holder3]})
  
          # flatten nested lists
          kwargs = {'sources': [1, [2, [3]]]}
          self.assertEqual([1, 2, 3], extract(kwargs, 'sources'))
  
      def test_pkgconfig_module(self):
          dummystate = mock.Mock()
          dummystate.subproject = 'dummy'
          _mock = mock.Mock(spec=mesonbuild.dependencies.ExternalDependency)
          _mock.pcdep = mock.Mock()
          _mock.pcdep.name = "some_name"
          _mock.version_reqs = []
  
          # pkgconfig dependency as lib
          deps = mesonbuild.modules.pkgconfig.DependenciesHelper(dummystate, "thislib")
          deps.add_pub_libs([_mock])
          self.assertEqual(deps.format_reqs(deps.pub_reqs), "some_name")
  
          # pkgconfig dependency as requires
          deps = mesonbuild.modules.pkgconfig.DependenciesHelper(dummystate, "thislib")
          deps.add_pub_reqs([_mock])
          self.assertEqual(deps.format_reqs(deps.pub_reqs), "some_name")
  
      def _test_all_naming(self, cc, env, patterns, platform):
          shr = patterns[platform]['shared']
          stc = patterns[platform]['static']
          shrstc = shr + tuple([x for x in stc if x not in shr])
          stcshr = stc + tuple([x for x in shr if x not in stc])
          p = cc.get_library_naming(env, LibType.SHARED)
          self.assertEqual(p, shr)
          p = cc.get_library_naming(env, LibType.STATIC)
          self.assertEqual(p, stc)
          p = cc.get_library_naming(env, LibType.PREFER_STATIC)
          self.assertEqual(p, stcshr)
          p = cc.get_library_naming(env, LibType.PREFER_SHARED)
          self.assertEqual(p, shrstc)
          # Test find library by mocking up openbsd
          if platform != 'openbsd':
              return
          with tempfile.TemporaryDirectory() as tmpdir:
              for i in ['libfoo.so.6.0', 'libfoo.so.5.0', 'libfoo.so.54.0', 'libfoo.so.66a.0b', 'libfoo.so.70.0.so.1']:
                  libpath = Path(tmpdir) / i
                  libpath.write_text('', encoding='utf-8')
              found = cc._find_library_real('foo', env, [tmpdir], '', LibType.PREFER_SHARED)
              self.assertEqual(os.path.basename(found[0]), 'libfoo.so.54.0')
  
      def test_find_library_patterns(self):
          '''
          Unit test for the library search patterns used by find_library()
          '''
          unix_static = ('lib{}.a', '{}.a')
          msvc_static = ('lib{}.a', 'lib{}.lib', '{}.a', '{}.lib')
          # This is the priority list of pattern matching for library searching
          patterns = {'openbsd': {'shared': ('lib{}.so', '{}.so', 'lib{}.so.[0-9]*.[0-9]*', '{}.so.[0-9]*.[0-9]*'),
                                  'static': unix_static},
                      'linux': {'shared': ('lib{}.so', '{}.so'),
                                'static': unix_static},
                      'darwin': {'shared': ('lib{}.dylib', 'lib{}.so', '{}.dylib', '{}.so'),
                                 'static': unix_static},
                      'cygwin': {'shared': ('cyg{}.dll', 'cyg{}.dll.a', 'lib{}.dll',
                                            'lib{}.dll.a', '{}.dll', '{}.dll.a'),
                                 'static': ('cyg{}.a',) + unix_static},
                      'windows-msvc': {'shared': ('lib{}.lib', '{}.lib'),
                                       'static': msvc_static},
                      'windows-mingw': {'shared': ('lib{}.dll.a', 'lib{}.lib', 'lib{}.dll',
                                                   '{}.dll.a', '{}.lib', '{}.dll'),
                                        'static': msvc_static}}
          env = get_fake_env()
          cc = detect_c_compiler(env, MachineChoice.HOST)
          if is_osx():
              self._test_all_naming(cc, env, patterns, 'darwin')
          elif is_cygwin():
              self._test_all_naming(cc, env, patterns, 'cygwin')
          elif is_windows():
              if cc.get_argument_syntax() == 'msvc':
                  self._test_all_naming(cc, env, patterns, 'windows-msvc')
              else:
                  self._test_all_naming(cc, env, patterns, 'windows-mingw')
          elif is_openbsd():
              self._test_all_naming(cc, env, patterns, 'openbsd')
          else:
              self._test_all_naming(cc, env, patterns, 'linux')
              env.machines.host.system = 'openbsd'
              self._test_all_naming(cc, env, patterns, 'openbsd')
              env.machines.host.system = 'darwin'
              self._test_all_naming(cc, env, patterns, 'darwin')
              env.machines.host.system = 'cygwin'
              self._test_all_naming(cc, env, patterns, 'cygwin')
              env.machines.host.system = 'windows'
              self._test_all_naming(cc, env, patterns, 'windows-mingw')
  
      @skipIfNoPkgconfig
      def test_pkgconfig_parse_libs(self):
          '''
          Unit test for parsing of pkg-config output to search for libraries
  
          https://github.com/mesonbuild/meson/issues/3951
          '''
          def create_static_lib(name):
              if not is_osx():
                  name.open('w', encoding='utf-8').close()
                  return
              src = name.with_suffix('.c')
              out = name.with_suffix('.o')
              with src.open('w', encoding='utf-8') as f:
                  f.write('int meson_foobar (void) { return 0; }')
              subprocess.check_call(['clang', '-c', str(src), '-o', str(out)])
              subprocess.check_call(['ar', 'csr', str(name), str(out)])
  
          with tempfile.TemporaryDirectory() as tmpdir:
              pkgbin = ExternalProgram('pkg-config', command=['pkg-config'], silent=True)
              env = get_fake_env()
              compiler = detect_c_comp
  
  [... the rest is omitted ...]
files.new_path: run_unittests.py
files.old_contents: #!/usr/bin/env python3
  # Copyright 2016-2021 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.
  
  # Work around some pathlib bugs...
  from mesonbuild import _pathlib
  import sys
  sys.modules['pathlib'] = _pathlib
  
  import time
  import stat
  import subprocess
  import re
  import json
  import tempfile
  import textwrap
  import os
  import shutil
  import unittest
  import platform
  import pickle
  import functools
  import io
  import operator
  import threading
  import zipfile, tarfile
  import hashlib
  from itertools import chain
  from unittest import mock
  from configparser import ConfigParser
  from contextlib import contextmanager
  from glob import glob
  from pathlib import (PurePath, Path)
  from distutils.dir_util import copy_tree
  import typing as T
  
  import mesonbuild.mlog
  import mesonbuild.depfile
  import mesonbuild.dependencies.base
  import mesonbuild.dependencies.factory
  import mesonbuild.compilers
  import mesonbuild.envconfig
  import mesonbuild.environment
  import mesonbuild.mesonlib
  import mesonbuild.coredata
  import mesonbuild.modules.gnome
  from mesonbuild.interpreter import Interpreter
  from mesonbuild.interpreterbase import typed_pos_args, InvalidArguments, ObjectHolder
  from mesonbuild.interpreterbase import typed_pos_args, InvalidArguments, typed_kwargs, ContainerTypeInfo, KwargInfo
  from mesonbuild.ast import AstInterpreter
  from mesonbuild.mesonlib import (
      BuildDirLock, LibType, MachineChoice, PerMachine, Version, is_windows,
      is_osx, is_cygwin, is_dragonflybsd, is_openbsd, is_haiku, is_sunos,
      windows_proof_rmtree, windows_proof_rm, python_command,
      version_compare, split_args, quote_arg, relpath, is_linux, git,
      search_version
  )
  from mesonbuild.compilers import (
      detect_static_linker, detect_c_compiler, detect_cpp_compiler,
      detect_objc_compiler, detect_objcpp_compiler, detect_d_compiler,
      detect_swift_compiler, compiler_from_language
  )
  from mesonbuild.environment import detect_ninja
  from mesonbuild.mesonlib import MesonException, EnvironmentException, OptionKey
  from mesonbuild.dependencies import PkgConfigDependency
  from mesonbuild.programs import ExternalProgram
  import mesonbuild.dependencies.base
  from mesonbuild.build import Target, ConfigurationData
  import mesonbuild.modules.pkgconfig
  from mesonbuild.scripts import destdir_join
  
  from mesonbuild.mtest import TAPParser, TestResult
  from mesonbuild.mesonmain import setup_vsenv
  from mesonbuild.wrap.wrap import PackageDefinition, WrapException
  
  from run_tests import (
      Backend, FakeBuild, FakeCompilerOptions,
      ensure_backend_detects_changes, exe_suffix, get_backend_commands,
      get_builddir_target_args, get_fake_env, get_fake_options, get_meson_script,
      run_configure_inprocess, run_mtest_inprocess
  )
  
  if T.TYPE_CHECKING:
      from mesonbuild.compilers import Compiler
  
  URLOPEN_TIMEOUT = 5
  
  @contextmanager
  def chdir(path: str):
      curdir = os.getcwd()
      os.chdir(path)
      try:
          yield
      finally:
          os.chdir(curdir)
  
  
  def get_dynamic_section_entry(fname: str, entry: str) -> T.Optional[str]:
      if is_cygwin() or is_osx():
          raise unittest.SkipTest('Test only applicable to ELF platforms')
  
      try:
          raw_out = subprocess.check_output(['readelf', '-d', fname],
                                            universal_newlines=True)
      except FileNotFoundError:
          # FIXME: Try using depfixer.py:Elf() as a fallback
          raise unittest.SkipTest('readelf not found')
      pattern = re.compile(entry + r': \[(.*?)\]')
      for line in raw_out.split('\n'):
          m = pattern.search(line)
          if m is not None:
              return str(m.group(1))
      return None # The file did not contain the specified entry.
  
  def get_soname(fname: str) -> T.Optional[str]:
      return get_dynamic_section_entry(fname, 'soname')
  
  def get_rpath(fname: str) -> T.Optional[str]:
      raw = get_dynamic_section_entry(fname, r'(?:rpath|runpath)')
      # Get both '' and None here
      if not raw:
          return None
      # nix/nixos adds a bunch of stuff to the rpath out of necessity that we
      # don't check for, so clear those
      final = ':'.join([e for e in raw.split(':') if not e.startswith('/nix')])
      return final
  
  def is_tarball():
      if not os.path.isdir('docs'):
          return True
      return False
  
  def is_ci():
      if 'CI' in os.environ:
          return True
      return False
  
  def _git_init(project_dir):
      # If a user has git configuration init.defaultBranch set we want to override that
      with tempfile.TemporaryDirectory() as d:
          out = git(['--version'], str(d))[1]
      if version_compare(search_version(out), '>= 2.28'):
          extra_cmd = ['--initial-branch', 'master']
      else:
          extra_cmd = []
  
      subprocess.check_call(['git', 'init'] + extra_cmd, cwd=project_dir, stdout=subprocess.DEVNULL)
      subprocess.check_call(['git', 'config',
                             'user.name', 'Author Person'], cwd=project_dir)
      subprocess.check_call(['git', 'config',
                             'user.email', 'teh_coderz@example.com'], cwd=project_dir)
      _git_add_all(project_dir)
  
  def _git_add_all(project_dir):
      subprocess.check_call('git add *', cwd=project_dir, shell=True,
                            stdout=subprocess.DEVNULL)
      subprocess.check_call(['git', 'commit', '-a', '-m', 'I am a project'], cwd=project_dir,
                            stdout=subprocess.DEVNULL)
  
  @functools.lru_cache()
  def is_real_gnu_compiler(path):
      '''
      Check if the gcc we have is a real gcc and not a macOS wrapper around clang
      '''
      if not path:
          return False
      out = subprocess.check_output([path, '--version'], universal_newlines=True, stderr=subprocess.STDOUT)
      return 'Free Software Foundation' in out
  
  def skipIfNoExecutable(exename):
      '''
      Skip this test if the given executable is not found.
      '''
      def wrapper(func):
          @functools.wraps(func)
          def wrapped(*args, **kwargs):
              if shutil.which(exename) is None:
                  raise unittest.SkipTest(exename + ' not found')
              return func(*args, **kwargs)
          return wrapped
      return wrapper
  
  def skipIfNoPkgconfig(f):
      '''
      Skip this test if no pkg-config is found, unless we're on CI.
      This allows users to run our test suite without having
      pkg-config installed on, f.ex., macOS, while ensuring that our CI does not
      silently skip the test because of misconfiguration.
  
      Note: Yes, we provide pkg-config even while running Windows CI
      '''
      @functools.wraps(f)
      def wrapped(*args, **kwargs):
          if not is_ci() and shutil.which('pkg-config') is None:
              raise unittest.SkipTest('pkg-config not found')
          return f(*args, **kwargs)
      return wrapped
  
  def skipIfNoPkgconfigDep(depname):
      '''
      Skip this test if the given pkg-config dep is not found, unless we're on CI.
      '''
      def wrapper(func):
          @functools.wraps(func)
          def wrapped(*args, **kwargs):
              if not is_ci() and shutil.which('pkg-config') is None:
                  raise unittest.SkipTest('pkg-config not found')
              if not is_ci() and subprocess.call(['pkg-config', '--exists', depname]) != 0:
                  raise unittest.SkipTest(f'pkg-config dependency {depname} not found.')
              return func(*args, **kwargs)
          return wrapped
      return wrapper
  
  def skip_if_no_cmake(f):
      '''
      Skip this test if no cmake is found, unless we're on CI.
      This allows users to run our test suite without having
      cmake installed on, f.ex., macOS, while ensuring that our CI does not
      silently skip the test because of misconfiguration.
      '''
      @functools.wraps(f)
      def wrapped(*args, **kwargs):
          if not is_ci() and shutil.which('cmake') is None:
              raise unittest.SkipTest('cmake not found')
          return f(*args, **kwargs)
      return wrapped
  
  def skip_if_not_language(lang: str):
      def wrapper(func):
          @functools.wraps(func)
          def wrapped(*args, **kwargs):
              try:
                  compiler_from_language(get_fake_env(), lang, MachineChoice.HOST)
              except EnvironmentException:
                  raise unittest.SkipTest(f'No {lang} compiler found.')
              return func(*args, **kwargs)
          return wrapped
      return wrapper
  
  def skip_if_env_set(key):
      '''
      Skip a test if a particular env is set, except when running under CI
      '''
      def wrapper(func):
          @functools.wraps(func)
          def wrapped(*args, **kwargs):
              old = None
              if key in os.environ:
                  if not is_ci():
                      raise unittest.SkipTest(f'Env var {key!r} set, skipping')
                  old = os.environ.pop(key)
              try:
                  return func(*args, **kwargs)
              finally:
                  if old is not None:
                      os.environ[key] = old
          return wrapped
      return wrapper
  
  def skip_if_not_base_option(feature):
      """Skip tests if The compiler does not support a given base option.
  
      for example, ICC doesn't currently support b_sanitize.
      """
      def actual(f):
          @functools.wraps(f)
          def wrapped(*args, **kwargs):
              env = get_fake_env()
              cc = detect_c_compiler(env, MachineChoice.HOST)
              key = OptionKey(feature)
              if key not in cc.base_options:
                  raise unittest.SkipTest(
                      f'{feature} not available with {cc.id}')
              return f(*args, **kwargs)
          return wrapped
      return actual
  
  
  @contextmanager
  def temp_filename():
      '''A context manager which provides a filename to an empty temporary file.
  
      On exit the file will be deleted.
      '''
  
      fd, filename = tempfile.mkstemp()
      os.close(fd)
      try:
          yield filename
      finally:
          try:
              os.remove(filename)
          except OSError:
              pass
  
  @contextmanager
  def no_pkgconfig():
      '''
      A context manager that overrides shutil.which and ExternalProgram to force
      them to return None for pkg-config to simulate it not existing.
      '''
      old_which = shutil.which
      old_search = ExternalProgram._search
  
      def new_search(self, name, search_dir):
          if name == 'pkg-config':
              return [None]
          return old_search(self, name, search_dir)
  
      def new_which(cmd, *kwargs):
          if cmd == 'pkg-config':
              return None
          return old_which(cmd, *kwargs)
  
      shutil.which = new_which
      ExternalProgram._search = new_search
      try:
          yield
      finally:
          shutil.which = old_which
          ExternalProgram._search = old_search
  
  
  class InternalTests(unittest.TestCase):
  
      def test_version_number(self):
          self.assertEqual(search_version('foobar 1.2.3'), '1.2.3')
          self.assertEqual(search_version('1.2.3'), '1.2.3')
          self.assertEqual(search_version('foobar 2016.10.28 1.2.3'), '1.2.3')
          self.assertEqual(search_version('2016.10.28 1.2.3'), '1.2.3')
          self.assertEqual(search_version('foobar 2016.10.128'), '2016.10.128')
          self.assertEqual(search_version('2016.10.128'), '2016.10.128')
          self.assertEqual(search_version('2016.10'), '2016.10')
          self.assertEqual(search_version('2016.10 1.2.3'), '1.2.3')
          self.assertEqual(search_version('oops v1.2.3'), '1.2.3')
          self.assertEqual(search_version('2016.oops 1.2.3'), '1.2.3')
          self.assertEqual(search_version('2016.x'), 'unknown version')
  
      def test_mode_symbolic_to_bits(self):
          modefunc = mesonbuild.mesonlib.FileMode.perms_s_to_bits
          self.assertEqual(modefunc('---------'), 0)
          self.assertEqual(modefunc('r--------'), stat.S_IRUSR)
          self.assertEqual(modefunc('---r-----'), stat.S_IRGRP)
          self.assertEqual(modefunc('------r--'), stat.S_IROTH)
          self.assertEqual(modefunc('-w-------'), stat.S_IWUSR)
          self.assertEqual(modefunc('----w----'), stat.S_IWGRP)
          self.assertEqual(modefunc('-------w-'), stat.S_IWOTH)
          self.assertEqual(modefunc('--x------'), stat.S_IXUSR)
          self.assertEqual(modefunc('-----x---'), stat.S_IXGRP)
          self.assertEqual(modefunc('--------x'), stat.S_IXOTH)
          self.assertEqual(modefunc('--S------'), stat.S_ISUID)
          self.assertEqual(modefunc('-----S---'), stat.S_ISGID)
          self.assertEqual(modefunc('--------T'), stat.S_ISVTX)
          self.assertEqual(modefunc('--s------'), stat.S_ISUID | stat.S_IXUSR)
          self.assertEqual(modefunc('-----s---'), stat.S_ISGID | stat.S_IXGRP)
          self.assertEqual(modefunc('--------t'), stat.S_ISVTX | stat.S_IXOTH)
          self.assertEqual(modefunc('rwx------'), stat.S_IRWXU)
          self.assertEqual(modefunc('---rwx---'), stat.S_IRWXG)
          self.assertEqual(modefunc('------rwx'), stat.S_IRWXO)
          # We could keep listing combinations exhaustively but that seems
          # tedious and pointless. Just test a few more.
          self.assertEqual(modefunc('rwxr-xr-x'),
                           stat.S_IRWXU |
                           stat.S_IRGRP | stat.S_IXGRP |
                           stat.S_IROTH | stat.S_IXOTH)
          self.assertEqual(modefunc('rw-r--r--'),
                           stat.S_IRUSR | stat.S_IWUSR |
                           stat.S_IRGRP |
                           stat.S_IROTH)
          self.assertEqual(modefunc('rwsr-x---'),
                           stat.S_IRWXU | stat.S_ISUID |
                           stat.S_IRGRP | stat.S_IXGRP)
  
      def test_compiler_args_class_none_flush(self):
          cc = mesonbuild.compilers.ClangCCompiler([], 'fake', MachineChoice.HOST, False, mock.Mock())
          a = cc.compiler_args(['-I.'])
          #first we are checking if the tree construction deduplicates the correct -I argument
          a += ['-I..']
          a += ['-I./tests/']
          a += ['-I./tests2/']
          #think this here as assertion, we cannot apply it, otherwise the CompilerArgs would already flush the changes:
          # assertEqual(a, ['-I.', '-I./tests2/', '-I./tests/', '-I..', '-I.'])
          a += ['-I.']
          a += ['-I.', '-I./tests/']
          self.assertEqual(a, ['-I.', '-I./tests/', '-I./tests2/', '-I..'])
  
          #then we are checking that when CompilerArgs already have a build container list, that the deduplication is taking the correct one
          a += ['-I.', '-I./tests2/']
          self.assertEqual(a, ['-I.', '-I./tests2/', '-I./tests/', '-I..'])
  
      def test_compiler_args_class_d(self):
          d = mesonbuild.compilers.DmdDCompiler([], 'fake', MachineChoice.HOST, 'info', 'arch')
          # check include order is kept when deduplicating
          a = d.compiler_args(['-Ifirst', '-Isecond', '-Ithird'])
          a += ['-Ifirst']
          self.assertEqual(a, ['-Ifirst', '-Isecond', '-Ithird'])
  
      def test_compiler_args_class_clike(self):
          cc = mesonbuild.compilers.ClangCCompiler([], 'fake', MachineChoice.HOST, False, mock.Mock())
          # Test that empty initialization works
          a = cc.compiler_args()
          self.assertEqual(a, [])
          # Test that list initialization works
          a = cc.compiler_args(['-I.', '-I..'])
          self.assertEqual(a, ['-I.', '-I..'])
          # Test that there is no de-dup on initialization
          self.assertEqual(cc.compiler_args(['-I.', '-I.']), ['-I.', '-I.'])
  
          ## Test that appending works
          a.append('-I..')
          self.assertEqual(a, ['-I..', '-I.'])
          a.append('-O3')
          self.assertEqual(a, ['-I..', '-I.', '-O3'])
  
          ## Test that in-place addition works
          a += ['-O2', '-O2']
          self.assertEqual(a, ['-I..', '-I.', '-O3', '-O2', '-O2'])
          # Test that removal works
          a.remove('-O2')
          self.assertEqual(a, ['-I..', '-I.', '-O3', '-O2'])
          # Test that de-dup happens on addition
          a += ['-Ifoo', '-Ifoo']
          self.assertEqual(a, ['-Ifoo', '-I..', '-I.', '-O3', '-O2'])
  
          # .extend() is just +=, so we don't test it
  
          ## Test that addition works
          # Test that adding a list with just one old arg works and yields the same array
          a = a + ['-Ifoo']
          self.assertEqual(a, ['-Ifoo', '-I..', '-I.', '-O3', '-O2'])
          # Test that adding a list with one arg new and one old works
          a = a + ['-Ifoo', '-Ibaz']
          self.assertEqual(a, ['-Ifoo', '-Ibaz', '-I..', '-I.', '-O3', '-O2'])
          # Test that adding args that must be prepended and appended works
          a = a + ['-Ibar', '-Wall']
          self.assertEqual(a, ['-Ibar', '-Ifoo', '-Ibaz', '-I..', '-I.', '-O3', '-O2', '-Wall'])
  
          ## Test that reflected addition works
          # Test that adding to a list with just one old arg works and yields the same array
          a = ['-Ifoo'] + a
          self.assertEqual(a, ['-Ibar', '-Ifoo', '-Ibaz', '-I..', '-I.', '-O3', '-O2', '-Wall'])
          # Test that adding to a list with just one new arg that is not pre-pended works
          a = ['-Werror'] + a
          self.assertEqual(a, ['-Ibar', '-Ifoo', '-Ibaz', '-I..', '-I.', '-Werror', '-O3', '-O2', '-Wall'])
          # Test that adding to a list with two new args preserves the order
          a = ['-Ldir', '-Lbah'] + a
          self.assertEqual(a, ['-Ibar', '-Ifoo', '-Ibaz', '-I..', '-I.', '-Ldir', '-Lbah', '-Werror', '-O3', '-O2', '-Wall'])
          # Test that adding to a list with old args does nothing
          a = ['-Ibar', '-Ibaz', '-Ifoo'] + a
          self.assertEqual(a, ['-Ibar', '-Ifoo', '-Ibaz', '-I..', '-I.', '-Ldir', '-Lbah', '-Werror', '-O3', '-O2', '-Wall'])
  
          ## Test that adding libraries works
          l = cc.compiler_args(['-Lfoodir', '-lfoo'])
          self.assertEqual(l, ['-Lfoodir', '-lfoo'])
          # Adding a library and a libpath appends both correctly
          l += ['-Lbardir', '-lbar']
          self.assertEqual(l, ['-Lbardir', '-Lfoodir', '-lfoo', '-lbar'])
          # Adding the same library again does nothing
          l += ['-lbar']
          self.assertEqual(l, ['-Lbardir', '-Lfoodir', '-lfoo', '-lbar'])
  
          ## Test that 'direct' append and extend works
          l = cc.compiler_args(['-Lfoodir', '-lfoo'])
          self.assertEqual(l, ['-Lfoodir', '-lfoo'])
          # Direct-adding a library and a libpath appends both correctly
          l.extend_direct(['-Lbardir', '-lbar'])
          self.assertEqual(l, ['-Lfoodir', '-lfoo', '-Lbardir', '-lbar'])
          # Direct-adding the same library again still adds it
          l.append_direct('-lbar')
          self.assertEqual(l, ['-Lfoodir', '-lfoo', '-Lbardir', '-lbar', '-lbar'])
          # Direct-adding with absolute path deduplicates
          l.append_direct('/libbaz.a')
          self.assertEqual(l, ['-Lfoodir', '-lfoo', '-Lbardir', '-lbar', '-lbar', '/libbaz.a'])
          # Adding libbaz again does nothing
          l.append_direct('/libbaz.a')
          self.assertEqual(l, ['-Lfoodir', '-lfoo', '-Lbardir', '-lbar', '-lbar', '/libbaz.a'])
  
      def test_compiler_args_class_gnuld(self):
          ## Test --start/end-group
          linker = mesonbuild.linkers.GnuBFDDynamicLinker([], MachineChoice.HOST, '-Wl,', [])
          gcc = mesonbuild.compilers.GnuCCompiler([], 'fake', False, MachineChoice.HOST, mock.Mock(), linker=linker)
          ## Ensure that the fake compiler is never called by overriding the relevant function
          gcc.get_default_include_dirs = lambda: ['/usr/include', '/usr/share/include', '/usr/local/include']
          ## Test that 'direct' append and extend works
          l = gcc.compiler_args(['-Lfoodir', '-lfoo'])
          self.assertEqual(l.to_native(copy=True), ['-Lfoodir', '-Wl,--start-group', '-lfoo', '-Wl,--end-group'])
          # Direct-adding a library and a libpath appends both correctly
          l.extend_direct(['-Lbardir', '-lbar'])
          self.assertEqual(l.to_native(copy=True), ['-Lfoodir', '-Wl,--start-group', '-lfoo', '-Lbardir', '-lbar', '-Wl,--end-group'])
          # Direct-adding the same library again still adds it
          l.append_direct('-lbar')
          self.assertEqual(l.to_native(copy=True), ['-Lfoodir', '-Wl,--start-group', '-lfoo', '-Lbardir', '-lbar', '-lbar', '-Wl,--end-group'])
          # Direct-adding with absolute path deduplicates
          l.append_direct('/libbaz.a')
          self.assertEqual(l.to_native(copy=True), ['-Lfoodir', '-Wl,--start-group', '-lfoo', '-Lbardir', '-lbar', '-lbar', '/libbaz.a', '-Wl,--end-group'])
          # Adding libbaz again does nothing
          l.append_direct('/libbaz.a')
          self.assertEqual(l.to_native(copy=True), ['-Lfoodir', '-Wl,--start-group', '-lfoo', '-Lbardir', '-lbar', '-lbar', '/libbaz.a', '-Wl,--end-group'])
          # Adding a non-library argument doesn't include it in the group
          l += ['-Lfoo', '-Wl,--export-dynamic']
          self.assertEqual(l.to_native(copy=True), ['-Lfoo', '-Lfoodir', '-Wl,--start-group', '-lfoo', '-Lbardir', '-lbar', '-lbar', '/libbaz.a', '-Wl,--end-group', '-Wl,--export-dynamic'])
          # -Wl,-lfoo is detected as a library and gets added to the group
          l.append('-Wl,-ldl')
          self.assertEqual(l.to_native(copy=True), ['-Lfoo', '-Lfoodir', '-Wl,--start-group', '-lfoo', '-Lbardir', '-lbar', '-lbar', '/libbaz.a', '-Wl,--export-dynamic', '-Wl,-ldl', '-Wl,--end-group'])
  
      def test_compiler_args_remove_system(self):
          ## Test --start/end-group
          linker = mesonbuild.linkers.GnuBFDDynamicLinker([], MachineChoice.HOST, '-Wl,', [])
          gcc = mesonbuild.compilers.GnuCCompiler([], 'fake', False, MachineChoice.HOST, mock.Mock(), linker=linker)
          ## Ensure that the fake compiler is never called by overriding the relevant function
          gcc.get_default_include_dirs = lambda: ['/usr/include', '/usr/share/include', '/usr/local/include']
          ## Test that 'direct' append and extend works
          l = gcc.compiler_args(['-Lfoodir', '-lfoo'])
          self.assertEqual(l.to_native(copy=True), ['-Lfoodir', '-Wl,--start-group', '-lfoo', '-Wl,--end-group'])
          ## Test that to_native removes all system includes
          l += ['-isystem/usr/include', '-isystem=/usr/share/include', '-DSOMETHING_IMPORTANT=1', '-isystem', '/usr/local/include']
          self.assertEqual(l.to_native(copy=True), ['-Lfoodir', '-Wl,--start-group', '-lfoo', '-Wl,--end-group', '-DSOMETHING_IMPORTANT=1'])
  
      def test_string_templates_substitution(self):
          dictfunc = mesonbuild.mesonlib.get_filenames_templates_dict
          substfunc = mesonbuild.mesonlib.substitute_values
          ME = mesonbuild.mesonlib.MesonException
  
          # Identity
          self.assertEqual(dictfunc([], []), {})
  
          # One input, no outputs
          inputs = ['bar/foo.c.in']
          outputs = []
          ret = dictfunc(inputs, outputs)
          d = {'@INPUT@': inputs, '@INPUT0@': inputs[0],
               '@PLAINNAME@': 'foo.c.in', '@BASENAME@': 'foo.c'}
          # Check dictionary
          self.assertEqual(ret, d)
          # Check substitutions
          cmd = ['some', 'ordinary', 'strings']
          self.assertEqual(substfunc(cmd, d), cmd)
          cmd = ['@INPUT@.out', 'ordinary', 'strings']
          self.assertEqual(substfunc(cmd, d), [inputs[0] + '.out'] + cmd[1:])
          cmd = ['@INPUT0@.out', '@PLAINNAME@.ok', 'strings']
          self.assertEqual(substfunc(cmd, d),
                           [inputs[0] + '.out'] + [d['@PLAINNAME@'] + '.ok'] + cmd[2:])
          cmd = ['@INPUT@', '@BASENAME@.hah', 'strings']
          self.assertEqual(substfunc(cmd, d),
                           inputs + [d['@BASENAME@'] + '.hah'] + cmd[2:])
          cmd = ['@OUTPUT@']
          self.assertRaises(ME, substfunc, cmd, d)
  
          # One input, one output
          inputs = ['bar/foo.c.in']
          outputs = ['out.c']
          ret = dictfunc(inputs, outputs)
          d = {'@INPUT@': inputs, '@INPUT0@': inputs[0],
               '@PLAINNAME@': 'foo.c.in', '@BASENAME@': 'foo.c',
               '@OUTPUT@': outputs, '@OUTPUT0@': outputs[0], '@OUTDIR@': '.'}
          # Check dictionary
          self.assertEqual(ret, d)
          # Check substitutions
          cmd = ['some', 'ordinary', 'strings']
          self.assertEqual(substfunc(cmd, d), cmd)
          cmd = ['@INPUT@.out', '@OUTPUT@', 'strings']
          self.assertEqual(substfunc(cmd, d),
                           [inputs[0] + '.out'] + outputs + cmd[2:])
          cmd = ['@INPUT0@.out', '@PLAINNAME@.ok', '@OUTPUT0@']
          self.assertEqual(substfunc(cmd, d),
                           [inputs[0] + '.out', d['@PLAINNAME@'] + '.ok'] + outputs)
          cmd = ['@INPUT@', '@BASENAME@.hah', 'strings']
          self.assertEqual(substfunc(cmd, d),
                           inputs + [d['@BASENAME@'] + '.hah'] + cmd[2:])
  
          # One input, one output with a subdir
          outputs = ['dir/out.c']
          ret = dictfunc(inputs, outputs)
          d = {'@INPUT@': inputs, '@INPUT0@': inputs[0],
               '@PLAINNAME@': 'foo.c.in', '@BASENAME@': 'foo.c',
               '@OUTPUT@': outputs, '@OUTPUT0@': outputs[0], '@OUTDIR@': 'dir'}
          # Check dictionary
          self.assertEqual(ret, d)
  
          # Two inputs, no outputs
          inputs = ['bar/foo.c.in', 'baz/foo.c.in']
          outputs = []
          ret = dictfunc(inputs, outputs)
          d = {'@INPUT@': inputs, '@INPUT0@': inputs[0], '@INPUT1@': inputs[1]}
          # Check dictionary
          self.assertEqual(ret, d)
          # Check substitutions
          cmd = ['some', 'ordinary', 'strings']
          self.assertEqual(substfunc(cmd, d), cmd)
          cmd = ['@INPUT@', 'ordinary', 'strings']
          self.assertEqual(substfunc(cmd, d), inputs + cmd[1:])
          cmd = ['@INPUT0@.out', 'ordinary', 'strings']
          self.assertEqual(substfunc(cmd, d), [inputs[0] + '.out'] + cmd[1:])
          cmd = ['@INPUT0@.out', '@INPUT1@.ok', 'strings']
          self.assertEqual(substfunc(cmd, d), [inputs[0] + '.out', inputs[1] + '.ok'] + cmd[2:])
          cmd = ['@INPUT0@', '@INPUT1@', 'strings']
          self.assertEqual(substfunc(cmd, d), inputs + cmd[2:])
          # Many inputs, can't use @INPUT@ like this
          cmd = ['@INPUT@.out', 'ordinary', 'strings']
          self.assertRaises(ME, substfunc, cmd, d)
          # Not enough inputs
          cmd = ['@INPUT2@.out', 'ordinary', 'strings']
          self.assertRaises(ME, substfunc, cmd, d)
          # Too many inputs
          cmd = ['@PLAINNAME@']
          self.assertRaises(ME, substfunc, cmd, d)
          cmd = ['@BASENAME@']
          self.assertRaises(ME, substfunc, cmd, d)
          # No outputs
          cmd = ['@OUTPUT@']
          self.assertRaises(ME, substfunc, cmd, d)
          cmd = ['@OUTPUT0@']
          self.assertRaises(ME, substfunc, cmd, d)
          cmd = ['@OUTDIR@']
          self.assertRaises(ME, substfunc, cmd, d)
  
          # Two inputs, one output
          outputs = ['dir/out.c']
          ret = dictfunc(inputs, outputs)
          d = {'@INPUT@': inputs, '@INPUT0@': inputs[0], '@INPUT1@': inputs[1],
               '@OUTPUT@': outputs, '@OUTPUT0@': outputs[0], '@OUTDIR@': 'dir'}
          # Check dictionary
          self.assertEqual(ret, d)
          # Check substitutions
          cmd = ['some', 'ordinary', 'strings']
          self.assertEqual(substfunc(cmd, d), cmd)
          cmd = ['@OUTPUT@', 'ordinary', 'strings']
          self.assertEqual(substfunc(cmd, d), outputs + cmd[1:])
          cmd = ['@OUTPUT@.out', 'ordinary', 'strings']
          self.assertEqual(substfunc(cmd, d), [outputs[0] + '.out'] + cmd[1:])
          cmd = ['@OUTPUT0@.out', '@INPUT1@.ok', 'strings']
          self.assertEqual(substfunc(cmd, d), [outputs[0] + '.out', inputs[1] + '.ok'] + cmd[2:])
          # Many inputs, can't use @INPUT@ like this
          cmd = ['@INPUT@.out', 'ordinary', 'strings']
          self.assertRaises(ME, substfunc, cmd, d)
          # Not enough inputs
          cmd = ['@INPUT2@.out', 'ordinary', 'strings']
          self.assertRaises(ME, substfunc, cmd, d)
          # Not enough outputs
          cmd = ['@OUTPUT2@.out', 'ordinary', 'strings']
          self.assertRaises(ME, substfunc, cmd, d)
  
          # Two inputs, two outputs
          outputs = ['dir/out.c', 'dir/out2.c']
          ret = dictfunc(inputs, outputs)
          d = {'@INPUT@': inputs, '@INPUT0@': inputs[0], '@INPUT1@': inputs[1],
               '@OUTPUT@': outputs, '@OUTPUT0@': outputs[0], '@OUTPUT1@': outputs[1],
               '@OUTDIR@': 'dir'}
          # Check dictionary
          self.assertEqual(ret, d)
          # Check substitutions
          cmd = ['some', 'ordinary', 'strings']
          self.assertEqual(substfunc(cmd, d), cmd)
          cmd = ['@OUTPUT@', 'ordinary', 'strings']
          self.assertEqual(substfunc(cmd, d), outputs + cmd[1:])
          cmd = ['@OUTPUT0@', '@OUTPUT1@', 'strings']
          self.assertEqual(substfunc(cmd, d), outputs + cmd[2:])
          cmd = ['@OUTPUT0@.out', '@INPUT1@.ok', '@OUTDIR@']
          self.assertEqual(substfunc(cmd, d), [outputs[0] + '.out', inputs[1] + '.ok', 'dir'])
          # Many inputs, can't use @INPUT@ like this
          cmd = ['@INPUT@.out', 'ordinary', 'strings']
          self.assertRaises(ME, substfunc, cmd, d)
          # Not enough inputs
          cmd = ['@INPUT2@.out', 'ordinary', 'strings']
          self.assertRaises(ME, substfunc, cmd, d)
          # Not enough outputs
          cmd = ['@OUTPUT2@.out', 'ordinary', 'strings']
          self.assertRaises(ME, substfunc, cmd, d)
          # Many outputs, can't use @OUTPUT@ like this
          cmd = ['@OUTPUT@.out', 'ordinary', 'strings']
          self.assertRaises(ME, substfunc, cmd, d)
  
      def test_needs_exe_wrapper_override(self):
          config = ConfigParser()
          config['binaries'] = {
              'c': '\'/usr/bin/gcc\'',
          }
          config['host_machine'] = {
              'system': '\'linux\'',
              'cpu_family': '\'arm\'',
              'cpu': '\'armv7\'',
              'endian': '\'little\'',
          }
          # Can not be used as context manager because we need to
          # open it a second time and this is not possible on
          # Windows.
          configfile = tempfile.NamedTemporaryFile(mode='w+', delete=False)
          configfilename = configfile.name
          config.write(configfile)
          configfile.flush()
          configfile.close()
          opts = get_fake_options()
          opts.cross_file = (configfilename,)
          env = get_fake_env(opts=opts)
          detected_value = env.need_exe_wrapper()
          os.unlink(configfilename)
  
          desired_value = not detected_value
          config['properties'] = {
              'needs_exe_wrapper': 'true' if desired_value else 'false'
          }
  
          configfile = tempfile.NamedTemporaryFile(mode='w+', delete=False)
          configfilename = configfile.name
          config.write(configfile)
          configfile.close()
          opts = get_fake_options()
          opts.cross_file = (configfilename,)
          env = get_fake_env(opts=opts)
          forced_value = env.need_exe_wrapper()
          os.unlink(configfilename)
  
          self.assertEqual(forced_value, desired_value)
  
      def test_listify(self):
          listify = mesonbuild.mesonlib.listify
          # Test sanity
          self.assertEqual([1], listify(1))
          self.assertEqual([], listify([]))
          self.assertEqual([1], listify([1]))
          # Test flattening
          self.assertEqual([1, 2, 3], listify([1, [2, 3]]))
          self.assertEqual([1, 2, 3], listify([1, [2, [3]]]))
          self.assertEqual([1, [2, [3]]], listify([1, [2, [3]]], flatten=False))
          # Test flattening and unholdering
          class TestHeldObj(mesonbuild.mesonlib.HoldableObject):
              def __init__(self, val: int) -> None:
                  self._val = val
          class MockInterpreter:
              def __init__(self) -> None:
                  self.subproject = ''
                  self.environment = None
          heldObj1 = TestHeldObj(1)
          holder1 = ObjectHolder(heldObj1, MockInterpreter())
          self.assertEqual([holder1], listify(holder1))
          self.assertEqual([holder1], listify([holder1]))
          self.assertEqual([holder1, 2], listify([holder1, 2]))
          self.assertEqual([holder1, 2, 3], listify([holder1, 2, [3]]))
  
      def test_extract_as_list(self):
          extract = mesonbuild.mesonlib.extract_as_list
          # Test sanity
          kwargs = {'sources': [1, 2, 3]}
          self.assertEqual([1, 2, 3], extract(kwargs, 'sources'))
          self.assertEqual(kwargs, {'sources': [1, 2, 3]})
          self.assertEqual([1, 2, 3], extract(kwargs, 'sources', pop=True))
          self.assertEqual(kwargs, {})
  
          class TestHeldObj(mesonbuild.mesonlib.HoldableObject):
              pass
          class MockInterpreter:
              def __init__(self) -> None:
                  self.subproject = ''
                  self.environment = None
          heldObj = TestHeldObj()
  
          # Test unholding
          holder3 = ObjectHolder(heldObj, MockInterpreter())
          kwargs = {'sources': [1, 2, holder3]}
          self.assertEqual(kwargs, {'sources': [1, 2, holder3]})
  
          # flatten nested lists
          kwargs = {'sources': [1, [2, [3]]]}
          self.assertEqual([1, 2, 3], extract(kwargs, 'sources'))
  
      def test_pkgconfig_module(self):
          dummystate = mock.Mock()
          dummystate.subproject = 'dummy'
          _mock = mock.Mock(spec=mesonbuild.dependencies.ExternalDependency)
          _mock.pcdep = mock.Mock()
          _mock.pcdep.name = "some_name"
          _mock.version_reqs = []
  
          # pkgconfig dependency as lib
          deps = mesonbuild.modules.pkgconfig.DependenciesHelper(dummystate, "thislib")
          deps.add_pub_libs([_mock])
          self.assertEqual(deps.format_reqs(deps.pub_reqs), "some_name")
  
          # pkgconfig dependency as requires
          deps = mesonbuild.modules.pkgconfig.DependenciesHelper(dummystate, "thislib")
          deps.add_pub_reqs([_mock])
          self.assertEqual(deps.format_reqs(deps.pub_reqs), "some_name")
  
      def _test_all_naming(self, cc, env, patterns, platform):
          shr = patterns[platform]['shared']
          stc = patterns[platform]['static']
          shrstc = shr + tuple([x for x in stc if x not in shr])
          stcshr = stc + tuple([x for x in shr if x not in stc])
          p = cc.get_library_naming(env, LibType.SHARED)
          self.assertEqual(p, shr)
          p = cc.get_library_naming(env, LibType.STATIC)
          self.assertEqual(p, stc)
          p = cc.get_library_naming(env, LibType.PREFER_STATIC)
          self.assertEqual(p, stcshr)
          p = cc.get_library_naming(env, LibType.PREFER_SHARED)
          self.assertEqual(p, shrstc)
          # Test find library by mocking up openbsd
          if platform != 'openbsd':
              return
          with tempfile.TemporaryDirectory() as tmpdir:
              for i in ['libfoo.so.6.0', 'libfoo.so.5.0', 'libfoo.so.54.0', 'libfoo.so.66a.0b', 'libfoo.so.70.0.so.1']:
                  libpath = Path(tmpdir) / i
                  libpath.write_text('', encoding='utf-8')
              found = cc._find_library_real('foo', env, [tmpdir], '', LibType.PREFER_SHARED)
              self.assertEqual(os.path.basename(found[0]), 'libfoo.so.54.0')
  
      def test_find_library_patterns(self):
          '''
          Unit test for the library search patterns used by find_library()
          '''
          unix_static = ('lib{}.a', '{}.a')
          msvc_static = ('lib{}.a', 'lib{}.lib', '{}.a', '{}.lib')
          # This is the priority list of pattern matching for library searching
          patterns = {'openbsd': {'shared': ('lib{}.so', '{}.so', 'lib{}.so.[0-9]*.[0-9]*', '{}.so.[0-9]*.[0-9]*'),
                                  'static': unix_static},
                      'linux': {'shared': ('lib{}.so', '{}.so'),
                                'static': unix_static},
                      'darwin': {'shared': ('lib{}.dylib', 'lib{}.so', '{}.dylib', '{}.so'),
                                 'static': unix_static},
                      'cygwin': {'shared': ('cyg{}.dll', 'cyg{}.dll.a', 'lib{}.dll',
                                            'lib{}.dll.a', '{}.dll', '{}.dll.a'),
                                 'static': ('cyg{}.a',) + unix_static},
                      'windows-msvc': {'shared': ('lib{}.lib', '{}.lib'),
                                       'static': msvc_static},
                      'windows-mingw': {'shared': ('lib{}.dll.a', 'lib{}.lib', 'lib{}.dll',
                                                   '{}.dll.a', '{}.lib', '{}.dll'),
                                        'static': msvc_static}}
          env = get_fake_env()
          cc = detect_c_compiler(env, MachineChoice.HOST)
          if is_osx():
              self._test_all_naming(cc, env, patterns, 'darwin')
          elif is_cygwin():
              self._test_all_naming(cc, env, patterns, 'cygwin')
          elif is_windows():
              if cc.get_argument_syntax() == 'msvc':
                  self._test_all_naming(cc, env, patterns, 'windows-msvc')
              else:
                  self._test_all_naming(cc, env, patterns, 'windows-mingw')
          elif is_openbsd():
              self._test_all_naming(cc, env, patterns, 'openbsd')
          else:
              self._test_all_naming(cc, env, patterns, 'linux')
              env.machines.host.system = 'openbsd'
              self._test_all_naming(cc, env, patterns, 'openbsd')
              env.machines.host.system = 'darwin'
              self._test_all_naming(cc, env, patterns, 'darwin')
              env.machines.host.system = 'cygwin'
              self._test_all_naming(cc, env, patterns, 'cygwin')
              env.machines.host.system = 'windows'
              self._test_all_naming(cc, env, patterns, 'windows-mingw')
  
      @skipIfNoPkgconfig
      def test_pkgconfig_parse_libs(self):
          '''
          Unit test for parsing of pkg-config output to search for libraries
  
          https://github.com/mesonbuild/meson/issues/3951
          '''
          def create_static_lib(name):
              if not is_osx():
                  name.open('w', encoding='utf-8').close()
                  return
              src = name.with_suffix('.c')
              out = name.with_suffix('.o')
              with src.open('w', encoding='utf-8') as f:
                  f.write('int meson_foobar (void) { return 0; }')
              subprocess.check_call(['clang', '-c', str(src), '-o', str(out)])
              subprocess.check_call(['ar', 'csr', str(name), str(out)])
  
          with tempfile.TemporaryDirectory() as tmpdir:
              pkgbin = ExternalProgram('pkg-config', command=['pkg-config'], silent=True)
              env = get_fake_env()
              compiler = detect_c_comp
  
  [... the rest is omitted ...]
files.old_path: run_unittests.py