mirror of
https://github.com/miurahr/aqtinstall.git
synced 2025-12-17 04:34:37 +03:00
Add support for commercial versions of Qt (#878)
* Add install-qt-commercial feature and tests * Make the auto-answers parameters, fix linter issues * Fork and execv instead of using subprocess * Return to simpler process execution method version * Fix test * Move commercial installer into its own file * Fix shadowing of symbol platform causing errors * Adapt test_cli for argparse format changes on py 3.13+ * Fix some errors, monkeypatch install test * Add --override super command * Properly handle --override and grab all the remaining commands when no quotes are given * Fix tests * Add base for modules, some niche features are not yet entirely implemented, and there are no updates to the testsuite * Fix some mistakes * Fix errors made with the monkeypatch, update Settings to make sure its init * Tests commercial (#20) * Full support of installation of all modules and addons * Add auto setup of cache folder for each OS, add unattended parameter * Fix settings folders * Add graceful error message for overwrite case * Fix windows issue * Hidden summon works * Remove both subprocess direct calls * Dipose of temp folder * Fix path issue * Add list-qt-commercial command * Fix help info * Make no params valid for list-qt-commercial * Fix lint errors, and param overflow when no args are passed to list * Fix search * Add tests for coverage, fix lint * Test for overwriting, and for cache usage coverage * Return to clean exec, ignoring CI fail to preserve code clarity * Fix parsing of subprocess.run output for some python versions * Make output more readable in console for list-qt-commercial * Forward email and password to list request for users without a qtaccount.ini * Change default settings * Fix lint errors * Fix check error
This commit is contained in:
committed by
GitHub
parent
7917b2d725
commit
cbe159f38a
2
.gitignore
vendored
2
.gitignore
vendored
@@ -18,3 +18,5 @@ Qt/
|
||||
.eggs
|
||||
qtaccount.ini
|
||||
.pytest_cache
|
||||
.run/
|
||||
.python-version
|
||||
|
||||
362
aqt/commercial.py
Normal file
362
aqt/commercial.py
Normal file
@@ -0,0 +1,362 @@
|
||||
import json
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from logging import Logger, getLogger
|
||||
from pathlib import Path
|
||||
from typing import List, Optional
|
||||
|
||||
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.metadata import Version
|
||||
|
||||
|
||||
@dataclass
|
||||
class QtPackageInfo:
|
||||
name: str
|
||||
displayname: str
|
||||
version: str
|
||||
|
||||
|
||||
class QtPackageManager:
|
||||
def __init__(
|
||||
self, arch: str, version: Version, target: str, username: Optional[str] = None, password: Optional[str] = None
|
||||
):
|
||||
self.arch = arch
|
||||
self.version = version
|
||||
self.target = target
|
||||
self.cache_dir = self._get_cache_dir()
|
||||
self.packages: List[QtPackageInfo] = []
|
||||
self.username = username
|
||||
self.password = password
|
||||
|
||||
def _get_cache_dir(self) -> Path:
|
||||
"""Create and return cache directory path."""
|
||||
base_cache = Settings.qt_installer_cache_path
|
||||
cache_path = os.path.join(base_cache, self.target, self.arch, str(self.version))
|
||||
Path(cache_path).mkdir(parents=True, exist_ok=True)
|
||||
return Path(cache_path)
|
||||
|
||||
def _get_cache_file(self) -> Path:
|
||||
"""Get the cache file path."""
|
||||
return self.cache_dir / "packages.json"
|
||||
|
||||
def _save_to_cache(self) -> None:
|
||||
"""Save packages information to cache."""
|
||||
cache_data = [{"name": pkg.name, "displayname": pkg.displayname, "version": pkg.version} for pkg in self.packages]
|
||||
|
||||
with open(self._get_cache_file(), "w") as f:
|
||||
json.dump(cache_data, f, indent=2)
|
||||
|
||||
def _load_from_cache(self) -> bool:
|
||||
"""Load packages information from cache if available."""
|
||||
cache_file = self._get_cache_file()
|
||||
if not cache_file.exists():
|
||||
return False
|
||||
|
||||
try:
|
||||
with open(cache_file, "r") as f:
|
||||
cache_data = json.load(f)
|
||||
self.packages = [
|
||||
QtPackageInfo(name=pkg["name"], displayname=pkg["displayname"], version=pkg["version"])
|
||||
for pkg in cache_data
|
||||
]
|
||||
return True
|
||||
except (json.JSONDecodeError, KeyError):
|
||||
return False
|
||||
|
||||
def _parse_packages_xml(self, xml_content: str) -> None:
|
||||
"""Parse packages XML content and extract package information using defusedxml."""
|
||||
try:
|
||||
# Use defusedxml.ElementTree to safely parse the XML content
|
||||
root = ElementTree.fromstring(xml_content)
|
||||
self.packages = []
|
||||
|
||||
# Find all package elements using XPath-like expression
|
||||
# Note: defusedxml supports a subset of XPath
|
||||
for pkg in root.findall(".//package"):
|
||||
name = pkg.get("name", "")
|
||||
displayname = pkg.get("displayname", "")
|
||||
version = pkg.get("version", "")
|
||||
|
||||
if all([name, displayname, version]): # Ensure all required attributes are present
|
||||
self.packages.append(QtPackageInfo(name=name, displayname=displayname, version=version))
|
||||
except ElementTree.ParseError as e:
|
||||
raise RuntimeError(f"Failed to parse package XML: {e}")
|
||||
|
||||
def _get_version_string(self) -> str:
|
||||
"""Get formatted version string for package names."""
|
||||
return f"{self.version.major}{self.version.minor}{self.version.patch}"
|
||||
|
||||
def _get_base_package_name(self) -> str:
|
||||
"""Get the base package name for the current configuration."""
|
||||
version_str = self._get_version_string()
|
||||
return f"qt.qt{self.version.major}.{version_str}"
|
||||
|
||||
def gather_packages(self, installer_path: str) -> None:
|
||||
"""Gather package information using qt installer search command."""
|
||||
if self._load_from_cache():
|
||||
return
|
||||
|
||||
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,
|
||||
]
|
||||
|
||||
if self.username and self.password:
|
||||
cmd.extend(["--email", self.username, "--pw", self.password])
|
||||
|
||||
try:
|
||||
output = safely_run_save_output(cmd, Settings.qt_installer_timeout)
|
||||
|
||||
# Handle both string and CompletedProcess outputs
|
||||
output_text = output.stdout if hasattr(output, "stdout") else str(output)
|
||||
|
||||
# Extract the XML portion from the output
|
||||
xml_start = output_text.find("<availablepackages>")
|
||||
xml_end = output_text.find("</availablepackages>") + len("</availablepackages>")
|
||||
|
||||
if xml_start != -1 and xml_end != -1:
|
||||
xml_content = output_text[xml_start:xml_end]
|
||||
self._parse_packages_xml(xml_content)
|
||||
self._save_to_cache()
|
||||
else:
|
||||
# Log the actual output for debugging
|
||||
logger = getLogger("aqt.helper")
|
||||
logger.debug(f"Installer output: {output_text}")
|
||||
raise RuntimeError("Failed to find package information in installer output")
|
||||
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"Failed to get package information: {str(e)}")
|
||||
|
||||
def get_install_command(self, modules: Optional[List[str]], temp_dir: str) -> List[str]:
|
||||
"""Generate installation command based on requested modules."""
|
||||
package_name = f"{self._get_base_package_name()}.{self.arch}"
|
||||
cmd = ["install", package_name]
|
||||
|
||||
# No modules requested, return base package only
|
||||
if not modules:
|
||||
return cmd
|
||||
|
||||
# Ensure package cache exists
|
||||
self.gather_packages(temp_dir)
|
||||
|
||||
if "all" in modules:
|
||||
# Find all addon and direct module packages
|
||||
for pkg in self.packages:
|
||||
if f"{self._get_base_package_name()}.addons." in pkg.name or pkg.name.startswith(
|
||||
f"{self._get_base_package_name()}."
|
||||
):
|
||||
module_name = pkg.name.split(".")[-1]
|
||||
if module_name != self.arch: # Skip the base package
|
||||
cmd.append(pkg.name)
|
||||
else:
|
||||
# Add specifically requested modules that exist in either format
|
||||
for module in modules:
|
||||
addon_name = f"{self._get_base_package_name()}.addons.{module}"
|
||||
direct_name = f"{self._get_base_package_name()}.{module}"
|
||||
|
||||
# Check if either package name exists
|
||||
matching_pkg = next(
|
||||
(pkg.name for pkg in self.packages if pkg.name == addon_name or pkg.name == direct_name), None
|
||||
)
|
||||
|
||||
if matching_pkg:
|
||||
cmd.append(matching_pkg)
|
||||
|
||||
return cmd
|
||||
|
||||
|
||||
class CommercialInstaller:
|
||||
"""Qt Commercial installer that handles module installation and package management."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
target: str,
|
||||
arch: Optional[str],
|
||||
version: Optional[str],
|
||||
username: Optional[str] = None,
|
||||
password: Optional[str] = None,
|
||||
output_dir: Optional[str] = None,
|
||||
logger: Optional[Logger] = None,
|
||||
base_url: str = "https://download.qt.io",
|
||||
override: Optional[list[str]] = None,
|
||||
modules: Optional[List[str]] = None,
|
||||
no_unattended: bool = False,
|
||||
):
|
||||
self.override = override
|
||||
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
|
||||
|
||||
# Set OS-specific properties
|
||||
self.os_name = get_os_name()
|
||||
self._installer_filename = get_qt_installer_name()
|
||||
self.qt_account = get_qt_account_path()
|
||||
self.package_manager = QtPackageManager(self.arch, self.version, self.target, self.username, self.password)
|
||||
|
||||
@staticmethod
|
||||
def get_auto_answers() -> str:
|
||||
"""Get auto-answer options from settings."""
|
||||
settings_map = {
|
||||
"OperationDoesNotExistError": Settings.qt_installer_operationdoesnotexisterror,
|
||||
"OverwriteTargetDirectory": Settings.qt_installer_overwritetargetdirectory,
|
||||
"stopProcessesForUpdates": Settings.qt_installer_stopprocessesforupdates,
|
||||
"installationErrorWithCancel": Settings.qt_installer_installationerrorwithcancel,
|
||||
"installationErrorWithIgnore": Settings.qt_installer_installationerrorwithignore,
|
||||
"AssociateCommonFiletypes": Settings.qt_installer_associatecommonfiletypes,
|
||||
"telemetry-question": Settings.qt_installer_telemetry,
|
||||
}
|
||||
|
||||
answers = []
|
||||
for key, value in settings_map.items():
|
||||
answers.append(f"{key}={value}")
|
||||
|
||||
return ",".join(answers)
|
||||
|
||||
@staticmethod
|
||||
def build_command(
|
||||
installer_path: str,
|
||||
override: Optional[List[str]] = None,
|
||||
username: Optional[str] = None,
|
||||
password: Optional[str] = None,
|
||||
output_dir: Optional[str] = None,
|
||||
no_unattended: bool = False,
|
||||
) -> List[str]:
|
||||
"""Build the installation command with proper safeguards."""
|
||||
cmd = [installer_path]
|
||||
|
||||
# Add unattended flags unless explicitly disabled
|
||||
if not no_unattended:
|
||||
cmd.extend(["--accept-licenses", "--accept-obligations", "--confirm-command"])
|
||||
|
||||
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())])
|
||||
|
||||
# Add auto-answer options from settings
|
||||
auto_answers = CommercialInstaller.get_auto_answers()
|
||||
if auto_answers:
|
||||
cmd.extend(["--auto-answer", auto_answers])
|
||||
|
||||
return cmd
|
||||
|
||||
def install(self) -> None:
|
||||
"""Run the Qt installation process."""
|
||||
if (
|
||||
not self.qt_account.exists()
|
||||
and not (self.username and 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)
|
||||
|
||||
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)
|
||||
installer_path = temp_path / self._installer_filename
|
||||
|
||||
self.logger.info(f"Downloading Qt installer to {installer_path}")
|
||||
self.download_installer(installer_path, Settings.qt_installer_timeout)
|
||||
|
||||
try:
|
||||
cmd = []
|
||||
if self.override:
|
||||
cmd = self.build_command(str(installer_path), override=self.override, no_unattended=self.no_unattended)
|
||||
else:
|
||||
# Initialize package manager and gather packages
|
||||
self.package_manager.gather_packages(str(installer_path))
|
||||
|
||||
base_cmd = self.build_command(
|
||||
str(installer_path.absolute()),
|
||||
username=self.username,
|
||||
password=self.password,
|
||||
output_dir=self.output_dir,
|
||||
no_unattended=self.no_unattended,
|
||||
)
|
||||
|
||||
cmd = [
|
||||
*base_cmd,
|
||||
*self.package_manager.get_install_command(self.modules, temp_dir),
|
||||
]
|
||||
|
||||
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__()}")
|
||||
raise
|
||||
finally:
|
||||
self.logger.info("Qt installation completed successfully")
|
||||
|
||||
def download_installer(self, target_path: Path, timeout: int) -> None:
|
||||
url = f"{self.base_url}/official_releases/online_installers/{self._installer_filename}"
|
||||
try:
|
||||
response = requests.get(url, stream=True, timeout=timeout)
|
||||
response.raise_for_status()
|
||||
|
||||
with open(target_path, "wb") as f:
|
||||
for chunk in response.iter_content(chunk_size=8192):
|
||||
f.write(chunk)
|
||||
if self.os_name != "windows":
|
||||
os.chmod(target_path, 0o500)
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"Failed to download installer: {e}")
|
||||
|
||||
def _get_package_name(self) -> str:
|
||||
qt_version = f"{self.version.major}{self.version.minor}{self.version.patch}"
|
||||
return f"qt.qt{self.version.major}.{qt_version}.{self.arch}"
|
||||
168
aqt/helper.py
168
aqt/helper.py
@@ -24,6 +24,8 @@ import logging.config
|
||||
import os
|
||||
import posixpath
|
||||
import secrets
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from configparser import ConfigParser
|
||||
from logging import Handler, getLogger
|
||||
@@ -48,6 +50,64 @@ from aqt.exceptions import (
|
||||
)
|
||||
|
||||
|
||||
def get_os_name() -> str:
|
||||
system = sys.platform.lower()
|
||||
if system == "darwin":
|
||||
return "mac"
|
||||
if system == "linux":
|
||||
return "linux"
|
||||
if system in ("windows", "win32"): # Accept both windows and win32
|
||||
return "windows"
|
||||
raise ValueError(f"Unsupported operating system: {system}")
|
||||
|
||||
|
||||
def get_qt_local_folder_path() -> Path:
|
||||
os_name = get_os_name()
|
||||
if os_name == "windows":
|
||||
appdata = os.environ.get("APPDATA", str(Path.home() / "AppData" / "Roaming"))
|
||||
return Path(appdata) / "Qt"
|
||||
if os_name == "mac":
|
||||
return Path.home() / "Library" / "Application Support" / "Qt"
|
||||
return Path.home() / ".local" / "share" / "Qt"
|
||||
|
||||
|
||||
def get_qt_account_path() -> Path:
|
||||
return get_qt_local_folder_path() / "qtaccount.ini"
|
||||
|
||||
|
||||
def get_qt_installer_name() -> str:
|
||||
installer_dict = {
|
||||
"windows": "qt-unified-windows-x64-online.exe",
|
||||
"mac": "qt-unified-macOS-x64-online.dmg",
|
||||
"linux": "qt-unified-linux-x64-online.run",
|
||||
}
|
||||
return installer_dict[get_os_name()]
|
||||
|
||||
|
||||
def get_qt_installer_path() -> Path:
|
||||
return get_qt_local_folder_path() / get_qt_installer_name()
|
||||
|
||||
|
||||
def get_default_local_cache_path() -> Path:
|
||||
os_name = get_os_name()
|
||||
if os_name == "windows":
|
||||
appdata = os.environ.get("APPDATA", str(Path.home() / "AppData" / "Roaming"))
|
||||
return Path(appdata) / "aqt" / "cache"
|
||||
if os_name == "mac":
|
||||
return Path.home() / "Library" / "Application Support" / "aqt" / "cache"
|
||||
return Path.home() / ".local" / "share" / "aqt" / "cache"
|
||||
|
||||
|
||||
def get_default_local_temp_path() -> Path:
|
||||
os_name = get_os_name()
|
||||
if os_name == "windows":
|
||||
appdata = os.environ.get("APPDATA", str(Path.home() / "AppData" / "Roaming"))
|
||||
return Path(appdata) / "aqt" / "tmp"
|
||||
if os_name == "mac":
|
||||
return Path.home() / "Library" / "Application Support" / "aqt" / "tmp"
|
||||
return Path.home() / ".local" / "share" / "aqt" / "tmp"
|
||||
|
||||
|
||||
def _get_meta(url: str) -> requests.Response:
|
||||
return requests.get(url + ".meta4")
|
||||
|
||||
@@ -344,24 +404,41 @@ class SettingsClass:
|
||||
"_lock": Lock(),
|
||||
}
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.config: Optional[ConfigParser]
|
||||
self._lock: Lock
|
||||
self._initialize()
|
||||
|
||||
def __new__(cls, *p, **k):
|
||||
self = object.__new__(cls, *p, **k)
|
||||
self.__dict__ = cls._shared_state
|
||||
return self
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.config: Optional[ConfigParser]
|
||||
self._lock: Lock
|
||||
def _initialize(self) -> None:
|
||||
"""Initialize configuration if not already initialized."""
|
||||
if self.config is None:
|
||||
with self._lock:
|
||||
if self.config is None:
|
||||
self.config = MyConfigParser()
|
||||
self.configfile = os.path.join(os.path.dirname(__file__), "settings.ini")
|
||||
self.loggingconf = os.path.join(os.path.dirname(__file__), "logging.ini")
|
||||
self.config.read(self.configfile)
|
||||
|
||||
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)
|
||||
|
||||
def _get_config(self) -> ConfigParser:
|
||||
"""Safe getter for config that ensures it's initialized."""
|
||||
self._initialize()
|
||||
assert self.config is not None
|
||||
return self.config
|
||||
|
||||
def load_settings(self, file: Optional[Union[str, TextIO]] = None) -> None:
|
||||
if self.config is None:
|
||||
return
|
||||
|
||||
if file is not None:
|
||||
if isinstance(file, str):
|
||||
result = self.config.read(file)
|
||||
@@ -377,6 +454,24 @@ class SettingsClass:
|
||||
with open(self.configfile, "r") as f:
|
||||
self.config.read_file(f)
|
||||
|
||||
@property
|
||||
def qt_installer_cache_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", "cache_path") or config.get("qtcommercial", "cache_path").strip() == "":
|
||||
return str(get_default_local_cache_path())
|
||||
return config.get("qtcommercial", "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() == "":
|
||||
return str(get_default_local_temp_path())
|
||||
return config.get("qtcommercial", "temp_path")
|
||||
|
||||
@property
|
||||
def archive_download_location(self):
|
||||
return self.config.get("aqt", "archive_download_location", fallback=".")
|
||||
@@ -473,6 +568,58 @@ class SettingsClass:
|
||||
"""
|
||||
return self.config.getint("aqt", "min_module_size", fallback=41)
|
||||
|
||||
# Qt Commercial Installer properties
|
||||
@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)
|
||||
|
||||
@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")
|
||||
|
||||
@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")
|
||||
|
||||
@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")
|
||||
|
||||
@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")
|
||||
|
||||
@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")
|
||||
|
||||
@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")
|
||||
|
||||
@property
|
||||
def qt_installer_telemetry(self) -> str:
|
||||
"""Handle telemetry settings in Qt installer."""
|
||||
return self._get_config().get("qtcommercial", "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)
|
||||
|
||||
def qt_installer_cleanup(self) -> None:
|
||||
"""Control whether to use unattended installation flags."""
|
||||
import shutil
|
||||
|
||||
shutil.rmtree(self.qt_installer_temp_path)
|
||||
|
||||
|
||||
Settings = SettingsClass()
|
||||
|
||||
@@ -482,3 +629,18 @@ def setup_logging(env_key="LOG_CFG"):
|
||||
if config is not None and os.path.exists(config):
|
||||
Settings.loggingconf = config
|
||||
logging.config.fileConfig(Settings.loggingconf)
|
||||
|
||||
|
||||
def safely_run(cmd: List[str], timeout: int) -> None:
|
||||
try:
|
||||
subprocess.run(cmd, shell=False, timeout=timeout)
|
||||
except Exception:
|
||||
raise
|
||||
|
||||
|
||||
def safely_run_save_output(cmd: List[str], timeout: int) -> Any:
|
||||
try:
|
||||
result = subprocess.run(cmd, shell=False, capture_output=True, text=True, timeout=timeout)
|
||||
return result
|
||||
except Exception:
|
||||
raise
|
||||
|
||||
206
aqt/installer.py
206
aqt/installer.py
@@ -42,6 +42,7 @@ from typing import List, Optional, Tuple, cast
|
||||
|
||||
import aqt
|
||||
from aqt.archives import QtArchives, QtPackage, SrcDocExamplesArchives, ToolArchives
|
||||
from aqt.commercial import CommercialInstaller
|
||||
from aqt.exceptions import (
|
||||
AqtException,
|
||||
ArchiveChecksumError,
|
||||
@@ -59,8 +60,11 @@ from aqt.helper import (
|
||||
Settings,
|
||||
downloadBinaryFile,
|
||||
get_hash,
|
||||
get_os_name,
|
||||
get_qt_installer_name,
|
||||
retry_on_bad_connection,
|
||||
retry_on_errors,
|
||||
safely_run_save_output,
|
||||
setup_logging,
|
||||
)
|
||||
from aqt.metadata import ArchiveId, MetadataFactory, QtRepoProperty, SimpleSpec, Version, show_list, suggested_follow_up
|
||||
@@ -124,9 +128,20 @@ class CommonInstallArgParser(BaseArgumentParser):
|
||||
class InstallArgParser(CommonInstallArgParser):
|
||||
"""Install-qt arguments and options"""
|
||||
|
||||
override: Optional[List[str]]
|
||||
arch: Optional[str]
|
||||
qt_version: str
|
||||
qt_version_spec: str
|
||||
version: Optional[str]
|
||||
user: Optional[str]
|
||||
password: Optional[str]
|
||||
operation_does_not_exist_error: str
|
||||
overwrite_target_dir: str
|
||||
stop_processes_for_updates: str
|
||||
installation_error_with_cancel: str
|
||||
installation_error_with_ignore: str
|
||||
associate_common_filetypes: str
|
||||
telemetry: str
|
||||
|
||||
modules: Optional[List[str]]
|
||||
archives: Optional[List[str]]
|
||||
@@ -657,6 +672,47 @@ class Cli:
|
||||
)
|
||||
show_list(meta)
|
||||
|
||||
def run_install_qt_commercial(self, args: InstallArgParser) -> None:
|
||||
"""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:
|
||||
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
|
||||
|
||||
def show_help(self, args=None):
|
||||
"""Display help message"""
|
||||
self.parser.print_help()
|
||||
@@ -667,7 +723,7 @@ class Cli:
|
||||
py_build = platform.python_compiler()
|
||||
return f"aqtinstall(aqt) v{aqt.__version__} on Python {py_version} [{py_impl} {py_build}]"
|
||||
|
||||
def show_aqt_version(self, args=None):
|
||||
def show_aqt_version(self, args: Optional[list[str]] = None) -> None:
|
||||
"""Display version information"""
|
||||
self.logger.info(self._format_aqt_version())
|
||||
|
||||
@@ -750,6 +806,134 @@ class Cli:
|
||||
)
|
||||
self._set_common_options(install_tool_parser)
|
||||
|
||||
def _set_install_qt_commercial_parser(self, install_qt_commercial_parser: argparse.ArgumentParser) -> None:
|
||||
install_qt_commercial_parser.set_defaults(func=self.run_install_qt_commercial)
|
||||
|
||||
# Create mutually exclusive group for override vs standard parameters
|
||||
exclusive_group = install_qt_commercial_parser.add_mutually_exclusive_group()
|
||||
exclusive_group.add_argument(
|
||||
"--override",
|
||||
nargs=argparse.REMAINDER,
|
||||
help="Will ignore all other parameters and use everything after this parameter as "
|
||||
"input for the official Qt installer",
|
||||
)
|
||||
|
||||
# Make standard arguments optional when override is used by adding a custom action
|
||||
class ConditionalRequiredAction(argparse.Action):
|
||||
def __call__(self, parser, namespace, values, option_string=None) -> None:
|
||||
if not hasattr(namespace, "override") or not namespace.override:
|
||||
setattr(namespace, self.dest, values)
|
||||
|
||||
install_qt_commercial_parser.add_argument(
|
||||
"target",
|
||||
nargs="?",
|
||||
choices=["desktop", "android", "ios"],
|
||||
help="Target platform",
|
||||
action=ConditionalRequiredAction,
|
||||
)
|
||||
install_qt_commercial_parser.add_argument(
|
||||
"arch", nargs="?", help="Target architecture", action=ConditionalRequiredAction
|
||||
)
|
||||
install_qt_commercial_parser.add_argument("version", nargs="?", help="Qt version", action=ConditionalRequiredAction)
|
||||
|
||||
install_qt_commercial_parser.add_argument(
|
||||
"--user",
|
||||
help="Qt account username",
|
||||
)
|
||||
install_qt_commercial_parser.add_argument(
|
||||
"--password",
|
||||
help="Qt account password",
|
||||
)
|
||||
install_qt_commercial_parser.add_argument(
|
||||
"--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",
|
||||
)
|
||||
list_parser.add_argument(
|
||||
"search_terms",
|
||||
nargs="*",
|
||||
help="Optional search terms to pass to the installer search command. If not provided, lists all packages",
|
||||
)
|
||||
list_parser.set_defaults(func=self.run_list_qt_commercial)
|
||||
|
||||
def run_list_qt_commercial(self, args) -> None:
|
||||
"""Execute Qt commercial package listing"""
|
||||
self.show_aqt_version()
|
||||
|
||||
# Create temporary directory to download 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)
|
||||
|
||||
# Get installer based on OS
|
||||
installer_filename = get_qt_installer_name()
|
||||
installer_path = temp_path / installer_filename
|
||||
|
||||
try:
|
||||
# Download installer
|
||||
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)
|
||||
response.raise_for_status()
|
||||
|
||||
with open(installer_path, "wb") as f:
|
||||
for chunk in response.iter_content(chunk_size=8192):
|
||||
f.write(chunk)
|
||||
|
||||
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
|
||||
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
|
||||
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}")
|
||||
finally:
|
||||
# Clean up
|
||||
Settings.qt_installer_cleanup()
|
||||
|
||||
def _warn_on_deprecated_command(self, old_name: str, new_name: str) -> None:
|
||||
self.logger.warning(
|
||||
f"The command '{old_name}' is deprecated and marked for removal in a future version of aqt.\n"
|
||||
@@ -764,6 +948,7 @@ class Cli:
|
||||
)
|
||||
|
||||
def _make_all_parsers(self, subparsers: argparse._SubParsersAction) -> None:
|
||||
"""Creates all command parsers and adds them to the subparsers"""
|
||||
|
||||
def make_parser_it(cmd: str, desc: str, set_parser_cmd, formatter_class):
|
||||
kwargs = {"formatter_class": formatter_class} if formatter_class else {}
|
||||
@@ -798,13 +983,22 @@ class Cli:
|
||||
if cmd_type != "src":
|
||||
parser.add_argument("-m", "--modules", action="store_true", help="Print list of available modules")
|
||||
|
||||
# Create install command parsers
|
||||
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.",
|
||||
self._set_install_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")
|
||||
@@ -948,14 +1142,13 @@ class Cli:
|
||||
)
|
||||
list_parser.set_defaults(func=self.run_list_tool)
|
||||
|
||||
def _make_common_parsers(self, subparsers: argparse._SubParsersAction):
|
||||
def _make_common_parsers(self, subparsers: argparse._SubParsersAction) -> None:
|
||||
help_parser = subparsers.add_parser("help")
|
||||
help_parser.set_defaults(func=self.show_help)
|
||||
#
|
||||
version_parser = subparsers.add_parser("version")
|
||||
version_parser.set_defaults(func=self.show_aqt_version)
|
||||
|
||||
def _set_common_options(self, subparser):
|
||||
def _set_common_options(self, subparser: argparse.ArgumentParser) -> None:
|
||||
subparser.add_argument(
|
||||
"-O",
|
||||
"--outputdir",
|
||||
@@ -1236,7 +1429,8 @@ def run_installer(
|
||||
listener.stop()
|
||||
|
||||
|
||||
def init_worker_sh():
|
||||
def init_worker_sh() -> None:
|
||||
"""Initialize worker signal handling"""
|
||||
signal.signal(signal.SIGINT, signal.SIG_IGN)
|
||||
|
||||
|
||||
@@ -1248,7 +1442,7 @@ def installer(
|
||||
archive_dest: Path,
|
||||
settings_ini: str,
|
||||
keep: bool,
|
||||
):
|
||||
) -> None:
|
||||
"""
|
||||
Installer function to download archive files and extract it.
|
||||
It is called through multiprocessing.Pool()
|
||||
|
||||
@@ -1,32 +1,45 @@
|
||||
[DEFAULTS]
|
||||
|
||||
[aqt]
|
||||
concurrency: 4
|
||||
baseurl: https://download.qt.io
|
||||
7zcmd: 7z
|
||||
print_stacktrace_on_error: False
|
||||
always_keep_archives: False
|
||||
archive_download_location: .
|
||||
min_module_size: 41
|
||||
concurrency : 4
|
||||
baseurl : https://download.qt.io
|
||||
7zcmd : 7z
|
||||
print_stacktrace_on_error : False
|
||||
always_keep_archives : False
|
||||
archive_download_location : .
|
||||
min_module_size : 41
|
||||
|
||||
[requests]
|
||||
connection_timeout: 3.5
|
||||
response_timeout: 30
|
||||
max_retries_on_connection_error: 5
|
||||
retry_backoff: 0.1
|
||||
max_retries_on_checksum_error: 5
|
||||
max_retries_to_retrieve_hash: 5
|
||||
hash_algorithm: sha256
|
||||
INSECURE_NOT_FOR_PRODUCTION_ignore_hash: False
|
||||
connection_timeout : 3.5
|
||||
response_timeout : 30
|
||||
max_retries_on_connection_error : 5
|
||||
retry_backoff : 0.1
|
||||
max_retries_on_checksum_error : 5
|
||||
max_retries_to_retrieve_hash : 5
|
||||
hash_algorithm : sha256
|
||||
INSECURE_NOT_FOR_PRODUCTION_ignore_hash : False
|
||||
|
||||
[qtcommercial]
|
||||
unattended : True
|
||||
installer_timeout : 1800
|
||||
operation_does_not_exist_error : Ignore
|
||||
overwrite_target_directory : Yes
|
||||
stop_processes_for_updates : Ignore
|
||||
installation_error_with_cancel : Ignore
|
||||
installation_error_with_ignore : Ignore
|
||||
associate_common_filetypes : Yes
|
||||
telemetry : No
|
||||
cache_path :
|
||||
temp_dir :
|
||||
|
||||
[mirrors]
|
||||
trusted_mirrors:
|
||||
trusted_mirrors :
|
||||
https://download.qt.io
|
||||
blacklist:
|
||||
blacklist :
|
||||
http://mirrors.ocf.berkeley.edu
|
||||
http://mirrors.tuna.tsinghua.edu.cn
|
||||
http://mirrors.geekpie.club
|
||||
fallbacks:
|
||||
fallbacks :
|
||||
https://qtproject.mirror.liquidtelecom.com/
|
||||
https://mirrors.aliyun.com/qt/
|
||||
https://mirrors.ustc.edu.cn/qtproject/
|
||||
@@ -44,7 +57,7 @@ fallbacks:
|
||||
https://qt.mirror.constant.com/
|
||||
|
||||
[kde_patches]
|
||||
patches:
|
||||
patches :
|
||||
0001-toolchain.prf-Use-vswhere-to-obtain-VS-installation-.patch
|
||||
0002-Fix-allocated-memory-of-QByteArray-returned-by-QIODe.patch
|
||||
0003-Update-CLDR-to-v37-adding-Nigerian-Pidgin-as-a-new-l.patch
|
||||
@@ -240,4 +253,4 @@ patches:
|
||||
0193-Remove-the-unnecessary-template-parameter-from-the-c.patch
|
||||
0194-Fix-memory-leak-when-using-small-caps-font.patch
|
||||
0195-Make-sure-_q_printerChanged-is-called-even-if-only-p.patch
|
||||
0196-fix-Alt-shortcut-on-non-US-layouts.patch
|
||||
0196-fix-Alt-shortcut-on-non-US-layouts.patch
|
||||
@@ -1,3 +1,4 @@
|
||||
import platform
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
@@ -15,8 +16,9 @@ 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-doc,install-example,install-src,"
|
||||
"list-qt,list-tool,list-doc,list-example,list-src,help,version}\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"
|
||||
" ...\n"
|
||||
"\n"
|
||||
"Another unofficial Qt Installer.\n"
|
||||
@@ -32,7 +34,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-doc,install-example,install-src,list-qt,"
|
||||
" {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"
|
||||
" Please refer to each help message by using '--help' "
|
||||
"with each subcommand\n",
|
||||
@@ -520,3 +523,29 @@ 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
|
||||
|
||||
@@ -1676,30 +1676,30 @@ def test_install_qt6_wasm_autodesktop(monkeypatch, capsys, version, str_version,
|
||||
|
||||
assert result == 0
|
||||
|
||||
# Check output format
|
||||
out, err = capsys.readouterr()
|
||||
sys.stdout.write(out)
|
||||
sys.stderr.write(err)
|
||||
# Check output format
|
||||
out, err = capsys.readouterr()
|
||||
sys.stdout.write(out)
|
||||
sys.stderr.write(err)
|
||||
|
||||
# Use regex that works for all platforms
|
||||
expected_pattern = re.compile(
|
||||
r"^INFO : aqtinstall\(aqt\) v.*? on Python 3.*?\n"
|
||||
r"INFO : You are installing the Qt6-WASM version of Qt\n"
|
||||
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|qtpaths)(?:6)?\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"
|
||||
r"(?:INFO : Found extension .*?\n)*"
|
||||
r"(?:INFO : Downloading (?:qt[^\n]*|icu[^\n]*)\n"
|
||||
r"Finished installation of .*?\.7z in \d+\.\d+\n)*"
|
||||
r"INFO : Finished installation\n"
|
||||
r"INFO : Time elapsed: \d+\.\d+ second\n$"
|
||||
)
|
||||
# Use regex that works for all platforms
|
||||
expected_pattern = re.compile(
|
||||
r"^INFO : aqtinstall\(aqt\) v.*? on Python 3.*?\n"
|
||||
r"INFO : You are installing the Qt6-WASM version of Qt\n"
|
||||
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|qtpaths)(?:6)?\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"
|
||||
r"(?:INFO : Found extension .*?\n)*"
|
||||
r"(?:INFO : Downloading (?:qt[^\n]*|icu[^\n]*)\n"
|
||||
r"Finished installation of .*?\.7z in \d+\.\d+\n)*"
|
||||
r"INFO : Finished installation\n"
|
||||
r"INFO : Time elapsed: \d+\.\d+ second\n$"
|
||||
)
|
||||
|
||||
assert expected_pattern.match(err)
|
||||
assert expected_pattern.match(err)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -2054,3 +2054,104 @@ 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)
|
||||
|
||||
@@ -1269,3 +1269,19 @@ 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
|
||||
|
||||
Reference in New Issue
Block a user