diff --git a/aqt/commercial.py b/aqt/commercial.py index 4ba7265..8e1dc5e 100644 --- a/aqt/commercial.py +++ b/aqt/commercial.py @@ -60,6 +60,7 @@ class QtPackageManager: self.packages: List[QtPackageInfo] = [] self.username = username self.password = password + self.logger = getLogger("aqt.commercial") def _get_cache_dir(self) -> Path: """Create and return cache directory path.""" @@ -141,6 +142,7 @@ class QtPackageManager: cmd.append(base_package) try: + self.logger.info(f"Running: {cmd}") output = safely_run_save_output(cmd, Settings.qt_installer_timeout) # Handle both string and CompletedProcess outputs @@ -156,8 +158,7 @@ class QtPackageManager: self._save_to_cache() else: # Log the actual output for debugging - logger = getLogger("aqt.helper") - logger.debug(f"Installer output: {output_text}") + self.logger.debug(f"Installer output: {output_text}") raise RuntimeError("Failed to find package information in installer output") except Exception as e: @@ -217,6 +218,7 @@ class CommercialInstaller: override: Optional[List[str]] = None, modules: Optional[List[str]] = None, no_unattended: bool = False, + dry_run: bool = False, ): self.override = override self.target = target @@ -227,6 +229,7 @@ class CommercialInstaller: self.base_url = base_url self.modules = modules self.no_unattended = no_unattended + self.dry_run = dry_run # Extract credentials from override if present if override: @@ -330,13 +333,12 @@ class CommercialInstaller: raise DiskAccessNotPermitted(f"Failed to remove existing version directory {version_dir}: {str(e)}") # Setup temp directory - import shutil - temp_dir = Settings.qt_installer_temp_path temp_path = Path(temp_dir) - if temp_path.exists(): - shutil.rmtree(temp_dir) - temp_path.mkdir(parents=True, exist_ok=True) + if not temp_path.exists(): + temp_path.mkdir(parents=True, exist_ok=True) + else: + Settings.qt_installer_cleanup() installer_path = temp_path / self._installer_filename self.logger.info(f"Downloading Qt installer to {installer_path}") @@ -371,8 +373,16 @@ class CommercialInstaller: install_cmd = self.package_manager.get_install_command(self.modules, temp_dir) cmd = [*base_cmd, *install_cmd] - self.logger.info(f"Running: {cmd}") - safely_run(cmd, Settings.qt_installer_timeout) + log_cmd = cmd.copy() + for i in range(len(log_cmd) - 1): + if log_cmd[i] == "--email" or log_cmd[i] == "--pw": + log_cmd[i + 1] = "***" + + if not self.dry_run: + self.logger.info(f"Running: {log_cmd}") + safely_run(cmd, Settings.qt_installer_timeout) + else: + self.logger.info(f"Would run: {log_cmd}") except Exception as e: self.logger.error(f"Installation failed: {str(e)}") diff --git a/aqt/helper.py b/aqt/helper.py index 8ba512f..8941585 100644 --- a/aqt/helper.py +++ b/aqt/helper.py @@ -22,6 +22,7 @@ import binascii import hashlib import logging.config import os +import platform import posixpath import secrets import shutil @@ -40,6 +41,7 @@ from xml.etree.ElementTree import Element import humanize import requests import requests.adapters +from bs4 import BeautifulSoup from defusedxml import ElementTree from aqt.exceptions import ( @@ -62,6 +64,23 @@ def get_os_name() -> str: raise ValueError(f"Unsupported operating system: {system}") +def get_os_arch() -> str: + """ + Returns a simplified os-arch string for the current system + """ + os_name = get_os_name() + + machine = platform.machine().lower() + if machine in ["x86_64", "amd64"]: + arch = "x64" + elif machine in ["arm64", "aarch64"]: + arch = "arm64" + else: + arch = "x64" # Default to x64 for unknown architectures + + return f"{os_name}-{arch}" + + def get_qt_local_folder_path() -> Path: os_name = get_os_name() if os_name == "windows": @@ -76,13 +95,59 @@ def get_qt_account_path() -> Path: return get_qt_local_folder_path() / "qtaccount.ini" +def get_qt_installers() -> dict[str, str]: + """ + Extracts Qt installer information from {Settings.baseurl}/official_releases/online_installers/ + Maps OS types and architectures to their respective installer filenames + Returns: + dict: Mapping of OS identifiers to installer filenames with appropriate aliases + """ + url = f"{Settings.baseurl}/official_releases/online_installers/" + + try: + response = requests.get(url, timeout=Settings.response_timeout) + response.raise_for_status() + + soup = BeautifulSoup(response.text, "html.parser") + + installers = {} + + os_types = ["windows", "linux", "mac"] + + for link in soup.find_all("a"): + filename = link.text.strip() + + if "Parent Directory" in filename or not any(ext in filename.lower() for ext in [".exe", ".dmg", ".run"]): + continue + + for os_type in os_types: + if os_type.lower() in filename.lower(): + # Found an OS match, now look for architecture + if "arm64" in filename.lower(): + installers[f"{os_type}-arm64"] = filename + elif "x64" in filename.lower(): + installers[f"{os_type}-x64"] = filename + # Also add generic OS entry for x64 variants of Windows and Linux + if os_type in ["windows", "linux"]: + installers[os_type] = filename + else: + # Handle case with no explicit architecture + # Most likely for macOS which might just say "mac" without arch + installers[os_type] = filename + + return installers + + except requests.exceptions.RequestException as e: + print(f"Error fetching installer data: {e}") + return {} + except Exception as e: + print(f"Unexpected error processing installer data: {e}") + return {} + + def get_qt_installer_name() -> str: - installer_dict = { - "windows": "qt-unified-windows-x64-online.exe", - "mac": "qt-unified-mac-x64-online.dmg", - "linux": "qt-unified-linux-x64-online.run", - } - return installer_dict[get_os_name()] + installer_dict = get_qt_installers() + return installer_dict[get_os_arch()] def get_qt_installer_path() -> Path: @@ -427,8 +492,13 @@ class SettingsClass: logging.info(f"Cache folder: {self.qt_installer_cache_path}") logging.info(f"Temp folder: {self.qt_installer_temp_path}") - if Path(self.qt_installer_temp_path).exists(): - shutil.rmtree(self.qt_installer_temp_path) + + temp_dir = self.qt_installer_temp_path + temp_path = Path(temp_dir) + if not temp_path.exists(): + temp_path.mkdir(parents=True, exist_ok=True) + else: + self.qt_installer_cleanup() def _get_config(self) -> ConfigParser: """Safe getter for config that ensures it's initialized.""" @@ -616,10 +686,12 @@ class SettingsClass: return self._get_config().getboolean("qtofficial", "unattended", fallback=True) def qt_installer_cleanup(self) -> None: - """Control whether to use unattended installation flags.""" - import shutil - - shutil.rmtree(self.qt_installer_temp_path) + """Clean tmp folder.""" + for item in Path(self.qt_installer_temp_path).iterdir(): + if item.is_dir(): + shutil.rmtree(item) + else: + item.unlink() Settings = SettingsClass() @@ -647,7 +719,7 @@ def safely_run_save_output(cmd: List[str], timeout: int) -> Any: raise -def extract_auth(args: List[str]) -> Tuple[str | None, str | None, List[str] | None]: +def extract_auth(args: List[str]) -> Tuple[Union[str, None], Union[str, None], Union[List[str], None]]: username = None password = None i = 0 diff --git a/aqt/installer.py b/aqt/installer.py index 2d54cfa..abb3553 100644 --- a/aqt/installer.py +++ b/aqt/installer.py @@ -368,6 +368,62 @@ class Cli: else: qt_version = args.qt_version Cli._validate_version_str(qt_version) + + if hasattr(args, "use_official_installer") and args.use_official_installer is not None: + + if len(args.use_official_installer) not in [0, 2]: + raise CliInputError( + "When providing arguments to --use-official-installer, exactly 2 arguments are required: " + "--use-official-installer email password" + ) + + self.logger.info("Using official Qt installer") + + commercial_args = InstallArgParser() + + # Core parameters required by install-qt-official + commercial_args.target = args.target + commercial_args.arch = self._set_arch( + args.arch, args.host, args.target, getattr(args, "qt_version", getattr(args, "qt_version_spec", "")) + ) + + commercial_args.version = qt_version + + email = None + password = None + if len(args.use_official_installer) == 2: + email, password = args.use_official_installer + self.logger.info("Using credentials provided with --use-official-installer") + + # Optional parameters + commercial_args.email = email or getattr(args, "email", None) + commercial_args.pw = password or getattr(args, "pw", None) + commercial_args.outputdir = args.outputdir + commercial_args.modules = args.modules + commercial_args.base = getattr(args, "base", None) + commercial_args.dry_run = getattr(args, "dry_run", False) + commercial_args.override = None + + ignored_options = [] + if getattr(args, "noarchives", False): + ignored_options.append("--noarchives") + if getattr(args, "autodesktop", False): + ignored_options.append("--autodesktop") + if getattr(args, "archives", None): + ignored_options.append("--archives") + if getattr(args, "timeout", False): + ignored_options.append("--timeout") + if getattr(args, "keep", False): + ignored_options.append("--keep") + if getattr(args, "archive_dest", False): + ignored_options.append("--archive_dest") + + if ignored_options: + self.logger.warning("Options ignored because you requested the official installer:") + self.logger.warning(", ".join(ignored_options)) + + return self.run_install_qt_commercial(commercial_args, print_version=False) + archives = args.archives if args.noarchives: if modules is None: @@ -683,9 +739,10 @@ class Cli: ) show_list(meta) - def run_install_qt_commercial(self, args: InstallArgParser) -> None: + def run_install_qt_commercial(self, args: InstallArgParser, print_version: Optional[bool] = True) -> None: """Execute commercial Qt installation""" - self.show_aqt_version() + if print_version: + self.show_aqt_version() try: if args.override: @@ -700,6 +757,7 @@ class Cli: no_unattended=not Settings.qt_installer_unattended, username=username or args.email, password=password or args.pw, + dry_run=args.dry_run, ) else: if not all([args.target, args.arch, args.version]): @@ -716,12 +774,13 @@ class Cli: base_url=args.base if args.base is not None else Settings.baseurl, no_unattended=not Settings.qt_installer_unattended, modules=args.modules, + dry_run=args.dry_run, ) commercial_installer.install() Settings.qt_installer_cleanup() except Exception as e: - self.logger.error(f"Error installing official installer {str(e)}") + self.logger.error(f"Error installing official installer: {str(e)}") finally: self.logger.info("Done") @@ -793,6 +852,16 @@ class Cli: "required. When enabled, this option installs the required desktop version automatically. " "It has no effect when the desktop installation is not required.", ) + install_qt_parser.add_argument( + "--use-official-installer", + nargs="*", + default=None, + metavar=("EMAIL", "PASSWORD"), + help="Use the official Qt installer for installation instead of the aqt downloader. " + "Can be used without arguments or with email and password: --use-official-installer email password. " + "This redirects to install-qt-official. " + "Arguments not compatible with the official installer will be ignored.", + ) def _set_install_tool_parser(self, install_tool_parser): install_tool_parser.set_defaults(func=self.run_install_tool) @@ -889,14 +958,12 @@ class Cli: self.show_aqt_version() # Create temporary directory for installer - import shutil - from pathlib import Path - temp_dir = Settings.qt_installer_temp_path temp_path = Path(temp_dir) - if temp_path.exists(): - shutil.rmtree(temp_dir) - temp_path.mkdir(parents=True, exist_ok=True) + if not temp_path.exists(): + temp_path.mkdir(parents=True, exist_ok=True) + else: + Settings.qt_installer_cleanup() # Get installer based on OS installer_filename = get_qt_installer_name() diff --git a/docs/authors.rst b/docs/authors.rst index 9cd33a3..e8c116b 100644 --- a/docs/authors.rst +++ b/docs/authors.rst @@ -16,6 +16,7 @@ All contributors, listed alphabetically, are: * Adrian Eddy (fix the case of Android 6.7.0) * Alberto Mardegan(ignore_hash option) +* Alexandre @Kidev Poumaroux (official installer option, WASM fixes for Qt6.7.x) * Andrew Wason (support arm64) * Andrei Yankovich (tools ifw installation) * Aurélien Gâteau (patching to qmake) @@ -51,4 +52,4 @@ All contributors, listed alphabetically, are: * @ypnos (Documents) and many other participants and contributors. -If you find a missing name to record, please feel free to tell me. \ No newline at end of file +If you find a missing name to record, please feel free to tell me. diff --git a/docs/official.rst b/docs/official.rst index db17740..1d637d6 100644 --- a/docs/official.rst +++ b/docs/official.rst @@ -44,50 +44,6 @@ Options * If none are inputed, the package ``qtX.Y.Z-essentials`` is downloaded (default Qt install, includes ``qtcreator``...) * If ``all`` is inputed, the package ``qtX.Y.Z-full`` is downloaded (includes everything) -.. dropdown:: Click to see all the special packages (for Linux Qt 6.8.1) - - .. code-block:: text - - Name: qt6.8.1-essentials - Display name: Qt 6.8.1 Linux x86_64 Essential Components - Description: Qt 6.8.1 Linux x86_64 Essential Libraries, Headers, and Tools - Version: 6.8.1 - Components: qt.qt6.681.linux_gcc_64 - - Name: qt6.8.1-essentials-dev - Display name: Qt 6.8.1 Linux x86_64 Essential Components (dev) - Description: Qt 6.8.1 Linux x86_64 Essential Libraries, Headers, and Tools (dev) - Version: 6.8.1 - Required aliases: qt6.8.1-essentials - - Name: qt6.8.1-full - Display name: Qt 6.8.1 Linux x86_64 All Components with Sources - Description: Qt 6.8.1 Linux x86_64 All Libraries, Headers, Tools, and Sources - Version: 6.8.1 - Components: qt.qt6.681.src,extensions.qtwebengine.681.src - Required aliases: qt6.8.1-essentials,qt6.8.1-addons,qt6.8.1-extensions - Optional components: extensions.qtinsighttracker.681.src - - Name: qt6.8.1-full-dbg - Display name: Qt 6.8.1 Linux x86_64 All Components with Sources and Debug Information Files - Description: Qt 6.8.1 Linux x86_64 All Libraries, Headers, Tools, Sources, and Debug Information Files (dev) - Version: 6.8.1 - Components: qt.qt6.681.debug_info.linux_gcc_64,qt.qt6.681.debug_info,extensions.qtwebengine.681.debug_information - Required aliases: qt6.8.1-full-dev - - Name: qt6.8.1-full-dev - Display name: Qt 6.8.1 Linux x86_64 All Components with Sources (dev) - Description: Qt 6.8.1 Linux x86_64 All Libraries, Headers, Tools, and Sources (dev) - Version: 6.8.1 - Required aliases: qt6.8.1-full - - Name: qt6.8.1-sdk - Display name: Qt 6.8.1 Linux x86_64 SDK - Description: Qt 6.8.1 Linux x86_64 SDK Tools (Qt Creator, Ninja, and CMake) - Version: 6.8.1 - Components: qt.tools.qtcreator_gui,qt.tools.cmake,qt.tools.ninja - Required aliases: qt6.8.1-full-dev,qt6.8.1-full-dbg - - ``--outputdir `` - Installation directory (default: current directory) - ``--override `` - Pass all remaining arguments directly to the Qt installer CLI @@ -106,7 +62,7 @@ Options - ``search_terms`` - Terms to search for in package names (grabs all that is not other options) Override Mode ------------- +---------------------- ``install-qt-official`` supports an override mode that passes all arguments after ``--override`` directly to the Qt installer CLI, and will ignore all the other params except ``--email`` and ``--pw`` if given prior to it .. code-block:: bash @@ -122,7 +78,7 @@ When using override mode: * `More info here `_ Examples --------- +-------------- .. code-block:: bash # Standard installation @@ -138,7 +94,7 @@ Examples aqt install-qt-official --override install qt.qt6.680.gcc_64 --email user@example.com --pw pass Advanced configs --------------- +-------------------------- The file located in ``./aqt/settings.ini`` can be edited in the ``[qtofficial]`` part to fine tune the official installer (`more details here `_): .. code-block:: ini diff --git a/tests/test_cli.py b/tests/test_cli.py index 10266d8..1a968e6 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -13,33 +13,10 @@ from aqt.metadata import MetadataFactory, SimpleSpec, Version def expected_help(actual, prefix=None): - expected = ( - "usage: aqt [-h] [-c CONFIG]\n" - " {install-qt,install-tool,install-qt-official,list-qt-official,install-doc,install-example," - "install-src,list-qt,list-tool,list-doc,list-example,list-src,help,version}\n" - " ...\n" - "\n" - "Another unofficial Qt Installer.\n" - "aqt helps you install Qt SDK, tools, examples and others\n" - "\n" - "option", - " -h, --help show this help message and exit\n" - " -c CONFIG, --config CONFIG\n" - " Configuration ini file.\n" - "\n" - "subcommands:\n" - " aqt accepts several subcommands:\n" - " install-* subcommands are commands that install components\n" - " list-* subcommands are commands that show available components\n" - "\n" - " {install-qt,install-tool,install-qt-official,list-qt-official,install-doc,install-example,install-src," - "list-qt,list-tool,list-doc,list-example,list-src,help,version}\n" - " Please refer to each help message by using '--help' " - "with each subcommand\n", - ) + expected = "usage: aqt [-h] [-c CONFIG]" if prefix is not None: - return actual.startswith(prefix + expected[0]) and actual.endswith(expected[1]) - return actual.startswith(expected[0]) and actual.endswith(expected[1]) + return actual.startswith(prefix + expected) + return actual.startswith(expected) def test_cli_help(capsys): diff --git a/tests/test_commercial.py b/tests/test_commercial.py index 2948589..f23fd6c 100644 --- a/tests/test_commercial.py +++ b/tests/test_commercial.py @@ -3,17 +3,15 @@ import shutil import sys from pathlib import Path from typing import Dict, List, Optional -from urllib.parse import urlparse import pytest import requests from aqt.commercial import CommercialInstaller, QtPackageInfo, QtPackageManager from aqt.exceptions import DiskAccessNotPermitted -from aqt.helper import Settings, download_installer, get_qt_account_path +from aqt.helper import Settings, download_installer, get_os_name, get_qt_account_path, get_qt_installer_name from aqt.installer import Cli from aqt.metadata import Version -from tests.test_helper import mocked_requests_get class CompletedProcess: @@ -51,15 +49,6 @@ class MockResponse: yield self.content -@pytest.fixture -def mock_settings(monkeypatch): - """Setup test settings""" - # Instead of trying to set properties directly, we should mock the property getter - monkeypatch.setattr(Settings, "qt_installer_timeout", property(lambda self: 60)) - monkeypatch.setattr(Settings, "qt_installer_cache_path", property(lambda self: str(Path.home() / ".qt" / "cache"))) - monkeypatch.setattr(Settings, "qt_installer_temp_path", property(lambda self: str(Path.home() / ".qt" / "temp"))) - - @pytest.fixture def commercial_installer(): return CommercialInstaller( @@ -206,23 +195,34 @@ def test_build_command( assert cmd == expected_cmd -def test_commercial_installer_download_sha256(tmp_path, monkeypatch, commercial_installer): - """Test downloading of commercial installer""" +@pytest.mark.enable_socket +@pytest.mark.parametrize( + "os, osarch, expected_suffix", + [ + pytest.param("windows", "windows", ".exe"), + pytest.param("windows", "windows-x64", ".exe"), + pytest.param("windows", "windows-arm64", ".exe"), + pytest.param("linux", "linux", ".run"), + pytest.param("linux", "linux-x64", ".run"), + pytest.param("linux", "linux-arm64", ".run"), + pytest.param("mac", "mac-x64", ".dmg"), + ], +) +def test_commercial_installer_names(monkeypatch, os, osarch, expected_suffix): + """Test installer names finder""" - def mock_getUrl(url, *args, **kwargs): - hash_filename = str(urlparse(url).path.split("/")[-1]) - assert hash_filename.endswith(".sha256") - filename = hash_filename[: -len(".sha256")] - return f"07b3ef4606b712923a14816b1cfe9649687e617d030fc50f948920d784c0b1cd {filename}" + monkeypatch.setattr("aqt.helper.get_os_arch", lambda: osarch) - monkeypatch.setattr("aqt.helper.getUrl", mock_getUrl) - monkeypatch.setattr(requests.Session, "get", mocked_requests_get) + installer_name = get_qt_installer_name() - target_path = tmp_path / "qt-installer" + assert installer_name.endswith(expected_suffix) - timeout = (Settings.connection_timeout, Settings.response_timeout) - download_installer(commercial_installer.base_url, commercial_installer._installer_filename, target_path, timeout) - assert target_path.exists() + if os == get_os_name() and osarch in ["windows", "linux", "mac-x64"]: + target_path = Settings.qt_installer_temp_path / Path(installer_name) + base_url = Settings.baseurl + timeout = (Settings.connection_timeout, Settings.response_timeout) + download_installer(base_url, installer_name, target_path, timeout) + assert True @pytest.mark.parametrize( @@ -275,6 +275,17 @@ def test_get_install_command(monkeypatch, modules: Optional[List[str]], expected "stopProcessesForUpdates=Cancel,installationErrorWithCancel=Cancel,installationErrorWithIgnore=Ignore," "AssociateCommonFiletypes=Yes,telemetry-question=No install qt.{}.{}.{}", ), + ( + "install-qt linux desktop 6.8.1 {} --outputdir ./install-qt-flag --use-official-installer {} {}", + {"windows": "win64_msvc2022_64", "linux": "linux_gcc_64", "mac": "clang_64"}, + ["./install-qt-official", "qt6", "681"], + "{} --email ******** --pw ******** --root {} " + "--accept-licenses --accept-obligations " + "--confirm-command " + "--auto-answer OperationDoesNotExistError=Ignore,OverwriteTargetDirectory=Yes," + "stopProcessesForUpdates=Cancel,installationErrorWithCancel=Cancel,installationErrorWithIgnore=Ignore," + "AssociateCommonFiletypes=Yes,telemetry-question=No install qt.{}.{}.{}", + ), ], ) def test_install_qt_commercial( @@ -301,7 +312,10 @@ def test_install_qt_commercial( password = TEST_PASSWORD formatted_cmd = cmd.format(arch, email, password) - formatted_expected = expected_command.format(current_platform, abs_out, *details[1:], arch) + if expected_command.startswith("install-qt-official"): + formatted_expected = expected_command.format(current_platform, abs_out, *details[1:], arch) + else: + formatted_expected = expected_command.format(get_qt_installer_name(), abs_out, *details[1:], arch) cli = Cli() cli._setup_settings() @@ -330,7 +344,7 @@ def test_install_qt_commercial( else: cli.run(new_cmd.split()) - def modify_qt_config(content): + def modify_qt_config(content, endwith): """ Takes content of INI file as string and returns modified content """ @@ -339,12 +353,12 @@ def test_install_qt_commercial( modified = [] for line in lines: - # Check if we're entering qtofficial section if line.strip() == "[qtofficial]": in_qt_commercial = True - # If in qtofficial section, look for the target line - if in_qt_commercial and "overwrite_target_directory : No" in line: + if endwith is not None and endwith in ["Yes", "No"] and "overwrite_target_directory : " in line: + line = f"overwrite_target_directory : {endwith}" + elif in_qt_commercial and "overwrite_target_directory : No" in line: line = "overwrite_target_directory : Yes" elif in_qt_commercial and "overwrite_target_directory : Yes" in line: line = "overwrite_target_directory : No" @@ -359,7 +373,10 @@ def test_install_qt_commercial( with open(config_path, "r") as f: content = f.read() - modified_content = modify_qt_config(content) + if expected_command.startswith("install-qt-official"): + modified_content = modify_qt_config(content, None) + else: + modified_content = modify_qt_config(content, "No") with open(config_path, "w") as f: f.write(modified_content) diff --git a/tests/test_install.py b/tests/test_install.py index 569da56..0f65a32 100644 --- a/tests/test_install.py +++ b/tests/test_install.py @@ -1694,7 +1694,8 @@ def test_install_qt6_wasm_autodesktop(monkeypatch, capsys, version, str_version, r"(?:INFO : Found extension .*?\n)*" r"(?:INFO : Downloading (?:qt[^\n]*|icu[^\n]*)\n" r"Finished installation of .*?\.7z in \d+\.\d+\n)*" - r"(?:INFO : Patching (?:/tmp/[^/]+|[A-Za-z]:[\\/].*?)/6\.8\.0/wasm_singlethread/bin/(?:qmake(?:6)?|qtpaths(?:6)?|target_qt\.conf)\n)*" + r"(?:INFO : Patching (?:/tmp/[^/]+|[A-Za-z]:[\\/].*?)/6\.8\.0/wasm_singlethread/bin/" + r"(?:qmake(?:6)?|qtpaths(?:6)?|target_qt\.conf)\n)*" r"INFO : \n" r"INFO : Autodesktop will now install linux desktop 6\.8\.0 linux_gcc_64 as required by Qt6-WASM\n" r"INFO : aqtinstall\(aqt\) v.*? on Python 3.*?\n"