Repository: miurahr/aqtinstall (python)

Issue:
[Bug]: aqt 3.2.1 fails under python 3.9
### Bug description

When running aqt version 3.2.1 under python 3.9, it gives this error message:
```
Traceback (most recent call last):
  File "/Users/francis/gitlab/builds/iqwiKzxd/0/kuvacode/smartshooter6/out/venv/bin/aqt", line 5, in <module>
    from aqt.__main__ import main
  File "/Users/francis/gitlab/builds/iqwiKzxd/0/kuvacode/smartshooter6/out/venv/lib/python3.9/site-packages/aqt/__init__.py", line 30, in <module>
    from aqt.installer import Cli
  File "/Users/francis/gitlab/builds/iqwiKzxd/0/kuvacode/smartshooter6/out/venv/lib/python3.9/site-packages/aqt/installer.py", line 45, in <module>
    from aqt.archives import QtArchives, QtPackage, SrcDocExamplesArchives, ToolArchives
  File "/Users/francis/gitlab/builds/iqwiKzxd/0/kuvacode/smartshooter6/out/venv/lib/python3.9/site-packages/aqt/archives.py", line 32, in <module>
    from aqt.helper import Settings, get_hash, getUrl, ssplit
  File "/Users/francis/gitlab/builds/iqwiKzxd/0/kuvacode/smartshooter6/out/venv/lib/python3.9/site-packages/aqt/helper.py", line 650, in <module>
    def extract_auth(args: List[str]) -> Tuple[str | None, str | None, List[str] | None]:
TypeError: unsupported operand type(s) for |: 'type' and 'NoneType'
```

Python 3.9 is the current version present on macOS Sonoma, so it would be nice to fix this.
Reverting back to aqt 3.2.0 solves the issue.

### Expected behavior

Not to trigger python exception.

### aqt and python version

aqt v3.2.1 on python v3.9.6

### Operating System

MacOS

### Relevant log output

```shell
Traceback (most recent call last):
  File "/Users/francis/gitlab/builds/iqwiKzxd/0/kuvacode/smartshooter6/out/venv/bin/aqt", line 5, in <module>
    from aqt.__main__ import main
  File "/Users/francis/gitlab/builds/iqwiKzxd/0/kuvacode/smartshooter6/out/venv/lib/python3.9/site-packages/aqt/__init__.py", line 30, in <module>
    from aqt.installer import Cli
  File "/Users/francis/gitlab/builds/iqwiKzxd/0/kuvacode/smartshooter6/out/venv/lib/python3.9/site-packages/aqt/installer.py", line 45, in <module>
    from aqt.archives import QtArchives, QtPackage, SrcDocExamplesArchives, ToolArchives
  File "/Users/francis/gitlab/builds/iqwiKzxd/0/kuvacode/smartshooter6/out/venv/lib/python3.9/site-packages/aqt/archives.py", line 32, in <module>
    from aqt.helper import Settings, get_hash, getUrl, ssplit
  File "/Users/francis/gitlab/builds/iqwiKzxd/0/kuvacode/smartshooter6/out/venv/lib/python3.9/site-packages/aqt/helper.py", line 650, in <module>
    def extract_auth(args: List[str]) -> Tuple[str | None, str | None, List[str] | None]:
TypeError: unsupported operand type(s) for |: 'type' and 'NoneType'
```

### Code of Conduct

- [x] I agree to follow this project's Code of Conduct

Pull request description:
Add --use-official-installer, fix official installer download after update 4.9
This PR:
- Adds the `--use-official-installer` option to `install-qt`, enabling a straight forward way to use the official installer while still using the standard install command. 
- Adds tests for the `--use-official-installer` option
- Updates `authors.rst` and `official.rst` to fix mistakes in the documentation (warnings reported by Codacy)
- Fixes #905 
- Fixes the official installer download failure caused by the recent name change of the Qt installer binaries

### `use-official-installer` option

```console
--use-official-installer [email pw]
```
If used, this will ignore your `host` parameter, and use the current OS (Qt restriction). You can use it without any parameter if you already signed in (so if you have a qtaccount.ini valid for the current user). Otherwise, you need to provide an email and a password:  
```console
kidev:~$ python -m aqt install-qt linux desktop 6.8.2 linux_gcc_64 -m qtpdf --outputdir ./Qt --use-official-installer 'email@gmail.com' 'password'
kidev:~$ python -m aqt install-qt linux desktop 6.8.2 linux_gcc_64 -m qtpdf --outputdir ./Qt --use-official-installer
```
```console
INFO    : aqtinstall(aqt) v0.1.dev1898 on Python 3.12.8 [CPython GCC 14.2.1 20240910]
INFO    : Using official Qt installer
INFO    : Using credentials provided with --use-official-installer
INFO    : Downloading Qt installer to /home/kidev/.local/share/aqt/tmp/qt-online-installer-linux-x64-online.run
INFO    : Redirected: mirrors.20i.com
INFO    : Running: ['/home/kidev/.local/share/aqt/tmp/qt-online-installer-linux-x64-online.run', '--accept-licenses', '--accept-obligations', '--confirm-command', '--email', '***', '--pw', '***', '--root', '/home/kidev/Work/perso/github/aqtinstall/Qt', '--auto-answer', 'OperationDoesNotExistError=Ignore,OverwriteTargetDirectory=No,stopProcessesForUpdates=Ignore,installationErrorWithCancel=Ignore,installationErrorWithIgnore=Ignore,AssociateCommonFiletypes=Yes,telemetry-question=No', 'install', 'qt.qt6.682.linux_gcc_64']
[0] Arguments: /home/kidev/.local/share/aqt/tmp/qt-online-installer-linux-x64-online.run, --accept-licenses, --accept-obligations, --confirm-command, --email, ******, --pw, ******, --root, /home/kidev/Work/perso/github/aqtinstall/Qt, --auto-answer, OperationDoesNotExistError=Ignore,OverwriteTargetDirectory=No,stopProcessesForUpdates=Ignore,installationErrorWithCancel=Ignore,installationErrorWithIgnore=Ignore,AssociateCommonFiletypes=Yes,telemetry-question=No, install, qt.qt6.682.linux_gcc_64
[1] Operations sanity check succeeded.
[2] Using metadata cache from "/home/kidev/.cache/qt-unified-linux-online"
[2] Found 487 cached items.
[2] QtAccount information:
...
[48546] Done
[48547] Got a replacement installer base binary: "/home/kidev/Work/perso/github/aqtinstall/Qt/installerbase"
[48547] Writing maintenance tool: "/home/kidev/Work/perso/github/aqtinstall/Qt/MaintenanceTool.new"
[48547] Writing maintenance tool.
[48636] Wrote permissions for maintenance tool.
[48640] Wrote the binary with the new replacement.
[48645] Removed installer base binary "/home/kidev/Work/perso/github/aqtinstall/Qt/installerbase" after updating the maintenance tool.
[48661] Maintenance tool hard restart: false.
[48661] Installation finished!
[48662] Components installed successfully
INFO    : Qt installation completed successfully
INFO    : Done
```

Patch:
diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml
index c5b3944..7fbb2bf 100644
--- a/.github/workflows/check.yml
+++ b/.github/workflows/check.yml
@@ -23,10 +23,10 @@ jobs:
       with:
         fetch-depth: 20
         fetch-tags: true
-    - name: Set up Python 3.13
+    - name: Set up Python 3.12
       uses: actions/setup-python@v5
       with:
-        python-version: '3.13'
+        python-version: '3.12'
     - name: Install tools
       run: |
         pip install -U pip
diff --git a/.github/workflows/publish-to-pypi.yml b/.github/workflows/publish-to-pypi.yml
index d9bdbdd..4f185e8 100644
--- a/.github/workflows/publish-to-pypi.yml
+++ b/.github/workflows/publish-to-pypi.yml
@@ -20,10 +20,10 @@ jobs:
           fetch-depth: 50
       - name: Fetch release tag
         run: git fetch --depth=1 origin +refs/tags/*:refs/tags/*
-      - name: Set up Python 3.13🐍
+      - name: Set up Python 3.12🐍
         uses: actions/setup-python@v5
         with:
-          python-version: '3.13'
+          python-version: '3.12'
       - name: Install build and twine
         run: python -m pip install build twine --user
       - name: Build release assets
diff --git a/.github/workflows/run-linter.yml b/.github/workflows/run-linter.yml
index f3c2992..e234871 100644
--- a/.github/workflows/run-linter.yml
+++ b/.github/workflows/run-linter.yml
@@ -20,7 +20,7 @@ jobs:
       - name: Setup python
         uses: actions/setup-python@v5
         with:
-          python-version: '3.13'
+          python-version: '3.12'
           architecture: x64
       - name: Setup reviewdog
         run: |
@@ -43,7 +43,7 @@ jobs:
       - name: Setup python
         uses: actions/setup-python@v5
         with:
-          python-version: '3.13'
+          python-version: '3.12'
           architecture: x64
       - name: Install dependencies
         run: |
diff --git a/.github/workflows/upload-release-artifacts.yml b/.github/workflows/upload-release-artifacts.yml
index 7b7399b..d395bac 100644
--- a/.github/workflows/upload-release-artifacts.yml
+++ b/.github/workflows/upload-release-artifacts.yml
@@ -20,7 +20,7 @@ jobs:
           {os: windows-latest, arch: x64, output_file: 'aqt_x64.exe', primary_artifact: 'aqt.exe', secondary_artifact: 'aqt_x64.exe'},
           {os: macOS-13, arch: x64, output_file: 'aqt_x64', primary_artifact: 'aqt-macos', secondary_artifact: 'aqt-macos_x64'}
         ]
-        py: [3.13]
+        py: [3.12]
     defaults:
       run:
         shell: ${{ matrix.system.os == 'windows-latest' && 'pwsh' || 'bash' }} {0}
diff --git a/.readthedocs.yml b/.readthedocs.yml
index 84b3b30..c8eed33 100644
--- a/.readthedocs.yml
+++ b/.readthedocs.yml
@@ -17,7 +17,7 @@ python:
 build:
   os: ubuntu-22.04
   tools:
-    python: "3.13"
+    python: "3.10"
   jobs:
     post_checkout:
       - git fetch --depth 1000
diff --git a/README.rst b/README.rst
index 58a7fd1..d091f07 100644
--- a/README.rst
+++ b/README.rst
@@ -58,7 +58,7 @@ Requirements
     3.9
 
 - Recommended Python version:
-    3.13 (frequently tested on)
+    3.12 (frequently tested on)
 
 - Dependencies:
     requests
diff --git a/aqt/commercial.py b/aqt/commercial.py
index 4ba7265..8e1dc5e 100644
--- a/aqt/commercial.py
+++ b/aqt/commercial.py
@@ -60,6 +60,7 @@ class QtPackageManager:
         self.packages: List[QtPackageInfo] = []
         self.username = username
         self.password = password
+        self.logger = getLogger("aqt.commercial")
 
     def _get_cache_dir(self) -> Path:
         """Create and return cache directory path."""
@@ -141,6 +142,7 @@ class QtPackageManager:
         cmd.append(base_package)
 
         try:
+            self.logger.info(f"Running: {cmd}")
             output = safely_run_save_output(cmd, Settings.qt_installer_timeout)
 
             # Handle both string and CompletedProcess outputs
@@ -156,8 +158,7 @@ class QtPackageManager:
                 self._save_to_cache()
             else:
                 # Log the actual output for debugging
-                logger = getLogger("aqt.helper")
-                logger.debug(f"Installer output: {output_text}")
+                self.logger.debug(f"Installer output: {output_text}")
                 raise RuntimeError("Failed to find package information in installer output")
 
         except Exception as e:
@@ -217,6 +218,7 @@ class CommercialInstaller:
         override: Optional[List[str]] = None,
         modules: Optional[List[str]] = None,
         no_unattended: bool = False,
+        dry_run: bool = False,
     ):
         self.override = override
         self.target = target
@@ -227,6 +229,7 @@ class CommercialInstaller:
         self.base_url = base_url
         self.modules = modules
         self.no_unattended = no_unattended
+        self.dry_run = dry_run
 
         # Extract credentials from override if present
         if override:
@@ -330,13 +333,12 @@ class CommercialInstaller:
                     raise DiskAccessNotPermitted(f"Failed to remove existing version directory {version_dir}: {str(e)}")
 
         # Setup temp directory
-        import shutil
-
         temp_dir = Settings.qt_installer_temp_path
         temp_path = Path(temp_dir)
-        if temp_path.exists():
-            shutil.rmtree(temp_dir)
-        temp_path.mkdir(parents=True, exist_ok=True)
+        if not temp_path.exists():
+            temp_path.mkdir(parents=True, exist_ok=True)
+        else:
+            Settings.qt_installer_cleanup()
         installer_path = temp_path / self._installer_filename
 
         self.logger.info(f"Downloading Qt installer to {installer_path}")
@@ -371,8 +373,16 @@ class CommercialInstaller:
                 install_cmd = self.package_manager.get_install_command(self.modules, temp_dir)
                 cmd = [*base_cmd, *install_cmd]
 
-            self.logger.info(f"Running: {cmd}")
-            safely_run(cmd, Settings.qt_installer_timeout)
+            log_cmd = cmd.copy()
+            for i in range(len(log_cmd) - 1):
+                if log_cmd[i] == "--email" or log_cmd[i] == "--pw":
+                    log_cmd[i + 1] = "***"
+
+            if not self.dry_run:
+                self.logger.info(f"Running: {log_cmd}")
+                safely_run(cmd, Settings.qt_installer_timeout)
+            else:
+                self.logger.info(f"Would run: {log_cmd}")
 
         except Exception as e:
             self.logger.error(f"Installation failed: {str(e)}")
diff --git a/aqt/helper.py b/aqt/helper.py
index 8ba512f..8941585 100644
--- a/aqt/helper.py
+++ b/aqt/helper.py
@@ -22,6 +22,7 @@ import binascii
 import hashlib
 import logging.config
 import os
+import platform
 import posixpath
 import secrets
 import shutil
@@ -40,6 +41,7 @@ from xml.etree.ElementTree import Element
 import humanize
 import requests
 import requests.adapters
+from bs4 import BeautifulSoup
 from defusedxml import ElementTree
 
 from aqt.exceptions import (
@@ -62,6 +64,23 @@ def get_os_name() -> str:
     raise ValueError(f"Unsupported operating system: {system}")
 
 
+def get_os_arch() -> str:
+    """
+    Returns a simplified os-arch string for the current system
+    """
+    os_name = get_os_name()
+
+    machine = platform.machine().lower()
+    if machine in ["x86_64", "amd64"]:
+        arch = "x64"
+    elif machine in ["arm64", "aarch64"]:
+        arch = "arm64"
+    else:
+        arch = "x64"  # Default to x64 for unknown architectures
+
+    return f"{os_name}-{arch}"
+
+
 def get_qt_local_folder_path() -> Path:
     os_name = get_os_name()
     if os_name == "windows":
@@ -76,13 +95,59 @@ def get_qt_account_path() -> Path:
     return get_qt_local_folder_path() / "qtaccount.ini"
 
 
+def get_qt_installers() -> dict[str, str]:
+    """
+    Extracts Qt installer information from {Settings.baseurl}/official_releases/online_installers/
+    Maps OS types and architectures to their respective installer filenames
+    Returns:
+        dict: Mapping of OS identifiers to installer filenames with appropriate aliases
+    """
+    url = f"{Settings.baseurl}/official_releases/online_installers/"
+
+    try:
+        response = requests.get(url, timeout=Settings.response_timeout)
+        response.raise_for_status()
+
+        soup = BeautifulSoup(response.text, "html.parser")
+
+        installers = {}
+
+        os_types = ["windows", "linux", "mac"]
+
+        for link in soup.find_all("a"):
+            filename = link.text.strip()
+
+            if "Parent Directory" in filename or not any(ext in filename.lower() for ext in [".exe", ".dmg", ".run"]):
+                continue
+
+            for os_type in os_types:
+                if os_type.lower() in filename.lower():
+                    # Found an OS match, now look for architecture
+                    if "arm64" in filename.lower():
+                        installers[f"{os_type}-arm64"] = filename
+                    elif "x64" in filename.lower():
+                        installers[f"{os_type}-x64"] = filename
+                        # Also add generic OS entry for x64 variants of Windows and Linux
+                        if os_type in ["windows", "linux"]:
+                            installers[os_type] = filename
+                    else:
+                        # Handle case with no explicit architecture
+                        # Most likely for macOS which might just say "mac" without arch
+                        installers[os_type] = filename
+
+        return installers
+
+    except requests.exceptions.RequestException as e:
+        print(f"Error fetching installer data: {e}")
+        return {}
+    except Exception as e:
+        print(f"Unexpected error processing installer data: {e}")
+        return {}
+
+
 def get_qt_installer_name() -> str:
-    installer_dict = {
-        "windows": "qt-unified-windows-x64-online.exe",
-        "mac": "qt-unified-mac-x64-online.dmg",
-        "linux": "qt-unified-linux-x64-online.run",
-    }
-    return installer_dict[get_os_name()]
+    installer_dict = get_qt_installers()
+    return installer_dict[get_os_arch()]
 
 
 def get_qt_installer_path() -> Path:
@@ -427,8 +492,13 @@ class SettingsClass:
 
                     logging.info(f"Cache folder: {self.qt_installer_cache_path}")
                     logging.info(f"Temp folder: {self.qt_installer_temp_path}")
-                    if Path(self.qt_installer_temp_path).exists():
-                        shutil.rmtree(self.qt_installer_temp_path)
+
+                    temp_dir = self.qt_installer_temp_path
+                    temp_path = Path(temp_dir)
+                    if not temp_path.exists():
+                        temp_path.mkdir(parents=True, exist_ok=True)
+                    else:
+                        self.qt_installer_cleanup()
 
     def _get_config(self) -> ConfigParser:
         """Safe getter for config that ensures it's initialized."""
@@ -616,10 +686,12 @@ class SettingsClass:
         return self._get_config().getboolean("qtofficial", "unattended", fallback=True)
 
     def qt_installer_cleanup(self) -> None:
-        """Control whether to use unattended installation flags."""
-        import shutil
-
-        shutil.rmtree(self.qt_installer_temp_path)
+        """Clean tmp folder."""
+        for item in Path(self.qt_installer_temp_path).iterdir():
+            if item.is_dir():
+                shutil.rmtree(item)
+            else:
+                item.unlink()
 
 
 Settings = SettingsClass()
@@ -647,7 +719,7 @@ def safely_run_save_output(cmd: List[str], timeout: int) -> Any:
         raise
 
 
-def extract_auth(args: List[str]) -> Tuple[str | None, str | None, List[str] | None]:
+def extract_auth(args: List[str]) -> Tuple[Union[str, None], Union[str, None], Union[List[str], None]]:
     username = None
     password = None
     i = 0
diff --git a/aqt/installer.py b/aqt/installer.py
index 2d54cfa..abb3553 100644
--- a/aqt/installer.py
+++ b/aqt/installer.py
@@ -368,6 +368,62 @@ class Cli:
         else:
             qt_version = args.qt_version
             Cli._validate_version_str(qt_version)
+
+        if hasattr(args, "use_official_installer") and args.use_official_installer is not None:
+
+            if len(args.use_official_installer) not in [0, 2]:
+                raise CliInputError(
+                    "When providing arguments to --use-official-installer, exactly 2 arguments are required: "
+                    "--use-official-installer email password"
+                )
+
+            self.logger.info("Using official Qt installer")
+
+            commercial_args = InstallArgParser()
+
+            # Core parameters required by install-qt-official
+            commercial_args.target = args.target
+            commercial_args.arch = self._set_arch(
+                args.arch, args.host, args.target, getattr(args, "qt_version", getattr(args, "qt_version_spec", ""))
+            )
+
+            commercial_args.version = qt_version
+
+            email = None
+            password = None
+            if len(args.use_official_installer) == 2:
+                email, password = args.use_official_installer
+                self.logger.info("Using credentials provided with --use-official-installer")
+
+            # Optional parameters
+            commercial_args.email = email or getattr(args, "email", None)
+            commercial_args.pw = password or getattr(args, "pw", None)
+            commercial_args.outputdir = args.outputdir
+            commercial_args.modules = args.modules
+            commercial_args.base = getattr(args, "base", None)
+            commercial_args.dry_run = getattr(args, "dry_run", False)
+            commercial_args.override = None
+
+            ignored_options = []
+            if getattr(args, "noarchives", False):
+                ignored_options.append("--noarchives")
+            if getattr(args, "autodesktop", False):
+                ignored_options.append("--autodesktop")
+            if getattr(args, "archives", None):
+                ignored_options.append("--archives")
+            if getattr(args, "timeout", False):
+                ignored_options.append("--timeout")
+            if getattr(args, "keep", False):
+                ignored_options.append("--keep")
+            if getattr(args, "archive_dest", False):
+                ignored_options.append("--archive_dest")
+
+            if ignored_options:
+                self.logger.warning("Options ignored because you requested the official installer:")
+                self.logger.warning(", ".join(ignored_options))
+
+            return self.run_install_qt_commercial(commercial_args, print_version=False)
+
         archives = args.archives
         if args.noarchives:
             if modules is None:
@@ -683,9 +739,10 @@ class Cli:
         )
         show_list(meta)
 
-    def run_install_qt_commercial(self, args: InstallArgParser) -> None:
+    def run_install_qt_commercial(self, args: InstallArgParser, print_version: Optional[bool] = True) -> None:
         """Execute commercial Qt installation"""
-        self.show_aqt_version()
+        if print_version:
+            self.show_aqt_version()
 
         try:
             if args.override:
@@ -700,6 +757,7 @@ class Cli:
                     no_unattended=not Settings.qt_installer_unattended,
                     username=username or args.email,
                     password=password or args.pw,
+                    dry_run=args.dry_run,
                 )
             else:
                 if not all([args.target, args.arch, args.version]):
@@ -716,12 +774,13 @@ class Cli:
                     base_url=args.base if args.base is not None else Settings.baseurl,
                     no_unattended=not Settings.qt_installer_unattended,
                     modules=args.modules,
+                    dry_run=args.dry_run,
                 )
 
             commercial_installer.install()
             Settings.qt_installer_cleanup()
         except Exception as e:
-            self.logger.error(f"Error installing official installer {str(e)}")
+            self.logger.error(f"Error installing official installer: {str(e)}")
         finally:
             self.logger.info("Done")
 
@@ -793,6 +852,16 @@ class Cli:
             "required. When enabled, this option installs the required desktop version automatically. "
             "It has no effect when the desktop installation is not required.",
         )
+        install_qt_parser.add_argument(
+            "--use-official-installer",
+            nargs="*",
+            default=None,
+            metavar=("EMAIL", "PASSWORD"),
+            help="Use the official Qt installer for installation instead of the aqt downloader. "
+            "Can be used without arguments or with email and password: --use-official-installer email password. "
+            "This redirects to install-qt-official. "
+            "Arguments not compatible with the official installer will be ignored.",
+        )
 
     def _set_install_tool_parser(self, install_tool_parser):
         install_tool_parser.set_defaults(func=self.run_install_tool)
@@ -889,14 +958,12 @@ class Cli:
         self.show_aqt_version()
 
         # Create temporary directory for installer
-        import shutil
-        from pathlib import Path
-
         temp_dir = Settings.qt_installer_temp_path
         temp_path = Path(temp_dir)
-        if temp_path.exists():
-            shutil.rmtree(temp_dir)
-        temp_path.mkdir(parents=True, exist_ok=True)
+        if not temp_path.exists():
+            temp_path.mkdir(parents=True, exist_ok=True)
+        else:
+            Settings.qt_installer_cleanup()
 
         # Get installer based on OS
         installer_filename = get_qt_installer_name()
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index 4a96be5..a36ca26 100644
--- a/azure-pipelines.yml
+++ b/azure-pipelines.yml
@@ -7,13 +7,13 @@ jobs:
   pool:
     vmImage: 'ubuntu-22.04'
   steps:
-    - task: UsePythonVersion@0
-      inputs:
-        versionSpec: '3.13'
-        addToPath: true
-    - bash: python ci/generate_azure_pipelines_matrices.py
-      name: mtrx
-      displayName: Generate test matrices and set variables in Azure Pipelines
+  - task: UsePythonVersion@0
+    inputs:
+      versionSpec: '3.12'
+      addToPath: true
+  - bash: python ci/generate_azure_pipelines_matrices.py
+    name: mtrx
+    displayName: Generate test matrices and set variables in Azure Pipelines
 
 - job: Mac
   dependsOn: MatricesGenerator
diff --git a/ci/generate_azure_pipelines_matrices.py b/ci/generate_azure_pipelines_matrices.py
index 36b1ee0..2d6e467 100644
--- a/ci/generate_azure_pipelines_matrices.py
+++ b/ci/generate_azure_pipelines_matrices.py
@@ -138,7 +138,7 @@ class PlatformBuildJobs:
         self.build_jobs = build_jobs
 
 
-python_versions = ["3.13"]
+python_versions = ["3.12"]
 
 qt_versions = ["6.8.1"]
 
diff --git a/docs/authors.rst b/docs/authors.rst
index 9cd33a3..e8c116b 100644
--- a/docs/authors.rst
+++ b/docs/authors.rst
@@ -16,6 +16,7 @@ All contributors, listed alphabetically, are:
 
 * Adrian Eddy (fix the case of Android 6.7.0)
 * Alberto Mardegan(ignore_hash option)
+* Alexandre @Kidev Poumaroux (official installer option, WASM fixes for Qt6.7.x)
 * Andrew Wason (support arm64)
 * Andrei Yankovich (tools ifw installation)
 * Aurélien Gâteau (patching to qmake)
@@ -51,4 +52,4 @@ All contributors, listed alphabetically, are:
 * @ypnos (Documents)
 
 and many other participants and contributors.
-If you find a missing name to record, please feel free to tell me.
\ No newline at end of file
+If you find a missing name to record, please feel free to tell me.
diff --git a/docs/official.rst b/docs/official.rst
index db17740..1d637d6 100644
--- a/docs/official.rst
+++ b/docs/official.rst
@@ -44,50 +44,6 @@ Options
   * If none are inputed, the package ``qtX.Y.Z-essentials`` is downloaded (default Qt install, includes ``qtcreator``...)
   * If ``all`` is inputed, the package ``qtX.Y.Z-full`` is downloaded (includes everything)
 
-.. dropdown:: Click to see all the special packages (for Linux Qt 6.8.1)
-
-   .. code-block:: text
-
-      Name: qt6.8.1-essentials
-      Display name: Qt 6.8.1 Linux x86_64 Essential Components
-      Description: Qt 6.8.1 Linux x86_64 Essential Libraries, Headers, and Tools
-      Version: 6.8.1
-      Components: qt.qt6.681.linux_gcc_64
-       
-      Name: qt6.8.1-essentials-dev
-      Display name: Qt 6.8.1 Linux x86_64 Essential Components (dev)
-      Description: Qt 6.8.1 Linux x86_64 Essential Libraries, Headers, and Tools (dev)
-      Version: 6.8.1
-      Required aliases: qt6.8.1-essentials
-       
-      Name: qt6.8.1-full
-      Display name: Qt 6.8.1 Linux x86_64 All Components with Sources
-      Description: Qt 6.8.1 Linux x86_64 All Libraries, Headers, Tools, and Sources
-      Version: 6.8.1
-      Components: qt.qt6.681.src,extensions.qtwebengine.681.src
-      Required aliases: qt6.8.1-essentials,qt6.8.1-addons,qt6.8.1-extensions
-      Optional components: extensions.qtinsighttracker.681.src
-       
-      Name: qt6.8.1-full-dbg
-      Display name: Qt 6.8.1 Linux x86_64 All Components with Sources and Debug Information Files
-      Description: Qt 6.8.1 Linux x86_64 All Libraries, Headers, Tools, Sources, and Debug Information Files (dev)
-      Version: 6.8.1
-      Components: qt.qt6.681.debug_info.linux_gcc_64,qt.qt6.681.debug_info,extensions.qtwebengine.681.debug_information
-      Required aliases: qt6.8.1-full-dev
-       
-      Name: qt6.8.1-full-dev
-      Display name: Qt 6.8.1 Linux x86_64 All Components with Sources (dev)
-      Description: Qt 6.8.1 Linux x86_64 All Libraries, Headers, Tools, and Sources (dev)
-      Version: 6.8.1
-      Required aliases: qt6.8.1-full
- 
-      Name: qt6.8.1-sdk
-      Display name: Qt 6.8.1 Linux x86_64 SDK
-      Description: Qt 6.8.1 Linux x86_64 SDK Tools (Qt Creator, Ninja, and CMake)
-      Version: 6.8.1
-      Components: qt.tools.qtcreator_gui,qt.tools.cmake,qt.tools.ninja
-      Required aliases: qt6.8.1-full-dev,qt6.8.1-full-dbg
-
 - ``--outputdir <path>`` - Installation directory (default: current directory)
 - ``--override <args...>`` - Pass all remaining arguments directly to the Qt installer CLI
 
@@ -106,7 +62,7 @@ Options
 - ``search_terms`` - Terms to search for in package names (grabs all that is not other options)
 
 Override Mode
-------------
+----------------------
 ``install-qt-official`` supports an override mode that passes all arguments after ``--override`` directly to the Qt installer CLI, and will ignore all the other params except ``--email`` and ``--pw`` if given prior to it
 
 .. code-block:: bash
@@ -122,7 +78,7 @@ When using override mode:
 * `More info here <https://doc.qt.io/qt-6/get-and-install-qt-cli.html>`_
 
 Examples
---------
+--------------
 .. code-block:: bash
 
    # Standard installation
@@ -138,7 +94,7 @@ Examples
    aqt install-qt-official --override install qt.qt6.680.gcc_64 --email user@example.com --pw pass
 
 Advanced configs
---------------
+--------------------------
 The file located in ``./aqt/settings.ini`` can be edited in the ``[qtofficial]`` part to fine tune the official installer (`more details here <https://doc.qt.io/qt-6/get-and-install-qt-cli.html#message-identifiers-for-auto-answer>`_):
 
 .. code-block:: ini
diff --git a/pyproject.toml b/pyproject.toml
index 561a19c..c519ecb 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -116,7 +116,7 @@ max-line-length = 125
 
 [tool.black]
 line-length = 125
-target-version = ['py313']
+target-version = ['py312']
 
 [tool.isort]
 line_length = 125
@@ -194,14 +194,14 @@ commands =
     python -m pytest -vv
 
 [testenv:check]
-basepython = python3.13
+basepython = python3.12
 extras = check
 commands =
     check-manifest {toxinidir}
     flake8 aqt tests
 
 [testenv:mypy]
-basepython = python3.13
+basepython = python3.12
 extras = check
 commands = mypy aqt
 deps =
@@ -210,14 +210,14 @@ deps =
     types-psutil
 
 [testenv:docs]
-basepython = python3.13
+basepython = python3.12
 extras = docs
 commands =
     sphinx-build {posargs:-E} -W -b html docs build/docs
     sphinx-build -W -b linkcheck docs build/docs
 
 [testenv:mprof]
-basepython = python3.13
+basepython = python3.12
 extras = debug
 commands =
     mprof run --multiprocess python -m aqt install-qt -O /tmp -d /tmp linux desktop 6.8.1
@@ -227,7 +227,7 @@ deps =
     matplotlib
 
 [testenv:fil]
-basepython = python3.13
+basepython = python3.12
 extras = debug
 commands =
     fil-profile run -m aqt install-qt -O /tmp -d /tmp linux desktop 6.8.1


Test patch:
diff --git a/.github/workflows/test-install-qt.yml b/.github/workflows/test-install-qt.yml
index 204c4b8..82cb079 100644
--- a/.github/workflows/test-install-qt.yml
+++ b/.github/workflows/test-install-qt.yml
@@ -15,24 +15,24 @@ jobs:
     strategy:
       matrix:
         os: [windows-latest, ubuntu-latest]
-        py: ["3.13"]
+        py: ["3.12"]
         qtver: [6.5.3, 6.6.3]
         artifact: [standard]
         include:
           - os: windows-latest
-            py: "3.13"
+            py: "3.12"
             qtver: 6.6.3
             artifact: binary
           - os: windows-latest
-            py: "3.13"
+            py: "3.12"
             qtver: 6.7.3
             artifact: standard
           - os: ubuntu-latest
-            py: "3.13"
+            py: "3.12"
             qtver: 6.8.0
             artifact: standard
           - os: ubuntu-latest
-            py: "3.13"
+            py: "3.12"
             # 6.8.1 introduces Operations elements in Updates.xml
             qtver: 6.8.1
             artifact: standard
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 10266d8..1a968e6 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -13,33 +13,10 @@ from aqt.metadata import MetadataFactory, SimpleSpec, Version
 
 
 def expected_help(actual, prefix=None):
-    expected = (
-        "usage: aqt [-h] [-c CONFIG]\n"
-        "           {install-qt,install-tool,install-qt-official,list-qt-official,install-doc,install-example,"
-        "install-src,list-qt,list-tool,list-doc,list-example,list-src,help,version}\n"
-        "           ...\n"
-        "\n"
-        "Another unofficial Qt Installer.\n"
-        "aqt helps you install Qt SDK, tools, examples and others\n"
-        "\n"
-        "option",
-        "  -h, --help            show this help message and exit\n"
-        "  -c CONFIG, --config CONFIG\n"
-        "                        Configuration ini file.\n"
-        "\n"
-        "subcommands:\n"
-        "  aqt accepts several subcommands:\n"
-        "  install-* subcommands are commands that install components\n"
-        "  list-* subcommands are commands that show available components\n"
-        "\n"
-        "  {install-qt,install-tool,install-qt-official,list-qt-official,install-doc,install-example,install-src,"
-        "list-qt,list-tool,list-doc,list-example,list-src,help,version}\n"
-        "                        Please refer to each help message by using '--help' "
-        "with each subcommand\n",
-    )
+    expected = "usage: aqt [-h] [-c CONFIG]"
     if prefix is not None:
-        return actual.startswith(prefix + expected[0]) and actual.endswith(expected[1])
-    return actual.startswith(expected[0]) and actual.endswith(expected[1])
+        return actual.startswith(prefix + expected)
+    return actual.startswith(expected)
 
 
 def test_cli_help(capsys):
diff --git a/tests/test_commercial.py b/tests/test_commercial.py
index 2948589..f23fd6c 100644
--- a/tests/test_commercial.py
+++ b/tests/test_commercial.py
@@ -3,17 +3,15 @@ import shutil
 import sys
 from pathlib import Path
 from typing import Dict, List, Optional
-from urllib.parse import urlparse
 
 import pytest
 import requests
 
 from aqt.commercial import CommercialInstaller, QtPackageInfo, QtPackageManager
 from aqt.exceptions import DiskAccessNotPermitted
-from aqt.helper import Settings, download_installer, get_qt_account_path
+from aqt.helper import Settings, download_installer, get_os_name, get_qt_account_path, get_qt_installer_name
 from aqt.installer import Cli
 from aqt.metadata import Version
-from tests.test_helper import mocked_requests_get
 
 
 class CompletedProcess:
@@ -51,15 +49,6 @@ class MockResponse:
         yield self.content
 
 
-@pytest.fixture
-def mock_settings(monkeypatch):
-    """Setup test settings"""
-    # Instead of trying to set properties directly, we should mock the property getter
-    monkeypatch.setattr(Settings, "qt_installer_timeout", property(lambda self: 60))
-    monkeypatch.setattr(Settings, "qt_installer_cache_path", property(lambda self: str(Path.home() / ".qt" / "cache")))
-    monkeypatch.setattr(Settings, "qt_installer_temp_path", property(lambda self: str(Path.home() / ".qt" / "temp")))
-
-
 @pytest.fixture
 def commercial_installer():
     return CommercialInstaller(
@@ -206,23 +195,34 @@ def test_build_command(
     assert cmd == expected_cmd
 
 
-def test_commercial_installer_download_sha256(tmp_path, monkeypatch, commercial_installer):
-    """Test downloading of commercial installer"""
+@pytest.mark.enable_socket
+@pytest.mark.parametrize(
+    "os, osarch, expected_suffix",
+    [
+        pytest.param("windows", "windows", ".exe"),
+        pytest.param("windows", "windows-x64", ".exe"),
+        pytest.param("windows", "windows-arm64", ".exe"),
+        pytest.param("linux", "linux", ".run"),
+        pytest.param("linux", "linux-x64", ".run"),
+        pytest.param("linux", "linux-arm64", ".run"),
+        pytest.param("mac", "mac-x64", ".dmg"),
+    ],
+)
+def test_commercial_installer_names(monkeypatch, os, osarch, expected_suffix):
+    """Test installer names finder"""
 
-    def mock_getUrl(url, *args, **kwargs):
-        hash_filename = str(urlparse(url).path.split("/")[-1])
-        assert hash_filename.endswith(".sha256")
-        filename = hash_filename[: -len(".sha256")]
-        return f"07b3ef4606b712923a14816b1cfe9649687e617d030fc50f948920d784c0b1cd {filename}"
+    monkeypatch.setattr("aqt.helper.get_os_arch", lambda: osarch)
 
-    monkeypatch.setattr("aqt.helper.getUrl", mock_getUrl)
-    monkeypatch.setattr(requests.Session, "get", mocked_requests_get)
+    installer_name = get_qt_installer_name()
 
-    target_path = tmp_path / "qt-installer"
+    assert installer_name.endswith(expected_suffix)
 
-    timeout = (Settings.connection_timeout, Settings.response_timeout)
-    download_installer(commercial_installer.base_url, commercial_installer._installer_filename, target_path, timeout)
-    assert target_path.exists()
+    if os == get_os_name() and osarch in ["windows", "linux", "mac-x64"]:
+        target_path = Settings.qt_installer_temp_path / Path(installer_name)
+        base_url = Settings.baseurl
+        timeout = (Settings.connection_timeout, Settings.response_timeout)
+        download_installer(base_url, installer_name, target_path, timeout)
+        assert True
 
 
 @pytest.mark.parametrize(
@@ -275,6 +275,17 @@ def test_get_install_command(monkeypatch, modules: Optional[List[str]], expected
             "stopProcessesForUpdates=Cancel,installationErrorWithCancel=Cancel,installationErrorWithIgnore=Ignore,"
             "AssociateCommonFiletypes=Yes,telemetry-question=No install qt.{}.{}.{}",
         ),
+        (
+            "install-qt linux desktop 6.8.1 {} --outputdir ./install-qt-flag --use-official-installer {} {}",
+            {"windows": "win64_msvc2022_64", "linux": "linux_gcc_64", "mac": "clang_64"},
+            ["./install-qt-official", "qt6", "681"],
+            "{} --email ******** --pw ******** --root {} "
+            "--accept-licenses --accept-obligations "
+            "--confirm-command "
+            "--auto-answer OperationDoesNotExistError=Ignore,OverwriteTargetDirectory=Yes,"
+            "stopProcessesForUpdates=Cancel,installationErrorWithCancel=Cancel,installationErrorWithIgnore=Ignore,"
+            "AssociateCommonFiletypes=Yes,telemetry-question=No install qt.{}.{}.{}",
+        ),
     ],
 )
 def test_install_qt_commercial(
@@ -301,7 +312,10 @@ def test_install_qt_commercial(
     password = TEST_PASSWORD
 
     formatted_cmd = cmd.format(arch, email, password)
-    formatted_expected = expected_command.format(current_platform, abs_out, *details[1:], arch)
+    if expected_command.startswith("install-qt-official"):
+        formatted_expected = expected_command.format(current_platform, abs_out, *details[1:], arch)
+    else:
+        formatted_expected = expected_command.format(get_qt_installer_name(), abs_out, *details[1:], arch)
 
     cli = Cli()
     cli._setup_settings()
@@ -330,7 +344,7 @@ def test_install_qt_commercial(
     else:
         cli.run(new_cmd.split())
 
-    def modify_qt_config(content):
+    def modify_qt_config(content, endwith):
         """
         Takes content of INI file as string and returns modified content
         """
@@ -339,12 +353,12 @@ def test_install_qt_commercial(
         modified = []
 
         for line in lines:
-            # Check if we're entering qtofficial section
             if line.strip() == "[qtofficial]":
                 in_qt_commercial = True
 
-            # If in qtofficial section, look for the target line
-            if in_qt_commercial and "overwrite_target_directory : No" in line:
+            if endwith is not None and endwith in ["Yes", "No"] and "overwrite_target_directory : " in line:
+                line = f"overwrite_target_directory : {endwith}"
+            elif in_qt_commercial and "overwrite_target_directory : No" in line:
                 line = "overwrite_target_directory : Yes"
             elif in_qt_commercial and "overwrite_target_directory : Yes" in line:
                 line = "overwrite_target_directory : No"
@@ -359,7 +373,10 @@ def test_install_qt_commercial(
     with open(config_path, "r") as f:
         content = f.read()
 
-    modified_content = modify_qt_config(content)
+    if expected_command.startswith("install-qt-official"):
+        modified_content = modify_qt_config(content, None)
+    else:
+        modified_content = modify_qt_config(content, "No")
 
     with open(config_path, "w") as f:
         f.write(modified_content)
diff --git a/tests/test_install.py b/tests/test_install.py
index 569da56..0f65a32 100644
--- a/tests/test_install.py
+++ b/tests/test_install.py
@@ -1694,7 +1694,8 @@ def test_install_qt6_wasm_autodesktop(monkeypatch, capsys, version, str_version,
         r"(?:INFO    : Found extension .*?\n)*"
         r"(?:INFO    : Downloading (?:qt[^\n]*|icu[^\n]*)\n"
         r"Finished installation of .*?\.7z in \d+\.\d+\n)*"
-        r"(?:INFO    : Patching (?:/tmp/[^/]+|[A-Za-z]:[\\/].*?)/6\.8\.0/wasm_singlethread/bin/(?:qmake(?:6)?|qtpaths(?:6)?|target_qt\.conf)\n)*"
+        r"(?:INFO    : Patching (?:/tmp/[^/]+|[A-Za-z]:[\\/].*?)/6\.8\.0/wasm_singlethread/bin/"
+        r"(?:qmake(?:6)?|qtpaths(?:6)?|target_qt\.conf)\n)*"
         r"INFO    : \n"
         r"INFO    : Autodesktop will now install linux desktop 6\.8\.0 linux_gcc_64 as required by Qt6-WASM\n"
         r"INFO    : aqtinstall\(aqt\) v.*? on Python 3.*?\n"