Repository: miurahr/aqtinstall
Pull request #684 (2023-05-29 16:59:10)

Add `ignore_hash` option in `settings.ini`
Based on the work of @mardy and as a quick workaround for some problems described in #521 and #224, here is a proposition to add `ignore_hash` option in `settings.ini`. When set to `True`, `sha256` hashes would not be checked.

ddalcino: You could save yourself and a lot of users some trouble by checking the sha1 hash (just pass `sha1` to the get_hash function), rather than not checking the sha256 hash at all. The sha1 hash is pretty much always available at the mirrors. Failure to check the hash at all makes it pretty easy to download corrupted files.

We started having trouble with #521 when we moved to sha256 for security reasons. The sha256 hash is often unavailable at mirrors, and not available for a few hours after new releases are uploaded.

Whatever you call the new option, I think we need to make it extremely clear that this option is insecure and not for production use. Personally, I would include the text `INSECURE_NOT_FOR_PRODUCTION` in the name of the option itself. We don't want to see malware getting into anyone's production binaries because someone didn't understand the consequences of turning on this option.

Also, this feature is going to need documentation (see the `docs` folder).
lebarsfa: Here are some changes:
- As suggested, renamed `ignore_hash` option to `INSECURE_NOT_FOR_PRODUCTION_ignore_hash`.
- New `hash_algorithm` option, to be able to choose `sha1` instead of `sha256`. However, if https://download.qt.io/ is not available, it seems the desired mirror will need to be added to `trusted_mirrors` list for the hashes to be downloaded from there. Unfortunately, it appears that the mirrors often do not provide any hash for Updates.xml, so I proposed to just show a warning for that specific file when this happens, and continue the downloads of the other files, which should have available hashes.
- Documentation for both options.

I am not sure of what to do with sonatype-lift comments, I did not have them on my fork...

Patch:
diff --git a/.github/workflows/upload-release-artifacts.yml b/.github/workflows/upload-release-artifacts.yml
index 5c78bc5..026380f 100644
--- a/.github/workflows/upload-release-artifacts.yml
+++ b/.github/workflows/upload-release-artifacts.yml
@@ -12,12 +12,18 @@ on:
 
 jobs:
   build-standalone:
-    runs-on: ${{ matrix.os }}
+    runs-on: ${{ matrix.system.os }}
     strategy:
       matrix:
-        os: [windows-latest]
+        system: [
+          {os: windows-latest, arch: x86, output_file: 'aqt_x86.exe', primary_artifact: '', secondary_artifact: 'aqt_x86.exe'},
+          {os: windows-latest, arch: x64, output_file: 'aqt_x64.exe', primary_artifact: 'aqt.exe', secondary_artifact: 'aqt_x64.exe'},
+          {os: macOS-latest, arch: x64, output_file: 'aqt_x64', primary_artifact: 'aqt-macos', secondary_artifact: 'aqt-macos_x64'}
+        ]
         py: [3.9]
-        arch: [x86, x64]
+    defaults:
+      run:
+        shell: ${{ matrix.system.os == 'windows-latest' && 'pwsh' || 'bash' }} {0}
     steps:
       - uses: actions/checkout@v3
         with:
@@ -27,35 +33,35 @@ jobs:
         uses: actions/setup-python@v4
         with:
           python-version: ${{ matrix.py }}
-          architecture: ${{ matrix.arch }}
+          architecture: ${{ matrix.system.arch }}
       - name: Build standalone binary
         run: |
           python -m venv venv
-          venv/Scripts/activate.ps1
+          ${{ matrix.system.os == 'windows-latest' && 'venv/Scripts/activate.ps1' || 'chmod +x venv/bin/activate && venv/bin/activate' }}
           python -m pip install -U pip wheel setuptools setuptools_scm pyinstaller
           python -m pip install .
-          python tools/build_standalone.py ${{ matrix.arch }}
-          deactivate
-          Remove-Item venv -Recurse -Force
-        shell: pwsh
+          python tools/build_standalone.py ${{ matrix.system.arch }}
+          ${{ matrix.system.os == 'windows-latest' && 'deactivate' || 'chmod +x venv/bin/deactivate && venv/bin/deactivate' }}
+          ${{ matrix.system.os == 'windows-latest' && 'Remove-Item venv -Recurse -Force' || 'rm -rf venv' }}
 
       - name: Upload to Release
         uses: svenstaro/upload-release-action@v2
         with:
           repo_token: ${{ secrets.GITHUB_TOKEN }}
-          file: dist\aqt_x64.exe
-          asset_name: aqt.exe
+          file: dist/${{ matrix.system.output_file }}
+          asset_name: ${{ matrix.system.primary_artifact }}
           tag: ${{ github.ref }}
           overwrite: true
-        if: matrix.arch=='x64' && startsWith(github.ref, 'refs/tags/v')
+        if: matrix.system.primary_artifact!='' && startsWith(github.ref, 'refs/tags/v')
       - name: Upload to Release for all architectures
         uses: svenstaro/upload-release-action@v2
         with:
           repo_token: ${{ secrets.GITHUB_TOKEN }}
-          file: dist\aqt_${{ matrix.arch }}.exe
+          file: dist/${{ matrix.system.output_file }}
+          asset_name: ${{ matrix.system.secondary_artifact }}
           tag: ${{ github.ref }}
           overwrite: true
-        if: startsWith(github.ref, 'refs/tags/v')
+        if: matrix.system.secondary_artifact!='' && startsWith(github.ref, 'refs/tags/v')
       - name: Update continuous build
         uses: svenstaro/upload-release-action@v2
         with:
@@ -63,9 +69,9 @@ jobs:
           overwrite: true
           prerelease: true
           tag: Continuous
-          file: dist\aqt_x64.exe
-          asset_name: aqt.exe
-        if: matrix.arch=='x64' && startsWith(github.ref, 'refs/tags/v')
+          file: dist/${{ matrix.system.output_file }}
+          asset_name: ${{ matrix.system.primary_artifact }}
+        if: matrix.system.primary_artifact!='' && startsWith(github.ref, 'refs/tags/v')
       - name: Update continuous build for all architectures
         uses: svenstaro/upload-release-action@v2
         with:
@@ -73,5 +79,6 @@ jobs:
           overwrite: true
           prerelease: true
           tag: Continuous
-          file: dist\aqt_${{ matrix.arch }}.exe
-        if: startsWith(github.ref, 'refs/tags/v')
+          file: dist/${{ matrix.system.output_file }}
+          asset_name: ${{ matrix.system.secondary_artifact }}
+        if: matrix.system.secondary_artifact!='' && startsWith(github.ref, 'refs/tags/v')
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/archives.py b/aqt/archives.py
index 214ca0e..6559844 100644
--- a/aqt/archives.py
+++ b/aqt/archives.py
@@ -27,7 +27,7 @@ from xml.etree.ElementTree import Element  # noqa
 
 from defusedxml import ElementTree
 
-from aqt.exceptions import ArchiveDownloadError, ArchiveListError, NoPackageFound
+from aqt.exceptions import ArchiveDownloadError, ArchiveListError, ChecksumDownloadFailure, NoPackageFound
 from aqt.helper import Settings, get_hash, getUrl, ssplit
 from aqt.metadata import QtRepoProperty, Version
 
@@ -390,7 +390,16 @@ class QtArchives:
 
     def _download_update_xml(self, update_xml_path):
         """Hook for unit test."""
-        xml_hash = get_hash(update_xml_path, "sha256", self.timeout)
+        if not Settings.ignore_hash:
+            try:
+                xml_hash = get_hash(update_xml_path, Settings.hash_algorithm, self.timeout)
+            except ChecksumDownloadFailure:
+                self.logger.warning(
+                    "Failed to download checksum for the file 'Updates.xml'. This may happen on unofficial mirrors."
+                )
+                xml_hash = None
+        else:
+            xml_hash = None
         return getUrl(posixpath.join(self.base, update_xml_path), self.timeout, xml_hash)
 
     def _parse_update_xml(self, os_target_folder, update_xml_text, target_packages: Optional[ModuleToPackage]):
diff --git a/aqt/combinations.json b/aqt/combinations.json
index e3d7ef6..5767f99 100644
--- a/aqt/combinations.json
+++ b/aqt/combinations.json
@@ -272,6 +272,45 @@
           "qtwebview"
         ],
         "qt_version": "6.5"
+      },
+      {
+        "modules": [
+          "debug_info",
+          "qt3d",
+          "qt5compat",
+          "qtcharts",
+          "qtconnectivity",
+          "qtdatavis3d",
+          "qtgraphs",
+          "qtgrpc",
+          "qthttpserver",
+          "qtimageformats",
+          "qtlanguageserver",
+          "qtlocation",
+          "qtlottie",
+          "qtmultimedia",
+          "qtnetworkauth",
+          "qtpdf",
+          "qtpositioning",
+          "qtquick3d",
+          "qtquick3dphysics",
+          "qtquickeffectmaker",
+          "qtquicktimeline",
+          "qtremoteobjects",
+          "qtscxml",
+          "qtsensors",
+          "qtserialbus",
+          "qtserialport",
+          "qtshadertools",
+          "qtspeech",
+          "qtvirtualkeyboard",
+          "qtwaylandcompositor",
+          "qtwebchannel",
+          "qtwebengine",
+          "qtwebsockets",
+          "qtwebview"
+        ],
+        "qt_version": "6.6"
       }
     ],
     "new_archive": [
@@ -577,7 +616,7 @@
         "tool_name": "tools_generic"
       },
       {
-        "arch": "qt.tools.ifw.45",
+        "arch": "qt.tools.ifw.46",
         "os_name": "linux",
         "target": "desktop",
         "tool_name": "tools_ifw"
@@ -594,18 +633,6 @@
         "target": "desktop",
         "tool_name": "tools_ninja"
       },
-      {
-        "arch": "qt.tools.openssl.src",
-        "os_name": "linux",
-        "target": "desktop",
-        "tool_name": "tools_openssl_src"
-      },
-      {
-        "arch": "qt.tools.openssl.gcc_64",
-        "os_name": "linux",
-        "target": "desktop",
-        "tool_name": "tools_openssl_x64"
-      },
       {
         "arch": "qt.tools.opensslv3.src",
         "os_name": "linux",
@@ -678,12 +705,24 @@
         "target": "desktop",
         "tool_name": "tools_qtdesignstudio_generation2"
       },
+      {
+        "arch": "qtdesignstudio.lts",
+        "os_name": "linux",
+        "target": "desktop",
+        "tool_name": "tools_qtdesignstudio_generation2_lts"
+      },
       {
         "arch": "qt.tools.qtcreator.telemetry",
         "os_name": "linux",
         "target": "desktop",
         "tool_name": "tools_telemetry"
       },
+      {
+        "arch": "qt.tools.qtcreator_gui.telemetry_evaluator",
+        "os_name": "linux",
+        "target": "desktop",
+        "tool_name": "tools_telemetry_eval_gui"
+      },
       {
         "arch": "qt.tools.qtcreator_gui.telemetry",
         "os_name": "linux",
@@ -721,7 +760,7 @@
         "tool_name": "tools_generic"
       },
       {
-        "arch": "qt.tools.ifw.45",
+        "arch": "qt.tools.ifw.46",
         "os_name": "mac",
         "target": "desktop",
         "tool_name": "tools_ifw"
@@ -792,12 +831,24 @@
         "target": "desktop",
         "tool_name": "tools_qtdesignstudio_generation2"
       },
+      {
+        "arch": "qtdesignstudio.lts",
+        "os_name": "mac",
+        "target": "desktop",
+        "tool_name": "tools_qtdesignstudio_generation2_lts"
+      },
       {
         "arch": "qt.tools.qtcreator.telemetry",
         "os_name": "mac",
         "target": "desktop",
         "tool_name": "tools_telemetry"
       },
+      {
+        "arch": "qt.tools.qtcreator_gui.telemetry_evaluator",
+        "os_name": "mac",
+        "target": "desktop",
+        "tool_name": "tools_telemetry_eval_gui"
+      },
       {
         "arch": "qt.tools.qtcreator_gui.telemetry",
         "os_name": "mac",
@@ -835,7 +886,7 @@
         "tool_name": "tools_generic"
       },
       {
-        "arch": "qt.tools.ifw.45",
+        "arch": "qt.tools.ifw.46",
         "os_name": "windows",
         "target": "desktop",
         "tool_name": "tools_ifw"
@@ -918,24 +969,6 @@
         "target": "desktop",
         "tool_name": "tools_ninja"
       },
-      {
-        "arch": "qt.tools.openssl.src",
-        "os_name": "windows",
-        "target": "desktop",
-        "tool_name": "tools_openssl_src"
-      },
-      {
-        "arch": "qt.tools.openssl.win_x64",
-        "os_name": "windows",
-        "target": "desktop",
-        "tool_name": "tools_openssl_x64"
-      },
-      {
-        "arch": "qt.tools.openssl.win_x86",
-        "os_name": "windows",
-        "target": "desktop",
-        "tool_name": "tools_openssl_x86"
-      },
       {
         "arch": "qt.tools.opensslv3.src",
         "os_name": "windows",
@@ -1020,12 +1053,24 @@
         "target": "desktop",
         "tool_name": "tools_qtdesignstudio_generation2"
       },
+      {
+        "arch": "qtdesignstudio.lts",
+        "os_name": "windows",
+        "target": "desktop",
+        "tool_name": "tools_qtdesignstudio_generation2_lts"
+      },
       {
         "arch": "qt.tools.qtcreator.telemetry",
         "os_name": "windows",
         "target": "desktop",
         "tool_name": "tools_telemetry"
       },
+      {
+        "arch": "qt.tools.qtcreator_gui.telemetry_evaluator",
+        "os_name": "windows",
+        "target": "desktop",
+        "tool_name": "tools_telemetry_eval_gui"
+      },
       {
         "arch": "qt.tools.qtcreator_gui.telemetry",
         "os_name": "windows",
@@ -1153,7 +1198,11 @@
       "6.4.1",
       "6.4.2",
       "6.4.3",
-      "6.5.0"
+      "6.5.0",
+      "6.5.1",
+      "6.5.2",
+      "6.5.3",
+      "6.6.0"
     ]
   }
 ]
\ No newline at end of file
diff --git a/aqt/helper.py b/aqt/helper.py
index 2e6dee7..4529acb 100644
--- a/aqt/helper.py
+++ b/aqt/helper.py
@@ -94,7 +94,15 @@ def getUrl(url: str, timeout: Tuple[float, float], expected_hash: Optional[bytes
                 raise ArchiveDownloadError(msg)
         result: str = r.text
         filename = url.split("/")[-1]
-        actual_hash = hashlib.sha256(bytes(result, "utf-8")).digest()
+        _kwargs = {"usedforsecurity": False} if sys.version_info >= (3, 9) else {}
+        if Settings.hash_algorithm == "sha256":
+            actual_hash = hashlib.sha256(bytes(result, "utf-8"), **_kwargs).digest()
+        elif Settings.hash_algorithm == "sha1":
+            actual_hash = hashlib.sha1(bytes(result, "utf-8"), **_kwargs).digest()
+        elif Settings.hash_algorithm == "md5":
+            actual_hash = hashlib.md5(bytes(result, "utf-8"), **_kwargs).digest()
+        else:
+            raise ArchiveChecksumError(f"Unknown hash algorithm: {Settings.hash_algorithm}.\nPlease check settings.ini")
         if expected_hash is not None and expected_hash != actual_hash:
             raise ArchiveChecksumError(
                 f"Downloaded file {filename} is corrupted! Detect checksum error.\n"
@@ -104,7 +112,7 @@ def getUrl(url: str, timeout: Tuple[float, float], expected_hash: Optional[bytes
     return result
 
 
-def downloadBinaryFile(url: str, out: Path, hash_algo: str, exp: bytes, timeout: Tuple[float, float]) -> None:
+def downloadBinaryFile(url: str, out: Path, hash_algo: str, exp: Optional[bytes], timeout: Tuple[float, float]) -> None:
     logger = getLogger("aqt.helper")
     filename = Path(url).name
     with requests.sessions.Session() as session:
@@ -126,7 +134,10 @@ def downloadBinaryFile(url: str, out: Path, hash_algo: str, exp: bytes, timeout:
         except requests.exceptions.Timeout as e:
             raise ArchiveConnectionError(f"Connection timeout: {e.args}") from e
         else:
-            hash = hashlib.new(hash_algo)
+            if sys.version_info >= (3, 9):
+                hash = hashlib.new(hash_algo, usedforsecurity=False)
+            else:
+                hash = hashlib.new(hash_algo)
             try:
                 with open(out, "wb") as fd:
                     for chunk in r.iter_content(chunk_size=8196):
@@ -456,6 +467,14 @@ class SettingsClass:
     def max_retries_to_retrieve_hash(self):
         return self.config.getint("requests", "max_retries_to_retrieve_hash", fallback=int(self.max_retries))
 
+    @property
+    def hash_algorithm(self):
+        return self.config.get("requests", "hash_algorithm", fallback="sha256")
+
+    @property
+    def ignore_hash(self):
+        return self.config.getboolean("requests", "INSECURE_NOT_FOR_PRODUCTION_ignore_hash", fallback=False)
+
     @property
     def backoff_factor(self):
         return self.config.getfloat("requests", "retry_backoff", fallback=0.1)
diff --git a/aqt/installer.py b/aqt/installer.py
index 4dec756..458e217 100644
--- a/aqt/installer.py
+++ b/aqt/installer.py
@@ -233,21 +233,27 @@ class Cli:
             "This may not install properly, but we will try our best."
         )
 
-    def _set_sevenzip(self, external):
+    def _set_sevenzip(self, external: Optional[str]) -> Optional[str]:
         sevenzip = external
+        fallback = Settings.zipcmd
         if sevenzip is None:
-            return None
-
+            if EXT7Z:
+                self.logger.warning(f"The py7zr module failed to load. Falling back to '{fallback}' for .7z extraction.")
+                self.logger.warning("You can use the  '--external | -E' flags to select your own extraction tool.")
+                sevenzip = fallback
+            else:
+                # Just use py7zr
+                return None
         try:
             subprocess.run(
                 [sevenzip, "--help"],
                 stdout=subprocess.DEVNULL,
                 stderr=subprocess.DEVNULL,
             )
+            return sevenzip
         except FileNotFoundError as e:
-            raise CliInputError("Specified 7zip command executable does not exist: {!r}".format(sevenzip)) from e
-
-        return sevenzip
+            qualifier = "Specified" if sevenzip == external else "Fallback"
+            raise CliInputError(f"{qualifier} 7zip command executable does not exist: '{sevenzip}'") from e
 
     @staticmethod
     def _set_arch(arch: Optional[str], os_name: str, target: str, qt_version_or_spec: str) -> str:
@@ -358,9 +364,6 @@ class Cli:
             timeout = (Settings.connection_timeout, Settings.response_timeout)
         modules = args.modules
         sevenzip = self._set_sevenzip(args.external)
-        if EXT7Z and sevenzip is None:
-            # override when py7zr is not exist
-            sevenzip = self._set_sevenzip("7z")
         if args.base is not None:
             if not self._check_mirror(args.base):
                 raise CliInputError(
@@ -390,7 +393,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]:
@@ -475,9 +478,6 @@ class Cli:
         else:
             timeout = (Settings.connection_timeout, Settings.response_timeout)
         sevenzip = self._set_sevenzip(args.external)
-        if EXT7Z and sevenzip is None:
-            # override when py7zr is not exist
-            sevenzip = self._set_sevenzip(Settings.zipcmd)
         modules = getattr(args, "modules", None)  # `--modules` is invalid for `install-src`
         archives = args.archives
         all_extra = True if modules is not None and "all" in modules else False
@@ -549,9 +549,6 @@ class Cli:
         else:
             base_dir = output_dir
         sevenzip = self._set_sevenzip(args.external)
-        if EXT7Z and sevenzip is None:
-            # override when py7zr is not exist
-            sevenzip = self._set_sevenzip(Settings.zipcmd)
         version = getattr(args, "version", None)
         if version is not None:
             Cli._validate_version_str(version, allow_minus=True)
@@ -726,8 +723,9 @@ class Cli:
         install_qt_parser.add_argument(
             "--autodesktop",
             action="store_true",
-            help="For Qt6 android, ios, and wasm installations, an additional desktop Qt installation is required. "
-            "When enabled, this option installs the required desktop version automatically.",
+            help="For Qt6 android, ios, wasm, and msvc_arm64 installations, an additional desktop Qt installation is "
+            "required. When enabled, this option installs the required desktop version automatically. "
+            "It has no effect when the desktop installation is not required.",
         )
 
     def _set_install_tool_parser(self, install_tool_parser, *, is_legacy: bool):
@@ -1069,24 +1067,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(
@@ -1209,12 +1217,12 @@ def installer(
     logger.addHandler(qh)
     #
     timeout = (Settings.connection_timeout, Settings.response_timeout)
-    hash = get_hash(qt_package.archive_path, algorithm="sha256", timeout=timeout)
+    hash = get_hash(qt_package.archive_path, Settings.hash_algorithm, timeout) if not Settings.ignore_hash else None
 
     def download_bin(_base_url):
         url = posixpath.join(_base_url, qt_package.archive_path)
         logger.debug("Download URL: {}".format(url))
-        return downloadBinaryFile(url, archive, "sha256", hash, timeout)
+        return downloadBinaryFile(url, archive, Settings.hash_algorithm, hash, timeout)
 
     retry_on_errors(
         action=lambda: retry_on_bad_connection(download_bin, base_url),
@@ -1226,7 +1234,12 @@ def installer(
 
     if tarfile.is_tarfile(archive):
         with tarfile.open(archive) as tar_archive:
-            tar_archive.extractall(path=base_dir)
+            if hasattr(tarfile, "data_filter"):
+                tar_archive.extractall(filter="tar", path=base_dir)
+            else:
+                # remove this when the minimum Python version is 3.12
+                logger.warning("Extracting may be unsafe; consider updating Python to 3.11.4 or greater")
+                tar_archive.extractall(path=base_dir)
     elif zipfile.is_zipfile(archive):
         with zipfile.ZipFile(archive) as zip_archive:
             zip_archive.extractall(path=base_dir)
diff --git a/aqt/metadata.py b/aqt/metadata.py
index 64a98b2..a2ac66d 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()
@@ -699,7 +707,7 @@ class MetadataFactory:
 
     def fetch_http(self, rest_of_url: str, is_check_hash: bool = True) -> str:
         timeout = (Settings.connection_timeout, Settings.response_timeout)
-        expected_hash = get_hash(rest_of_url, "sha256", timeout) if is_check_hash else None
+        expected_hash = get_hash(rest_of_url, Settings.hash_algorithm, timeout) if is_check_hash else None
         base_urls = self.base_url, random.choice(Settings.fallbacks)
 
         err: BaseException = AssertionError("unraisable")
@@ -784,7 +792,7 @@ class MetadataFactory:
 
     def _fetch_module_metadata(self, folder: str, predicate: Optional[Callable[[Element], bool]] = None):
         rest_of_url = posixpath.join(self.archive_id.to_url(), folder, "Updates.xml")
-        xml = self.fetch_http(rest_of_url)
+        xml = self.fetch_http(rest_of_url) if not Settings.ignore_hash else self.fetch_http(rest_of_url, False)
         return xml_to_modules(
             xml,
             predicate=predicate if predicate else MetadataFactory._has_nonempty_downloads,
@@ -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/settings.ini b/aqt/settings.ini
index 9b37968..3641e00 100644
--- a/aqt/settings.ini
+++ b/aqt/settings.ini
@@ -16,6 +16,8 @@ max_retries_on_connection_error: 5
 retry_backoff: 0.1
 max_retries_on_checksum_error: 5
 max_retries_to_retrieve_hash: 5
+hash_algorithm: sha256
+INSECURE_NOT_FOR_PRODUCTION_ignore_hash: False
 
 [mirrors]
 trusted_mirrors:
@@ -27,7 +29,6 @@ blacklist:
 fallbacks:
     https://qtproject.mirror.liquidtelecom.com/
     https://mirrors.aliyun.com/qt/
-    https://mirrors.sjtug.sjtu.edu.cn/qt/
     https://mirrors.ustc.edu.cn/qtproject/
     https://ftp.jaist.ac.jp/pub/qtproject/
     https://ftp.yz.yamagata-u.ac.jp/pub/qtproject/
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
 
   ##----------------------------------------------------
diff --git a/docs/CHANGELOG.rst b/docs/CHANGELOG.rst
index 6ebb24f..474e936 100644
--- a/docs/CHANGELOG.rst
+++ b/docs/CHANGELOG.rst
@@ -13,6 +13,46 @@ All notable changes to this project will be documented in this file.
 `Unreleased`_
 =============
 
+`v3.1.8`_ (1, Nov. 2023)
+========================
+
+Changed
+-------
+- Add 6.5.3 and openssl as known versions (#718)
+- Docs: remove deprecated configuration description (#714)
+- Test: test on python 3.8, 3.9 and 3.11 (#715)
+- Docs: Update documentation for ``--autodesktop`` flag (#713)
+- Use 'tar' filter when extracting tarfiles (#707)
+- Log a warning when aqtinstall falls back to an external 7z extraction tool (#705)
+- Bump py7zr@0.20.6(#702)
+
+Fixed
+-----
+- Fix failed CI (#716)
+- Fix installation of win64_msvc2019_arm64 arch (#711)
+- Fix ``test_install`` that fails on Python<3.11.4 (#708)
+- Fix failing documentation builds (#706)
+- Fix: exception when target path is relative (#702)
+
+`v3.1.7`_ (1, Aug. 2023)
+========================
+
+Added
+-----
+Add support for standalone sdktool installation(#677)
+
+Fixed
+-----
+- Fixed command to check tools_mingw90 (#680)
+- Fixed help text for list-tool
+
+Changed
+-------
+* Add Qt 6.6.0, 6.5.2 and 6.5.1 as known version(#685,#698)
+* Default blacklist setting(#689)
+* Add test for sdktool(#678)
+
+
 `v3.1.6`_ (4, May, 2023)
 ========================
 
@@ -227,7 +267,9 @@ Security
 * Check Update.xml file with SHA256 hash (#493)
 
 
-.. _Unreleased: https://github.com/miurahr/aqtinstall/compare/v3.1.6...HEAD
+.. _Unreleased: https://github.com/miurahr/aqtinstall/compare/v3.1.8...HEAD
+.. _v3.1.8: https://github.com/miurahr/aqtinstall/compare/v3.1.7...v3.1.8
+.. _v3.1.7: https://github.com/miurahr/aqtinstall/compare/v3.1.6...v3.1.7
 .. _v3.1.6: https://github.com/miurahr/aqtinstall/compare/v3.1.5...v3.1.6
 .. _v3.1.5: https://github.com/miurahr/aqtinstall/compare/v3.1.4...v3.1.5
 .. _v3.1.4: https://github.com/miurahr/aqtinstall/compare/v3.1.3...v3.1.4
diff --git a/docs/cli.rst b/docs/cli.rst
index 32c8e6c..756e61c 100644
--- a/docs/cli.rst
+++ b/docs/cli.rst
@@ -387,7 +387,7 @@ are described here:
 
 .. _py7zr: https://pypi.org/project/py7zr/
 .. _7-zip: https://www.7-zip.org/
-.. _Choco: https://community.chocolatey.org/packages/7zip/19.0
+.. _Choco: https://community.chocolatey.org/packages/7zip/
 .. _brew: https://formulae.brew.sh/formula/p7zip
 
 .. option:: --internal
@@ -545,10 +545,10 @@ There are various combinations to accept according to Qt version.
 
 .. option:: --autodesktop
 
-    If you are installing an ios or android version of Qt6, or the WASM version of Qt6,
+    If you are installing an ios, android, WASM, or msvc_arm64 version of Qt6,
     the corresponding desktop version of Qt must be installed alongside of it.
     Turn this option on to install it automatically.
-    This option will have no effect if you are installing Qt5 or a non-WASM desktop version of Qt6.
+    This option will have no effect if the desktop version of Qt is not required.
 
 .. option:: --noarchives
 
diff --git a/docs/conf.py b/docs/conf.py
index 1acd7a8..f2bea7b 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -55,7 +55,7 @@ master_doc = 'index'
 
 # General information about the project.
 project = u'aqtinstall'
-copyright = u'2019-2021, Hiroshi Miura'
+copyright = u'2019-2023, Hiroshi Miura'
 author = u'Hiroshi Miura'
 
 # The version info for the project you're documenting, acts as replacement for
diff --git a/docs/configuration.rst b/docs/configuration.rst
index 9d78b00..b0a4b29 100644
--- a/docs/configuration.rst
+++ b/docs/configuration.rst
@@ -31,6 +31,8 @@ A file is like as follows:
     retry_backoff: 0.1
     max_retries_on_checksum_error: 5
     max_retries_to_retrieve_hash: 5
+    hash_algorithm: sha256
+    INSECURE_NOT_FOR_PRODUCTION_ignore_hash: False
 
     [mirrors]
     trusted_mirrors:
@@ -130,6 +132,18 @@ max_retries_on_checksum_error:
     This setting controls how many times ``aqt`` will attempt to download a file,
     in the case of a checksum error.
 
+hash_algorithm:
+    This is either ``sha256``, ``sha1`` or ``md5``. ``sha256`` is the only safe 
+    value to use here. Default is ``sha256`` if not set.
+    See also ``trusted_mirrors`` setting.
+
+INSECURE_NOT_FOR_PRODUCTION_ignore_hash:
+    This is either ``True`` or ``False``.
+    The ``True`` setting disables hash checking when downloading files. Although
+    this is not recommended, this may help when hashes are not available.
+    The ``False`` setting will enforce hash checking. This is highly recommended
+    to avoid corrupted files.
+
 
 The ``[mirrors]`` section is a configuration for mirror handling.
 
@@ -156,8 +170,8 @@ blacklist:
     Some mirror sites ignore a connection from IP addresses out of their preffered one.
     It will cause connection error or connection timeout.
     There are some known mirror sites in default.
-    When you are happy with the default sites,
-    you can override with your custom settings.
+    If you are not happy with the default sites,
+    you can override them with custom settings.
 
 fallbacks:
     It is a list of URL where is a good for access.
diff --git a/pyproject.toml b/pyproject.toml
index c3af771..624b42e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -26,7 +26,7 @@ dependencies = [
     "defusedxml",
     "humanize",
     "patch>=1.16",
-    "py7zr>=0.20.2",
+    "py7zr>=0.20.6",
     "requests>2.20.0",
     "semantic-version",
     "texttable",
@@ -71,9 +71,9 @@ check = [
     "packaging",
 ]
 docs = [
-    "sphinx>=5.0",
-    "sphinx_rtd_theme",
-    "sphinx-py3doc-enhanced-theme",
+    "sphinx>=7.0",
+    "sphinx_rtd_theme>=1.3",
+    "sphinx-py3doc-enhanced-theme>=2.4",
 ]
 
 [project.urls]


Test patch:
diff --git a/.github/workflows/test-install-qt.yml b/.github/workflows/test-install-qt.yml
index 359a8e5..0f070cf 100644
--- a/.github/workflows/test-install-qt.yml
+++ b/.github/workflows/test-install-qt.yml
@@ -1,6 +1,6 @@
 name: Test on GH actions environment
 
-on: push
+on: [push, pull_request]
 
 jobs:
   test:
@@ -8,7 +8,7 @@ jobs:
     strategy:
       matrix:
         os: [windows-latest, macOS-latest, ubuntu-latest]
-        py: ["3.10"]
+        py: ["3.8", "3.10", "3.11"]
         qtver: [5.9.9, 5.12.8, 6.1.0]
         artifact: [standard]
         include:
@@ -112,15 +112,16 @@ jobs:
           if qtver.startswith('6'):
             command_line6 = []
             command_line6.extend(prefix)
-            if platform == 'ubuntu-20.04':
+            if platform.startswith("ubuntu"):
               command_line6.extend([qtver, "linux", "android", "android_armv7"])
               timeout = 360
-            elif platform == "macOS-latest":
+            elif platform.startswith("macOS"):
               command_line6.extend([qtver, "mac", "ios", "ios"])
               timeout = 360
             else:
               command_line6.extend([qtver, "windows", "android", "android_armv7"])
               timeout = 360
+            print("Execute: {}".format(command_line6))
             try:
               res = subprocess.run(command_line6, timeout=timeout, check=True)
             except subprocess.CalledProcessError as cpe:
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 91a568b..35265ae 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -7,6 +7,7 @@ from typing import Dict, List, Optional
 import pytest
 
 from aqt.exceptions import CliInputError
+from aqt.helper import Settings
 from aqt.installer import Cli
 from aqt.metadata import MetadataFactory, SimpleSpec, Version
 
@@ -382,13 +383,63 @@ def test_cli_unexpected_error(monkeypatch, capsys):
     )
 
 
-def test_cli_set_7zip(monkeypatch):
+@pytest.mark.parametrize("external_tool_exists", (True, False))
+def test_set_7zip_checks_external_tool_when_specified(monkeypatch, capsys, external_tool_exists: bool):
     cli = Cli()
     cli._setup_settings()
-    with pytest.raises(CliInputError) as err:
-        cli._set_sevenzip("some_nonexistent_binary")
-    assert err.type == CliInputError
-    assert format(err.value) == "Specified 7zip command executable does not exist: 'some_nonexistent_binary'"
+    external = "my_7z_extractor"
+
+    def mock_subprocess_run(args, **kwargs):
+        assert args[0] == external
+        if not external_tool_exists:
+            raise FileNotFoundError()
+
+    monkeypatch.setattr("aqt.installer.subprocess.run", mock_subprocess_run)
+    monkeypatch.setattr("aqt.installer.EXT7Z", False)
+    if external_tool_exists:
+        assert external == cli._set_sevenzip(external)
+    else:
+        with pytest.raises(CliInputError) as err:
+            cli._set_sevenzip(external)
+        assert format(err.value) == format(f"Specified 7zip command executable does not exist: '{external}'")
+    assert capsys.readouterr()[1] == ""
+
+
+@pytest.mark.parametrize("fallback_exists", (True, False))
+def test_set_7zip_uses_fallback_when_py7zr_missing(monkeypatch, capsys, fallback_exists: bool):
+    cli = Cli()
+    cli._setup_settings()
+    external, fallback = None, Settings.zipcmd
+
+    def mock_subprocess_run(args, **kwargs):
+        assert args[0] == fallback
+        if not fallback_exists:
+            raise FileNotFoundError()
+
+    monkeypatch.setattr("aqt.installer.subprocess.run", mock_subprocess_run)
+    monkeypatch.setattr("aqt.installer.EXT7Z", True)
+    if fallback_exists:
+        assert fallback == cli._set_sevenzip(external)
+    else:
+        with pytest.raises(CliInputError) as err:
+            cli._set_sevenzip(external)
+        assert format(err.value) == format(f"Fallback 7zip command executable does not exist: '{fallback}'")
+    assert f"Falling back to '{fallback}'" in capsys.readouterr()[1]
+
+
+@pytest.mark.parametrize("fallback_exists", (True, False))
+def test_set_7zip_chooses_p7zr_when_ext_missing(monkeypatch, capsys, fallback_exists: bool):
+    cli = Cli()
+    cli._setup_settings()
+    external = None
+
+    def mock_subprocess_run(args, **kwargs):
+        assert False, "Should not try to run anything"
+
+    monkeypatch.setattr("aqt.installer.subprocess.run", mock_subprocess_run)
+    monkeypatch.setattr("aqt.installer.EXT7Z", False)
+    assert cli._set_sevenzip(external) is None
+    assert capsys.readouterr()[1] == ""
 
 
 @pytest.mark.parametrize(
@@ -415,58 +466,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 +600,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 +620,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 96008ea..0d386f6 100644
--- a/tests/test_install.py
+++ b/tests/test_install.py
@@ -6,6 +6,7 @@ import posixpath
 import re
 import subprocess
 import sys
+import tarfile
 import textwrap
 from dataclasses import dataclass
 from datetime import datetime
@@ -108,7 +109,25 @@ class MockArchive:
         )
 
     def write_compressed_archive(self, dest: Path) -> None:
-        with TemporaryDirectory() as temp_dir, py7zr.SevenZipFile(dest / self.filename_7z, "w") as archive:
+        def open_writable_archive():
+            if self.filename_7z.endswith(".7z"):
+                return py7zr.SevenZipFile(dest / self.filename_7z, "w")
+            elif self.filename_7z.endswith(".tar.xz"):
+                return tarfile.open(dest / self.filename_7z, "w:xz")
+            # elif self.filename_7z.endswith(".zip"):
+            #     return tarfile.open(dest / "DUMMY-NOT-USED", "w")
+            else:
+                assert False, "Archive type not supported"
+
+        def write_to_archive(arc, src, arcname):
+            if self.filename_7z.endswith(".7z"):
+                arc.writeall(path=src, arcname=arcname)
+            elif self.filename_7z.endswith(".tar.xz"):
+                arc.add(name=src, arcname=arcname)
+            # elif self.filename_7z.endswith(".zip"):
+            #     shutil.make_archive(str(dest / self.filename_7z), "zip", src)
+
+        with TemporaryDirectory() as temp_dir, open_writable_archive() as archive:
             temp_path = Path(temp_dir)
 
             for folder in ("bin", "lib", "mkspecs"):
@@ -122,7 +141,7 @@ class MockArchive:
                 full_path.write_text(patched_file.unpatched_content, "utf_8")
 
             archive_name = "5.9" if self.version == "5.9.0" else self.version
-            archive.writeall(path=temp_path, arcname=archive_name)
+            write_to_archive(archive, temp_path, arcname=archive_name)
 
 
 def make_mock_geturl_download_archive(
@@ -138,7 +157,7 @@ def make_mock_geturl_download_archive(
     if desktop_archives is None:
         desktop_archives = []
     for _archive in [*standard_archives, *desktop_archives]:
-        assert _archive.filename_7z.endswith(".7z")
+        assert re.match(r".*\.(7z|tar\.xz)$", _archive.filename_7z), "Unsupported file type"
 
     standard_xml = "<Updates>\n{}\n</Updates>".format(
         "\n".join([archive.xml_package_update() for archive in standard_archives])
@@ -146,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),
@@ -437,6 +463,42 @@ def tool_archive(host: str, tool_name: str, variant: str, date: datetime = datet
                 r"INFO    : Time elapsed: .* second"
             ),
         ),
+        (
+            "install-src linux desktop 6.5.0".split(),
+            "linux",
+            "desktop",
+            "6.5.0",
+            {"std": ""},
+            {"std": ""},
+            {"std": "linux_x64/desktop/qt6_650_src_doc_examples/Updates.xml"},
+            {
+                "std": [
+                    MockArchive(
+                        filename_7z="qtbase-everywhere-src-6.5.0.tar.xz",
+                        update_xml_name="qt.qt6.650.src",
+                        version="6.5.0",
+                        contents=(
+                            PatchedFile(
+                                filename="Src/qtbase/QtBaseSource.cpp",
+                                unpatched_content="int main(){ return 0; }",
+                                patched_content=None,  # not patched
+                            ),
+                        ),
+                    ),
+                ]
+            },
+            re.compile(
+                r"^INFO    : aqtinstall\(aqt\) v.* on Python 3.*\n"
+                r"WARNING : The parameter 'target' with value 'desktop' is deprecated "
+                r"and marked for removal in a future version of aqt\.\n"
+                r"In the future, please omit this parameter\.\n"
+                r"INFO    : Downloading qtbase\.\.\.\n"
+                r"([^\n]*Extracting may be unsafe; consider updating Python to 3.11.4 or greater\n)?"
+                r"Finished installation of qtbase-everywhere-src-6\.5\.0\.tar\.xz in .*\n"
+                r"INFO    : Finished installation\n"
+                r"INFO    : Time elapsed: .* second"
+            ),
+        ),
         (
             "install-src windows 5.14.2".split(),
             "windows",
@@ -677,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",
@@ -1029,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: