mirror of
https://github.com/miurahr/aqtinstall.git
synced 2025-12-18 05:04:38 +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
|
.eggs
|
||||||
qtaccount.ini
|
qtaccount.ini
|
||||||
.pytest_cache
|
.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 os
|
||||||
import posixpath
|
import posixpath
|
||||||
import secrets
|
import secrets
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
from configparser import ConfigParser
|
from configparser import ConfigParser
|
||||||
from logging import Handler, getLogger
|
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:
|
def _get_meta(url: str) -> requests.Response:
|
||||||
return requests.get(url + ".meta4")
|
return requests.get(url + ".meta4")
|
||||||
|
|
||||||
@@ -344,24 +404,41 @@ class SettingsClass:
|
|||||||
"_lock": Lock(),
|
"_lock": Lock(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.config: Optional[ConfigParser]
|
||||||
|
self._lock: Lock
|
||||||
|
self._initialize()
|
||||||
|
|
||||||
def __new__(cls, *p, **k):
|
def __new__(cls, *p, **k):
|
||||||
self = object.__new__(cls, *p, **k)
|
self = object.__new__(cls, *p, **k)
|
||||||
self.__dict__ = cls._shared_state
|
self.__dict__ = cls._shared_state
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def _initialize(self) -> None:
|
||||||
self.config: Optional[ConfigParser]
|
"""Initialize configuration if not already initialized."""
|
||||||
self._lock: Lock
|
|
||||||
if self.config is None:
|
if self.config is None:
|
||||||
with self._lock:
|
with self._lock:
|
||||||
if self.config is None:
|
if self.config is None:
|
||||||
self.config = MyConfigParser()
|
self.config = MyConfigParser()
|
||||||
self.configfile = os.path.join(os.path.dirname(__file__), "settings.ini")
|
self.configfile = os.path.join(os.path.dirname(__file__), "settings.ini")
|
||||||
self.loggingconf = os.path.join(os.path.dirname(__file__), "logging.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:
|
def load_settings(self, file: Optional[Union[str, TextIO]] = None) -> None:
|
||||||
if self.config is None:
|
if self.config is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
if file is not None:
|
if file is not None:
|
||||||
if isinstance(file, str):
|
if isinstance(file, str):
|
||||||
result = self.config.read(file)
|
result = self.config.read(file)
|
||||||
@@ -377,6 +454,24 @@ class SettingsClass:
|
|||||||
with open(self.configfile, "r") as f:
|
with open(self.configfile, "r") as f:
|
||||||
self.config.read_file(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
|
@property
|
||||||
def archive_download_location(self):
|
def archive_download_location(self):
|
||||||
return self.config.get("aqt", "archive_download_location", fallback=".")
|
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)
|
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()
|
Settings = SettingsClass()
|
||||||
|
|
||||||
@@ -482,3 +629,18 @@ def setup_logging(env_key="LOG_CFG"):
|
|||||||
if config is not None and os.path.exists(config):
|
if config is not None and os.path.exists(config):
|
||||||
Settings.loggingconf = config
|
Settings.loggingconf = config
|
||||||
logging.config.fileConfig(Settings.loggingconf)
|
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
|
import aqt
|
||||||
from aqt.archives import QtArchives, QtPackage, SrcDocExamplesArchives, ToolArchives
|
from aqt.archives import QtArchives, QtPackage, SrcDocExamplesArchives, ToolArchives
|
||||||
|
from aqt.commercial import CommercialInstaller
|
||||||
from aqt.exceptions import (
|
from aqt.exceptions import (
|
||||||
AqtException,
|
AqtException,
|
||||||
ArchiveChecksumError,
|
ArchiveChecksumError,
|
||||||
@@ -59,8 +60,11 @@ from aqt.helper import (
|
|||||||
Settings,
|
Settings,
|
||||||
downloadBinaryFile,
|
downloadBinaryFile,
|
||||||
get_hash,
|
get_hash,
|
||||||
|
get_os_name,
|
||||||
|
get_qt_installer_name,
|
||||||
retry_on_bad_connection,
|
retry_on_bad_connection,
|
||||||
retry_on_errors,
|
retry_on_errors,
|
||||||
|
safely_run_save_output,
|
||||||
setup_logging,
|
setup_logging,
|
||||||
)
|
)
|
||||||
from aqt.metadata import ArchiveId, MetadataFactory, QtRepoProperty, SimpleSpec, Version, show_list, suggested_follow_up
|
from aqt.metadata import ArchiveId, MetadataFactory, QtRepoProperty, SimpleSpec, Version, show_list, suggested_follow_up
|
||||||
@@ -124,9 +128,20 @@ class CommonInstallArgParser(BaseArgumentParser):
|
|||||||
class InstallArgParser(CommonInstallArgParser):
|
class InstallArgParser(CommonInstallArgParser):
|
||||||
"""Install-qt arguments and options"""
|
"""Install-qt arguments and options"""
|
||||||
|
|
||||||
|
override: Optional[List[str]]
|
||||||
arch: Optional[str]
|
arch: Optional[str]
|
||||||
qt_version: str
|
qt_version: str
|
||||||
qt_version_spec: 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]]
|
modules: Optional[List[str]]
|
||||||
archives: Optional[List[str]]
|
archives: Optional[List[str]]
|
||||||
@@ -657,6 +672,47 @@ class Cli:
|
|||||||
)
|
)
|
||||||
show_list(meta)
|
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):
|
def show_help(self, args=None):
|
||||||
"""Display help message"""
|
"""Display help message"""
|
||||||
self.parser.print_help()
|
self.parser.print_help()
|
||||||
@@ -667,7 +723,7 @@ class Cli:
|
|||||||
py_build = platform.python_compiler()
|
py_build = platform.python_compiler()
|
||||||
return f"aqtinstall(aqt) v{aqt.__version__} on Python {py_version} [{py_impl} {py_build}]"
|
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"""
|
"""Display version information"""
|
||||||
self.logger.info(self._format_aqt_version())
|
self.logger.info(self._format_aqt_version())
|
||||||
|
|
||||||
@@ -750,6 +806,134 @@ class Cli:
|
|||||||
)
|
)
|
||||||
self._set_common_options(install_tool_parser)
|
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:
|
def _warn_on_deprecated_command(self, old_name: str, new_name: str) -> None:
|
||||||
self.logger.warning(
|
self.logger.warning(
|
||||||
f"The command '{old_name}' is deprecated and marked for removal in a future version of aqt.\n"
|
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:
|
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):
|
def make_parser_it(cmd: str, desc: str, set_parser_cmd, formatter_class):
|
||||||
kwargs = {"formatter_class": formatter_class} if formatter_class else {}
|
kwargs = {"formatter_class": formatter_class} if formatter_class else {}
|
||||||
@@ -798,13 +983,22 @@ class Cli:
|
|||||||
if cmd_type != "src":
|
if cmd_type != "src":
|
||||||
parser.add_argument("-m", "--modules", action="store_true", help="Print list of available modules")
|
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-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-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-doc", "Install documentation.", self.run_install_doc, False)
|
||||||
make_parser_sde("install-example", "Install examples.", self.run_install_example, 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)
|
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_parser(subparsers)
|
||||||
|
self._make_list_qt_commercial_parser(subparsers)
|
||||||
self._make_list_tool_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-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")
|
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)
|
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 = subparsers.add_parser("help")
|
||||||
help_parser.set_defaults(func=self.show_help)
|
help_parser.set_defaults(func=self.show_help)
|
||||||
#
|
|
||||||
version_parser = subparsers.add_parser("version")
|
version_parser = subparsers.add_parser("version")
|
||||||
version_parser.set_defaults(func=self.show_aqt_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(
|
subparser.add_argument(
|
||||||
"-O",
|
"-O",
|
||||||
"--outputdir",
|
"--outputdir",
|
||||||
@@ -1236,7 +1429,8 @@ def run_installer(
|
|||||||
listener.stop()
|
listener.stop()
|
||||||
|
|
||||||
|
|
||||||
def init_worker_sh():
|
def init_worker_sh() -> None:
|
||||||
|
"""Initialize worker signal handling"""
|
||||||
signal.signal(signal.SIGINT, signal.SIG_IGN)
|
signal.signal(signal.SIGINT, signal.SIG_IGN)
|
||||||
|
|
||||||
|
|
||||||
@@ -1248,7 +1442,7 @@ def installer(
|
|||||||
archive_dest: Path,
|
archive_dest: Path,
|
||||||
settings_ini: str,
|
settings_ini: str,
|
||||||
keep: bool,
|
keep: bool,
|
||||||
):
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Installer function to download archive files and extract it.
|
Installer function to download archive files and extract it.
|
||||||
It is called through multiprocessing.Pool()
|
It is called through multiprocessing.Pool()
|
||||||
|
|||||||
@@ -1,32 +1,45 @@
|
|||||||
[DEFAULTS]
|
[DEFAULTS]
|
||||||
|
|
||||||
[aqt]
|
[aqt]
|
||||||
concurrency: 4
|
concurrency : 4
|
||||||
baseurl: https://download.qt.io
|
baseurl : https://download.qt.io
|
||||||
7zcmd: 7z
|
7zcmd : 7z
|
||||||
print_stacktrace_on_error: False
|
print_stacktrace_on_error : False
|
||||||
always_keep_archives: False
|
always_keep_archives : False
|
||||||
archive_download_location: .
|
archive_download_location : .
|
||||||
min_module_size: 41
|
min_module_size : 41
|
||||||
|
|
||||||
[requests]
|
[requests]
|
||||||
connection_timeout: 3.5
|
connection_timeout : 3.5
|
||||||
response_timeout: 30
|
response_timeout : 30
|
||||||
max_retries_on_connection_error: 5
|
max_retries_on_connection_error : 5
|
||||||
retry_backoff: 0.1
|
retry_backoff : 0.1
|
||||||
max_retries_on_checksum_error: 5
|
max_retries_on_checksum_error : 5
|
||||||
max_retries_to_retrieve_hash: 5
|
max_retries_to_retrieve_hash : 5
|
||||||
hash_algorithm: sha256
|
hash_algorithm : sha256
|
||||||
INSECURE_NOT_FOR_PRODUCTION_ignore_hash: False
|
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]
|
[mirrors]
|
||||||
trusted_mirrors:
|
trusted_mirrors :
|
||||||
https://download.qt.io
|
https://download.qt.io
|
||||||
blacklist:
|
blacklist :
|
||||||
http://mirrors.ocf.berkeley.edu
|
http://mirrors.ocf.berkeley.edu
|
||||||
http://mirrors.tuna.tsinghua.edu.cn
|
http://mirrors.tuna.tsinghua.edu.cn
|
||||||
http://mirrors.geekpie.club
|
http://mirrors.geekpie.club
|
||||||
fallbacks:
|
fallbacks :
|
||||||
https://qtproject.mirror.liquidtelecom.com/
|
https://qtproject.mirror.liquidtelecom.com/
|
||||||
https://mirrors.aliyun.com/qt/
|
https://mirrors.aliyun.com/qt/
|
||||||
https://mirrors.ustc.edu.cn/qtproject/
|
https://mirrors.ustc.edu.cn/qtproject/
|
||||||
@@ -44,7 +57,7 @@ fallbacks:
|
|||||||
https://qt.mirror.constant.com/
|
https://qt.mirror.constant.com/
|
||||||
|
|
||||||
[kde_patches]
|
[kde_patches]
|
||||||
patches:
|
patches :
|
||||||
0001-toolchain.prf-Use-vswhere-to-obtain-VS-installation-.patch
|
0001-toolchain.prf-Use-vswhere-to-obtain-VS-installation-.patch
|
||||||
0002-Fix-allocated-memory-of-QByteArray-returned-by-QIODe.patch
|
0002-Fix-allocated-memory-of-QByteArray-returned-by-QIODe.patch
|
||||||
0003-Update-CLDR-to-v37-adding-Nigerian-Pidgin-as-a-new-l.patch
|
0003-Update-CLDR-to-v37-adding-Nigerian-Pidgin-as-a-new-l.patch
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import platform
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -15,8 +16,9 @@ from aqt.metadata import MetadataFactory, SimpleSpec, Version
|
|||||||
def expected_help(actual, prefix=None):
|
def expected_help(actual, prefix=None):
|
||||||
expected = (
|
expected = (
|
||||||
"usage: aqt [-h] [-c CONFIG]\n"
|
"usage: aqt [-h] [-c CONFIG]\n"
|
||||||
" {install-qt,install-tool,install-doc,install-example,install-src,"
|
" {install-qt,install-tool,install-qt-commercial,install-doc,install-example,"
|
||||||
"list-qt,list-tool,list-doc,list-example,list-src,help,version}\n"
|
"install-src,"
|
||||||
|
"list-qt,list-qt-commercial,list-tool,list-doc,list-example,list-src,help,version}\n"
|
||||||
" ...\n"
|
" ...\n"
|
||||||
"\n"
|
"\n"
|
||||||
"Another unofficial Qt Installer.\n"
|
"Another unofficial Qt Installer.\n"
|
||||||
@@ -32,7 +34,8 @@ def expected_help(actual, prefix=None):
|
|||||||
" install-* subcommands are commands that install components\n"
|
" install-* subcommands are commands that install components\n"
|
||||||
" list-* subcommands are commands that show available components\n"
|
" list-* subcommands are commands that show available components\n"
|
||||||
"\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"
|
"list-tool,list-doc,list-example,list-src,help,version}\n"
|
||||||
" Please refer to each help message by using '--help' "
|
" Please refer to each help message by using '--help' "
|
||||||
"with each subcommand\n",
|
"with each subcommand\n",
|
||||||
@@ -520,3 +523,29 @@ def test_get_autodesktop_dir_and_arch_non_android(
|
|||||||
), "Expected autodesktop install message."
|
), "Expected autodesktop install message."
|
||||||
elif expect["instruct"]:
|
elif expect["instruct"]:
|
||||||
assert any("You can install" in line for line in err_lines), "Expected install instruction message."
|
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
|
||||||
|
|||||||
@@ -2054,3 +2054,104 @@ def test_installer_passes_base_to_metadatafactory(
|
|||||||
sys.stderr.write(err)
|
sys.stderr.write(err)
|
||||||
|
|
||||||
assert expect_out.match(err), 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))
|
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
|
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