Repository: miurahr/aqtinstall

Issue:
Error when use qmake with Qt 6
**Describe the bug**
Hi,

Im trying use `qmake` with Qt 6 on Windows, but i always get error:

```
https://github.com/paulocoutinhox/prprojector/actions/runs/6080974346/job/16495785923#step:9:239
```

Can you help me understand?

**To Reproduce**

```
git clone https://github.com/paulocoutinhox/prprojector.git
cd prprojector
python3 -m pip install -r requirements.txt
python3 make.py run build-win-x64
```

**Expected behavior**

Run qmake without problems.

**Desktop (please complete the following information):**
 - OS: windows
 - `INFO    : aqtinstall(aqt) v3.1.7 on Python 3.9.6 [CPython Clang 14.0.3 (clang-1403.0.22.14.1)]`


Discussion:

Patch:
diff --git a/.readthedocs.yml b/.readthedocs.yml
index 0b53257..c8eed33 100644
--- a/.readthedocs.yml
+++ b/.readthedocs.yml
@@ -13,7 +13,6 @@ python:
         path: .
         extra_requirements:
            - docs
-   system_packages: false
 
 build:
   os: ubuntu-22.04
diff --git a/aqt/installer.py b/aqt/installer.py
index 2f9e791..3c16c64 100644
--- a/aqt/installer.py
+++ b/aqt/installer.py
@@ -390,7 +390,7 @@ class Cli:
         base_path = Path(base_dir)
 
         expect_desktop_archdir, autodesk_arch = self._get_autodesktop_dir_and_arch(
-            should_autoinstall, os_name, target, base_path, _version, is_wasm=(arch.startswith("wasm"))
+            should_autoinstall, os_name, target, base_path, _version, arch
         )
 
         def get_auto_desktop_archives() -> List[QtPackage]:
@@ -1069,24 +1069,34 @@ class Cli:
             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, is_wasm: bool = False
+        self, should_autoinstall: bool, host: str, target: str, base_path: Path, version: Version, arch: str
     ) -> Tuple[Optional[str], Optional[str]]:
         """Returns expected_desktop_arch_dir, desktop_arch_to_install"""
-        if version < Version("6.0.0") or (target not in ["ios", "android"] and not is_wasm):
+        is_wasm = arch.startswith("wasm")
+        is_msvc = "msvc" in arch
+        is_win_desktop_msvc_arm64 = host == "windows" and target == "desktop" and is_msvc and arch.endswith("arm64")
+        if version < Version("6.0.0") or (
+            target not in ["ios", "android"] and not is_wasm and not is_win_desktop_msvc_arm64
+        ):
             # We only need to worry about the desktop directory for Qt6 mobile or wasm installs.
             return None, None
 
-        installed_desktop_arch_dir = QtRepoProperty.find_installed_desktop_qt_dir(host, base_path, version)
+        installed_desktop_arch_dir = QtRepoProperty.find_installed_desktop_qt_dir(host, base_path, version, is_msvc=is_msvc)
         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)
+        default_desktop_arch = MetadataFactory(ArchiveId("qt", host, "desktop")).fetch_default_desktop_arch(version, is_msvc)
         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
 
-        qt_type = "Qt6-WASM" if is_wasm else target
+        if is_win_desktop_msvc_arm64:
+            qt_type = "MSVC Arm64"
+        elif is_wasm:
+            qt_type = "Qt6-WASM"
+        else:
+            qt_type = target
         if should_autoinstall:
             # No desktop Qt is installed, but the user has requested installation. Find out what to install.
             self.logger.info(
diff --git a/aqt/metadata.py b/aqt/metadata.py
index 64a98b2..272c15a 100644
--- a/aqt/metadata.py
+++ b/aqt/metadata.py
@@ -394,6 +394,11 @@ class QtRepoProperty:
     def default_linux_desktop_arch_dir() -> str:
         return "gcc_64"
 
+    @staticmethod
+    def default_win_msvc_desktop_arch_dir(_version: Version) -> str:
+        """_version is unused, but we expect it to matter for future releases"""
+        return "msvc2019_64"
+
     @staticmethod
     def default_mac_desktop_arch_dir(version: Version) -> str:
         return "macos" if version in SimpleSpec(">=6.1.2") else "clang_64"
@@ -465,7 +470,7 @@ class QtRepoProperty:
         return default_arch
 
     @staticmethod
-    def find_installed_desktop_qt_dir(host: str, base_path: Path, version: Version) -> Optional[Path]:
+    def find_installed_desktop_qt_dir(host: str, base_path: Path, version: Version, is_msvc: bool = False) -> Optional[Path]:
         """
         Locates the default installed desktop qt directory, somewhere in base_path.
         """
@@ -476,6 +481,9 @@ class QtRepoProperty:
         elif host == "linux":
             arch_path = installed_qt_version_dir / QtRepoProperty.default_linux_desktop_arch_dir()
             return arch_path if (arch_path / "bin/qmake").is_file() else None
+        elif host == "windows" and is_msvc:
+            arch_path = installed_qt_version_dir / QtRepoProperty.default_win_msvc_desktop_arch_dir(version)
+            return arch_path if (arch_path / "bin/qmake.exe").is_file() else None
 
         def contains_qmake_exe(arch_path: Path) -> bool:
             return (arch_path / "bin/qmake.exe").is_file()
@@ -920,12 +928,14 @@ 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:
+    def fetch_default_desktop_arch(self, version: Version, is_msvc: bool = False) -> 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"
+        elif self.archive_id.host == "windows" and is_msvc:
+            return "win64_msvc2019_64"
         arches = [arch for arch in self.fetch_arches(version) if QtRepoProperty.MINGW_ARCH_PATTERN.match(arch)]
         selected_arch = QtRepoProperty.select_default_mingw(arches, is_dir=False)
         if not selected_arch:
diff --git a/aqt/updater.py b/aqt/updater.py
index 1790a5d..fe04078 100644
--- a/aqt/updater.py
+++ b/aqt/updater.py
@@ -293,6 +293,7 @@ class Updater:
                 "android_arm64_v8a",
                 "android_x86",
                 "android_armv7",
+                "win64_msvc2019_arm64",
             ]:  # desktop version
                 updater.make_qtconf(base_dir, version_dir, arch_dir)
                 updater.patch_qmake()
@@ -308,13 +309,13 @@ class Updater:
                     updater.patch_qtcore(target)
             elif version in SimpleSpec(">=5.0,<6.0"):
                 updater.patch_qmake()
-            else:  # qt6 mobile or wasm
+            else:  # qt6 mobile, wasm, or msvc-arm64
                 if installed_desktop_arch_dir is not None:
                     desktop_arch_dir = installed_desktop_arch_dir
                 else:
                     # Use MetadataFactory to check what the default architecture should be
                     meta = MetadataFactory(ArchiveId("qt", os_name, "desktop"))
-                    desktop_arch_dir = meta.fetch_default_desktop_arch(version)
+                    desktop_arch_dir = meta.fetch_default_desktop_arch(version, is_msvc="msvc" in target.arch)
 
                 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)
diff --git a/ci/generate_azure_pipelines_matrices.py b/ci/generate_azure_pipelines_matrices.py
index b913670..a418c35 100644
--- a/ci/generate_azure_pipelines_matrices.py
+++ b/ci/generate_azure_pipelines_matrices.py
@@ -189,6 +189,15 @@ windows_build_jobs.extend(
             spec=">1,<5.15",  # Don't redirect output! Must be wrapped in quotes!
             mirror=random.choice(MIRRORS),
         ),
+        BuildJob(
+            "install-qt",
+            "6.5.2",
+            "windows",
+            "desktop",
+            "win64_msvc2019_arm64",
+            "msvc2019_arm64",
+            is_autodesktop=True,    # Should install win64_msvc2019_arm64 in parallel
+        ),
         BuildJob(
             # Archives stored as .zip
             "install-src", "6.4.3", "windows", "desktop", "gcc_64", "gcc_64", subarchives="qtlottie",
diff --git a/ci/steps.yml b/ci/steps.yml
index d0c33a7..d09a764 100644
--- a/ci/steps.yml
+++ b/ci/steps.yml
@@ -172,7 +172,16 @@ steps:
       export PATH=$(QT_BINDIR):$PATH
       qmake $(Build.BinariesDirectory)/tests/accelbubble
       make
-    condition: and(eq(variables['TARGET'], 'android'), or(eq(variables['Agent.OS'], 'Linux'), eq(variables['Agent.OS'], 'Darwin')), ne(variables['SUBCOMMAND'], 'list'), ne(variables['SUBCOMMAND'], 'install-tool'))
+    condition: |
+      and(
+        eq(variables['TARGET'], 'android'),
+        or(
+          eq(variables['Agent.OS'], 'Linux'),
+          eq(variables['Agent.OS'], 'Darwin')
+        ),
+        ne(variables['SUBCOMMAND'], 'list'),
+        ne(variables['SUBCOMMAND'], 'install-tool')
+      )
     displayName: Build accelbubble example application to test for android
 
   ##----------------------------------------------------
@@ -294,7 +303,12 @@ steps:
         qmake $(Build.BinariesDirectory)\tests\helloworld
         mingw32-make
       }
-    condition: and(eq( variables['Agent.OS'], 'Windows_NT'), eq(variables['MODULE'], ''), eq(variables['SUBCOMMAND'], 'install-qt'))
+    condition: |
+      and(
+        eq(variables['Agent.OS'], 'Windows_NT'),
+        eq(variables['MODULE'], ''),
+        eq(variables['SUBCOMMAND'], 'install-qt')
+      )
     displayName: build test with qmake w/o extra module
   - powershell: |
       # Load modules from cache  
@@ -312,7 +326,13 @@ steps:
       cd ..
       qmake $(Build.BinariesDirectory)\tests\redditclient
       nmake
-    condition: and(eq( variables['Agent.OS'], 'Windows_NT'), eq(variables['TOOLCHAIN'], 'MSVC'), ne(variables['MODULE'], ''), ne(variables['VSVER'], '2019'))
+    condition: |
+      and(
+        eq(variables['Agent.OS'], 'Windows_NT'),
+        eq(variables['TOOLCHAIN'], 'MSVC'),
+        ne(variables['MODULE'], ''),
+        ne(variables['VSVER'], '2019')
+      )
     displayName: build test with qmake with MSVC with extra module
   - bash: |
       set -ex
@@ -325,11 +345,15 @@ steps:
       qmake $(Build.BinariesDirectory)/tests/redditclient
       make
     condition: |
-      and(eq( variables['TARGET'], 'desktop'),
-          or(eq(variables['Agent.OS'], 'Linux'),
-          eq(variables['Agent.OS'], 'Darwin')),
-          ne(variables['MODULE'], ''),
-          eq(variables['SUBCOMMAND'], 'install-qt'))
+      and(
+        eq( variables['TARGET'], 'desktop'),
+        or(
+          eq(variables['Agent.OS'], 'Linux'),
+          eq(variables['Agent.OS'], 'Darwin')
+        ),
+        ne(variables['MODULE'], ''),
+        eq(variables['SUBCOMMAND'], 'install-qt')
+      )
     displayName: Build test with qmake for Linux and macOS with extra module
 
   ##----------------------------------------------------


Test patch:
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 91a568b..20a9dfd 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -415,58 +415,130 @@ def test_cli_choose_archive_dest(
 
 
 @pytest.mark.parametrize(
-    "host, is_auto, mocked_mingw, existing_arch_dirs, expect",
+    "host, target, arch, is_auto, mocked_arches, existing_arch_dirs, expect",
     (
         (  # not installed
             "windows",
+            "android",
+            "android_armv7",
             False,
-            "win64_mingw99",
+            ["win64_mingw99"],
             ["not_mingw"],
             {"install": None, "instruct": "win64_mingw99", "use_dir": "mingw99_64"},
         ),
         (  # Alt Desktop Qt already installed
             "windows",
+            "android",
+            "android_armv7",
             False,
-            "win64_mingw99",
+            ["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"}),
+        (
+            "linux",
+            "android",
+            "android_armv7",
+            False,
+            [],
+            ["gcc_32"],
+            {"install": None, "instruct": "gcc_64", "use_dir": "gcc_64"},
+        ),
         (  # Desktop Qt already installed
             "linux",
+            "android",
+            "android_armv7",
             False,
-            None,
+            [],
             ["gcc_64"],
             {"install": None, "instruct": None, "use_dir": "gcc_64"},
         ),
         (  # not installed
             "windows",
+            "android",
+            "android_armv7",
             True,
-            "win64_mingw99",
+            ["win64_mingw99"],
             ["not_mingw"],
             {"install": "win64_mingw99", "instruct": None, "use_dir": "mingw99_64"},
         ),
         (  # Alt Desktop Qt already installed
             "windows",
+            "android",
+            "android_armv7",
             True,
-            "win64_mingw99",
+            ["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"}),
+        (
+            "linux",
+            "android",
+            "android_armv7",
+            True,
+            [],
+            ["gcc_32"],
+            {"install": "gcc_64", "instruct": None, "use_dir": "gcc_64"},
+        ),
         (  # Desktop Qt already installed
             "linux",
+            "android",
+            "android_armv7",
             True,
-            None,
+            [],
             ["gcc_64"],
             {"install": None, "instruct": None, "use_dir": "gcc_64"},
         ),
+        (  # MSVC arm64 with --autodesktop: should install min64_msvc2019_64
+            "windows",
+            "desktop",
+            "win64_msvc2019_arm64",
+            True,
+            ["win64_mingw", "win64_msvc2019_64", "win64_msvc2019_arm64", "wasm_singlethread", "wasm_multithread"],
+            ["mingw128_32"],
+            {"install": "win64_msvc2019_64", "instruct": None, "use_dir": "msvc2019_64"},
+        ),
+        (  # MSVC arm64 without --autodesktop: should point to min64_msvc2019_64
+            "windows",
+            "desktop",
+            "win64_msvc2019_arm64",
+            False,
+            ["win64_mingw", "win64_msvc2019_64", "win64_msvc2019_arm64", "wasm_singlethread", "wasm_multithread"],
+            ["mingw128_32"],
+            {"install": None, "instruct": "win64_msvc2019_64", "use_dir": "msvc2019_64"},
+        ),
+        (  # MSVC arm64 without --autodesktop, with correct target already installed
+            "windows",
+            "desktop",
+            "win64_msvc2019_arm64",
+            False,
+            ["win64_mingw", "win64_msvc2019_64", "win64_msvc2019_arm64", "wasm_singlethread", "wasm_multithread"],
+            ["msvc2019_64"],
+            {"install": None, "instruct": None, "use_dir": "msvc2019_64"},
+        ),
+        (  # MSVC arm64 without --autodesktop, with wrong target already installed
+            "windows",
+            "desktop",
+            "win64_msvc2019_arm64",
+            False,
+            ["win64_mingw", "win64_msvc2019_64", "win64_msvc2019_arm64", "wasm_singlethread", "wasm_multithread"],
+            ["mingw128_32"],
+            {"install": None, "instruct": "win64_msvc2019_64", "use_dir": "msvc2019_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]
+def test_get_autodesktop_dir_and_arch_non_android(
+    monkeypatch,
+    capsys,
+    host: str,
+    target: str,
+    arch: str,
+    is_auto: bool,
+    mocked_arches: List[str],
+    existing_arch_dirs: List[str],
+    expect: Dict[str, str],
 ):
     """
     :is_auto:               Simulates passing `--autodesktop` to aqt
@@ -477,16 +549,16 @@ def test_get_autodesktop_dir_and_arch(
     :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(MetadataFactory, "fetch_arches", lambda *args: mocked_arches)
     monkeypatch.setattr(Cli, "run", lambda *args: 0)
 
-    target = "android"
     version = "6.2.3"
     cli = Cli()
     cli._setup_settings()
 
+    flavor = "MSVC Arm64" if arch == "win64_msvc2019_arm64" else target
     expect_msg_prefix = (
-        f"You are installing the {target} version of Qt, "
+        f"You are installing the {flavor} version of Qt, "
         f"which requires that the desktop version of Qt is also installed."
     )
 
@@ -497,7 +569,7 @@ def test_get_autodesktop_dir_and_arch(
             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)
+            is_auto, host, target, base_dir, Version(version), arch
         )
         # It should choose the correct desktop arch directory for updates
         assert autodesktop_arch_dir == expect["use_dir"]
diff --git a/tests/test_install.py b/tests/test_install.py
index 507d498..0d386f6 100644
--- a/tests/test_install.py
+++ b/tests/test_install.py
@@ -165,8 +165,15 @@ def make_mock_geturl_download_archive(
     desktop_xml = "<Updates>\n{}\n</Updates>".format(
         "\n".join([archive.xml_package_update() for archive in desktop_archives])
     )
+    merged_xml = "<Updates>\n{}{}\n</Updates>".format(
+        "\n".join([archive.xml_package_update() for archive in standard_archives]),
+        "\n".join([archive.xml_package_update() for archive in desktop_archives]),
+    )
 
     def mock_getUrl(url: str, *args, **kwargs) -> str:
+        if standard_updates_url == desktop_updates_url and url.endswith(standard_updates_url):
+            # Edge case where both standard and desktop come from the same Updates.xml: ie msvc2019_arm64 and msvc2019_64
+            return merged_xml
         for xml, updates_url in (
             (standard_xml, standard_updates_url),
             (desktop_xml, desktop_updates_url),
@@ -732,6 +739,68 @@ def tool_archive(host: str, tool_name: str, variant: str, date: datetime = datet
                 r"INFO    : Time elapsed: .* second"
             ),
         ),
+        (
+            "install-qt windows desktop 6.5.2 win64_msvc2019_arm64 --autodesktop".split(),
+            "windows",
+            "desktop",
+            "6.5.2",
+            {"std": "win64_msvc2019_arm64", "desk": "win64_msvc2019_64"},
+            {"std": "msvc2019_arm64", "desk": "msvc2019_64"},
+            {"std": "windows_x86/desktop/qt6_652/Updates.xml", "desk": "windows_x86/desktop/qt6_652/Updates.xml"},
+            {
+                "std": [
+                    MockArchive(
+                        filename_7z="qtbase-windows-win64_msvc2019_arm64.7z",
+                        update_xml_name="qt.qt6.652.win64_msvc2019_arm64",
+                        contents=(
+                            # Qt 6 msvc-arm64 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.5.2{sep}msvc2019_arm64{sep}target\n"
+                                "HostPrefix=../../msvc2019_64\n"
+                                "HostData=../msvc2019_arm64\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.5.2\\msvc2019_64\\bin\n"
+                                "... blah blah blah ...\n",
+                            ),
+                        ),
+                    ),
+                ],
+                "desk": [plain_qtbase_archive("qt.qt6.652.win64_msvc2019_64", "win64_msvc2019_64", host="windows")],
+            },
+            re.compile(
+                r"^INFO    : aqtinstall\(aqt\) v.* on Python 3.*\n"
+                r"INFO    : You are installing the MSVC Arm64 version of Qt, which requires that the desktop version of "
+                r"Qt is also installed. Now installing Qt: desktop 6.5.2 win64_msvc2019_64\n"
+                r"INFO    : Downloading qtbase...\n"
+                r"Finished installation of qtbase-windows-win64_msvc2019_arm64.7z in .*\n"
+                r"INFO    : Downloading qtbase...\n"
+                r"Finished installation of qtbase-windows-win64_msvc2019_64.7z in .*\n"
+                r"INFO    : Patching .*6\.5\.2[/\\]msvc2019_arm64[/\\]bin[/\\]qmake.bat\n"
+                r"INFO    : Finished installation\n"
+                r"INFO    : Time elapsed: .* second"
+            ),
+        ),
         (
             "install-qt linux android 6.4.1 android_arm64_v8a".split(),
             "linux",
@@ -1084,8 +1153,8 @@ def test_install(
     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],
+        "aqt.metadata.MetadataFactory.fetch_arches",
+        lambda *args: [{"windows": "win64_mingw1234", "linux": "gcc_64", "mac": "clang_64"}[host]],
     )
 
     with TemporaryDirectory() as output_dir: