Repository: miurahr/aqtinstall
Pull request #888 (2025-01-30 14:58:37)

chore: support trusted publisher to publish with OIDC
PyPI encourage us to use "Trusted Publishing". This is a part of migration.


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/.github/workflows/publish-to-pypi.yml b/.github/workflows/publish-to-pypi.yml
index 430490f..85701c7 100644
--- a/.github/workflows/publish-to-pypi.yml
+++ b/.github/workflows/publish-to-pypi.yml
@@ -10,8 +10,8 @@ on:
       - v*
 
 jobs:
-  build-n-publish:
-    name: Build and publish Python 🐍 distributions 📦 to PyPI
+  build:
+    name: Build package
     runs-on: ubuntu-22.04
     steps:
       - name: Checkout 🛎️
@@ -30,6 +30,23 @@ jobs:
         run: python -m build ./
       - name: twine check
         run: python -m twine check dist/*
+      - name: upload dists
+        uses: actions/upload-artifact@v4
+        with:
+          name: release-dists
+          path: dist/
+
+  publish:
+    name: publish Python 🐍 distributions 📦 to PyPI
+    runs-on: ubuntu-22.04
+    permissions:
+      id-token: write
+    steps:
+      - name: Retrieve release distributions
+        uses: actions/download-artifact@v4
+        with:
+          name: release-dists
+          path: dist/
       - name: Publish distribution 📦 to Test PyPI when releases branch
         if: ${{ startsWith(github.event.ref, 'refs/heads/releases') }}
         uses: pypa/gh-action-pypi-publish@release/v1
@@ -39,5 +56,3 @@ jobs:
       - name: Publish distribution 📦 to PyPI
         if: ${{ startsWith(github.event.ref, 'refs/tags') }}
         uses: pypa/gh-action-pypi-publish@release/v1
-        with:
-          password: ${{ secrets.pypi_password }}
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