files: #!/usr/bin/env python3
  #
  # Copyright (C) 2018 Linus Jahn <lnj@kaidan.im>
  # Copyright (C) 2019-2021 Hiroshi Miura <miurahr@linux.com>
  # Copyright (C) 2020, Aurélien Gâteau
  #
  # Permission is hereby granted, free of charge, to any person obtaining a copy of
  # this software and associated documentation files (the "Software"), to deal in
  # the Software without restriction, including without limitation the rights to
  # use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
  # the Software, and to permit persons to whom the Software is furnished to do so,
  # subject to the following conditions:
  #
  # The above copyright notice and this permission notice shall be included in all
  # copies or substantial portions of the Software.
  #
  # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
  # FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
  # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
  # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
  # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  
  import argparse
  import gc
  import multiprocessing
  import os
  import platform
  import posixpath
  import signal
  import subprocess
  import sys
  import time
  from logging import getLogger
  from logging.handlers import QueueHandler
  from pathlib import Path
  from tempfile import TemporaryDirectory
  from typing import List, Optional
  
  import aqt
  from aqt.archives import QtArchives, QtPackage, SrcDocExamplesArchives, ToolArchives
  from aqt.exceptions import (
      AqtException,
      ArchiveChecksumError,
      ArchiveDownloadError,
      ArchiveExtractionError,
      ArchiveListError,
      CliInputError,
      CliKeyboardInterrupt,
      OutOfMemory,
  )
  from aqt.helper import (
      MyQueueListener,
      Settings,
      downloadBinaryFile,
      get_hash,
      retry_on_bad_connection,
      retry_on_errors,
      setup_logging,
  )
  from aqt.metadata import ArchiveId, MetadataFactory, QtRepoProperty, SimpleSpec, Version, show_list, suggested_follow_up
  from aqt.updater import Updater
  
  try:
      import py7zr
  
      EXT7Z = False
  except ImportError:
      EXT7Z = True
  
  
  class Cli:
      """CLI main class to parse command line argument and launch proper functions."""
  
      __slot__ = ["parser", "combinations", "logger"]
  
      UNHANDLED_EXCEPTION_CODE = 254
  
      def __init__(self):
          parser = argparse.ArgumentParser(
              prog="aqt",
              description="Another unofficial Qt Installer.\naqt helps you install Qt SDK, tools, examples and others\n",
              formatter_class=argparse.RawTextHelpFormatter,
              add_help=True,
          )
          parser.add_argument(
              "-c",
              "--config",
              type=argparse.FileType("r"),
              help="Configuration ini file.",
          )
          subparsers = parser.add_subparsers(
              title="subcommands",
              description="aqt accepts several subcommands:\n"
              "install-* subcommands are commands that install components\n"
              "list-* subcommands are commands that show available components\n\n"
              "commands {install|tool|src|examples|doc} are deprecated and marked for removal\n",
              help="Please refer to each help message by using '--help' with each subcommand",
          )
          self._make_all_parsers(subparsers)
          parser.set_defaults(func=self.show_help)
          self.parser = parser
  
      def run(self, arg=None) -> int:
          args = self.parser.parse_args(arg)
          self._setup_settings(args)
          try:
              args.func(args)
              return 0
          except AqtException as e:
              self.logger.error(format(e), exc_info=Settings.print_stacktrace_on_error)
              if e.should_show_help:
                  self.show_help()
              return 1
          except Exception as e:
              # If we didn't account for it, and wrap it in an AqtException, it's a bug.
              self.logger.exception(e)  # Print stack trace
              self.logger.error(
                  f"{self._format_aqt_version()}\n"
                  f"Working dir: `{os.getcwd()}`\n"
                  f"Arguments: `{sys.argv}` Host: `{platform.uname()}`\n"
                  "===========================PLEASE FILE A BUG REPORT===========================\n"
                  "You have discovered a bug in aqt.\n"
                  "Please file a bug report at https://github.com/miurahr/aqtinstall/issues.\n"
                  "Please remember to include a copy of this program's output in your report."
              )
              return Cli.UNHANDLED_EXCEPTION_CODE
  
      def _check_tools_arg_combination(self, os_name, tool_name, arch):
          for c in Settings.tools_combinations:
              if c["os_name"] == os_name and c["tool_name"] == tool_name and c["arch"] == arch:
                  return True
          return False
  
      def _check_qt_arg_combination(self, qt_version, os_name, target, arch):
          for c in Settings.qt_combinations:
              if c["os_name"] == os_name and c["target"] == target and c["arch"] == arch:
                  return True
          return False
  
      def _check_qt_arg_versions(self, version):
          return version in Settings.available_versions
  
      def _check_qt_arg_version_offline(self, version):
          return version in Settings.available_offline_installer_version
  
      def _set_sevenzip(self, external):
          sevenzip = external
          if sevenzip is None:
              return None
  
          try:
              subprocess.run(
                  [sevenzip, "--help"],
                  stdout=subprocess.DEVNULL,
                  stderr=subprocess.DEVNULL,
              )
          except FileNotFoundError as e:
              raise CliInputError("Specified 7zip command executable does not exist: {!r}".format(sevenzip)) from e
  
          return sevenzip
  
      @staticmethod
      def _set_arch(arch: Optional[str], os_name: str, target: str, qt_version_or_spec: str) -> str:
          """Choose a default architecture, if one can be determined"""
          if arch is not None and arch != "":
              return arch
          if os_name == "linux" and target == "desktop":
              return "gcc_64"
          elif os_name == "mac" and target == "desktop":
              return "clang_64"
          elif os_name == "mac" and target == "ios":
              return "ios"
          elif target == "android":
              try:
                  if Version(qt_version_or_spec) >= Version("5.14.0"):
                      return "android"
              except ValueError:
                  pass
          raise CliInputError("Please supply a target architecture.", should_show_help=True)
  
      def _check_mirror(self, mirror):
          if mirror is None:
              pass
          elif mirror.startswith("http://") or mirror.startswith("https://") or mirror.startswith("ftp://"):
              pass
          else:
              return False
          return True
  
      def _check_modules_arg(self, qt_version, modules):
          if modules is None:
              return True
          available = Settings.available_modules(qt_version)
          if available is None:
              return False
          return all([m in available for m in modules])
  
      @staticmethod
      def _determine_qt_version(
          qt_version_or_spec: str, host: str, target: str, arch: str, base_url: str = Settings.baseurl
      ) -> Version:
          def choose_highest(x: Optional[Version], y: Optional[Version]) -> Optional[Version]:
              if x and y:
                  return max(x, y)
              return x or y
  
          def opt_version_for_spec(ext: str, _spec: SimpleSpec) -> Optional[Version]:
              try:
                  return MetadataFactory(ArchiveId("qt", host, target, ext), spec=_spec, base_url=base_url).getList().latest()
              except AqtException:
                  return None
  
          try:
              return Version(qt_version_or_spec)
          except ValueError:
              pass
          try:
              spec = SimpleSpec(qt_version_or_spec)
          except ValueError as e:
              raise CliInputError(f"Invalid version or SimpleSpec: '{qt_version_or_spec}'\n" + SimpleSpec.usage()) from e
          else:
              version: Optional[Version] = None
              for ext in QtRepoProperty.possible_extensions_for_arch(arch):
                  version = choose_highest(version, opt_version_for_spec(ext, spec))
              if not version:
                  raise CliInputError(
                      f"No versions of Qt exist for spec={spec} with host={host}, target={target}, arch={arch}"
                  )
              getLogger("aqt.installer").info(f"Resolved spec '{qt_version_or_spec}' to {version}")
              return version
  
      @staticmethod
      def choose_archive_dest(archive_dest: Optional[str], keep: bool, temp_dir: str) -> Path:
          """
          Choose archive download destination, based on context.
  
          There are three potential behaviors here:
          1. By default, return a temp directory that will be removed on program exit.
          2. If the user has asked to keep archives, but has not specified a destination,
              we return Settings.archive_download_location ("." by default).
          3. If the user has asked to keep archives and specified a destination,
              we create the destination dir if it doesn't exist, and return that directory.
          """
          if not archive_dest:
              return Path(Settings.archive_download_location if keep else temp_dir)
          dest = Path(archive_dest)
          dest.mkdir(parents=True, exist_ok=True)
          return dest
  
      def run_install_qt(self, args):
          """Run install subcommand"""
          start_time = time.perf_counter()
          self.show_aqt_version()
          if args.is_legacy:
              self._warn_on_deprecated_command("install", "install-qt")
          target: str = args.target
          os_name: str = args.host
          arch: str = self._set_arch(
              args.arch, os_name, target, getattr(args, "qt_version", getattr(args, "qt_version_spec", None))
          )
          keep: bool = args.keep or Settings.always_keep_archives
          archive_dest: Optional[str] = args.archive_dest
          output_dir = args.outputdir
          if output_dir is None:
              base_dir = os.getcwd()
          else:
              base_dir = output_dir
          if args.timeout is not None:
              timeout = (args.timeout, args.timeout)
          else:
              timeout = (Settings.connection_timeout, Settings.response_timeout)
          modules = args.modules
          sevenzip = self._set_sevenzip(args.external)
          if EXT7Z and sevenzip is None:
              # override when py7zr is not exist
              sevenzip = self._set_sevenzip("7z")
          if args.base is not None:
              if not self._check_mirror(args.base):
                  raise CliInputError(
                      "The `--base` option requires a url where the path `online/qtsdkrepository` exists.",
                      should_show_help=True,
                  )
              base = args.base
          else:
              base = Settings.baseurl
          if hasattr(args, "qt_version_spec"):
              qt_version: str = str(Cli._determine_qt_version(args.qt_version_spec, os_name, target, arch, base_url=base))
          else:
              qt_version: str = args.qt_version
              Cli._validate_version_str(qt_version)
          archives = args.archives
          if args.noarchives:
              if modules is None:
                  raise CliInputError("When `--noarchives` is set, the `--modules` option is mandatory.")
              if archives is not None:
                  raise CliInputError("Options `--archives` and `--noarchives` are mutually exclusive.")
          else:
              if modules is not None and archives is not None:
                  archives.append(modules)
          nopatch = args.noarchives or (archives is not None and "qtbase" not in archives)  # type: bool
          warn_on_missing_desktop_qt: bool = not args.autodesktop
          if not self._check_qt_arg_versions(qt_version):
              self.logger.warning("Specified Qt version is unknown: {}.".format(qt_version))
          if not self._check_qt_arg_combination(qt_version, os_name, target, arch):
              self.logger.warning(
                  "Specified target combination is not valid or unknown: {} {} {}".format(os_name, target, arch)
              )
          all_extra = True if modules is not None and "all" in modules else False
          if not all_extra and not self._check_modules_arg(qt_version, modules):
              self.logger.warning("Some of specified modules are unknown.")
  
          qt_archives = retry_on_bad_connection(
              lambda base_url: QtArchives(
                  os_name,
                  target,
                  qt_version,
                  arch,
                  base=base_url,
                  subarchives=archives,
                  modules=modules,
                  all_extra=all_extra,
                  is_include_base_package=not args.noarchives,
                  timeout=timeout,
              ),
              base,
          )
          target_config = qt_archives.get_target_config()
          with TemporaryDirectory() as temp_dir:
              _archive_dest = Cli.choose_archive_dest(archive_dest, keep, temp_dir)
              run_installer(qt_archives.get_packages(), base_dir, sevenzip, keep, _archive_dest)
          self._handle_missing_desktop_qt(os_name, target, Version(qt_version), Path(base_dir), warn_on_missing_desktop_qt)
          if not nopatch:
              Updater.update(target_config, base_dir)
          self.logger.info("Finished installation")
          self.logger.info("Time elapsed: {time:.8f} second".format(time=time.perf_counter() - start_time))
  
      def _run_src_doc_examples(self, flavor, args, cmd_name: Optional[str] = None):
          if not cmd_name:
              cmd_name = flavor
  
          self.show_aqt_version()
          if args.is_legacy:
              self._warn_on_deprecated_command(old_name=cmd_name, new_name=f"install-{cmd_name}")
          elif getattr(args, "target", None) is not None:
              self._warn_on_deprecated_parameter("target", args.target)
          target = "desktop"  # The only valid target for src/doc/examples is "desktop"
          os_name = args.host
          output_dir = args.outputdir
          if output_dir is None:
              base_dir = os.getcwd()
          else:
              base_dir = output_dir
          keep: bool = args.keep or Settings.always_keep_archives
          archive_dest: Optional[str] = args.archive_dest
          if args.base is not None:
              base = args.base
          else:
              base = Settings.baseurl
          if hasattr(args, "qt_version_spec"):
              qt_version = str(Cli._determine_qt_version(args.qt_version_spec, os_name, target, arch="", base_url=base))
          else:
              qt_version = args.qt_version
              Cli._validate_version_str(qt_version)
          if args.timeout is not None:
              timeout = (args.timeout, args.timeout)
          else:
              timeout = (Settings.connection_timeout, Settings.response_timeout)
          sevenzip = self._set_sevenzip(args.external)
          if EXT7Z and sevenzip is None:
              # override when py7zr is not exist
              sevenzip = self._set_sevenzip(Settings.zipcmd)
          modules = getattr(args, "modules", None)  # `--modules` is invalid for `install-src`
          archives = args.archives
          all_extra = True if modules is not None and "all" in modules else False
          if not self._check_qt_arg_versions(qt_version):
              self.logger.warning("Specified Qt version is unknown: {}.".format(qt_version))
  
          srcdocexamples_archives: SrcDocExamplesArchives = retry_on_bad_connection(
              lambda base_url: SrcDocExamplesArchives(
                  flavor,
                  os_name,
                  target,
                  qt_version,
                  base=base_url,
                  subarchives=archives,
                  modules=modules,
                  all_extra=all_extra,
                  timeout=timeout,
              ),
              base,
          )
          with TemporaryDirectory() as temp_dir:
              _archive_dest = Cli.choose_archive_dest(archive_dest, keep, temp_dir)
              run_installer(srcdocexamples_archives.get_packages(), base_dir, sevenzip, keep, _archive_dest)
          self.logger.info("Finished installation")
  
      def run_install_src(self, args):
          """Run src subcommand"""
          if not hasattr(args, "qt_version"):
              base = args.base if hasattr(args, "base") else Settings.baseurl
              args.qt_version = str(
                  Cli._determine_qt_version(args.qt_version_spec, args.host, args.target, arch="", base_url=base)
              )
          if args.kde and args.qt_version != "5.15.2":
              raise CliInputError("KDE patch: unsupported version!!")
          start_time = time.perf_counter()
          self._run_src_doc_examples("src", args)
          if args.kde:
              if args.outputdir is None:
                  target_dir = os.path.join(os.getcwd(), args.qt_version, "Src")
              else:
                  target_dir = os.path.join(args.outputdir, args.qt_version, "Src")
              Updater.patch_kde(target_dir)
          self.logger.info("Time elapsed: {time:.8f} second".format(time=time.perf_counter() - start_time))
  
      def run_install_example(self, args):
          """Run example subcommand"""
          start_time = time.perf_counter()
          self._run_src_doc_examples("examples", args, cmd_name="example")
          self.logger.info("Time elapsed: {time:.8f} second".format(time=time.perf_counter() - start_time))
  
      def run_install_doc(self, args):
          """Run doc subcommand"""
          start_time = time.perf_counter()
          self._run_src_doc_examples("doc", args)
          self.logger.info("Time elapsed: {time:.8f} second".format(time=time.perf_counter() - start_time))
  
      def run_install_tool(self, args):
          """Run tool subcommand"""
          start_time = time.perf_counter()
          self.show_aqt_version()
          if args.is_legacy:
              self._warn_on_deprecated_command("tool", "install-tool")
          tool_name = args.tool_name  # such as tools_openssl_x64
          os_name = args.host  # windows, linux and mac
          target = "desktop" if args.is_legacy else args.target  # desktop, android and ios
          output_dir = args.outputdir
          if output_dir is None:
              base_dir = os.getcwd()
          else:
              base_dir = output_dir
          sevenzip = self._set_sevenzip(args.external)
          if EXT7Z and sevenzip is None:
              # override when py7zr is not exist
              sevenzip = self._set_sevenzip(Settings.zipcmd)
          version = getattr(args, "version", None)
          if version is not None:
              Cli._validate_version_str(version, allow_minus=True)
          keep: bool = args.keep or Settings.always_keep_archives
          archive_dest: Optional[str] = args.archive_dest
          if args.base is not None:
              base = args.base
          else:
              base = Settings.baseurl
          if args.timeout is not None:
              timeout = (args.timeout, args.timeout)
          else:
              timeout = (Settings.connection_timeout, Settings.response_timeout)
          if args.tool_variant is None:
              archive_id = ArchiveId("tools", os_name, target, "")
              meta = MetadataFactory(archive_id, base_url=base, is_latest_version=True, tool_name=tool_name)
              try:
                  archs = meta.getList()
              except ArchiveDownloadError as e:
                  msg = f"Failed to locate XML data for the tool '{tool_name}'."
                  raise ArchiveListError(msg, suggested_action=suggested_follow_up(meta)) from e
  
          else:
              archs = [args.tool_variant]
  
          for arch in archs:
              if not self._check_tools_arg_combination(os_name, tool_name, arch):
                  self.logger.warning("Specified target combination is not valid: {} {} {}".format(os_name, tool_name, arch))
  
              tool_archives: ToolArchives = retry_on_bad_connection(
                  lambda base_url: ToolArchives(
                      os_name=os_name,
                      tool_name=tool_name,
                      target=target,
                      base=base_url,
                      version_str=version,
                      arch=arch,
                      timeout=timeout,
                  ),
                  base,
              )
              with TemporaryDirectory() as temp_dir:
                  _archive_dest = Cli.choose_archive_dest(archive_dest, keep, temp_dir)
                  run_installer(tool_archives.get_packages(), base_dir, sevenzip, keep, _archive_dest)
          self.logger.info("Finished installation")
          self.logger.info("Time elapsed: {time:.8f} second".format(time=time.perf_counter() - start_time))
  
      def run_list_qt(self, args: argparse.ArgumentParser):
          """Print versions of Qt, extensions, modules, architectures"""
  
          if not args.target:
              print(" ".join(ArchiveId.TARGETS_FOR_HOST[args.host]))
              return
          if args.target not in ArchiveId.TARGETS_FOR_HOST[args.host]:
              raise CliInputError("'{0.target}' is not a valid target for host '{0.host}'".format(args))
          if args.modules:
              modules_ver, modules_query, is_long = args.modules[0], tuple(args.modules), False
          elif args.long_modules:
              modules_ver, modules_query, is_long = args.long_modules[0], tuple(args.long_modules), True
          else:
              modules_ver, modules_query, is_long = None, None, False
  
          for version_str in (modules_ver, args.extensions, args.arch, args.archives[0] if args.archives else None):
              Cli._validate_version_str(version_str, allow_latest=True, allow_empty=True)
  
          spec = None
          try:
              if args.spec is not None:
                  spec = SimpleSpec(args.spec)
          except ValueError as e:
              raise CliInputError(f"Invalid version specification: '{args.spec}'.\n" + SimpleSpec.usage()) from e
  
          meta = MetadataFactory(
              archive_id=ArchiveId(
                  "qt",
                  args.host,
                  args.target,
                  args.extension if args.extension else "",
              ),
              spec=spec,
              is_latest_version=args.latest_version,
              modules_query=modules_query,
              is_long_listing=is_long,
              extensions_ver=args.extensions,
              architectures_ver=args.arch,
              archives_query=args.archives,
          )
          show_list(meta)
  
      def run_list_tool(self, args: argparse.ArgumentParser):
          """Print tools"""
  
          if not args.target:
              print(" ".join(ArchiveId.TARGETS_FOR_HOST[args.host]))
              return
          if args.target not in ArchiveId.TARGETS_FOR_HOST[args.host]:
              raise CliInputError("'{0.target}' is not a valid target for host '{0.host}'".format(args))
  
          meta = MetadataFactory(
              archive_id=ArchiveId("tools", args.host, args.target),
              tool_name=args.tool_name,
              is_long_listing=args.long,
          )
          show_list(meta)
  
      def run_list_src_doc_examples(self, args: argparse.ArgumentParser, cmd_type: str):
          target = "desktop"  # The only valid target for src/doc/examples is "desktop"
          version = Cli._determine_qt_version(args.qt_version_spec, args.host, target, arch="")
          is_fetch_modules: bool = getattr(args, "modules", False)
          meta = MetadataFactory(
              archive_id=ArchiveId("qt", args.host, target, "src_doc_examples"),
              src_doc_examples_query=(cmd_type, version, is_fetch_modules),
          )
          show_list(meta)
  
      def show_help(self, args=None):
          """Display help message"""
          self.parser.print_help()
  
      def _format_aqt_version(self) -> str:
          py_version = platform.python_version()
          py_impl = platform.python_implementation()
          py_build = platform.python_compiler()
          return f"aqtinstall(aqt) v{aqt.__version__} on Python {py_version} [{py_impl} {py_build}]"
  
      def show_aqt_version(self, args=None):
          """Display version information"""
          self.logger.info(self._format_aqt_version())
  
      def _set_install_qt_parser(self, install_qt_parser, *, is_legacy: bool):
          install_qt_parser.set_defaults(func=self.run_install_qt, is_legacy=is_legacy)
          self._set_common_arguments(install_qt_parser, is_legacy=is_legacy)
          self._set_common_options(install_qt_parser)
          install_qt_parser.add_argument(
              "arch",
              nargs="?",
              help="\ntarget linux/desktop: gcc_64, wasm_32"
              "\ntarget mac/desktop:   clang_64, wasm_32"
              "\ntarget mac/ios:       ios"
              "\nwindows/desktop:      win64_msvc2019_64, win32_msvc2019"
              "\n                      win64_msvc2017_64, win32_msvc2017"
              "\n                      win64_msvc2015_64, win32_msvc2015"
              "\n                      win64_mingw81, win32_mingw81"
              "\n                      win64_mingw73, win32_mingw73"
              "\n                      win32_mingw53"
              "\n                      wasm_32"
              "\nwindows/winrt:        win64_msvc2019_winrt_x64, win64_msvc2019_winrt_x86"
              "\n                      win64_msvc2017_winrt_x64, win64_msvc2017_winrt_x86"
              "\n                      win64_msvc2019_winrt_armv7"
              "\n                      win64_msvc2017_winrt_armv7"
              "\nandroid:              Qt 5.14:          android (optional)"
              "\n                      Qt 5.13 or below: android_x86_64, android_arm64_v8a"
              "\n                                        android_x86, android_armv7",
          )
          self._set_module_options(install_qt_parser)
          self._set_archive_options(install_qt_parser)
          install_qt_parser.add_argument(
              "--noarchives",
              action="store_true",
              help="No base packages; allow mod amendment with --modules option.",
          )
          install_qt_parser.add_argument(
              "--autodesktop",
              action="store_true",
              help="For android/ios installations, a desktop Qt installation is required. "
              "When enabled, this option installs the required desktop version automatically.",
          )
  
      def _set_install_tool_parser(self, install_tool_parser, *, is_legacy: bool):
          install_tool_parser.set_defaults(func=self.run_install_tool, is_legacy=is_legacy)
          install_tool_parser.add_argument("host", choices=["linux", "mac", "windows"], help="host os name")
          if not is_legacy:
              install_tool_parser.add_argument(
                  "target",
                  default=None,
                  choices=["desktop", "winrt", "android", "ios"],
                  help="Target SDK.",
              )
          install_tool_parser.add_argument("tool_name", help="Name of tool such as tools_ifw, tools_mingw")
          if is_legacy:
              install_tool_parser.add_argument("version", help="Version of tool variant")
  
          tool_variant_opts = {} if is_legacy else {"nargs": "?", "default": None}
          install_tool_parser.add_argument(
              "tool_variant",
              **tool_variant_opts,
              help="Name of tool variant, such as qt.tools.ifw.41. "
              "Please use 'aqt list-tool' to list acceptable values for this parameter.",
          )
          self._set_common_options(install_tool_parser)
  
      def _warn_on_deprecated_command(self, old_name: str, new_name: str):
          self.logger.warning(
              f"The command '{old_name}' is deprecated and marked for removal in a future version of aqt.\n"
              f"In the future, please use the command '{new_name}' instead."
          )
  
      def _warn_on_deprecated_parameter(self, parameter_name: str, value: str):
          self.logger.warning(
              f"The parameter '{parameter_name}' with value '{value}' is deprecated and marked for "
              f"removal in a future version of aqt.\n"
              f"In the future, please omit this parameter."
          )
  
      @staticmethod
      def _get_missing_desktop_arch(host: str, target: str, version: Version, base_dir: Path) -> Optional[str]:
          """
          For mobile Qt installations, the desktop version of Qt is a dependency.
          If the desktop version is not installed, this function returns the architecture that should be installed.
          If no desktop Qt is required, or it is already installed, this function returns None.
          """
          if target not in ["ios", "android"]:
              return None
          if host != "windows":
              arch = aqt.updater.default_desktop_arch_dir(host, version)
              expected_qmake = base_dir / format(version) / arch / "bin/qmake"
              return arch if not expected_qmake.is_file() else None
          else:
              existing_desktop_qt = QtRepoProperty.find_installed_qt_mingw_dir(base_dir / format(version))
              if existing_desktop_qt:
                  return None
              return MetadataFactory(ArchiveId("qt", host, "desktop")).fetch_default_desktop_arch(version)
  
      def _handle_missing_desktop_qt(self, host: str, target: str, version: Version, base_dir: Path, should_warn: bool):
          missing_desktop_arch = Cli._get_missing_desktop_arch(host, target, version, base_dir)
          if not missing_desktop_arch:
              return
  
          msg_prefix = (
              f"You are installing the {target} version of Qt, which requires that the desktop version of Qt "
              f"is also installed."
          )
          if should_warn:
              self.logger.warning(
                  f"{msg_prefix} You can install it with the following command:\n"
                  f"          `aqt install-qt {host} desktop {version} {missing_desktop_arch}`"
              )
          else:
              self.logger.info(f"{msg_prefix} Now installing Qt: desktop {version} {missing_desktop_arch}")
              self.run(["install-qt", host, "desktop", format(version), missing_desktop_arch])
  
      def _make_all_parsers(self, subparsers: argparse._SubParsersAction):
          deprecated_msg = "This command is deprecated and marked for removal in a future version of aqt."
  
          def make_parser_it(cmd: str, desc: str, is_legacy: bool, set_parser_cmd, formatter_class):
              description = f"{desc} {deprecated_msg}" if is_legacy else desc
              kwargs = {"formatter_class": formatter_class} if formatter_class else {}
              p = subparsers.add_parser(cmd, description=description, **kwargs)
              set_parser_cmd(p, is_legacy=is_legacy)
  
          def make_parser_sde(cmd: str, desc: str, is_legacy: bool, action, is_add_kde: bool, is_add_modules: bool = True):
              description = f"{desc} {deprecated_msg}" if is_legacy else desc
              parser = subparsers.add_parser(cmd, description=description)
              parser.set_defaults(func=action, is_legacy=is_legacy)
              self._set_common_arguments(parser, is_legacy=is_legacy, is_target_deprecated=True)
              self._set_common_options(parser)
              if is_add_modules:
                  self._set_module_options(parser)
              self._set_archive_options(parser)
              if is_add_kde:
                  parser.add_argument("--kde", action="store_true", help="patching with KDE patch kit.")
  
          def make_parser_list_sde(cmd: str, desc: str, cmd_type: str):
              parser = subparsers.add_parser(cmd, description=desc)
              parser.add_argument("host", choices=["linux", "mac", "windows"], help="host os name")
              parser.add_argument(
                  "qt_version_spec",
                  metavar="(VERSION | SPECIFICATION)",
                  help='Qt version in the format of "5.X.Y" or SimpleSpec like "5.X" or "<6.X"',
              )
              parser.set_defaults(func=lambda args: self.run_list_src_doc_examples(args, cmd_type))
  
              if cmd_type != "src":
                  parser.add_argument("-m", "--modules", action="store_true", help="Print list of available modules")
  
          make_parser_it("install-qt", "Install Qt.", False, self._set_install_qt_parser, argparse.RawTextHelpFormatter)
          make_parser_it("install-tool", "Install tools.", False, self._set_install_tool_parser, None)
          make_parser_sde("install-doc", "Install documentation.", False, self.run_install_doc, False)
          make_parser_sde("install-example", "Install examples.", False, self.run_install_example, False)
          make_parser_sde("install-src", "Install source.", False, self.run_install_src, True, is_add_modules=False)
  
          self._make_list_qt_parser(subparsers)
          self._make_list_tool_parser(subparsers)
          make_parser_list_sde("list-doc", "List documentation archives available (use with install-doc)", "doc")
          make_parser_list_sde("list-example", "List example archives available (use with install-example)", "examples")
          make_parser_list_sde("list-src", "List source archives available (use with install-src)", "src")
  
         
  
  [... the rest is omitted ...]
files: aqt/installer.py
files: #!/usr/bin/env python3
  #
  # Copyright (C) 2018 Linus Jahn <lnj@kaidan.im>
  # Copyright (C) 2019-2021 Hiroshi Miura <miurahr@linux.com>
  # Copyright (C) 2020, Aurélien Gâteau
  #
  # Permission is hereby granted, free of charge, to any person obtaining a copy of
  # this software and associated documentation files (the "Software"), to deal in
  # the Software without restriction, including without limitation the rights to
  # use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
  # the Software, and to permit persons to whom the Software is furnished to do so,
  # subject to the following conditions:
  #
  # The above copyright notice and this permission notice shall be included in all
  # copies or substantial portions of the Software.
  #
  # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
  # FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
  # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
  # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
  # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  
  import argparse
  import gc
  import multiprocessing
  import os
  import platform
  import posixpath
  import signal
  import subprocess
  import sys
  import time
  from logging import getLogger
  from logging.handlers import QueueHandler
  from pathlib import Path
  from tempfile import TemporaryDirectory
  from typing import List, Optional
  
  import aqt
  from aqt.archives import QtArchives, QtPackage, SrcDocExamplesArchives, ToolArchives
  from aqt.exceptions import (
      AqtException,
      ArchiveChecksumError,
      ArchiveDownloadError,
      ArchiveExtractionError,
      ArchiveListError,
      CliInputError,
      CliKeyboardInterrupt,
      OutOfMemory,
  )
  from aqt.helper import (
      MyQueueListener,
      Settings,
      downloadBinaryFile,
      get_hash,
      retry_on_bad_connection,
      retry_on_errors,
      setup_logging,
  )
  from aqt.metadata import ArchiveId, MetadataFactory, QtRepoProperty, SimpleSpec, Version, show_list, suggested_follow_up
  from aqt.updater import Updater
  
  try:
      import py7zr
  
      EXT7Z = False
  except ImportError:
      EXT7Z = True
  
  
  class Cli:
      """CLI main class to parse command line argument and launch proper functions."""
  
      __slot__ = ["parser", "combinations", "logger"]
  
      UNHANDLED_EXCEPTION_CODE = 254
  
      def __init__(self):
          parser = argparse.ArgumentParser(
              prog="aqt",
              description="Another unofficial Qt Installer.\naqt helps you install Qt SDK, tools, examples and others\n",
              formatter_class=argparse.RawTextHelpFormatter,
              add_help=True,
          )
          parser.add_argument(
              "-c",
              "--config",
              type=argparse.FileType("r"),
              help="Configuration ini file.",
          )
          subparsers = parser.add_subparsers(
              title="subcommands",
              description="aqt accepts several subcommands:\n"
              "install-* subcommands are commands that install components\n"
              "list-* subcommands are commands that show available components\n\n"
              "commands {install|tool|src|examples|doc} are deprecated and marked for removal\n",
              help="Please refer to each help message by using '--help' with each subcommand",
          )
          self._make_all_parsers(subparsers)
          parser.set_defaults(func=self.show_help)
          self.parser = parser
  
      def run(self, arg=None) -> int:
          args = self.parser.parse_args(arg)
          self._setup_settings(args)
          try:
              args.func(args)
              return 0
          except AqtException as e:
              self.logger.error(format(e), exc_info=Settings.print_stacktrace_on_error)
              if e.should_show_help:
                  self.show_help()
              return 1
          except Exception as e:
              # If we didn't account for it, and wrap it in an AqtException, it's a bug.
              self.logger.exception(e)  # Print stack trace
              self.logger.error(
                  f"{self._format_aqt_version()}\n"
                  f"Working dir: `{os.getcwd()}`\n"
                  f"Arguments: `{sys.argv}` Host: `{platform.uname()}`\n"
                  "===========================PLEASE FILE A BUG REPORT===========================\n"
                  "You have discovered a bug in aqt.\n"
                  "Please file a bug report at https://github.com/miurahr/aqtinstall/issues.\n"
                  "Please remember to include a copy of this program's output in your report."
              )
              return Cli.UNHANDLED_EXCEPTION_CODE
  
      def _check_tools_arg_combination(self, os_name, tool_name, arch):
          for c in Settings.tools_combinations:
              if c["os_name"] == os_name and c["tool_name"] == tool_name and c["arch"] == arch:
                  return True
          return False
  
      def _check_qt_arg_combination(self, qt_version, os_name, target, arch):
          for c in Settings.qt_combinations:
              if c["os_name"] == os_name and c["target"] == target and c["arch"] == arch:
                  return True
          return False
  
      def _check_qt_arg_versions(self, version):
          return version in Settings.available_versions
  
      def _check_qt_arg_version_offline(self, version):
          return version in Settings.available_offline_installer_version
  
      def _set_sevenzip(self, external):
          sevenzip = external
          if sevenzip is None:
              return None
  
          try:
              subprocess.run(
                  [sevenzip, "--help"],
                  stdout=subprocess.DEVNULL,
                  stderr=subprocess.DEVNULL,
              )
          except FileNotFoundError as e:
              raise CliInputError("Specified 7zip command executable does not exist: {!r}".format(sevenzip)) from e
  
          return sevenzip
  
      @staticmethod
      def _set_arch(arch: Optional[str], os_name: str, target: str, qt_version_or_spec: str) -> str:
          """Choose a default architecture, if one can be determined"""
          if arch is not None and arch != "":
              return arch
          if os_name == "linux" and target == "desktop":
              return "gcc_64"
          elif os_name == "mac" and target == "desktop":
              return "clang_64"
          elif os_name == "mac" and target == "ios":
              return "ios"
          elif target == "android":
              try:
                  if Version(qt_version_or_spec) >= Version("5.14.0"):
                      return "android"
              except ValueError:
                  pass
          raise CliInputError("Please supply a target architecture.", should_show_help=True)
  
      def _check_mirror(self, mirror):
          if mirror is None:
              pass
          elif mirror.startswith("http://") or mirror.startswith("https://") or mirror.startswith("ftp://"):
              pass
          else:
              return False
          return True
  
      def _check_modules_arg(self, qt_version, modules):
          if modules is None:
              return True
          available = Settings.available_modules(qt_version)
          if available is None:
              return False
          return all([m in available for m in modules])
  
      @staticmethod
      def _determine_qt_version(
          qt_version_or_spec: str, host: str, target: str, arch: str, base_url: str = Settings.baseurl
      ) -> Version:
          def choose_highest(x: Optional[Version], y: Optional[Version]) -> Optional[Version]:
              if x and y:
                  return max(x, y)
              return x or y
  
          def opt_version_for_spec(ext: str, _spec: SimpleSpec) -> Optional[Version]:
              try:
                  return MetadataFactory(ArchiveId("qt", host, target, ext), spec=_spec, base_url=base_url).getList().latest()
              except AqtException:
                  return None
  
          try:
              return Version(qt_version_or_spec)
          except ValueError:
              pass
          try:
              spec = SimpleSpec(qt_version_or_spec)
          except ValueError as e:
              raise CliInputError(f"Invalid version or SimpleSpec: '{qt_version_or_spec}'\n" + SimpleSpec.usage()) from e
          else:
              version: Optional[Version] = None
              for ext in QtRepoProperty.possible_extensions_for_arch(arch):
                  version = choose_highest(version, opt_version_for_spec(ext, spec))
              if not version:
                  raise CliInputError(
                      f"No versions of Qt exist for spec={spec} with host={host}, target={target}, arch={arch}"
                  )
              getLogger("aqt.installer").info(f"Resolved spec '{qt_version_or_spec}' to {version}")
              return version
  
      @staticmethod
      def choose_archive_dest(archive_dest: Optional[str], keep: bool, temp_dir: str) -> Path:
          """
          Choose archive download destination, based on context.
  
          There are three potential behaviors here:
          1. By default, return a temp directory that will be removed on program exit.
          2. If the user has asked to keep archives, but has not specified a destination,
              we return Settings.archive_download_location ("." by default).
          3. If the user has asked to keep archives and specified a destination,
              we create the destination dir if it doesn't exist, and return that directory.
          """
          if not archive_dest:
              return Path(Settings.archive_download_location if keep else temp_dir)
          dest = Path(archive_dest)
          dest.mkdir(parents=True, exist_ok=True)
          return dest
  
      def run_install_qt(self, args):
          """Run install subcommand"""
          start_time = time.perf_counter()
          self.show_aqt_version()
          if args.is_legacy:
              self._warn_on_deprecated_command("install", "install-qt")
          target: str = args.target
          os_name: str = args.host
          arch: str = self._set_arch(
              args.arch, os_name, target, getattr(args, "qt_version", getattr(args, "qt_version_spec", None))
          )
          keep: bool = args.keep or Settings.always_keep_archives
          archive_dest: Optional[str] = args.archive_dest
          output_dir = args.outputdir
          if output_dir is None:
              base_dir = os.getcwd()
          else:
              base_dir = output_dir
          if args.timeout is not None:
              timeout = (args.timeout, args.timeout)
          else:
              timeout = (Settings.connection_timeout, Settings.response_timeout)
          modules = args.modules
          sevenzip = self._set_sevenzip(args.external)
          if EXT7Z and sevenzip is None:
              # override when py7zr is not exist
              sevenzip = self._set_sevenzip("7z")
          if args.base is not None:
              if not self._check_mirror(args.base):
                  raise CliInputError(
                      "The `--base` option requires a url where the path `online/qtsdkrepository` exists.",
                      should_show_help=True,
                  )
              base = args.base
          else:
              base = Settings.baseurl
          if hasattr(args, "qt_version_spec"):
              qt_version: str = str(Cli._determine_qt_version(args.qt_version_spec, os_name, target, arch, base_url=base))
          else:
              qt_version: str = args.qt_version
              Cli._validate_version_str(qt_version)
          archives = args.archives
          if args.noarchives:
              if modules is None:
                  raise CliInputError("When `--noarchives` is set, the `--modules` option is mandatory.")
              if archives is not None:
                  raise CliInputError("Options `--archives` and `--noarchives` are mutually exclusive.")
          else:
              if modules is not None and archives is not None:
                  archives.append(modules)
          nopatch = args.noarchives or (archives is not None and "qtbase" not in archives)  # type: bool
          if not self._check_qt_arg_versions(qt_version):
              self.logger.warning("Specified Qt version is unknown: {}.".format(qt_version))
          if not self._check_qt_arg_combination(qt_version, os_name, target, arch):
              self.logger.warning(
                  "Specified target combination is not valid or unknown: {} {} {}".format(os_name, target, arch)
              )
          all_extra = True if modules is not None and "all" in modules else False
          if not all_extra and not self._check_modules_arg(qt_version, modules):
              self.logger.warning("Some of specified modules are unknown.")
  
          qt_archives = retry_on_bad_connection(
              lambda base_url: QtArchives(
                  os_name,
                  target,
                  qt_version,
                  arch,
                  base=base_url,
                  subarchives=archives,
                  modules=modules,
                  all_extra=all_extra,
                  is_include_base_package=not args.noarchives,
                  timeout=timeout,
              ),
              base,
          )
          target_config = qt_archives.get_target_config()
          with TemporaryDirectory() as temp_dir:
              _archive_dest = Cli.choose_archive_dest(archive_dest, keep, temp_dir)
              run_installer(qt_archives.get_packages(), base_dir, sevenzip, keep, _archive_dest)
          if not nopatch:
              Updater.update(target_config, base_dir)
          self.logger.info("Finished installation")
          self.logger.info("Time elapsed: {time:.8f} second".format(time=time.perf_counter() - start_time))
  
      def _run_src_doc_examples(self, flavor, args, cmd_name: Optional[str] = None):
          if not cmd_name:
              cmd_name = flavor
  
          self.show_aqt_version()
          if args.is_legacy:
              self._warn_on_deprecated_command(old_name=cmd_name, new_name=f"install-{cmd_name}")
          elif getattr(args, "target", None) is not None:
              self._warn_on_deprecated_parameter("target", args.target)
          target = "desktop"  # The only valid target for src/doc/examples is "desktop"
          os_name = args.host
          output_dir = args.outputdir
          if output_dir is None:
              base_dir = os.getcwd()
          else:
              base_dir = output_dir
          keep: bool = args.keep or Settings.always_keep_archives
          archive_dest: Optional[str] = args.archive_dest
          if args.base is not None:
              base = args.base
          else:
              base = Settings.baseurl
          if hasattr(args, "qt_version_spec"):
              qt_version = str(Cli._determine_qt_version(args.qt_version_spec, os_name, target, arch="", base_url=base))
          else:
              qt_version = args.qt_version
              Cli._validate_version_str(qt_version)
          if args.timeout is not None:
              timeout = (args.timeout, args.timeout)
          else:
              timeout = (Settings.connection_timeout, Settings.response_timeout)
          sevenzip = self._set_sevenzip(args.external)
          if EXT7Z and sevenzip is None:
              # override when py7zr is not exist
              sevenzip = self._set_sevenzip(Settings.zipcmd)
          modules = getattr(args, "modules", None)  # `--modules` is invalid for `install-src`
          archives = args.archives
          all_extra = True if modules is not None and "all" in modules else False
          if not self._check_qt_arg_versions(qt_version):
              self.logger.warning("Specified Qt version is unknown: {}.".format(qt_version))
  
          srcdocexamples_archives: SrcDocExamplesArchives = retry_on_bad_connection(
              lambda base_url: SrcDocExamplesArchives(
                  flavor,
                  os_name,
                  target,
                  qt_version,
                  base=base_url,
                  subarchives=archives,
                  modules=modules,
                  all_extra=all_extra,
                  timeout=timeout,
              ),
              base,
          )
          with TemporaryDirectory() as temp_dir:
              _archive_dest = Cli.choose_archive_dest(archive_dest, keep, temp_dir)
              run_installer(srcdocexamples_archives.get_packages(), base_dir, sevenzip, keep, _archive_dest)
          self.logger.info("Finished installation")
  
      def run_install_src(self, args):
          """Run src subcommand"""
          if not hasattr(args, "qt_version"):
              base = args.base if hasattr(args, "base") else Settings.baseurl
              args.qt_version = str(
                  Cli._determine_qt_version(args.qt_version_spec, args.host, args.target, arch="", base_url=base)
              )
          if args.kde and args.qt_version != "5.15.2":
              raise CliInputError("KDE patch: unsupported version!!")
          start_time = time.perf_counter()
          self._run_src_doc_examples("src", args)
          if args.kde:
              if args.outputdir is None:
                  target_dir = os.path.join(os.getcwd(), args.qt_version, "Src")
              else:
                  target_dir = os.path.join(args.outputdir, args.qt_version, "Src")
              Updater.patch_kde(target_dir)
          self.logger.info("Time elapsed: {time:.8f} second".format(time=time.perf_counter() - start_time))
  
      def run_install_example(self, args):
          """Run example subcommand"""
          start_time = time.perf_counter()
          self._run_src_doc_examples("examples", args, cmd_name="example")
          self.logger.info("Time elapsed: {time:.8f} second".format(time=time.perf_counter() - start_time))
  
      def run_install_doc(self, args):
          """Run doc subcommand"""
          start_time = time.perf_counter()
          self._run_src_doc_examples("doc", args)
          self.logger.info("Time elapsed: {time:.8f} second".format(time=time.perf_counter() - start_time))
  
      def run_install_tool(self, args):
          """Run tool subcommand"""
          start_time = time.perf_counter()
          self.show_aqt_version()
          if args.is_legacy:
              self._warn_on_deprecated_command("tool", "install-tool")
          tool_name = args.tool_name  # such as tools_openssl_x64
          os_name = args.host  # windows, linux and mac
          target = "desktop" if args.is_legacy else args.target  # desktop, android and ios
          output_dir = args.outputdir
          if output_dir is None:
              base_dir = os.getcwd()
          else:
              base_dir = output_dir
          sevenzip = self._set_sevenzip(args.external)
          if EXT7Z and sevenzip is None:
              # override when py7zr is not exist
              sevenzip = self._set_sevenzip(Settings.zipcmd)
          version = getattr(args, "version", None)
          if version is not None:
              Cli._validate_version_str(version, allow_minus=True)
          keep: bool = args.keep or Settings.always_keep_archives
          archive_dest: Optional[str] = args.archive_dest
          if args.base is not None:
              base = args.base
          else:
              base = Settings.baseurl
          if args.timeout is not None:
              timeout = (args.timeout, args.timeout)
          else:
              timeout = (Settings.connection_timeout, Settings.response_timeout)
          if args.tool_variant is None:
              archive_id = ArchiveId("tools", os_name, target, "")
              meta = MetadataFactory(archive_id, base_url=base, is_latest_version=True, tool_name=tool_name)
              try:
                  archs = meta.getList()
              except ArchiveDownloadError as e:
                  msg = f"Failed to locate XML data for the tool '{tool_name}'."
                  raise ArchiveListError(msg, suggested_action=suggested_follow_up(meta)) from e
  
          else:
              archs = [args.tool_variant]
  
          for arch in archs:
              if not self._check_tools_arg_combination(os_name, tool_name, arch):
                  self.logger.warning("Specified target combination is not valid: {} {} {}".format(os_name, tool_name, arch))
  
              tool_archives: ToolArchives = retry_on_bad_connection(
                  lambda base_url: ToolArchives(
                      os_name=os_name,
                      tool_name=tool_name,
                      target=target,
                      base=base_url,
                      version_str=version,
                      arch=arch,
                      timeout=timeout,
                  ),
                  base,
              )
              with TemporaryDirectory() as temp_dir:
                  _archive_dest = Cli.choose_archive_dest(archive_dest, keep, temp_dir)
                  run_installer(tool_archives.get_packages(), base_dir, sevenzip, keep, _archive_dest)
          self.logger.info("Finished installation")
          self.logger.info("Time elapsed: {time:.8f} second".format(time=time.perf_counter() - start_time))
  
      def run_list_qt(self, args: argparse.ArgumentParser):
          """Print versions of Qt, extensions, modules, architectures"""
  
          if not args.target:
              print(" ".join(ArchiveId.TARGETS_FOR_HOST[args.host]))
              return
          if args.target not in ArchiveId.TARGETS_FOR_HOST[args.host]:
              raise CliInputError("'{0.target}' is not a valid target for host '{0.host}'".format(args))
          if args.modules:
              modules_ver, modules_query, is_long = args.modules[0], tuple(args.modules), False
          elif args.long_modules:
              modules_ver, modules_query, is_long = args.long_modules[0], tuple(args.long_modules), True
          else:
              modules_ver, modules_query, is_long = None, None, False
  
          for version_str in (modules_ver, args.extensions, args.arch, args.archives[0] if args.archives else None):
              Cli._validate_version_str(version_str, allow_latest=True, allow_empty=True)
  
          spec = None
          try:
              if args.spec is not None:
                  spec = SimpleSpec(args.spec)
          except ValueError as e:
              raise CliInputError(f"Invalid version specification: '{args.spec}'.\n" + SimpleSpec.usage()) from e
  
          meta = MetadataFactory(
              archive_id=ArchiveId(
                  "qt",
                  args.host,
                  args.target,
                  args.extension if args.extension else "",
              ),
              spec=spec,
              is_latest_version=args.latest_version,
              modules_query=modules_query,
              is_long_listing=is_long,
              extensions_ver=args.extensions,
              architectures_ver=args.arch,
              archives_query=args.archives,
          )
          show_list(meta)
  
      def run_list_tool(self, args: argparse.ArgumentParser):
          """Print tools"""
  
          if not args.target:
              print(" ".join(ArchiveId.TARGETS_FOR_HOST[args.host]))
              return
          if args.target not in ArchiveId.TARGETS_FOR_HOST[args.host]:
              raise CliInputError("'{0.target}' is not a valid target for host '{0.host}'".format(args))
  
          meta = MetadataFactory(
              archive_id=ArchiveId("tools", args.host, args.target),
              tool_name=args.tool_name,
              is_long_listing=args.long,
          )
          show_list(meta)
  
      def run_list_src_doc_examples(self, args: argparse.ArgumentParser, cmd_type: str):
          target = "desktop"  # The only valid target for src/doc/examples is "desktop"
          version = Cli._determine_qt_version(args.qt_version_spec, args.host, target, arch="")
          is_fetch_modules: bool = getattr(args, "modules", False)
          meta = MetadataFactory(
              archive_id=ArchiveId("qt", args.host, target, "src_doc_examples"),
              src_doc_examples_query=(cmd_type, version, is_fetch_modules),
          )
          show_list(meta)
  
      def show_help(self, args=None):
          """Display help message"""
          self.parser.print_help()
  
      def _format_aqt_version(self) -> str:
          py_version = platform.python_version()
          py_impl = platform.python_implementation()
          py_build = platform.python_compiler()
          return f"aqtinstall(aqt) v{aqt.__version__} on Python {py_version} [{py_impl} {py_build}]"
  
      def show_aqt_version(self, args=None):
          """Display version information"""
          self.logger.info(self._format_aqt_version())
  
      def _set_install_qt_parser(self, install_qt_parser, *, is_legacy: bool):
          install_qt_parser.set_defaults(func=self.run_install_qt, is_legacy=is_legacy)
          self._set_common_arguments(install_qt_parser, is_legacy=is_legacy)
          self._set_common_options(install_qt_parser)
          install_qt_parser.add_argument(
              "arch",
              nargs="?",
              help="\ntarget linux/desktop: gcc_64, wasm_32"
              "\ntarget mac/desktop:   clang_64, wasm_32"
              "\ntarget mac/ios:       ios"
              "\nwindows/desktop:      win64_msvc2019_64, win32_msvc2019"
              "\n                      win64_msvc2017_64, win32_msvc2017"
              "\n                      win64_msvc2015_64, win32_msvc2015"
              "\n                      win64_mingw81, win32_mingw81"
              "\n                      win64_mingw73, win32_mingw73"
              "\n                      win32_mingw53"
              "\n                      wasm_32"
              "\nwindows/winrt:        win64_msvc2019_winrt_x64, win64_msvc2019_winrt_x86"
              "\n                      win64_msvc2017_winrt_x64, win64_msvc2017_winrt_x86"
              "\n                      win64_msvc2019_winrt_armv7"
              "\n                      win64_msvc2017_winrt_armv7"
              "\nandroid:              Qt 5.14:          android (optional)"
              "\n                      Qt 5.13 or below: android_x86_64, android_arm64_v8a"
              "\n                                        android_x86, android_armv7",
          )
          self._set_module_options(install_qt_parser)
          self._set_archive_options(install_qt_parser)
          install_qt_parser.add_argument(
              "--noarchives",
              action="store_true",
              help="No base packages; allow mod amendment with --modules option.",
          )
  
      def _set_install_tool_parser(self, install_tool_parser, *, is_legacy: bool):
          install_tool_parser.set_defaults(func=self.run_install_tool, is_legacy=is_legacy)
          install_tool_parser.add_argument("host", choices=["linux", "mac", "windows"], help="host os name")
          if not is_legacy:
              install_tool_parser.add_argument(
                  "target",
                  default=None,
                  choices=["desktop", "winrt", "android", "ios"],
                  help="Target SDK.",
              )
          install_tool_parser.add_argument("tool_name", help="Name of tool such as tools_ifw, tools_mingw")
          if is_legacy:
              install_tool_parser.add_argument("version", help="Version of tool variant")
  
          tool_variant_opts = {} if is_legacy else {"nargs": "?", "default": None}
          install_tool_parser.add_argument(
              "tool_variant",
              **tool_variant_opts,
              help="Name of tool variant, such as qt.tools.ifw.41. "
              "Please use 'aqt list-tool' to list acceptable values for this parameter.",
          )
          self._set_common_options(install_tool_parser)
  
      def _warn_on_deprecated_command(self, old_name: str, new_name: str):
          self.logger.warning(
              f"The command '{old_name}' is deprecated and marked for removal in a future version of aqt.\n"
              f"In the future, please use the command '{new_name}' instead."
          )
  
      def _warn_on_deprecated_parameter(self, parameter_name: str, value: str):
          self.logger.warning(
              f"The parameter '{parameter_name}' with value '{value}' is deprecated and marked for "
              f"removal in a future version of aqt.\n"
              f"In the future, please omit this parameter."
          )
  
      def _make_all_parsers(self, subparsers: argparse._SubParsersAction):
          deprecated_msg = "This command is deprecated and marked for removal in a future version of aqt."
  
          def make_parser_it(cmd: str, desc: str, is_legacy: bool, set_parser_cmd, formatter_class):
              description = f"{desc} {deprecated_msg}" if is_legacy else desc
              kwargs = {"formatter_class": formatter_class} if formatter_class else {}
              p = subparsers.add_parser(cmd, description=description, **kwargs)
              set_parser_cmd(p, is_legacy=is_legacy)
  
          def make_parser_sde(cmd: str, desc: str, is_legacy: bool, action, is_add_kde: bool, is_add_modules: bool = True):
              description = f"{desc} {deprecated_msg}" if is_legacy else desc
              parser = subparsers.add_parser(cmd, description=description)
              parser.set_defaults(func=action, is_legacy=is_legacy)
              self._set_common_arguments(parser, is_legacy=is_legacy, is_target_deprecated=True)
              self._set_common_options(parser)
              if is_add_modules:
                  self._set_module_options(parser)
              self._set_archive_options(parser)
              if is_add_kde:
                  parser.add_argument("--kde", action="store_true", help="patching with KDE patch kit.")
  
          def make_parser_list_sde(cmd: str, desc: str, cmd_type: str):
              parser = subparsers.add_parser(cmd, description=desc)
              parser.add_argument("host", choices=["linux", "mac", "windows"], help="host os name")
              parser.add_argument(
                  "qt_version_spec",
                  metavar="(VERSION | SPECIFICATION)",
                  help='Qt version in the format of "5.X.Y" or SimpleSpec like "5.X" or "<6.X"',
              )
              parser.set_defaults(func=lambda args: self.run_list_src_doc_examples(args, cmd_type))
  
              if cmd_type != "src":
                  parser.add_argument("-m", "--modules", action="store_true", help="Print list of available modules")
  
          make_parser_it("install-qt", "Install Qt.", False, self._set_install_qt_parser, argparse.RawTextHelpFormatter)
          make_parser_it("install-tool", "Install tools.", False, self._set_install_tool_parser, None)
          make_parser_sde("install-doc", "Install documentation.", False, self.run_install_doc, False)
          make_parser_sde("install-example", "Install examples.", False, self.run_install_example, False)
          make_parser_sde("install-src", "Install source.", False, self.run_install_src, True, is_add_modules=False)
  
          self._make_list_qt_parser(subparsers)
          self._make_list_tool_parser(subparsers)
          make_parser_list_sde("list-doc", "List documentation archives available (use with install-doc)", "doc")
          make_parser_list_sde("list-example", "List example archives available (use with install-example)", "examples")
          make_parser_list_sde("list-src", "List source archives available (use with install-src)", "src")
  
          make_parser_it("install", "Install Qt.", True, self._set_install_qt_parser, argparse.RawTextHelpFormatter)
          make_parser_it("tool", "Install tools.", True, self._set_install_tool_parser, None)
          make_parser_sde("doc", "Install documentation.", True, self.run_install_doc, False)
          make_parser_sde("examples", "Install examples.", True, self.run_install_example, False)
          make_parser_sde("src", "Install source.", True, self.run_install_src, True)
  
          self._make_common_parsers(subparsers)
  
      def _make_list_qt_parser(self, subparsers: argparse._SubParsersAction):
          """Creates a subparser that works with the MetadataFactory, and adds it to the `subparsers` parameter"""
          list_parser: argparse.ArgumentParser = subparsers.add_parser(
              "list-qt",
              formatter_class=argparse.RawDescriptionHelpFormatter,
              epilog="Examples:\n"
              "$ aqt list-qt mac                                                # print all targets for Mac OS\n"
              "$ aqt list-qt mac desktop                                        # print all versions of Qt 5\n"
              "$ aqt list-qt mac desktop --extension wasm                       # print all wasm versions of Qt 5\n"
              '$ aqt list-qt mac desktop --spec "5.9"                           # print all versions of Qt 5.9\n'
              '$ aqt list-qt mac desktop --spec "5.9" --latest-version          # print latest Qt 5.9\n'
              "$ aqt list-qt mac desktop --modules 5.12.0 clang_64              # print modules for 5.12.0\n"
              "$ aqt list-qt mac desktop --spec 5.9 --modules latest clang_64   # print modules for latest 5.9\n"
              "$ aqt list-qt mac desktop --extensions 5.9.0                     # print choices for --extension flag\n"
              "$ aqt list-qt mac desktop --arch 5.9.9                           # print architectures for 5.9.9/mac/desktop\n"
              "$ aqt list-qt mac desktop --arch latest                          # print architectures for the latest Qt 5\n"
              "$ aqt list-qt mac desktop --archives 5.9.0 clang_64              # list archives in base Qt installation\n"
              "$ aqt list-qt mac desktop --archives 5.14.0 clang_64 debug_info  # list archives in debug_info
  
  [... the rest is omitted ...]
files: aqt/installer.py
files: import hashlib
  import logging
  import os
  import re
  import subprocess
  import sys
  import textwrap
  from dataclasses import dataclass
  from datetime import datetime
  from pathlib import Path
  from tempfile import TemporaryDirectory
  from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple
  
  import py7zr
  import pytest
  
  from aqt.archives import QtPackage
  from aqt.exceptions import ArchiveDownloadError, ArchiveExtractionError
  from aqt.helper import Settings
  from aqt.installer import Cli, installer
  
  
  class MockMultiprocessingContext:
      """
      By default, monkeypatch will only patch objects in the main process.
      When multiprocessing is used, child processes will not be patched.
      This class forces all work to be done in the main process, so that all
      patched objects remain patched.
  
      NOTE: This probably isn't the right way to solve the problem, but it is
      the only way I have found to solve it.
      """
  
      def __init__(self, *args):
          pass
  
      class Pool:
          def __init__(self, *args):
              pass
  
          def starmap(self, func: Callable, func_args: List[Tuple], *args):
              for set_of_args in func_args:
                  func(*set_of_args)
  
          def close(self):
              pass
  
          def join(self):
              pass
  
          def terminate(self):
              pass
  
  
  class MockMultiprocessingManager:
      class Queue:
          def __init__(self, *args):
              pass
  
          def put_nowait(self, log_record: Optional[logging.LogRecord]):
              # NOTE: This is certainly not the right way to do this, but it works locally
              if not log_record or log_record.levelno < logging.INFO:
                  return
              print(log_record.message, file=sys.stderr)
  
          def get(self, *args):
              return None
  
  
  GET_URL_TYPE = Callable[[str, Any], str]
  DOWNLOAD_ARCHIVE_TYPE = Callable[[str, str, str, bytes, Any], None]
  LIST_OF_FILES_AND_CONTENTS = Iterable[Dict[str, str]]
  
  
  @dataclass
  class PatchedFile:
      filename: str
      unpatched_content: str
      patched_content: Optional[str]
  
      def expected_content(self, **kwargs) -> str:
          if not self.patched_content:
              return self.unpatched_content
          return self.patched_content.format(**kwargs)
  
  
  @dataclass
  class MockArchive:
      filename_7z: str
      update_xml_name: str
      contents: Iterable[PatchedFile]
      version: str = ""
      arch_dir: str = ""
      should_install: bool = True
      date: datetime = datetime.now()
  
      def xml_package_update(self) -> str:
          return textwrap.dedent(
              f"""\
               <PackageUpdate>
                <Name>{self.update_xml_name}</Name>
                <Version>{self.version}-0-{self.date.strftime("%Y%m%d%H%M")}</Version>
                <Description>none</Description>
                <DownloadableArchives>{self.filename_7z}</DownloadableArchives>
               </PackageUpdate>"""
          )
  
      def write_compressed_archive(self, dest: Path) -> None:
          with TemporaryDirectory() as temp_dir, py7zr.SevenZipFile(dest / self.filename_7z, "w") as archive:
              temp_path = Path(temp_dir)
  
              for folder in ("bin", "lib", "mkspecs"):
                  (temp_path / self.arch_dir / folder).mkdir(parents=True, exist_ok=True)
  
              # Use `self.contents` to write qmake binary, qmake script, QtCore binaries, etc
              for patched_file in self.contents:
                  full_path = temp_path / self.arch_dir / patched_file.filename
                  if not full_path.parent.exists():
                      full_path.parent.mkdir(parents=True)
                  full_path.write_text(patched_file.unpatched_content, "utf_8")
  
              archive_name = "5.9" if self.version == "5.9.0" else self.version
              archive.writeall(path=temp_path, arcname=archive_name)
  
  
  def make_mock_geturl_download_archive(
      archives: Iterable[MockArchive],
      arch: str,
      os_name: str,
      updates_url: str,
  ) -> Tuple[GET_URL_TYPE, DOWNLOAD_ARCHIVE_TYPE]:
      """
      Returns a mock 'getUrl' and a mock 'downloadArchive' function.
      """
      for _arc in archives:
          assert _arc.filename_7z.endswith(".7z")
  
      xml = "<Updates>\n{}\n</Updates>".format("\n".join([archive.xml_package_update() for archive in archives]))
  
      def mock_getUrl(url: str, *args, **kwargs) -> str:
          if url.endswith(updates_url):
              return xml
          elif url.endswith(".sha256"):
              filename = url.split("/")[-1][: -len(".sha256")]
              return f"{hashlib.sha256(bytes(xml, 'utf-8')).hexdigest()} {filename}"
          assert False
  
      def mock_download_archive(url: str, out: str, *args):
          """Make a mocked 7z archive at out_filename"""
  
          def locate_archive() -> MockArchive:
              for arc in archives:
                  if Path(out).name == arc.filename_7z:
                      return arc
              assert False, "Requested an archive that was not mocked"
  
          locate_archive().write_compressed_archive(Path(out).parent)
  
      return mock_getUrl, mock_download_archive
  
  
  @pytest.fixture(autouse=True)
  def disable_multiprocessing(monkeypatch):
      # This blocks all multiprocessing, which would otherwise spawn processes that are not monkeypatched
      monkeypatch.setattr(
          "aqt.installer.multiprocessing.get_context",
          lambda *args: MockMultiprocessingContext(),
      )
  
      monkeypatch.setattr(
          "aqt.installer.multiprocessing.Manager",
          MockMultiprocessingManager,
      )
  
  
  def qtcharts_module(ver: str, arch: str) -> MockArchive:
      addons = "addons." if ver[0] == "6" else ""
      prefix = "qt" if ver.startswith("5.9.") else f"qt.qt{ver[0]}"
      return MockArchive(
          filename_7z=f"qtcharts-windows-{arch}.7z",
          update_xml_name=f"{prefix}.{ver.replace('.', '')}.{addons}qtcharts.{arch}",
          version=ver,
          # arch_dir: filled in later
          contents=(
              PatchedFile(
                  filename="modules/Charts.json",
                  unpatched_content=textwrap.dedent(
                      f"""\
                      {{
                          "module_name": "Charts",
                          "version": "{ver}",
                          "built_with": {{
                              "compiler_id": "GNU",
                              "compiler_target": "",
                              "compiler_version": "1.2.3.4",
                              "cross_compiled": false,
                              "target_system": "Windows"
                          }}
                      }}
                      """
                  ),
                  patched_content=None,  # it doesn't get patched
              ),
          ),
      )
  
  
  def qtpositioning_module(ver: str, arch: str) -> MockArchive:
      addons = "addons." if ver[0] == "6" else ""
      prefix = "qt" if ver.startswith("5.9.") else f"qt.qt{ver[0]}"
      return MockArchive(
          filename_7z=f"qtlocation-windows-{arch}.7z",
          update_xml_name=f"{prefix}.{ver.replace('.', '')}.{addons}qtpositioning.{arch}",
          version=ver,
          # arch_dir: filled in later
          contents=(
              PatchedFile(
                  filename="modules/Positioning.json",
                  unpatched_content=textwrap.dedent(
                      f"""\
                      {{
                          "module_name": "Positioning",
                          "version": "{ver}",
                          "built_with": {{
                              "compiler_id": "GNU",
                              "compiler_target": "",
                              "compiler_version": "1.2.3.4",
                              "cross_compiled": false,
                              "target_system": "Windows"
                          }}
                      }}
                      """
                  ),
                  patched_content=None,  # it doesn't get patched
              ),
          ),
      )
  
  
  def plain_qtbase_archive(update_xml_name: str, arch: str, should_install: bool = True) -> MockArchive:
      return MockArchive(
          filename_7z=f"qtbase-windows-{arch}.7z",
          update_xml_name=update_xml_name,
          contents=(
              PatchedFile(
                  filename="mkspecs/qconfig.pri",
                  unpatched_content="... blah blah blah ...\n"
                  "QT_EDITION = Not OpenSource\n"
                  "QT_LICHECK = Not Empty\n"
                  "... blah blah blah ...\n",
                  patched_content="... blah blah blah ...\n"
                  "QT_EDITION = OpenSource\n"
                  "QT_LICHECK =\n"
                  "... blah blah blah ...\n",
              ),
          ),
          should_install=should_install,
      )
  
  
  def tool_archive(host: str, tool_name: str, variant: str, date: datetime = datetime.now()) -> MockArchive:
      return MockArchive(
          filename_7z=f"{tool_name}-{host}-{variant}.7z",
          update_xml_name=variant,
          contents=(
              PatchedFile(
                  filename=f"bin/{tool_name}-{variant}.exe",
                  unpatched_content="Some executable binary file",
                  patched_content=None,  # it doesn't get patched
              ),
          ),
          should_install=True,
          date=date,
      )
  
  
  @pytest.mark.parametrize(
      "cmd, host, target, version, arch, arch_dir, updates_url, archives, expect_out",
      (
          (
              "install-tool linux desktop tools_qtcreator qt.tools.qtcreator".split(),
              "linux",
              "desktop",
              "1.2.3-0-197001020304",
              "",
              "",
              "linux_x64/desktop/tools_qtcreator/Updates.xml",
              [
                  tool_archive("linux", "tools_qtcreator", "qt.tools.qtcreator"),
              ],
              re.compile(
                  r"^INFO    : aqtinstall\(aqt\) v.* on Python 3.*\n"
                  r"INFO    : Downloading qt.tools.qtcreator...\n"
                  r"Finished installation of tools_qtcreator-linux-qt.tools.qtcreator.7z in .*\n"
                  r"INFO    : Finished installation\n"
                  r"INFO    : Time elapsed: .* second"
              ),
          ),
          (
              "tool linux tools_qtcreator 1.2.3-0-197001020304 qt.tools.qtcreator".split(),
              "linux",
              "desktop",
              "1.2.3",
              "",
              "",
              "linux_x64/desktop/tools_qtcreator/Updates.xml",
              [
                  tool_archive("linux", "tools_qtcreator", "qt.tools.qtcreator", datetime(1970, 1, 2, 3, 4, 5, 6)),
              ],
              re.compile(
                  r"^INFO    : aqtinstall\(aqt\) v.* on Python 3.*\n"
                  r"WARNING : The command 'tool' is deprecated and marked for removal in a future version of aqt.\n"
                  r"In the future, please use the command 'install-tool' instead.\n"
                  r"INFO    : Downloading qt.tools.qtcreator...\n"
                  r"Finished installation of tools_qtcreator-linux-qt.tools.qtcreator.7z in .*\n"
                  r"INFO    : Finished installation\n"
                  r"INFO    : Time elapsed: .* second"
              ),
          ),
          (
              "install 5.14.0 windows desktop win32_mingw73".split(),
              "windows",
              "desktop",
              "5.14.0",
              "win32_mingw73",
              "mingw73_32",
              "windows_x86/desktop/qt5_5140/Updates.xml",
              [
                  plain_qtbase_archive("qt.qt5.5140.win32_mingw73", "win32_mingw73"),
              ],
              re.compile(
                  r"^INFO    : aqtinstall\(aqt\) v.* on Python 3.*\n"
                  r"WARNING : The command 'install' is deprecated"
                  r" and marked for removal in a future version of aqt.\n"
                  r"In the future, please use the command 'install-qt' instead.\n"
                  r"INFO    : Downloading qtbase...\n"
                  r"Finished installation of qtbase-windows-win32_mingw73.7z in .*\n"
                  r"INFO    : Finished installation\n"
                  r"INFO    : Time elapsed: .* second"
              ),
          ),
          (
              "install-src windows desktop 5.14.2".split(),
              "windows",
              "desktop",
              "5.14.2",
              "",
              "",
              "windows_x86/desktop/qt5_5142_src_doc_examples/Updates.xml",
              [
                  MockArchive(
                      filename_7z="qtbase-everywhere-src-5.14.2.7z",
                      update_xml_name="qt.qt5.5142.src",
                      version="5.14.2",
                      contents=(
                          PatchedFile(
                              filename="Src/qtbase/QtBaseSource.cpp",
                              unpatched_content="int main(){ return 0; }",
                              patched_content=None,  # not patched
                          ),
                      ),
                  ),
              ],
              re.compile(
                  r"^INFO    : aqtinstall\(aqt\) v.* on Python 3.*\n"
                  r"WARNING : The parameter 'target' with value 'desktop' is deprecated "
                  r"and marked for removal in a future version of aqt\.\n"
                  r"In the future, please omit this parameter\.\n"
                  r"INFO    : Downloading qtbase\.\.\.\n"
                  r"Finished installation of qtbase-everywhere-src-5\.14\.2\.7z in .*\n"
                  r"INFO    : Finished installation\n"
                  r"INFO    : Time elapsed: .* second"
              ),
          ),
          (
              "install-src windows 5.14.2".split(),
              "windows",
              "desktop",
              "5.14.2",
              "",
              "",
              "windows_x86/desktop/qt5_5142_src_doc_examples/Updates.xml",
              [
                  MockArchive(
                      filename_7z="qtbase-everywhere-src-5.14.2.7z",
                      update_xml_name="qt.qt5.5142.src",
                      version="5.14.2",
                      contents=(
                          PatchedFile(
                              filename="Src/qtbase/QtBaseSource.cpp",
                              unpatched_content="int main(){ return 0; }",
                              patched_content=None,  # not patched
                          ),
                      ),
                  ),
              ],
              re.compile(
                  r"^INFO    : aqtinstall\(aqt\) v.* on Python 3.*\n"
                  r"INFO    : Downloading qtbase\.\.\.\n"
                  r"Finished installation of qtbase-everywhere-src-5\.14\.2\.7z in .*\n"
                  r"INFO    : Finished installation\n"
                  r"INFO    : Time elapsed: .* second"
              ),
          ),
          (
              "install 5.9.0 windows desktop win32_mingw53".split(),
              "windows",
              "desktop",
              "5.9.0",
              "win32_mingw53",
              "mingw53_32",
              "windows_x86/desktop/qt5_59/Updates.xml",
              [
                  plain_qtbase_archive("qt.59.win32_mingw53", "win32_mingw53"),
              ],
              re.compile(
                  r"^INFO    : aqtinstall\(aqt\) v.* on Python 3.*\n"
                  r"WARNING : The command 'install' is deprecated"
                  r" and marked for removal in a future version of aqt.\n"
                  r"In the future, please use the command 'install-qt' instead.\n"
                  r"INFO    : Downloading qtbase...\n"
                  r"Finished installation of qtbase-windows-win32_mingw53.7z in .*\n"
                  r"INFO    : Finished installation\n"
                  r"INFO    : Time elapsed: .* second"
              ),
          ),
          (
              "install-qt windows desktop 5.9.0 win32_mingw53".split(),
              "windows",
              "desktop",
              "5.9.0",
              "win32_mingw53",
              "mingw53_32",
              "windows_x86/desktop/qt5_59/Updates.xml",
              [
                  plain_qtbase_archive("qt.59.win32_mingw53", "win32_mingw53"),
              ],
              re.compile(
                  r"^INFO    : aqtinstall\(aqt\) v.* on Python 3.*\n"
                  r"INFO    : Downloading qtbase...\n"
                  r"Finished installation of qtbase-windows-win32_mingw53.7z in .*\n"
                  r"INFO    : Finished installation\n"
                  r"INFO    : Time elapsed: .* second"
              ),
          ),
          (
              "install-qt windows desktop 5.14.0 win32_mingw73".split(),
              "windows",
              "desktop",
              "5.14.0",
              "win32_mingw73",
              "mingw73_32",
              "windows_x86/desktop/qt5_5140/Updates.xml",
              [
                  plain_qtbase_archive("qt.qt5.5140.win32_mingw73", "win32_mingw73"),
              ],
              re.compile(
                  r"^INFO    : aqtinstall\(aqt\) v.* on Python 3.*\n"
                  r"INFO    : Downloading qtbase...\n"
                  r"Finished installation of qtbase-windows-win32_mingw73.7z in .*\n"
                  r"INFO    : Finished installation\n"
                  r"INFO    : Time elapsed: .* second"
              ),
          ),
          (
              "install-qt windows desktop 5.14.0 win64_mingw73 -m qtcharts".split(),
              "windows",
              "desktop",
              "5.14.0",
              "win64_mingw73",
              "mingw73_64",
              "windows_x86/desktop/qt5_5140/Updates.xml",
              [
                  plain_qtbase_archive("qt.qt5.5140.win64_mingw73", "win64_mingw73"),
                  qtcharts_module("5.14.0", "win64_mingw73"),
              ],
              re.compile(
                  r"^INFO    : aqtinstall\(aqt\) v.* on Python 3.*\n"
                  r"INFO    : Downloading qtbase...\n"
                  r"Finished installation of qtbase-windows-win64_mingw73.7z in .*\n"
                  r"INFO    : Downloading qtcharts...\n"
                  r"Finished installation of qtcharts-windows-win64_mingw73.7z in .*\n"
                  r"INFO    : Finished installation\n"
                  r"INFO    : Time elapsed: .* second"
              ),
          ),
          (
              "install-qt windows desktop 6.2.0 win64_mingw73 -m qtpositioning --noarchives".split(),
              "windows",
              "desktop",
              "6.2.0",
              "win64_mingw73",
              "mingw73_64",
              "windows_x86/desktop/qt6_620/Updates.xml",
              [
                  plain_qtbase_archive("qt.qt6.620.win64_mingw73", "win64_mingw73", should_install=False),
                  qtpositioning_module("6.2.0", "win64_mingw73"),
              ],
              re.compile(
                  r"^INFO    : aqtinstall\(aqt\) v.* on Python 3.*\n"
                  r"INFO    : Downloading qtlocation...\n"
                  r"Finished installation of qtlocation-windows-win64_mingw73.7z in .*\n"
                  r"INFO    : Finished installation\n"
                  r"INFO    : Time elapsed: .* second"
              ),
          ),
          (
              "install-qt windows desktop 6.1.0 win64_mingw81 -m qtcharts".split(),
              "windows",
              "desktop",
              "6.1.0",
              "win64_mingw81",
              "mingw81_64",
              "windows_x86/desktop/qt6_610/Updates.xml",
              [
                  plain_qtbase_archive("qt.qt6.610.win64_mingw81", "win64_mingw81"),
                  qtcharts_module("6.1.0", "win64_mingw81"),
              ],
              re.compile(
                  r"^INFO    : aqtinstall\(aqt\) v.* on Python 3.*\n"
                  r"INFO    : Downloading qtbase...\n"
                  r"Finished installation of qtbase-windows-win64_mingw81.7z in .*\n"
                  r"INFO    : Downloading qtcharts...\n"
                  r"Finished installation of qtcharts-windows-win64_mingw81.7z in .*\n"
                  r"INFO    : Finished installation\n"
                  r"INFO    : Time elapsed: .* second"
              ),
          ),
          (
              "install-qt windows android 6.1.0 android_armv7".split(),
              "windows",
              "android",
              "6.1.0",
              "android_armv7",
              "android_armv7",
              "windows_x86/android/qt6_610_armv7/Updates.xml",
              [
                  MockArchive(
                      filename_7z="qtbase-windows-android_armv7.7z",
                      update_xml_name="qt.qt6.610.android_armv7",
                      contents=(
                          # Qt 6 non-desktop should patch qconfig.pri, qmake script and target_qt.conf
                          PatchedFile(
                              filename="mkspecs/qconfig.pri",
                              unpatched_content="... blah blah blah ...\n"
                              "QT_EDITION = Not OpenSource\n"
                              "QT_LICHECK = Not Empty\n"
                              "... blah blah blah ...\n",
                              patched_content="... blah blah blah ...\n"
                              "QT_EDITION = OpenSource\n"
                              "QT_LICHECK =\n"
                              "... blah blah blah ...\n",
                          ),
                          PatchedFile(
                              filename="bin/target_qt.conf",
                              unpatched_content="Prefix=/Users/qt/work/install/target\n"
                              "HostPrefix=../../\n"
                              "HostData=target\n",
                              patched_content="Prefix={base_dir}{sep}6.1.0{sep}android_armv7{sep}target\n"
                              "HostPrefix=../../mingw81_64\n"
                              "HostData=../android_armv7\n",
                          ),
                          PatchedFile(
                              filename="bin/qmake.bat",
                              unpatched_content="... blah blah blah ...\n"
                              "/Users/qt/work/install/bin\n"
                              "... blah blah blah ...\n",
                              patched_content="... blah blah blah ...\n"
                              "{base_dir}\\6.1.0\\mingw81_64\\bin\n"
                              "... blah blah blah ...\n",
                          ),
                      ),
                  ),
              ],
              re.compile(
                  r"^INFO    : aqtinstall\(aqt\) v.* on Python 3.*\n"
                  r"INFO    : Downloading qtbase...\n"
                  r"Finished installation of qtbase-windows-android_armv7.7z in .*\n"
                  r"WARNING : You are installing the android version of Qt, which requires that the desktop version of "
                  r"Qt is also installed. You can install it with the following command:\n"
                  r"          `aqt install-qt windows desktop 6.1.0 MINGW_MOCK_DEFAULT`\n"
                  r"INFO    : Patching .*6\.1\.0[/\\]android_armv7[/\\]bin[/\\]qmake.bat\n"
                  r"INFO    : Finished installation\n"
                  r"INFO    : Time elapsed: .* second"
              ),
          ),
          (
              "install-qt linux android 6.3.0 android_arm64_v8a".split(),
              "linux",
              "android",
              "6.3.0",
              "android_arm64_v8a",
              "android_arm64_v8a",
              "linux_x64/android/qt6_630_arm64_v8a/Updates.xml",
              [
                  MockArchive(
                      filename_7z="qtbase-linux-android_arm64_v8a.7z",
                      update_xml_name="qt.qt6.630.android_arm64_v8a",
                      contents=(
                          # Qt 6 non-desktop should patch qconfig.pri, qmake script and target_qt.conf
                          PatchedFile(
                              filename="mkspecs/qconfig.pri",
                              unpatched_content="... blah blah blah ...\n"
                              "QT_EDITION = Not OpenSource\n"
                              "QT_LICHECK = Not Empty\n"
                              "... blah blah blah ...\n",
                              patched_content="... blah blah blah ...\n"
                              "QT_EDITION = OpenSource\n"
                              "QT_LICHECK =\n"
                              "... blah blah blah ...\n",
                          ),
                          PatchedFile(
                              filename="bin/target_qt.conf",
                              unpatched_content="Prefix=/home/qt/work/install/target\n"
                              "HostPrefix=../../\n"
                              "HostData=target\n",
                              patched_content="Prefix={base_dir}{sep}6.3.0{sep}android_arm64_v8a{sep}target\n"
                              "HostPrefix=../../gcc_64\n"
                              "HostData=../android_arm64_v8a\n",
                          ),
                          PatchedFile(
                              filename="bin/qmake",
                              unpatched_content="... blah blah blah ...\n"
                              "/home/qt/work/install/bin\n"
                              "... blah blah blah ...\n",
                              patched_content="... blah blah blah ...\n"
                              "{base_dir}/6.3.0/gcc_64/bin\n"
                              "... blah blah blah ...\n",
                          ),
                      ),
                  ),
              ],
              re.compile(
                  r"^INFO    : aqtinstall\(aqt\) v.* on Python 3.*\n"
                  r"INFO    : Downloading qtbase...\n"
                  r"Finished installation of qtbase-linux-android_arm64_v8a.7z in .*\n"
                  r"WARNING : You are installing the android version of Qt, which requires that the desktop version of "
                  r"Qt is also installed. You can install it with the following command:\n"
                  r"          `aqt install-qt linux desktop 6\.3\.0 gcc_64`\n"
                  r"INFO    : Patching .*6\.3\.0[/\\]android_arm64_v8a[/\\]bin[/\\]qmake\n"
                  r"INFO    : Finished installation\n"
                  r"INFO    : Time elapsed: .* second"
              ),
          ),
          (
              "install-qt mac ios 6.1.2".split(),
              "mac",
              "ios",
              "6.1.2",
              "ios",
              "ios",
              "mac_x64/ios/qt6_612/Updates.xml",
              [
                  MockArchive(
                      filename_7z="qtbase-mac-ios.7z",
                      update_xml_name="qt.qt6.612.ios",
                      contents=(
                          # Qt 6 non-desktop should patch qconfig.pri, qmake script and target_qt.conf
                          PatchedFile(
                              filename="mkspecs/qconfig.pri",
                              unpatched_content="... blah blah blah ...\n"
                              "QT_EDITION = Not OpenSource\n"
                              "QT_LICHECK = Not Empty\n"
                              "... blah blah blah ...\n",
                              patched_content="... blah blah blah ...\n"
                              "QT_EDITION = OpenSource\n"
                              "QT_LICHECK =\n"
                              "... blah blah blah ...\n",
                          ),
                          PatchedFile(
                              filename="bin/target_qt.conf",
                              unpatched_content="Prefix=/Users/qt/work/install/target\n"
                              "HostPrefix=../../\n"
                              "HostData=target\n",
                              patched_content="Prefix={base_dir}{sep}6.1.2{sep}ios{sep}target\n"
                              "HostPrefix=../../macos\n"
                              "HostData=../ios\n",
                          ),
                          PatchedFile(
                              filename="bin/qmake",
                              unpatched_content="... blah blah blah ...\n"
                              "/Users/qt/work/install/bin\n"
                              "... blah blah blah ...\n",
                              patched_content="... blah blah blah ...\n"
                              "{base_dir}/6.1.2/macos/bin\n"
                              "... blah blah blah ...\n",
                          ),
                      ),
                  ),
              ],
              re.compile(
                  r"^INFO    : aqtinstall\(aqt\) v.* on Python 3.*\n"
                  r"INFO    : Downloading qtbase...\n"
                  r"Finished installation of qtbase-mac-ios.7z in .*\n"
                  r"WARNING : You are installing the ios version of Qt, which requires that the desktop version of Qt is "
                  r"also installed. You can install it with the following command:\n"
                  r"          `aqt install-qt mac desktop 6\.1\.2 macos`\n"
                  r"INFO    : Patching .*6\.1\.2[/\\]ios[/\\]bin[/\\]qmake\n"
                  r"INFO    : Finished installation\n"
                  r"INFO    : Time elapsed: .* second"
              ),
          ),
      ),
  )
  def test_install(
      monkeypatch,
      capsys,
      cmd: List[str],
      host: str,
      target: str,
      version: str,
      arch: str,
      arch_dir: str,
      updates_url: str,
      archives: List[MockArchive],
      expect_out,  # type: re.Pattern
  ):
  
      # For convenience, fill in version and arch dir: prevents repetitive data declarations
      for i in range(len(archives)):
          archives[i].version = version
          archives[i].arch_dir = arch_dir
  
      mock_get_url, mock_download_archive = make_mock_geturl_download_archive(archives, arch, host, updates_url)
      monkeypatch.setattr("aqt.archives.getUrl", mock_get_url)
      monkeypatch.setattr("aqt.helper.getUrl", mock_get_url)
      monkeypatch.setattr("aqt.installer.downloadBinaryFile", mock_download_archive)
      monkeypatch.setattr("aqt.metadata.MetadataFactory.fetch_default_desktop_arch", lambda *args: "MINGW_MOCK_DEFAULT")
  
      with TemporaryDirectory() as output_dir:
          cli = Cli()
          cli._setup_settings()
  
          assert 0 == cli.run(cmd + ["--outputdir", output_dir])
  
          out, err = capsys.readouterr()
          sys.stdout.write(out)
          sys.stderr.write(err)
  
          assert expect_out.match(err)
  
          installed_path = Path(output_dir) / version / arch_dir
          if version == "5.9.0":
              installed_path = Path(output_dir) / "5.9" / arch_dir
          assert installed_path.is_dir()
          for archive in archives:
              if not archive.should_install:
                  continue
              for patched_file in archive.contents:
                  file_path = installed_path / patched_file.filename
                  assert file_path.is_file()
  
                  expect_content = patched_file.expected_content(base_dir=output_dir, sep=os.sep)
                  actual_content = file_path.read_text(encoding="utf_8")
                  assert actual_content == expect_content
  
  
  @pytest.mark.parametrize(
      "cmd, xml_file, expected",
      (
          (
              "install-qt windows desktop 5.16.0 win32_mingw73",
              None,
              "WARNING : Specified Qt version is unknown: 5.16.0.\n"
              "ERROR   : Failed to locate XML data for Qt version '5.16.0'.\n"
              "==============================Suggested follow-up:==============================\n"
              "* Please use 'aqt list-qt windows desktop' to show versions available.\n",
          ),
          (
              "install-qt windows desktop 5.15.0 bad_arch",
              "windows-5150-update.xml",
              "WARNING : Specified target combination is not valid or unknown: windows desktop bad_arch\n"
              "ERROR   : The packages ['qt_base'] were not found while parsing XML of package information!\n"
              "==============================Suggested follow-up:==============================\n"
              "* Please use 'aqt list-qt windows desktop --arch 5.15.0' to show architectures available.\n",
          ),
          (
              "install-qt windows desktop 5.15.0 win32_mingw73 -m nonexistent foo",
              "windows-5150-update.xml",
              "WARNING : Some of specified modules are unknown.\n"
              "ERROR   : The packages ['foo', 'nonexistent', 'qt_base'] were not found"
              " while parsing XML of package information!\n"
              "==============================Suggested follow-up:==============================\n"
              "* Please use 'aqt list-qt windows desktop --arch 5.15.0' to show architectures available.\n"
              "* Please use 'aqt list-qt windows desktop --modules 5.15.0 <arch>' to show modules available.\n",
          ),
          (
              "install-doc windows desktop 5.15.0 -m nonexistent foo",
              "windows-5152-src-doc-example-update.xml",
              "WARNING : The parameter 'target' with value 'desktop' is deprecated"
              " and marked for removal in a future "
              "version of aqt.\n"
              "In the future, please omit this parameter.\n"
              "ERROR   : The packages ['doc', 'foo', 'nonexistent'] were not found"
              " while parsing XML of package information!\n"
              "==============================Suggested follow-up:==============================\n"
              "* Please use 'aqt list-doc windows 5.15.0 --modules' to show modules available.\n",
          ),
          (
              "install-doc windows 5.15.0 -m nonexistent foo",
              "windows-5152-src-doc-example-update.xml",
              "ERROR   : The packages ['doc', 'foo', 'nonexistent'] were not found"
              " while parsing XML of package information!\n"
              "==============================Suggested follow-up:==============================\n"
              "* Please use 'aqt list-doc windows 5.15.0 --modules' to show modules available.\n",
  
  
  [... the rest is omitted ...]
files: tests/test_install.py
files: import hashlib
  import logging
  import os
  import re
  import subprocess
  import sys
  import textwrap
  from dataclasses import dataclass
  from datetime import datetime
  from pathlib import Path
  from tempfile import TemporaryDirectory
  from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple
  
  import py7zr
  import pytest
  
  from aqt.archives import QtPackage
  from aqt.exceptions import ArchiveDownloadError, ArchiveExtractionError
  from aqt.helper import Settings
  from aqt.installer import Cli, installer
  
  
  class MockMultiprocessingContext:
      """
      By default, monkeypatch will only patch objects in the main process.
      When multiprocessing is used, child processes will not be patched.
      This class forces all work to be done in the main process, so that all
      patched objects remain patched.
  
      NOTE: This probably isn't the right way to solve the problem, but it is
      the only way I have found to solve it.
      """
  
      def __init__(self, *args):
          pass
  
      class Pool:
          def __init__(self, *args):
              pass
  
          def starmap(self, func: Callable, func_args: List[Tuple], *args):
              for set_of_args in func_args:
                  func(*set_of_args)
  
          def close(self):
              pass
  
          def join(self):
              pass
  
          def terminate(self):
              pass
  
  
  class MockMultiprocessingManager:
      class Queue:
          def __init__(self, *args):
              pass
  
          def put_nowait(self, log_record: Optional[logging.LogRecord]):
              # NOTE: This is certainly not the right way to do this, but it works locally
              if not log_record or log_record.levelno < logging.INFO:
                  return
              print(log_record.message, file=sys.stderr)
  
          def get(self, *args):
              return None
  
  
  GET_URL_TYPE = Callable[[str, Any], str]
  DOWNLOAD_ARCHIVE_TYPE = Callable[[str, str, str, bytes, Any], None]
  LIST_OF_FILES_AND_CONTENTS = Iterable[Dict[str, str]]
  
  
  @dataclass
  class PatchedFile:
      filename: str
      unpatched_content: str
      patched_content: Optional[str]
  
      def expected_content(self, **kwargs) -> str:
          if not self.patched_content:
              return self.unpatched_content
          return self.patched_content.format(**kwargs)
  
  
  @dataclass
  class MockArchive:
      filename_7z: str
      update_xml_name: str
      contents: Iterable[PatchedFile]
      version: str = ""
      arch_dir: str = ""
      should_install: bool = True
      date: datetime = datetime.now()
  
      def xml_package_update(self) -> str:
          return textwrap.dedent(
              f"""\
               <PackageUpdate>
                <Name>{self.update_xml_name}</Name>
                <Version>{self.version}-0-{self.date.strftime("%Y%m%d%H%M")}</Version>
                <Description>none</Description>
                <DownloadableArchives>{self.filename_7z}</DownloadableArchives>
               </PackageUpdate>"""
          )
  
      def write_compressed_archive(self, dest: Path) -> None:
          with TemporaryDirectory() as temp_dir, py7zr.SevenZipFile(dest / self.filename_7z, "w") as archive:
              temp_path = Path(temp_dir)
  
              for folder in ("bin", "lib", "mkspecs"):
                  (temp_path / self.arch_dir / folder).mkdir(parents=True, exist_ok=True)
  
              # Use `self.contents` to write qmake binary, qmake script, QtCore binaries, etc
              for patched_file in self.contents:
                  full_path = temp_path / self.arch_dir / patched_file.filename
                  if not full_path.parent.exists():
                      full_path.parent.mkdir(parents=True)
                  full_path.write_text(patched_file.unpatched_content, "utf_8")
  
              archive_name = "5.9" if self.version == "5.9.0" else self.version
              archive.writeall(path=temp_path, arcname=archive_name)
  
  
  def make_mock_geturl_download_archive(
      archives: Iterable[MockArchive],
      arch: str,
      os_name: str,
      updates_url: str,
  ) -> Tuple[GET_URL_TYPE, DOWNLOAD_ARCHIVE_TYPE]:
      """
      Returns a mock 'getUrl' and a mock 'downloadArchive' function.
      """
      for _arc in archives:
          assert _arc.filename_7z.endswith(".7z")
  
      xml = "<Updates>\n{}\n</Updates>".format("\n".join([archive.xml_package_update() for archive in archives]))
  
      def mock_getUrl(url: str, *args, **kwargs) -> str:
          if url.endswith(updates_url):
              return xml
          elif url.endswith(".sha256"):
              filename = url.split("/")[-1][: -len(".sha256")]
              return f"{hashlib.sha256(bytes(xml, 'utf-8')).hexdigest()} {filename}"
          assert False
  
      def mock_download_archive(url: str, out: str, *args):
          """Make a mocked 7z archive at out_filename"""
  
          def locate_archive() -> MockArchive:
              for arc in archives:
                  if Path(out).name == arc.filename_7z:
                      return arc
              assert False, "Requested an archive that was not mocked"
  
          locate_archive().write_compressed_archive(Path(out).parent)
  
      return mock_getUrl, mock_download_archive
  
  
  @pytest.fixture(autouse=True)
  def disable_multiprocessing(monkeypatch):
      # This blocks all multiprocessing, which would otherwise spawn processes that are not monkeypatched
      monkeypatch.setattr(
          "aqt.installer.multiprocessing.get_context",
          lambda *args: MockMultiprocessingContext(),
      )
  
      monkeypatch.setattr(
          "aqt.installer.multiprocessing.Manager",
          MockMultiprocessingManager,
      )
  
  
  def qtcharts_module(ver: str, arch: str) -> MockArchive:
      addons = "addons." if ver[0] == "6" else ""
      prefix = "qt" if ver.startswith("5.9.") else f"qt.qt{ver[0]}"
      return MockArchive(
          filename_7z=f"qtcharts-windows-{arch}.7z",
          update_xml_name=f"{prefix}.{ver.replace('.', '')}.{addons}qtcharts.{arch}",
          version=ver,
          # arch_dir: filled in later
          contents=(
              PatchedFile(
                  filename="modules/Charts.json",
                  unpatched_content=textwrap.dedent(
                      f"""\
                      {{
                          "module_name": "Charts",
                          "version": "{ver}",
                          "built_with": {{
                              "compiler_id": "GNU",
                              "compiler_target": "",
                              "compiler_version": "1.2.3.4",
                              "cross_compiled": false,
                              "target_system": "Windows"
                          }}
                      }}
                      """
                  ),
                  patched_content=None,  # it doesn't get patched
              ),
          ),
      )
  
  
  def qtpositioning_module(ver: str, arch: str) -> MockArchive:
      addons = "addons." if ver[0] == "6" else ""
      prefix = "qt" if ver.startswith("5.9.") else f"qt.qt{ver[0]}"
      return MockArchive(
          filename_7z=f"qtlocation-windows-{arch}.7z",
          update_xml_name=f"{prefix}.{ver.replace('.', '')}.{addons}qtpositioning.{arch}",
          version=ver,
          # arch_dir: filled in later
          contents=(
              PatchedFile(
                  filename="modules/Positioning.json",
                  unpatched_content=textwrap.dedent(
                      f"""\
                      {{
                          "module_name": "Positioning",
                          "version": "{ver}",
                          "built_with": {{
                              "compiler_id": "GNU",
                              "compiler_target": "",
                              "compiler_version": "1.2.3.4",
                              "cross_compiled": false,
                              "target_system": "Windows"
                          }}
                      }}
                      """
                  ),
                  patched_content=None,  # it doesn't get patched
              ),
          ),
      )
  
  
  def plain_qtbase_archive(update_xml_name: str, arch: str, should_install: bool = True) -> MockArchive:
      return MockArchive(
          filename_7z=f"qtbase-windows-{arch}.7z",
          update_xml_name=update_xml_name,
          contents=(
              PatchedFile(
                  filename="mkspecs/qconfig.pri",
                  unpatched_content="... blah blah blah ...\n"
                  "QT_EDITION = Not OpenSource\n"
                  "QT_LICHECK = Not Empty\n"
                  "... blah blah blah ...\n",
                  patched_content="... blah blah blah ...\n"
                  "QT_EDITION = OpenSource\n"
                  "QT_LICHECK =\n"
                  "... blah blah blah ...\n",
              ),
          ),
          should_install=should_install,
      )
  
  
  def tool_archive(host: str, tool_name: str, variant: str, date: datetime = datetime.now()) -> MockArchive:
      return MockArchive(
          filename_7z=f"{tool_name}-{host}-{variant}.7z",
          update_xml_name=variant,
          contents=(
              PatchedFile(
                  filename=f"bin/{tool_name}-{variant}.exe",
                  unpatched_content="Some executable binary file",
                  patched_content=None,  # it doesn't get patched
              ),
          ),
          should_install=True,
          date=date,
      )
  
  
  @pytest.mark.parametrize(
      "cmd, host, target, version, arch, arch_dir, updates_url, archives, expect_out",
      (
          (
              "install-tool linux desktop tools_qtcreator qt.tools.qtcreator".split(),
              "linux",
              "desktop",
              "1.2.3-0-197001020304",
              "",
              "",
              "linux_x64/desktop/tools_qtcreator/Updates.xml",
              [
                  tool_archive("linux", "tools_qtcreator", "qt.tools.qtcreator"),
              ],
              re.compile(
                  r"^INFO    : aqtinstall\(aqt\) v.* on Python 3.*\n"
                  r"INFO    : Downloading qt.tools.qtcreator...\n"
                  r"Finished installation of tools_qtcreator-linux-qt.tools.qtcreator.7z in .*\n"
                  r"INFO    : Finished installation\n"
                  r"INFO    : Time elapsed: .* second"
              ),
          ),
          (
              "tool linux tools_qtcreator 1.2.3-0-197001020304 qt.tools.qtcreator".split(),
              "linux",
              "desktop",
              "1.2.3",
              "",
              "",
              "linux_x64/desktop/tools_qtcreator/Updates.xml",
              [
                  tool_archive("linux", "tools_qtcreator", "qt.tools.qtcreator", datetime(1970, 1, 2, 3, 4, 5, 6)),
              ],
              re.compile(
                  r"^INFO    : aqtinstall\(aqt\) v.* on Python 3.*\n"
                  r"WARNING : The command 'tool' is deprecated and marked for removal in a future version of aqt.\n"
                  r"In the future, please use the command 'install-tool' instead.\n"
                  r"INFO    : Downloading qt.tools.qtcreator...\n"
                  r"Finished installation of tools_qtcreator-linux-qt.tools.qtcreator.7z in .*\n"
                  r"INFO    : Finished installation\n"
                  r"INFO    : Time elapsed: .* second"
              ),
          ),
          (
              "install 5.14.0 windows desktop win32_mingw73".split(),
              "windows",
              "desktop",
              "5.14.0",
              "win32_mingw73",
              "mingw73_32",
              "windows_x86/desktop/qt5_5140/Updates.xml",
              [
                  plain_qtbase_archive("qt.qt5.5140.win32_mingw73", "win32_mingw73"),
              ],
              re.compile(
                  r"^INFO    : aqtinstall\(aqt\) v.* on Python 3.*\n"
                  r"WARNING : The command 'install' is deprecated"
                  r" and marked for removal in a future version of aqt.\n"
                  r"In the future, please use the command 'install-qt' instead.\n"
                  r"INFO    : Downloading qtbase...\n"
                  r"Finished installation of qtbase-windows-win32_mingw73.7z in .*\n"
                  r"INFO    : Finished installation\n"
                  r"INFO    : Time elapsed: .* second"
              ),
          ),
          (
              "install-src windows desktop 5.14.2".split(),
              "windows",
              "desktop",
              "5.14.2",
              "",
              "",
              "windows_x86/desktop/qt5_5142_src_doc_examples/Updates.xml",
              [
                  MockArchive(
                      filename_7z="qtbase-everywhere-src-5.14.2.7z",
                      update_xml_name="qt.qt5.5142.src",
                      version="5.14.2",
                      contents=(
                          PatchedFile(
                              filename="Src/qtbase/QtBaseSource.cpp",
                              unpatched_content="int main(){ return 0; }",
                              patched_content=None,  # not patched
                          ),
                      ),
                  ),
              ],
              re.compile(
                  r"^INFO    : aqtinstall\(aqt\) v.* on Python 3.*\n"
                  r"WARNING : The parameter 'target' with value 'desktop' is deprecated "
                  r"and marked for removal in a future version of aqt\.\n"
                  r"In the future, please omit this parameter\.\n"
                  r"INFO    : Downloading qtbase\.\.\.\n"
                  r"Finished installation of qtbase-everywhere-src-5\.14\.2\.7z in .*\n"
                  r"INFO    : Finished installation\n"
                  r"INFO    : Time elapsed: .* second"
              ),
          ),
          (
              "install-src windows 5.14.2".split(),
              "windows",
              "desktop",
              "5.14.2",
              "",
              "",
              "windows_x86/desktop/qt5_5142_src_doc_examples/Updates.xml",
              [
                  MockArchive(
                      filename_7z="qtbase-everywhere-src-5.14.2.7z",
                      update_xml_name="qt.qt5.5142.src",
                      version="5.14.2",
                      contents=(
                          PatchedFile(
                              filename="Src/qtbase/QtBaseSource.cpp",
                              unpatched_content="int main(){ return 0; }",
                              patched_content=None,  # not patched
                          ),
                      ),
                  ),
              ],
              re.compile(
                  r"^INFO    : aqtinstall\(aqt\) v.* on Python 3.*\n"
                  r"INFO    : Downloading qtbase\.\.\.\n"
                  r"Finished installation of qtbase-everywhere-src-5\.14\.2\.7z in .*\n"
                  r"INFO    : Finished installation\n"
                  r"INFO    : Time elapsed: .* second"
              ),
          ),
          (
              "install 5.9.0 windows desktop win32_mingw53".split(),
              "windows",
              "desktop",
              "5.9.0",
              "win32_mingw53",
              "mingw53_32",
              "windows_x86/desktop/qt5_59/Updates.xml",
              [
                  plain_qtbase_archive("qt.59.win32_mingw53", "win32_mingw53"),
              ],
              re.compile(
                  r"^INFO    : aqtinstall\(aqt\) v.* on Python 3.*\n"
                  r"WARNING : The command 'install' is deprecated"
                  r" and marked for removal in a future version of aqt.\n"
                  r"In the future, please use the command 'install-qt' instead.\n"
                  r"INFO    : Downloading qtbase...\n"
                  r"Finished installation of qtbase-windows-win32_mingw53.7z in .*\n"
                  r"INFO    : Finished installation\n"
                  r"INFO    : Time elapsed: .* second"
              ),
          ),
          (
              "install-qt windows desktop 5.9.0 win32_mingw53".split(),
              "windows",
              "desktop",
              "5.9.0",
              "win32_mingw53",
              "mingw53_32",
              "windows_x86/desktop/qt5_59/Updates.xml",
              [
                  plain_qtbase_archive("qt.59.win32_mingw53", "win32_mingw53"),
              ],
              re.compile(
                  r"^INFO    : aqtinstall\(aqt\) v.* on Python 3.*\n"
                  r"INFO    : Downloading qtbase...\n"
                  r"Finished installation of qtbase-windows-win32_mingw53.7z in .*\n"
                  r"INFO    : Finished installation\n"
                  r"INFO    : Time elapsed: .* second"
              ),
          ),
          (
              "install-qt windows desktop 5.14.0 win32_mingw73".split(),
              "windows",
              "desktop",
              "5.14.0",
              "win32_mingw73",
              "mingw73_32",
              "windows_x86/desktop/qt5_5140/Updates.xml",
              [
                  plain_qtbase_archive("qt.qt5.5140.win32_mingw73", "win32_mingw73"),
              ],
              re.compile(
                  r"^INFO    : aqtinstall\(aqt\) v.* on Python 3.*\n"
                  r"INFO    : Downloading qtbase...\n"
                  r"Finished installation of qtbase-windows-win32_mingw73.7z in .*\n"
                  r"INFO    : Finished installation\n"
                  r"INFO    : Time elapsed: .* second"
              ),
          ),
          (
              "install-qt windows desktop 5.14.0 win64_mingw73 -m qtcharts".split(),
              "windows",
              "desktop",
              "5.14.0",
              "win64_mingw73",
              "mingw73_64",
              "windows_x86/desktop/qt5_5140/Updates.xml",
              [
                  plain_qtbase_archive("qt.qt5.5140.win64_mingw73", "win64_mingw73"),
                  qtcharts_module("5.14.0", "win64_mingw73"),
              ],
              re.compile(
                  r"^INFO    : aqtinstall\(aqt\) v.* on Python 3.*\n"
                  r"INFO    : Downloading qtbase...\n"
                  r"Finished installation of qtbase-windows-win64_mingw73.7z in .*\n"
                  r"INFO    : Downloading qtcharts...\n"
                  r"Finished installation of qtcharts-windows-win64_mingw73.7z in .*\n"
                  r"INFO    : Finished installation\n"
                  r"INFO    : Time elapsed: .* second"
              ),
          ),
          (
              "install-qt windows desktop 6.2.0 win64_mingw73 -m qtpositioning --noarchives".split(),
              "windows",
              "desktop",
              "6.2.0",
              "win64_mingw73",
              "mingw73_64",
              "windows_x86/desktop/qt6_620/Updates.xml",
              [
                  plain_qtbase_archive("qt.qt6.620.win64_mingw73", "win64_mingw73", should_install=False),
                  qtpositioning_module("6.2.0", "win64_mingw73"),
              ],
              re.compile(
                  r"^INFO    : aqtinstall\(aqt\) v.* on Python 3.*\n"
                  r"INFO    : Downloading qtlocation...\n"
                  r"Finished installation of qtlocation-windows-win64_mingw73.7z in .*\n"
                  r"INFO    : Finished installation\n"
                  r"INFO    : Time elapsed: .* second"
              ),
          ),
          (
              "install-qt windows desktop 6.1.0 win64_mingw81 -m qtcharts".split(),
              "windows",
              "desktop",
              "6.1.0",
              "win64_mingw81",
              "mingw81_64",
              "windows_x86/desktop/qt6_610/Updates.xml",
              [
                  plain_qtbase_archive("qt.qt6.610.win64_mingw81", "win64_mingw81"),
                  qtcharts_module("6.1.0", "win64_mingw81"),
              ],
              re.compile(
                  r"^INFO    : aqtinstall\(aqt\) v.* on Python 3.*\n"
                  r"INFO    : Downloading qtbase...\n"
                  r"Finished installation of qtbase-windows-win64_mingw81.7z in .*\n"
                  r"INFO    : Downloading qtcharts...\n"
                  r"Finished installation of qtcharts-windows-win64_mingw81.7z in .*\n"
                  r"INFO    : Finished installation\n"
                  r"INFO    : Time elapsed: .* second"
              ),
          ),
          (
              "install-qt windows android 6.1.0 android_armv7".split(),
              "windows",
              "android",
              "6.1.0",
              "android_armv7",
              "android_armv7",
              "windows_x86/android/qt6_610_armv7/Updates.xml",
              [
                  MockArchive(
                      filename_7z="qtbase-windows-android_armv7.7z",
                      update_xml_name="qt.qt6.610.android_armv7",
                      contents=(
                          # Qt 6 non-desktop should patch qconfig.pri, qmake script and target_qt.conf
                          PatchedFile(
                              filename="mkspecs/qconfig.pri",
                              unpatched_content="... blah blah blah ...\n"
                              "QT_EDITION = Not OpenSource\n"
                              "QT_LICHECK = Not Empty\n"
                              "... blah blah blah ...\n",
                              patched_content="... blah blah blah ...\n"
                              "QT_EDITION = OpenSource\n"
                              "QT_LICHECK =\n"
                              "... blah blah blah ...\n",
                          ),
                          PatchedFile(
                              filename="bin/target_qt.conf",
                              unpatched_content="Prefix=/Users/qt/work/install/target\n"
                              "HostPrefix=../../\n"
                              "HostData=target\n",
                              patched_content="Prefix={base_dir}{sep}6.1.0{sep}android_armv7{sep}target\n"
                              "HostPrefix=../../mingw81_64\n"
                              "HostData=../android_armv7\n",
                          ),
                          PatchedFile(
                              filename="bin/qmake.bat",
                              unpatched_content="... blah blah blah ...\n"
                              "/Users/qt/work/install/bin\n"
                              "... blah blah blah ...\n",
                              patched_content="... blah blah blah ...\n"
                              "{base_dir}\\6.1.0\\mingw81_64\\bin\n"
                              "... blah blah blah ...\n",
                          ),
                      ),
                  ),
              ],
              re.compile(
                  r"^INFO    : aqtinstall\(aqt\) v.* on Python 3.*\n"
                  r"INFO    : Downloading qtbase...\n"
                  r"Finished installation of qtbase-windows-android_armv7.7z in .*\n"
                  r"INFO    : Patching .*6\.1\.0[/\\]android_armv7[/\\]bin[/\\]qmake.bat\n"
                  r"INFO    : Finished installation\n"
                  r"INFO    : Time elapsed: .* second"
              ),
          ),
          (
              "install-qt linux android 6.3.0 android_arm64_v8a".split(),
              "linux",
              "android",
              "6.3.0",
              "android_arm64_v8a",
              "android_arm64_v8a",
              "linux_x64/android/qt6_630_arm64_v8a/Updates.xml",
              [
                  MockArchive(
                      filename_7z="qtbase-linux-android_arm64_v8a.7z",
                      update_xml_name="qt.qt6.630.android_arm64_v8a",
                      contents=(
                          # Qt 6 non-desktop should patch qconfig.pri, qmake script and target_qt.conf
                          PatchedFile(
                              filename="mkspecs/qconfig.pri",
                              unpatched_content="... blah blah blah ...\n"
                              "QT_EDITION = Not OpenSource\n"
                              "QT_LICHECK = Not Empty\n"
                              "... blah blah blah ...\n",
                              patched_content="... blah blah blah ...\n"
                              "QT_EDITION = OpenSource\n"
                              "QT_LICHECK =\n"
                              "... blah blah blah ...\n",
                          ),
                          PatchedFile(
                              filename="bin/target_qt.conf",
                              unpatched_content="Prefix=/home/qt/work/install/target\n"
                              "HostPrefix=../../\n"
                              "HostData=target\n",
                              patched_content="Prefix={base_dir}{sep}6.3.0{sep}android_arm64_v8a{sep}target\n"
                              "HostPrefix=../../gcc_64\n"
                              "HostData=../android_arm64_v8a\n",
                          ),
                          PatchedFile(
                              filename="bin/qmake",
                              unpatched_content="... blah blah blah ...\n"
                              "/home/qt/work/install/bin\n"
                              "... blah blah blah ...\n",
                              patched_content="... blah blah blah ...\n"
                              "{base_dir}/6.3.0/gcc_64/bin\n"
                              "... blah blah blah ...\n",
                          ),
                      ),
                  ),
              ],
              re.compile(
                  r"^INFO    : aqtinstall\(aqt\) v.* on Python 3.*\n"
                  r"INFO    : Downloading qtbase...\n"
                  r"Finished installation of qtbase-linux-android_arm64_v8a.7z in .*\n"
                  r"INFO    : Patching .*6\.3\.0[/\\]android_arm64_v8a[/\\]bin[/\\]qmake\n"
                  r"INFO    : Finished installation\n"
                  r"INFO    : Time elapsed: .* second"
              ),
          ),
          (
              "install-qt mac ios 6.1.2".split(),
              "mac",
              "ios",
              "6.1.2",
              "ios",
              "ios",
              "mac_x64/ios/qt6_612/Updates.xml",
              [
                  MockArchive(
                      filename_7z="qtbase-mac-ios.7z",
                      update_xml_name="qt.qt6.612.ios",
                      contents=(
                          # Qt 6 non-desktop should patch qconfig.pri, qmake script and target_qt.conf
                          PatchedFile(
                              filename="mkspecs/qconfig.pri",
                              unpatched_content="... blah blah blah ...\n"
                              "QT_EDITION = Not OpenSource\n"
                              "QT_LICHECK = Not Empty\n"
                              "... blah blah blah ...\n",
                              patched_content="... blah blah blah ...\n"
                              "QT_EDITION = OpenSource\n"
                              "QT_LICHECK =\n"
                              "... blah blah blah ...\n",
                          ),
                          PatchedFile(
                              filename="bin/target_qt.conf",
                              unpatched_content="Prefix=/Users/qt/work/install/target\n"
                              "HostPrefix=../../\n"
                              "HostData=target\n",
                              patched_content="Prefix={base_dir}{sep}6.1.2{sep}ios{sep}target\n"
                              "HostPrefix=../../macos\n"
                              "HostData=../ios\n",
                          ),
                          PatchedFile(
                              filename="bin/qmake",
                              unpatched_content="... blah blah blah ...\n"
                              "/Users/qt/work/install/bin\n"
                              "... blah blah blah ...\n",
                              patched_content="... blah blah blah ...\n"
                              "{base_dir}/6.1.2/macos/bin\n"
                              "... blah blah blah ...\n",
                          ),
                      ),
                  ),
              ],
              re.compile(
                  r"^INFO    : aqtinstall\(aqt\) v.* on Python 3.*\n"
                  r"INFO    : Downloading qtbase...\n"
                  r"Finished installation of qtbase-mac-ios.7z in .*\n"
                  r"INFO    : Patching .*6\.1\.2[/\\]ios[/\\]bin[/\\]qmake\n"
                  r"INFO    : Finished installation\n"
                  r"INFO    : Time elapsed: .* second"
              ),
          ),
      ),
  )
  def test_install(
      monkeypatch,
      capsys,
      cmd: List[str],
      host: str,
      target: str,
      version: str,
      arch: str,
      arch_dir: str,
      updates_url: str,
      archives: List[MockArchive],
      expect_out,  # type: re.Pattern
  ):
  
      # For convenience, fill in version and arch dir: prevents repetitive data declarations
      for i in range(len(archives)):
          archives[i].version = version
          archives[i].arch_dir = arch_dir
  
      mock_get_url, mock_download_archive = make_mock_geturl_download_archive(archives, arch, host, updates_url)
      monkeypatch.setattr("aqt.archives.getUrl", mock_get_url)
      monkeypatch.setattr("aqt.helper.getUrl", mock_get_url)
      monkeypatch.setattr("aqt.installer.downloadBinaryFile", mock_download_archive)
  
      with TemporaryDirectory() as output_dir:
          cli = Cli()
          cli._setup_settings()
  
          assert 0 == cli.run(cmd + ["--outputdir", output_dir])
  
          out, err = capsys.readouterr()
          sys.stdout.write(out)
          sys.stderr.write(err)
  
          assert expect_out.match(err)
  
          installed_path = Path(output_dir) / version / arch_dir
          if version == "5.9.0":
              installed_path = Path(output_dir) / "5.9" / arch_dir
          assert installed_path.is_dir()
          for archive in archives:
              if not archive.should_install:
                  continue
              for patched_file in archive.contents:
                  file_path = installed_path / patched_file.filename
                  assert file_path.is_file()
  
                  expect_content = patched_file.expected_content(base_dir=output_dir, sep=os.sep)
                  actual_content = file_path.read_text(encoding="utf_8")
                  assert actual_content == expect_content
  
  
  @pytest.mark.parametrize(
      "cmd, xml_file, expected",
      (
          (
              "install-qt windows desktop 5.16.0 win32_mingw73",
              None,
              "WARNING : Specified Qt version is unknown: 5.16.0.\n"
              "ERROR   : Failed to locate XML data for Qt version '5.16.0'.\n"
              "==============================Suggested follow-up:==============================\n"
              "* Please use 'aqt list-qt windows desktop' to show versions available.\n",
          ),
          (
              "install-qt windows desktop 5.15.0 bad_arch",
              "windows-5150-update.xml",
              "WARNING : Specified target combination is not valid or unknown: windows desktop bad_arch\n"
              "ERROR   : The packages ['qt_base'] were not found while parsing XML of package information!\n"
              "==============================Suggested follow-up:==============================\n"
              "* Please use 'aqt list-qt windows desktop --arch 5.15.0' to show architectures available.\n",
          ),
          (
              "install-qt windows desktop 5.15.0 win32_mingw73 -m nonexistent foo",
              "windows-5150-update.xml",
              "WARNING : Some of specified modules are unknown.\n"
              "ERROR   : The packages ['foo', 'nonexistent', 'qt_base'] were not found"
              " while parsing XML of package information!\n"
              "==============================Suggested follow-up:==============================\n"
              "* Please use 'aqt list-qt windows desktop --arch 5.15.0' to show architectures available.\n"
              "* Please use 'aqt list-qt windows desktop --modules 5.15.0 <arch>' to show modules available.\n",
          ),
          (
              "install-doc windows desktop 5.15.0 -m nonexistent foo",
              "windows-5152-src-doc-example-update.xml",
              "WARNING : The parameter 'target' with value 'desktop' is deprecated"
              " and marked for removal in a future "
              "version of aqt.\n"
              "In the future, please omit this parameter.\n"
              "ERROR   : The packages ['doc', 'foo', 'nonexistent'] were not found"
              " while parsing XML of package information!\n"
              "==============================Suggested follow-up:==============================\n"
              "* Please use 'aqt list-doc windows 5.15.0 --modules' to show modules available.\n",
          ),
          (
              "install-doc windows 5.15.0 -m nonexistent foo",
              "windows-5152-src-doc-example-update.xml",
              "ERROR   : The packages ['doc', 'foo', 'nonexistent'] were not found"
              " while parsing XML of package information!\n"
              "==============================Suggested follow-up:==============================\n"
              "* Please use 'aqt list-doc windows 5.15.0 --modules' to show modules available.\n",
          ),
          (
              "install-example windows 5.15.0 -m nonexistent foo",
              "windows-5152-src-doc-example-update.xml",
              "ERROR   : The packages ['examples', 'foo', 'nonexistent'] were not found"
              " while parsing XML of package information!\n"
              "==============================Suggested follow-up:==============================\n"
              "* Please use 'aqt list-example windows 5.15.0 --modules' to show modules available.\n",
          ),
          (
              "install-tool windows desktop tools_vcredist nonexistent",
              "windows-desktop-tools_vcredist-update.xml",
              "WARNING : Specified target combination is not valid: windows tools_vcredist nonexistent\n"
              "ERROR   : The package 'nonexistent' was not found while parsing XML of package information!\n"
              "==============================Suggested follow-up:==============================\n"
              "* Please use 'aqt list-tool windows desktop tools_vcredist' to show tool variants available.\n",
          ),
          (
              "install-tool windows desktop tools
  
  [... the rest is omitted ...]
files: tests/test_install.py