diff --git a/.gitignore b/.gitignore
index 0b3170f..fe46499 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,6 @@
# Generated repos
venv/
+log/
sysroot-android-64/
build-android-64/
diff --git a/README.md b/README.md
index aaaaf11..4030dd1 100644
--- a/README.md
+++ b/README.md
@@ -57,7 +57,7 @@ The aforementioned limitations constitute obstacles that can be tackled by PyQt-
* [1.4.4. Test the demo app in your virtual environment](#virtual-environment-app-test)
* [1.5. Install the external dependencies](#external-dependency-installation)
* [1.5.1. Download a set of external dependencies for pyqtdeploy](#external-dependency-download)
- * [1.5.2. Install Zlib for pyqtdeploy](#zlib-installation)
+ * [1.5.2. Install zlib for pyqtdeploy](#zlib-installation)
* [1.5.3. Install Java for Android Studio](#java-installation)
* [1.5.4. Install Android Studio](#android-studio-installation)
* [1.5.5. Install correct Android SDK and Tools](#android-sdk-installation)
@@ -68,7 +68,16 @@ The aforementioned limitations constitute obstacles that can be tackled by PyQt-
* [1.8. Run the app](#app-run)
* [2. Generating your own app](#custom-app)
* [2.1. Create your python package](#package-creation)
+ * [2.1.1. Advanced python package](#package-creation-advanced")
+ * [2.1.2. Non-python file management](#package-creation-non-python-management)
+ * [2.1.3. Standard python package](#package-creation-standard)
* [2.2. Update the sysroot](#sysroot-configuration)
+ * [2.2.1. Specify non-python modules](#sysroot-non-python-modules)
+ * [2.2.2. Specify non-standard python modules](#sysroot-non-standard-python-modules)
+ * [2.2.2.1. Modules with wheels](#sysroot-non-standard-python-modules-with-wheels)
+ * [2.2.2.2. Modules without wheels](#sysroot-non-standard-python-modules-without-wheels)
+ * [2.2.3. Specify standard python modules](#sysroot-standard-python-modules)
+ * [2.2.4. Create custom plugins](#sysroot-custom-plugins)
* [2.3. Configure the pdt](#pdt-configuration)
* [2.4. Build the app](#app-generation)
* [2.5. Debug the app](#app-debugging)
@@ -82,9 +91,9 @@ The aforementioned limitations constitute obstacles that can be tackled by PyQt-
## 1. Getting started
-:mag: This tutorial guides you through the process of generating a cross-platform app from a simple PyQt5 demo app.
+> :mag: **Info**: This tutorial guides you through the process of generating a cross-platform app from a simple PyQt5 demo app.
-:trophy: By the end of the tutorial, you will be able to launch the simple PyQt5 demo app from your Android phone:
+> :dart: **Target**: By the end of the tutorial, you will be able to launch the following simple PyQt5 demo app from your Android phone.
@@ -95,12 +104,14 @@ The aforementioned limitations constitute obstacles that can be tackled by PyQt-
### 1.1. Check the pre-requisites
+> :warning: **Warning**: It is highly recommended to use the following machine specs, as support can be provided.
+
Specs of Linux machine used:
- `Ubuntu 22.04` (EOL April 2032) with around 40-50GB available (to install the dependencies)
- `Python 3.10.12` (EOL October 2026) pre-installed on Ubuntu 22
-:bulb: _Refer to [Virtual Machine Setup](docs/troubleshooting/common_issues.md#virtual-machine-setup) if you don't have a Linux OS available on your machine._
+> :bulb: **Tip**: Refer to [Virtual Machine Setup](docs/troubleshooting/common_issues.md#virtual-machine-setup) if you don't have a Linux OS available on your machine.
Specs of target OS:
@@ -110,35 +121,54 @@ Specs of target OS:
### 1.2. Download the github repo
-Use the HTTPS method if you don't have a Github account:
+Use the command from the relevant method below to download the github repo.
-```
-cd $HOME/Documents \
-&& git clone https://github.com/achille-martin/pyqt-crom.git
-```
+
+
+
+ CONDITION |
+ METHOD |
+ COMMAND |
+
+
+
+ You don't have a Github account |
+ HTTPS |
+ cd $HOME/Documents &&
+git clone https://github.com/achille-martin/pyqt-crom.git
+ |
+
+
+ You have a Github account and SSH keys set up |
+ SSH |
+ cd $HOME/Documents &&
+git clone git@github.com:achille-martin/pyqt-crom
+ |
+
-Use the SSH method if you have a Github account (and [SSH key setup](https://docs.github.com/en/authentication/connecting-to-github-with-ssh)):
+For stability purposes, pick a [release name](https://github.com/achille-martin/pyqt-crom/releases) (which we will call ``) and once you have downloaded the github repo, access your desired release with the following command.
+
+> :warning: **Warning**: The `` placeholder in the command below needs to be modified to match your desired release name. For instance, it can be `v2.0.0`.
```
-cd $HOME/Documents \
-&& git clone git@github.com:achille-martin/pyqt-crom
+cd $HOME/Documents/pyqt-crom &&
+git checkout
```
### 1.3. Setup the path to the main repo
-:warning: _We will use `PYQT_CROM_DIR` as the variable containing the path to the main repo._
+> :triangular_flag_on_post: **Important**: We will use `PYQT_CROM_DIR` as the variable containing the path to the main repo.
Add the variable to your `.bashrc` with:
```
-printf "%s\n" \
-"" \
-"# Environment variable for PyQt-CroM path" \
-"export PYQT_CROM_DIR=$HOME/Documents/pyqt-crom" \
-"" \
->> $HOME/.bashrc \
-&& source $HOME/.bashrc
+text_to_add="
+# Environment variable for PyQt-CroM path
+export PYQT_CROM_DIR=$HOME/Documents/pyqt-crom
+" &&
+printf "$text_to_add" >> $HOME/.bashrc &&
+source $HOME/.bashrc
```
@@ -148,15 +178,15 @@ printf "%s\n" \
#### 1.4.1. Create a python virtual environment
```
-sudo apt-get update \
-&& sudo apt-get install python3-pip \
-&& python3 -m pip install --upgrade pip \
-&& sudo apt-get install python3-virtualenv \
-&& cd $PYQT_CROM_DIR \
-&& mkdir -p venv \
-&& cd venv \
-&& virtualenv pyqt-crom-venv -p python3 \
-&& cd ..
+sudo apt-get update &&
+sudo apt-get install python3-pip &&
+python3 -m pip install --upgrade pip &&
+sudo apt-get install python3-virtualenv &&
+cd $PYQT_CROM_DIR &&
+mkdir -p venv &&
+cd venv &&
+virtualenv pyqt-crom-venv -p python3 &&
+cd ..
```
@@ -166,7 +196,7 @@ sudo apt-get update \
source $PYQT_CROM_DIR/venv/pyqt-crom-venv/bin/activate
```
-:bulb: _To exit the virtual environment, type in your terminal `deactivate`._
+> :bulb: **Tip**: To exit the virtual environment, type in your terminal `deactivate`.
#### 1.4.3. Install the necessary pip packages
@@ -186,19 +216,19 @@ pip3 install --upgrade pip
Install the pip packages in the virtual environment with:
```
-cd $PYQT_CROM_DIR \
-&& pip3 cache purge \
-&& pip3 install -r requirements.txt
+cd $PYQT_CROM_DIR &&
+pip3 cache purge &&
+pip3 install -r requirements.txt
```
-:bulb: _You can confirm the installed pip packages with `pip3 list --local`._
+> :bulb: **Tip**: You can confirm the installed pip packages with `pip3 list --local`.
#### 1.4.4. Test the demo app in your virtual environment
```
-cd $PYQT_CROM_DIR/examples/demo/demo_project/demo_pkg \
-&& python3 demo_app.py
+cd $PYQT_CROM_DIR/examples/demo/demo_project/demo_pkg &&
+python3 demo_app.py
```
The PyQt5 demo app will start and you can confirm that it is displayed properly on your machine:
@@ -218,15 +248,15 @@ The PyQt5 demo app will start and you can confirm that it is displayed properly
Download the sources with:
```
-cd $PYQT_CROM_DIR/utils/resources \
-&& chmod +x download_sources.sh \
-&& ./download_sources.sh
+cd $PYQT_CROM_DIR/utils/bash &&
+chmod +x download_sources.sh &&
+./download_sources.sh
```
-:bulb: _You can confirm that the list of packages required matches with the versions from `$PYQT_CROM_DIR/examples/demo/demo_project/sysroot.toml`._
+> :bulb: **Tip**: You can confirm that the list of packages required matches with the versions from `$PYQT_CROM_DIR/examples/demo/demo_project/sysroot.toml`.
-#### 1.5.2. Install Zlib for pyqtdeploy
+#### 1.5.2. Install zlib for pyqtdeploy
Install zlib on Ubuntu with:
@@ -234,9 +264,9 @@ Install zlib on Ubuntu with:
sudo apt install zlib1g-dev
```
-Zlib is required by the pyqtdeploy project `$PYQT_CROM_DIR/examples/demo/demo_project/config.pdt` to correctly identify the dependencies from the `$PYQT_CROM_DIR/examples/demo/demo_project/sysroot.toml`.
+The `zlib` component is required by `Qt` component in `$PYQT_CROM_DIR/examples/demo/demo_project/sysroot.toml` to be pre-installed, but there is no default way specified in the `zlib` component itself to install it. Therefore, the installation method needs to be explicitly specified in `$PYQT_CROM_DIR/examples/demo/demo_project/sysroot.toml`.
-:bulb: _Sysroot setup tips can be obtained from [Riverbank website](https://www.riverbankcomputing.com/static/Docs/pyqtdeploy/sysroot.html)._
+> :bulb: **Tip**: Sysroot setup tips can be obtained from [Riverbank website](https://www.riverbankcomputing.com/static/Docs/pyqtdeploy/sysroot.html).
#### 1.5.3. Install Java for Android Studio
@@ -250,11 +280,11 @@ sudo apt install openjdk-11-jdk openjdk-11-jre
Set the default java and javac version to 11 using:
```
-sudo update-alternatives --config java \
-&& sudo update-alternatives --config javac
+sudo update-alternatives --config java &&
+sudo update-alternatives --config javac
```
-:hand: _Confirm the version with `java -version && javac -version` which should be `v11.0.21`._
+> :triangular_flag_on_post: **Important**: Confirm the version with `java -version && javac -version` which should be `v11.0.21`.
#### 1.5.4. Install Android Studio
@@ -262,27 +292,27 @@ sudo update-alternatives --config java \
Download Android Studio version `2023.1.1.26` with:
```
-sudo apt-get install wget \
-&& cd $HOME/Downloads \
-&& wget https://redirector.gvt1.com/edgedl/android/studio/ide-zips/2023.1.1.26/android-studio-2023.1.1.26-linux.tar.gz
+sudo apt-get install wget &&
+cd $HOME/Downloads &&
+wget https://redirector.gvt1.com/edgedl/android/studio/ide-zips/2023.1.1.26/android-studio-2023.1.1.26-linux.tar.gz
```
Move the contents of the downloaded `tar.gz` to your `$HOME` directory using:
```
-cd $HOME/Downloads \
-&& tar -xvf android-studio-2023.1.1.26-linux.tar.gz \
-&& mv android-studio $HOME
+cd $HOME/Downloads &&
+tar -xvf android-studio-2023.1.1.26-linux.tar.gz &&
+mv android-studio $HOME
```
Start the installation with:
```
-cd $HOME/android-studio/bin \
-&& ./studio.sh
+cd $HOME/android-studio/bin &&
+./studio.sh
```
-:bulb: _Tip: if there is an issue with android studio start, use `sudo ./studio.sh`._
+> :bulb: **Tip**: If there is an issue with android studio start, use `sudo ./studio.sh`.
The Android Studio installer will start:
- Do not import settings
@@ -293,9 +323,8 @@ The Android Studio installer will start:
- Start the download (unless you want to install extra features)
- Close Android Studio
-:hand: _Make sure that the default SDK has been installed in `$HOME/Android/Sdk` and that `$HOME/Android/Sdk/platforms` contains `android-28` folder only.
-The reason why android-28 (corresponding to Android v9.0) is selected is because there are restrictions depending on the Java version installed and the Qt version installed.
-If `$HOME/Android/Sdk/platforms` does not contain `android-28` folder only, follow the instructions at the [next step](#android-sdk-installation) to set things up correctly._
+> :triangular_flag_on_post: **Important**: Make sure that the default SDK has been installed in `$HOME/Android/Sdk` and that `$HOME/Android/Sdk/platforms` contains `android-28` folder only.
+The reason why android-28 (corresponding to Android v9.0) is selected is because there are restrictions depending on the Java version installed and the Qt version installed. If `$HOME/Android/Sdk/platforms` does not contain `android-28` folder only, follow the instructions at the [next step](#android-sdk-installation) to set things up correctly.
#### 1.5.5. Install correct Android SDK and Tools
@@ -312,12 +341,12 @@ If `$HOME/Android/Sdk/platforms` does not contain `android-28` folder only, foll
- Download SDK Platform-Tools v28.0.3 to match the SDK Build-Tools version and add it to your SDK folder using:
```
-cd $HOME/Downloads \
-&& wget https://dl.google.com/android/repository/platform-tools_r28.0.3-linux.zip \
-&& sudo apt-get install unzip \
-&& unzip platform-tools_r28.0.3-linux.zip \
-&& rm -r $HOME/Android/Sdk/platform-tools \
-&& mv platform-tools $HOME/Android/Sdk
+cd $HOME/Downloads &&
+wget https://dl.google.com/android/repository/platform-tools_r28.0.3-linux.zip &&
+sudo apt-get install unzip &&
+unzip platform-tools_r28.0.3-linux.zip &&
+rm -r $HOME/Android/Sdk/platform-tools &&
+mv platform-tools $HOME/Android/Sdk
```
@@ -329,9 +358,9 @@ cd $HOME/Downloads \
- Make sure that in the `SDK Tools` tab, the following is installed: NDK Side-By-Side v21.4.7075529 (equivalent to r21e). According to the [Qt Website](https://doc.qt.io/qt-5/android-getting-started.html), this is the one recommended for Qt5.15.2.
- Close Android Studio
-:hand: _Make sure that `$HOME/Android/Sdk/ndk/21.4.7075529/platforms` contains the folder `android-28`._
+> :triangular_flag_on_post: **Important**: Make sure that `$HOME/Android/Sdk/ndk/21.4.7075529/platforms` contains the folder `android-28`.
-:bulb: _The NDK corresponds to the minimum version required to run the app. Technically, you could choose a lower version than Android API 9.0 (android-28)._
+> :bulb: **Tip**: The NDK corresponds to the minimum version required to run the app. Technically, you could choose a lower version than Android API 9.0 (android-28).
#### 1.5.7. Install Qt from the installer
@@ -339,11 +368,11 @@ cd $HOME/Downloads \
Download the Qt version which matches the one in `$PYQT_CROM_DIR/examples/demo/demo_project/sysroot.toml` from the open source online installer:
```
-sudo apt-get install libxcb-xfixes0-dev libxcb-xinerama0 \
-&& cd $HOME/Downloads \
-&& wget https://d13lb3tujbc8s0.cloudfront.net/onlineinstallers/qt-unified-linux-x64-4.6.1-online.run \
-&& chmod +x qt*.run \
-&& ./qt-unified-linux-x64-4.6.1-online.run
+sudo apt-get install libxcb-xfixes0-dev libxcb-xinerama0 &&
+cd $HOME/Downloads &&
+wget https://d13lb3tujbc8s0.cloudfront.net/onlineinstallers/qt-unified-linux-x64-4.6.1-online.run &&
+chmod +x qt*.run &&
+./qt-unified-linux-x64-4.6.1-online.run
```
A Qt window will appear on which you can sign up:
@@ -355,9 +384,9 @@ A Qt window will appear on which you can sign up:
- Select folder location `$HOME/Qt5.15.2`
- Installation will start
-:hand: _Make sure that you can access `$HOME/Qt5.15.2/5.15.2` and that the folder `android` is located inside of it._
+> :triangular_flag_on_post: **Important**: Make sure that you can access `$HOME/Qt5.15.2/5.15.2` and that the folder `android` is located inside of it.
-:bulb: _The package `libxcb-xinerama0` is installed to prevent an issue inherent to Qt5.15 (but solved in Qt6) with `xcb` Qt platform plugin, according to [QT DEBUG reports](https://bugreports.qt.io/browse/QTBUG-84749)._
+> :bulb: **Tip**: The package `libxcb-xinerama0` is installed to prevent an issue inherent to Qt5.15 (but solved in Qt6) with `xcb` Qt platform plugin, according to [QT DEBUG reports](https://bugreports.qt.io/browse/QTBUG-84749).
### 1.6. Setup the environment variables
@@ -365,13 +394,13 @@ A Qt window will appear on which you can sign up:
Load the environment variables on terminal startup with:
```
-printf "%s\n" \
-"" \
-"# Load extra environment variables for PyQt-CroM" \
-"source $PYQT_CROM_DIR/utils/resources/path_setup.sh" \
-"" \
->> $HOME/.bashrc \
-&& source $HOME/.bashrc
+chmod +x $PYQT_CROM_DIR/utils/bash/setup_path.sh &&
+text_to_add="
+# Load extra environment variables for PyQt-CroM
+source $PYQT_CROM_DIR/utils/bash/setup_path.sh
+" &&
+printf "$text_to_add" >> $HOME/.bashrc &&
+source $HOME/.bashrc
```
@@ -380,12 +409,17 @@ printf "%s\n" \
Start the building process of the .apk with:
```
-cd $PYQT_CROM_DIR/utils \
-&& python3 build_app.py --pdt $PYQT_CROM_DIR/examples/demo/demo_project/config.pdt --jobs 1 --target android-64 --qmake $QT_DIR/android/bin/qmake --verbose
+cd $PYQT_CROM_DIR/utils/python &&
+python3 build_app.py \
+ --pdt $PYQT_CROM_DIR/examples/demo/demo_project/config.pdt \
+ --jobs 1 \
+ --target android-64 \
+ --qmake $QT_DIR/android/bin/qmake \
+ --verbose
```
-:hourglass_flowing_sand: _Let the app build (it may take a while). The app is built when you see "BUILD SUCCESSFUL"._
+> :hourglass_flowing_sand: **Wait**: Let the app build (it may take a while). The app is built when you see "BUILD SUCCESSFUL".
-:bulb: _The Android Manifest, `build.gradle` and `gradle.properties` can be checked at debug stage in `$PYQT_CROM_DIR/examples/demo/demo_project/build-android-64/android-build`._
+> :bulb: **Tip**: The Android Manifest, `build.gradle` and `gradle.properties` can be checked at debug stage in `$PYQT_CROM_DIR/examples/demo/demo_project/build-android-64/android-build`.
### 1.8. Run the app
@@ -397,69 +431,246 @@ You can then either:
- Install [BlueStacks](https://www.bluestacks.com/download.html) on Windows, enable hyper-V, open `my games` and install the .apk, run the app offline
- Setup a virtual device in Android Studio, install the app and run it on the virtual device
-:trophy: Congratulations! You have completed the tutorial. You can view the [demo app running on an Android phone](#pyqt5-demo-app-android-video).
+> :trophy: **Reward**: Congratulations! You have completed the tutorial. You can view the [demo app running on an Android phone](#pyqt5-demo-app-android-video).
[:arrow_heading_up: Back to TOP](#toc)
## 2. Generating your own app
-:mag: This section describes the steps to generate an `Android` app (.apk) from a custom `PyQt5` app.
+> :mag: **Info**: This section describes the steps to generate an `Android` app (.apk) from a custom `PyQt5` app.
-:bulb: _Make sure to go through the [Getting Started tutorial](#getting-started) to correctly setup your machine and environment._
+> :triangular_flag_on_post: **Important**: Make sure to go through the [Getting Started tutorial](#getting-started) to correctly setup your machine and environment.
-:warning: _In this section, placeholders are defined between `<>`. For instance, `` can be `demo_pkg` or `test_pkg`._
+> :warning: **Warning**: In this section, placeholders are defined between `<>`. For instance, `` can be `demo_pkg`, or `test_pkg`, or even `hello`.
### 2.1. Create your python package
Start by creating a project folder:
-* Create a folder `` wherever you want (and remember the absolute path of its parent folder referred to as ``)
+* Create a folder called `` wherever you want on your machine
+* Identify the absolute path of its parent folder, referred to as ``, so that your project folder is located as `/`
+
+> :bulb: **Tip**: For instance, the [demo project folder](examples/demo/demo_project) is called ` = demo_project` and the absolute path to its parent folder is ` = $PYQT_CROM_DIR/examples/demo`.
Inside of the project folder, create a python package to hold your `PyQt5` app:
-* Create a folder `/`
-* Populate `/` with at least `__init__.py` file and a `.py` script (you can add more files if required by your package)
+* Create a folder `/`, where `` is the name of your "root" python package (identifying the root of your python packages if you require nested python packages)
+* Populate `/` with at least `__init__.py` file and a `.py` script (you can add more files and folders / packages if required by your package)
_Note that the `.py` must contain a unique `main()` function (or any similar distinctive entry point)._
-:bulb: _An example of python package is given in the [demo project folder](examples/demo/demo_project/demo_pkg)._
+> :bulb: **Tip**: An example of root python package (called `demo_pkg`) is given in the [demo project folder](examples/demo/demo_project).
+
+
+#### 2.1.1. Advanced python package
+
+If you wish to create an "advanced" root python package (i.e. including nested python packages), you need to understand the optimal structure of the root package, which is inspired from the [Standard python package](https://docs.python-guide.org/writing/structure/) structure (i.e. containing the `setup.py` file).
+
+This is the structure of an advanced python package:
+
+* `` (located at the same level as the [config.pdt file](#pdt-configuration))
+ * `__init__.py` (required to consider `` a python package)
+ * `` (name of your main python package)
+ * `__init__.py`
+ * `main_file.py` (`main()` from this file is usually considered as entrypoint for the [config.pdt file](#pdt-configuration))
+ * `` (name of your second python package)
+ * `__init__.py`
+ * `second_file.py` (python module containing functionalities useful for `main_file.py` for instance)
+
+To import the functionalities from `second_file.py` into `main_file.py`, you need to refer to the ``, because that is the only package actually identified by the [config.pdt file](#pdt-configuration). This means: `from root_pkg_name.second_pkg_name.second_file import functionality`.
+
+> :warning: **Warning**: If you want to test your "advanced" root python package on your source OS, you need to include the paths of each python package with `sys.path.append`.
+
+> :bulb: **Tip**: An example of "advanced" root python package is given in [example external project](examples/external/external_python_project) and the root package is called `externalpy_pkg`.
+
+
+#### 2.1.2. Non-python file management
+
+If you wish to add resources (non-python files) to your root python package, you need to understand how to fetch the data from your python scripts / modules.
+
+This is the structure of a python package including non-python files:
+
+* `` (located at the same level as the [config.pdt file](#pdt-configuration))
+ * `__init__.py` (required to consider `` a python package)
+ * `` (name of your main python package)
+ * `__init__.py`
+ * `main_file.py` (`main()` from this file is usually considered as entrypoint for the [config.pdt file](#pdt-configuration))
+ * `data` (name of the "package" containing non-python files)
+ * `__init__.py` (actually required for `importlib.resources` to identify the data inside the "package")
+ * `.png` (image which is not a python script)
+
+To perform operations on `img.png` from `main_file.py`, it is recommended to use [importlib.resources built-in python module](https://docs.python.org/3.10/library/importlib.html#module-importlib.resources). The module can provide a path to the image contained within the built package. For instance: `with importlib.resources.path(os.path.join('root_pkg_name', 'data'), 'img.png') as path:`.
+
+:warning: _Multiplexed path (dotted chain of packages) only works on directories, therefore, it is necessary to reference the data package name (nested package) with `os.path.join('root_pkg_name', 'data')` instead of `root_pkg_name.data` for the built application (Android app for instance) due to how `pyqtdeploy` freezes the resources._
+
+> :warning: **Warning**: If you want to test your root python package including non-python files on your source OS, you need to include the paths of each python package with `sys.path.append`.
+
+> :bulb: **Tip**: An example of a root python package including non-python files is given in [example external project](examples/external/external_python_project) and the root package is called `externalpy_pkg`.
+
+
+#### 2.1.3. Standard python package
+
+If you have created a [Standard python package](https://docs.python-guide.org/writing/structure/), you can generate its wheels and load your package into your application by following the [non-standard python module with wheels tutorial](#sysroot-non-standard-python-modules-with-wheels).
### 2.2. Configure the sysroot
Inside of your `` folder, add the sysroot config to specify application dependencies:
-* Create a file called `sysroot.toml` and populate it with all the modules used by your app.
+* Create / copy a file called `sysroot.toml` and populate it with all the non-python modules, as well as the non-standard python modules, used by your app.
+
+> :bulb: **Tip**: An example of sysroot config is given in the [demo project folder](examples/demo/demo_project).
+
+
+#### 2.2.1. Specify non-python modules
+
+Non-python modules are modules and libraries that are not related to python.
+
+> :bulb: **Tip**: To display all options / tags available for the sysroot packages listed in the sysroot file, type in a terminal: `pyqtdeploy-sysroot --options `.
+
+> :bulb: **Tip**: For instance, in the [demo sysroot](examples/demo/demo_project/sysroot.toml), `[Qt]` is a non-python module and some of its options / tags include: `disabled_features`, `edition`, `ssl`.
+
+> :warning: **Warning**: The following non-python modules are compulsory for the `PyQt5` app to compile / work as expected, and must be listed at the end of the sysroot file.
+
+* `[Qt]`
+* `[SIP]`
+* `[zlib]` (Note that `zlib` is also a standard python module, but it is usually installed from external source in `pyqtdeploy`)
+
+
+#### 2.2.2. Specify non-standard python modules
+
+Non-standard python modules are modules and libraries that are python libraries, but not from the [standard python library](https://docs.python.org/3/library/index.html).
+
+> :bulb: **Tip**: In the [demo sysroot](examples/demo/demo_project/sysroot.toml), `[PyQt]` is a non-standard python module for instance. Whenever you import a sub-module from the main module, you need to update the sysroot. For instance, if you imported `QtSql` in your `PyQt5` app (that you want to release for Android), then you must include `QtSql` in `[PyQt.android] installed_modules`. A relevant example is provided in [example database sysroot](exmaples/database/database_management_project).
+
+> :warning: **Warning**: In the [demo sysroot](examples/demo/demo_project/sysroot.toml), `[PyQt]` is a non-standard python module that is obvisouly compulsory for `PyQt` apps (`PyQt5` for instance).
+
+There are 2 ways to install non-standard python modules:
+* From wheels (built distribution)
+* From source (source distribution)
+
+
+##### 2.2.2.1. Modules with wheels
+
+Python wheels are the new standard of pre-built binary package format for Python modules and libraries. This means that Python modules are ready to be installed by unpacking, without having to build anything.
+
+The advantage of python wheels is that they enable faster installation of a package, compared to a package that needs to get built.
+
+The limitation of python wheels is that they are platform and version dependent, so they are tied to a specific version of Python on a specific platform. Sometimes finding the right wheel can be challenging. The right platform for a wheel is the platform on which the wheel is unpacked. That means, if you build apps from Linux OS (source OS) for Android OS (target OS), get the wheels for Linux. Once unpacked, the wheels content is retrieved by utilities from `pyqtdeploy` and frozen into the "built" app.
+
+> :bulb: **Tip**: [Python Wheels website](https://pythonwheels.com/) offers an overview of all Python modules with wheels available.
+
+The recommended way to specify a non-standard python module with wheels in the sysroot is:
+
+1) Identify from [PyPI](https://pypi.org/) the `` and `` you are looking for (for instance `pyyaml` version `6.0.1`, as demonstrated in [example external project](examples/external/external_python_project))
-_For instance, if you imported `QtSql` in your `PyQt5` app, then you must include `QtSql` in `[PyQt.android] installed_modules`._
+2) Install the desired `` (with `pip3 install `) on your source OS (for instance Linux) and make sure that your `PyQt5` application works as expected
-:bulb: _An example of sysroot config is given in the [demo project folder](examples/demo/demo_project)._
+3) Get the wheels for the specific python package name and version, using the script:
+
+```
+chmod +x $PYQT_CROM_DIR/utils/bash/get_python_package_wheel.sh &&
+$PYQT_CROM_DIR/utils/bash/get_python_package_wheel.sh
+```
+
+> :bulb: **Tip**: The script returns the name of the wheels (which we will call ``) suited for your source OS specifications, which you can copy-paste.
+
+> :bulb: **Tip**: In the example, the script returns `PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl`.
+
+:warning: _If your desired non-standard python module does not have wheels:_
+* Request them to the maintainer
+* Refer to sub-section [Sysroot non-standard python modules without wheels](#sysroot-non-standard-python-modules-without-wheels)
+* Refer to sub-section [Create custom sysroot plugins](#sysroot-custom-plugins)
+
+4) Get the `` used to refer to `` in `site-packages` with:
+
+```
+python3 $PYQT_CROM_DIR/utils/python/py_package_dependency_collector.py -n
+```
+
+The `` will be displayed at the top of the block between `==========`.
+
+> :bulb: **Tip**: Sometimes `` is different than `` and this is the name used by `pyqtdeploy` to retrieve the python package name (for instance `pyyaml` is used on [PyPI](https://pypi.org/), but `yaml` folder is used in `$PYQT_CROM_DIR/venv/pyqt-crom-venv/lib/python3.10/site-packages`, so `=yaml`).
+
+5) Collect all the `` required by `` with:
+
+```
+python3 $PYQT_CROM_DIR/utils/python/py_package_dependency_collector.py -n
+```
+
+The `` for `` will be displayed at the bottom of the block between `==========`.
+
+> :bulb: **Tip**: You can get all `` from python package path with `cd python3 $PYQT_CROM_DIR/utils/python/py_package_dependency_collector.py -p `.
+
+> :warning: **Warning**: If `` depends on other non-standard python modules (called `` and displayed in the middle of the block between `==========`), apply the [sysroot update process](sysroot-configuration) for each of these modules in addition to the current one. For instance, `pandas` depends on `numpy` among other modules.
+
+> :bulb: **Tip**: Extra tip: `sys` module does not need to be explicitly imported.
+
+6) Add a section to the `sysroot.toml` reflecting the `` dependency, before the [required non-python modules](#sysroot-non-python-modules):
+
+```
+[]
+plugin = "wheel"
+wheel = ""
+dependencies =
+```
+
+> :bulb: **Tip**: You can exclude some files from being added to the built app with the line `exclusions: []`, as shown on [Riverbank website](https://www.riverbankcomputing.com/static/Docs/pyqtdeploy/sysroot.html#defining-a-component-using-the-sysroot-specification-file).
+
+> :bulb: **Tip**: The example of `yaml` is provided in [example external project sysroot](examples/external/external_python_project/sysroot.toml).
+
+
+##### 2.2.2.2. Modules without wheels
+
+> :warning: **Warning**: This case has not been documented yet.
+
+
+#### 2.2.3. Specify standard python modules
+
+Standard python modules come from the [standard python library](https://docs.python.org/3/library/index.html).
+
+> :warning: **Warning**: In the `sysroot.toml` file, `[Python]` is the only standard python "module" allowed and it is compulsory for `PyQt5` apps.
+
+> :warning: **Warning**: All standard python modules (apart from `Python` itself) must be added / removed from the `.pdt`.
+
+
+#### 2.2.4. Create custom plugins
+
+In case you cannot import / find the module you need, you can create your own sysroot plugins.
+
+To create your own sysroot plugins, follow the [Riverbank sysroot plugin tutorial](https://www.riverbankcomputing.com/static/Docs/pyqtdeploy/sysroot.html#defining-a-component-using-a-plugin).
+
+> :bulb: **Tip**: Example and original plugins provided with `pyqtdeploy` can be found in `pyqtdeploy-3.3.0/pyqtdeploy/sysroot/plugins`.
+
+> :bulb: **Tip**: Refer to [Riverbank website](https://www.riverbankcomputing.com/static/Docs/pyqtdeploy/sysroot.html) for more in-depth information about sysroot (System Root).
### 2.3. Configure the pdt
-Inside of your `` folder, add the pdt config to specify python dependencies and build requests:
-* Create a file called `config.pdt` and configure it
+Inside of your `` folder, add the pdt config to further specify python dependencies and build requests:
+* Create / copy a file called `config.pdt` and configure it
+
+> :bulb: **Tip**: An example of pdt config is given in the [demo project folder](examples/demo/demo_project).
+
+> :bulb: **Tip**: Interact with pdt config through the pyqtdeploy command: `pyqtdeploy `.
To configure the `config.pdt` file, you need to understand and use the various areas shown in the following pictures:
-* Open the `config.pdt` file with: `cd / && pyqtdeploy config.pdt`.
+* Open the `config.pdt` file with: `cd / && pyqtdeploy config.pdt`.
* [AREA 1] In the `Application source tab > Name area`, add the `` with no spaces. This is the app name shown at export time.
-* [AREA 2] In the `Application source tab`, click on the `Scan` button to select your `/` folder.
+* [AREA 2] In the `Application source tab`, click on the `Scan` button to select your `/` folder.
* [AREA 3] In the `Application source tab > Application Package Directory area`, tick the files and folders you want to include into your application.
-* [AREA 4] In the `Application source tab > Entry point area`, add the `.:main` to tell where the entry point of your application is.
+* [AREA 4] In the `Application source tab > Entry point area`, add the `.:main` to tell where the entry point of your application is.
* [AREA 5] In the `Packages tab > Sysroot specification file area`, click on the file icon to the right to select the desired `sysroot.toml` file.
* [AREA 6] In the `Packages tab > Standard Library area`, tick all the python libraries you have imported in your python application. You can leave the coloured blocks as they import required libraries to build the python application.
-* [AREA 7] In the `Packages tab > Core Packages area`, tick all the external packages that you have imported in your python application. You can leave the coloured blocks as they import required libraries to build the python application.
+* [AREA 7] In the `Packages tab > Other Packages area`, tick all the external packages that you have imported in your python application. You can leave the coloured blocks as they import required libraries to build the python application.
* Save the `config.pdt` with `Ctrl + S` and close it.
-:bulb: _An example of pdt config is given in the [demo project folder](examples/demo/demo_project)._
-
-:bulb: _For more information about pdt files, read the [Riverbank website page](https://www.riverbankcomputing.com/static/Docs/pyqtdeploy/pyqtdeploy.html)._
+> :bulb: **Tip**: For more information about pdt files, read the [Riverbank website page](https://www.riverbankcomputing.com/static/Docs/pyqtdeploy/pyqtdeploy.html).
### 2.4. Build the app
@@ -467,11 +678,16 @@ To configure the `config.pdt` file, you need to understand and use the various a
Generate the `.apk` file using:
```
-cd $PYQT_CROM_DIR/utils \
-&& python3 build_app.py --pdt //config.pdt --jobs 1 --target android-64 --qmake $QT_DIR/android/bin/qmake --verbose
+cd $PYQT_CROM_DIR/utils/python &&
+python3 build_app.py \
+ --pdt //config.pdt \
+ --jobs 1 \
+ --target android-64 \
+ --qmake $QT_DIR/android/bin/qmake \
+ --verbose
```
-:bulb: _The `.apk` can be found in the `//releases/` folder._
+> :bulb: **Tip**: The `.apk` can be found in the `//releases/` folder.
### 2.5. Debug the app
@@ -487,7 +703,7 @@ To setup an Emulator, refer to [Android Emulator setup](docs/troubleshooting/com
## 3. Enhancing your app
-:mag: This section offers feature examples to enhance your custom PyQt app.
+> :mag: **Info**: This section offers feature examples to enhance your custom PyQt app.
To discover or analyse PyQt5 features, look at the section dedicated to [PyQt5 features](docs/features/pyqt5_features.md).
@@ -496,16 +712,18 @@ To discover or analyse PyQt5 features, look at the section dedicated to [PyQt5 f
## 4. Releasing your app
-:mag: This section provides a detailed tutorial on how to release your custom app onto main app stores.
+> :mag: **Info**: This section provides a detailed tutorial on how to release your custom app onto main app stores.
To learn more about releasing your own app on app stores, follow the online tutorial.
+> :warning: **Warning**: The tutorial has not been released yet.
+
[:arrow_heading_up: Back to TOP](#toc)
## 5. Troubleshooting
-:mag: This section offers advice to get unstuck when creating your app.
+> :mag: **Info**: This section offers advice to get unstuck when creating your app.
To find out about common setup and running issues, look at the section dedicated to [Common issues](docs/troubleshooting/common_issues.md).
@@ -514,7 +732,7 @@ To find out about common setup and running issues, look at the section dedicated
## 6. Roadmap
-:mag: This section describes the broad roadmap to deliver a functional repo.
+> :mag: **Info**: This section describes the broad roadmap to deliver a functional repo.
![Roadmap Diagram](http://www.plantuml.com/plantuml/proxy?cache=no&fmt=svg&src=https://raw.githubusercontent.com/achille-martin/pyqt-crom/main/docs/roadmap/roadmap.iuml)
@@ -525,11 +743,11 @@ To find out about common setup and running issues, look at the section dedicated
Repository created and maintained by [Achille Martin](https://github.com/achille-martin).
-:clap: Gigantic thanks to [Phil Thompson](https://pypi.org/user/PhilThompson/), the creator and maintainer of [PyQt](https://riverbankcomputing.com/software/pyqt/intro) and [pyqtdeploy](https://pypi.org/project/pyqtdeploy/).
+:clap: **Thank**: Gigantic thanks to [Phil Thompson](https://pypi.org/user/PhilThompson/), the creator and maintainer of [PyQt](https://riverbankcomputing.com/software/pyqt/intro) and [pyqtdeploy](https://pypi.org/project/pyqtdeploy/).
-:heartpulse: Sincere thanks to the well-intentioned international developers who create apps benefitting the community.
+:heartpulse: **Thank**: Sincere thanks to the well-intentioned international developers who create apps benefitting the community.
-_For more information about licencing details, take a look at the section dedicated to [Licencing](docs/licencing/licencing_information.md)._
+:mortar_board: For more information about licencing details, take a look at the section dedicated to [Licencing](docs/licencing/licencing_information.md).
[:arrow_heading_up: Back to TOP](#toc)
diff --git a/docs/features/pyqt5_features.md b/docs/features/pyqt5_features.md
index f4a5c3e..880d101 100644
--- a/docs/features/pyqt5_features.md
+++ b/docs/features/pyqt5_features.md
@@ -1,6 +1,6 @@
# PyQt5 Features
-:mag: This section introduces the main PyQt5 features needed to create a custom standard app.
+> :mag: **Info**: This section introduces the main PyQt5 features needed to create a custom standard app.
## Table of Contents
@@ -36,7 +36,7 @@ PyQt5 lets you work with many other database engines as shown on [tutorialspoint
#### 1.2. Setup for database management
-Before attempting to run any `SQLite`-related actions, make sure that the library is available on your machine:
+> :triangular_flag_on_post: **Important**: Before attempting to run any `SQLite`-related actions, make sure that the library is available on your machine with the following command.
```
sudo apt-get install sqlite3
@@ -58,7 +58,7 @@ A dialog window will pop up in which you can perform the following:
- Add a row to the database
- Remove a row from the database
-You can view the content of the generated and edited database at any time outside of the application with:
+> :bulb: **Tip**: You can view the content of the generated and edited database at any time outside of the application with the following command.
```
cd $PYQT_CROM_DIR/examples/database
@@ -81,7 +81,7 @@ This demo app is built on the one highlighted in the [Getting started](../../REA
- Once the pop-up has been acknowledged, a database (called `sportsdatabase.db`) is created in the `home` folder as shown in the alert window, if not already existing
- In the dialog window displaying the content of the database, rows can be added, removed or edited
-:bulb: _You can view the content of `sportsdatabase.db` at any time by following the instructions in [Example PyQt5 app with database](#database-pyqt5-demo-app) after ensuring that your [Database manager](#database-management-setup) is correctly setup._
+> :bulb: **Tip**: You can view the content of `sportsdatabase.db` at any time by following the instructions in [Example PyQt5 app with database](#database-pyqt5-demo-app) after ensuring that your [Database manager](#database-management-setup) is correctly setup.
@@ -110,7 +110,6 @@ As we are showcasing a prototyping tool for mobile apps, we have decided to expl
To visualise a basic example of 2D Graphics in a PyQt app, run the following:
-
```
cd $PYQT_CROM_DIR/examples/graphics
python3 pyqt5_app_with_graphics.py
@@ -177,7 +176,7 @@ cd $PYQT_CROM_DIR/examples/network
python3 pyqt5_app_with_bluetooth.py
```
-:bulb: _If you encounter issues with Bluetooth, please refer to the [Bluetooth troubleshooting](../troubleshooting/common_issues.md#virtual-machine-bluetooth)._
+> :bulb: **Tip**: If you encounter issues with Bluetooth, please refer to the [Bluetooth troubleshooting](../troubleshooting/common_issues.md#virtual-machine-bluetooth).
The example Bluetooth app provides the following experience:
- A window appears on the screen with 2 buttons: Search for Bluetooth devices and EXIT
@@ -186,8 +185,6 @@ The example Bluetooth app provides the following experience:
- Once your Bluetooth is ON, click again on Search for Bluetooth devices so that the app can search for nearby devices for 5 seconds
- The app displays the list of found nearby devices on the main window
-
-
#### 3.3. Example operational PyQt5 Bluetooth app
@@ -198,9 +195,9 @@ cd $PYQT_CROM_DIR/examples/network/bluetooth_scanner_project/bluetooth_scanner_p
python3 operational_pyqt5_app_with_bluetooth.py
```
-:bulb: _If you encounter issues with Bluetooth, please refer to the [Bluetooth troubleshooting](../troubleshooting/common_issues.md#virtual-machine-bluetooth)._
+> :bulb: **Tip**: If you encounter issues with Bluetooth, please refer to the [Bluetooth troubleshooting](../troubleshooting/common_issues.md#virtual-machine-bluetooth).
-The operational example Bluetooth app has a similar behaviour to the [example Bluetooth app](#bluetooth-pyqt5-demo-app).
+> :bulb: **Tip**: The operational example Bluetooth app has a similar behaviour to the [example Bluetooth app](#bluetooth-pyqt5-demo-app).
diff --git a/docs/licencing/licencing_information.md b/docs/licencing/licencing_information.md
index f651d79..fdba876 100644
--- a/docs/licencing/licencing_information.md
+++ b/docs/licencing/licencing_information.md
@@ -1,6 +1,6 @@
# Licencing information
-:mag: This section gathers the licences of the main dependencies.
+> :mag: **Info**: This section gathers the licences of the main dependencies.
| Dependency | Version | Licence |
| --- | --- | --- |
diff --git a/docs/troubleshooting/common_issues.md b/docs/troubleshooting/common_issues.md
index 2c309ff..6094a72 100644
--- a/docs/troubleshooting/common_issues.md
+++ b/docs/troubleshooting/common_issues.md
@@ -1,6 +1,6 @@
# Common issues
-:mag: This section walks you through tips and fixes for the main challenges you might encounter while setting up the repo or running the scripts.
+> :mag: **Info**: This section walks you through tips and fixes for the main challenges you might encounter while setting up the repo or running the scripts.
## Table of Contents
@@ -61,13 +61,32 @@ sudo apt-get install libxcb-xinerama0
### 4. Setup repo with a Virtual Machine
-To setup a Linux Virtual Machine on Windows via VirtualBox, follow [It's FOSS virtualbox setup tutorial](https://itsfoss.com/install-linux-in-virtualbox/).
-
-If you would prefer to setup a Linux Virtual Machine on MacOS via VirtualBox, follow [TecAdmin virtualbox setup tutorial](https://tecadmin.net/how-to-install-virtualbox-on-macos/).
-
-It is also recommended to install the VirtualBox Guest Additions. Follow the [LinuxTechi guest addition setup tutorial](https://www.linuxtechi.com/install-virtualbox-guest-additions-on-ubuntu/) for more information.
-
-:bulb: _It is also recommended to set the size of the Virtual Machine to at least 50GB so that there is enough space to download and install all dependencies._
+Follow the relevant tutorial to setup a Virtual Machine on your machine.
+
+
+
+> :bulb: **Tip**: It is recommended to install the VirtualBox Guest Additions. Follow the [LinuxTechi guest addition setup tutorial](https://www.linuxtechi.com/install-virtualbox-guest-additions-on-ubuntu/) for more information.
+
+> :bulb: **Tip**: It is also recommended to set the size of the Virtual Machine to at least 50GB so that there is enough space to download and install all dependencies.
### 5. Setup Bluetooth in a Virtual Machine
@@ -88,7 +107,7 @@ If you are using VirtualBox (Virtual Machine) and you want to run a pyqt5 app re
To setup an Android Emulator, it is recommended to use Android Studio.
-_If you want to set up the Android Emulator in VirtualBox, please refer to [this issue](https://github.com/achille-martin/pyqt-crom/issues/12)._
+> :bulb: **Tip**: If you want to set up the Android Emulator in VirtualBox, please refer to [this issue](https://github.com/achille-martin/pyqt-crom/issues/12).
To setup the Android Emulator in Ubuntu, make sure that you have:
* Android Studio installed (refer to [External dependencies setup](../../README.md#external-dependency-installation) if needed)
@@ -112,7 +131,7 @@ sudo chown /dev/kvm
Once the Android Emulator is set up and running, you can drag and drop your `.apk` to install it and run it.
-If you wish to access more Android logs, please refer to [this issue](https://github.com/achille-martin/pyqt-crom/issues/12), which mentions tips for `adb`, the Android Debug Bridge.
+> :bulb: **Tip**: If you wish to access more Android logs, please refer to [this issue](https://github.com/achille-martin/pyqt-crom/issues/12), which mentions tips for `adb`, the Android Debug Bridge.
[:arrow_heading_up: Back to TOP](#toc)
diff --git a/examples/demo/module_finder_project/config.pdt b/examples/demo/module_finder_project/config.pdt
new file mode 100644
index 0000000..de0c610
--- /dev/null
+++ b/examples/demo/module_finder_project/config.pdt
@@ -0,0 +1,27 @@
+version = 0
+sysroot = "sysroot.toml"
+sysroots_dir = ""
+parts = [ "PyQt:PyQt5.QtWidgets", "Python:logging",]
+
+[Application]
+entry_point = "modfinder_pkg.modfinder_app:main"
+is_console = false
+is_bundle = false
+name = "ModFinderApp"
+qmake_configuration = ""
+script = ""
+syspath = ""
+
+[Application.Package]
+name = "modfinder_pkg"
+exclude = [ "*.pyc", "*.pyd", "*.pyo", "*.pyx", "*.pxi", "__pycache__", "*-info", "EGG_INFO", "*.so",]
+[[Application.Package.Content]]
+name = "__init__.py"
+included = true
+is_directory = false
+
+[[Application.Package.Content]]
+name = "modfinder_app.py"
+included = true
+is_directory = false
+
diff --git a/utils/__init__.py b/examples/demo/module_finder_project/modfinder_pkg/__init__.py
similarity index 100%
rename from utils/__init__.py
rename to examples/demo/module_finder_project/modfinder_pkg/__init__.py
diff --git a/examples/demo/module_finder_project/modfinder_pkg/modfinder_app.py b/examples/demo/module_finder_project/modfinder_pkg/modfinder_app.py
new file mode 100755
index 0000000..1d03899
--- /dev/null
+++ b/examples/demo/module_finder_project/modfinder_pkg/modfinder_app.py
@@ -0,0 +1,195 @@
+#!/usr/bin/env python3
+
+# MIT License
+
+# Copyright (c) 2024 Achille MARTIN
+
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+
+# Module Finder PyQt5 app
+# Inspired by Martin Fitzpatrick from https://www.pythonguis.com/tutorials/creating-your-first-pyqt-window/
+#
+# This app helps you identify
+# the python module dependencies
+# related to specific non-standard python libraries
+
+
+## Imports
+
+import sys
+sys_modules = dict(sorted(sys.modules.items()))
+sys_modules_base = list(sys_modules.keys())
+sys_modules_base.sort()
+
+import_error_msg = ""
+sys_modules_base_with_pkg = None
+sys_modules_pkg = None
+try:
+ # Import your non-standard python pkg
+ import numpy
+ sys_modules = dict(sorted(sys.modules.items()))
+ sys_modules_base_with_pkg = list(sys_modules.keys())
+ sys_modules_base_with_pkg.sort()
+ sys_modules_pkg = list(
+ set(
+ sys_modules_base_with_pkg
+ ).difference(
+ set(sys_modules_base)
+ )
+ )
+ sys_modules_pkg.sort()
+except Exception as e:
+ import_error_msg = f"Exception caught: {e}"
+
+import os
+import logging as log_tool # The logging library for debugging
+from PyQt5.QtWidgets import QApplication, QMainWindow, QPushButton, QMessageBox
+from PyQt5.QtCore import QStandardPaths
+
+built_in_modules = list(sys.stdlib_module_names)
+built_in_modules.sort()
+
+## Main variables and objects
+
+# Retrieve app data folder path reference
+std_app_data_folder_path_list = QStandardPaths.writableLocation(
+ QStandardPaths.AppDataLocation
+)
+
+# Define custom app data folder path
+std_app_data_folder_path = ""
+if isinstance(std_app_data_folder_path_list, list):
+ std_app_data_folder_path = std_app_data_folder_path_list[0]
+else:
+ std_app_data_folder_path = str(std_app_data_folder_path_list)
+app_data_folder_path = os.path.join(
+ std_app_data_folder_path,
+ 'pyqt5_app_data',
+)
+
+# Generate custom app data folder at convenient writable location
+os.makedirs(app_data_folder_path, exist_ok = True)
+
+# Set logger config and instantiate object
+logger_logging_level = "DEBUG"
+logger_output_file_name = "modfinder_app.log"
+logger_output_prefix_format = "[%(asctime)s] [%(levelname)s] - %(message)s"
+logger = log_tool.getLogger(__name__)
+logger.setLevel(logger_logging_level)
+logger_output_file_path = os.path.join(app_data_folder_path, str(logger_output_file_name))
+file_handler = log_tool.FileHandler(logger_output_file_path)
+formatter = log_tool.Formatter(logger_output_prefix_format)
+file_handler.setFormatter(formatter)
+logger.addHandler(file_handler)
+
+# Set additional log output location
+# for non-root debugging
+# when flag is_non_root_debug_active is set to True
+is_non_root_debug_active = True
+# WARNING: this feature might require
+# to allow storage access permission before launching the app
+if is_non_root_debug_active:
+ # Retrieve documents folder path reference
+ # to save logs at a location accessible by non-root users
+ std_documents_folder_path_list = QStandardPaths.writableLocation(
+ QStandardPaths.DocumentsLocation
+ )
+ # Define custom alternative app data folder path
+ std_documents_folder_path = ""
+ if isinstance(std_documents_folder_path_list, list):
+ std_documents_folder_path = std_documents_folder_path_list[0]
+ else:
+ std_documents_folder_path = str(std_documents_folder_path_list)
+ alternative_app_data_folder_path = os.path.join(
+ std_documents_folder_path,
+ 'pyqt5_app_data',
+ )
+ # Generate alternative app data folder at convenient writable location
+ os.makedirs(alternative_app_data_folder_path, exist_ok = True)
+ # Add log file output alternative to logger
+ logger_output_file_path_alternative = os.path.join(
+ alternative_app_data_folder_path,
+ str(logger_output_file_name)
+ )
+ file_handler_alternative = log_tool.FileHandler(
+ logger_output_file_path_alternative
+ )
+ formatter_alternative = log_tool.Formatter(logger_output_prefix_format)
+ file_handler_alternative.setFormatter(formatter_alternative)
+ logger.addHandler(file_handler_alternative)
+
+## Investigating the imports
+logger.debug("---- SYS MODULES BASE ----")
+logger.debug(sys_modules_base)
+logger.debug("---- SYS MODULES BASE WITH PKG ----")
+logger.debug(sys_modules_base_with_pkg)
+logger.debug("---- SYS MODULES NUMPY ----")
+logger.debug(sys_modules_pkg)
+logger.debug("---- PYTHON BUILT-IN MODULES ----")
+logger.debug(built_in_modules)
+logger.debug("---- IMPORT ERRORS ----")
+logger.debug(import_error_msg)
+
+
+## Class definition
+
+# Subclass QMainWindow to customize your application's main window
+class MainWindow(QMainWindow):
+ def __init__(self):
+ super().__init__()
+
+ self.setWindowTitle("Module Finder PyQt5 app")
+
+ button = QPushButton("Press Here for the magic")
+
+ button.setCheckable(True)
+ button.clicked.connect(self.on_button_clicked)
+
+ # Set properties of the widget in the Window.
+ self.setCentralWidget(button)
+
+
+ def on_button_clicked(self):
+ print("Button has been clicked!")
+ alert = QMessageBox()
+ alert.setText('You clicked the button!')
+ alert.exec()
+
+
+## Application definition
+
+def main():
+ # Only one QApplication instance is needed per application.
+ # Pass in sys.argv to allow command line arguments for the app: `QApplication(sys.argv)`
+ # If command line arguments are not needed, use: `QApplication([])`
+ app = QApplication([])
+
+ # Create a QMainWindow object which represents the Main Window.
+ main_window = MainWindow()
+ main_window.showMaximized() # This line will show windows that are normally hidden. Plus, it will maximise the main window.
+
+ # Start the application event loop.
+ app.exec()
+
+ # The application will only reach here when exiting or event loop has stopped.
+
+if __name__ == "__main__":
+ # This section needs to only define main
+ # due to how pyqtdeploy is implemented to build packages
+ main()
diff --git a/examples/demo/module_finder_project/sysroot.toml b/examples/demo/module_finder_project/sysroot.toml
new file mode 100644
index 0000000..9a02fdb
--- /dev/null
+++ b/examples/demo/module_finder_project/sysroot.toml
@@ -0,0 +1,77 @@
+# The sysroot for the demo application.
+
+# Python ######################################################################
+
+[Python]
+version = "3.10.12"
+install_host_from_source = true
+
+[Python.win]
+install_host_from_source = false
+
+# PyQt ########################################################################
+
+[PyQt]
+version = "5.15.10"
+
+[PyQt.android]
+disabled_features = ["PyQt_Desktop_OpenGL", "PyQt_Printer"]
+installed_modules = ["QtCore", "QtGui", "QtWidgets"]
+
+[PyQt.ios]
+disabled_features = ["PyQt_Desktop_OpenGL", "PyQt_MacOSXOnly",
+ "PyQt_MacCocoaViewContainer", "PyQt_Printer", "PyQt_Process",
+ "PyQt_NotBootstrapped"]
+installed_modules = ["QtCore", "QtGui", "QtWidgets"]
+
+[PyQt.linux]
+installed_modules = ["QtCore", "QtGui", "QtWidgets"]
+
+[PyQt.macos]
+installed_modules = ["QtCore", "QtGui", "QtWidgets"]
+
+[PyQt.win]
+disabled_features = ["PyQt_Desktop_OpenGL"]
+installed_modules = ["QtCore", "QtGui", "QtWidgets"]
+
+# Qt ##########################################################################
+
+[Qt]
+version = "5.15.2"
+edition = "opensource"
+configure_options = ["-opengl", "desktop", "-no-dbus", "-qt-pcre"]
+skip = ["qtactiveqt", "qtdoc", "qtgamepad",
+ "qtquickcontrols", "qtquickcontrols2",
+ "qtremoteobjects", "qtscript", "qtscxml", "qtserialbus",
+ "qtserialport", "qtspeech", "qtsvg", "qttools", "qttranslations",
+ "qtwayland", "qtwebchannel", "qtwebengine", "qtwebsockets",
+ "qtwebview", "qtxmlpatterns"]
+
+[Qt.android]
+install_from_source = false
+
+[Qt.ios]
+install_from_source = false
+
+[Qt.linux]
+
+[Qt.macos]
+
+[Qt.win]
+static_msvc_runtime = true
+
+# SIP #########################################################################
+
+[SIP]
+abi_major_version = 12
+module_name = "PyQt5.sip"
+
+# zlib ########################################################################
+
+[zlib]
+install_from_source = false
+
+[zlib.win]
+version = "1.2.13"
+install_from_source = true
+static_msvc_runtime = true
diff --git a/examples/external/external_python_project/config.pdt b/examples/external/external_python_project/config.pdt
new file mode 100644
index 0000000..fad220a
--- /dev/null
+++ b/examples/external/external_python_project/config.pdt
@@ -0,0 +1,72 @@
+version = 0
+sysroot = "sysroot.toml"
+sysroots_dir = ""
+parts = [ "PyQt:PyQt5.QtWidgets", "yaml:yaml", "Python:logging", "Python:importlib.resources",]
+
+[Application]
+entry_point = "externalpy_pkg.externalpy.pyqt5_app_with_yaml:main"
+is_console = false
+is_bundle = false
+name = "ExternalPyApp"
+qmake_configuration = ""
+script = ""
+syspath = ""
+
+[Application.Package]
+name = "externalpy_pkg"
+exclude = [ "*.pyc", "*.pyd", "*.pyo", "*.pyx", "*.pxi", "__pycache__", "*-info", "EGG_INFO", "*.so",]
+[[Application.Package.Content]]
+name = "__init__.py"
+included = true
+is_directory = false
+
+[[Application.Package.Content]]
+name = "config"
+included = true
+is_directory = true
+[[Application.Package.Content.Content]]
+name = "__init__.py"
+included = true
+is_directory = false
+
+[[Application.Package.Content.Content]]
+name = "app_config.yaml"
+included = true
+is_directory = false
+
+
+[[Application.Package.Content]]
+name = "externalpy"
+included = true
+is_directory = true
+[[Application.Package.Content.Content]]
+name = "__init__.py"
+included = true
+is_directory = false
+
+[[Application.Package.Content.Content]]
+name = "pyqt5_app_with_yaml.py"
+included = true
+is_directory = false
+
+
+[[Application.Package.Content]]
+name = "README.md"
+included = true
+is_directory = false
+
+[[Application.Package.Content]]
+name = "tools"
+included = true
+is_directory = true
+[[Application.Package.Content.Content]]
+name = "__init__.py"
+included = true
+is_directory = false
+
+[[Application.Package.Content.Content]]
+name = "validation_tools.py"
+included = true
+is_directory = false
+
+
diff --git a/examples/external/external_python_project/externalpy_pkg/README.md b/examples/external/external_python_project/externalpy_pkg/README.md
new file mode 100644
index 0000000..4065f3e
--- /dev/null
+++ b/examples/external/external_python_project/externalpy_pkg/README.md
@@ -0,0 +1,7 @@
+# Externalpy
+
+---
+
+## Introduction
+
+This is an example standard python package demonstrating the pyqtdeploy capability of integrating non-standard (external) python packages into a generated application.
diff --git a/examples/external/external_python_project/externalpy_pkg/__init__.py b/examples/external/external_python_project/externalpy_pkg/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/examples/external/external_python_project/externalpy_pkg/config/__init__.py b/examples/external/external_python_project/externalpy_pkg/config/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/examples/external/external_python_project/externalpy_pkg/config/app_config.yaml b/examples/external/external_python_project/externalpy_pkg/config/app_config.yaml
new file mode 100644
index 0000000..5222124
--- /dev/null
+++ b/examples/external/external_python_project/externalpy_pkg/config/app_config.yaml
@@ -0,0 +1,4 @@
+app_name: "PyQt5 app with YAML"
+target_performance:
+ min_app_load_speed_s: 3
+ max_nb_crash_allowed: 0
diff --git a/examples/external/external_python_project/externalpy_pkg/externalpy/__init__.py b/examples/external/external_python_project/externalpy_pkg/externalpy/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/examples/external/external_python_project/externalpy_pkg/externalpy/pyqt5_app_with_yaml.py b/examples/external/external_python_project/externalpy_pkg/externalpy/pyqt5_app_with_yaml.py
new file mode 100644
index 0000000..bf22390
--- /dev/null
+++ b/examples/external/external_python_project/externalpy_pkg/externalpy/pyqt5_app_with_yaml.py
@@ -0,0 +1,292 @@
+#!/usr/bin/env python3
+
+# MIT License
+
+# Copyright (c) 2024 Achille MARTIN
+
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+
+# PyQt5 app with PyYAML
+
+## Validated imports
+
+from PyQt5.QtCore import QStandardPaths
+from os import makedirs
+from os.path import isfile, realpath, dirname, join, exists, pardir
+import logging as log_tool # The logging library for debugging
+import sys
+
+## Main variables and objects
+
+# Retrieve app data folder path reference
+std_app_data_folder_path_list = QStandardPaths.writableLocation(
+ QStandardPaths.AppDataLocation
+)
+if std_app_data_folder_path_list is None:
+ sys.exit("[ERROR] Cannot find writable App Data folder path")
+
+# Define custom app data folder path
+std_app_data_folder_path = ""
+if isinstance(std_app_data_folder_path_list, list):
+ std_app_data_folder_path = std_app_data_folder_path_list[0]
+else:
+ std_app_data_folder_path = str(std_app_data_folder_path_list)
+app_data_folder_path = join(
+ std_app_data_folder_path,
+ 'pyqt5_app_data',
+)
+
+# Generate custom app data folder at convenient writable location
+makedirs(app_data_folder_path, exist_ok = True)
+
+# Set logger config and instantiate object
+logger_logging_level = "DEBUG"
+logger_output_file_name = "pyqt5_app_with_yaml.log"
+logger_output_prefix_format = "[%(asctime)s] [%(levelname)s] - %(message)s"
+logger = log_tool.getLogger(__name__)
+logger.setLevel(logger_logging_level)
+logger_output_file_path = join(app_data_folder_path, str(logger_output_file_name))
+file_handler = log_tool.FileHandler(logger_output_file_path)
+formatter = log_tool.Formatter(logger_output_prefix_format)
+file_handler.setFormatter(formatter)
+logger.addHandler(file_handler)
+
+# Set additional log output location
+# for non-root debugging
+# when flag is_non_root_debug_active is set to True
+is_non_root_debug_active = False
+# WARNING: this feature might require
+# to allow storage access permission before launching the app
+if is_non_root_debug_active:
+ # Retrieve documents folder path reference
+ # to save logs at a location accessible by non-root users
+ std_documents_folder_path_list = QStandardPaths.writableLocation(
+ QStandardPaths.DocumentsLocation
+ )
+ if std_documents_folder_path_list is None:
+ sys.exit("[ERROR] Cannot find writable documents folder path")
+ # Define custom alternative app data folder path
+ std_documents_folder_path = ""
+ if isinstance(std_documents_folder_path_list, list):
+ std_documents_folder_path = std_documents_folder_path_list[0]
+ else:
+ std_documents_folder_path = str(std_documents_folder_path_list)
+ alternative_app_data_folder_path = join(
+ std_documents_folder_path,
+ 'pyqt5_app_data',
+ )
+ # Generate alternative app data folder at convenient writable location
+ makedirs(alternative_app_data_folder_path, exist_ok = True)
+ # Add log file output alternative to logger
+ logger_output_file_path_alternative = join(
+ alternative_app_data_folder_path,
+ str(logger_output_file_name)
+ )
+ file_handler_alternative = log_tool.FileHandler(
+ logger_output_file_path_alternative
+ )
+ formatter_alternative = log_tool.Formatter(logger_output_prefix_format)
+ file_handler_alternative.setFormatter(formatter_alternative)
+ logger.addHandler(file_handler_alternative)
+
+# Reference app config file name
+app_config_file_name = "app_config.yaml"
+
+## Non-validated imports
+try:
+ from PyQt5.QtWidgets import QApplication, QMainWindow, QPushButton, QMessageBox
+ from yaml import safe_load
+ import importlib.resources as resources
+except Exception as e:
+ logger.debug("pre-main - Error caught while importing: {e}")
+
+## Local package imports
+## Used to search for packages
+## on the local machine
+## (for testing purposes only)
+
+importer_folder = dirname(
+ realpath(__file__)
+)
+parent_of_importer_folder = join(
+ importer_folder,
+ pardir,
+)
+parent_of_parent_of_importer_folder = join(
+ parent_of_importer_folder,
+ pardir,
+)
+sys.path.append(parent_of_importer_folder)
+sys.path.append(parent_of_parent_of_importer_folder)
+
+from externalpy_pkg.tools.validation_tools import validate_pkg_format
+
+
+## Class definition
+
+# Subclass QMainWindow to customize your application's main window
+class MainWindow(QMainWindow):
+ def __init__(self):
+ super().__init__()
+
+ logger.debug(f"MainWindow::__init__ - Entered function")
+
+ # Initialise class attributes
+ self.setWindowTitle("PyQt5 app with YAML")
+
+ # Initialise button
+ button = QPushButton("Press Here for the magic")
+ button.setCheckable(True)
+ button.clicked.connect(self.on_button_clicked)
+
+ # Import app config from yaml file
+ self.app_config_file_path = ""
+ self.app_config = dict()
+ self.app_name = ""
+ self.min_app_load_speed_s = ""
+ self.max_nb_crash_allowed = ""
+ try:
+ logger.debug(
+ f"""
+ MainWindow::__init__ -
+ Getting file path and content of {app_config_file_name}
+ """
+ )
+ with resources.path(
+ validate_pkg_format('externalpy_pkg.config', logger),
+ app_config_file_name) as path:
+ self.app_config_file_path = realpath(path)
+ logger.debug(
+ f"""
+ MainWindow::__init__ -
+ Obtained file path for {app_config_file_name}:
+ {self.app_config_file_path}
+ """
+ )
+ self.app_config = self.import_yaml_config(self.app_config_file_path)
+ logger.debug(f"MainWindow::__init__ - Obtained file path: {self.app_config_file_path}")
+ logger.debug(f"MainWindow::__init__ - Obtained file content: \n{self.app_config}")
+ self.app_name = self.app_config.get('app_name', '')
+ target_performance_details = self.app_config.get('target_performance', dict())
+ self.min_app_load_speed_s = target_performance_details.get('min_app_load_speed_s', '')
+ self.max_nb_crash_allowed = target_performance_details.get('max_nb_crash_allowed', '')
+ except Exception as e:
+ logger.warn(
+ f"""
+ MainWindow::__init__ -
+ Exception caught: {e}
+ Cannot read information from config file:
+ {app_config_file_name}
+ """
+ )
+
+ # Set properties of the widget in the Window
+ self.setCentralWidget(button)
+
+ logger.debug(f"MainWindow::__init__ - Exiting function")
+
+ def on_button_clicked(self):
+
+ logger.debug(f"MainWindow::on_button_clicked - Entered function")
+
+ # Display alert message
+ alert = QMessageBox()
+ alert.setText(
+ f"""
+ You clicked the button!
+
+ Loaded config file {app_config_file_name} for the app.
+
+ The app name is: {self.app_name}
+
+ The target performance is:
+ * Min app loading speed = {self.min_app_load_speed_s} s
+ * Max number of crashes allowed = {self.max_nb_crash_allowed}
+
+ Config file is located at:
+ {self.app_config_file_path}
+
+ Log file is located at:
+ {logger_output_file_path}
+ """
+ )
+
+ alert.exec()
+
+ logger.debug(f"MainWindow::on_button_clicked - Exiting function")
+
+ def import_yaml_config(self, config_file_path):
+
+ logger.debug(f"MainWindow::import_yaml_config - Entered function")
+
+ empty_dict = dict()
+ config = empty_dict
+ # Check whether config file exists
+ if not isfile(config_file_path):
+ logger.debug(
+ f"""
+ MainWindow::import_yaml_config -
+ Cannot import yaml config from `{config_file_path}`
+ Because file does not exist
+ Returning empty dictionary
+ """
+ )
+ return empty_dict
+
+ # Import yaml config from file
+ with open(config_file_path, 'r') as file:
+ config = safe_load(file)
+ logger.debug(
+ f"""
+ MainWindow::import_yaml_config -
+ Imported yaml config from `{config_file_path}`:
+ {config}
+ """
+ )
+
+ logger.debug(f"MainWindow::import_yaml_config - Exiting function")
+
+ return config
+
+
+## Application definition
+
+def main():
+ logger.info("******************************\n")
+ logger.debug(f"main - Log output file can be found at: {logger_output_file_path}")
+ # Only one QApplication instance is needed per application.
+ # Pass in sys.argv to allow command line arguments for the app: `QApplication(sys.argv)`
+ # If command line arguments are not needed, use: `QApplication([])`
+ app = QApplication([])
+
+ # Create a QMainWindow object which represents the Main Window.
+ main_window = MainWindow()
+ main_window.showMaximized() # This line will show windows that are normally hidden. Plus, it will maximise the main window.
+
+ # Start the application event loop.
+ logger.info("main - App started")
+ sys.exit(app.exec())
+ logger.info("main - App terminated")
+
+ # The application will only reach here when exiting or event loop has stopped.
+
+if __name__ == "__main__":
+ # This section needs to only define main
+ # due to how pyqtdeploy is implemented to build packages
+ main()
diff --git a/examples/external/external_python_project/externalpy_pkg/tools/__init__.py b/examples/external/external_python_project/externalpy_pkg/tools/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/examples/external/external_python_project/externalpy_pkg/tools/validation_tools.py b/examples/external/external_python_project/externalpy_pkg/tools/validation_tools.py
new file mode 100644
index 0000000..b09e1e0
--- /dev/null
+++ b/examples/external/external_python_project/externalpy_pkg/tools/validation_tools.py
@@ -0,0 +1,107 @@
+#!/usr/bin/env python3
+
+# MIT License
+
+# Copyright (c) 2024 Achille MARTIN
+
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+
+from os.path import join
+import importlib.util
+
+def validate_pkg_format(dotted_pkg_name, logger=None):
+ """
+ Get the python package name
+ following the package naming convention
+ that is most relevant
+ for the application in use
+
+ Parameters
+ ----------
+ dotted_pkg_name: str
+ The package name to validate
+ for subpackages, separate with dots
+ example: 'pkg_1.subpkg_1'
+
+ Returns
+ -------
+ str
+ Same package name
+ but with the relevant
+ package naming convention
+ """
+
+ logger.debug(
+ f"""
+ validation_tools::validate_pkg_format -
+ Received request to validate: {dotted_pkg_name}
+ """
+ ) if logger else None
+
+ # Initialise pkg name to return
+ supported_pkg_name = ''
+
+ # Split the dotted pkg name at the dots
+ split_pkg_name_list = dotted_pkg_name.split('.')
+
+ # Create a joined pkg name readable by the OS
+ joined_pkg_name = join(*split_pkg_name_list)
+
+ logger.debug(
+ f"""
+ validation_tools::validate_pkg_format -
+ Generated joined package name: {joined_pkg_name}
+ """
+ ) if logger else None
+
+ # Evaluate which pkg name is suited for the application
+ if importlib.util.find_spec(joined_pkg_name) is not None:
+ supported_pkg_name = joined_pkg_name
+ logger.debug(
+ f"""
+ validation_tools::validate_pkg_format -
+ Found joined package name
+ """
+ ) if logger else None
+ elif importlib.util.find_spec(dotted_pkg_name) is not None:
+ supported_pkg_name = dotted_pkg_name
+ logger.debug(
+ f"""
+ validation_tools::validate_pkg_format -
+ Found dotted package name
+ """
+ ) if logger else None
+ else:
+ logger.warn(
+ f"""
+ validation_tools::validate_pkg_format -
+ Cannot find package name
+ """
+ ) if logger else None
+ pass
+
+ logger.debug(
+ f"""
+ validation_tools::validate_pkg_format -
+ Returning package name: {supported_pkg_name}
+ """
+ ) if logger else None
+
+ return supported_pkg_name
+
diff --git a/examples/external/external_python_project/sysroot.toml b/examples/external/external_python_project/sysroot.toml
new file mode 100644
index 0000000..ed8a5db
--- /dev/null
+++ b/examples/external/external_python_project/sysroot.toml
@@ -0,0 +1,96 @@
+# The sysroot for externalpy application.
+
+# Python ######################################################################
+
+[Python]
+version = "3.10.12"
+install_host_from_source = true
+
+[Python.win]
+install_host_from_source = false
+
+# PyYAML ######################################################################
+
+[yaml]
+plugin = "wheel"
+wheel = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"
+dependencies = [
+ "Python:base64",
+ "Python:binascii",
+ "Python:codecs",
+ "Python:collections",
+ "Python:copyreg",
+ "Python:datetime",
+ "Python:io",
+ "Python:re",
+ "Python:types",
+ "Python:warnings",
+]
+
+# PyQt ########################################################################
+
+[PyQt]
+version = "5.15.10"
+
+[PyQt.android]
+disabled_features = ["PyQt_Desktop_OpenGL", "PyQt_Printer"]
+installed_modules = ["QtCore", "QtGui", "QtWidgets"]
+
+[PyQt.ios]
+disabled_features = ["PyQt_Desktop_OpenGL", "PyQt_MacOSXOnly",
+ "PyQt_MacCocoaViewContainer", "PyQt_Printer", "PyQt_Process",
+ "PyQt_NotBootstrapped"]
+installed_modules = ["QtCore", "QtGui", "QtWidgets"]
+
+[PyQt.linux]
+installed_modules = ["QtCore", "QtGui", "QtWidgets"]
+
+[PyQt.macos]
+installed_modules = ["QtCore", "QtGui", "QtWidgets"]
+
+[PyQt.win]
+disabled_features = ["PyQt_Desktop_OpenGL"]
+installed_modules = ["QtCore", "QtGui", "QtWidgets"]
+
+# Qt ##########################################################################
+
+[Qt]
+version = "5.15.2"
+edition = "opensource"
+configure_options = ["-opengl", "desktop", "-no-dbus", "-qt-pcre"]
+skip = ["qtactiveqt", "qtdoc", "qtgamepad",
+ "qtquickcontrols", "qtquickcontrols2",
+ "qtremoteobjects", "qtscript", "qtscxml", "qtserialbus",
+ "qtserialport", "qtspeech", "qtsvg", "qttools", "qttranslations",
+ "qtwayland", "qtwebchannel", "qtwebengine", "qtwebsockets",
+ "qtwebview", "qtxmlpatterns"]
+
+[Qt.android]
+install_from_source = false
+
+[Qt.ios]
+install_from_source = false
+
+[Qt.linux]
+
+[Qt.macos]
+
+[Qt.win]
+static_msvc_runtime = true
+
+# SIP #########################################################################
+
+[SIP]
+abi_major_version = 12
+module_name = "PyQt5.sip"
+
+# zlib ########################################################################
+
+[zlib]
+install_from_source = false
+
+[zlib.win]
+version = "1.2.13"
+install_from_source = true
+static_msvc_runtime = true
+
diff --git a/requirements.txt b/requirements.txt
index 6382070..322a61f 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -4,4 +4,8 @@ PyQt5-sip==12.13.0
pyqtdeploy==3.3.0
sip==6.7.12
PyQt-builder==1.15.3
+PyYAML==6.0.1
pipdeptree==2.18.1
+list-imports @ git+https://github.com/achille-martin/list-imports@2024.3.20
+importlib_metadata==7.1.0
+stdlib-list==0.10.0
diff --git a/utils/bash/download_sources.sh b/utils/bash/download_sources.sh
new file mode 100755
index 0000000..7d11f34
--- /dev/null
+++ b/utils/bash/download_sources.sh
@@ -0,0 +1,54 @@
+#!/usr/bin/env bash
+
+# MIT License
+
+# Copyright (c) 2024 Achille MARTIN
+
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+
+############################################
+# PURPOSE
+# -------
+# Download external sources
+# required by the repo and its dependencies
+############################################
+
+# Create folder to hold the downloaded external sources
+sources_folder_path="$PYQT_CROM_DIR/utils/resources"
+mkdir -p $sources_folder_path
+
+# Download the sources into the created folder
+# if they have not been downloaded yet
+cd $sources_folder_path
+## Python
+python_xz_file="Python-3.10.12.tar.xz"
+if [[ -e $python_xz_file ]]; then
+ printf "$python_xz_file is already downloaded into $sources_folder_path\n"
+else
+ wget https://www.python.org/ftp/python/3.10.12/$python_xz_file
+ printf "Downloaded $python_xz_file into $sources_folder_path\n"
+fi
+## Qt
+qt_xz_file="qt-everywhere-src-5.15.2.tar.xz"
+if [[ -e $qt_xz_file ]]; then
+ printf "$qt_xz_file is already downloaded into $sources_folder_path\n"
+else
+ wget https://download.qt.io/archive/qt/5.15/5.15.2/single/$qt_xz_file
+ printf "Downloaded $qt_xz_file into $sources_folder_path\n"
+fi
diff --git a/utils/bash/get_command_output_help.sh b/utils/bash/get_command_output_help.sh
new file mode 100755
index 0000000..24ffc18
--- /dev/null
+++ b/utils/bash/get_command_output_help.sh
@@ -0,0 +1,198 @@
+#!/usr/bin/env bash
+
+# MIT License
+
+# Copyright (c) 2024 Achille MARTIN
+
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+
+#########################################################
+# PURPOSE
+# -------
+# Collect information about a command
+# for help and debugging purposes
+#
+# INPUTS
+# ------
+# * Command to collect help and debug information about
+# * Additional relevant files to boost help
+#
+# OUTPUTS
+# -------
+# * Markdown file containing:
+# * Information about source OS
+# * Information about python
+# * Information about repo
+# * Information about dependencies
+# * Get venv/ file architecture for instance
+# * Get resources/ content for instance
+# * Information about command input
+#########################################################
+
+# Define print usage function
+print_usage() {
+ printf "Usage: get_command_output_help.sh \"\" -f \" ... \"\n"
+}
+
+# Ensure correct number of required arguments is supplied
+if [[ "$#" -lt 1 ]]; then
+ print_usage
+ exit 1
+fi
+
+# Ensure that the required arguments are correctly supplied
+command_to_run="$1"
+if [[ "$command_to_run" == "-f"* ]]; then
+ printf "First required argument is not valid: $1\n"
+ print_usage
+ exit 1
+fi
+
+# Get optional arguments
+flags="f:" # Expected arguments for `f` flag
+OPTIND=2 # Ignore the first required argument
+file_input_list=()
+while getopts $flags flag; do
+ case "${flag}" in
+ f) file_input_list+=( "${OPTARG}" ) ;;
+ *) print_usage ; exit 1 ;;
+ esac
+done
+
+# Define handy variables
+code_snippet_start="\n \`\`\` \n"
+code_snippet_end=$code_snippet_start
+text_to_add=""
+
+# Create log folder if not already created
+log_folder="$PYQT_CROM_DIR/log"
+mkdir -p $log_folder
+
+# Create file to hold information
+current_date_time=$(date +"%Y_%m_%d-%H_%M_%S" |& tee '/dev/null')
+log_file_name="command_output_help_$current_date_time.md"
+log_file_path="$log_folder/$log_file_name"
+
+printf "[INFO] Creating file $log_file_path to hold information...\n"
+
+touch $log_file_path
+
+printf "[INFO] ...Done.\n"
+
+# Pre-populate the log file with relevant information about the request
+printf "[INFO] Populating file with relevant information about the request...\n"
+
+current_working_directory=$(pwd |& tee '/dev/null')
+text_to_add="# Command output help
+
+## Request information
+* Command requested: $code_snippet_start $command_to_run $code_snippet_end
+* Current working directory: $code_snippet_start $current_working_directory $code_snippet_end
+* Date and time requested at: $code_snippet_start $current_date_time $code_snippet_end
+* User emitting request: $code_snippet_start $USER $code_snippet_end
+"
+printf "$text_to_add" >> $log_file_path 2>&1
+
+printf "[INFO] ...Done.\n"
+
+# Get context information to understand the command output
+
+## Get information about source OS
+printf "[INFO] Populating file with information about source OS...\n"
+
+os_pretty_name=$(cat "/etc/os-release" | grep "PRETTY_NAME" |& tee '/dev/null')
+text_to_add="## Source OS information
+* Source OS pretty name: $code_snippet_start $os_pretty_name $code_snippet_end
+"
+printf "$text_to_add" >> $log_file_path 2>&1
+
+printf "[INFO] ...Done.\n"
+
+## Get information about python in virtual environment
+printf "[INFO] Populating file with information about python...\n"
+
+source $PYQT_CROM_DIR/venv/pyqt-crom-venv/bin/activate
+python_version=$(python --version |& tee '/dev/null')
+pip_version=$(pip --version |& tee '/dev/null')
+pip_packages_installed=$(pip list --local |& tee '/dev/null')
+pip_dependency_tree=$(pipdeptree --local |& tee '/dev/null')
+python_site_packages=$(ls -l $PYQT_CROM_DIR/venv/pyqt-crom-venv/lib/python3.10/site-packages |& tee '/dev/null')
+
+text_to_add="## Python information
+* Python version: $code_snippet_start $python_version $code_snippet_end
+* Pip version: $code_snippet_start $pip_version $code_snippet_end
+* Pip packages installed: $code_snippet_start $pip_packages_installed $code_snippet_end
+* Pip dependency tree: $code_snippet_start $pip_dependency_tree $code_snippet_end
+* Python site packages: $code_snippet_start $python_site_packages $code_snippet_end
+"
+printf "$text_to_add" >> $log_file_path 2>&1
+
+printf "[INFO] ...Done.\n"
+
+## Get information about the general repo
+printf "[INFO] Populating file with information about repo...\n"
+
+git_branch_current=$(git branch --show-current |& tee '/dev/null')
+latest_commit_date=$(git log -1 --format=%cd |& tee '/dev/null')
+environment_variables=$(cat $PYQT_CROM_DIR/utils/bash/setup_path.sh |& tee '/dev/null')
+
+text_to_add="## General repo information
+* Git branch: $code_snippet_start $git_branch_current $code_snippet_end
+* Latest git commit date: $code_snippet_start $latest_commit_date $code_snippet_end
+* Environment variables: $code_snippet_start $environment_variables $code_snippet_end
+"
+printf "$text_to_add" >> $log_file_path 2>&1
+
+printf "[INFO] ...Done.\n"
+
+## Get information about repo dependencies
+printf "[INFO] Populating file with information about repo dependencies...\n"
+
+external_sources_list=$(ls -l $PYQT_CROM_DIR/utils/resources |& tee '/dev/null')
+
+text_to_add="## Repo dependency information
+* External sources: $code_snippet_start $external_sources_list $code_snippet_end
+"
+printf "$text_to_add" >> $log_file_path 2>&1
+
+printf "[INFO] ...Done.\n"
+
+# Get information about the command input
+printf "[INFO] Populating file with information about command input...\n"
+
+text_to_add="## Command input information
+* Command run: $code_snippet_start $(eval $command_to_run |& tee '/dev/null') $code_snippet_end
+"
+printf "$text_to_add" >> $log_file_path 2>&1
+
+printf "[INFO] ...Done.\n"
+
+# Append extra file content
+printf "[INFO] Populating file with extra input file content...\n"
+
+text_to_add="## Extra file content information
+"
+printf "$text_to_add" >> $log_file_path 2>&1
+for file_path in $file_input_list; do
+ file_content=$(cat $file_path |& tee '/dev/null')
+ printf "* File \`$file_path\`: $code_snippet_start $file_content $code_snippet_end\n" >> $log_file_path 2>&1
+done
+
+printf "[INFO] ...Done.\n"
+
diff --git a/utils/bash/get_python_package_wheel.sh b/utils/bash/get_python_package_wheel.sh
new file mode 100755
index 0000000..349c4bd
--- /dev/null
+++ b/utils/bash/get_python_package_wheel.sh
@@ -0,0 +1,87 @@
+#!/usr/bin/env bash
+
+# MIT License
+
+# Copyright (c) 2024 Achille MARTIN
+
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+
+###################################################
+# PURPOSE
+# -------
+# Identify wheels
+# for desired non-standard python package
+# with specific package version
+# for the current OS specifications
+#
+# INPUTS
+# ------
+# * Python package name
+# * Python package version number
+#
+# OUTPUTS
+# -------
+# * Wheel name associated to input python package
+###################################################
+
+# Define print usage function
+print_usage() {
+ printf "Usage: get_python_package_wheel.sh \n"
+}
+
+# Ensure that package name and version are supplied
+if [[ "$#" -ne 2 ]]; then
+ print_usage
+ exit 1
+fi
+
+# Define a custom error handler function
+handle_error() {
+ printf "An error occurred: $1\n"
+ exit 1
+}
+
+# Set the error handler to be called when an error occurs
+trap 'handle_error "please review the input arguments"' ERR
+
+# Collect required arguments
+python_package_name=$1
+python_package_version=$2
+printf "Python package deisred: $python_package_name\n"
+printf "Python package version desired: $python_package_version\n"
+printf "______\n"
+
+# Download the wheels and no dependencies for the current OS specifications
+# The wheels are downloaded into the `tmp` folder (cleared up on reboot)
+# The wheels do not include dependencies for the python package
+temp_wheels_folder_path="/tmp"
+if [ ! -d "$temp_wheels_folder_path" ]; then
+ echo "$temp_wheels_folder_path does not exist"
+ echo "Ensure your OS is supported"
+fi
+pip download --only-binary :all: --no-deps -d $temp_wheels_folder_path $python_package_name==$python_package_version
+
+# Get the name of the wheels from the downloaded material
+regex_pattern="$python_package_version.*\.whl"
+wheel_name=$(find $temp_wheels_folder_path -printf "%f\n" 2>/dev/null | grep -iE $regex_pattern)
+
+# Print out the name of the wheels
+printf "______\n"
+printf "Wheel name for Python package $python_package_name (version $python_package_version) and for current OS specifications is:\n"
+printf "$wheel_name\n"
diff --git a/utils/bash/setup_path.sh b/utils/bash/setup_path.sh
new file mode 100755
index 0000000..63b35c0
--- /dev/null
+++ b/utils/bash/setup_path.sh
@@ -0,0 +1,41 @@
+#!/usr/bin/env bash
+
+# MIT License
+
+# Copyright (c) 2024 Achille MARTIN
+
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+
+##########################################
+# PURPOSE
+# -------
+# Setup handy path variables
+# for the repo and its dependencies
+##########################################
+
+export PYQT_CROM_DIR=$HOME/Documents/pyqt-crom
+export RESOURCES_DIR=$PYQT_CROM_DIR/utils/resources
+export QT_DIR=$HOME/Qt5.15.2/5.15.2
+export ANDROID_SDK_ROOT=$HOME/Android/Sdk
+export ANDROID_NDK_ROOT=$ANDROID_SDK_ROOT/ndk/21.4.7075529
+export ANDROID_NDK_PLATFORM=android-28
+# export ANDROID_NDK_HOST=linux-x86_64
+export JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64
+export JAVA_JRE=/usr/lib/jvm/java-11-openjdk-amd64/jre
+export PATH=$PATH:$JAVA_HOME/bin
diff --git a/utils/python/__init__.py b/utils/python/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/utils/build_app.py b/utils/python/build_app.py
similarity index 85%
rename from utils/build_app.py
rename to utils/python/build_app.py
index bc2ac32..88b1918 100644
--- a/utils/build_app.py
+++ b/utils/python/build_app.py
@@ -23,6 +23,8 @@
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
+# Modified by Achille MARTIN 2024
+
import argparse
import os
@@ -47,35 +49,54 @@ def run(args):
sys.exit(ec)
-# Parse the command line.
+# Parse the command line
parser = argparse.ArgumentParser()
-parser.add_argument('--pdt',
- help="the .pdt file used to define application sources and imported packages",
- metavar="FILE",
- required=True)
-parser.add_argument('--jobs',
- help="the number of make jobs to be run in parallel on Linux and "
- "macOS [default: 1]",
- metavar="NUMBER", type=int, default=1)
-parser.add_argument('--qmake',
- help="the qmake executable when using an existing Qt installation",
- metavar="FILE")
-parser.add_argument('--target', help="the target architecture", default='')
-parser.add_argument('--reload-sysroot',
- help="Delete existing sysroot build folder and load target sysroot file",
- action='store_true')
-parser.add_argument('--quiet', help="disable progress messages",
- action='store_true')
-parser.add_argument('--verbose', help="enable verbose progress messages",
- action='store_true')
+parser.add_argument(
+ '--pdt',
+ help="the .pdt file used to define application sources and imported packages",
+ metavar="FILE",
+ required=True,
+)
+parser.add_argument(
+ '--jobs',
+ help="the number of make jobs to be run in parallel on Linux and macOS [default: 1]",
+ metavar="NUMBER",
+ type=int,
+ default=1,
+)
+parser.add_argument(
+ '--qmake',
+ help="the qmake executable when using an existing Qt installation",
+ metavar="FILE",
+)
+parser.add_argument(
+ '--target',
+ help="the target architecture",
+ default='',
+)
+parser.add_argument(
+ '--reload-sysroot',
+ help="Delete existing sysroot build folder and load target sysroot file",
+ action='store_true',
+)
+parser.add_argument(
+ '--quiet',
+ help="disable progress messages",
+ action='store_true',
+)
+parser.add_argument(
+ '--verbose',
+ help="enable verbose progress messages",
+ action='store_true',
+)
cmd_line_args = parser.parse_args()
-pdt = os.path.abspath(cmd_line_args.pdt) if cmd_line_args.pdt else None
+pdt = os.path.realpath(cmd_line_args.pdt) if cmd_line_args.pdt else None
if not os.path.exists(pdt):
print(f"[ERROR] Path to .pdt file {pdt} does not exist.", file=sys.stderr)
print("Please specify a .pdt file that exists.")
sys.exit(2)
jobs = cmd_line_args.jobs
-qmake = os.path.abspath(cmd_line_args.qmake) if cmd_line_args.qmake else None
+qmake = os.path.realpath(cmd_line_args.qmake) if cmd_line_args.qmake else None
target = cmd_line_args.target
reload_sysroot = cmd_line_args.reload_sysroot
quiet = cmd_line_args.quiet
@@ -94,7 +115,7 @@ def run(args):
# Initialise handy variables and tools
## Define pdt location as reference (for practicality)
-pdt_dir = os.path.dirname(os.path.abspath(pdt))
+pdt_dir = os.path.dirname(os.path.realpath(pdt))
print(f"[INFO] Pdt directory location is: {pdt_dir}. This is the reference directory.")
## Define default variable values
app_name_default = "MyCrossPlatformApp"
@@ -116,7 +137,14 @@ def run(args):
## Generate practical release directory structure
## By creating directories with date timestamp to store the app releases
current_datetime = datetime.now().strftime("%Y_%m_%d-%H_%M_%S")
-app_release_dir = os.path.join(app_package_dir, os.path.pardir, 'releases', str(current_datetime))
+app_release_dir = os.path.realpath(
+ os.path.join(
+ app_package_dir,
+ os.path.pardir,
+ 'releases',
+ str(current_datetime),
+ )
+)
print(f"[INFO] The app release dir is set to: {app_release_dir}")
os.makedirs(app_release_dir, exist_ok=True)
@@ -215,7 +243,7 @@ def run(args):
print("\n----- RUNNING QMAKE -----\n")
# Run qmake. Use the qmake left by pyqtdeploy-sysroot if there is one.
-sysroot_dir = os.path.abspath('sysroot-' + target)
+sysroot_dir = os.path.realpath('sysroot-' + target)
qmake_path = os.path.join(sysroot_dir, 'Qt', 'bin', 'qmake')
if sys.platform == 'win32':
@@ -262,7 +290,7 @@ def run(args):
if target.startswith('android'):
# Copy the output app to the specified release directory
shutil.copy(os.path.join(apk_dir, apk), app_release_dir)
- print(f"The released app {apk} can be found in {os.path.abspath(app_release_dir)}\n")
+ print(f"The released app {apk} can be found in {app_release_dir}\n")
print(f"""Debug tip: the {apk} file can also be found in the '{apk_dir}' directory.
Run adb to install it to a simulator.""")
@@ -270,18 +298,18 @@ def run(args):
xcodeproj_app = app_entrypoint_name + '.xcodeproj'
# Copy the output built app to the specified release directory
shutil.copy(os.path.join(pdt_dir, build_dir, xcodeproj_app), app_release_dir)
- print(f"The released app {xcodeproj_app} can be found in {os.path.abspath(app_release_dir)}\n")
+ print(f"The released app {xcodeproj_app} can be found in {app_release_dir}\n")
print(f"""Debug tip: the {xcodeproj_app} file can be found in the '{build_dir}' directory.
Run Xcode to build the app and run it in the simulator or deploy it to a device.""")
elif target.startswith('win') or sys.platform == 'win32':
# Copy the output built app to the specified release directory
shutil.copy(os.path.join(pdt_dir, build_dir, app_entrypoint_name), app_release_dir)
- print(f"The released app {app_entrypoint_name} can be found in {os.path.abspath(app_release_dir)}\n")
+ print(f"The released app {app_entrypoint_name} can be found in {app_release_dir}\n")
print(f"The {app_entrypoint_name} executable can be found in the '{os.path.join(build_dir, 'release')}' directory.")
else:
# Copy the output built app to the specified release directory
shutil.copy(os.path.join(pdt_dir, build_dir, app_entrypoint_name), app_release_dir)
- print(f"The released app {app_entrypoint_name} can be found in {os.path.abspath(app_release_dir)}\n")
+ print(f"The released app {app_entrypoint_name} can be found in {app_release_dir}\n")
print(f"Debug tip: the {app_entrypoint_name} executable can be found in the '{build_dir}' directory.")
diff --git a/utils/pdt_parser.py b/utils/python/pdt_parser.py
similarity index 100%
rename from utils/pdt_parser.py
rename to utils/python/pdt_parser.py
diff --git a/utils/python/py_package_dependency_collector.py b/utils/python/py_package_dependency_collector.py
new file mode 100755
index 0000000..ec8c458
--- /dev/null
+++ b/utils/python/py_package_dependency_collector.py
@@ -0,0 +1,589 @@
+#!/usr/bin/env python3
+
+# MIT License
+
+# Copyright (c) 2024 Achille MARTIN
+
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+
+# ---- INTRODUCTION ----
+# Python package dependency collector
+# based on module imports
+
+# Assumption 1: this script assumes that pip package names
+# that are searched have been installed on the machine
+# Assumption 2: this script will not search
+# for folders with unique leading underscore
+# and will not search
+# for files with unique leading underscore
+# since they are all meant for internal use
+
+# Example:
+# pip install pyyaml
+# python3 py_package_dependency_collector -n pyyaml
+
+import argparse
+import sys
+from os import walk
+from os.path import (
+ realpath,
+ join,
+ isdir,
+ isfile,
+ basename,
+)
+import subprocess
+from importlib_metadata import packages_distributions
+from stdlib_list import stdlib_list
+import list_imports
+from inspect import cleandoc as cl
+import re
+
+
+class PyDepCollector():
+ def __init__(self):
+
+ # Initialise attributes
+ self.deps_collected_list = []
+ self.pip_required_deps_list = []
+ self.site_pkg_names_list = []
+
+ print(
+ cl(
+ f"""
+ [DEBUG] Initialised PyDepCollector object
+ Dependencies collected list is:
+ {self.deps_collected_list}
+ ----------
+ """
+ )
+ )
+
+ def collect_deps_from_pkg_name(
+ self,
+ py_pkg_name,
+ dir_name_exclusion_list=[],
+ file_name_exclusion_list=[]):
+
+ print(
+ cl(
+ f"""
+ [DEBUG] Received request
+ to collect deps
+ from pkg name:
+ {py_pkg_name}
+ with directories to exclude:
+ {dir_name_exclusion_list}
+ and files to exclude:
+ {file_name_exclusion_list}
+ ----------
+ """
+ )
+ )
+
+ # Initialise pkg installation path list
+ # associated to py pkg name
+ pkg_installation_path_list = []
+
+ try:
+ # Ensure that py pkg name is a string
+ if not isinstance(py_pkg_name, str):
+ raise Exception(
+ f"""
+ Python package name must be a string
+ This is your input: {py_pkg_name}
+ ----------
+ """
+ )
+
+ # Reset pip required deps list
+ self.reset_pip_required_deps_list()
+
+ # Retrieve PyPI package name from pip package name
+ # using `pip show `
+ pip_show_res_b = subprocess.check_output(
+ ["pip", "show", py_pkg_name]
+ )
+ pip_show_res_b_list = pip_show_res_b.split(b"\n")
+ pip_show_line_index_list = [0, 1, -1, -2, -3, -4]
+ pip_show_res_list = [
+ pip_show_res_b_list[idx].decode('utf-8')
+ for idx
+ in pip_show_line_index_list
+ ]
+ pip_show_res_dict = {
+ elem.split(":")[0]: elem.split(":")[1].lstrip()
+ for elem
+ in pip_show_res_list
+ if ":" in elem
+ }
+ pypi_pkg_name = pip_show_res_dict["Name"]
+ print(
+ cl(
+ f"""
+ [DEBUG] pkg name {py_pkg_name}
+ is equivalent to pypi package name:
+ {pypi_pkg_name}
+ ----------
+ """
+ )
+ )
+
+ # Retrieve site-packages names from PyPI package name
+ # using `importlib.metadata`
+ # Note that the names starting with a unique underscore
+ # are dropped
+ pkg_distr_dict = packages_distributions()
+ site_pkg_names_list = [
+ key
+ for key, value in pkg_distr_dict.items()
+ if (value[0] == pypi_pkg_name
+ and not re.search("^_[^_].*$", key))
+ ]
+ self.site_pkg_names_list.extend(site_pkg_names_list)
+ self.site_pkg_names_list.sort()
+ print(
+ cl(
+ f"""
+ [DEBUG] pypi package name {pypi_pkg_name}
+ is associated to site package names:
+ {self.site_pkg_names_list}
+ ----------
+ """
+ )
+ )
+
+ # Identify installation path of site-packages names
+ pkg_installation_path_list = [
+ join(pip_show_res_dict["Location"], name)
+ for name
+ in self.site_pkg_names_list
+ ]
+ pkg_installation_path_list.sort()
+ print(
+ cl(
+ f"""
+ [DEBUG] site package names can be found
+ at locations:
+ {pkg_installation_path_list}
+ ----------
+ """
+ )
+ )
+
+ # Save pip required deps
+ self.pip_required_deps_list.extend(
+ pip_show_res_dict["Requires"].split(",")
+ )
+ self.pip_required_deps_list.sort()
+ print(
+ cl(
+ f"""
+ [DEBUG] pip required deps identified:
+ {self.pip_required_deps_list}
+ ----------
+ """
+ )
+ )
+
+ except Exception as e:
+ print(
+ cl(
+ f"""
+ [WARN] Cannot collect deps from pkg name {py_pkg_name}
+ Exception caught: {e}
+ ----------
+ """
+ )
+ )
+
+ # Search through the python package
+ # at the specific path
+ for path in pkg_installation_path_list:
+ self.collect_deps_from_pkg_path(
+ path,
+ dir_name_exclusion_list,
+ file_name_exclusion_list,
+ )
+
+ def collect_deps_from_pkg_path(
+ self,
+ py_pkg_path,
+ dir_name_exclusion_list=[],
+ file_name_exclusion_list=[]):
+
+ print(
+ cl(
+ f"""
+ [DEBUG] Received request
+ to collect deps
+ from pkg path:
+ {py_pkg_path}
+ with directories to exclude:
+ {dir_name_exclusion_list}
+ and files to exclude:
+ {file_name_exclusion_list}
+ ----------
+ """
+ )
+ )
+
+ try:
+ # Ensure that py pkg path is a string
+ if not isinstance(py_pkg_path, str):
+ raise Exception(
+ f"""
+ Python package name must be a string
+ This is your input: {py_pkg_path}
+ ----------
+ """
+ )
+
+ # Determine if path exists
+ py_pkg_path = realpath(py_pkg_path)
+ print(
+ cl(
+ f"""
+ [DEBUG] The pkg path exists
+ and is equivalent to:
+ {py_pkg_path}
+ ----------
+ """
+ )
+ )
+
+ # Initialise list of python files in path
+ py_file_list = []
+
+ # Determine whether path is a directory
+ if isdir(py_pkg_path):
+ # Create a list of directories
+ # and exclude directory names
+ # starting with `_` (since they are for internal use)
+ # as well as the dir names
+ # specified in the dir_name_exclusion_list
+ for dir_path, dir_names, file_names in walk(py_pkg_path, topdown=True):
+ # Modify directory names in place
+ # to make the exclusion effective
+ dir_names[:] = [
+ d
+ for d in dir_names
+ if (d not in set(dir_name_exclusion_list)
+ and not re.search("^_[^_].*$", d))
+ ]
+ for file_name in file_names:
+ file_path = join(dir_path, file_name)
+ # Confirm that file has python extension
+ # and does not start with a unique `_`
+ # since meant for internal use only
+ # Besides, exclude file names
+ # specified in the file_name_exclusion_list
+ if (not re.search("^_[^_].*$", file_path)
+ and re.search("(\.py)$", file_path)
+ and file_name not in file_name_exclusion_list):
+ py_file_list.append(file_path)
+
+ # Determine whether path is a file
+ if (isfile(py_pkg_path)
+ and basename(py_pkg_path)
+ not in file_name_exclusion_list):
+ py_file_list.append(py_pkg_path)
+
+ # Collect imports from each py file
+ for file_path in py_file_list:
+ py_file_imports = []
+ try:
+ py_file_imports = list_imports.get(file_path)
+ except Exception as e:
+ print(
+ cl(
+ f"""
+ [WARN] Cannot collect deps for py file {file_path}
+ Exception caught: {e}
+ ----------
+ """
+ )
+ )
+ self.deps_collected_list.extend(py_file_imports)
+
+ except Exception as e:
+ print(
+ cl(
+ f"""
+ [WARN] Cannot collect deps from pkg path {py_pkg_path}
+ Exception caught: {e}
+ ----------
+ """
+ )
+ )
+
+ # Make deps collected list unique and sorted for clarity
+ self.deps_collected_list = list(
+ set(
+ self.deps_collected_list
+ )
+ )
+ self.deps_collected_list.sort()
+
+ def get_deps_list(self):
+ print(
+ cl(
+ f"""
+ [DEBUG] Received request
+ to share deps list
+ ----------
+ """
+ )
+ )
+ return self.deps_collected_list
+
+ # Method returning the top-level imports (before the `.`)
+ # and sorting the imports by module origin if `pdt_format`
+ # is set to `True`
+ def get_top_level_deps_list(self, pdt_format=False):
+ print(
+ cl(
+ f"""
+ [DEBUG] Received request
+ to share top-level deps list
+ with pdt format option {pdt_format}
+ ----------
+ """
+ )
+ )
+
+ # Split deps collected
+ # and retrieve only top level module
+ top_level_deps_collected_list = [
+ dep_name.split(".")[0]
+ for dep_name
+ in self.deps_collected_list
+ ]
+
+ # Sort the imports by module origin
+ # if pdt format is desired
+ if pdt_format:
+ # Identify python version
+ # to list standard libraries
+ # associated to the version
+ major, minor, micro = sys.version_info[:3]
+ py_version = f"{major}.{minor}"
+ py_std_libs_list = stdlib_list(py_version)
+
+ top_level_deps_collected_list = [
+ "Python:" + lib
+ if lib in py_std_libs_list
+ else lib
+ for lib
+ in top_level_deps_collected_list
+ ]
+
+ # Make the top level deps list unique and sorted
+ top_level_deps_collected_list = list(
+ set(
+ top_level_deps_collected_list
+ )
+ )
+ top_level_deps_collected_list.sort()
+
+ return top_level_deps_collected_list
+
+ def get_pip_required_deps_list(self):
+ print(
+ cl(
+ f"""
+ [DEBUG] Received request
+ to share pip required deps list
+ ----------
+ """
+ )
+ )
+ return self.pip_required_deps_list
+
+ def get_site_pkg_names_list(self):
+ print(
+ cl(
+ f"""
+ [DEBUG] Received request
+ to share site pkg names list
+ ----------
+ """
+ )
+ )
+ return self.site_pkg_names_list
+
+ def reset_deps_list(self):
+ print(
+ cl(
+ f"""
+ [DEBUG] Received request
+ to reset deps list
+ ----------
+ """
+ )
+ )
+ self.deps_collected_list = []
+
+ def reset_pip_required_deps_list(self):
+ print(
+ cl(
+ f"""
+ [DEBUG] Received request
+ to reset pip required deps list
+ ----------
+ """
+ )
+ )
+ self.deps_collected_list = []
+
+# Method defining custom argument type
+# for a list of strings
+def list_of_strings(arg):
+ return arg.split(',')
+
+def parse_args():
+ # Instantiate argument parser
+ parser = argparse.ArgumentParser()
+
+ # Add arguments
+ parser.add_argument(
+ "-n",
+ "--pkg-name",
+ dest="pkg_name",
+ type=str,
+ action="store",
+ help="Search through an installed pip python package for its dependencies",
+ )
+ parser.add_argument(
+ "-p",
+ "--pkg-path",
+ dest="pkg_path",
+ type=str,
+ action="store",
+ help="Search through a python package for its dependencies from its root path",
+ )
+ parser.add_argument(
+ "-d",
+ "--exclude-dirs",
+ dest="exclude_dir_list",
+ type=list_of_strings,
+ action="store",
+ help="List directory names (comma-separated) to exclude from the search",
+ )
+ parser.add_argument(
+ "-f",
+ "--exclude-files",
+ dest="exclude_file_list",
+ type=list_of_strings,
+ action="store",
+ help="List file names (comma-separated) to exclude from the search",
+ )
+
+ # Collect arguments
+ args = parser.parse_args()
+
+ # Ensure one argument only is used
+ if not args.pkg_name and not args.pkg_path:
+ parser.error("Please specify at least one argument between -n and -p")
+ elif args.pkg_name and args.pkg_path:
+ parser.error("Please only specify one argument -n or -p")
+ else:
+ pass
+
+ return args
+
+def main():
+ # Parse arguments
+ inputs=parse_args()
+
+ # Convert None inputs where required
+ if inputs.exclude_dir_list is None:
+ inputs.exclude_dir_list = []
+ if inputs.exclude_file_list is None:
+ inputs.exclude_file_list = []
+
+ # Instantiate python dependency collector
+ py_dep_collector = PyDepCollector()
+
+ # Start dependency collection
+ if inputs.pkg_name:
+ py_dep_collector.collect_deps_from_pkg_name(
+ inputs.pkg_name,
+ inputs.exclude_dir_list,
+ inputs.exclude_file_list,
+ )
+ elif inputs.pkg_path:
+ py_dep_collector.collect_deps_from_pkg_path(
+ inputs.pkg_path,
+ inputs.exclude_dir_list,
+ inputs.exclude_file_list,
+ )
+ else:
+ pass
+
+ # Get dependency list
+ # deps_list = py_dep_collector.get_deps_list()
+ # deps_list = py_dep_collector.get_top_level_deps_list()
+ deps_list = py_dep_collector.get_top_level_deps_list(pdt_format=True)
+
+ # Get site packages name list
+ site_pkg_names_list = py_dep_collector.get_site_pkg_names_list()
+
+ # Get pip required dependency list
+ pip_required_deps_list = py_dep_collector.get_pip_required_deps_list()
+
+ # Print output in a clear way
+ # for the user
+ if inputs.pkg_name:
+ print(
+ cl(
+ f"""
+ ====================
+ [INFO] Site packages names obtained:
+ {site_pkg_names_list}
+ ----------
+ """
+ )
+ )
+
+ print(
+ cl(
+ f"""
+ [INFO] Pip required dependencies obtained:
+ {pip_required_deps_list}
+ ----------
+ """
+ )
+ )
+ else:
+ print(cl(f"===================="))
+
+ print(
+ cl(
+ f"""
+ [INFO] Dependencies obtained:
+ {deps_list}
+ ====================
+ """
+ )
+ )
+
+
+if __name__ == "__main__":
+ main()
diff --git a/utils/resources/download_sources.sh b/utils/resources/download_sources.sh
deleted file mode 100755
index 885cd72..0000000
--- a/utils/resources/download_sources.sh
+++ /dev/null
@@ -1,3 +0,0 @@
-#! /usr/bin/env bash
-wget https://www.python.org/ftp/python/3.10.12/Python-3.10.12.tar.xz
-wget https://download.qt.io/archive/qt/5.15/5.15.2/single/qt-everywhere-src-5.15.2.tar.xz
diff --git a/utils/resources/path_setup.sh b/utils/resources/path_setup.sh
deleted file mode 100644
index 4a8afc8..0000000
--- a/utils/resources/path_setup.sh
+++ /dev/null
@@ -1,11 +0,0 @@
-#! /usr/bin/env bash
-export PYQT_CROM_DIR=$HOME/Documents/pyqt-crom
-export RESOURCES_DIR=$PYQT_CROM_DIR/utils/resources
-export QT_DIR=$HOME/Qt5.15.2/5.15.2
-export ANDROID_SDK_ROOT=$HOME/Android/Sdk
-export ANDROID_NDK_ROOT=$ANDROID_SDK_ROOT/ndk/21.4.7075529
-export ANDROID_NDK_PLATFORM=android-28
-# export ANDROID_NDK_HOST=linux-x86_64
-export JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64
-export JAVA_JRE=/usr/lib/jvm/java-11-openjdk-amd64/jre
-export PATH=$PATH:$JAVA_HOME/bin