Repository: miurahr/aqtinstall
Pull request #883 (2025-01-29 08:55:43)

Commercial fixes, CI tests, more tests, coverage
- Fixes issues with clashing auth method in commercial calls
- Add a file dedicated to commercial testing
- Add CI tests
- Improve coverage

WIP

Note: I think my auto-format is not well configured for .sh files, I'll fix to not overwrite lines I did not touch for nothing

gitguardian[bot]: #### ⚠️ GitGuardian has uncovered 2 secrets following the scan of your pull request.

Please consider investigating the findings and remediating the incidents. Failure to do so may lead to compromising the associated services or software components.

Since your pull request originates from a forked repository, GitGuardian is not able to associate the secrets uncovered with secret incidents on your GitGuardian dashboard.
Skipping this check run and merging your pull request will create secret incidents on your GitGuardian dashboard.

<details>
<summary>🔎 Detected hardcoded secrets in your pull request</summary>
<br>

| GitGuardian id | GitGuardian status | Secret                         | Commit           | Filename        |                      |
| -------------- | ------------------ | ------------------------------ | ---------------- | --------------- | -------------------- |
| [-](https://dashboard.gitguardian.com/workspace/213462/incidents/secrets) | - | Generic Password | c3f48a816126fe760fcf7a71c0202ac4729ed5c3 | tests/test_commercial.py | [View secret](https://github.com/miurahr/aqtinstall/commit/c3f48a816126fe760fcf7a71c0202ac4729ed5c3#diff-abd9e433cb5429314e8f2c9bad99c188e96753529e12afc30a4052bc35cff24aR261) |
| [-](https://dashboard.gitguardian.com/workspace/213462/incidents/secrets) | - | Generic Password | 3975f6d79c2353a02df731b48e18652717d51111 | tests/test_commercial.py | [View secret](https://github.com/miurahr/aqtinstall/commit/3975f6d79c2353a02df731b48e18652717d51111#diff-abd9e433cb5429314e8f2c9bad99c188e96753529e12afc30a4052bc35cff24aR261) |



</details>

<details>
<summary>🛠 Guidelines to remediate hardcoded secrets</summary>
<br>

1. Understand the implications of revoking this secret by investigating where it is used in your code.
2. Replace and store your secrets safely. [Learn here](https://blog.gitguardian.com/secrets-api-management?utm_source=product&amp;utm_medium=GitHub_checks&amp;utm_campaign=check_run_comment) the best practices.
3. Revoke and [rotate these secrets](https://docs.gitguardian.com/secrets-detection/secrets-detection-engine/detectors/generics/generic_password#revoke-the-secret?utm_source=product&amp;utm_medium=GitHub_checks&amp;utm_campaign=check_run_comment).
4. If possible, [rewrite git history](https://blog.gitguardian.com/rewriting-git-history-cheatsheet?utm_source=product&amp;utm_medium=GitHub_checks&amp;utm_campaign=check_run_comment). Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.

To avoid such incidents in the future consider

-   following these [best practices](https://blog.gitguardian.com/secrets-api-management/?utm_source=product&amp;utm_medium=GitHub_checks&amp;utm_campaign=check_run_comment) for managing and storing secrets including API keys and other credentials
-   install [secret detection on pre-commit](https://docs.gitguardian.com/ggshield-docs/integrations/git-hooks/pre-commit?utm_source=product&amp;utm_medium=GitHub_checks&amp;utm_campaign=check_run_comment) to catch secret before it leaves your machine and ease remediation.


</details>

---

<sup>🦉 [GitGuardian](https://dashboard.gitguardian.com/auth/login/?utm_medium=checkruns&amp;utm_source=github&amp;utm_campaign=cr1) detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.<br/></sup>
Kidev: Looks like my trick did not work, sad
```
password = "I_swear_this_is_not_a_Secr3t"
```
miurahr: >@miurahr You'll need to [create a Qt account](https://login.qt.io/register) (you only need a throwaway email and a password for it to work), and setup Action secrets `AQT_TEST_EMAIL` `AQT_TEST_PASSWORD` . The rest should work then (did it on my PR repo). This prevents the storage and sharing of plain credentials, even if phony.

DONE.


Patch:
diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml
index 9ff3bc7..7fbb2bf 100644
--- a/.github/workflows/check.yml
+++ b/.github/workflows/check.yml
@@ -3,18 +3,21 @@ name: Check tox tests
 on:
   push:
     branches:
-      - master
-      - releases/*
+    - master
+    - releases/*
   pull_request:
     types:
-      - opened
-      - synchronize
-      - reopened
+    - opened
+    - synchronize
+    - reopened
 
 jobs:
   check_document:
     name: Check packaging 📦
     runs-on: ubuntu-22.04
+    env:
+      AQT_TEST_EMAIL: ${{ secrets.AQT_TEST_EMAIL }}
+      AQT_TEST_PASSWORD: ${{ secrets.AQT_TEST_PASSWORD }}
     steps:
     - uses: actions/checkout@v4
       with:
diff --git a/.gitignore b/.gitignore
index 5df3509..14255c5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -20,3 +20,4 @@ qtaccount.ini
 .pytest_cache
 .run/
 .python-version
+.env
diff --git a/aqt/commercial.py b/aqt/commercial.py
index e144f79..792dca5 100644
--- a/aqt/commercial.py
+++ b/aqt/commercial.py
@@ -9,7 +9,15 @@ import requests
 from defusedxml import ElementTree
 
 from aqt.exceptions import DiskAccessNotPermitted
-from aqt.helper import Settings, get_os_name, get_qt_account_path, get_qt_installer_name, safely_run, safely_run_save_output
+from aqt.helper import (
+    Settings,
+    extract_auth,
+    get_os_name,
+    get_qt_account_path,
+    get_qt_installer_name,
+    safely_run,
+    safely_run_save_output,
+)
 from aqt.metadata import Version
 
 
@@ -103,19 +111,14 @@ class QtPackageManager:
         version_str = self._get_version_string()
         base_package = f"qt.qt{self.version.major}.{version_str}"
 
-        cmd = [
-            installer_path,
-            "--accept-licenses",
-            "--accept-obligations",
-            "--confirm-command",
-            "--default-answer",
-            "search",
-            base_package,
-        ]
+        cmd = [installer_path, "--accept-licenses", "--accept-obligations", "--confirm-command", "--default-answer"]
 
         if self.username and self.password:
             cmd.extend(["--email", self.username, "--pw", self.password])
 
+        cmd.append("search")
+        cmd.append(base_package)
+
         try:
             output = safely_run_save_output(cmd, Settings.qt_installer_timeout)
 
@@ -190,7 +193,7 @@ class CommercialInstaller:
         output_dir: Optional[str] = None,
         logger: Optional[Logger] = None,
         base_url: str = "https://download.qt.io",
-        override: Optional[list[str]] = None,
+        override: Optional[List[str]] = None,
         modules: Optional[List[str]] = None,
         no_unattended: bool = False,
     ):
@@ -198,14 +201,21 @@ class CommercialInstaller:
         self.target = target
         self.arch = arch or ""
         self.version = Version(version) if version else Version("0.0.0")
-        self.username = username
-        self.password = password
         self.output_dir = output_dir
         self.logger = logger or getLogger(__name__)
         self.base_url = base_url
         self.modules = modules
         self.no_unattended = no_unattended
 
+        # Extract credentials from override if present
+        if override:
+            extracted_username, extracted_password, self.override = extract_auth(override)
+            self.username = extracted_username or username
+            self.password = extracted_password or password
+        else:
+            self.username = username
+            self.password = password
+
         # Set OS-specific properties
         self.os_name = get_os_name()
         self._installer_filename = get_qt_installer_name()
@@ -247,15 +257,15 @@ class CommercialInstaller:
         if not no_unattended:
             cmd.extend(["--accept-licenses", "--accept-obligations", "--confirm-command"])
 
+        # Add authentication if provided
+        if username and password:
+            cmd.extend(["--email", username, "--pw", password])
+
         if override:
             # When using override, still include unattended flags unless disabled
             cmd.extend(override)
             return cmd
 
-        # Add authentication if provided
-        if username and password:
-            cmd.extend(["--email", username, "--pw", password])
-
         # Add output directory if specified
         if output_dir:
             cmd.extend(["--root", str(Path(output_dir).resolve())])
@@ -271,36 +281,34 @@ class CommercialInstaller:
         """Run the Qt installation process."""
         if (
             not self.qt_account.exists()
-            and not (self.username and self.password)
+            and (not self.username or not self.password)
             and not os.environ.get("QT_INSTALLER_JWT_TOKEN")
         ):
             raise RuntimeError(
                 "No Qt account credentials found. Provide username and password or ensure qtaccount.ini exists."
             )
 
-        # Check output directory if specified
-        if self.output_dir:
-            output_path = Path(self.output_dir) / str(self.version)
-            if output_path.exists():
-                if Settings.qt_installer_overwritetargetdirectory.lower() == "yes":
-                    self.logger.warning(f"Target directory {output_path} exists - removing as overwrite is enabled")
-                    try:
-                        import shutil
-
-                        shutil.rmtree(output_path)
-                    except (OSError, PermissionError) as e:
-                        raise DiskAccessNotPermitted(f"Failed to remove existing target directory {output_path}: {str(e)}")
-                else:
-                    msg = (
-                        f"Target directory {output_path} already exists. "
-                        "Set overwrite_target_directory='Yes' in settings.ini to overwrite, or select another directory."
-                    )
-                    raise DiskAccessNotPermitted(msg)
-
         # Setup cache directory
         cache_path = Path(Settings.qt_installer_cache_path)
         cache_path.mkdir(parents=True, exist_ok=True)
 
+        # Setup output directory and validate access
+        output_dir = Path(self.output_dir) if self.output_dir else Path(os.getcwd()) / "Qt"
+        version_dir = output_dir / str(self.version)
+        qt_base_dir = output_dir
+
+        if qt_base_dir.exists():
+            if Settings.qt_installer_overwritetargetdirectory.lower() == "yes":
+                self.logger.warning(f"Target directory {qt_base_dir} exists - removing as overwrite is enabled")
+                try:
+                    import shutil
+
+                    if version_dir.exists():
+                        shutil.rmtree(version_dir)
+                except (OSError, PermissionError) as e:
+                    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
@@ -316,7 +324,16 @@ class CommercialInstaller:
         try:
             cmd = []
             if self.override:
-                cmd = self.build_command(str(installer_path), override=self.override, no_unattended=self.no_unattended)
+                if not self.username or not self.password:
+                    self.username, self.password, self.override = extract_auth(self.override)
+
+                cmd = self.build_command(
+                    str(installer_path),
+                    override=self.override,
+                    no_unattended=self.no_unattended,
+                    username=self.username,
+                    password=self.password,
+                )
             else:
                 # Initialize package manager and gather packages
                 self.package_manager.gather_packages(str(installer_path))
@@ -325,20 +342,18 @@ class CommercialInstaller:
                     str(installer_path.absolute()),
                     username=self.username,
                     password=self.password,
-                    output_dir=self.output_dir,
+                    output_dir=str(qt_base_dir.absolute()),
                     no_unattended=self.no_unattended,
                 )
 
-                cmd = [
-                    *base_cmd,
-                    *self.package_manager.get_install_command(self.modules, temp_dir),
-                ]
+                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)
+
         except Exception as e:
-            self.logger.error(f"Installation failed with exit code {e.__str__()}")
+            self.logger.error(f"Installation failed: {str(e)}")
             raise
         finally:
             self.logger.info("Qt installation completed successfully")
diff --git a/aqt/helper.py b/aqt/helper.py
index 92976ac..28a368e 100644
--- a/aqt/helper.py
+++ b/aqt/helper.py
@@ -459,18 +459,18 @@ class SettingsClass:
         """Path for Qt installer cache."""
         config = self._get_config()
         # If no cache_path or blank, return default without modifying config
-        if not config.has_option("qtcommercial", "cache_path") or config.get("qtcommercial", "cache_path").strip() == "":
+        if not config.has_option("qtofficial", "cache_path") or config.get("qtofficial", "cache_path").strip() == "":
             return str(get_default_local_cache_path())
-        return config.get("qtcommercial", "cache_path")
+        return config.get("qtofficial", "cache_path")
 
     @property
     def qt_installer_temp_path(self) -> str:
         """Path for Qt installer cache."""
         config = self._get_config()
         # If no cache_path or blank, return default without modifying config
-        if not config.has_option("qtcommercial", "temp_path") or config.get("qtcommercial", "temp_path").strip() == "":
+        if not config.has_option("qtofficial", "temp_path") or config.get("qtofficial", "temp_path").strip() == "":
             return str(get_default_local_temp_path())
-        return config.get("qtcommercial", "temp_path")
+        return config.get("qtofficial", "temp_path")
 
     @property
     def archive_download_location(self):
@@ -572,47 +572,47 @@ class SettingsClass:
     @property
     def qt_installer_timeout(self) -> int:
         """Timeout for Qt commercial installer operations in seconds."""
-        return self._get_config().getint("qtcommercial", "installer_timeout", fallback=3600)
+        return self._get_config().getint("qtofficial", "installer_timeout", fallback=3600)
 
     @property
     def qt_installer_operationdoesnotexisterror(self) -> str:
         """Handle OperationDoesNotExistError in Qt installer."""
-        return self._get_config().get("qtcommercial", "operation_does_not_exist_error", fallback="Ignore")
+        return self._get_config().get("qtofficial", "operation_does_not_exist_error", fallback="Ignore")
 
     @property
     def qt_installer_overwritetargetdirectory(self) -> str:
         """Handle overwriting target directory in Qt installer."""
-        return self._get_config().get("qtcommercial", "overwrite_target_directory", fallback="No")
+        return self._get_config().get("qtofficial", "overwrite_target_directory", fallback="No")
 
     @property
     def qt_installer_stopprocessesforupdates(self) -> str:
         """Handle stopping processes for updates in Qt installer."""
-        return self._get_config().get("qtcommercial", "stop_processes_for_updates", fallback="Cancel")
+        return self._get_config().get("qtofficial", "stop_processes_for_updates", fallback="Cancel")
 
     @property
     def qt_installer_installationerrorwithcancel(self) -> str:
         """Handle installation errors with cancel option in Qt installer."""
-        return self._get_config().get("qtcommercial", "installation_error_with_cancel", fallback="Cancel")
+        return self._get_config().get("qtofficial", "installation_error_with_cancel", fallback="Cancel")
 
     @property
     def qt_installer_installationerrorwithignore(self) -> str:
         """Handle installation errors with ignore option in Qt installer."""
-        return self._get_config().get("qtcommercial", "installation_error_with_ignore", fallback="Ignore")
+        return self._get_config().get("qtofficial", "installation_error_with_ignore", fallback="Ignore")
 
     @property
     def qt_installer_associatecommonfiletypes(self) -> str:
         """Handle file type associations in Qt installer."""
-        return self._get_config().get("qtcommercial", "associate_common_filetypes", fallback="Yes")
+        return self._get_config().get("qtofficial", "associate_common_filetypes", fallback="Yes")
 
     @property
     def qt_installer_telemetry(self) -> str:
         """Handle telemetry settings in Qt installer."""
-        return self._get_config().get("qtcommercial", "telemetry", fallback="No")
+        return self._get_config().get("qtofficial", "telemetry", fallback="No")
 
     @property
     def qt_installer_unattended(self) -> bool:
         """Control whether to use unattended installation flags."""
-        return self._get_config().getboolean("qtcommercial", "unattended", fallback=True)
+        return self._get_config().getboolean("qtofficial", "unattended", fallback=True)
 
     def qt_installer_cleanup(self) -> None:
         """Control whether to use unattended installation flags."""
@@ -644,3 +644,26 @@ def safely_run_save_output(cmd: List[str], timeout: int) -> Any:
         return result
     except Exception:
         raise
+
+
+def extract_auth(args: List[str]) -> Tuple[str | None, str | None, List[str] | None]:
+    username = None
+    password = None
+    i = 0
+    while i < len(args):
+        if args[i] == "--email":
+            if i + 1 < len(args):
+                username = args[i + 1]
+                del args[i : i + 2]
+            else:
+                del args[i]
+            continue
+        elif args[i] == "--pw":
+            if i + 1 < len(args):
+                password = args[i + 1]
+                del args[i : i + 2]
+            else:
+                del args[i]
+            continue
+        i += 1
+    return username, password, args
diff --git a/aqt/installer.py b/aqt/installer.py
index ccd515a..88f858d 100644
--- a/aqt/installer.py
+++ b/aqt/installer.py
@@ -28,6 +28,7 @@ import multiprocessing
 import os
 import platform
 import posixpath
+import re
 import signal
 import subprocess
 import sys
@@ -59,6 +60,7 @@ from aqt.helper import (
     MyQueueListener,
     Settings,
     downloadBinaryFile,
+    extract_auth,
     get_hash,
     get_os_name,
     get_qt_installer_name,
@@ -123,6 +125,7 @@ class CommonInstallArgParser(BaseArgumentParser):
     internal: bool
     keep: bool
     archive_dest: Optional[str]
+    dry_run: bool
 
 
 class InstallArgParser(CommonInstallArgParser):
@@ -133,8 +136,8 @@ class InstallArgParser(CommonInstallArgParser):
     qt_version: str
     qt_version_spec: str
     version: Optional[str]
-    user: Optional[str]
-    password: Optional[str]
+    email: Optional[str]
+    pw: Optional[str]
     operation_does_not_exist_error: str
     overwrite_target_dir: str
     stop_processes_for_updates: str
@@ -337,6 +340,7 @@ class Cli:
         arch: str = self._set_arch(args.arch, os_name, target, qt_version_or_spec)
         keep: bool = args.keep or Settings.always_keep_archives
         archive_dest: Optional[str] = args.archive_dest
+        dry_run: bool = args.dry_run
         output_dir = args.outputdir
         if output_dir is None:
             base_dir = os.getcwd()
@@ -405,47 +409,50 @@ class Cli:
 
         with TemporaryDirectory() as temp_dir:
             _archive_dest = Cli.choose_archive_dest(archive_dest, keep, temp_dir)
-            run_installer(qt_archives.get_packages(), base_dir, sevenzip, keep, _archive_dest)
-
-        if not nopatch:
-            Updater.update(target_config, base_path, expect_desktop_archdir)
-
-        # If autodesktop is enabled and we need a desktop installation, do it first
-        if should_autoinstall and autodesk_arch is not None:
-            is_wasm = arch.startswith("wasm")
-            is_msvc = "msvc" in arch
-            is_win_desktop_msvc_arm64 = (
-                effective_os_name == "windows"
-                and target == "desktop"
-                and is_msvc
-                and arch.endswith(("arm64", "arm64_cross_compiled"))
-            )
-            if is_win_desktop_msvc_arm64:
-                qt_type = "MSVC Arm64"
-            elif is_wasm:
-                qt_type = "Qt6-WASM"
-            else:
-                qt_type = target
+            run_installer(qt_archives.get_packages(), base_dir, sevenzip, keep, _archive_dest, dry_run=dry_run)
 
-            # Create new args for desktop installation
-            self.logger.info("")
-            self.logger.info(
-                f"Autodesktop will now install {effective_os_name} desktop "
-                f"{qt_version} {autodesk_arch} as required by {qt_type}"
-            )
+            if dry_run:
+                return
 
-            desktop_args = args
-            args.autodesktop = False
-            args.host = effective_os_name
-            args.target = "desktop"
-            args.arch = autodesk_arch
+            if not nopatch:
+                Updater.update(target_config, base_path, expect_desktop_archdir)
+
+            # If autodesktop is enabled and we need a desktop installation, do it first
+            if should_autoinstall and autodesk_arch is not None:
+                is_wasm = arch.startswith("wasm")
+                is_msvc = "msvc" in arch
+                is_win_desktop_msvc_arm64 = (
+                    effective_os_name == "windows"
+                    and target == "desktop"
+                    and is_msvc
+                    and arch.endswith(("arm64", "arm64_cross_compiled"))
+                )
+                if is_win_desktop_msvc_arm64:
+                    qt_type = "MSVC Arm64"
+                elif is_wasm:
+                    qt_type = "Qt6-WASM"
+                else:
+                    qt_type = target
 
-            # Run desktop installation first
-            self.run_install_qt(desktop_args)
+                # Create new args for desktop installation
+                self.logger.info("")
+                self.logger.info(
+                    f"Autodesktop will now install {effective_os_name} desktop "
+                    f"{qt_version} {autodesk_arch} as required by {qt_type}"
+                )
 
-        else:
-            self.logger.info("Finished installation")
-            self.logger.info("Time elapsed: {time:.8f} second".format(time=time.perf_counter() - start_time))
+                desktop_args = args
+                args.autodesktop = False
+                args.host = effective_os_name
+                args.target = "desktop"
+                args.arch = autodesk_arch
+
+                # Run desktop installation first
+                self.run_install_qt(desktop_args)
+
+            else:
+                self.logger.info("Finished installation")
+                self.logger.info("Time elapsed: {time:.8f} second".format(time=time.perf_counter() - start_time))
 
     def _run_src_doc_examples(self, flavor, args, cmd_name: Optional[str] = None):
         self.show_aqt_version()
@@ -498,7 +505,9 @@ class Cli:
         )
         with TemporaryDirectory() as temp_dir:
             _archive_dest = Cli.choose_archive_dest(archive_dest, keep, temp_dir)
-            run_installer(srcdocexamples_archives.get_packages(), base_dir, sevenzip, keep, _archive_dest)
+            run_installer(
+                srcdocexamples_archives.get_packages(), base_dir, sevenzip, keep, _archive_dest, dry_run=args.dry_run
+            )
         self.logger.info("Finished installation")
 
     def run_install_src(self, args):
@@ -585,7 +594,7 @@ class Cli:
             )
             with TemporaryDirectory() as temp_dir:
                 _archive_dest = Cli.choose_archive_dest(archive_dest, keep, temp_dir)
-                run_installer(tool_archives.get_packages(), base_dir, sevenzip, keep, _archive_dest)
+                run_installer(tool_archives.get_packages(), base_dir, sevenzip, keep, _archive_dest, dry_run=args.dry_run)
         self.logger.info("Finished installation")
         self.logger.info("Time elapsed: {time:.8f} second".format(time=time.perf_counter() - start_time))
 
@@ -676,42 +685,43 @@ class Cli:
         """Execute commercial Qt installation"""
         self.show_aqt_version()
 
-        if args.override:
-            commercial_installer = CommercialInstaller(
-                target="",  # Empty string as placeholder
-                arch="",
-                version=None,
-                logger=self.logger,
-                base_url=args.base if args.base is not None else Settings.baseurl,
-                override=args.override,
-                no_unattended=not Settings.qt_installer_unattended,
-            )
-        else:
-            if not all([args.target, args.arch, args.version]):
-                raise CliInputError("target, arch, and version are required")
-
-            commercial_installer = CommercialInstaller(
-                target=args.target,
-                arch=args.arch,
-                version=args.version,
-                username=args.user,
-                password=args.password,
-                output_dir=args.outputdir,
-                logger=self.logger,
-                base_url=args.base if args.base is not None else Settings.baseurl,
-                no_unattended=not Settings.qt_installer_unattended,
-                modules=args.modules,
-            )
-
         try:
+            if args.override:
+                username, password, override_args = extract_auth(args.override)
+                commercial_installer = CommercialInstaller(
+                    target="",  # Empty string as placeholder
+                    arch="",
+                    version=None,
+                    logger=self.logger,
+                    base_url=args.base if args.base is not None else Settings.baseurl,
+                    override=override_args,
+                    no_unattended=not Settings.qt_installer_unattended,
+                    username=username or args.email,
+                    password=password or args.pw,
+                )
+            else:
+                if not all([args.target, args.arch, args.version]):
+                    raise CliInputError("target, arch, and version are required")
+
+                commercial_installer = CommercialInstaller(
+                    target=args.target,
+                    arch=args.arch,
+                    version=args.version,
+                    username=args.email,
+                    password=args.pw,
+                    output_dir=args.outputdir,
+                    logger=self.logger,
+                    base_url=args.base if args.base is not None else Settings.baseurl,
+                    no_unattended=not Settings.qt_installer_unattended,
+                    modules=args.modules,
+                )
+
             commercial_installer.install()
             Settings.qt_installer_cleanup()
-        except DiskAccessNotPermitted:
-            # Let DiskAccessNotPermitted propagate up without additional logging
-            raise
         except Exception as e:
-            self.logger.error(f"Commercial installation failed: {str(e)}")
-            raise
+            self.logger.error(f"Error installing official installer {str(e)}")
+        finally:
+            self.logger.info("Done")
 
     def show_help(self, args=None):
         """Display help message"""
@@ -837,44 +847,46 @@ class Cli:
         install_qt_commercial_parser.add_argument("version", nargs="?", help="Qt version", action=ConditionalRequiredAction)
 
         install_qt_commercial_parser.add_argument(
-            "--user",
-            help="Qt account username",
+            "--email",
+            help="Qt account email",
         )
         install_qt_commercial_parser.add_argument(
-            "--password",
+            "--pw",
             help="Qt account password",
         )
         install_qt_commercial_parser.add_argument(
+            "-m",
             "--modules",
             nargs="*",
             help="Add modules",
         )
         self._set_common_options(install_qt_commercial_parser)
 
-    def _make_list_qt_commercial_parser(self, subparsers: argparse._SubParsersAction) -> None:
-        """Creates a subparser for listing Qt commercial packages"""
-        list_parser = subparsers.add_parser(
-            "list-qt-commercial",
-            formatter_class=argparse.RawDescriptionHelpFormatter,
-            epilog="Examples:\n"
-            "$ aqt list-qt-commercial                 # list all available packages\n"
-            "$ aqt list-qt-commercial gcc_64          # search for specific archs\n"
-            "$ aqt list-qt-commercial 6.8.1           # search for specific versions\n"
-            "$ aqt list-qt-commercial qtquick3d       # search for specific packages\n"
-            "$ aqt list-qt-commercial gcc_64 6.8.1    # search for multiple terms at once\n",
+    def _set_list_qt_commercial_parser(self, list_qt_commercial_parser: argparse.ArgumentParser) -> None:
+        """Configure parser for list-qt-official command with flexible argument handling."""
+        list_qt_commercial_parser.set_defaults(func=self.run_list_qt_commercial)
+
+        list_qt_commercial_parser.add_argument(
+            "--email",
+            help="Qt account email",
         )
-        list_parser.add_argument(
+        list_qt_commercial_parser.add_argument(
+            "--pw",
+            help="Qt account password",
+        )
+
+        # Capture all remaining arguments as search terms
+        list_qt_commercial_parser.add_argument(
             "search_terms",
-            nargs="*",
-            help="Optional search terms to pass to the installer search command. If not provided, lists all packages",
+            nargs="*",  # Zero or more arguments
+            help="Search terms (all non-option arguments are treated as search terms)",
         )
-        list_parser.set_defaults(func=self.run_list_qt_commercial)
 
     def run_list_qt_commercial(self, args) -> None:
-        """Execute Qt commercial package listing"""
+        """Execute Qt commercial package listing."""
         self.show_aqt_version()
 
-        # Create temporary directory to download installer
+        # Create temporary directory for installer
         import shutil
         from pathlib import Path
 
@@ -893,6 +905,7 @@ class Cli:
             self.logger.info(f"Downloading Qt installer to {installer_path}")
             base_url = Settings.baseurl
             url = f"{base_url}/official_releases/online_installers/{installer_filename}"
+
             import requests
 
             response = requests.get(url, stream=True, timeout=Settings.qt_installer_timeout)
@@ -905,33 +918,30 @@ class Cli:
             if get_os_name() != "windows":
                 os.chmod(installer_path, 0o500)
 
-            # Build search command
-            cmd = [
-                str(installer_path),
-                "--accept-licenses",
-                "--accept-obligations",
-                "--confirm-command",
-                "search",
-                "" if not args.search_terms else " ".join(args.search_terms),
-            ]
-
-            # Run search and display output
+            # Build command
+            cmd = [str(installer_path), "--accept-licenses", "--accept-obligations", "--confirm-command"]
+
+            if args.email and args.pw:
+                cmd.extend(["--email", args.email, "--pw", args.pw])
+
+            cmd.append("search")
+
+            # Add all search terms if present
+            if args.search_terms:
+                cmd.extend(args.search_terms)
+
+            # Run search
             output = safely_run_save_output(cmd, Settings.qt_installer_timeout)
 
-            # Process and print the output properly
             if output.stdout:
-                # Print the actual output with proper newlines
-                print(output.stdout)
-
-                # If there are any errors, print them as warnings
+                self.logger.info(output.stdout)
                 if output.stderr:
                     for line in output.stderr.splitlines():
                         self.logger.warning(line)
 
         except Exception as e:
-            self.logger.error(f"Failed to list Qt commercial packages: {e}")
+            self.logger.error(f"Failed to list Qt official packages: {e}")
         finally:
-            # Clean up
             Settings.qt_installer_cleanup()
 
     def _warn_on_deprecated_command(self, old_name: str, new_name: str) -> None:
@@ -987,18 +997,23 @@ class Cli:
         make_parser_it("install-qt", "Install Qt.", self._set_install_qt_parser, argparse.RawTextHelpFormatter)
         make_parser_it("install-tool", "Install tools.", self._set_install_tool_parser, None)
         make_parser_it(
-            "install-qt-commercial",
-            "Install Qt commercial.",
+            "install-qt-official",
+            "Install Qt with official installer.",
             self._set_install_qt_commercial_parser,
             argparse.RawTextHelpFormatter,
         )
+        make_parser_it(
+            "list-qt-official",
+            "Search packages using Qt official installer.",
+            self._set_list_qt_commercial_parser,
+            argparse.RawTextHelpFormatter,
+        )
         make_parser_sde("install-doc", "Install documentation.", self.run_install_doc, False)
         make_parser_sde("install-example", "Install examples.", self.run_install_example, False)
         make_parser_sde("install-src", "Install source.", self.run_install_src, True, is_add_modules=False)
 
         # Create list command parsers
         self._make_list_qt_parser(subparsers)
-        self._make_list_qt_commercial_parser(subparsers)
         self._make_list_tool_parser(subparsers)
         make_parser_list_sde("list-doc", "List documentation archives available (use with install-doc)", "doc")
         make_parser_list_sde("list-example", "List example archives available (use with install-example)", "examples")
@@ -1018,11 +1033,16 @@ class Cli:
             '$ aqt list-qt mac desktop --spec "5.9" --latest-version          # print latest Qt 5.9\n'
             "$ aqt list-qt mac desktop --modules 5.12.0 clang_64              # print modules for 5.12.0\n"
             "$ aqt list-qt mac desktop --spec 5.9 --modules latest clang_64   # print modules for latest 5.9\n"
-            "$ aqt list-qt mac desktop --arch 5.9.9                           # print architectures for 5.9.9/mac/desktop\n"
-            "$ aqt list-qt mac desktop --arch latest                          # print architectures for the latest Qt 5\n"
-            "$ aqt list-qt mac desktop --archives 5.9.0 clang_64              # list archives in base Qt installation\n"
-            "$ aqt list-qt mac desktop --archives 5.14.0 clang_64 debug_info  # list archives in debug_info module\n"
-            "$ aqt list-qt all_os wasm --arch 6.8.1                           # print architectures for Qt WASM 6.8.1\n",
+            "$ aqt list-qt mac desktop --arch 5.9.9                           # print architectures for "
+            "5.9.9/mac/desktop\n"
+            "$ aqt list-qt mac desktop --arch latest                          # print architectures for the "
+            "latest Qt 5\n"
+            "$ aqt list-qt mac desktop --archives 5.9.0 clang_64              # list archives in base Qt "
+            "installation\n"
+            "$ aqt list-qt mac desktop --archives 5.14.0 clang_64 debug_info  # list archives in debug_info "
+            "module\n"
+            "$ aqt list-qt all_os wasm --arch 6.8.1                           # print architectures for Qt WASM "
+            "6.8.1\n",
         )
         list_parser.add_argument(
             "host",
@@ -1182,6 +1202,11 @@ class Cli:
             default=None,
             help="Set the destination path for downloaded archives (temp directory by default).",
         )
+        subparser.add_argument(
+            "--dry-run",
+            action="store_true",
+            help="Print what would be downloaded and installed without actually doing it",
+        )
 
     def _set_module_options(self, subparser):
         subparser.add_argument("-m", "--modules", nargs="*", help="Specify extra modules to install")
@@ -1354,7 +1379,28 @@ def run_installer(
     sevenzip: Optional[str],
     keep: bool,
     archive_dest: Path,
+    dry_run: bool = False,
 ):
+
+    if dry_run:
+        logger = getLogger("aqt.installer")
+        logger.info("DRY RUN: Would download and install the following:")
+        for arc in archives:
+            line_parts = [f"  - {arc.name}: {arc.archive_path}"]
+
+            if hasattr(arc, "package_desc") and arc.package_desc:
+                size_match = re.search(r"Size: ([^,]+)", arc.package_desc)
+                if size_match:
+                    line_parts.append(f" ({size_match.group(1)})")
+
+            if arc.archive_install_path and arc.archive_install_path.strip():
+                line_parts.append(f" -> {arc.archive_install_path}")
+
+            logger.info("".join(line_parts))
+
+        logger.info(f"Total packages: {len(archives)}")
+        return
+
     queue = multiprocessing.Manager().Queue(-1)
     listener = MyQueueListener(queue)
     listener.start()
diff --git a/aqt/settings.ini b/aqt/settings.ini
index 7512433..5a176c3 100644
--- a/aqt/settings.ini
+++ b/aqt/settings.ini
@@ -19,11 +19,11 @@ max_retries_to_retrieve_hash : 5
 hash_algorithm : sha256
 INSECURE_NOT_FOR_PRODUCTION_ignore_hash : False
 
-[qtcommercial]
+[qtofficial]
 unattended : True
 installer_timeout : 1800
 operation_does_not_exist_error : Ignore
-overwrite_target_directory : Yes
+overwrite_target_directory : No
 stop_processes_for_updates : Ignore
 installation_error_with_cancel : Ignore
 installation_error_with_ignore : Ignore
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index 0bd7904..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.12'
-        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
@@ -22,9 +22,9 @@ jobs:
   strategy:
     matrix: $[ dependencies.MatricesGenerator.outputs['mtrx.mac'] ]
   steps:
-    - script: brew install p7zip
-      displayName: Install 7zip
-    - template: ci/steps.yml
+  - script: brew install p7zip
+    displayName: Install 7zip
+  - template: ci/steps.yml
 
 - job: Windows
   dependsOn: MatricesGenerator
@@ -37,7 +37,7 @@ jobs:
     startYear: $[format('{0:yyyy}', pipeline.startTime)]
     startMonth: $[format('{0:MM}', pipeline.startTime)]
   steps:
-    - template: ci/steps.yml
+  - template: ci/steps.yml
 
 - job: Linux
   dependsOn: MatricesGenerator
@@ -46,7 +46,7 @@ jobs:
   strategy:
     matrix: $[ dependencies.MatricesGenerator.outputs['mtrx.linux'] ]
   steps:
-    - template: ci/steps.yml
+  - template: ci/steps.yml
 
 #- job: Linux_ARM64
 #  dependsOn: MatricesGenerator
diff --git a/ci/generate_azure_pipelines_matrices.py b/ci/generate_azure_pipelines_matrices.py
index 5fdf560..d8c00c7 100644
--- a/ci/generate_azure_pipelines_matrices.py
+++ b/ci/generate_azure_pipelines_matrices.py
@@ -3,7 +3,6 @@ This sets variables for a matrix of QT versions to test downloading against with
 """
 import collections
 import json
-import secrets as random
 import re
 from itertools import product
 from typing import Dict, Optional
@@ -46,7 +45,8 @@ class BuildJob:
         is_autodesktop: bool = False,
         tool_options: Optional[Dict[str, str]] = None,
         check_output_cmd: Optional[str] = None,
-        emsdk_version: str = "sdk-fastcomp-1.38.27-64bit@3.1.29", # did not change for safety, created func self.emsdk_version()
+        emsdk_version: str = "sdk-fastcomp-1.38.27-64bit@3.1.29",
+        # did not change for safety, created func self.emsdk_version()
         autodesk_arch_folder: Optional[str] = None,
     ):
         self.command = command
@@ -167,13 +167,13 @@ for qt_version in qt_versions:
         BuildJob("install-qt", qt_version, "mac", "desktop", "clang_64", "macos")
     )
 mac_build_jobs.append(BuildJob(
-            "install-qt",
-            "6.2.0",
-            "mac",
-            "desktop",
-            "clang_64",
-            "macos",
-            module="qtcharts qtnetworkauth", ))
+    "install-qt",
+    "6.2.0",
+    "mac",
+    "desktop",
+    "clang_64",
+    "macos",
+    module="qtcharts qtnetworkauth", ))
 
 # Windows Desktop
 for qt_version in qt_versions:
@@ -211,7 +211,7 @@ windows_build_jobs.extend(
 # Extra modules test
 linux_build_jobs.extend(
     [
-       BuildJob(
+        BuildJob(
             # Archives stored as .7z
             "install-src", "6.1.0", "linux", "desktop", "gcc_64", "gcc_64", subarchives="qtlottie",
             # Fail the job if this path does not exist:
@@ -235,10 +235,12 @@ linux_build_jobs.extend(
             "install-example", "6.1.3", "linux", "desktop", "gcc_64", "gcc_64",
             subarchives="qtdoc", module="qtcharts",
             # Fail the job if these paths do not exist:
-            check_output_cmd="ls -lh ./Examples/Qt-6.1.3/charts/ ./Examples/Qt-6.1.3/demos/ ./Examples/Qt-6.1.3/tutorials/",
+            check_output_cmd="ls -lh ./Examples/Qt-6.1.3/charts/ ./Examples/Qt-6.1.3/demos/ "
+                             "./Examples/Qt-6.1.3/tutorials/",
         ),
         # test for list commands
-        BuildJob('list-qt', '6.1.0', 'linux', 'desktop', 'gcc_64', '', spec=">6.0,<6.1.1", list_options={'HAS_WASM': "False"}),
+        BuildJob('list-qt', '6.1.0', 'linux', 'desktop', 'gcc_64', '', spec=">6.0,<6.1.1",
+                 list_options={'HAS_WASM': "False"}),
         BuildJob('list-qt', '6.1.0', 'linux', 'android', 'android_armv7', '', spec=">6.0,<6.1.1", list_options={}),
     ]
 )
@@ -271,7 +273,8 @@ windows_build_jobs.append(
 # WASM post 6.7.x
 linux_build_jobs.append(
     BuildJob("install-qt", "6.7.3", "all_os", "wasm", "wasm_multithread", "wasm_multithread",
-             is_autodesktop=True, emsdk_version=f"sdk-{BuildJob.emsdk_version_for_qt("6.7.3")}-64bit", autodesk_arch_folder="gcc_64")
+             is_autodesktop=True, emsdk_version=f"sdk-{BuildJob.emsdk_version_for_qt("6.7.3")}-64bit",
+             autodesk_arch_folder="gcc_64")
 )
 for job_queue, host, desk_arch, target, qt_version in (
     (linux_build_jobs, "all_os", "linux_gcc_64", "wasm", qt_versions[0]),
@@ -281,7 +284,8 @@ for job_queue, host, desk_arch, target, qt_version in (
     for wasm_arch in ("wasm_singlethread", "wasm_multithread"):
         job_queue.append(
             BuildJob("install-qt", qt_version, host, target, wasm_arch, wasm_arch,
-                     is_autodesktop=True, emsdk_version=f"sdk-{BuildJob.emsdk_version_for_qt(qt_version)}-64bit", autodesk_arch_folder=desk_arch)
+                     is_autodesktop=True, emsdk_version=f"sdk-{BuildJob.emsdk_version_for_qt(qt_version)}-64bit",
+                     autodesk_arch_folder=desk_arch)
         )
 
 # mobile SDK
@@ -335,6 +339,20 @@ mac_build_jobs.append(
     BuildJob("install-tool", "", "mac", "desktop", "", "", tool_options=tool_options_mac)
 )
 
+# Commercial
+windows_build_jobs.append(
+    BuildJob("install-qt-official", qt_version="6.8.1", target="desktop", arch="win64_msvc2022_64",
+             archdir="win64_msvc2022_64", module="qtquick3d")
+)
+linux_build_jobs.append(
+    BuildJob("install-qt-official", qt_version="6.8.1", target="desktop", arch="linux_gcc_64",
+             archdir="linux_gcc_64", module="qtquick3d")
+)
+mac_build_jobs.append(
+    BuildJob("install-qt-official", qt_version="6.8.1", target="desktop", arch="clang_64",
+             archdir="clang_64", module="qtquick3d")
+)
+
 matrices = {}
 
 for platform_build_job in all_platform_build_jobs:
@@ -375,8 +393,8 @@ for platform_build_job in all_platform_build_jobs:
                 ("OUTPUT_DIR", build_job.output_dir if build_job.output_dir else ""),
                 ("QT_BINDIR", build_job.qt_bindir()),
                 ("WIN_QT_BINDIR", build_job.win_qt_bindir()),
-                ("EMSDK_VERSION", (build_job.emsdk_version+"@main").split('@')[0]),
-                ("EMSDK_TAG",  (build_job.emsdk_version+"@main").split('@')[1]),
+                ("EMSDK_VERSION", (build_job.emsdk_version + "@main").split('@')[0]),
+                ("EMSDK_TAG", (build_job.emsdk_version + "@main").split('@')[1]),
                 ("WIN_AUTODESK_QT_BINDIR", build_job.win_autodesk_qt_bindir()),
                 ("TOOL1_ARGS", build_job.tool_options.get("TOOL1_ARGS", "")),
                 ("LIST_TOOL1_CMD", build_job.tool_options.get("LIST_TOOL1_CMD", "")),
diff --git a/ci/steps.yml b/ci/steps.yml
index ca09a46..5ed24b3 100644
--- a/ci/steps.yml
+++ b/ci/steps.yml
@@ -1,496 +1,503 @@
+env:
+  AQT_TEST_EMAIL: ${{ secrets.AQT_TEST_EMAIL }}
+  AQT_TEST_PASSWORD: ${{ secrets.AQT_TEST_PASSWORD }}
 steps:
-  - task: UsePythonVersion@0
-    inputs:
-      versionSpec: $(PYTHON_VERSION)
-      architecture: 'x64'
-  - powershell: |
-      pip install -e .
-    displayName: install package
+- task: UsePythonVersion@0
+  inputs:
+    versionSpec: $(PYTHON_VERSION)
+    architecture: 'x64'
+- powershell: |
+    pip install -e .
+  displayName: install package
 
-  # Install linux dependencies
-  - bash: |
-      sudo apt-get update
-      sudo apt-get install -y libgl1-mesa-dev libxkbcommon-x11-0
-      if [[ "$(SUBCOMMAND)" == "install-tool" ]]; then
-        sudo apt-get install -y libxcb-{icccm4,image0,keysyms1,randr0,render-util0,shape0,sync1,xfixes0,xinerama0,xkb1}
-      fi
-    condition: and(eq(variables['TARGET'], 'desktop'), eq(variables['Agent.OS'], 'Linux'))
-    displayName: install test dependency for Linux
+# Install linux dependencies
+- bash: |
+    sudo apt-get update
+    sudo apt-get install -y libgl1-mesa-dev libxkbcommon-x11-0
+    if [[ "$(SUBCOMMAND)" == "install-tool" || "$(SUBCOMMAND)" == "install-qt-official" ]]; then
+      sudo apt-get install -y libxcb-{icccm4,image0,keysyms1,randr0,render-util0,shape0,sync1,xfixes0,xinerama0,xkb1}
+    fi
+  condition: and(eq(variables['TARGET'], 'desktop'), eq(variables['Agent.OS'], 'Linux'))
+  displayName: install test dependency for Linux
 
-  # Run Aqt
-  ##----------------------------------------------------
-  ## we insert sleep in random 1sec < duration < 60sec to reduce
-  ## download server load.
-  - bash: |
-      set -ex
-      number=$RANDOM
-      let "number %= 60" || true
-      let "number += 1"
-      sleep $number
-      mkdir Qt
-      cd Qt
-      if [[ "$(SUBCOMMAND)" == "install-qt" ]]; then
-        opt=""
-        if [[ "$(QT_BASE_MIRROR)" != "" ]]; then
-          opt+=" -b $(QT_BASE_MIRROR)"
-        fi
-        if [[ "$(MODULE)" != "" ]]; then
-          opt+=" -m $(MODULE)"
-        fi
-        if [[ "$(OUTPUT_DIR)" != "" ]]; then
-          opt+=" --outputdir $(OUTPUT_DIR)"
-          sudo mkdir -p "$(OUTPUT_DIR)"
-          sudo chown $(whoami) "$(OUTPUT_DIR)"
-        fi
-        if [[ "$(SUBARCHIVES)" != "" ]]; then
-          opt+=" --archives $(SUBARCHIVES)"
-        fi
-        if [[ "$(IS_AUTODESKTOP)" == "True" ]]; then
-          opt+=" --autodesktop"
-        elif [[ "$(TARGET)" == "android" || "$(TARGET)" == "ios" ]]; then
-          if [[ "$(HOST)" == "windows" ]]; then
-            python -m aqt install-qt $(HOST) desktop $(QT_VERSION) mingw81_64 --archives qtbase
-          else
-            # qtdeclarative contains `qmlimportscanner`: necessary for ios builds, Qt6+
-            python -m aqt install-qt $(HOST) desktop $(QT_VERSION) --archives qtbase qtdeclarative
-          fi
-        fi
-        if [[ "$(SPEC)" == "" ]]; then
-          python -m aqt install-qt $(HOST) $(TARGET) $(QT_VERSION) $(ARCH) $opt
+# Run Aqt
+##----------------------------------------------------
+## we insert sleep in random 1sec < duration < 60sec to reduce
+## download server load.
+- bash: |
+    set -ex
+    number=$RANDOM
+    let "number %= 60" || true
+    let "number += 1"
+    sleep $number
+    mkdir Qt
+    cd Qt
+    if [[ "$(SUBCOMMAND)" == "install-qt-official" ]]; then
+      opt+=" --email $(AQT_TEST_EMAIL) --pw $(AQT_TEST_PASSWORD)"
+      python -m aqt install-qt-official $opt $(TARGET) $(ARCH) $(QT_VERSION) --m $(MODULE)
+    fi
+    if [[ "$(SUBCOMMAND)" == "install-qt" ]]; then
+      opt=""
+      if [[ "$(QT_BASE_MIRROR)" != "" ]]; then
+        opt+=" -b $(QT_BASE_MIRROR)"
+      fi
+      if [[ "$(MODULE)" != "" ]]; then
+        opt+=" -m $(MODULE)"
+      fi
+      if [[ "$(OUTPUT_DIR)" != "" ]]; then
+        opt+=" --outputdir $(OUTPUT_DIR)"
+        sudo mkdir -p "$(OUTPUT_DIR)"
+        sudo chown $(whoami) "$(OUTPUT_DIR)"
+      fi
+      if [[ "$(SUBARCHIVES)" != "" ]]; then
+        opt+=" --archives $(SUBARCHIVES)"
+      fi
+      if [[ "$(IS_AUTODESKTOP)" == "True" ]]; then
+        opt+=" --autodesktop"
+      elif [[ "$(TARGET)" == "android" || "$(TARGET)" == "ios" ]]; then
+        if [[ "$(HOST)" == "windows" ]]; then
+          python -m aqt install-qt $(HOST) desktop $(QT_VERSION) mingw81_64 --archives qtbase
         else
-          python -m aqt install-qt $(HOST) $(TARGET) "$(SPEC)" $(ARCH) $opt
-        fi
-        if [[ "$(OUTPUT_DIR)" != "" ]]; then
-          # Use 'strings' to read binary
-          echo "Verify patched value of qt_prfxpath"
-          [[ "$(strings $(QT_BINDIR)/qmake | grep qt_prfxpath | cut -d '=' -f 2)" == "$(OUTPUT_DIR)/$(QT_VERSION)/$(ARCHDIR)" ]]
-          echo "Verify patched value of qt_epfxpath"
-          [[ "$(strings $(QT_BINDIR)/qmake | grep qt_epfxpath | cut -d '=' -f 2)" == "$(OUTPUT_DIR)/$(QT_VERSION)/$(ARCHDIR)" ]]
-          echo "Verify patched value of qt_hpfxpath"
-          [[ "$(strings $(QT_BINDIR)/qmake | grep qt_hpfxpath | cut -d '=' -f 2)" == "$(OUTPUT_DIR)/$(QT_VERSION)/$(ARCHDIR)" ]]
-
-          # Use 'qmake -query' to check paths
-          echo "Hide qt.conf so it doesn't interfere with test"
-          mv $(QT_BINDIR)/qt.conf $(QT_BINDIR)/_qt.conf
-          #export PATH=$(QT_BINDIR):$PATH
-          echo "Require that qt_epfxpath was set to '$(OUTPUT_DIR)/$(QT_VERSION)/$(ARCHDIR)'"
-          [[ $($(QT_BINDIR)/qmake -query QT_INSTALL_PREFIX) == "$(OUTPUT_DIR)/$(QT_VERSION)/$(ARCHDIR)" ]]
-          echo "Require that qt_prfxpath was set to '$(OUTPUT_DIR)/$(QT_VERSION)/$(ARCHDIR)'"
-          [[ $($(QT_BINDIR)/qmake -query QT_INSTALL_PREFIX/dev) == "$(OUTPUT_DIR)/$(QT_VERSION)/$(ARCHDIR)" ]]
-          echo "Require that qt_hpfxpath was set to '$(OUTPUT_DIR)/$(QT_VERSION)/$(ARCHDIR)'"
-          [[ $($(QT_BINDIR)/qmake -query QT_HOST_PREFIX) == "$(OUTPUT_DIR)/$(QT_VERSION)/$(ARCHDIR)" ]]
+          # qtdeclarative contains `qmlimportscanner`: necessary for ios builds, Qt6+
+          python -m aqt install-qt $(HOST) desktop $(QT_VERSION) --archives qtbase qtdeclarative
         fi
       fi
-      if [[ "$(SUBCOMMAND)" == "list" ]]; then
-        aqt list-qt $(HOST)                                                     # print all targets for host
-        aqt list-tool $(HOST) $(TARGET)                                         # print all tools for host/target
-        aqt list-tool $(HOST) $(TARGET) qt3dstudio_runtime_240                  # print all tool variant names for qt3dstudio
-        aqt list-tool $(HOST) $(TARGET) qt3dstudio_runtime_240 --long           # print tool variant names, versions, release dates
-        if [[ "$(TARGET)" == "desktop" ]]; then
-          aqt list-tool $(HOST) $(TARGET) tools_qtcreator                       # print all tool variant names for qtcreator
-          aqt list-tool $(HOST) $(TARGET) tools_qtcreator -l                    # print tool variant names, versions, release dates
-        fi
-        aqt list-qt $(HOST) $(TARGET)                                           # print all versions of Qt
-        aqt list-qt $(HOST) $(TARGET) --spec "$(SPEC)"                          # print all versions of Qt in SimpleSpec
-        ver=$(aqt list-qt $(HOST) $(TARGET) --spec "$(SPEC)" --latest-version)  # latest Qt in SimpleSpec
-        [ $ver == $(QT_VERSION) ]                                               # latest version in SPEC must be QT_VERSION
-        aqt list-qt $(HOST) $(TARGET) --spec "$(SPEC)" --modules latest $(ARCH) # print modules for latest in SimpleSpec
-        aqt list-qt $(HOST) $(TARGET) --modules $(QT_VERSION) $(ARCH)           # print modules for version/host/target/arch
-        aqt list-qt $(HOST) $(TARGET) --arch $(QT_VERSION)                      # print architectures for version/host/target
-        if [[ "$(TARGET)" == "desktop" ]]; then
-          if [[ "$(HAS_WASM)" == "True" ]]; then                                # fail if 'wasm_32' is not in the list
-            aqt list-qt $(HOST) $(TARGET) --arch $(QT_VERSION) | grep -w -q "wasm_32"
-          else                                                                  # fail if 'wasm_32' is in the list
-            ! aqt list-qt $(HOST) $(TARGET) --arch $(QT_VERSION) | grep -w -q "wasm_32"
-          fi
-        
-          # Extra check for Qt 6.5.0 WASM:
-          for host in mac linux windows; do
-            export WASM_ARCHES=$(aqt list-qt $host desktop --arch 6.5.0)
-            echo $WASM_ARCHES | grep -w -q "wasm_singlethread"                  # fail if 'wasm_singlethread' is not in the list
-            echo $WASM_ARCHES | grep -w -q "wasm_multithread"                   # fail if 'wasm_multithread' is not in the list
-          done
-        fi
-        aqt list-qt $(HOST) $(TARGET) --spec "$(SPEC)" --arch latest
-
-        # list-src/doc/example
-        aqt list-src $(HOST) $(QT_VERSION)                                      # print source archives available for host/desktop/version
-        aqt list-doc $(HOST) $(QT_VERSION)                                      # print docs archives available for host/desktop/version
-        aqt list-doc $(HOST) $(QT_VERSION) --modules                            # print docs modules available for host/desktop/version
-        aqt list-example $(HOST) $(QT_VERSION)                                  # print example archives available for host/desktop/version
-        aqt list-example $(HOST) $(QT_VERSION) --modules                        # print example modules available for host/desktop/version
+      if [[ "$(SPEC)" == "" ]]; then
+        python -m aqt install-qt $(HOST) $(TARGET) $(QT_VERSION) $(ARCH) $opt
+      else
+        python -m aqt install-qt $(HOST) $(TARGET) "$(SPEC)" $(ARCH) $opt
       fi
-      if [[ "$(SUBCOMMAND)" == "install-src" ]]; then
-        python -m aqt $(SUBCOMMAND) $(HOST) $(QT_VERSION) --archives $(SUBARCHIVES)
-        $(CHECK_OUTPUT_CMD)
+      if [[ "$(OUTPUT_DIR)" != "" ]]; then
+        # Use 'strings' to read binary
+        echo "Verify patched value of qt_prfxpath"
+        [[ "$(strings $(QT_BINDIR)/qmake | grep qt_prfxpath | cut -d '=' -f 2)" == "$(OUTPUT_DIR)/$(QT_VERSION)/$(ARCHDIR)" ]]
+        echo "Verify patched value of qt_epfxpath"
+        [[ "$(strings $(QT_BINDIR)/qmake | grep qt_epfxpath | cut -d '=' -f 2)" == "$(OUTPUT_DIR)/$(QT_VERSION)/$(ARCHDIR)" ]]
+        echo "Verify patched value of qt_hpfxpath"
+        [[ "$(strings $(QT_BINDIR)/qmake | grep qt_hpfxpath | cut -d '=' -f 2)" == "$(OUTPUT_DIR)/$(QT_VERSION)/$(ARCHDIR)" ]]
+    
+        # Use 'qmake -query' to check paths
+        echo "Hide qt.conf so it doesn't interfere with test"
+        mv $(QT_BINDIR)/qt.conf $(QT_BINDIR)/_qt.conf
+        #export PATH=$(QT_BINDIR):$PATH
+        echo "Require that qt_epfxpath was set to '$(OUTPUT_DIR)/$(QT_VERSION)/$(ARCHDIR)'"
+        [[ $($(QT_BINDIR)/qmake -query QT_INSTALL_PREFIX) == "$(OUTPUT_DIR)/$(QT_VERSION)/$(ARCHDIR)" ]]
+        echo "Require that qt_prfxpath was set to '$(OUTPUT_DIR)/$(QT_VERSION)/$(ARCHDIR)'"
+        [[ $($(QT_BINDIR)/qmake -query QT_INSTALL_PREFIX/dev) == "$(OUTPUT_DIR)/$(QT_VERSION)/$(ARCHDIR)" ]]
+        echo "Require that qt_hpfxpath was set to '$(OUTPUT_DIR)/$(QT_VERSION)/$(ARCHDIR)'"
+        [[ $($(QT_BINDIR)/qmake -query QT_HOST_PREFIX) == "$(OUTPUT_DIR)/$(QT_VERSION)/$(ARCHDIR)" ]]
       fi
-      if [[ "$(SUBCOMMAND)" =~ ^install-(doc|example)$ ]]; then
-        opt=""
-        if [[ "$(MODULE)" != "" ]]; then
-          opt+=" -m $(MODULE)"
-        fi
-        python -m aqt $(SUBCOMMAND) $(HOST) $(QT_VERSION) --archives $(SUBARCHIVES) $opt
-        $(CHECK_OUTPUT_CMD)
+    fi
+    if [[ "$(SUBCOMMAND)" == "list" ]]; then
+      aqt list-qt $(HOST)                                                     # print all targets for host
+      aqt list-tool $(HOST) $(TARGET)                                         # print all tools for host/target
+      aqt list-tool $(HOST) $(TARGET) qt3dstudio_runtime_240                  # print all tool variant names for qt3dstudio
+      aqt list-tool $(HOST) $(TARGET) qt3dstudio_runtime_240 --long           # print tool variant names, versions, release dates
+      if [[ "$(TARGET)" == "desktop" ]]; then
+        aqt list-tool $(HOST) $(TARGET) tools_qtcreator                       # print all tool variant names for qtcreator
+        aqt list-tool $(HOST) $(TARGET) tools_qtcreator -l                    # print tool variant names, versions, release dates
       fi
-      if [[ "$(SUBCOMMAND)" == "install-tool" ]]; then
-        opt=""
-        if [[ "$(OUTPUT_DIR)" != "" ]]; then
-          opt+=" --outputdir $(OUTPUT_DIR)"
-          sudo mkdir -p "$(OUTPUT_DIR)"
-          sudo chown $(whoami) "$(OUTPUT_DIR)"
+      aqt list-qt $(HOST) $(TARGET)                                           # print all versions of Qt
+      aqt list-qt $(HOST) $(TARGET) --spec "$(SPEC)"                          # print all versions of Qt in SimpleSpec
+      ver=$(aqt list-qt $(HOST) $(TARGET) --spec "$(SPEC)" --latest-version)  # latest Qt in SimpleSpec
+      [ $ver == $(QT_VERSION) ]                                               # latest version in SPEC must be QT_VERSION
+      aqt list-qt $(HOST) $(TARGET) --spec "$(SPEC)" --modules latest $(ARCH) # print modules for latest in SimpleSpec
+      aqt list-qt $(HOST) $(TARGET) --modules $(QT_VERSION) $(ARCH)           # print modules for version/host/target/arch
+      aqt list-qt $(HOST) $(TARGET) --arch $(QT_VERSION)                      # print architectures for version/host/target
+      if [[ "$(TARGET)" == "desktop" ]]; then
+        if [[ "$(HAS_WASM)" == "True" ]]; then                                # fail if 'wasm_32' is not in the list
+          aqt list-qt $(HOST) $(TARGET) --arch $(QT_VERSION) | grep -w -q "wasm_32"
+        else                                                                  # fail if 'wasm_32' is in the list
+          ! aqt list-qt $(HOST) $(TARGET) --arch $(QT_VERSION) | grep -w -q "wasm_32"
         fi
-        python -m aqt $(SUBCOMMAND) $(HOST) $(TARGET) $(TOOL1_ARGS) $opt
-        $(LIST_TOOL1_CMD)
-        echo "Testing $(TOOL1_ARGS) with '$(TEST_TOOL1_CMD)'"
-        $(TEST_TOOL1_CMD)
-        python -m aqt $(SUBCOMMAND) $(HOST) $(TARGET) $(TOOL2_ARGS) $opt
-        $(LIST_TOOL2_CMD)
-        echo "Testing $(TOOL2_ARGS) with '$(TEST_TOOL2_CMD)'"
-        $(TEST_TOOL2_CMD)
+    
+        # Extra check for Qt 6.5.0 WASM:
+        for host in mac linux windows; do
+          export WASM_ARCHES=$(aqt list-qt $host desktop --arch 6.5.0)
+          echo $WASM_ARCHES | grep -w -q "wasm_singlethread"                  # fail if 'wasm_singlethread' is not in the list
+          echo $WASM_ARCHES | grep -w -q "wasm_multithread"                   # fail if 'wasm_multithread' is not in the list
+        done
       fi
-    workingDirectory: $(Build.BinariesDirectory)
-    env:
-      AQT_CONFIG: $(Build.SourcesDirectory)/ci/settings.ini
-      LOG_CFG: $(Build.SourcesDirectory)/ci/logging.ini
-    displayName: Run Aqt
-
-  ##----------------------------------------------------
-  # for Android target
-  - bash: |
-      set -ex
-      if [[ "$(Agent.OS)" == "Linux" ]]; then
-        wget https://dl.google.com/android/repository/android-ndk-r21e-linux-x86_64.zip
-        unzip android-ndk-r21e-linux-x86_64.zip
+      aqt list-qt $(HOST) $(TARGET) --spec "$(SPEC)" --arch latest
+    
+      # list-src/doc/example
+      aqt list-src $(HOST) $(QT_VERSION)                                      # print source archives available for host/desktop/version
+      aqt list-doc $(HOST) $(QT_VERSION)                                      # print docs archives available for host/desktop/version
+      aqt list-doc $(HOST) $(QT_VERSION) --modules                            # print docs modules available for host/desktop/version
+      aqt list-example $(HOST) $(QT_VERSION)                                  # print example archives available for host/desktop/version
+      aqt list-example $(HOST) $(QT_VERSION) --modules                        # print example modules available for host/desktop/version
+    fi
+    if [[ "$(SUBCOMMAND)" == "install-src" ]]; then
+      python -m aqt $(SUBCOMMAND) $(HOST) $(QT_VERSION) --archives $(SUBARCHIVES)
+      $(CHECK_OUTPUT_CMD)
+    fi
+    if [[ "$(SUBCOMMAND)" =~ ^install-(doc|example)$ ]]; then
+      opt="" 
+      if [[ "$(MODULE)" != "" ]]; then
+        opt+=" -m $(MODULE)"
       fi
-      if [[ "$(Agent.OS)" == "Darwin" ]]; then
-        wget https://dl.google.com/android/repository/android-ndk-r21e-darwin-x86_64.zip
-        unzip android-ndk-r21e-darwin-x86_64.zip
+      python -m aqt $(SUBCOMMAND) $(HOST) $(QT_VERSION) --archives $(SUBARCHIVES) $opt
+      $(CHECK_OUTPUT_CMD)
+    fi
+    if [[ "$(SUBCOMMAND)" == "install-tool" ]]; then
+      opt=""
+      if [[ "$(OUTPUT_DIR)" != "" ]]; then
+        opt+=" --outputdir $(OUTPUT_DIR)"
+        sudo mkdir -p "$(OUTPUT_DIR)"
+        sudo chown $(whoami) "$(OUTPUT_DIR)"
       fi
-      export ANDROID_NDK_ROOT=$(Build.SourcesDirectory)/android-ndk-r21e
-      mkdir $(Build.BinariesDirectory)/tests
-      (cd $(Build.BinariesDirectory)/tests; 7zr x $(Build.SourcesDirectory)/ci/accelbubble.7z)
-      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-qt'),
-        ne(variables['SUBCOMMAND'], 'install-tool')
-      )
-    displayName: Build accelbubble example application to test for android
+      python -m aqt $(SUBCOMMAND) $(HOST) $(TARGET) $(TOOL1_ARGS) $opt
+      $(LIST_TOOL1_CMD)
+      echo "Testing $(TOOL1_ARGS) with '$(TEST_TOOL1_CMD)'"
+      $(TEST_TOOL1_CMD)
+      python -m aqt $(SUBCOMMAND) $(HOST) $(TARGET) $(TOOL2_ARGS) $opt
+      $(LIST_TOOL2_CMD)
+      echo "Testing $(TOOL2_ARGS) with '$(TEST_TOOL2_CMD)'"
+      $(TEST_TOOL2_CMD)
+    fi
+  workingDirectory: $(Build.BinariesDirectory)
+  env:
+    AQT_CONFIG: $(Build.SourcesDirectory)/ci/settings.ini
+    LOG_CFG: $(Build.SourcesDirectory)/ci/logging.ini
+  displayName: Run Aqt
 
-  ##----------------------------------------------------
-  # for iOS target
-  - bash: |
-      set -ex
-      mkdir $(Build.BinariesDirectory)/tests
-      (cd $(Build.BinariesDirectory)/tests; 7zr x $(Build.SourcesDirectory)/ci/accelbubble.7z)
-      export PATH=$(QT_BINDIR):$PATH
-      qmake $(Build.BinariesDirectory)/tests/accelbubble
-      make
-    condition: |
-        and(eq(variables['TARGET'], 'ios'), 
-            ne(variables['SUBCOMMAND'], 'list'), 
-            ne(variables['SUBCOMMAND'], 'install-tool'))
-    displayName: Build accelbubble example application to test for ios
+##----------------------------------------------------
+# for Android target
+- bash: |
+    set -ex
+    if [[ "$(Agent.OS)" == "Linux" ]]; then
+      wget https://dl.google.com/android/repository/android-ndk-r21e-linux-x86_64.zip
+      unzip android-ndk-r21e-linux-x86_64.zip
+    fi
+    if [[ "$(Agent.OS)" == "Darwin" ]]; then
+      wget https://dl.google.com/android/repository/android-ndk-r21e-darwin-x86_64.zip
+      unzip android-ndk-r21e-darwin-x86_64.zip
+    fi
+    export ANDROID_NDK_ROOT=$(Build.SourcesDirectory)/android-ndk-r21e
+    mkdir $(Build.BinariesDirectory)/tests
+    (cd $(Build.BinariesDirectory)/tests; 7zr x $(Build.SourcesDirectory)/ci/accelbubble.7z)
+    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-qt'),
+      ne(variables['SUBCOMMAND'], 'install-tool')
+    )
+  displayName: Build accelbubble example application to test for android
 
-  ##----------------------------------------------------
-  # Cache Powershell Modules in $MODULES_FOLDER
-  - task: Cache@2
-    inputs:
-      key: '"pwsh modules" | "$(startYear)" | "$(startMonth)"'
-      path: $(MODULES_FOLDER)
-      cacheHitVar: PSModules_IsCached
-    condition: |
-        and(eq(variables['Agent.OS'], 'Windows_NT'), 
-            eq(variables['SUBCOMMAND'], 'install-qt'))
-    displayName: Cache Powershell Modules
-  # On cache miss: Download Powershell Modules to $MODULES_FOLDER
-  - powershell: |
-      Install-PackageProvider NuGet -Force
-      Import-PackageProvider NuGet -Force
-      Set-PSRepository -Name PSGallery -InstallationPolicy Trusted
-      md -p $(MODULES_FOLDER)   # mkdir
-      Save-Module -Name Pscx -AllowPrerelease -Path $(MODULES_FOLDER)
-    condition: |
-        and(eq(variables['Agent.OS'], 'Windows_NT'), 
-            eq(variables['SUBCOMMAND'], 'install-qt'),
-            ne(variables.PSModules_IsCached, 'true'))
-    displayName: Download Powershell Modules for Windows on cache miss
+##----------------------------------------------------
+# for iOS target
+- bash: |
+    set -ex
+    mkdir $(Build.BinariesDirectory)/tests
+    (cd $(Build.BinariesDirectory)/tests; 7zr x $(Build.SourcesDirectory)/ci/accelbubble.7z)
+    export PATH=$(QT_BINDIR):$PATH
+    qmake $(Build.BinariesDirectory)/tests/accelbubble
+    make
+  condition: |
+    and(eq(variables['TARGET'], 'ios'), 
+        ne(variables['SUBCOMMAND'], 'list'), 
+        ne(variables['SUBCOMMAND'], 'install-tool'))
+  displayName: Build accelbubble example application to test for ios
 
-  ##----------------------------------------------------
-  # determine Windows build system
-  - powershell: |
-      if ('$(ARCH)' -like '*msvc*') {
-        Write-Host '##vso[task.setvariable variable=TOOLCHAIN]MSVC'
-      }
-      if ('$(ARCH)' -like '*mingw*') {
-        Write-Host '##vso[task.setvariable variable=TOOLCHAIN]MINGW'
-      }
-      if ('$(ARCH)' -like 'win64_msvc*') {
-        Write-Host '##vso[task.setvariable variable=ARCHITECTURE]amd64'
-      } else {
-        Write-Host '##vso[task.setvariable variable=ARCHITECTURE]x86'
-      }
-      if ('$(ARCH)' -like '*msvc*') {
-        Write-Host '##vso[task.setvariable variable=VSVER]2022'
-      }
-      cd $(WIN_QT_BINDIR)
-      unzip $(Build.SourcesDirectory)\ci\jom_1_1_3.zip
-    condition: |
-        and(eq(variables['Agent.OS'], 'Windows_NT'), 
-            eq(variables['SUBCOMMAND'], 'install-qt'))
-    displayName: Detect toolchain for Windows and update PATH
+##----------------------------------------------------
+# Cache Powershell Modules in $MODULES_FOLDER
+- task: Cache@2
+  inputs:
+    key: '"pwsh modules" | "$(startYear)" | "$(startMonth)"'
+    path: $(MODULES_FOLDER)
+    cacheHitVar: PSModules_IsCached
+  condition: |
+    and(eq(variables['Agent.OS'], 'Windows_NT'), 
+        eq(variables['SUBCOMMAND'], 'install-qt'))
+  displayName: Cache Powershell Modules
+# On cache miss: Download Powershell Modules to $MODULES_FOLDER
+- powershell: |
+    Install-PackageProvider NuGet -Force
+    Import-PackageProvider NuGet -Force
+    Set-PSRepository -Name PSGallery -InstallationPolicy Trusted
+    md -p $(MODULES_FOLDER)   # mkdir
+    Save-Module -Name Pscx -AllowPrerelease -Path $(MODULES_FOLDER)
+  condition: |
+    and(eq(variables['Agent.OS'], 'Windows_NT'), 
+        eq(variables['SUBCOMMAND'], 'install-qt'),
+        ne(variables.PSModules_IsCached, 'true'))
+  displayName: Download Powershell Modules for Windows on cache miss
 
-  # When no modules
-  - script: |
-      set -ex
-      mkdir $(Build.BinariesDirectory)/tests
-      (cd $(Build.BinariesDirectory)/tests; 7zr x $(Build.SourcesDirectory)/ci/helloworld.7z)
-      export PATH=$(QT_BINDIR):$PATH
-      qmake $(Build.BinariesDirectory)/tests/helloworld
-      make
-    condition: |
-        and(
-          eq( variables['TARGET'], 'desktop' ),
-          not( startsWith( variables['ARCH'], 'wasm_' ) ),
-          or(
-            eq(variables['Agent.OS'], 'Linux'),
-            eq(variables['Agent.OS'], 'Darwin')
-          ),
-          eq(variables['MODULE'], ''),
-          eq(variables['SUBCOMMAND'], 'install-qt')
-        )
-    displayName: Build test with qmake for Linux and macOS w/o extra module
-  # Update the MSVC build step to use the new compiler flags
-  - powershell: |
-      if ( $env:TOOLCHAIN -eq 'MSVC' ) {
-        # Load modules from cache  
-        $Env:PSModulePath = '$(MODULES_FOLDER)', $Env:PSModulePath -join [System.IO.Path]::PathSeparator
-        Write-Host $Env:PSModulePath
-        Import-Module "Pscx"
-        Import-Module "VSSetup"
+##----------------------------------------------------
+# determine Windows build system
+- powershell: |
+    if ('$(ARCH)' -like '*msvc*') {
+      Write-Host '##vso[task.setvariable variable=TOOLCHAIN]MSVC'
+    }
+    if ('$(ARCH)' -like '*mingw*') {
+      Write-Host '##vso[task.setvariable variable=TOOLCHAIN]MINGW'
+    }
+    if ('$(ARCH)' -like 'win64_msvc*') {
+      Write-Host '##vso[task.setvariable variable=ARCHITECTURE]amd64'
+    } else {
+      Write-Host '##vso[task.setvariable variable=ARCHITECTURE]x86'
+    }
+    if ('$(ARCH)' -like '*msvc*') {
+      Write-Host '##vso[task.setvariable variable=VSVER]2022'
+    }
+    cd $(WIN_QT_BINDIR)
+    unzip $(Build.SourcesDirectory)\ci\jom_1_1_3.zip
+  condition: |
+    and(eq(variables['Agent.OS'], 'Windows_NT'), 
+        eq(variables['SUBCOMMAND'], 'install-qt'))
+  displayName: Detect toolchain for Windows and update PATH
 
-        Import-VisualStudioVars -VisualStudioVersion $(VSVER) -Architecture $(ARCHITECTURE)
-        $env:Path += ";$(WIN_QT_BINDIR)"
-        mkdir $(Build.BinariesDirectory)\tests
-        cd $(Build.BinariesDirectory)\tests
-        7z x $(Build.SourcesDirectory)\ci\helloworld.7z
-        cd ..
-        qmake $(Build.BinariesDirectory)\tests\helloworld
-        jom
-      } elseif ( $env:TOOLCHAIN -eq 'MINGW' ) {
-        python -m aqt install-tool $(if (($QT_BASE_MIRROR + "") -ne "") { "-b $QT_BASE_MIRROR" } else {""}) `
-          --outputdir $(Build.BinariesDirectory)/Qt $(HOST) desktop $(MINGW_TOOL_NAME) qt.tools.$(MINGW_VARIANT)
-        if ($?) {
-          Write-Host 'Successfully installed tools_mingw'
-        } else {
-          throw 'Failed to install tools_mingw'
-        }
-        Set-Item -Path Env:Path -Value ("$(Build.BinariesDirectory)\Qt\Tools\$(MINGW_FOLDER)\bin;$(WIN_QT_BINDIR);" + $Env:Path)
-        Write-Host "Path == " + $env:Path
-        mkdir $(Build.BinariesDirectory)\tests
-        cd $(Build.BinariesDirectory)\tests
-        7z x $(Build.SourcesDirectory)\ci\helloworld.7z
-        cd ..
-        qmake $(Build.BinariesDirectory)\tests\helloworld
-        mingw32-make
-      }
-    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
-    env:
-      AQT_CONFIG: $(Build.SourcesDirectory)/ci/settings.ini
-      LOG_CFG: $(Build.SourcesDirectory)/ci/logging.ini
-
-# When --archives non-empty
-  - script: |
-        set -ex
-        rm -rf $(Build.BinariesDirectory)/tests
-        mkdir $(Build.BinariesDirectory)/tests
-        (cd $(Build.BinariesDirectory)/tests && 7zr x $(Build.SourcesDirectory)/ci/helloworld_qttools.7z)
-        export PATH=$(QT_BINDIR):$PATH
-        qmake -d $(Build.BinariesDirectory)/tests/helloworld_qttools
-        make
-    condition: |
-        and(
-          eq( variables['TARGET'], 'desktop' ),
-          not( startsWith( variables['ARCH'], 'wasm_' ) ),
-          or(
-            eq(variables['Agent.OS'], 'Linux'),
-            eq(variables['Agent.OS'], 'Darwin')
-          ),
-          contains(variables['SUBARCHIVES'], 'qttools'),
-          eq(variables['SUBCOMMAND'], 'install-qt')
-        )
-    displayName: build test with qmake for Linux and macOS with specific Qt modules (QT += uitools)
-  - powershell: |
-        if ( $env:TOOLCHAIN -eq 'MSVC' ) {
-          # Load modules from cache
-          $Env:PSModulePath = '$(MODULES_FOLDER)', $Env:PSModulePath -join [System.IO.Path]::PathSeparator
-          Write-Host $Env:PSModulePath
-          Import-Module "Pscx"
-          Import-Module "VSSetup"
-
-          Import-VisualStudioVars -VisualStudioVersion $(VSVER) -Architecture $(ARCHITECTURE)
-          $env:Path += ";$(WIN_QT_BINDIR)"
-          try { rm -r -fo $(Build.BinariesDirectory)\tests } catch { $Error.Clear() }
-          mkdir $(Build.BinariesDirectory)\tests
-          cd $(Build.BinariesDirectory)\tests
-          7z x $(Build.SourcesDirectory)\ci\helloworld_qttools.7z
-          qmake -d $(Build.BinariesDirectory)\tests\helloworld_qttools
-          nmake
-        } elseif ( $env:TOOLCHAIN -eq 'MINGW' ) {
-          Set-Item -Path Env:Path -Value ("$(Build.BinariesDirectory)\Qt\Tools\$(MINGW_FOLDER)\bin;$(WIN_QT_BINDIR);" + $Env:Path)
-          Write-Host "Path == " + $env:Path
-          if (![bool] (Get-Command -ErrorAction Ignore -Type Application mingw32-make)) {
-            python -m aqt install-tool $(if (($QT_BASE_MIRROR + "") -ne "") { "-b $QT_BASE_MIRROR" } else {""}) `
-               --outputdir $(Build.BinariesDirectory)/Qt $(HOST) desktop $(MINGW_TOOL_NAME) qt.tools.$(MINGW_VARIANT)
-            if ($?) {
-              Write-Host 'Successfully installed tools_mingw'
-            } else {
-              throw 'Failed to install tools_mingw'
-            }
-          }
-          try { rm -r -fo $(Build.BinariesDirectory)\tests } catch { $Error.Clear() }
-          mkdir $(Build.BinariesDirectory)\tests
-          cd $(Build.BinariesDirectory)\tests
-          7z x $(Build.SourcesDirectory)\ci\helloworld_qttools.7z
-          qmake -d $(Build.BinariesDirectory)\tests\helloworld_qttools
-          mingw32-make
-        }
-    condition: |
-        and(
-          eq( variables['Agent.OS'], 'Windows_NT'),
-          contains(variables['SUBARCHIVES'], 'qttools'),
-          eq(variables['SUBCOMMAND'], 'install-qt')
-        )
-    displayName: build test with qmake with specific Qt modules (QT += uitools)
-    env:
-      AQT_CONFIG: $(Build.SourcesDirectory)/ci/settings.ini
-      LOG_CFG: $(Build.SourcesDirectory)/ci/logging.ini
-
-  - powershell: |
+# When no modules
+- script: |
+    set -ex
+    mkdir $(Build.BinariesDirectory)/tests
+    (cd $(Build.BinariesDirectory)/tests; 7zr x $(Build.SourcesDirectory)/ci/helloworld.7z)
+    export PATH=$(QT_BINDIR):$PATH
+    qmake $(Build.BinariesDirectory)/tests/helloworld
+    make
+  condition: |
+    and(
+      eq( variables['TARGET'], 'desktop' ),
+      not( startsWith( variables['ARCH'], 'wasm_' ) ),
+      or(
+        eq(variables['Agent.OS'], 'Linux'),
+        eq(variables['Agent.OS'], 'Darwin')
+      ),
+      eq(variables['MODULE'], ''),
+      eq(variables['SUBCOMMAND'], 'install-qt')
+    )
+  displayName: Build test with qmake for Linux and macOS w/o extra module
+# Update the MSVC build step to use the new compiler flags
+- powershell: |
+    if ( $env:TOOLCHAIN -eq 'MSVC' ) {
       # Load modules from cache  
       $Env:PSModulePath = '$(MODULES_FOLDER)', $Env:PSModulePath -join [System.IO.Path]::PathSeparator
       Write-Host $Env:PSModulePath
       Import-Module "Pscx"
       Import-Module "VSSetup"
-
+    
       Import-VisualStudioVars -VisualStudioVersion $(VSVER) -Architecture $(ARCHITECTURE)
       $env:Path += ";$(WIN_QT_BINDIR)"
-      echo Add Qt to PATH: $env:PATH
-      mkdir $(Build.BinariesDirectory)/tests
-      cd $(Build.BinariesDirectory)/tests
-      7z x $(Build.SourcesDirectory)/ci/redditclient.7z
-      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'], '2022')
-      )
-    displayName: build test with qmake with MSVC with extra module
-  - bash: |
-      set -ex
-      mkdir $(Build.BinariesDirectory)/tests
-      (cd $(Build.BinariesDirectory)/tests; 7zr x $(Build.SourcesDirectory)/ci/redditclient.7z)
-      if [[ "6" -eq "${QT_VERSION:0:1}" ]]; then
-        (cd $(Build.BinariesDirectory)/tests/redditclient;patch -i redditclient_6.diff -p1)
-      fi
-      export PATH=$(QT_BINDIR):$PATH
-      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')
-      )
-    displayName: Build test with qmake for Linux and macOS with extra module
-
-  ##----------------------------------------------------
-  # wasm_32/single/multithread on linux and mac
-  - script: |
-      set -uex
-      git clone --depth=1 --branch=$(EMSDK_TAG) https://github.com/emscripten-core/emsdk.git
-      cd emsdk
-      ./emsdk install $(EMSDK_VERSION)
-      ./emsdk activate --embedded $(EMSDK_VERSION)
-      source $(Build.BinariesDirectory)/emsdk/emsdk_env.sh
-      mkdir $(Build.BinariesDirectory)/tests
-      if [[ $(QT_VERSION) = 6* ]]; then
-        OPENGLWINDOW_7Z="openglwindow_qt6.7z"
-      else
-        OPENGLWINDOW_7Z="openglwindow.7z"
-      fi
-      (cd $(Build.BinariesDirectory)/tests; 7zr x $(Build.SourcesDirectory)/ci/$OPENGLWINDOW_7Z)
-      export PATH=$(QT_BINDIR):$PATH
-      qmake $(Build.BinariesDirectory)/tests/openglwindow
-      make
-    workingDirectory: $(Build.BinariesDirectory)
-    condition: |
-      and(
-        startsWith( variables['ARCH'], 'wasm_' ),
-        or(
-          eq(variables['Agent.OS'], 'Linux'),
-          eq(variables['Agent.OS'], 'Darwin')
-        )
-      )
-    displayName: 'Build WebAssembler sample project on mac/linux'
-
-  # wasm_32/single/multithread on Windows cmd.exe
-  - powershell: |
-      git clone --depth=1 --branch=$(EMSDK_TAG) https://github.com/emscripten-core/emsdk.git
-      cd emsdk
-      .\emsdk install $(EMSDK_VERSION)
-      .\emsdk activate --embedded $(EMSDK_VERSION)
-      .\emsdk_env.bat
       mkdir $(Build.BinariesDirectory)\tests
       cd $(Build.BinariesDirectory)\tests
-      $env:Path += ";$(WIN_QT_BINDIR)"
-      echo "Add Qt/qmake to PATH at $(WIN_QT_BINDIR):" 
-      echo "$env:Path"
-      if ('$(QT_VERSION)' -like '6*') {
-        7z x $(Build.SourcesDirectory)\ci\openglwindow_qt6.7z
-        echo "Inlined qmake.bat command is: $(WIN_AUTODESK_QT_BINDIR)\qmake -qtconf $(WIN_QT_BINDIR)\target_qt.conf $(Build.BinariesDirectory)\tests\openglwindow"
-        $(WIN_AUTODESK_QT_BINDIR)\qmake.exe -qtconf "$(WIN_QT_BINDIR)\target_qt.conf" $(Build.BinariesDirectory)\tests\openglwindow
+      7z x $(Build.SourcesDirectory)\ci\helloworld.7z
+      cd ..
+      qmake $(Build.BinariesDirectory)\tests\helloworld
+      jom
+    } elseif ( $env:TOOLCHAIN -eq 'MINGW' ) {
+      python -m aqt install-tool $(if (($QT_BASE_MIRROR + "") -ne "") { "-b $QT_BASE_MIRROR" } else {""}) `
+        --outputdir $(Build.BinariesDirectory)/Qt $(HOST) desktop $(MINGW_TOOL_NAME) qt.tools.$(MINGW_VARIANT)
+      if ($?) {
+        Write-Host 'Successfully installed tools_mingw'
       } else {
-        7z x $(Build.SourcesDirectory)\ci\openglwindow.7z
-        echo "Qt5: run qmake.exe"
-        qmake $(Build.BinariesDirectory)\tests\openglwindow
-      }
-      if ($false -eq $?) {
-        Write-Error "qmake failed."
-        Write-Host "##vso[task.logissue type=error]qmake failed."
-        exit(1)
+        throw 'Failed to install tools_mingw'
       }
-      make
-      if ($false -eq $?) {
-        Write-Error "make failed."
-        Write-Host "##vso[task.logissue type=error]nmake failed."
-        exit(1)
+      Set-Item -Path Env:Path -Value ("$(Build.BinariesDirectory)\Qt\Tools\$(MINGW_FOLDER)\bin;$(WIN_QT_BINDIR);" + $Env:Path)
+      Write-Host "Path == " + $env:Path
+      mkdir $(Build.BinariesDirectory)\tests
+      cd $(Build.BinariesDirectory)\tests
+      7z x $(Build.SourcesDirectory)\ci\helloworld.7z
+      cd ..
+      qmake $(Build.BinariesDirectory)\tests\helloworld
+      mingw32-make
+    }
+  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
+  env:
+    AQT_CONFIG: $(Build.SourcesDirectory)/ci/settings.ini
+    LOG_CFG: $(Build.SourcesDirectory)/ci/logging.ini
+
+# When --archives non-empty
+- script: |
+    set -ex
+    rm -rf $(Build.BinariesDirectory)/tests
+    mkdir $(Build.BinariesDirectory)/tests
+    (cd $(Build.BinariesDirectory)/tests && 7zr x $(Build.SourcesDirectory)/ci/helloworld_qttools.7z)
+    export PATH=$(QT_BINDIR):$PATH
+    qmake -d $(Build.BinariesDirectory)/tests/helloworld_qttools
+    make
+  condition: |
+    and(
+      eq( variables['TARGET'], 'desktop' ),
+      not( startsWith( variables['ARCH'], 'wasm_' ) ),
+      or(
+        eq(variables['Agent.OS'], 'Linux'),
+        eq(variables['Agent.OS'], 'Darwin')
+      ),
+      contains(variables['SUBARCHIVES'], 'qttools'),
+      eq(variables['SUBCOMMAND'], 'install-qt')
+    )
+  displayName: build test with qmake for Linux and macOS with specific Qt modules (QT += uitools)
+- powershell: |
+    if ( $env:TOOLCHAIN -eq 'MSVC' ) {
+      # Load modules from cache
+      $Env:PSModulePath = '$(MODULES_FOLDER)', $Env:PSModulePath -join [System.IO.Path]::PathSeparator
+      Write-Host $Env:PSModulePath
+      Import-Module "Pscx"
+      Import-Module "VSSetup"
+    
+      Import-VisualStudioVars -VisualStudioVersion $(VSVER) -Architecture $(ARCHITECTURE)
+      $env:Path += ";$(WIN_QT_BINDIR)"
+      try { rm -r -fo $(Build.BinariesDirectory)\tests } catch { $Error.Clear() }
+      mkdir $(Build.BinariesDirectory)\tests
+      cd $(Build.BinariesDirectory)\tests
+      7z x $(Build.SourcesDirectory)\ci\helloworld_qttools.7z
+      qmake -d $(Build.BinariesDirectory)\tests\helloworld_qttools
+      nmake
+    } elseif ( $env:TOOLCHAIN -eq 'MINGW' ) {
+      Set-Item -Path Env:Path -Value ("$(Build.BinariesDirectory)\Qt\Tools\$(MINGW_FOLDER)\bin;$(WIN_QT_BINDIR);" + $Env:Path)
+      Write-Host "Path == " + $env:Path
+      if (![bool] (Get-Command -ErrorAction Ignore -Type Application mingw32-make)) {
+        python -m aqt install-tool $(if (($QT_BASE_MIRROR + "") -ne "") { "-b $QT_BASE_MIRROR" } else {""}) `
+           --outputdir $(Build.BinariesDirectory)/Qt $(HOST) desktop $(MINGW_TOOL_NAME) qt.tools.$(MINGW_VARIANT)
+        if ($?) {
+          Write-Host 'Successfully installed tools_mingw'
+        } else {
+          throw 'Failed to install tools_mingw'
+        }
       }
-    workingDirectory: $(Build.BinariesDirectory)
-    condition: |
-      and(
-        startsWith( variables['ARCH'], 'wasm_' ),
-        eq( variables['Agent.OS'], 'Windows_NT' )
+      try { rm -r -fo $(Build.BinariesDirectory)\tests } catch { $Error.Clear() }
+      mkdir $(Build.BinariesDirectory)\tests
+      cd $(Build.BinariesDirectory)\tests
+      7z x $(Build.SourcesDirectory)\ci\helloworld_qttools.7z
+      qmake -d $(Build.BinariesDirectory)\tests\helloworld_qttools
+      mingw32-make
+    }
+  condition: |
+    and(
+      eq( variables['Agent.OS'], 'Windows_NT'),
+      contains(variables['SUBARCHIVES'], 'qttools'),
+      eq(variables['SUBCOMMAND'], 'install-qt')
+    )
+  displayName: build test with qmake with specific Qt modules (QT += uitools)
+  env:
+    AQT_CONFIG: $(Build.SourcesDirectory)/ci/settings.ini
+    LOG_CFG: $(Build.SourcesDirectory)/ci/logging.ini
+
+- powershell: |
+    # Load modules from cache  
+    $Env:PSModulePath = '$(MODULES_FOLDER)', $Env:PSModulePath -join [System.IO.Path]::PathSeparator
+    Write-Host $Env:PSModulePath
+    Import-Module "Pscx"
+    Import-Module "VSSetup"
+    
+    Import-VisualStudioVars -VisualStudioVersion $(VSVER) -Architecture $(ARCHITECTURE)
+    $env:Path += ";$(WIN_QT_BINDIR)"
+    echo Add Qt to PATH: $env:PATH
+    mkdir $(Build.BinariesDirectory)/tests
+    cd $(Build.BinariesDirectory)/tests
+    7z x $(Build.SourcesDirectory)/ci/redditclient.7z
+    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'], '2022')
+    )
+  displayName: build test with qmake with MSVC with extra module
+- bash: |
+    set -ex
+    mkdir $(Build.BinariesDirectory)/tests
+    (cd $(Build.BinariesDirectory)/tests; 7zr x $(Build.SourcesDirectory)/ci/redditclient.7z)
+    if [[ "6" -eq "${QT_VERSION:0:1}" ]]; then
+      (cd $(Build.BinariesDirectory)/tests/redditclient;patch -i redditclient_6.diff -p1)
+    fi
+    export PATH=$(QT_BINDIR):$PATH
+    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')
+    )
+  displayName: Build test with qmake for Linux and macOS with extra module
+
+##----------------------------------------------------
+# wasm_32/single/multithread on linux and mac
+- script: |
+    set -uex
+    git clone --depth=1 --branch=$(EMSDK_TAG) https://github.com/emscripten-core/emsdk.git
+    cd emsdk
+    ./emsdk install $(EMSDK_VERSION)
+    ./emsdk activate --embedded $(EMSDK_VERSION)
+    source $(Build.BinariesDirectory)/emsdk/emsdk_env.sh
+    mkdir $(Build.BinariesDirectory)/tests
+    if [[ $(QT_VERSION) = 6* ]]; then
+      OPENGLWINDOW_7Z="openglwindow_qt6.7z"
+    else
+      OPENGLWINDOW_7Z="openglwindow.7z"
+    fi
+    (cd $(Build.BinariesDirectory)/tests; 7zr x $(Build.SourcesDirectory)/ci/$OPENGLWINDOW_7Z)
+    export PATH=$(QT_BINDIR):$PATH
+    qmake $(Build.BinariesDirectory)/tests/openglwindow
+    make
+  workingDirectory: $(Build.BinariesDirectory)
+  condition: |
+    and(
+      startsWith( variables['ARCH'], 'wasm_' ),
+      or(
+        eq(variables['Agent.OS'], 'Linux'),
+        eq(variables['Agent.OS'], 'Darwin')
       )
-    displayName: 'Build WebAssembler sample project on windows'
+    )
+  displayName: 'Build WebAssembler sample project on mac/linux'
+
+# wasm_32/single/multithread on Windows cmd.exe
+- powershell: |
+    git clone --depth=1 --branch=$(EMSDK_TAG) https://github.com/emscripten-core/emsdk.git
+    cd emsdk
+    .\emsdk install $(EMSDK_VERSION)
+    .\emsdk activate --embedded $(EMSDK_VERSION)
+    .\emsdk_env.bat
+    mkdir $(Build.BinariesDirectory)\tests
+    cd $(Build.BinariesDirectory)\tests
+    $env:Path += ";$(WIN_QT_BINDIR)"
+    echo "Add Qt/qmake to PATH at $(WIN_QT_BINDIR):" 
+    echo "$env:Path"
+    if ('$(QT_VERSION)' -like '6*') {
+      7z x $(Build.SourcesDirectory)\ci\openglwindow_qt6.7z
+      echo "Inlined qmake.bat command is: $(WIN_AUTODESK_QT_BINDIR)\qmake -qtconf $(WIN_QT_BINDIR)\target_qt.conf $(Build.BinariesDirectory)\tests\openglwindow"
+      $(WIN_AUTODESK_QT_BINDIR)\qmake.exe -qtconf "$(WIN_QT_BINDIR)\target_qt.conf" $(Build.BinariesDirectory)\tests\openglwindow
+    } else {
+      7z x $(Build.SourcesDirectory)\ci\openglwindow.7z
+      echo "Qt5: run qmake.exe"
+      qmake $(Build.BinariesDirectory)\tests\openglwindow
+    }
+    if ($false -eq $?) {
+      Write-Error "qmake failed."
+      Write-Host "##vso[task.logissue type=error]qmake failed."
+      exit(1)
+    }
+    make
+    if ($false -eq $?) {
+      Write-Error "make failed."
+      Write-Host "##vso[task.logissue type=error]nmake failed."
+      exit(1)
+    }
+  workingDirectory: $(Build.BinariesDirectory)
+  condition: |
+    and(
+      startsWith( variables['ARCH'], 'wasm_' ),
+      eq( variables['Agent.OS'], 'Windows_NT' )
+    )
+  displayName: 'Build WebAssembler sample project on windows'


Test patch:
diff --git a/tests/test_cli.py b/tests/test_cli.py
index a6577f9..10266d8 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -1,4 +1,3 @@
-import platform
 import re
 import sys
 from pathlib import Path
@@ -16,9 +15,8 @@ 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-commercial,install-doc,install-example,"
-        "install-src,"
-        "list-qt,list-qt-commercial,list-tool,list-doc,list-example,list-src,help,version}\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"
@@ -34,9 +32,8 @@ def expected_help(actual, prefix=None):
         "  install-* subcommands are commands that install components\n"
         "  list-* subcommands are commands that show available components\n"
         "\n"
-        "  {install-qt,install-tool,install-qt-commercial,install-doc,install-example,"
-        "install-src,list-qt,list-qt-commercial,"
-        "list-tool,list-doc,list-example,list-src,help,version}\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",
     )
@@ -523,29 +520,3 @@ def test_get_autodesktop_dir_and_arch_non_android(
             ), "Expected autodesktop install message."
         elif expect["instruct"]:
             assert any("You can install" in line for line in err_lines), "Expected install instruction message."
-
-
-@pytest.mark.parametrize(
-    "cmd, expected_arch, expected_err",
-    [
-        pytest.param(
-            "install-qt-commercial desktop {} 6.8.0",
-            {"windows": "win64_msvc2022_64", "linux": "linux_gcc_64", "mac": "clang_64"},
-            "No Qt account credentials found. Either provide --user and --password or",
-        ),
-    ],
-)
-def test_cli_login_qt_commercial(capsys, monkeypatch, cmd, expected_arch, expected_err):
-    """Test commercial Qt installation command"""
-    # Detect current platform
-    current_platform = platform.system().lower()
-    arch = expected_arch[current_platform]
-    cmd = cmd.format(arch)
-
-    cli = Cli()
-    cli._setup_settings()
-    result = cli.run(cmd.split())
-
-    _, err = capsys.readouterr()
-    assert str(err).find(expected_err)
-    assert not result == 0
diff --git a/tests/test_commercial.py b/tests/test_commercial.py
new file mode 100644
index 0000000..d922b37
--- /dev/null
+++ b/tests/test_commercial.py
@@ -0,0 +1,375 @@
+import os
+import shutil
+import sys
+from pathlib import Path
+from tempfile import TemporaryDirectory
+from typing import Dict, List, Optional
+
+import pytest
+import requests
+
+from aqt.commercial import CommercialInstaller, QtPackageInfo, QtPackageManager
+from aqt.exceptions import DiskAccessNotPermitted
+from aqt.helper import Settings, get_qt_account_path
+from aqt.installer import Cli
+from aqt.metadata import Version
+
+
+class CompletedProcess:
+    def __init__(self, args, returncode):
+        self.args = args
+        self.returncode = returncode
+        self.stdout = None
+        self.stderr = None
+
+
+# Test data
+MOCK_XML_RESPONSE = """<?xml version="1.0" encoding="UTF-8"?>
+<availablepackages>
+    <package name="qt.qt6.680.gcc_64" displayname="Desktop gcc" version="6.8.0-0-202312011"/>
+    <package name="qt.qt6.680.addons.qtquick3d" displayname="Qt Quick 3D" version="6.8.0-0-202312011"/>
+</availablepackages>"""
+
+TEST_EMAIL = os.getenv("AQT_TEST_EMAIL")
+TEST_PASSWORD = os.getenv("AQT_TEST_PASSWORD")
+
+
+class MockResponse:
+    def __init__(self, status_code: int = 200, content: bytes = b"", text: str = "", headers: Dict = None):
+        self.status_code = status_code
+        self.content = content
+        self.text = text
+        self.headers = headers or {}
+        self.ok = status_code == 200
+
+    def raise_for_status(self):
+        if not self.ok:
+            raise requests.HTTPError(f"HTTP Error: {self.status_code}")
+
+    def iter_content(self, chunk_size=None):
+        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(
+        target="desktop",
+        arch="gcc_64",
+        version="6.8.0",
+        username=TEST_EMAIL,
+        password=TEST_PASSWORD,
+        output_dir="./test_output",
+    )
+
+
+@pytest.mark.enable_socket
+@pytest.mark.parametrize(
+    "cmd, expected_arch, expected_err",
+    [
+        pytest.param(
+            "install-qt-official desktop {} 6.8.0",
+            {"windows": "win64_msvc2022_64", "linux": "linux_gcc_64", "mac": "clang_64"},
+            "No Qt account credentials found",
+        ),
+    ],
+)
+def test_cli_login_qt_commercial(capsys, monkeypatch, cmd, expected_arch, expected_err):
+    """Test commercial Qt installation command"""
+    # Detect current platform
+    current_platform = sys.platform.lower()
+    arch = expected_arch[current_platform]
+    cmd = cmd.format(arch)
+
+    if get_qt_account_path().exists():
+        os.remove(get_qt_account_path())
+
+    cli = Cli()
+    cli._setup_settings()
+    cli.run(cmd.split())
+
+    out, err = capsys.readouterr()
+    assert expected_err in err or expected_err in out
+
+
+def test_package_manager_init():
+    """Test QtPackageManager initialization"""
+    manager = QtPackageManager(
+        arch="gcc_64",
+        version=Version("6.8.0"),
+        target="desktop",
+        username=TEST_EMAIL,
+        password=TEST_PASSWORD,
+    )
+    assert manager.arch == "gcc_64"
+    assert str(manager.version) == "6.8.0"
+    assert manager.target == "desktop"
+    assert manager.username == TEST_EMAIL
+    assert manager.password == TEST_PASSWORD
+
+
+@pytest.mark.parametrize(
+    "xml_content, expected_packages",
+    [
+        (
+            MOCK_XML_RESPONSE,
+            [
+                QtPackageInfo(name="qt.qt6.680.gcc_64", displayname="Desktop gcc", version="6.8.0-0-202312011"),
+                QtPackageInfo(name="qt.qt6.680.addons.qtquick3d", displayname="Qt Quick 3D", version="6.8.0-0-202312011"),
+            ],
+        )
+    ],
+)
+def test_parse_packages_xml(xml_content: str, expected_packages: List[QtPackageInfo]):
+    """Test parsing of package XML data"""
+    manager = QtPackageManager(arch="gcc_64", version=Version("6.8.0"), target="desktop")
+    manager._parse_packages_xml(xml_content)
+
+    assert len(manager.packages) == len(expected_packages)
+    for actual, expected in zip(manager.packages, expected_packages):
+        assert actual.name == expected.name
+        assert actual.displayname == expected.displayname
+        assert actual.version == expected.version
+
+
+def test_commercial_installer_auto_answers():
+    """Test generation of auto-answer options"""
+    auto_answers = CommercialInstaller.get_auto_answers()
+    assert "OperationDoesNotExistError=Ignore" in auto_answers
+    assert "OverwriteTargetDirectory=No" in auto_answers
+    assert "telemetry-question=No" in auto_answers
+
+
+@pytest.mark.parametrize(
+    "installer_path, override, username, password, output_dir, no_unattended, expected_cmd",
+    [
+        (
+            "/path/to/installer",
+            None,
+            "user",
+            "pass",
+            "./output",
+            False,
+            [
+                "/path/to/installer",
+                "--accept-licenses",
+                "--accept-obligations",
+                "--confirm-command",
+                "--email",
+                "user",
+                "--pw",
+                "pass",
+                "--root",
+                str(Path("./output").absolute()),
+                "--auto-answer",
+                CommercialInstaller.get_auto_answers(),
+            ],
+        ),
+        (
+            "/path/to/installer",
+            ["--override", "arg1", "arg2"],
+            None,
+            None,
+            None,
+            True,
+            ["/path/to/installer", "--override", "arg1", "arg2"],
+        ),
+    ],
+)
+def test_build_command(
+    installer_path: str,
+    override: Optional[List[str]],
+    username: Optional[str],
+    password: Optional[str],
+    output_dir: Optional[str],
+    no_unattended: bool,
+    expected_cmd: List[str],
+):
+    """Test building of installer command"""
+    cmd = CommercialInstaller.build_command(
+        installer_path,
+        override=override,
+        username=username,
+        password=password,
+        output_dir=output_dir,
+        no_unattended=no_unattended,
+    )
+    assert cmd == expected_cmd
+
+
+@pytest.mark.enable_socket
+def test_commercial_installer_download(monkeypatch, commercial_installer):
+    """Test downloading of commercial installer"""
+
+    def mock_requests_get(*args, **kwargs):
+        return MockResponse(content=b"installer_content")
+
+    monkeypatch.setattr(requests, "get", mock_requests_get)
+
+    with TemporaryDirectory() as temp_dir:
+        target_path = Path(temp_dir) / "qt-installer"
+        commercial_installer.download_installer(target_path, timeout=60)
+        assert target_path.exists()
+        assert target_path.read_bytes() == b"installer_content"
+
+
+@pytest.mark.parametrize(
+    "modules, expected_command",
+    [
+        (None, ["install", "qt.qt6.680.gcc_64"]),
+        (["qtquick3d"], ["install", "qt.qt6.680.gcc_64", "qt.qt6.680.addons.qtquick3d"]),
+        (["all"], ["install", "qt.qt6.680.gcc_64", "qt.qt6.680.addons.qtquick3d"]),
+    ],
+)
+def test_get_install_command(monkeypatch, modules: Optional[List[str]], expected_command: List[str]):
+    """Test generation of install commands"""
+    manager = QtPackageManager(arch="gcc_64", version=Version("6.8.0"), target="desktop")
+
+    def mock_gather_packages(self, installer_path: str) -> None:
+        self.packages = [
+            QtPackageInfo(name="qt.qt6.680.gcc_64", displayname="Desktop gcc", version="6.8.0"),
+            QtPackageInfo(name="qt.qt6.680.addons.qtquick3d", displayname="Qt Quick 3D", version="6.8.0"),
+        ]
+
+    monkeypatch.setattr(QtPackageManager, "gather_packages", mock_gather_packages)
+
+    command = manager.get_install_command(modules, "./temp")
+    assert command == expected_command
+
+
+@pytest.mark.enable_socket
+@pytest.mark.parametrize(
+    "cmd, arch_dict, details, expected_command",
+    [
+        (
+            "install-qt-official desktop {} 6.8.1 " "--outputdir ./install-qt-official --email {} --pw {}",
+            {"windows": "win64_msvc2022_64", "linux": "linux_gcc_64", "mac": "clang_64"},
+            ["./install-qt-official", "qt6", "681"],
+            "qt-unified-{}-x64-online.run --email ******** --pw ******** --root {} "
+            "--accept-licenses --accept-obligations "
+            "--confirm-command "
+            "--auto-answer OperationDoesNotExistError=Ignore,OverwriteTargetDirectory=No,"
+            "stopProcessesForUpdates=Cancel,installationErrorWithCancel=Cancel,installationErrorWithIgnore=Ignore,"
+            "AssociateCommonFiletypes=Yes,telemetry-question=No install qt.{}.{}.{}",
+        ),
+        (
+            "install-qt-official desktop {} 6.8.1 --outputdir ./install-qt-official --email {} --pw {}",
+            {"windows": "win64_msvc2022_64", "linux": "linux_gcc_64", "mac": "clang_64"},
+            ["./install-qt-official", "qt6", "681"],
+            "qt-unified-{}-x64-online.run --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(
+    capsys, monkeypatch, cmd: str, arch_dict: dict[str, str], details: list[str], expected_command: str
+) -> None:
+    """Test commercial Qt installation command"""
+
+    def mock_safely_run(*args, **kwargs):
+        return CompletedProcess(args=args[0], returncode=0)
+
+    monkeypatch.setattr("aqt.commercial.safely_run", mock_safely_run)
+
+    current_platform = sys.platform.lower()
+    arch = arch_dict[current_platform]
+
+    abs_out = Path(details[0]).absolute()
+
+    # Get the email and password from the test parameters
+    email = TEST_EMAIL
+    password = TEST_PASSWORD
+
+    formatted_cmd = cmd.format(arch, email, password)
+    formatted_expected = expected_command.format(current_platform, abs_out, *details[1:], arch)
+
+    cli = Cli()
+    cli._setup_settings()
+
+    # First test the normal installation command
+    try:
+        cli.run(formatted_cmd.split())
+    except AttributeError:
+        out = " ".join(capsys.readouterr())
+        assert str(out).find(formatted_expected) >= 0
+
+    abs_out.joinpath(f"6.8.{str(details[2])[-1]}").mkdir(exist_ok=True, parents=True)
+
+    # Create a new command with the temp directory
+    new_cmd = (
+        f"install-qt-official desktop {arch} 6.8.{str(details[2])[-1]} --outputdir {abs_out} --email {email} "
+        f"--pw {password}"
+    )
+
+    # This should raise DiskAccessNotPermitted only for the first test (680)
+    if details[2] == "680":
+        with pytest.raises(DiskAccessNotPermitted) as exc_info:
+            cli.run(new_cmd.split())
+        assert "Target directory" in str(exc_info.value)
+        assert "already exists" in str(exc_info.value)
+    else:
+        cli.run(new_cmd.split())
+
+    def modify_qt_config(content):
+        """
+        Takes content of INI file as string and returns modified content
+        """
+        lines = content.splitlines()
+        in_qt_commercial = False
+        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:
+                line = "overwrite_target_directory : Yes"
+            elif in_qt_commercial and "overwrite_target_directory : Yes" in line:
+                line = "overwrite_target_directory : No"
+
+            modified.append(line)
+
+        return "\n".join(modified)
+
+    script_dir = os.path.dirname(os.path.abspath(__file__))
+    config_path = os.path.join(script_dir, "../aqt/settings.ini")
+
+    with open(config_path, "r") as f:
+        content = f.read()
+
+    modified_content = modify_qt_config(content)
+
+    with open(config_path, "w") as f:
+        f.write(modified_content)
+        Settings._initialize()
+
+    shutil.rmtree(abs_out)
+
+
+@pytest.mark.parametrize(
+    "args, expected_error",
+    [
+        (["list-qt-official", "--bad-flag"], "usage: aqt [-h] [-c CONFIG]"),
+    ],
+)
+def test_list_qt_commercial_errors(capsys, args, expected_error):
+    """Test error handling in list-qt-official command"""
+    cli = Cli()
+    with pytest.raises(SystemExit):
+        cli.run(args)
+    _, err = capsys.readouterr()
+    assert expected_error in err
diff --git a/tests/test_install.py b/tests/test_install.py
index 02c0d78..585e271 100644
--- a/tests/test_install.py
+++ b/tests/test_install.py
@@ -2054,104 +2054,3 @@ def test_installer_passes_base_to_metadatafactory(
         sys.stderr.write(err)
 
         assert expect_out.match(err), err
-
-
-class CompletedProcess:
-    def __init__(self, args, returncode):
-        self.args = args
-        self.returncode = returncode
-        self.stdout = None
-        self.stderr = None
-
-
-@pytest.mark.enable_socket
-@pytest.mark.parametrize(
-    "cmd, arch_dict, details, expected_command",
-    [
-        (
-            "install-qt-commercial desktop {} 6.8.0 " "--outputdir ./install-qt-commercial " "--user {} --password {}",
-            {"windows": "win64_msvc2022_64", "linux": "linux_gcc_64", "mac": "clang_64"},
-            ["./install-qt-commercial", "qt6", "680"],
-            "qt-unified-{}-x64-online.run --email ******** --pw ******** --root {} "
-            "--accept-licenses --accept-obligations "
-            "--confirm-command "
-            "--auto-answer OperationDoesNotExistError=Ignore,OverwriteTargetDirectory=No,"
-            "stopProcessesForUpdates=Cancel,installationErrorWithCancel=Cancel,installationErrorWithIgnore=Ignore,"
-            "AssociateCommonFiletypes=Yes,telemetry-question=No install qt.{}.{}.{}",
-        ),
-        (
-            "install-qt-commercial desktop {} 6.8.1 " "--outputdir ./install-qt-commercial " "--user {} --password {}",
-            {"windows": "win64_msvc2022_64", "linux": "linux_gcc_64", "mac": "clang_64"},
-            ["./install-qt-commercial", "qt6", "681"],
-            "qt-unified-{}-x64-online.run --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(
-    capsys, monkeypatch, cmd: str, arch_dict: dict[str, str], details: list[str], expected_command: str
-) -> None:
-    """Test commercial Qt installation command"""
-
-    # Mock subprocess.run instead of run_static_subprocess_dynamically
-    def mock_subprocess_run(*args, **kwargs):
-        # This will be called instead of the real subprocess.run
-        return CompletedProcess(args=args[0], returncode=0)
-
-    # Patch subprocess.run directly
-    monkeypatch.setattr("subprocess.run", mock_subprocess_run)
-
-    current_platform = sys.platform.lower()
-    arch = arch_dict[current_platform]
-
-    abs_out = Path(details[0]).absolute()
-
-    formatted_cmd = cmd.format(arch, "vofab76634@gholar.com", "WxK43TdWCTmxsrrpnsWbjPfPXVq3mtLK")
-    formatted_expected = expected_command.format(current_platform, abs_out, *details[1:], arch)
-
-    cli = Cli()
-    cli._setup_settings()
-
-    try:
-        cli.run(formatted_cmd.split())
-    except AttributeError:
-        out = " ".join(capsys.readouterr())
-        assert str(out).find(formatted_expected) >= 0
-
-    def modify_qt_config(content):
-        """
-        Takes content of INI file as string and returns modified content
-        """
-        lines = content.splitlines()
-        in_qt_commercial = False
-        modified = []
-
-        for line in lines:
-            # Check if we're entering qtcommercial section
-            if line.strip() == "[qtcommercial]":
-                in_qt_commercial = True
-
-            # If in qtcommercial section, look for the target line
-            if 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"
-
-            modified.append(line)
-
-        return "\n".join(modified)
-
-    script_dir = os.path.dirname(os.path.abspath(__file__))
-    config_path = os.path.join(script_dir, "../aqt/settings.ini")
-
-    with open(config_path, "r") as f:
-        content = f.read()
-
-    modified_content = modify_qt_config(content)
-
-    with open(config_path, "w") as f:
-        f.write(modified_content)
diff --git a/tests/test_list.py b/tests/test_list.py
index 15d0824..be4ce88 100644
--- a/tests/test_list.py
+++ b/tests/test_list.py
@@ -1336,19 +1336,3 @@ def test_find_installed_qt_mingw_dir(expected_result: str, installed_files: List
 
         actual_result = QtRepoProperty.find_installed_desktop_qt_dir(host, base_path, Version(qt_ver))
         assert (actual_result.name if actual_result else None) == expected_result
-
-
-# Test error cases
-@pytest.mark.parametrize(
-    "args, expected_error",
-    [
-        (["list-qt-commercial", "--bad-flag"], "usage: aqt [-h] [-c CONFIG]"),
-    ],
-)
-def test_list_qt_commercial_errors(capsys, args, expected_error):
-    """Test error handling in list-qt-commercial command"""
-    cli = Cli()
-    with pytest.raises(SystemExit):
-        cli.run(args)
-    _, err = capsys.readouterr()
-    assert expected_error in err