Repository: miurahr/aqtinstall

Issue:
Android installs missing gcc_64 directory
If you install Android Qt6 builds on linux like (this is how the github action works):
`python3 -m aqt install 6.3.0 linux android android_armv7 -O /home/runner/work/GPXSee/Qt`
the resulting Qt installation is broken as it is missing the gcc_64 directory which contains the actual
Qt binaries (in android_armv7 there are only symlinks) like qmake.

As a result, qmake-based builds are not possible with Qt installed using aqtinstall.


Discussion:
ddalcino: I'm not sure what's going on with the CI run here. The `tox` run is failing because of a problem loading `isort`; it's being tracked in several places, including here: https://github.com/gforcada/flake8-isort/issues/115 There's a workaround people are talking about, and I think it involves pinning the versions of `flake8` and `flake8-isort`. I am unable to get it to work though.
ddalcino: I think this PR is done for now, except for a couple of things:

1. It should probably be squashed into fewer commits
2. I’m not really happy about the way QtRepoProperty is turning out. It definitely needs its own file, but right now it cannot be moved without breaking out Version and a couple other things without creating dependency cycles. This would split metadata.py into at least 4 different files.
3. There’s some question about whether—autodesktop should be opt-in or opt-out. Ideally, it should be opt-out, but that cannot be done without breaking current interface 
miurahr: I'd like to publish this improvements as v3.0.0beta/rc versions and ask users to test their environments.
It is better to recreate  commit history for future maintenance before merge.
miurahr: I'd like to merge here to master. @ddalcino could you re-create commit history and fix merge conflict?
miurahr: I think it worth squashing commits to merge.

Patch:
diff --git a/aqt/installer.py b/aqt/installer.py
index 0ecc94d..94c115f 100644
--- a/aqt/installer.py
+++ b/aqt/installer.py
@@ -35,10 +35,10 @@ from logging import getLogger
 from logging.handlers import QueueHandler
 from pathlib import Path
 from tempfile import TemporaryDirectory
-from typing import List, Optional
+from typing import List, Optional, Tuple
 
 import aqt
-from aqt.archives import QtArchives, QtPackage, SrcDocExamplesArchives, ToolArchives
+from aqt.archives import QtArchives, QtPackage, SrcDocExamplesArchives, TargetConfig, ToolArchives
 from aqt.exceptions import (
     AqtException,
     ArchiveChecksumError,
@@ -59,7 +59,7 @@ from aqt.helper import (
     setup_logging,
 )
 from aqt.metadata import ArchiveId, MetadataFactory, QtRepoProperty, SimpleSpec, Version, show_list, suggested_follow_up
-from aqt.updater import Updater
+from aqt.updater import Updater, dir_for_version
 
 try:
     import py7zr
@@ -299,6 +299,23 @@ class Cli:
             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
+        should_autoinstall: bool = args.autodesktop
+        _version = Version(qt_version)
+        base_path = Path(base_dir)
+
+        expect_desktop_archdir, autodesk_arch = self._get_autodesktop_dir_and_arch(
+            should_autoinstall, os_name, target, base_path, _version
+        )
+
+        auto_desktop_archives: List[QtPackage] = (
+            retry_on_bad_connection(
+                lambda base_url: QtArchives(os_name, "desktop", qt_version, autodesk_arch, base=base_url, timeout=timeout),
+                base,
+            ).archives
+            if autodesk_arch is not None
+            else []
+        )
+
         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):
@@ -309,7 +326,7 @@ class Cli:
         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(
+        qt_archives: QtArchives = retry_on_bad_connection(
             lambda base_url: QtArchives(
                 os_name,
                 target,
@@ -324,12 +341,17 @@ class Cli:
             ),
             base,
         )
+        qt_archives.archives.extend(auto_desktop_archives)
         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)
+            Updater.update(target_config, base_path, expect_desktop_archdir)
+            if autodesk_arch is not None:
+                d_target_config = TargetConfig(str(_version), "desktop", autodesk_arch, os_name)
+                Updater.update(d_target_config, base_path, expect_desktop_archdir)
         self.logger.info("Finished installation")
         self.logger.info("Time elapsed: {time:.8f} second".format(time=time.perf_counter() - start_time))
 
@@ -603,6 +625,12 @@ class Cli:
             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)
@@ -941,6 +969,38 @@ class Cli:
         except ValueError as e:
             raise CliInputError(f"Invalid version: '{version_str}'! Please use the form '5.X.Y'.") from e
 
+    def _get_autodesktop_dir_and_arch(
+        self, should_autoinstall: bool, host: str, target: str, base_path: Path, version: Version
+    ) -> Tuple[Optional[str], Optional[str]]:
+        """Returns expected_desktop_arch_dir, desktop_arch_to_install"""
+        if target in ["ios", "android"]:
+            installed_desktop_arch_dir = QtRepoProperty.find_installed_desktop_qt_dir(host, base_path, version)
+            if installed_desktop_arch_dir:
+                # An acceptable desktop Qt is already installed, so don't do anything.
+                self.logger.info(f"Found installed {host}-desktop Qt at {installed_desktop_arch_dir}")
+                return installed_desktop_arch_dir.name, None
+
+            default_desktop_arch = MetadataFactory(ArchiveId("qt", host, "desktop")).fetch_default_desktop_arch(version)
+            desktop_arch_dir = QtRepoProperty.get_arch_dir_name(host, default_desktop_arch, version)
+            expected_desktop_arch_path = base_path / dir_for_version(version) / desktop_arch_dir
+            if should_autoinstall:
+                # No desktop Qt is installed, but the user has requested installation. Find out what to install.
+                self.logger.info(
+                    f"You are installing the {target} version of Qt, which requires that the desktop version of Qt "
+                    f"is also installed. Now installing Qt: desktop {version} {default_desktop_arch}"
+                )
+                return expected_desktop_arch_path.name, default_desktop_arch
+            else:
+                self.logger.warning(
+                    f"You are installing the {target} version of Qt, which requires that the desktop version of Qt "
+                    f"is also installed. You can install it with the following command:\n"
+                    f"          `aqt install-qt {host} desktop {version} {default_desktop_arch}`"
+                )
+                return expected_desktop_arch_path.name, None
+        else:
+            # We do not need to worry about the desktop directory if target is not mobile.
+            return None, None
+
 
 def is_64bit() -> bool:
     """check if running platform is 64bit python."""
diff --git a/aqt/metadata.py b/aqt/metadata.py
index 416b916..ba71406 100644
--- a/aqt/metadata.py
+++ b/aqt/metadata.py
@@ -25,7 +25,9 @@ import re
 import secrets as random
 import shutil
 from abc import ABC, abstractmethod
+from functools import reduce
 from logging import getLogger
+from pathlib import Path
 from typing import Callable, Dict, Generator, Iterable, Iterator, List, Optional, Tuple, Union
 from urllib.parse import ParseResult, urlparse
 from xml.etree.ElementTree import Element
@@ -374,6 +376,39 @@ class QtRepoProperty:
     Intended to help decouple the logic of aqt from specific properties of the Qt repository.
     """
 
+    @staticmethod
+    def dir_for_version(ver: Version) -> str:
+        return "5.9" if ver == Version("5.9.0") else f"{ver.major}.{ver.minor}.{ver.patch}"
+
+    @staticmethod
+    def get_arch_dir_name(host: str, arch: str, version: Version) -> str:
+        if arch is None:
+            return ""
+        elif arch.startswith("win64_mingw"):
+            return arch[6:] + "_64"
+        elif arch.startswith("win32_mingw"):
+            return arch[6:] + "_32"
+        elif arch.startswith("win"):
+            m = re.match(r"win\d{2}_(?P<msvc>msvc\d{4})_(?P<winrt>winrt_x\d{2})", arch)
+            if m:
+                return f"{m.group('winrt')}_{m.group('msvc')}"
+            else:
+                return arch[6:]
+        elif host == "mac" and arch == "clang_64":
+            return QtRepoProperty.default_desktop_arch_dir(host, version)
+        else:
+            return arch
+
+    @staticmethod
+    def default_desktop_arch_dir(host: str, version: Union[Version, str]) -> str:
+        version: Version = version if isinstance(version, Version) else Version(version)
+        if host == "linux":
+            return "gcc_64"
+        elif host == "mac":
+            return "macos" if version in SimpleSpec(">=6.1.2") else "clang_64"
+        else:  # Windows
+            raise NotImplementedError("This function should not be called in this way!")
+
     @staticmethod
     def extension_for_arch(architecture: str, is_version_ge_6: bool) -> str:
         if architecture == "wasm_32":
@@ -395,6 +430,66 @@ class QtRepoProperty:
             return [ext_lt_6]
         return [ext_lt_6, ext_ge_6]
 
+    # Architecture, as reported in Updates.xml
+    MINGW_ARCH_PATTERN = re.compile(r"^win(?P<bits>\d+)_mingw(?P<version>\d+)?$")
+    # Directory that corresponds to an architecture
+    MINGW_DIR_PATTERN = re.compile(r"^mingw(?P<version>\d+)?_(?P<bits>\d+)$")
+
+    @staticmethod
+    def select_default_mingw(mingw_arches: List[str], is_dir: bool) -> Optional[str]:
+        """
+        Selects a default architecture from a non-empty list of mingw architectures, matching the pattern
+        MetadataFactory.MINGW_ARCH_PATTERN. Meant to be called on a list of installed mingw architectures,
+        or a list of architectures available for installation.
+        """
+
+        ArchBitsVer = Tuple[str, int, Optional[int]]
+        pattern = QtRepoProperty.MINGW_DIR_PATTERN if is_dir else QtRepoProperty.MINGW_ARCH_PATTERN
+
+        def mingw_arch_with_bits_and_version(arch: str) -> Optional[ArchBitsVer]:
+            match = pattern.match(arch)
+            if not match:
+                return None
+            bits = int(match.group("bits"))
+            ver = None if not match.group("version") else int(match.group("version"))
+            return arch, bits, ver
+
+        def select_superior_arch(lhs: ArchBitsVer, rhs: ArchBitsVer) -> ArchBitsVer:
+            _, l_bits, l_ver = lhs
+            _, r_bits, r_ver = rhs
+            if l_bits != r_bits:
+                return lhs if l_bits > r_bits else rhs
+            elif r_ver is None:
+                return lhs
+            elif l_ver is None:
+                return rhs
+            return lhs if l_ver > r_ver else rhs
+
+        candidates: List[ArchBitsVer] = list(filter(None, map(mingw_arch_with_bits_and_version, mingw_arches)))
+        if len(candidates) == 0:
+            return None
+        default_arch, _, _ = reduce(select_superior_arch, candidates)
+        return default_arch
+
+    @staticmethod
+    def find_installed_desktop_qt_dir(host: str, base_path: Path, version: Version) -> Optional[Path]:
+        """
+        Locates the default installed desktop qt directory, somewhere in base_path.
+        """
+        installed_qt_version_dir = base_path / QtRepoProperty.dir_for_version(version)
+        if host != "windows":
+            arch_path = installed_qt_version_dir / QtRepoProperty.default_desktop_arch_dir(host, version)
+            return arch_path if (arch_path / "bin/qmake").is_file() else None
+
+        def contains_qmake_exe(arch_path: Path) -> bool:
+            return (arch_path / "bin/qmake.exe").is_file()
+
+        paths = [d for d in installed_qt_version_dir.glob("mingw*")]
+        directories = list(filter(contains_qmake_exe, paths))
+        arch_dirs = [d.name for d in directories]
+        selected_dir = QtRepoProperty.select_default_mingw(arch_dirs, is_dir=True)
+        return installed_qt_version_dir / selected_dir if selected_dir else None
+
 
 class MetadataFactory:
     """Retrieve metadata of Qt variations, versions, and descriptions from Qt site."""
@@ -825,6 +920,18 @@ class MetadataFactory:
             return str(self.archive_id)
         return "{} with spec {}".format(self.archive_id, self.spec)
 
+    def fetch_default_desktop_arch(self, version: Version) -> str:
+        assert self.archive_id.target == "desktop", "This function is meant to fetch desktop architectures"
+        if self.archive_id.host == "linux":
+            return "gcc_64"
+        elif self.archive_id.host == "mac":
+            return "clang_64"
+        arches = list(filter(lambda arch: QtRepoProperty.MINGW_ARCH_PATTERN.match(arch), self.fetch_arches(version)))
+        selected_arch = QtRepoProperty.select_default_mingw(arches, is_dir=False)
+        if not selected_arch:
+            raise EmptyMetadata("No default desktop architecture available")
+        return selected_arch
+
 
 def suggested_follow_up(meta: MetadataFactory) -> List[str]:
     """Makes an informed guess at what the user got wrong, in the event of an error."""
diff --git a/aqt/updater.py b/aqt/updater.py
index 86f78b9..37b8db6 100644
--- a/aqt/updater.py
+++ b/aqt/updater.py
@@ -20,34 +20,20 @@
 # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 import logging
 import os
-import pathlib
-import re
 import subprocess
 from logging import getLogger
-from typing import Union
+from pathlib import Path
+from typing import Optional
 
 import patch
 
 from aqt.archives import TargetConfig
 from aqt.exceptions import UpdaterError
 from aqt.helper import Settings
-from aqt.metadata import SimpleSpec, Version
+from aqt.metadata import QtRepoProperty, SimpleSpec, Version
 
-
-def default_desktop_arch_dir(host: str, version: Union[Version, str]) -> str:
-    version: Version = version if isinstance(version, Version) else Version(version)
-    if host == "linux":
-        return "gcc_64"
-    elif host == "mac":
-        return "macos" if version in SimpleSpec(">=6.1.2") else "clang_64"
-    else:  # Windows
-        # This is a temporary solution. This arch directory cannot exist for many versions of Qt.
-        # TODO: determine this dynamically
-        return "mingw81_64"
-
-
-def dir_for_version(ver: Version) -> str:
-    return "5.9" if ver == Version("5.9.0") else f"{ver.major}.{ver.minor}.{ver.patch}"
+default_desktop_arch_dir = QtRepoProperty.default_desktop_arch_dir
+dir_for_version = QtRepoProperty.dir_for_version
 
 
 def unpatched_path(os_name: str, final_component: str) -> str:
@@ -55,13 +41,13 @@ def unpatched_path(os_name: str, final_component: str) -> str:
 
 
 class Updater:
-    def __init__(self, prefix: pathlib.Path, logger):
+    def __init__(self, prefix: Path, logger):
         self.logger = logger
         self.prefix = prefix
         self.qmake_path = None
         self.qconfigs = {}
 
-    def _patch_binfile(self, file: pathlib.Path, key: bytes, newpath: bytes):
+    def _patch_binfile(self, file: Path, key: bytes, newpath: bytes):
         """Patch binary file with key/value"""
         st = file.stat()
         data = file.read_bytes()
@@ -76,7 +62,7 @@ class Updater:
         file.write_bytes(data)
         os.chmod(str(file), st.st_mode)
 
-    def _append_string(self, file: pathlib.Path, val: str):
+    def _append_string(self, file: Path, val: str):
         """Append string to file"""
         st = file.stat()
         data = file.read_text("UTF-8")
@@ -84,7 +70,7 @@ class Updater:
         file.write_text(data, "UTF-8")
         os.chmod(str(file), st.st_mode)
 
-    def _patch_textfile(self, file: pathlib.Path, old: str, new: str):
+    def _patch_textfile(self, file: Path, old: str, new: str):
         st = file.stat()
         data = file.read_text("UTF-8")
         data = data.replace(old, new)
@@ -181,10 +167,9 @@ class Updater:
                 newpath=bytes(str(self.prefix), "UTF-8"),
             )
 
-    def patch_qmake_script(self, base_dir, qt_version: str, os_name):
-        arch_dir = default_desktop_arch_dir(os_name, qt_version)
+    def patch_qmake_script(self, base_dir, qt_version: str, os_name: str, desktop_arch_dir: str):
         sep = "\\" if os_name == "windows" else "/"
-        patched = sep.join([base_dir, qt_version, arch_dir, "bin"])
+        patched = sep.join([base_dir, qt_version, desktop_arch_dir, "bin"])
         unpatched = unpatched_path(os_name, "bin")
         qmake_path = self.prefix / "bin" / ("qmake.bat" if os_name == "windows" else "qmake")
         self.logger.info(f"Patching {qmake_path}")
@@ -225,7 +210,7 @@ class Updater:
             f.write("cd /D {}\n".format(os.path.join(base_dir, qt_version, arch_dir)))
             f.write("echo Remember to call vcvarsall.bat to complete environment setup!\n")
 
-    def set_license(self, base_dir, qt_version, arch_dir):
+    def set_license(self, base_dir: str, qt_version: str, arch_dir: str):
         """Update qtconfig.pri as OpenSource"""
         with open(os.path.join(base_dir, qt_version, arch_dir, "mkspecs", "qconfig.pri"), "r+") as f:
             lines = f.readlines()
@@ -238,46 +223,37 @@ class Updater:
                     line = "QT_LICHECK =\n"
                 f.write(line)
 
-    def patch_target_qt_conf(self, base_dir, qt_version, arch_dir, os_name):
+    def patch_target_qt_conf(self, base_dir: str, qt_version: str, arch_dir: str, os_name: str, desktop_arch_dir: str):
         target_qt_conf = self.prefix / "bin" / "target_qt.conf"
         old_targetprefix = f'Prefix={unpatched_path(os_name, "target")}'
-        new_hostprefix = f"HostPrefix=../../{default_desktop_arch_dir(os_name, qt_version)}"
-        new_targetprefix = "Prefix={}".format(str(pathlib.Path(base_dir).joinpath(qt_version, arch_dir, "target")))
+        new_hostprefix = f"HostPrefix=../../{desktop_arch_dir}"
+        new_targetprefix = "Prefix={}".format(str(Path(base_dir).joinpath(qt_version, arch_dir, "target")))
         new_hostdata = "HostData=../{}".format(arch_dir)
         self._patch_textfile(target_qt_conf, old_targetprefix, new_targetprefix)
         self._patch_textfile(target_qt_conf, "HostPrefix=../../", new_hostprefix)
         self._patch_textfile(target_qt_conf, "HostData=target", new_hostdata)
 
     @classmethod
-    def update(cls, target: TargetConfig, base_dir: str):
+    def update(cls, target: TargetConfig, base_path: Path, installed_desktop_arch_dir: Optional[str]):
         """
         Make Qt configuration files, qt.conf and qtconfig.pri.
         And update pkgconfig and patch Qt5Core and qmake
+
+        :param installed_desktop_arch_dir:  This is the path to a desktop Qt  installation, like `Qt/6.3.0/mingw_win64`.
+                                            This may or may not contain an actual desktop Qt installation.
+                                            If it does not, the Updater will patch files in a mobile Qt installation
+                                            that point to this directory, and this installation will be non-functional
+                                            until the user installs a desktop Qt in this directory.
         """
         logger = getLogger("aqt.updater")
         arch = target.arch
         version = Version(target.version)
         os_name = target.os_name
         version_dir = dir_for_version(version)
-        if arch is None:
-            arch_dir = ""
-        elif arch.startswith("win64_mingw"):
-            arch_dir = arch[6:] + "_64"
-        elif arch.startswith("win32_mingw"):
-            arch_dir = arch[6:] + "_32"
-        elif arch.startswith("win"):
-            m = re.match(r"win\d{2}_(msvc\d{4})_(winrt_x\d{2})", arch)
-            if m:
-                a, b = m.groups()
-                arch_dir = b + "_" + a
-            else:
-                arch_dir = arch[6:]
-        elif os_name == "mac" and arch == "clang_64":
-            arch_dir = default_desktop_arch_dir(os_name, version)
-        else:
-            arch_dir = arch
+        arch_dir = QtRepoProperty.get_arch_dir_name(os_name, arch, version)
+        base_dir = str(base_path)
         try:
-            prefix = pathlib.Path(base_dir) / version_dir / arch_dir
+            prefix = base_path / version_dir / arch_dir
             updater = Updater(prefix, logger)
             updater.set_license(base_dir, version_dir, arch_dir)
             if target.arch not in [
@@ -304,8 +280,14 @@ class Updater:
             elif version in SimpleSpec(">=5.0,<6.0"):
                 updater.patch_qmake()
             else:  # qt6 non-desktop
-                updater.patch_qmake_script(base_dir, version_dir, target.os_name)
-                updater.patch_target_qt_conf(base_dir, version_dir, arch_dir, target.os_name)
+                desktop_arch_dir = (
+                    installed_desktop_arch_dir
+                    if installed_desktop_arch_dir is not None
+                    else default_desktop_arch_dir(os_name, version)
+                )
+
+                updater.patch_qmake_script(base_dir, version_dir, target.os_name, desktop_arch_dir)
+                updater.patch_target_qt_conf(base_dir, version_dir, arch_dir, target.os_name, desktop_arch_dir)
         except IOError as e:
             raise UpdaterError(f"Updater caused an IO error: {e}") from e
 
diff --git a/ci/generate_azure_pipelines_matrices.py b/ci/generate_azure_pipelines_matrices.py
index b6bd545..307a082 100644
--- a/ci/generate_azure_pipelines_matrices.py
+++ b/ci/generate_azure_pipelines_matrices.py
@@ -32,6 +32,7 @@ class BuildJob:
         list_options=None,
         spec=None,
         mingw_variant: str = "",
+        is_autodesktop: bool = False,
         tool_options: Optional[Dict[str, str]] = None,
         check_output_cmd: Optional[str] = None,
     ):
@@ -45,6 +46,7 @@ class BuildJob:
         self.mirror = mirror
         self.subarchives = subarchives
         self.mingw_variant: str = mingw_variant
+        self.is_autodesktop: bool = is_autodesktop
         self.list_options = list_options if list_options else {}
         self.tool_options: Dict[str, str] = tool_options if tool_options else {}
         # `steps.yml` assumes that qt_version is the highest version that satisfies spec
@@ -268,15 +270,19 @@ mac_build_jobs.append(
 # mobile SDK
 mac_build_jobs.extend(
     [
-        BuildJob("install-qt", "5.15.2", "mac", "ios", "ios", "ios"),
-        BuildJob("install-qt", "6.2.2", "mac", "ios", "ios", "ios", module="qtsensors"),
-        BuildJob(
-            "install-qt", "6.1.0", "mac", "android", "android_armv7", "android_armv7"
-        ),
+        BuildJob("install-qt", "5.15.2", "mac", "ios", "ios", "ios", is_autodesktop=True),
+        BuildJob("install-qt", "6.2.2", "mac", "ios", "ios", "ios", module="qtsensors", is_autodesktop=False),
+        BuildJob("install-qt", "6.1.0", "mac", "android", "android_armv7", "android_armv7", is_autodesktop=True),
     ]
 )
 linux_build_jobs.extend(
-    [BuildJob("install-qt", "6.1.0", "linux", "android", "android_armv7", "android_armv7")]
+    [BuildJob("install-qt", "6.1.0", "linux", "android", "android_armv7", "android_armv7", is_autodesktop=True)]
+)
+
+# Qt 6.3.0 for Windows-Android has win64_mingw available, but not win64_mingw81.
+# This will test that the path to mingw is not hardcoded.
+windows_build_jobs.extend(
+    [BuildJob("install-qt", "6.3.0", "windows", "android", "android_armv7", "android_armv7", is_autodesktop=True)]
 )
 
 # Test binary patch of qmake
@@ -368,6 +374,7 @@ for platform_build_job in all_platform_build_jobs:
                 ("SPEC", build_job.spec if build_job.spec else ""),
                 ("MINGW_VARIANT", build_job.mingw_variant),
                 ("MINGW_FOLDER", build_job.mingw_folder()),
+                ("IS_AUTODESKTOP", str(build_job.is_autodesktop)),
                 ("HAS_EXTENSIONS", build_job.list_options.get("HAS_EXTENSIONS", "False")),
                 ("USE_EXTENSION", build_job.list_options.get("USE_EXTENSION", "None")),
                 ("OUTPUT_DIR", build_job.output_dir if build_job.output_dir else ""),
diff --git a/ci/steps.yml b/ci/steps.yml
index aaa0d58..752f51e 100644
--- a/ci/steps.yml
+++ b/ci/steps.yml
@@ -45,12 +45,9 @@ steps:
         if [[ "$(SUBARCHIVES)" != "" ]]; then
           opt+=" --archives $(SUBARCHIVES)"
         fi
-        if [[ "$(SPEC)" == "" ]]; then
-          python -m aqt install-qt $(HOST) $(TARGET) $(QT_VERSION) $(ARCH) $opt
-        else
-          python -m aqt install-qt $(HOST) $(TARGET) "$(SPEC)" $(ARCH) $opt
-        fi
-        if [[ "$(TARGET)" == "android" || "$(TARGET)" == "ios" ]]; then
+        if [[ "$(IS_AUTODESKTOP)" == "True" ]]; then
+          opt+=" --autodesktop"
+        elif [[ "$(TARGET)" == "android" || "$(TARGET)" == "ios" ]]; then
           if [[ "$(HOST)" == "windows" ]]; then
             python -m aqt install-qt $(HOST) desktop $(QT_VERSION) mingw81_64 --archives qtbase
           else
@@ -58,6 +55,11 @@ steps:
             python -m aqt install-qt $(HOST) desktop $(QT_VERSION) --archives qtbase qtdeclarative
           fi
         fi
+        if [[ "$(SPEC)" == "" ]]; then
+          python -m aqt install-qt $(HOST) $(TARGET) $(QT_VERSION) $(ARCH) $opt
+        else
+          python -m aqt install-qt $(HOST) $(TARGET) "$(SPEC)" $(ARCH) $opt
+        fi
         if [[ "$(OUTPUT_DIR)" != "" ]]; then
           # Use 'strings' to read binary
           echo "Verify patched value of qt_prfxpath"
diff --git a/docs/cli.rst b/docs/cli.rst
index ed041c1..5095e92 100644
--- a/docs/cli.rst
+++ b/docs/cli.rst
@@ -510,6 +510,7 @@ install-qt command
         [-d | --archive-dest] <path>
         [-m | --modules (all | <module> [<module>...])]
         [--archives <archive> [<archive>...]]
+        [--autodesktop]
         [--noarchives]
         <host> <target> (<Qt version> | <spec>) [<arch>]
 
@@ -557,6 +558,11 @@ There are various combinations to accept according to Qt version.
 
     Use the :ref:`List-Qt Command` to list available architectures.
 
+.. option:: --autodesktop
+
+    If you are installing an ios or android version of Qt, the corresponding desktop version
+    of Qt must be installed alongside of it. Turn this option on to install it automatically.
+
 .. option:: --noarchives
 
     [Advanced] Specify not to install all base packages.
@@ -816,7 +822,7 @@ Example: Installing Android (armv7) Qt 5.10.2:
 
 .. code-block:: console
 
-    aqt install-qt linux android 5.10.2 android_armv7
+    aqt install-qt linux android 5.10.2 android_armv7 --autodesktop
 
 
 Example: Install examples, doc and source:
diff --git a/docs/getting_started.rst b/docs/getting_started.rst
index c5b9905..2d5e43e 100644
--- a/docs/getting_started.rst
+++ b/docs/getting_started.rst
@@ -296,6 +296,13 @@ Finally, let's install Qt 6.2.0 for Android armv7 with the ``qtcharts`` and
 
     $ aqt install-qt linux android 6.2.0 android_armv7 -m qtcharts qtnetworkauth
 
+Please note that when you install Qt for android or ios, the installation will not
+be functional unless you install the corresponding desktop version of Qt alongside it.
+You can do this automatically with the ``--autodesktop`` flag:
+
+.. code-block:: console
+
+    $ aqt install-qt linux android 6.2.0 android_armv7 -m qtcharts qtnetworkauth --autodesktop
 
 Installing Qt for WASM
 ----------------------


Test patch:
diff --git a/tests/test_cli.py b/tests/test_cli.py
index e5d0637..7940997 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -1,7 +1,8 @@
 import re
 import sys
 from pathlib import Path
-from typing import Optional
+from tempfile import TemporaryDirectory
+from typing import Dict, List, Optional
 
 import pytest
 
@@ -374,3 +375,103 @@ def test_cli_choose_archive_dest(
 
     assert Cli.choose_archive_dest(archive_dest, keep, temp_dir) == Path(expect)
     assert enclosed["made_dir"] == should_make_dir
+
+
+@pytest.mark.parametrize(
+    "host, is_auto, mocked_mingw, existing_arch_dirs, expect",
+    (
+        (  # not installed
+            "windows",
+            False,
+            "win64_mingw99",
+            ["not_mingw"],
+            {"install": None, "instruct": "win64_mingw99", "use_dir": "mingw99_64"},
+        ),
+        (  # Alt Desktop Qt already installed
+            "windows",
+            False,
+            "win64_mingw99",
+            ["mingw128_32"],
+            {"install": None, "instruct": None, "use_dir": "mingw128_32"},
+        ),
+        # not installed
+        ("linux", False, None, ["gcc_32"], {"install": None, "instruct": "gcc_64", "use_dir": "gcc_64"}),
+        (  # Desktop Qt already installed
+            "linux",
+            False,
+            None,
+            ["gcc_64"],
+            {"install": None, "instruct": None, "use_dir": "gcc_64"},
+        ),
+        (  # not installed
+            "windows",
+            True,
+            "win64_mingw99",
+            ["not_mingw"],
+            {"install": "win64_mingw99", "instruct": None, "use_dir": "mingw99_64"},
+        ),
+        (  # Alt Desktop Qt already installed
+            "windows",
+            True,
+            "win64_mingw99",
+            ["mingw128_32"],
+            {"install": None, "instruct": None, "use_dir": "mingw128_32"},
+        ),
+        # not installed
+        ("linux", True, None, ["gcc_32"], {"install": "gcc_64", "instruct": None, "use_dir": "gcc_64"}),
+        (  # Desktop Qt already installed
+            "linux",
+            True,
+            None,
+            ["gcc_64"],
+            {"install": None, "instruct": None, "use_dir": "gcc_64"},
+        ),
+    ),
+)
+def test_get_autodesktop_dir_and_arch(
+    monkeypatch, capsys, host: str, is_auto: bool, mocked_mingw: str, existing_arch_dirs: List[str], expect: Dict[str, str]
+):
+    """
+    :is_auto:               Simulates passing `--autodesktop` to aqt
+    :mocked_mingw:          When we ask MetadataFactory for a list of available architectures, we return this value
+    :existing_arch_dirs:    Directories that contain an existing file at `arch_dir/bin/qmake`
+    :expect[install]:       The archdir we expect aqt to install
+    :expect[instruct]:      The architecture we expect aqt to ask the user to install
+    :expect[use_dir]:       The directory that includes `bin/qmake`; we will patch files in the mobile installation
+                            with this value
+    """
+    monkeypatch.setattr(MetadataFactory, "fetch_arches", lambda *args: [mocked_mingw])
+    monkeypatch.setattr(Cli, "run", lambda *args: 0)
+
+    target = "android"
+    version = "6.2.3"
+    cli = Cli()
+    cli._setup_settings()
+
+    expect_msg_prefix = (
+        f"You are installing the {target} version of Qt, "
+        f"which requires that the desktop version of Qt is also installed."
+    )
+
+    with TemporaryDirectory() as temp_dir:
+        base_dir = Path(temp_dir)
+        for arch_dir in existing_arch_dirs:
+            qmake = base_dir / version / arch_dir / f"bin/qmake{'.exe' if host == 'windows' else ''}"
+            qmake.parent.mkdir(parents=True)
+            qmake.write_text("exe file")
+        autodesktop_arch_dir, autodesktop_arch_to_install = cli._get_autodesktop_dir_and_arch(
+            is_auto, host, target, base_dir, Version(version)
+        )
+        # It should choose the correct desktop arch directory for updates
+        assert autodesktop_arch_dir == expect["use_dir"]
+
+        out, err = capsys.readouterr()
+        if expect["install"]:
+            assert err.strip() == f"INFO    : {expect_msg_prefix} Now installing Qt: desktop {version} {expect['install']}"
+        elif expect["instruct"]:
+            assert (
+                err.strip() == f"WARNING : {expect_msg_prefix} You can install it with the following command:\n"
+                f"          `aqt install-qt {host} desktop {version} {expect['instruct']}`"
+            )
+        else:
+            assert err.strip() == f"INFO    : Found installed {host}-desktop Qt at {base_dir / version / expect['use_dir']}"
diff --git a/tests/test_install.py b/tests/test_install.py
index 1aeb47a..9124d5c 100644
--- a/tests/test_install.py
+++ b/tests/test_install.py
@@ -1,6 +1,7 @@
 import hashlib
 import logging
 import os
+import posixpath
 import re
 import subprocess
 import sys
@@ -124,34 +125,49 @@ class MockArchive:
 
 
 def make_mock_geturl_download_archive(
-    archives: Iterable[MockArchive],
-    arch: str,
-    os_name: str,
-    updates_url: str,
+    *,
+    standard_archives: List[MockArchive],
+    desktop_archives: Optional[List[MockArchive]] = None,
+    standard_updates_url: str,
+    desktop_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")
+    if desktop_archives is None:
+        desktop_archives = []
+    for _archive in [*standard_archives, *desktop_archives]:
+        assert _archive.filename_7z.endswith(".7z")
 
-    xml = "<Updates>\n{}\n</Updates>".format("\n".join([archive.xml_package_update() for archive in archives]))
+    standard_xml = "<Updates>\n{}\n</Updates>".format(
+        "\n".join([archive.xml_package_update() for archive in standard_archives])
+    )
+    desktop_xml = "<Updates>\n{}\n</Updates>".format(
+        "\n".join([archive.xml_package_update() for archive in desktop_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
+        for xml, updates_url in (
+            (standard_xml, standard_updates_url),
+            (desktop_xml, desktop_updates_url),
+        ):
+            basename = posixpath.dirname(updates_url)
+            if not updates_url:
+                continue
+            elif url.endswith(updates_url):
+                return xml
+            elif basename in url and url.endswith(".sha256"):
+                filename = url.split("/")[-1][: -len(".sha256")]
+                return f"{hashlib.sha256(bytes(xml, 'utf-8')).hexdigest()} {filename}"
+        assert False, f"No mocked url available for '{url}'"
 
     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
+            for archive in [*standard_archives, *desktop_archives]:
+                if Path(out).name == archive.filename_7z:
+                    return archive
             assert False, "Requested an archive that was not mocked"
 
         locate_archive().write_compressed_archive(Path(out).parent)
@@ -237,9 +253,9 @@ def qtpositioning_module(ver: str, arch: str) -> MockArchive:
     )
 
 
-def plain_qtbase_archive(update_xml_name: str, arch: str, should_install: bool = True) -> MockArchive:
+def plain_qtbase_archive(update_xml_name: str, arch: str, host: str = "windows", should_install: bool = True) -> MockArchive:
     return MockArchive(
-        filename_7z=f"qtbase-windows-{arch}.7z",
+        filename_7z=f"qtbase-{host}-{arch}.7z",
         update_xml_name=update_xml_name,
         contents=(
             PatchedFile(
@@ -282,12 +298,10 @@ def tool_archive(host: str, tool_name: str, variant: str, date: datetime = datet
             "linux",
             "desktop",
             "1.2.3-0-197001020304",
-            "",
-            "",
-            "linux_x64/desktop/tools_qtcreator/Updates.xml",
-            [
-                tool_archive("linux", "tools_qtcreator", "qt.tools.qtcreator"),
-            ],
+            {"std": ""},
+            {"std": ""},
+            {"std": "linux_x64/desktop/tools_qtcreator/Updates.xml"},
+            {"std": [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"
@@ -301,12 +315,10 @@ def tool_archive(host: str, tool_name: str, variant: str, date: datetime = datet
             "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)),
-            ],
+            {"std": ""},
+            {"std": ""},
+            {"std": "linux_x64/desktop/tools_qtcreator/Updates.xml"},
+            {"std": [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"
@@ -322,12 +334,10 @@ def tool_archive(host: str, tool_name: str, variant: str, date: datetime = datet
             "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"),
-            ],
+            {"std": "win32_mingw73"},
+            {"std": "mingw73_32"},
+            {"std": "windows_x86/desktop/qt5_5140/Updates.xml"},
+            {"std": [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"
@@ -344,23 +354,25 @@ def tool_archive(host: str, tool_name: str, variant: str, date: datetime = datet
             "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
+            {"std": ""},
+            {"std": ""},
+            {"std": "windows_x86/desktop/qt5_5142_src_doc_examples/Updates.xml"},
+            {
+                "std": [
+                    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 "
@@ -377,23 +389,25 @@ def tool_archive(host: str, tool_name: str, variant: str, date: datetime = datet
             "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
+            {"std": ""},
+            {"std": ""},
+            {"std": "windows_x86/desktop/qt5_5142_src_doc_examples/Updates.xml"},
+            {
+                "std": [
+                    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"
@@ -407,12 +421,10 @@ def tool_archive(host: str, tool_name: str, variant: str, date: datetime = datet
             "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"),
-            ],
+            {"std": "win32_mingw53"},
+            {"std": "mingw53_32"},
+            {"std": "windows_x86/desktop/qt5_59/Updates.xml"},
+            {"std": [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"
@@ -429,12 +441,10 @@ def tool_archive(host: str, tool_name: str, variant: str, date: datetime = datet
             "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"),
-            ],
+            {"std": "win32_mingw53"},
+            {"std": "mingw53_32"},
+            {"std": "windows_x86/desktop/qt5_59/Updates.xml"},
+            {"std": [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"
@@ -448,12 +458,10 @@ def tool_archive(host: str, tool_name: str, variant: str, date: datetime = datet
             "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"),
-            ],
+            {"std": "win32_mingw73"},
+            {"std": "mingw73_32"},
+            {"std": "windows_x86/desktop/qt5_5140/Updates.xml"},
+            {"std": [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"
@@ -467,13 +475,15 @@ def tool_archive(host: str, tool_name: str, variant: str, date: datetime = datet
             "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"),
-            ],
+            {"std": "win64_mingw73"},
+            {"std": "mingw73_64"},
+            {"std": "windows_x86/desktop/qt5_5140/Updates.xml"},
+            {
+                "std": [
+                    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"
@@ -489,13 +499,15 @@ def tool_archive(host: str, tool_name: str, variant: str, date: datetime = datet
             "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"),
-            ],
+            {"std": "win64_mingw73"},
+            {"std": "mingw73_64"},
+            {"std": "windows_x86/desktop/qt6_620/Updates.xml"},
+            {
+                "std": [
+                    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"
@@ -509,13 +521,15 @@ def tool_archive(host: str, tool_name: str, variant: str, date: datetime = datet
             "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"),
-            ],
+            {"std": "win64_mingw81"},
+            {"std": "mingw81_64"},
+            {"std": "windows_x86/desktop/qt6_610/Updates.xml"},
+            {
+                "std": [
+                    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"
@@ -531,49 +545,54 @@ def tool_archive(host: str, tool_name: str, variant: str, date: datetime = datet
             "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",
+            {"std": "android_armv7"},
+            {"std": "android_armv7"},
+            {"std": "windows_x86/android/qt6_610_armv7/Updates.xml"},
+            {
+                "std": [
+                    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=../../mingw1234_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\\mingw1234_64\\bin\n"
+                                "... blah blah blah ...\n",
+                            ),
                         ),
                     ),
-                ),
-            ],
+                ]
+            },
             re.compile(
                 r"^INFO    : aqtinstall\(aqt\) v.* on Python 3.*\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 win64_mingw1234`\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"
@@ -586,49 +605,54 @@ def tool_archive(host: str, tool_name: str, variant: str, date: datetime = datet
             "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",
+            {"std": "android_arm64_v8a"},
+            {"std": "android_arm64_v8a"},
+            {"std": "linux_x64/android/qt6_630_arm64_v8a/Updates.xml"},
+            {
+                "std": [
+                    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"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    : 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"
@@ -641,51 +665,118 @@ def tool_archive(host: str, tool_name: str, variant: str, date: datetime = datet
             "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",
+            {"std": "ios"},
+            {"std": "ios"},
+            {"std": "mac_x64/ios/qt6_612/Updates.xml"},
+            {
+                "std": [
+                    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",
+                            ),
                         ),
-                        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"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 clang_64`\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"
+            ),
+        ),
+        (
+            "install-qt mac ios 6.1.2 --autodesktop".split(),
+            "mac",
+            "ios",
+            "6.1.2",
+            {"std": "ios", "desk": "clang_64"},
+            {"std": "ios", "desk": "macos"},
+            {"std": "mac_x64/ios/qt6_612/Updates.xml", "desk": "mac_x64/desktop/qt6_612/Updates.xml"},
+            {
+                "std": [
+                    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",
+                            ),
                         ),
                     ),
-                ),
-            ],
+                ],
+                "desk": [plain_qtbase_archive("qt.qt6.612.clang_64", "clang_64", host="mac")],
+            },
             re.compile(
                 r"^INFO    : aqtinstall\(aqt\) v.* on Python 3.*\n"
+                r"INFO    : You are installing the ios version of Qt, which requires that the desktop version of "
+                r"Qt is also installed. Now installing Qt: desktop 6\.1\.2 clang_64\n"
                 r"INFO    : Downloading qtbase...\n"
                 r"Finished installation of qtbase-mac-ios.7z in .*\n"
+                r"INFO    : Downloading qtbase...\n"
+                r"Finished installation of qtbase-mac-clang_64.7z in .*\n"
                 r"INFO    : Patching .*6\.1\.2[/\\]ios[/\\]bin[/\\]qmake\n"
                 r"INFO    : Finished installation\n"
                 r"INFO    : Time elapsed: .* second"
@@ -700,22 +791,35 @@ def test_install(
     host: str,
     target: str,
     version: str,
-    arch: str,
-    arch_dir: str,
-    updates_url: str,
-    archives: List[MockArchive],
+    arch: Dict[str, str],
+    arch_dir: Dict[str, str],
+    updates_url: Dict[str, str],
+    archives: Dict[str, 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)
+    std_archives = archives["std"]
+    for i in range(len(std_archives)):
+        std_archives[i].version = version
+        std_archives[i].arch_dir = arch_dir["std"]
+    desktop_archives = archives.get("desk", [])
+    for i in range(len(desktop_archives)):
+        desktop_archives[i].version = version
+        desktop_archives[i].arch_dir = arch_dir["desk"]
+
+    mock_get_url, mock_download_archive = make_mock_geturl_download_archive(
+        standard_archives=std_archives,
+        desktop_archives=desktop_archives,
+        standard_updates_url=updates_url.get("std", ""),
+        desktop_updates_url=updates_url.get("desk", ""),
+    )
     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: {"windows": "win64_mingw1234", "linux": "gcc_64", "mac": "clang_64"}[host],
+    )
 
     with TemporaryDirectory() as output_dir:
         cli = Cli()
@@ -729,20 +833,21 @@ def test_install(
 
         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()
+        for key in arch_dir.keys():
+            installed_path = Path(output_dir) / version / arch_dir[key]
+            if version == "5.9.0":
+                installed_path = Path(output_dir) / "5.9" / arch_dir[key]
+            assert installed_path.is_dir()
+            for archive in archives[key]:
+                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
+                    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(
@@ -902,7 +1007,9 @@ def test_install_pool_exception(monkeypatch, capsys, exception_class, settings_f
     archives = [plain_qtbase_archive("qt.qt6.610.win64_mingw81", "win64_mingw81")]
 
     cmd = ["install-qt", host, target, ver, arch]
-    mock_get_url, mock_download_archive = make_mock_geturl_download_archive(archives, arch, host, updates_url)
+    mock_get_url, mock_download_archive = make_mock_geturl_download_archive(
+        standard_archives=archives, standard_updates_url=updates_url
+    )
     monkeypatch.setattr("aqt.archives.getUrl", mock_get_url)
     monkeypatch.setattr("aqt.helper.getUrl", mock_get_url)
     monkeypatch.setattr("aqt.installer.installer", mock_installer_func)
@@ -1003,7 +1110,9 @@ def test_installer_passes_base_to_metadatafactory(
         archives[i].version = version
         archives[i].arch_dir = arch_dir
 
-    basic_mock_get_url, mock_download_archive = make_mock_geturl_download_archive(archives, arch, host, updates_url)
+    basic_mock_get_url, mock_download_archive = make_mock_geturl_download_archive(
+        standard_archives=archives, standard_updates_url=updates_url
+    )
 
     def mock_get_url(url: str, *args, **kwargs) -> str:
         # If we are fetching an index.html file, get it from tests/data/
diff --git a/tests/test_list.py b/tests/test_list.py
index 8cedc03..363eabb 100644
--- a/tests/test_list.py
+++ b/tests/test_list.py
@@ -5,6 +5,7 @@ import re
 import shutil
 import sys
 from pathlib import Path
+from tempfile import TemporaryDirectory
 from typing import Dict, List, Set, Union
 from urllib.parse import urlparse
 
@@ -1117,3 +1118,58 @@ def test_fetch_http_download_error(monkeypatch, exception_on_error):
 
     # Require that a fallback url was tried
     assert len(urls_requested) == 2
+
+
+@pytest.mark.parametrize(
+    "host, expected, all_arches",
+    (
+        ("windows", "win32_mingw", ["win64_msvc2013_64", "win32_mingw", "win16_mingw"]),
+        ("windows", "win32_mingw", ["win64_msvc2013_64", "win32_mingw"]),
+        ("windows", "win1_mingw0", ["win64_msvc2013_64", "win_mingw900", "win1_mingw0"]),
+        ("windows", "win32_mingw1", ["win64_msvc2013_64", "win32_mingw", "win32_mingw1"]),
+        ("windows", "win64_mingw", ["win64_msvc2013_64", "win32_mingw900", "win64_mingw"]),
+        ("windows", "win64_mingw81", ["win64_msvc2013_64", "win64_mingw81", "win64_mingw"]),
+        ("windows", "win64_mingw73", ["win64_msvc2013_64", "win64_mingw53", "win64_mingw73"]),
+        ("windows", "win64_mingw", ["win64_msvc2013_64", "win64_mingw", "win64_mingw"]),
+        ("windows", EmptyMetadata(), []),
+        ("linux", "gcc_64", ["should not fetch arches"]),
+        ("mac", "clang_64", ["should not fetch arches"]),
+    ),
+)
+def test_select_default_mingw(monkeypatch, host: str, expected: Union[str, Exception], all_arches: List[str]):
+    monkeypatch.setattr("aqt.metadata.MetadataFactory.fetch_arches", lambda *args, **kwargs: all_arches)
+
+    if isinstance(expected, Exception):
+        with pytest.raises(type(expected)) as e:
+            MetadataFactory(ArchiveId("qt", host, "desktop")).fetch_default_desktop_arch(Version("1.2.3"))
+        assert e.type == type(expected)
+    else:
+        actual_arch = MetadataFactory(ArchiveId("qt", host, "desktop")).fetch_default_desktop_arch(Version("1.2.3"))
+        assert actual_arch == expected
+
+
+@pytest.mark.parametrize(
+    "expected_result, installed_files",
+    (
+        ("mingw73_32", ["mingw73_32/bin/qmake.exe", "msvc2017/bin/qmake.exe"]),
+        (None, ["msvc2017/bin/qmake.exe"]),
+        (None, ["mingw73_win/bin/qmake.exe"]),  # Bad directory: mingw73_win does not fit the mingw naming convention
+        (None, ["mingw73_32/bin/qmake", "msvc2017/bin/qmake.exe"]),
+        ("mingw81_32", ["mingw73_32/bin/qmake.exe", "mingw81_32/bin/qmake.exe"]),
+        ("mingw73_64", ["mingw73_64/bin/qmake.exe", "mingw73_32/bin/qmake.exe"]),
+    ),
+)
+def test_find_installed_qt_mingw_dir(expected_result: str, installed_files: List[str]):
+    qt_ver = "6.3.0"
+    host = "windows"
+
+    # Setup a mock install directory that includes some installed files
+    with TemporaryDirectory() as base_dir:
+        base_path = Path(base_dir)
+        for file in installed_files:
+            path = base_path / qt_ver / file
+            path.parent.mkdir(parents=True, exist_ok=True)
+            path.write_text("Mock installed file")
+
+        actual_result = QtRepoProperty.find_installed_desktop_qt_dir(host, base_path, Version(qt_ver))
+        assert (actual_result.name if actual_result else None) == expected_result
diff --git a/tests/test_updater.py b/tests/test_updater.py
index 6b67c45..2da1d2e 100644
--- a/tests/test_updater.py
+++ b/tests/test_updater.py
@@ -1,4 +1,5 @@
 import re
+from pathlib import Path
 from tempfile import TemporaryDirectory
 
 import pytest
@@ -58,7 +59,7 @@ def test_updater_update_license_io_error(monkeypatch, target_config: TargetConfi
     with pytest.raises(UpdaterError) as err:
         with TemporaryDirectory() as empty_dir:
             # Try to update a Qt installation that does not exist
-            Updater.update(target_config, base_dir=empty_dir)
+            Updater.update(target_config, base_path=Path(empty_dir), installed_desktop_arch_dir=None)
     assert err.type == UpdaterError
     err_msg = format(err.value)
     assert expected_err_pattern.match(err_msg)