├── requirements ├── release.txt └── codesanity.txt ├── ci ├── blackify └── pylintify ├── cuteci ├── __init__.py ├── install-qt.qs └── deploy_qt.py ├── test ├── build ├── Dockerfile └── run ├── .gitignore ├── CHANGELOG.md ├── .travis.yml ├── setup.py ├── LICENSE ├── DEVELOPMENT.md └── README.md /requirements/release.txt: -------------------------------------------------------------------------------- 1 | twine==1.13.0 2 | -------------------------------------------------------------------------------- /requirements/codesanity.txt: -------------------------------------------------------------------------------- 1 | black==19.3b0 2 | pylint==2.3.1 3 | -------------------------------------------------------------------------------- /ci/blackify: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -ev 3 | cd $(dirname $0)/.. 4 | 5 | black -l 120 . 6 | -------------------------------------------------------------------------------- /cuteci/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | CuteCI package 3 | """ 4 | __application__ = "cuteci" 5 | __version__ = "1.3.0" 6 | -------------------------------------------------------------------------------- /ci/pylintify: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -ev 3 | cd $(dirname $0)/.. 4 | 5 | pylint --output-format=parseable --max-line-length=120 cuteci 6 | -------------------------------------------------------------------------------- /test/build: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -ev 3 | cd $(dirname $0) 4 | 5 | docker build --build-arg UID=$(id -u) --build-arg GID=$(id -g) -t cuteci:1 . 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled python modules 2 | *.pyc 3 | 4 | # Setuptools distribution folder. 5 | /dist/ 6 | 7 | # Python egg metadata, regenerated from source files by setuptools. 8 | /*.egg-info 9 | /*.egg 10 | /build/ 11 | 12 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 1.3.0 4 | 5 | * Added support for Qt `5.12.3` 6 | * Added `--timeout` option to install command 7 | * Internal refactoring 8 | 9 | ## 1.2.1 10 | 11 | * Fix internal testing 12 | 13 | ## 1.2.0 14 | 15 | * Minor doc fixes 16 | * PyPI deployment is handled by CI 17 | 18 | ## 1.1.0 19 | 20 | * `cuteci` is published on PyPI 21 | 22 | ## 1.0.0 23 | 24 | * Project bootstrap 25 | -------------------------------------------------------------------------------- /test/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:18.04 2 | 3 | RUN apt-get update && apt-get install --yes --no-install-recommends --quiet \ 4 | python3 \ 5 | python3-pip \ 6 | python3-setuptools \ 7 | python3-wheel \ 8 | xvfb \ 9 | libfontconfig \ 10 | libdbus-1-3 11 | 12 | ENV PYTHONUNBUFFERED=1 13 | 14 | # Create worker user with same permissions than the caller of `build` script. 15 | ARG UID=1000 16 | ARG GID=1000 17 | ENV USER worker 18 | RUN groupadd --gid $GID worker 19 | RUN useradd --create-home --gid $GID --uid $UID $USER --shell /bin/bash \ 20 | && echo "$USER:worker" | chpasswd 21 | WORKDIR /home/$USER 22 | ENV PATH="/home/worker/.local/bin:${PATH}" 23 | USER worker 24 | 25 | ENTRYPOINT ["/bin/bash"] 26 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: xenial 2 | language: minimal 3 | services: 4 | - docker 5 | install: 6 | - test/build 7 | script: 8 | - docker run --rm -v $PWD:/home/worker/workspace cuteci:1 /home/worker/workspace/test/run 9 | deploy: 10 | provider: pypi 11 | user: hasboeuf 12 | password: 13 | secure: QCepFdmZvvaObE9vRM6V5pH9gmdYahRhxgKbWuHPADGHHLlUN9+ITOOIQDYZ+B+mxxZT7U0rEs4weDGF54G6NklqIJIy2JQttlROGUzO7uycON+psAgwThcPRfDmJ+AQCBOtWqjGwX6WHfpuwQKq4pX7WjF55MtiWMVq9eNEK+0eaiz041A1eR6iiG2Xrar53erLVnJaT6mArGKUWwXvqslUQRRdO8NM2iHO94OWK3SW3WbGVNegN8wRxLMVdJRKduvFeRH2kjnSWHM5OIpvEqyRDDMsk/AV8kRL7M/frp8dtwacRYkPd8xSKIYqFhxmqdv59lDHTVb+fG5CytH96285TogufAvh5J4w21j6VN38hv0CBEN5+tPoNSckP9DmeQmRyZhiKhSmuR35eEaXTaHJI9rTN6jmL4u0ocJ7jTLMW3vUSH0AsJ1c2p8kOrb+5Qs2/ZW7Mshi2k0VQYjdJNcDBROG83r96AFEULceUCV3jPUpIpySEsFWwrWrtb2KBGF6pVAL8N+bH5xo1JaKyXEhuNAghL8N/O8G9NOr34wVdOyL1DRzPp8279F8nz6+l03WjSfWUEGgm8YXYD9fp469ljEjfBF6k6GV4CzQdGGKHpSXGxCLBqPZT/CiJNjSqDR1013xv08fM2uMKT39zjmr/7eZ+SbbjK4p10x78oY= 14 | on: 15 | tags: true 16 | on: 17 | branch: master 18 | skip_existing: true 19 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tool to deploy Qt in headless mode. 3 | """ 4 | import setuptools 5 | import cuteci 6 | 7 | with open("README.md", "r") as fh: 8 | long_description = fh.read() 9 | 10 | setuptools.setup( 11 | name=cuteci.__application__, 12 | version=cuteci.__version__, 13 | author="Adrien Gavignet", 14 | author_email="adrien.gavignet@gmail.com", 15 | license="MIT", 16 | description="CuteCI is a simple tool allowing you to install Qt with desired packages in headless mode.", 17 | long_description=long_description, 18 | long_description_content_type="text/markdown", 19 | keywords="qt deploy install headless docker", 20 | url="https://github.com/hasboeuf/cuteci", 21 | packages=setuptools.find_packages(), 22 | package_data={"cuteci": ["install-qt.qs"]}, 23 | classifiers=[ 24 | "Programming Language :: Python :: 3", 25 | "License :: OSI Approved :: MIT License", 26 | "Operating System :: OS Independent", 27 | ], 28 | zip_safe=True, 29 | entry_points={"console_scripts": ["cuteci=cuteci.deploy_qt:main"]}, 30 | ) 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Adrien Gavignet 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /DEVELOPMENT.md: -------------------------------------------------------------------------------- 1 | # CuteCI 2 | 3 | ## Testing 4 | 5 | Covered by docker and Travis CI, have a look to `test` dir if you are curious. 6 | 7 | ## Code sanity 8 | 9 | Coding style is handled by `black` (via `ci/blackify`). 10 | Static checks are handled by `pylint` (via `ci/pylintify`). 11 | 12 | You must `pip3 install -r requirements/codesanity.txt` to use those scripts. 13 | 14 | Note: continuous integration does not check code sanity. 15 | 16 | ## Release procedure 17 | 18 | * Bump version 19 | * Update CHANGELOG 20 | * Push a new tag 21 | 22 | ### Publish via TravisCI 23 | 24 | TravisCI automatically publish `cuteci` on PyPI when a tag is pushed. 25 | 26 | Password in `.travis.yml` has been encrypted with: 27 | 28 | `travis encrypt --add deploy.password` 29 | 30 | ### Publish manually (not used anymore) 31 | 32 | * Deploy on Test PyPI 33 | 34 | ```bash 35 | pip3 uninstall cuteci 36 | git clean -d --force --dry-run 37 | git clean -d --force 38 | python3 setup.py sdist bdist_wheel 39 | twine upload --repository-url https://test.pypi.org/legacy/ dist/* 40 | pip3 install --index-url https://test.pypi.org/simple/ --no-deps cuteci 41 | cuteci --help 42 | python3 -c "import cuteci" 43 | ``` 44 | 45 | * Deploy on PyPI 46 | 47 | ```bash 48 | pip3 uninstall cuteci 49 | git clean -d --force --dry-run 50 | git clean -d --force 51 | python3 setup.py sdist bdist_wheel 52 | twine upload dist/* 53 | pip3 install cuteci 54 | cuteci --help 55 | python3 -c "import cuteci" 56 | ``` 57 | -------------------------------------------------------------------------------- /test/run: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -ev 3 | cd $(dirname $0)/.. 4 | 5 | # Build & Install 6 | python3 setup.py sdist bdist_wheel 7 | pip3 install dist/*.whl 8 | 9 | # Test Qt 5.12.3 10 | cuteci \ 11 | --installer http://download.qt.io/official_releases/qt/5.12/5.12.3/qt-opensource-linux-x64-5.12.3.run \ 12 | --timeout 60 \ 13 | list 14 | 15 | cuteci \ 16 | --rm \ 17 | --installer ~/workspace/qt-opensource-linux-x64-5.12.3.run \ 18 | install \ 19 | --verbose \ 20 | --destdir ~/Qt \ 21 | --packages qt.qt5.5123.qtnetworkauth,qt.qt5.5123.gcc_64 22 | ls ~/Qt/5.12.3/gcc_64/lib/libQt5NetworkAuth.so 23 | 24 | 25 | # Test Qt 5.12.2 26 | cuteci \ 27 | --installer http://download.qt.io/official_releases/qt/5.12/5.12.2/qt-opensource-linux-x64-5.12.2.run \ 28 | --timeout 60 \ 29 | list 30 | 31 | cuteci \ 32 | --rm \ 33 | --installer ~/workspace/qt-opensource-linux-x64-5.12.2.run \ 34 | install \ 35 | --verbose \ 36 | --destdir ~/Qt \ 37 | --packages qt.qt5.5122.qtnetworkauth,qt.qt5.5122.gcc_64 38 | ls ~/Qt/5.12.2/gcc_64/lib/libQt5NetworkAuth.so 39 | 40 | 41 | # Test Qt 5.11.3 42 | cuteci \ 43 | --installer http://download.qt.io/archive/qt/5.11/5.11.3/qt-opensource-linux-x64-5.11.3.run \ 44 | --timeout 60 \ 45 | list 46 | 47 | cuteci \ 48 | --rm \ 49 | --installer ~/workspace/qt-opensource-linux-x64-5.11.3.run \ 50 | install \ 51 | --verbose \ 52 | --destdir ~/Qt \ 53 | --packages qt.qt5.5113.qtnetworkauth,qt.qt5.5113.gcc_64 54 | ls ~/Qt/5.11.3/gcc_64/lib/libQt5NetworkAuth.so 55 | 56 | 57 | # Test Qt 5.10.1 58 | cuteci \ 59 | --installer http://download.qt.io/archive/qt/5.10/5.10.1/qt-opensource-linux-x64-5.10.1.run \ 60 | --timeout 60 \ 61 | list 62 | 63 | cuteci \ 64 | --rm \ 65 | --installer ~/workspace/qt-opensource-linux-x64-5.10.1.run \ 66 | install \ 67 | --verbose \ 68 | --destdir ~/Qt \ 69 | --packages qt.qt5.5101.qtnetworkauth,qt.qt5.5101.gcc_64 70 | ls ~/Qt/5.10.1/gcc_64/lib/libQt5NetworkAuth.so 71 | 72 | 73 | # Test Qt 5.9.7 + Tools 74 | cuteci \ 75 | --installer http://download.qt.io/archive/qt/5.9/5.9.7/qt-opensource-linux-x64-5.9.7.run \ 76 | --timeout 60 \ 77 | list 78 | 79 | cuteci \ 80 | --rm \ 81 | --installer ~/workspace/qt-opensource-linux-x64-5.9.7.run \ 82 | install \ 83 | --verbose \ 84 | --destdir ~/Qt \ 85 | --packages qt.qt5.597.qtnetworkauth,qt.qt5.597.gcc_64 \ 86 | --keep-tools 87 | ls ~/Qt/5.9.7/gcc_64/lib/libQt5NetworkAuth.so 88 | ls ~/Qt/Tools 89 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CuteCI 2 | 3 | [![Build Status](https://travis-ci.org/hasboeuf/cuteci.svg?branch=master)](https://travis-ci.org/hasboeuf/cuteci) 4 | [![PyPI version](https://badge.fury.io/py/cuteci.svg)](https://pypi.org/project/cuteci/) 5 | ![License](https://img.shields.io/github/license/mashape/apistatus.svg) 6 | 7 | CuteCI is a simple tool allowing you to install Qt with desired packages in headless mode. 8 | Qt installers are using Qt Installer Framework which provides scripting ability, 9 | CuteCI takes advantage of this. 10 | 11 | ## Requirements 12 | 13 | * `Python3` `pip3` 14 | * `cuteci` is in Python but has only been tested on Ubuntu (+docker). 15 | * `cuteci` has been tested with Qt installer `5.9.7` `5.10.1` `5.11.3` `5.12.2` `5.12.3`. 16 | 17 | ## Installation 18 | 19 | `pip3 install cuteci` 20 | 21 | ## Principle 22 | 23 | `cuteci` does few things: 24 | 25 | * Download Qt installer if you pass an url 26 | * Make installer executable 27 | * Install Qt with desired packages in the directory you choose 28 | 29 | `cuteci` can also only lists packages available in the installer. 30 | 31 | ## Usage 32 | 33 | Common options: 34 | 35 | * `--installer` (required): path or url to Qt installer. If url, choose an official one from `download.qt.io/official_releases/qt/`, this is because `md5sums.txt` is retrieved implicitely from it. 36 | * `--ui`: if set, Qt installer UI is shown (useful for debugging). 37 | * `--rm`: if set, Qt installer is removed at the end. 38 | * `--timeout`: duration in seconds to wait for the operation to be finished. 39 | 40 | ### List packages 41 | 42 | ```bash 43 | cuteci \ 44 | --installer \ 45 | [--ui] [--rm] \ 46 | list 47 | ``` 48 | 49 | Will output: 50 | 51 | ```bash 52 | ===LIST OF PACKAGES=== 53 | qt Qt 54 | qt.qt5.5122 Qt 5.12.2 55 | qt.tools Developer and Designer Tools 56 | qt.installer.changelog Qt Installer Changelog 57 | qt.license.lgpl Qt License LGPL 58 | qt.license.thirdparty Qt 3rd Party Licenses 59 | qt.license.python Python Software Foundation License Version 2 60 | qt.license.gplv3except License GPL3-EXCEPT 61 | qt.qt5.5122.gcc_64 Desktop gcc 64-bit 62 | qt.qt5.5122.android_x86 Android x86 63 | qt.qt5.5122.android_arm64_v8a Android ARM64-v8a 64 | qt.qt5.5122.android_armv7 Android ARMv7 65 | ... 66 | ===END OF PACKAGES=== 67 | ``` 68 | 69 | ### Install 70 | 71 | ```bash 72 | cuteci \ 73 | --installer \ 74 | [--ui] [--rm] \ 75 | install \ 76 | --destdir /opt/Qt \ 77 | --packages qt.qt5.5122.gcc_64,qt.qt5.5122.android_x86 \ 78 | [--verbose] [--keep-tools] 79 | ``` 80 | 81 | #### Notes 82 | 83 | * `destdir` should not contain a previous Qt installation (unless it has been installed with `cuteci`), 84 | otherwise installer will complain and script does not handle it. 85 | * If `--keep-tools` is set, QtCreator, Maintenance Tools, samples and doc will be kept, 86 | but you will not be able to install another version of Qt in `destdir`. 87 | 88 | ## Docker integration 89 | 90 | Here is the sample of a minimalist Dockerfile using `cuteci` to install Qt 5.12.2: 91 | 92 | ```bash 93 | FROM ubuntu:18.04 94 | 95 | RUN apt-get update && apt-get install -y --no-install-recommends \ 96 | libdbus-1-3 \ 97 | xvfb \ 98 | libfontconfig \ 99 | python3 \ 100 | python3-pip 101 | 102 | RUN pip3 install cuteci && \ 103 | cuteci \ 104 | --rm \ 105 | --installer http://download.qt.io/official_releases/qt/5.12/5.12.2/qt-opensource-linux-x64-5.12.2.run \ 106 | install \ 107 | --destdir /opt/Qt \ 108 | --packages qt.qt5.5122.gcc_64 109 | 110 | ENTRYPOINT ["/bin/bash"] 111 | ``` 112 | -------------------------------------------------------------------------------- /cuteci/install-qt.qs: -------------------------------------------------------------------------------- 1 | function debugObject(object) { 2 | var lines = []; 3 | for (var i in object) { 4 | lines.push([i, object[i]].join(" ")); 5 | } 6 | console.log(lines.join(",")); 7 | } 8 | 9 | function Controller() { 10 | // Not empty dir is no problem. 11 | installer.setMessageBoxAutomaticAnswer("OverwriteTargetDirectory", QMessageBox.Yes); 12 | // If Qt is already installed in dir, acknowlegde the error but situation is stuck. 13 | installer.setMessageBoxAutomaticAnswer("TargetDirectoryInUse", QMessageBox.Ok); 14 | // Allow to quit the installer when listing packages. 15 | installer.setMessageBoxAutomaticAnswer("cancelInstallation", QMessageBox.Yes); 16 | } 17 | 18 | Controller.prototype.WelcomePageCallback = function() { 19 | console.log("Welcome Page"); 20 | var widget = gui.currentPageWidget(); 21 | widget.completeChanged.connect(function() { 22 | // For some reason, this page needs some delay. 23 | gui.clickButton(buttons.NextButton); 24 | }); 25 | } 26 | 27 | Controller.prototype.CredentialsPageCallback = function() { 28 | console.log("Credentials Page"); 29 | gui.clickButton(buttons.NextButton); 30 | } 31 | 32 | Controller.prototype.IntroductionPageCallback = function() { 33 | console.log("Introduction Page"); 34 | gui.clickButton(buttons.NextButton); 35 | } 36 | 37 | Controller.prototype.TargetDirectoryPageCallback = function() { 38 | console.log("Target Directory Page"); 39 | var installDir = installer.environmentVariable("DESTDIR"); 40 | if (installDir) { 41 | // If not present we assume we want to list packages. 42 | var widget = gui.currentPageWidget(); 43 | widget.TargetDirectoryLineEdit.setText(installDir); 44 | } 45 | gui.clickButton(buttons.NextButton); 46 | } 47 | 48 | Controller.prototype.ComponentSelectionPageCallback = function() { 49 | console.log("Component Selection Page"); 50 | 51 | var components = installer.components(); 52 | console.log("Available packages: " + components.length); 53 | var packages = ["===LIST OF PACKAGES==="]; 54 | for (var i = 0 ; i < components.length ;i++) { 55 | packages.push(components[i].name + " " + components[i].displayName); 56 | } 57 | packages.push("===END OF PACKAGES==="); 58 | console.log(packages.join("\n")); 59 | 60 | if (installer.environmentVariable("LIST_PACKAGE_ONLY")) { 61 | // Early exit 62 | gui.clickButton(buttons.CancelButton); 63 | return; 64 | } 65 | 66 | wantedPackages = installer.environmentVariable("PACKAGES").split(","); 67 | console.log("Trying to install ", wantedPackages); 68 | 69 | var widget = gui.currentPageWidget(); 70 | widget.deselectAll(); 71 | 72 | for (var i in wantedPackages) { 73 | name = wantedPackages[i]; 74 | var found = false; 75 | for (var j in components) { 76 | if (components[j].name === name) { 77 | found = true; 78 | break; 79 | } 80 | } 81 | 82 | if (found) { 83 | console.log("Select " + name); 84 | widget.selectComponent(name); 85 | } else { 86 | console.log("Package " + name + " not found"); 87 | } 88 | } 89 | 90 | gui.clickButton(buttons.NextButton); 91 | } 92 | 93 | Controller.prototype.LicenseAgreementPageCallback = function() { 94 | console.log("Accept License Agreement Page"); 95 | var widget = gui.currentPageWidget(); 96 | widget.AcceptLicenseRadioButton.setChecked(true); 97 | gui.clickButton(buttons.NextButton); 98 | } 99 | 100 | Controller.prototype.ReadyForInstallationPageCallback = function() { 101 | console.log("Ready For Installation Page"); 102 | gui.clickButton(buttons.CommitButton); 103 | } 104 | 105 | Controller.prototype.PerformInstallationPageCallback = function() { 106 | console.log("Perform Installation Page"); 107 | installer.installationFinished.connect(function() { 108 | console.log("Installation finished"); 109 | gui.clickButton(buttons.NextButton); 110 | }); 111 | } 112 | 113 | Controller.prototype.FinishedPageCallback = function() { 114 | console.log("Finished Page"); 115 | var widget = gui.currentPageWidget(); 116 | if (widget.LaunchQtCreatorCheckBoxForm) { 117 | widget.LaunchQtCreatorCheckBoxForm.launchQtCreatorCheckBox.setChecked(false); 118 | } else if (widget.RunItCheckBox) { 119 | widget.RunItCheckBox.setChecked(false); 120 | } 121 | gui.clickButton(buttons.FinishButton); 122 | } 123 | -------------------------------------------------------------------------------- /cuteci/deploy_qt.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | Deploy Qt 5 | """ 6 | 7 | import sys 8 | import os 9 | import stat 10 | import shutil 11 | import argparse 12 | from urllib.request import urlopen 13 | import hashlib 14 | import re 15 | import subprocess 16 | 17 | import cuteci 18 | 19 | WORKING_DIR = os.getcwd() 20 | CURRENT_DIR = os.path.dirname(os.path.realpath(__file__)) 21 | MD5SUMS_FILENAME = "md5sums.txt" 22 | DEFAULT_INSTALL_SCRIPT = os.path.join(CURRENT_DIR, "install-qt.qs") 23 | 24 | EXIT_OK = 0 25 | EXIT_ERROR = 1 26 | 27 | 28 | def _get_version(path): 29 | # qt-opensource-windows-x86-5.12.2.exe 30 | # qt-opensource-mac-x64-5.12.2.dmg 31 | # qt-opensource-linux-x64-5.12.2.run 32 | basename = os.path.basename(path) 33 | res = re.search(r"-(\d+\.\d+.\d+)\.", basename) 34 | return res.group(1) 35 | 36 | 37 | def _get_major_minor_ver(path): 38 | return ".".join(_get_version(path).split(".")[:1]) 39 | 40 | 41 | def _get_install_script(version): 42 | path = os.path.join(CURRENT_DIR, "install-qt-{}.qs".format(version)) 43 | if not os.path.exists(path): 44 | print("No specific install script found, fallback to", DEFAULT_INSTALL_SCRIPT) 45 | path = DEFAULT_INSTALL_SCRIPT 46 | return path 47 | 48 | 49 | class DeployQt: 50 | """ 51 | Class in charge of Qt deployment 52 | """ 53 | 54 | def __init__(self, show_ui, rm_installer, qt_installer, timeout): 55 | self.verbose = False 56 | self.show_ui = show_ui 57 | self.rm_installer = rm_installer 58 | self.timeout = timeout 59 | if qt_installer.startswith("http"): 60 | self.installer_path = None 61 | self.installer_url = qt_installer 62 | else: 63 | self.installer_path = qt_installer 64 | self.installer_url = None 65 | 66 | def _run_installer(self, env): 67 | assert self.installer_path 68 | version = _get_major_minor_ver(self.installer_path) 69 | install_script = _get_install_script(version) 70 | cmd = [self.installer_path, "--script", install_script] 71 | if not self.show_ui: 72 | cmd.extend(["--platform", "minimal"]) 73 | if self.verbose: 74 | cmd.extend(["--verbose"]) 75 | proc = subprocess.Popen(cmd, stdout=sys.stdout, stderr=sys.stderr, env=env) 76 | try: 77 | print("Running installer", cmd) 78 | proc.wait(self.timeout) 79 | except subprocess.TimeoutExpired: 80 | proc.kill() 81 | raise Exception("Timeout while waiting for the installer (waited {}s), kill it".format(self.timeout)) 82 | 83 | def _cleanup(self): 84 | if self.rm_installer: 85 | assert self.installer_path 86 | print("Removing", self.installer_path) 87 | os.remove(self.installer_path) 88 | 89 | def _ensure_executable(self): 90 | assert self.installer_path 91 | os.chmod(self.installer_path, os.stat(self.installer_path).st_mode | stat.S_IEXEC) 92 | 93 | def download_qt(self): 94 | """ 95 | Download Qt if possible, also verify checksums. 96 | 97 | :raises Exception: in case of failure 98 | """ 99 | if not self.installer_url: 100 | print("Skip download: no url provided") 101 | return 102 | qt_url = self.installer_url 103 | filename = qt_url[qt_url.rfind("/") + 1 :] 104 | md5sums_url = qt_url[: qt_url.rfind("/")] + "/" + MD5SUMS_FILENAME 105 | 106 | self.installer_path = os.path.join(WORKING_DIR, filename) 107 | 108 | # Download Qt 109 | print("Download Qt", qt_url) 110 | 111 | def print_progress(size, length): 112 | # Print progress every 5% 113 | if not hasattr(print_progress, "prev"): 114 | print_progress.prev = -1 # Then 0% is printed 115 | percent = int(size * 100 / length) 116 | progress = percent - percent % 5 117 | if progress != print_progress.prev: 118 | print("\rFetched {}%".format(progress), end="") 119 | print_progress.prev = progress 120 | 121 | hash_md5 = hashlib.md5() 122 | with open(self.installer_path, "wb") as installer_file: 123 | req = urlopen(qt_url) 124 | length = int(req.getheader("content-length", 1500000000)) 125 | size = 0 126 | while True: 127 | chunk = req.read(4096) 128 | if not chunk: 129 | break 130 | size += len(chunk) 131 | hash_md5.update(chunk) 132 | installer_file.write(chunk) 133 | print_progress(size, length) 134 | 135 | # Download md5sums and check 136 | print() 137 | print("Download md5sums", md5sums_url) 138 | response = urlopen(md5sums_url) 139 | print("Check md5sums") 140 | if hash_md5.hexdigest() not in str(response.read()): 141 | print("Checksums do not match") 142 | return EXIT_ERROR 143 | print("Download OK", self.installer_path) 144 | 145 | def list_packages(self): 146 | """ 147 | List available packages in Qt Installer. 148 | 149 | :raises Exception: in case of failure 150 | """ 151 | self._ensure_executable() 152 | self.verbose = True # Listing needs to be verbose 153 | env = os.environ.copy() 154 | env["LIST_PACKAGE_ONLY"] = "1" 155 | self._run_installer(env) 156 | self._cleanup() 157 | 158 | def install(self, packages, destdir, keep_tools, verbose): 159 | """ 160 | Install Qt. 161 | 162 | :param list: packages to install 163 | :param str destdir: install directory 164 | :param keep_tools: if True, keep Qt Tools after installation 165 | :param bool verbose: enable verbosity 166 | :raises Exception: in case of failure 167 | """ 168 | self._ensure_executable() 169 | self.verbose = verbose 170 | env = os.environ.copy() 171 | env["PACKAGES"] = packages 172 | env["DESTDIR"] = destdir 173 | self._run_installer(env) 174 | self._cleanup() 175 | 176 | if not keep_tools: 177 | print("Cleaning destdir") 178 | files = os.listdir(destdir) 179 | for name in files: 180 | fullpath = os.path.join(destdir, name) 181 | if re.match(r"\d+\.\d+.\d+", name): 182 | # Qt stands in X.Y.Z dir, skip it 183 | print("Keep", fullpath) 184 | continue 185 | if os.path.isdir(fullpath): 186 | shutil.rmtree(fullpath) 187 | else: 188 | os.remove(fullpath) 189 | print("Remove", fullpath) 190 | 191 | 192 | def main(): 193 | """ 194 | Command line tool to deploy Qt 195 | """ 196 | parser = argparse.ArgumentParser(description=__doc__) 197 | parser.add_argument( 198 | "--version", action="version", version="{} {}".format(cuteci.__application__, cuteci.__version__) 199 | ) 200 | parser.add_argument("--ui", action="store_true", default=False, help="Installer UI displayed") 201 | parser.add_argument("--rm", action="store_true", default=False, help="Remove Qt installer") 202 | parser.add_argument("--installer", required=True, help="Path or url to Qt installer") 203 | parser.add_argument("--timeout", type=int, default=180, help="Timeout in seconds, default 180") 204 | 205 | subparsers = parser.add_subparsers(dest="action") 206 | 207 | subparsers.add_parser(name="list") 208 | 209 | install_parser = subparsers.add_parser(name="install") 210 | install_parser.add_argument("--packages", required=True, help="Comma separated list of package to install") 211 | install_parser.add_argument("--destdir", required=True, help="Path to install Qt, e.g.: /opt/Qt") 212 | install_parser.add_argument("--keep-tools", action="store_true", default=False, help="Keep tools, samples, doc etc") 213 | install_parser.add_argument("--verbose", action="store_true", default=False, help="Print debug info") 214 | 215 | args = parser.parse_args() 216 | action = args.action 217 | 218 | deployer = DeployQt(args.ui, args.rm, args.installer, args.timeout) 219 | 220 | try: 221 | deployer.download_qt() 222 | print("Download OK - Qt Installer is ready") 223 | 224 | if action == "list": 225 | deployer.list_packages() 226 | print("Listing OK - available packages are printed above") 227 | else: 228 | deployer.install(args.packages, args.destdir, args.keep_tools, args.verbose) 229 | print("Installation OK") 230 | except Exception as exception: 231 | print("FAIL", str(exception)) 232 | return EXIT_ERROR 233 | print("OK") 234 | return EXIT_OK 235 | 236 | 237 | if __name__ == "__main__": 238 | sys.exit(main()) 239 | --------------------------------------------------------------------------------